feat: add new ergo sdks to monorepo (#142)

* feat: add new ergo sdks to monorepo

* fix: resolve some pyright linter errors

* fix: python sdk (mostly) typesafe now

* feat: add tests for python sdk, mostly good pyright

* fix: update sdks/core to 1.1.0

* uv sync

* fix: body->query

* chore: pyright wrangling and coderabbit nits

* fix: ts lib search working

* chore: coderabbit nits
This commit is contained in:
doria 2025-06-26 17:07:23 -04:00 committed by GitHub
parent 4d5aa4cdb2
commit cb3b6104bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 14993 additions and 29 deletions

View File

@ -29,6 +29,7 @@ dependencies = [
]
[tool.uv]
dev-dependencies = [
"honcho-core>=1.1.0",
"pytest>=8.2.2",
"sqlalchemy-utils>=0.41.2",
"pytest-asyncio>=0.23.7",
@ -70,7 +71,7 @@ asyncio_default_fixture_loop_scope = "session"
# than mypy and with a good extension for VSCode/Cursor.
# https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright
# https://docs.basedpyright.com/latest/configuration/config-files/#sample-pyprojecttoml-file
include = ["src", "tests"]
include = ["src", "tests", "sdks/python/src"]
exclude = ["tests/**/disabled*.py"]
# By default BasedPyright is very strict, so you almost certainly want to disable
# some of the rules.
@ -96,4 +97,5 @@ reportImplicitOverride = false
# reportUnknownVariableType = false
# reportUnknownArgumentType = false
# reportUnknownParameterType = false
# reportUnknownMemberType = false
# reportUnknownMemberType = false
reportImportCycles = false

13
sdks/python/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
.vscode
.DS_Store
__pycache__
.mypy_cache
.ruff_cache
*.egg-info
dist
.venv
.env

167
sdks/python/README.md Normal file
View File

@ -0,0 +1,167 @@
# Honcho Python SDK
The official Python library for the [Honcho](https://github.com/plastic-labs/honcho) conversational memory platform. Honcho provides tools for managing peers, sessions, and conversation context across multi-party interactions, enabling advanced conversational AI applications with persistent memory and theory-of-mind capabilities.
## Installation
```bash
pip install honcho-ai
```
## Quick Start
```python
from honcho import Honcho
# Initialize client
client = Honcho(api_key="your-api-key")
# Create peers (participants in conversations)
alice = client.peer("alice")
bob = client.peer("bob")
# Create a session for group conversations
session = client.session("conversation-1")
# Add messages to the session
session.add_messages([
alice.message("Hello, Bob!"),
bob.message("Hi Alice, how are you?")
])
# Query conversation context
response = alice.chat("What did Bob say to me?")
print(response)
```
## Core Concepts
### Peers
Peers represent participants in conversations.
```python
# Create peers
assistant = client.peer("assistant")
user = client.peer("user-123")
# Chat with global context
response = user.chat("What did I talk about yesterday?")
# Chat with perspective of another peer
response = user.chat("Does the assistant know my preferences?", target=assistant)
```
### Sessions
Sessions group related conversations and messages:
```python
# Create a session
session = client.session("project-discussion")
# Add peers to session
session.add_peers([alice, bob])
# Add messages
session.add_messages([
alice.message("Let's discuss the project timeline"),
bob.message("I think we need two more weeks")
])
# Get conversation context
context = session.get_context()
```
### Messages and Context
Retrieve and use conversation history:
```python
# Get messages from a session
messages = session.get_messages()
# Convert to OpenAI format for further prompting
openai_messages = context.to_openai(assistant="assistant")
# Convert to Anthropic format for further prompting
anthropic_messages = context.to_anthropic(assistant="assistant")
```
### Async Support
```python
from honcho import AsyncHoncho
async def main():
client = AsyncHoncho(api_key="your-api-key")
peer = client.peer("user")
response = await peer.chat("Hello!")
print(response)
```
### Metadata Management
```python
# Set peer metadata
user.set_metadata({"location": "San Francisco", "preferences": {"theme": "dark"}})
# Query using metadata context
response = user.chat("What's the weather like where I am?")
# Session metadata
session.set_metadata({"topic": "project-planning", "priority": "high"})
```
### Multi-Perspective Queries
```python
# Alice's view of what Bob knows
response = alice.chat("Does Bob remember our discussion about the budget?", target=bob)
# Session-specific perspective
response = alice.chat("What does Bob think about this project?",
target=bob,
session_id=session.id)
```
## Configuration
### Environment Variables
```bash
export HONCHO_API_KEY="your-api-key"
export HONCHO_BASE_URL="https://api.honcho.dev" # Optional
export HONCHO_WORKSPACE_ID="your-workspace" # Optional
```
### Client Options
```python
client = Honcho(
api_key="your-api-key",
environment="production", # or "local", "demo"
workspace_id="custom-workspace",
base_url="https://api.honcho.dev"
)
```
## Examples
Check out the `examples/` directory for complete usage examples:
- `example.py` - Comprehensive feature demonstration
- `chat.py` - Basic multi-peer chat
- `async_example.py` - Async/await usage
- `search.py` - Context search and retrieval
## License
Apache 2.0 - see [LICENSE](../../LICENSE) for details.
## Support
- [Documentation](https://docs.honcho.dev)
- [GitHub Issues](https://github.com/plastic-labs/honcho-sdks/issues)
- [Discord Community](https://discord.gg/honcho)

View File

@ -0,0 +1,129 @@
import asyncio
import logging
from honcho import AsyncHoncho
from honcho.async_client.session import SessionPeerConfig
logging.basicConfig(level=logging.INFO)
async def main():
# HONCHO_API_KEY is an environment variable
# HONCHO_URL is an *optional* environment variable
# HONCHO_WORKSPACE_ID is an *optional* environment variable
# Using local server for this example
honcho = AsyncHoncho(environment="local", workspace_id="test")
_workspaces = await honcho.get_workspaces()
# these don't make any API calls, just produce a AsyncPeer object in SDK
# in practice, these would be UUIDs, as peer IDs are unique within their workspace
assistant = await honcho.peer(id="bob")
alice = await honcho.peer(id="alice")
# empty since peers are not created until they are used
_peers = await honcho.get_peers()
# workspace-level metadata
_m = await honcho.get_metadata()
await honcho.set_metadata({"test": "test"})
# calling the dialectic chat endpoint makes an API call.
# when this call occurs, the "alice" peer will be get_or_create'd
# response will be None because we haven't talked yet!
_response = await alice.chat("what did alice have for breakfast today?")
# sessions are scoped to a set of peers and contain messages/content
# this is not an API call, like peers this is created lazily
my_session = await honcho.session(id="session_1")
# API call
await my_session.add_peers(
[alice, (assistant, SessionPeerConfig(observe_others=False, observe_me=False))]
)
# adding/removing peers from sessions creates a bidirectional relationship,
# so no need for operations like `alice.join(my_session)`.
# this will return a list of sessions [my_session]
# this is also an API call
_sessions = await alice.get_sessions()
# API call to create 1 or more messages (overload, can be Message or list[Message]
await my_session.add_messages(
[
# creates a Message object with peer_id="alice", etc etc
assistant.message("what did you have for breakfast today, alice?"),
alice.message("i had oatmeal."),
]
)
m = await my_session.get_metadata()
m["test"] = "test2"
await my_session.set_metadata(m)
# peers have one "omnipresent" global representation, comprised of all
# the content associated with that peer in this honcho instance.
# they also have a potentially infinite number of "local" representations,
# each one from the perspective of *another* peer in the honcho instance.
# this is a query to alice's global representation--no scope
_response = await alice.chat("what did the user have for breakfast today?")
# this is a query to alice's local representation *of the assistant*
_response = await alice.chat(
"does alice know what bob had for breakfast?", target=assistant
)
# this is a query to the assistant's local representation *of alice* in this session
_response = await assistant.chat(
"does the assistant know what alice had for breakfast?",
target=alice,
session_id=my_session.id,
)
# API call to store non-message content under a peer + optional session
await alice.add_messages(
"this might be a document about alice, say, a journal entry."
)
# This does make an API call because we set a configuration for this new peer
charlie = await honcho.peer(id="charlie", config={"observe_me": False})
await my_session.add_messages(charlie.message("hello world!"))
# session now has 3 members: alice, bob, and charlie. a message automatically adds a peer to a session.
# peers, sessions, and messages all have metadata which can be modified and used in queries.
# API call to get metadata?
charlie_metadata = await charlie.get_metadata()
charlie_metadata["location"] = "the moon"
# API call to store metadata?
await charlie.set_metadata(charlie_metadata)
# response will tell you that charlie is on the moon
_response = await charlie.chat("where is the user?")
# you can get the messages from a session, either fully or partially.
# (API call)
_messages = await my_session.get_messages()
context = await my_session.get_context()
_messages = context.to_openai(assistant=assistant.id)
_messages = context.to_anthropic(assistant=assistant.id)
await my_session.add_messages(
assistant.message("This is a test message using the property syntax")
)
print("Async sample code executed successfully!")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,41 @@
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("chat_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)
# Chat with alice
alice = peers[0]
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)
print("response returned:", response)
# Chat with alice in the session with a target
# This means you are querying alice's theory-of-mind representation of bob
bob = peers[1]
response = alice.chat("what did bob have for breakfast today?", target=bob)
print("response returned:", response)

View File

@ -0,0 +1,118 @@
import logging
from honcho import Honcho
from honcho.session import SessionPeerConfig
logging.basicConfig(level=logging.INFO)
# HONCHO_API_KEY is an environment variable
# HONCHO_URL is an *optional* environment variable
# HONCHO_WORKSPACE_ID is an *optional* environment variable
# Using local server for this example
honcho = Honcho(environment="local", workspace_id="test")
workspaces = honcho.get_workspaces()
# these don't make any API calls, just produce a Peer object in SDK
# in practice, these would be UUIDs, as peer IDs are unique within their workspace
assistant = honcho.peer(id="bob")
alice = honcho.peer(id="alice")
# empty since peers are not created until they are used
peers = honcho.get_peers()
# workspace-level metadata
_m = honcho.get_metadata()
honcho.set_metadata({"test": "test"})
# calling the dialectic chat endpoint makes an API call.
# when this call occurs, the "alice" peer will be get_or_create'd
# response will be None because we haven't talked yet!
response = alice.chat("what did alice have for breakfast today?")
# sessions are scoped to a set of peers and contain messages/content
# this is not an API call, like peers this is created lazily
my_session = honcho.session(id="session_1")
# API call
my_session.add_peers(
[alice, (assistant, SessionPeerConfig(observe_others=False, observe_me=False))]
)
# adding/removing peers from sessions creates a bidirectional relationship,
# so no need for operations like `alice.join(my_session)`.
# this will return a list of sessions [my_session]
# this is also an API call
_sessions = alice.get_sessions()
# API call to create 1 or more messages (overload, can be Message or list[Message]
my_session.add_messages(
[
# creates a Message object with peer_id="alice", etc etc
assistant.message("what did you have for breakfast today, alice?"),
alice.message("i had oatmeal."),
]
)
m = my_session.get_metadata()
m["test"] = "test2"
my_session.set_metadata(m)
# peers have one "omnipresent" global representation, comprised of all
# the content associated with that peer in this honcho instance.
# they also have a potentially infinite number of "local" representations,
# each one from the perspective of *another* peer in the honcho instance.
# this is a query to alice's global representation--no scope
response = alice.chat("what did the user have for breakfast today?")
# this is a query to alice's local representation *of the assistant*
response = alice.chat("does alice know what bob had for breakfast?", target=assistant)
# this is a query to the assistant's local representation *of alice* in this session
response = assistant.chat(
"does the assistant know what alice had for breakfast?",
target=alice,
session_id=my_session.id,
)
# API call to store non-message content under a peer + optional session
alice.add_messages("this might be a document about alice, say, a journal entry.")
charlie = honcho.peer(id="charlie")
my_session.add_messages(charlie.message("hello world!"))
# session now has 3 members: alice, bob, and charlie. a message automatically adds a peer to a session.
# peers, sessions, and messages all have metadata which can be modified and used in queries.
# API call to get metadata?
charlie_metadata = charlie.get_metadata()
charlie_metadata["location"] = "the moon"
# API call to store metadata?
charlie.set_metadata(charlie_metadata)
# response will tell you that charlie is on the moon
response = charlie.chat("where is the user?")
# you can get the messages from a session, either fully or partially.
# (API call)
messages = my_session.get_messages()
context = my_session.get_context()
messages = context.to_openai(assistant=assistant.id)
messages = context.to_anthropic(assistant=assistant.id)
my_session.add_messages(
assistant.message("This is a test message using the property syntax")
)
print("Sample code executed successfully!")

View File

@ -0,0 +1,31 @@
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)
# Get some context of the session
# Set the token limit super low so we only get a few of the tiny messages created
context = session.get_context(summary=True, tokens=50)
print("context returned:", context)

View File

@ -0,0 +1,37 @@
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)

View File

@ -0,0 +1,127 @@
"""
Example demonstrating Pydantic validation in the Honcho SDK.
This example shows how the SDK now uses Pydantic to validate inputs at runtime,
providing better error messages and type safety.
"""
import logging
from honcho import Honcho
from pydantic import ValidationError
logging.basicConfig(level=logging.INFO)
def demonstrate_validation():
"""Demonstrate various validation scenarios with the Pydantic-enhanced SDK."""
print("=== Pydantic Validation Examples ===\n")
# Example 1: Valid initialization
print("1. Valid initialization:")
try:
honcho = Honcho(
environment="local",
workspace_id="test_workspace",
timeout=30.0,
max_retries=3,
)
print(f"✅ Successfully created client: {honcho}")
except ValidationError as e:
print(f"❌ Validation error: {e}")
print("\n" + "=" * 50 + "\n")
# Example 2: Invalid timeout (negative)
print("2. Invalid timeout (negative value):")
try:
honcho = Honcho(environment="local", timeout=-5.0)
print("✅ This shouldn't happen!")
except ValidationError as e:
print(f"❌ Validation caught invalid timeout: {e.errors()[0]['msg']}")
print("\n" + "=" * 50 + "\n")
# Example 3: Invalid max_retries (negative)
print("3. Invalid max_retries (negative value):")
try:
honcho = Honcho(environment="local", max_retries=-1)
print("✅ This shouldn't happen!")
except ValidationError as e:
print(f"❌ Validation caught invalid max_retries: {e.errors()[0]['msg']}")
print("\n" + "=" * 50 + "\n")
# Example 4: Invalid peer ID (empty)
print("4. Invalid peer ID (empty string):")
try:
honcho = Honcho(environment="local", workspace_id="test")
peer = honcho.peer("")
print("✅ This shouldn't happen!")
except ValidationError as e:
print(f"❌ Validation caught empty peer ID: {e.errors()[0]['msg']}")
print("\n" + "=" * 50 + "\n")
# Example 5: Invalid session ID (empty)
print("5. Invalid session ID (empty string):")
try:
honcho = Honcho(environment="local", workspace_id="test")
session = honcho.session("")
print("✅ This shouldn't happen!")
except ValidationError as e:
print(f"❌ Validation caught empty session ID: {e.errors()[0]['msg']}")
print("\n" + "=" * 50 + "\n")
# Example 6: Valid peer and session creation
print("6. Valid peer and session creation:")
try:
honcho = Honcho(environment="local", workspace_id="test")
peer = honcho.peer("alice")
session = honcho.session("conversation_1")
print(f"✅ Created peer: {peer}")
print(f"✅ Created session: {session}")
except ValidationError as e:
print(f"❌ Validation error: {e}")
print("\n" + "=" * 50 + "\n")
# Example 7: Invalid message content (empty)
print("7. Invalid message content (empty string):")
try:
honcho = Honcho(environment="local", workspace_id="test")
peer = honcho.peer("alice")
message = peer.message("")
print("✅ This shouldn't happen!")
except ValidationError as e:
print(f"❌ Validation caught empty message content: {e.errors()[0]['msg']}")
print("\n" + "=" * 50 + "\n")
# Example 9: Valid operations
print("9. Valid operations (no API calls made):")
try:
honcho = Honcho(environment="local", workspace_id="test")
peer = honcho.peer("alice")
session = honcho.session("conversation_1")
# Create a valid message
message = peer.message("Hello, world!", metadata={"type": "greeting"})
print(
f"✅ Created message: peer_id={message['peer_id']}, content='{message['content']}'"
)
# Valid peer operations (validation passes, but no API calls made)
print("✅ All validations passed for standard operations")
except ValidationError as e:
print(f"❌ Validation error: {e}")
print("\n" + "=" * 50 + "\n")
print("🎉 Pydantic validation examples completed!")
if __name__ == "__main__":
demonstrate_validation()

View File

@ -0,0 +1,51 @@
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("search_test_" + str(uuid.uuid4()))
# Create a message with our special keyword
keyword = f"~special-{str(uuid.uuid4())}~"
session.add_messages(peers[0].message(f"I am a {keyword} message"))
# 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)
# Search the session for the special keyword
search_results = session.search(keyword)
print("searching the session")
print("search results returned:", [message for message in search_results])
# Search the workspace for the special keyword
search_results = honcho.search(keyword)
print("searching the workspace")
print("search results returned:", [message for message in search_results])
alice = peers[0]
# Add a different message to alice's global representation
different_keyword = f"~different-{str(uuid.uuid4())}~"
alice.add_messages(alice.message(f"I am a {different_keyword} message"))
# Search alice's global representation for the different message
search_results = alice.search(different_keyword)
print("searching alice's global representation")
print("search results returned:", [message for message in search_results])

View File

@ -0,0 +1,56 @@
[project]
name = "honcho-ai"
version = "1.1.0"
description = "Official DX Optimized Python SDK for Honcho"
dynamic = ["readme"]
license = "Apache-2.0"
authors = [
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
]
dependencies = [
"honcho-core>=1.1.0",
"httpx>=0.28.0, <1",
"pydantic>=2.0.0, <3",
]
requires-python = ">= 3.8"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Topic :: Software Development :: Libraries :: Python Modules",
]
[project.urls]
Homepage = "https://github.com/plastic-labs/honcho-sdks"
Repository = "https://github.com/plastic-labs/honcho-sdks"
[dependency-groups]
dev = [
"ruff>=0.11.13",
]
[tool.setuptools.dynamic]
readme = {file = ["README.md"], content-type = "text/markdown"}
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
[tool.setuptools.packages.find]
where = ["src"]
include = ["honcho", "honcho.async_client"]

View File

@ -0,0 +1,60 @@
"""
Honcho Python SDK
A Python client library for the Honcho conversational memory platform.
Provides tools for managing peers, sessions, and conversation context
across multi-party interactions.
Usage:
from honcho import Honcho
# Initialize client
client = Honcho(api_key="your-api-key")
# Create peers
alice = client.peer("alice")
bob = client.peer("bob")
# Create a session
session = client.session("conversation-1")
# Add peers to session
session.add_peers([alice, bob])
# Add messages
session.add_messages([
alice.message("Hello, Bob!"),
bob.message("Hi Alice, how are you?")
])
# Query conversation context
response = alice.chat("What did Bob say to me?")
"""
from .async_client import (
AsyncHoncho,
AsyncPage,
AsyncPeer,
AsyncSession,
)
from .client import Honcho
from .pagination import SyncPage
from .peer import Peer
from .session import Session
from .session_context import SessionContext
__version__ = "1.0.0"
__author__ = "Plastic Labs"
__email__ = "hello@plasticlabs.ai"
__all__ = [
"AsyncHoncho",
"AsyncPeer",
"AsyncSession",
"AsyncPage",
"Honcho",
"Peer",
"Session",
"SessionContext",
"SyncPage",
]

View File

@ -0,0 +1,18 @@
"""
Async client module for the Honcho Python SDK.
Provides async versions of all client classes for asynchronous operations
with the Honcho conversational memory platform.
"""
from .client import AsyncHoncho
from .pagination import AsyncPage
from .peer import AsyncPeer
from .session import AsyncSession
__all__ = [
"AsyncHoncho",
"AsyncPeer",
"AsyncSession",
"AsyncPage",
]

View File

@ -0,0 +1,330 @@
import logging
import os
from collections.abc import Mapping
from typing import Any, Literal
import httpx
from honcho_core import AsyncHoncho as AsyncHonchoCore
from honcho_core import Honcho as HonchoCore
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
from .pagination import AsyncPage
from .peer import AsyncPeer
from .session import AsyncSession
logger = logging.getLogger(__name__)
class AsyncHoncho(BaseModel):
"""
Main async client for the Honcho SDK.
Provides async access to peers, sessions, and workspace operations with configuration
from environment variables or explicit parameters. This is the primary entry
point for interacting with the Honcho conversational memory platform asynchronously.
Attributes:
api_key: API key for authentication
base_url: Base URL for the Honcho API
workspace_id: Workspace ID for scoping operations
"""
workspace_id: str = Field(
...,
min_length=1,
description="Workspace ID for scoping operations",
)
_client: AsyncHonchoCore = PrivateAttr()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
api_key: str | None = None,
environment: Literal["local", "production", "demo"] | None = None,
base_url: str | None = Field(None, description="Base URL for the Honcho API"),
workspace_id: str | None = Field(
None, min_length=1, description="Workspace ID for scoping operations"
),
timeout: float | None = Field(None, gt=0, description="Timeout in seconds"),
max_retries: int | None = Field(
None, ge=0, description="Maximum number of retries"
),
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
async_http_client: httpx.AsyncClient | None = Field(
None, description="Custom HTTP client"
),
http_client: httpx.Client | None = Field(
None, description="Custom HTTP client"
),
) -> None:
"""
Initialize the AsyncHoncho client.
Args:
api_key:
API key for authentication. If not provided, will attempt to
read from HONCHO_API_KEY environment variable
environment:
Environment to use (local or production)
base_url:
Base URL for the Honcho API. If not provided, will attempt to
read from HONCHO_URL environment variable or default to the
production API URL
workspace_id:
Workspace ID to use for operations. If not provided, will
attempt to read from HONCHO_WORKSPACE_ID environment variable
or default to "default"
timeout:
Optional custom timeout for the HTTP client.
max_retries:
Optional custom maximum number of retries for the HTTP client.
default_headers:
Optional custom default headers for the HTTP client.
default_query:
Optional custom default query parameters for the HTTP client.
http_client:
Optional custom httpx client.
"""
# Resolve workspace_id before calling super().__init__
resolved_workspace_id = workspace_id or os.getenv(
"HONCHO_WORKSPACE_ID", "default"
)
super().__init__(workspace_id=resolved_workspace_id)
# Build client kwargs, excluding None values that AsyncHonchoCore doesn't handle well
client_kwargs: dict[str, Any] = {}
if api_key is not None:
client_kwargs["api_key"] = api_key
if environment is not None:
client_kwargs["environment"] = environment
if base_url is not None:
client_kwargs["base_url"] = base_url
if timeout is not None:
client_kwargs["timeout"] = timeout
if max_retries is not None:
client_kwargs["max_retries"] = max_retries
if default_headers is not None:
client_kwargs["default_headers"] = default_headers
if default_query is not None:
client_kwargs["default_query"] = default_query
sync_client_kwargs = client_kwargs.copy()
async_client_kwargs = client_kwargs.copy()
if http_client is not None:
sync_client_kwargs["http_client"] = http_client
if async_http_client is not None:
async_client_kwargs["http_client"] = async_http_client
self._client = AsyncHonchoCore(**async_client_kwargs)
# Get or create the workspace using synchronous client
sync_client = HonchoCore(**sync_client_kwargs)
sync_client.workspaces.get_or_create(id=self.workspace_id)
@validate_call
async def peer(
self,
id: str = Field(
..., min_length=1, description="Unique identifier for the peer"
),
*,
config: dict[str, object] | None = Field(
None,
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
),
) -> AsyncPeer:
"""
Get or create a peer with the given ID.
Creates an AsyncPeer object that can be used to interact with the specified peer.
This method does not make an API call - the peer is created lazily when
its methods are first used.
Args:
id: Unique identifier for the peer within the workspace. Should be a
stable identifier that can be used consistently across sessions
config:
Optional configuration to set for this peer. If set, will get/create peer immediately with flags.
Returns:
An AsyncPeer object that can be used to send messages, join sessions, and
query the peer's knowledge representations
Raises:
ValidationError: If the peer ID is empty or invalid
"""
if config:
return await AsyncPeer.create(
id, self.workspace_id, self._client, config=config
)
return AsyncPeer(id, self.workspace_id, self._client)
async def get_peers(self) -> AsyncPage[AsyncPeer]:
"""
Get all peers in the current workspace.
Makes an async API call to retrieve all peers that have been created or used
within the current workspace. Returns a paginated result that transforms
inner client Peer objects to SDK AsyncPeer objects as they are consumed.
Returns:
An AsyncPage of AsyncPeer objects representing all peers in the workspace.
The page preserves pagination functionality while transforming objects
"""
peers_page = await self._client.workspaces.peers.list(
workspace_id=self.workspace_id
)
return AsyncPage(
peers_page, lambda peer: AsyncPeer(peer.id, self.workspace_id, self._client)
)
@validate_call
async def session(
self,
id: str = Field(
..., min_length=1, description="Unique identifier for the session"
),
*,
config: dict[str, object] | None = Field(
None,
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
),
) -> AsyncSession:
"""
Get or create a session with the given ID.
Creates an AsyncSession object that can be used to manage conversations between
multiple peers. This method does not make an API call - the session is
created lazily when its methods are first used.
Args:
id: Unique identifier for the session within the workspace. Should be a
stable identifier that can be used consistently to reference the
same conversation
config:
Optional configuration to set for this session. If set, will get/create session immediately with flags.
Returns:
An AsyncSession object that can be used to add peers, send messages, and
manage conversation context
Raises:
ValidationError: If the session ID is empty or invalid
"""
if config:
return await AsyncSession.create(
id, self.workspace_id, self._client, config=config
)
return AsyncSession(id, self.workspace_id, self._client)
async def get_sessions(self) -> AsyncPage[AsyncSession]:
"""
Get all sessions in the current workspace.
Makes an async API call to retrieve all sessions that have been created within
the current workspace.
Returns:
An AsyncPage of AsyncSession objects representing all sessions in the workspace.
Returns an empty page if no sessions exist
"""
sessions_page = await self._client.workspaces.sessions.list(
workspace_id=self.workspace_id
)
return AsyncPage(
sessions_page,
lambda session: AsyncSession(session.id, self.workspace_id, self._client),
)
async def get_metadata(self) -> dict[str, object]:
"""
Get metadata for the current workspace.
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.
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 {}
@validate_call
async def set_metadata(
self,
metadata: dict[str, object] = Field(..., description="Metadata dictionary"),
) -> None:
"""
Set metadata for the current workspace.
Makes an async API call to update the metadata associated with the current workspace.
This will overwrite any existing metadata with the provided values.
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)
async def get_workspaces(self) -> list[str]:
"""
Get all workspace IDs from the Honcho instance.
Makes an async API call to retrieve all workspace IDs that the authenticated
user has access to.
Returns:
A list of workspace ID strings. Returns an empty list if no workspaces
are accessible or none exist
"""
workspaces_page = await self._client.workspaces.list()
workspace_ids: list[str] = []
async for workspace in workspaces_page:
workspace_ids.append(workspace.id)
return workspace_ids
@validate_call
async def search(
self,
query: str = Field(..., min_length=1, description="The search query to use"),
) -> AsyncPage[Message]:
"""
Search for messages in the current workspace.
Makes an async API call to search for messages in the current workspace.
Args:
query: The search query to use
Returns:
An AsyncPage of Message objects representing the search results.
Returns an empty page if no messages are found.
"""
messages_page = await self._client.workspaces.search(
self.workspace_id, body=query
)
return AsyncPage(messages_page)
def __repr__(self) -> str:
"""
Return a string representation of the AsyncHoncho client.
Returns:
A string representation suitable for debugging
"""
return f"AsyncHoncho(workspace_id='{self.workspace_id}', base_url='{self._client.base_url}')"
def __str__(self) -> str:
"""
Return a human-readable string representation of the AsyncHoncho client.
Returns:
A string showing the workspace ID
"""
return f"AsyncHoncho Client (workspace: {self.workspace_id})"

View File

@ -0,0 +1,91 @@
from collections.abc import AsyncIterator, Callable
from typing import TypeVar
from honcho_core.pagination import AsyncPage as AsyncPageCore
from pydantic import Field, validate_call
T = TypeVar("T")
U = TypeVar("U")
class AsyncPage(AsyncPageCore[U]):
"""
Async paginated result wrapper that transforms objects from type T to type U.
Provides async iteration and transformation capabilities while preserving
pagination functionality from the underlying core AsyncPage.
"""
@validate_call
def __init__(
self,
original_page: AsyncPageCore[T] = Field(
..., description="The original AsyncPage to wrap"
),
transform_func: Callable[[T], U] | None = Field(
None,
description="Optional function to transform objects from type T to type U",
),
) -> None:
"""
Initialize the transformed async page.
Args:
original_page: The original AsyncPage to wrap
transform_func: Optional function to transform objects from type T to type U.
If None, objects are passed through unchanged.
"""
super().__init__(items=original_page.items) # pyright: ignore
self._original_page = original_page # pyright: ignore
self._transform_func = transform_func # pyright: ignore
@property
def items(self) -> list[U]: # pyright: ignore
"""Get all optionally transformed items as a list."""
if self._transform_func is not None:
return [self._transform_func(item) for item in self._original_page.items]
return self._original_page.items # pyright: ignore
async def __aiter__(self) -> AsyncIterator[U]:
"""Async iterate over optionally transformed objects."""
async for item in self._original_page:
if self._transform_func is not None:
yield self._transform_func(item)
else:
yield item # type: ignore # pyright: ignore
async def __agetitem__(self, index: int) -> U:
"""Get an optionally transformed object by index."""
item = await self._original_page.__agetitem__(index) # type: ignore # pyright: ignore
if self._transform_func is not None:
return self._transform_func(item) # pyright: ignore
return item # type: ignore # pyright: ignore
def __len__(self) -> int:
"""Get the length of the page."""
return len(self._original_page) # type: ignore # pyright: ignore
@property
async def data(self) -> list[U]:
"""Get all optionally transformed data as a list."""
data = await self._original_page.data # type: ignore # pyright: ignore
if self._transform_func is not None:
return [self._transform_func(item) for item in data] # pyright: ignore
return data # type: ignore # pyright: ignore
@property
def object(self) -> str:
"""Get the object type."""
return self._original_page.object # type: ignore # pyright: ignore
@property
def has_next_page(self) -> bool: # pyright: ignore
"""Check if there's a next page."""
return self._original_page.has_next_page # type: ignore # pyright: ignore
async def next_page(self) -> "AsyncPage[U] | None":
"""Get the next page with optional transformation applied."""
next_page = await self._original_page.next_page() # type: ignore # pyright: ignore
if next_page is None:
return None
return AsyncPage(next_page, self._transform_func) # pyright: ignore

View File

@ -0,0 +1,331 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from honcho_core import AsyncHoncho as AsyncHonchoCore
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 .pagination import AsyncPage
if TYPE_CHECKING:
from .session import AsyncSession
class AsyncPeer(BaseModel):
"""
Represents a peer in the Honcho system with async operations.
Peers can send messages, participate in sessions, and maintain both global
and local representations for contextual interactions. A peer represents
an entity (user, assistant, etc.) that can communicate within the system.
Attributes:
id: Unique identifier for this peer
_client: Reference to the parent AsyncHoncho client instance
"""
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"
)
_client: AsyncHonchoCore = PrivateAttr()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
peer_id: str = Field(
...,
min_length=1,
description="Unique identifier for this peer within the workspace",
),
workspace_id: str = Field(
..., min_length=1, description="Workspace ID for scoping operations"
),
client: AsyncHonchoCore = Field(
..., description="Reference to the parent AsyncHoncho client instance"
),
) -> None:
"""
Initialize a new AsyncPeer.
Args:
peer_id: Unique identifier for this peer within the workspace
workspace_id: Workspace ID for scoping operations
client: Reference to the parent AsyncHoncho client instance
"""
super().__init__(id=peer_id, workspace_id=workspace_id)
self._client = client
@classmethod
async def create(
cls,
peer_id: str,
workspace_id: str,
client: AsyncHonchoCore,
*,
config: dict[str, object] | None = None,
) -> AsyncPeer:
"""
Create a new AsyncPeer with optional configuration.
Args:
peer_id: Unique identifier for this peer within the workspace
workspace_id: Workspace ID for scoping operations
client: Reference to the parent AsyncHoncho client instance
config: Optional configuration to set for this peer.
If set, will get/create peer immediately with flags.
Returns:
A new AsyncPeer instance
"""
peer = cls(peer_id, workspace_id, client)
if config:
await client.workspaces.peers.get_or_create(
workspace_id=workspace_id,
id=peer_id,
configuration=config,
)
return peer
async def chat(
self,
queries: str | list[str],
*,
stream: bool = False,
target: str | AsyncPeer | None = None,
session_id: str | None = None,
) -> str | None:
"""
Query the peer's representation with a natural language question.
Makes an async API call to the Honcho dialectic endpoint to query either the peer's
global representation (all content associated with this peer) or their local
representation of another peer (what this peer knows about the target peer).
Args:
queries: The natural language question(s) to ask. Can be a single string or a list of strings.
stream: Whether to stream the response
target: Optional target peer for local representation queries. 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
Returns:
Response string containing the answer to the query, or None if no
relevant information is available
"""
response = await self._client.workspaces.peers.chat(
peer_id=self.id,
workspace_id=self.workspace_id,
queries=queries,
stream=stream,
target=str(target.id) if isinstance(target, AsyncPeer) else target,
session_id=session_id,
)
# "If the context provided doesn't help address the query, write absolutely NOTHING but "None""
if response.content in ("", None, "None"):
return None
return response.content
async def get_sessions(self) -> AsyncPage[AsyncSession]:
"""
Get all sessions this peer is a member of.
Makes an async API call to retrieve all sessions where this peer is an active participant.
Sessions are created when peers are added to them or send messages to them.
Returns:
An async paginated list of AsyncSession objects this peer belongs to. Returns an empty
list if the peer is not a member of any sessions
"""
from .session import AsyncSession
sessions_page = await self._client.workspaces.peers.sessions.list(
peer_id=self.id,
workspace_id=self.workspace_id,
)
return AsyncPage(
sessions_page,
lambda session: AsyncSession(session.id, self.workspace_id, self._client),
)
@validate_call
async def add_messages(
self,
content: str | MessageCreateParam | list[MessageCreateParam] = Field(
..., description="Content to add to the peer's representation"
),
) -> None:
"""
Add messages or content to this peer's global representation.
Makes an async API call to store content associated with this peer. This content
becomes part of the peer's global knowledge base and can be retrieved
through chat queries. Content can be provided as raw strings, Message objects,
or lists of Message objects.
Args:
content: Content to add to the peer's representation. Can be:
- str: Raw text content that will be converted to a Message
- Message: A single Message object to add
- List[Message]: Multiple Message objects to add in batch
"""
messages: list[MessageCreateParam]
if isinstance(content, str):
messages = [
MessageCreateParam(peer_id=self.id, content=content, metadata=None)
]
elif isinstance(content, list):
messages = content
else:
messages = [content]
await self._client.workspaces.peers.messages.create(
peer_id=self.id,
workspace_id=self.workspace_id,
messages=messages,
)
@validate_call
async def get_messages(
self,
*,
filters: dict[str, object] | None = Field(
None, description="Dictionary of filter criteria"
),
) -> AsyncPage[Message]:
"""
Get messages saved to this peer outside of a session with optional filtering.
Makes an API call to retrieve messages saved to this peer outside of a session.
Results can be filtered based on various criteria.
Args:
filters: Dictionary of filter criteria. Supported filters include:
- peer_id: Filter messages by the peer who created them
- metadata: Filter messages by metadata key-value pairs
- timestamp_start: Filter messages after a specific timestamp
- timestamp_end: Filter messages before a specific timestamp
Returns:
An AsyncPage of Message objects matching the specified criteria, ordered by
creation time (most recent first)
"""
messages_page = await self._client.workspaces.peers.messages.list(
peer_id=self.id,
workspace_id=self.workspace_id,
filter=filters,
)
return AsyncPage(messages_page)
@validate_call
def message(
self,
content: str = Field(
..., min_length=1, description="The text content for the message"
),
*,
metadata: dict[str, object] | None = Field(
None, description="Optional metadata dictionary"
),
) -> MessageCreateParam:
"""
Create a MessageCreateParam object attributed to this peer.
This is a convenience method for creating MessageCreateParam objects with this peer's ID.
The created MessageCreateParam can then be added to sessions or used in other operations.
Args:
content: The text content for the message
metadata: Optional metadata dictionary to associate with the message
Returns:
A new MessageCreateParam object with this peer's ID and the provided content
"""
return MessageCreateParam(peer_id=self.id, content=content, metadata=metadata)
async def get_metadata(self) -> dict[str, object]:
"""
Get the current metadata for this peer.
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.
Returns:
A dictionary containing the peer's metadata. Returns an empty dictionary
if no metadata is set
"""
peer = await self._client.workspaces.peers.get_or_create(
workspace_id=self.workspace_id,
id=self.id,
)
return peer.metadata or {}
@validate_call
async def set_metadata(
self,
metadata: dict[str, object] = Field(
..., description="Metadata dictionary to associate with this peer"
),
) -> None:
"""
Set the metadata for this peer.
Makes an async API call to update the metadata associated with this peer.
This will overwrite any existing metadata with the provided values.
Args:
metadata: A dictionary of metadata to associate with this peer.
Keys must be strings, values can be any JSON-serializable type
"""
await self._client.workspaces.peers.update(
peer_id=self.id,
workspace_id=self.workspace_id,
metadata=metadata,
)
@validate_call
async def search(
self,
query: str = Field(..., min_length=1, description="The search query to use"),
) -> AsyncPage[Message]:
"""
Search for messages in this peer's global representation.
Makes an async API call to search for messages in this peer's global representation.
Args:
query: The search query to use
Returns:
An AsyncPage of Message objects representing the search results.
Returns an empty page if no messages are found.
"""
messages_page = await self._client.workspaces.peers.search(
self.id, workspace_id=self.workspace_id, query=query
)
return AsyncPage(messages_page)
def __repr__(self) -> str:
"""
Return a string representation of the AsyncPeer.
Returns:
A string representation suitable for debugging
"""
return f"AsyncPeer(id='{self.id}')"
def __str__(self) -> str:
"""
Return a human-readable string representation of the AsyncPeer.
Returns:
The peer's ID
"""
return self.id

View File

@ -0,0 +1,513 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any
from honcho_core import AsyncHoncho as AsyncHonchoCore
from honcho_core._types import NOT_GIVEN
from honcho_core.types.workspaces.sessions import MessageCreateParam
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
from ..session_context import SessionContext
from .pagination import AsyncPage
if TYPE_CHECKING:
from .peer import AsyncPeer
try:
env_val = os.getenv("HONCHO_DEFAULT_CONTEXT_TOKENS")
_default_context_tokens = int(env_val) if env_val else None
except (ValueError, TypeError):
_default_context_tokens = None
class SessionPeerConfig(BaseModel):
observe_others: bool | None = Field(
None,
description="Whether this peer should form a session-level theory-of-mind representation of other peers in the session",
)
observe_me: bool | None = Field(
None,
description="Whether other peers in this session should try to form a session-level theory-of-mind representation of this peer",
)
class AsyncSession(BaseModel):
"""
Represents a session in Honcho with async operations.
Sessions are scoped to a set of peers and contain messages/content.
They create bidirectional relationships between peers and provide
a context for multi-party conversations and interactions.
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
"""
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"
)
_client: AsyncHonchoCore = PrivateAttr()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
session_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"
),
client: AsyncHonchoCore = Field(
..., description="Reference to the parent AsyncHoncho client instance"
),
) -> None:
"""
Initialize a new AsyncSession.
Args:
session_id: Unique identifier for this session within the workspace
workspace_id: Workspace ID for scoping operations
client: Reference to the parent AsyncHoncho client instance
"""
super().__init__(
id=session_id,
workspace_id=workspace_id,
)
self._client = client
@classmethod
async def create(
cls,
session_id: str,
workspace_id: str,
client: AsyncHonchoCore,
*,
config: dict[str, object] | None = None,
) -> AsyncSession:
"""
Create a new AsyncSession with optional configuration.
Args:
session_id: Unique identifier for this session within the workspace
workspace_id: Workspace ID for scoping operations
client: Reference to the parent AsyncHoncho client instance
config:
Optional configuration to set for this session. If set, will get/create session immediately with flags.
Returns:
A new AsyncSession instance
"""
session = cls(session_id, workspace_id, client)
if config:
await client.workspaces.sessions.get_or_create(
workspace_id=workspace_id,
id=session_id,
configuration=config,
)
return session
async def add_peers(
self,
peers: str
| AsyncPeer
| tuple[str, SessionPeerConfig]
| tuple[AsyncPeer, SessionPeerConfig]
| list[AsyncPeer | str]
| list[tuple[AsyncPeer | str, SessionPeerConfig]]
| list[AsyncPeer | str | tuple[AsyncPeer | str, SessionPeerConfig]] = Field(
..., description="Peers to add to the session"
),
) -> None:
"""
Add peers to this session.
Makes an async API call to add one or more peers to this session. Adding peers
creates bidirectional relationships and allows them to participate in
the session's conversations.
Args:
peers: Peers to add to the session. Can be:
- str: Single peer ID
- AsyncPeer: Single AsyncPeer object
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
- tuple[AsyncPeer, SessionPeerConfig]: Single AsyncPeer object and SessionPeerConfig
- List[tuple[Union[AsyncPeer, str], SessionPeerConfig]]: List of AsyncPeer objects and/or peer IDs and SessionPeerConfig
- Mixed lists with peers and tuples/lists containing peer+config combinations
"""
if not isinstance(peers, list):
peers = [peers]
peer_dict: dict[str, Any] = {}
for peer in peers:
if isinstance(peer, tuple):
# Handle tuple[str/AsyncPeer, SessionPeerConfig]
peer_id = peer[0] if isinstance(peer[0], str) else peer[0].id
peer_config = peer[1]
peer_dict[peer_id] = peer_config.model_dump(exclude_none=True)
else:
# Handle direct str or AsyncPeer
peer_id = peer if isinstance(peer, str) else peer.id
peer_dict[peer_id] = {}
await self._client.workspaces.sessions.peers.add(
session_id=self.id,
workspace_id=self.workspace_id,
body=peer_dict,
)
async def set_peers(
self,
peers: str
| AsyncPeer
| tuple[str, SessionPeerConfig]
| tuple[AsyncPeer, SessionPeerConfig]
| list[AsyncPeer | str]
| list[tuple[AsyncPeer | str, SessionPeerConfig]]
| list[AsyncPeer | str | tuple[AsyncPeer | str, SessionPeerConfig]] = Field(
..., description="Peers to set for the session"
),
) -> None:
"""
Set the complete peer list for this session.
Makes an API call to replace the current peer list with the provided peers.
This will remove any peers not in the new list and add any that are missing.
Args:
peers: Peers to set for the session. Can be:
- str: Single peer ID
- AsyncPeer: Single AsyncPeer object
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
- tuple[AsyncPeer, SessionPeerConfig]: Single AsyncPeer object and SessionPeerConfig
- List[tuple[Union[AsyncPeer, str], SessionPeerConfig]]: List of AsyncPeer objects and/or peer IDs and SessionPeerConfig
- Mixed lists with peers and tuples/lists containing peer+config combinations
"""
if not isinstance(peers, list):
peers = [peers]
peer_dict: dict[str, Any] = {}
for peer in peers:
if isinstance(peer, tuple):
# Handle tuple[str/AsyncPeer, SessionPeerConfig]
peer_id = peer[0] if isinstance(peer[0], str) else peer[0].id
peer_config = peer[1]
peer_dict[peer_id] = peer_config.model_dump(exclude_none=True)
else:
# Handle direct str or AsyncPeer
peer_id = peer if isinstance(peer, str) else peer.id
peer_dict[peer_id] = {}
await self._client.workspaces.sessions.peers.set(
session_id=self.id,
workspace_id=self.workspace_id,
body=peer_dict,
)
async def remove_peers(
self,
peers: str | AsyncPeer | list[AsyncPeer | str] = Field(
..., description="Peers to remove from the session"
),
) -> None:
"""
Remove peers from this session.
Makes an async API call to remove one or more peers from this session.
Removed peers will no longer be able to participate in the session
unless added back.
Args:
peers: Peers to remove from the session. Can be:
- str: Single peer ID
- AsyncPeer: Single AsyncPeer object
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
"""
if not isinstance(peers, list):
peers = [peers]
peer_ids = [peer if isinstance(peer, str) else peer.id for peer in peers]
await self._client.workspaces.sessions.peers.remove(
session_id=self.id,
workspace_id=self.workspace_id,
body=peer_ids,
)
async def get_peers(self) -> list[AsyncPeer]:
"""
Get all peers in this session.
Makes an async API call to retrieve the list of peer IDs that are currently
members of this session. Automatically converts the paginated response
into a list for us -- the max number of peers in a session is usually 10.
Returns:
A list of AsyncPeer objects that are members of this session
"""
from .peer import AsyncPeer
peers_page = await self._client.workspaces.sessions.peers.list(
session_id=self.id,
workspace_id=self.workspace_id,
)
return [
AsyncPeer(peer.id, self.workspace_id, self._client)
for peer in peers_page.items
]
async def get_peer_config(self, peer: str | AsyncPeer) -> SessionPeerConfig:
"""
Get the configuration for a peer in this session.
"""
from .peer import AsyncPeer
peer_get_config_response = (
await self._client.workspaces.sessions.peers.get_config(
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
workspace_id=self.workspace_id,
session_id=self.id,
)
)
return SessionPeerConfig(
observe_others=peer_get_config_response.observe_others,
observe_me=peer_get_config_response.observe_me,
)
async def set_peer_config(
self, peer: str | AsyncPeer, config: SessionPeerConfig
) -> None:
"""
Set the configuration for a peer in this session.
"""
from .peer import AsyncPeer
await self._client.workspaces.sessions.peers.set_config(
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
workspace_id=self.workspace_id,
session_id=self.id,
observe_others=NOT_GIVEN
if config.observe_others is None
else config.observe_others,
observe_me=NOT_GIVEN if config.observe_me is None else config.observe_me,
)
@validate_call
async def add_messages(
self,
messages: MessageCreateParam | list[MessageCreateParam] = Field(
..., description="Messages to add to the session"
),
) -> None:
"""
Add one or more messages to this session.
Makes an API call to store messages in this session. Any message added
to a session will automatically add the creating peer to the session
if they are not already a member.
Args:
messages: Messages to add to the session. Can be:
- MessageCreateParam: Single MessageCreateParam object
- List[MessageCreateParam]: List of MessageCreateParam objects
"""
if not isinstance(messages, list):
messages = [messages]
await self._client.workspaces.sessions.messages.create(
session_id=self.id,
workspace_id=self.workspace_id,
messages=[MessageCreateParam(**message) for message in messages],
)
@validate_call
async def get_messages(
self,
*,
filters: dict[str, object] | None = Field(
None, description="Dictionary of filter criteria"
),
) -> AsyncPage[Message]:
"""
Get messages from this session with optional filtering.
Makes an async API call to retrieve messages from this session. Results can be
filtered based on various criteria.
Args:
filters: Dictionary of filter criteria. Supported filters include:
- peer_id: Filter messages by the peer who created them
- metadata: Filter messages by metadata key-value pairs
- timestamp_start: Filter messages after a specific timestamp
- timestamp_end: Filter messages before a specific timestamp
Returns:
An async paginated list of Message objects matching the specified criteria, ordered by
creation time (most recent first)
"""
messages_page = await self._client.workspaces.sessions.messages.list(
session_id=self.id,
workspace_id=self.workspace_id,
filter=filters,
)
return AsyncPage(messages_page)
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.
Returns:
A dictionary containing the session's metadata. Returns an empty dictionary
if no metadata is set
"""
session = await self._client.workspaces.sessions.get_or_create(
workspace_id=self.workspace_id,
id=self.id,
)
return session.metadata or {}
@validate_call
async def set_metadata(
self,
metadata: dict[str, object] = Field(
..., description="Metadata dictionary to associate with this session"
),
) -> None:
"""
Set metadata for this session.
Makes an async API call to update the metadata associated with this session.
This will overwrite any existing metadata with the provided values.
Args:
metadata: A dictionary of metadata 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,
metadata=metadata,
)
@validate_call
async def get_context(
self,
*,
summary: bool = True,
tokens: int | None = Field(
None, gt=0, description="Maximum number of tokens to include in the context"
),
) -> SessionContext:
"""
Get optimized context for this session within a token limit.
Makes an async API call to retrieve a curated list of messages that provides
optimal context for the conversation while staying within the specified
token limit. Uses tiktoken for token counting, so results should be
compatible with OpenAI models.
Args:
summary: Whether to include summary information
tokens: Maximum number of tokens to include in the context.
Defaults to HONCHO_default_context_tokens environment
variable if it exists.
Returns:
A SessionContext object containing the optimized message history
that maximizes conversational context while respecting the token limit
Note:
Token counting is performed using tiktoken. For models using different
tokenizers, you may need to adjust the token limit accordingly.
"""
if not tokens:
tokens = _default_context_tokens
context = await self._client.workspaces.sessions.get_context(
session_id=self.id,
workspace_id=self.workspace_id,
tokens=tokens,
summary=summary,
)
return SessionContext(
session_id=self.id, messages=context.messages, summary=context.summary
)
@validate_call
async def search(
self,
query: str = Field(..., min_length=1, description="The search query to use"),
) -> AsyncPage[Message]:
"""
Search for messages in this session.
Makes an async API call to search for messages in this session.
Args:
query: The search query to use
Returns:
An AsyncPage of Message objects representing the search results.
Returns an empty page if no messages are found.
"""
messages_page = await self._client.workspaces.sessions.search(
self.id, workspace_id=self.workspace_id, query=query
)
return AsyncPage(messages_page)
async def working_rep(
self,
peer: str | AsyncPeer,
*,
target: str | AsyncPeer | None = None,
) -> dict[str, object]:
"""
Get the current working representation of the peer in this session.
Args:
peer: Peer to get the working representation of.
target: Optional target peer to get the representation of. If provided,
queries what `peer` knows about the `target`.
Returns:
A dictionary containing information about the peer.
"""
from .peer import AsyncPeer
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,
)
def __repr__(self) -> str:
"""
Return a string representation of the AsyncSession.
Returns:
A string representation suitable for debugging
"""
return f"AsyncSession(id='{self.id}')"
def __str__(self) -> str:
"""
Return a human-readable string representation of the AsyncSession.
Returns:
The session's ID
"""
return self.id

View File

@ -0,0 +1,304 @@
import logging
import os
from collections.abc import Mapping
from typing import Any, Literal
import httpx
from honcho_core import Honcho as HonchoCore
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
from .pagination import SyncPage
from .peer import Peer
from .session import Session
logger = logging.getLogger(__name__)
class Honcho(BaseModel):
"""
Main client for the Honcho SDK.
Provides access to peers, sessions, and workspace operations with configuration
from environment variables or explicit parameters. This is the primary entry
point for interacting with the Honcho conversational memory platform.
Attributes:
api_key: API key for authentication
base_url: Base URL for the Honcho API
workspace_id: Workspace ID for scoping operations
"""
workspace_id: str = Field(
...,
min_length=1,
description="Workspace ID for scoping operations",
)
_client: HonchoCore = PrivateAttr()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
api_key: str | None = None,
environment: Literal["local", "production", "demo"] | None = None,
base_url: str | None = Field(None, description="Base URL for the Honcho API"),
workspace_id: str | None = Field(
None, min_length=1, description="Workspace ID for scoping operations"
),
timeout: float | None = Field(None, gt=0, description="Timeout in seconds"),
max_retries: int | None = Field(
None, ge=0, description="Maximum number of retries"
),
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
http_client: httpx.Client | None = Field(
None, description="Custom HTTP client"
),
) -> None:
"""
Initialize the Honcho client.
Args:
api_key:
API key for authentication. If not provided, will attempt to
read from HONCHO_API_KEY environment variable
environment:
Environment to use (local or production)
base_url:
Base URL for the Honcho API. If not provided, will attempt to
read from HONCHO_URL environment variable or default to the
production API URL
workspace_id:
Workspace ID to use for operations. If not provided, will
attempt to read from HONCHO_WORKSPACE_ID environment variable
or default to "default"
timeout:
Optional custom timeout for the HTTP client.
max_retries:
Optional custom maximum number of retries for the HTTP client.
default_headers:
Optional custom default headers for the HTTP client.
default_query:
Optional custom default query parameters for the HTTP client.
http_client:
Optional custom httpx client.
"""
# Resolve workspace_id before calling super().__init__
resolved_workspace_id = workspace_id or os.getenv(
"HONCHO_WORKSPACE_ID", "default"
)
super().__init__(workspace_id=resolved_workspace_id)
# Build client kwargs, excluding None values that HonchoCore doesn't handle well
client_kwargs: dict[str, Any] = {}
if api_key is not None:
client_kwargs["api_key"] = api_key
if environment is not None:
client_kwargs["environment"] = environment
if base_url is not None:
client_kwargs["base_url"] = base_url
if timeout is not None:
client_kwargs["timeout"] = timeout
if max_retries is not None:
client_kwargs["max_retries"] = max_retries
if default_headers is not None:
client_kwargs["default_headers"] = default_headers
if default_query is not None:
client_kwargs["default_query"] = default_query
if http_client is not None:
client_kwargs["http_client"] = http_client
self._client = HonchoCore(**client_kwargs)
# Get or create the workspace
self._client.workspaces.get_or_create(id=self.workspace_id)
@validate_call
def peer(
self,
id: str = Field(
..., min_length=1, description="Unique identifier for the peer"
),
*,
config: dict[str, object] | None = Field(
None,
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
),
) -> Peer:
"""
Get or create a peer with the given ID.
Creates a Peer object that can be used to interact with the specified peer.
This method does not make an API call - the peer is created lazily when
its methods are first used.
Args:
id: Unique identifier for the peer within the workspace. Should be a
stable identifier that can be used consistently across sessions
config:
Optional configuration to set for this peer. If set, will get/create peer immediately with flags.
Returns:
A Peer object that can be used to send messages, join sessions, and
query the peer's knowledge representations
Raises:
ValidationError: If the peer ID is empty or invalid
"""
return Peer(id, self.workspace_id, self._client, config=config)
def get_peers(self) -> SyncPage[Peer]:
"""
Get all peers in the current workspace.
Makes an API call to retrieve all peers that have been created or used
within the current workspace. Returns a paginated result that transforms
inner client Peer objects to SDK Peer objects as they are consumed.
Returns:
A SyncPage of Peer objects representing all peers in the workspace.
The page preserves pagination functionality while transforming objects
"""
peers_page = self._client.workspaces.peers.list(workspace_id=self.workspace_id)
return SyncPage(
peers_page, lambda peer: Peer(peer.id, self.workspace_id, self._client)
)
@validate_call
def session(
self,
id: str = Field(
..., min_length=1, description="Unique identifier for the session"
),
*,
config: dict[str, object] | None = Field(
None,
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
),
) -> Session:
"""
Get or create a session with the given ID.
Creates a Session object that can be used to manage conversations between
multiple peers. This method does not make an API call - the session is
created lazily when its methods are first used.
Args:
id: Unique identifier for the session within the workspace. Should be a
stable identifier that can be used consistently to reference the
same conversation
config:
Optional configuration to set for this session. If set, will get/create session immediately with flags.
Returns:
A Session object that can be used to add peers, send messages, and
manage conversation context
Raises:
ValidationError: If the session ID is empty or invalid
"""
return Session(id, self.workspace_id, self._client, config=config)
def get_sessions(self) -> SyncPage[Session]:
"""
Get all sessions in the current workspace.
Makes an API call to retrieve all sessions that have been created within
the current workspace.
Returns:
A SyncPage of Session objects representing all sessions in the workspace.
Returns an empty page if no sessions exist
"""
sessions_page = self._client.workspaces.sessions.list(
workspace_id=self.workspace_id
)
return SyncPage(
sessions_page,
lambda session: Session(session.id, self.workspace_id, self._client),
)
def get_metadata(self) -> dict[str, object]:
"""
Get metadata for the current workspace.
Makes an API call to retrieve metadata associated with the current workspace.
Workspace metadata can include settings, configuration, or any other
key-value data associated with the workspace.
Returns:
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 {}
@validate_call
def set_metadata(
self,
metadata: dict[str, object] = Field(..., description="Metadata dictionary"),
) -> None:
"""
Set metadata for the current workspace.
Makes an API call to update the metadata associated with the current workspace.
This will overwrite any existing metadata with the provided values.
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)
def get_workspaces(self) -> list[str]:
"""
Get all workspace IDs from the Honcho instance.
Makes an API call to retrieve all workspace IDs that the authenticated
user has access to.
Returns:
A list of workspace ID strings. Returns an empty list if no workspaces
are accessible or none exist
"""
workspaces = self._client.workspaces.list()
return [workspace.id for workspace in workspaces]
@validate_call
def search(
self,
query: str = Field(..., min_length=1, description="The search query to use"),
) -> SyncPage[Message]:
"""
Search for messages in the current workspace.
Makes an API call to search for messages in the current workspace.
Args:
query: The search query to use
Returns:
A SyncPage of Message objects representing the search results.
Returns an empty page if no messages are found.
"""
messages_page = self._client.workspaces.search(self.workspace_id, body=query)
return SyncPage(messages_page)
def __repr__(self) -> str:
"""
Return a string representation of the Honcho client.
Returns:
A string representation suitable for debugging
"""
return f"Honcho(workspace_id='{self.workspace_id}', base_url='{self._client.base_url}')"
def __str__(self) -> str:
"""
Return a human-readable string representation of the Honcho client.
Returns:
A string showing the workspace ID
"""
return f"Honcho Client (workspace: {self.workspace_id})"

View File

@ -0,0 +1,82 @@
from collections.abc import Callable, Iterator
from typing import Optional, TypeVar # pyright: ignore
from honcho_core.pagination import SyncPage as SyncPageCore
from pydantic import Field, validate_call
T = TypeVar("T")
U = TypeVar("U")
class SyncPage(SyncPageCore[U]):
"""
Paginated result wrapper that transforms objects from type T to type U.
Provides iteration and transformation capabilities while preserving
pagination functionality from the underlying core SyncPage.
"""
@validate_call
def __init__( # pyright: ignore
self,
original_page: SyncPageCore[T] = Field(
..., description="The original SyncPage to wrap"
),
transform_func: Callable[[T], U] | None = Field(
None,
description="Optional function to transform objects from type T to type U",
),
) -> None:
"""
Initialize the transformed page.
Args:
original_page: The original SyncPage to wrap
transform_func: Optional function to transform objects from type T to type U.
If None, objects are passed through unchanged.
"""
self._original_page = original_page # pyright: ignore
self._transform_func = transform_func # pyright: ignore
def __iter__(self) -> Iterator[U]:
"""Iterate over optionally transformed objects."""
for item in self._original_page:
if self._transform_func is not None:
yield self._transform_func(item)
else:
yield item # pyright: ignore
def __getitem__(self, index: int) -> U:
"""Get an optionally transformed object by index."""
item = self._original_page[index] # pyright: ignore
if self._transform_func is not None:
return self._transform_func(item) # pyright: ignore
return item # pyright: ignore
def __len__(self) -> int:
"""Get the length of the page."""
return len(self._original_page) # pyright: ignore
@property
def data(self) -> list[U]:
"""Get all optionally transformed data as a list."""
if self._transform_func is not None:
return [self._transform_func(item) for item in self._original_page.data] # pyright: ignore
return self._original_page.data # pyright: ignore
@property
def object(self) -> str:
"""Get the object type."""
return self._original_page.object # pyright: ignore
@property
def has_next_page(self) -> bool: # pyright: ignore
"""Check if there's a next page."""
return self._original_page.has_next_page # pyright: ignore
def next_page(self) -> Optional["SyncPage[U]"]: # pyright: ignore
"""Get the next page with optional transformation applied."""
next_page = self._original_page.next_page() # pyright: ignore
if next_page is None:
return None
return SyncPage(next_page, self._transform_func) # pyright: ignore

View File

@ -0,0 +1,311 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from honcho_core import Honcho as HonchoCore
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 .pagination import SyncPage
if TYPE_CHECKING:
from .session import Session
class Peer(BaseModel):
"""
Represents a peer in the Honcho system.
Peers can send messages, participate in sessions, and maintain both global
and local representations for contextual interactions. A peer represents
an entity (user, assistant, etc.) that can communicate within the system.
Attributes:
id: Unique identifier for this peer
_client: Reference to the parent Honcho client instance
"""
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"
)
_client: HonchoCore = PrivateAttr()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
peer_id: str = Field(
...,
min_length=1,
description="Unique identifier for this peer within the workspace",
),
workspace_id: str = Field(
..., min_length=1, description="Workspace ID for scoping operations"
),
client: HonchoCore = Field(
..., description="Reference to the parent Honcho client instance"
),
*,
config: dict[str, object] | None = Field(
None,
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
),
) -> None:
"""
Initialize a new Peer.
Args:
peer_id: Unique identifier for this peer within the workspace
workspace_id: Workspace ID for scoping operations
client: Reference to the parent Honcho client instance
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)
self._client = client
if config:
self._client.workspaces.peers.get_or_create(
workspace_id=workspace_id,
id=peer_id,
configuration=config,
)
def chat(
self,
queries: str | list[str],
*,
stream: bool = False,
target: str | Peer | None = None,
session_id: str | None = None,
) -> str | None:
"""
Query the peer's representation with a natural language question.
Makes an API call to the Honcho dialectic endpoint to query either the peer's
global representation (all content associated with this peer) or their local
representation of another peer (what this peer knows about the target peer).
Args:
queries: The natural language question(s) to ask. Can be a single string or a list of strings.
stream: Whether to stream the response
target: Optional target peer for local representation queries. 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
Returns:
Response string containing the answer to the query, or None if no
relevant information is available
"""
response = self._client.workspaces.peers.chat(
peer_id=self.id,
workspace_id=self.workspace_id,
queries=queries,
stream=stream,
target=str(target.id) if isinstance(target, Peer) else target,
session_id=session_id,
)
if response.content in ("", None, "None"):
return None
return response.content
def get_sessions(self) -> SyncPage[Session]:
"""
Get all sessions this peer is a member of.
Makes an API call to retrieve all sessions where this peer is an active participant.
Sessions are created when peers are added to them or send messages to them.
Returns:
A paginated list of Session objects this peer belongs to. Returns an empty
list if the peer is not a member of any sessions
"""
from .session import Session
sessions_page = self._client.workspaces.peers.sessions.list(
peer_id=self.id,
workspace_id=self.workspace_id,
)
return SyncPage(
sessions_page,
lambda session: Session(session.id, self.workspace_id, self._client),
)
@validate_call
def add_messages(
self,
content: str | MessageCreateParam | list[MessageCreateParam] = Field(
..., description="Content to add to the peer's representation"
),
) -> None:
"""
Add messages or content to this peer's global representation.
Makes an API call to store content associated with this peer. This content
becomes part of the peer's global knowledge base and can be retrieved
through chat queries. Content can be provided as raw strings, Message objects,
or lists of Message objects.
Args:
content: Content to add to the peer's representation. Can be:
- str: Raw text content that will be converted to a Message
- Message: A single Message object to add
- List[Message]: Multiple Message objects to add in batch
"""
messages: list[MessageCreateParam]
if isinstance(content, str):
messages = [
MessageCreateParam(peer_id=self.id, content=content, metadata=None)
]
elif isinstance(content, list):
messages = content
else:
messages = [content]
self._client.workspaces.peers.messages.create(
peer_id=self.id,
workspace_id=self.workspace_id,
messages=messages,
)
@validate_call
def get_messages(
self,
*,
filters: dict[str, object] | None = Field(
None, description="Dictionary of filter criteria"
),
) -> SyncPage[Message]:
"""
Get messages saved to this peer outside of a session with optional filtering.
Makes an API call to retrieve messages saved to this peer outside of a session.
Results can be filtered based on various criteria.
Args:
filters: Dictionary of filter criteria. Supported filters include:
- peer_id: Filter messages by the peer who created them
- metadata: Filter messages by metadata key-value pairs
- timestamp_start: Filter messages after a specific timestamp
- timestamp_end: Filter messages before a specific timestamp
Returns:
A SyncPage of Message objects matching the specified criteria, ordered by
creation time (most recent first)
"""
messages_page = self._client.workspaces.peers.messages.list(
peer_id=self.id,
workspace_id=self.workspace_id,
filter=filters,
)
return SyncPage(messages_page)
@validate_call
def message(
self,
content: str = Field(
..., min_length=1, description="The text content for the message"
),
*,
metadata: dict[str, object] | None = Field(
None, description="Optional metadata dictionary"
),
) -> MessageCreateParam:
"""
Create a MessageCreateParam object attributed to this peer.
This is a convenience method for creating MessageCreateParam objects with this peer's ID.
The created MessageCreateParam can then be added to sessions or used in other operations.
Args:
content: The text content for the message
metadata: Optional metadata dictionary to associate with the message
Returns:
A new MessageCreateParam object with this peer's ID and the provided content
"""
return MessageCreateParam(peer_id=self.id, content=content, metadata=metadata)
def get_metadata(self) -> dict[str, object]:
"""
Get the current metadata for this peer.
Makes an API call to retrieve metadata associated with this peer. Metadata
can include custom attributes, settings, or any other key-value data
associated with the peer.
Returns:
A dictionary containing the peer's metadata. Returns an empty dictionary
if no metadata is set
"""
peer = self._client.workspaces.peers.get_or_create(
workspace_id=self.workspace_id,
id=self.id,
)
return peer.metadata or {}
@validate_call
def set_metadata(
self,
metadata: dict[str, object] = Field(
..., description="Metadata dictionary to associate with this peer"
),
) -> None:
"""
Set the metadata for this peer.
Makes an API call to update the metadata associated with this peer.
This will overwrite any existing metadata with the provided values.
Args:
metadata: A dictionary of metadata to associate with this peer.
Keys must be strings, values can be any JSON-serializable type
"""
self._client.workspaces.peers.update(
peer_id=self.id,
workspace_id=self.workspace_id,
metadata=metadata,
)
@validate_call
def search(
self,
query: str = Field(..., min_length=1, description="The search query to use"),
) -> SyncPage[Message]:
"""
Search for messages in this peer's global representation.
Makes an API call to search for messages in this peer's global representation.
Args:
query: The search query to use
Returns:
A SyncPage of Message objects representing the search results.
Returns an empty page if no messages are found.
"""
messages_page = self._client.workspaces.peers.search(
self.id, workspace_id=self.workspace_id, query=query
)
return SyncPage(messages_page)
def __repr__(self) -> str:
"""
Return a string representation of the Peer.
Returns:
A string representation suitable for debugging
"""
return f"Peer(id='{self.id}')"
def __str__(self) -> str:
"""
Return a human-readable string representation of the Peer.
Returns:
The peer's ID
"""
return self.id

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,490 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any
from honcho_core import Honcho as HonchoCore
from honcho_core._types import NOT_GIVEN
from honcho_core.types.workspaces.sessions import MessageCreateParam
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
from .pagination import SyncPage
from .session_context import SessionContext
if TYPE_CHECKING:
from .peer import Peer
try:
env_val = os.getenv("HONCHO_DEFAULT_CONTEXT_TOKENS")
_default_context_tokens = int(env_val) if env_val else None
except (ValueError, TypeError):
_default_context_tokens = None
class SessionPeerConfig(BaseModel):
observe_others: bool | None = Field(
None,
description="Whether this peer should form a session-level theory-of-mind representation of other peers in the session",
)
observe_me: bool | None = Field(
None,
description="Whether other peers in this session should try to form a session-level theory-of-mind representation of this peer",
)
class Session(BaseModel):
"""
Represents a session in Honcho.
Sessions are scoped to a set of peers and contain messages/content.
They create bidirectional relationships between peers and provide
a context for multi-party conversations and interactions.
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
"""
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"
)
_client: HonchoCore = PrivateAttr()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
session_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"
),
client: HonchoCore = Field(
..., description="Reference to the parent Honcho client instance"
),
*,
config: dict[str, object] | None = Field(
None,
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
),
) -> None:
"""
Initialize a new Session.
Args:
session_id: Unique identifier for this session within the workspace
workspace_id: Workspace ID for scoping operations
client: Reference to the parent Honcho client instance
config:
Optional configuration to set for this session. If set, will get/create session immediately with flags.
"""
super().__init__(
id=session_id,
workspace_id=workspace_id,
)
self._client = client
if config:
self._client.workspaces.sessions.get_or_create(
workspace_id=workspace_id,
id=session_id,
configuration=config,
)
def add_peers(
self,
peers: str
| Peer
| tuple[str, SessionPeerConfig]
| tuple[Peer, SessionPeerConfig]
| list[Peer | str]
| list[tuple[Peer | str, SessionPeerConfig]]
| list[Peer | str | tuple[Peer | str, SessionPeerConfig]] = Field(
..., description="Peers to add to the session"
),
) -> None:
"""
Add peers to this session.
Makes an async API call to add one or more peers to this session. Adding peers
creates bidirectional relationships and allows them to participate in
the session's conversations.
Args:
peers: Peers to add to the session. Can be:
- str: Single peer ID
- Peer: Single Peer object
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
- tuple[Peer, SessionPeerConfig]: Single Peer object and SessionPeerConfig
- List[tuple[Union[Peer, str], SessionPeerConfig]]: List of Peer objects and/or peer IDs and SessionPeerConfig
- Mixed lists with peers and tuples/lists containing peer+config combinations
"""
if not isinstance(peers, list):
peers = [peers]
peer_dict: dict[str, Any] = {}
for peer in peers:
if isinstance(peer, tuple):
# Handle tuple[str/Peer, SessionPeerConfig]
peer_id = peer[0] if isinstance(peer[0], str) else peer[0].id
peer_config = peer[1]
peer_dict[peer_id] = peer_config.model_dump(exclude_none=True)
else:
# Handle direct str or Peer
peer_id = peer if isinstance(peer, str) else peer.id
peer_dict[peer_id] = {}
self._client.workspaces.sessions.peers.add(
session_id=self.id,
workspace_id=self.workspace_id,
body=peer_dict,
)
def set_peers(
self,
peers: str
| Peer
| tuple[str, SessionPeerConfig]
| tuple[Peer, SessionPeerConfig]
| list[Peer | str]
| list[tuple[Peer | str, SessionPeerConfig]]
| list[Peer | str | tuple[Peer | str, SessionPeerConfig]] = Field(
..., description="Peers to set for the session"
),
) -> None:
"""
Set the complete peer list for this session.
Makes an API call to replace the current peer list with the provided peers.
This will remove any peers not in the new list and add any that are missing.
Args:
peers: Peers to set for the session. Can be:
- str: Single peer ID
- Peer: Single Peer object
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
- tuple[Peer, SessionPeerConfig]: Single Peer object and SessionPeerConfig
- List[tuple[Union[Peer, str], SessionPeerConfig]]: List of Peer objects and/or peer IDs and SessionPeerConfig
- Mixed lists with peers and tuples/lists containing peer+config combinations
"""
if not isinstance(peers, list):
peers = [peers]
peer_dict: dict[str, Any] = {}
for peer in peers:
if isinstance(peer, tuple):
# Handle tuple[str/Peer, SessionPeerConfig]
peer_id = peer[0] if isinstance(peer[0], str) else peer[0].id
peer_config = peer[1]
peer_dict[peer_id] = peer_config.model_dump(exclude_none=True)
else:
# Handle direct str or Peer
peer_id = peer if isinstance(peer, str) else peer.id
peer_dict[peer_id] = {}
self._client.workspaces.sessions.peers.set(
session_id=self.id,
workspace_id=self.workspace_id,
body=peer_dict,
)
def remove_peers(
self,
peers: str | Peer | list[Peer | str] = Field(
..., description="Peers to remove from the session"
),
) -> None:
"""
Remove peers from this session.
Makes an API call to remove one or more peers from this session.
Removed peers will no longer be able to participate in the session
unless added back.
Args:
peers: Peers to remove from the session. Can be:
- str: Single peer ID
- Peer: Single Peer object
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
"""
if not isinstance(peers, list):
peers = [peers]
peer_ids = [peer if isinstance(peer, str) else peer.id for peer in peers]
self._client.workspaces.sessions.peers.remove(
session_id=self.id,
workspace_id=self.workspace_id,
body=peer_ids,
)
def get_peers(self) -> list[Peer]:
"""
Get all peers in this session.
Makes an API call to retrieve the list of peer IDs that are currently
members of this session. Automatically converts the paginated response
into a list for us -- the max number of peers in a session is usually 10.
Returns:
A list of Peer objects that are members of this session
"""
from .peer import Peer
peers_page = self._client.workspaces.sessions.peers.list(
session_id=self.id,
workspace_id=self.workspace_id,
)
return [
Peer(peer.id, self.workspace_id, self._client) for peer in peers_page.items
]
def get_peer_config(self, peer: str | Peer) -> SessionPeerConfig:
"""
Get the configuration for a peer in this session.
"""
from .peer import Peer
peer_get_config_response = self._client.workspaces.sessions.peers.get_config(
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
workspace_id=self.workspace_id,
session_id=self.id,
)
return SessionPeerConfig(
observe_others=peer_get_config_response.observe_others,
observe_me=peer_get_config_response.observe_me,
)
def set_peer_config(self, peer: str | Peer, config: SessionPeerConfig) -> None:
"""
Set the configuration for a peer in this session.
"""
from .peer import Peer
self._client.workspaces.sessions.peers.set_config(
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
workspace_id=self.workspace_id,
session_id=self.id,
observe_others=NOT_GIVEN
if config.observe_others is None
else config.observe_others,
observe_me=NOT_GIVEN if config.observe_me is None else config.observe_me,
)
@validate_call
def add_messages(
self,
messages: MessageCreateParam | list[MessageCreateParam] = Field(
..., description="Messages to add to the session"
),
) -> None:
"""
Add one or more messages to this session.
Makes an API call to store messages in this session. Any message added
to a session will automatically add the creating peer to the session
if they are not already a member.
Args:
messages: Messages to add to the session. Can be:
- MessageCreateParam: Single MessageCreateParam object
- List[MessageCreateParam]: List of MessageCreateParam objects
"""
if not isinstance(messages, list):
messages = [messages]
self._client.workspaces.sessions.messages.create(
session_id=self.id,
workspace_id=self.workspace_id,
messages=[MessageCreateParam(**message) for message in messages],
)
@validate_call
def get_messages(
self,
*,
filters: dict[str, object] | None = Field(
None, description="Dictionary of filter criteria"
),
) -> SyncPage[Message]:
"""
Get messages from this session with optional filtering.
Makes an API call to retrieve messages from this session. Results can be
filtered based on various criteria.
Args:
filters: Dictionary of filter criteria. Supported filters include:
- peer_id: Filter messages by the peer who created them
- metadata: Filter messages by metadata key-value pairs
- timestamp_start: Filter messages after a specific timestamp
- timestamp_end: Filter messages before a specific timestamp
Returns:
A list of Message objects matching the specified criteria, ordered by
creation time (most recent first)
"""
messages_page = self._client.workspaces.sessions.messages.list(
session_id=self.id,
workspace_id=self.workspace_id,
filter=filters,
)
return SyncPage(messages_page)
def get_metadata(self) -> dict[str, object]:
"""
Get metadata for this session.
Makes an API call to retrieve the current metadata associated with this session.
Metadata can include custom attributes, settings, or any other key-value data.
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 {}
)
@validate_call
def set_metadata(
self,
metadata: dict[str, object] = Field(
..., description="Metadata dictionary to associate with this session"
),
) -> None:
"""
Set metadata for this session.
Makes an API call to update the metadata associated with this session.
This will overwrite any existing metadata with the provided values.
Args:
metadata: A dictionary of metadata 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,
metadata=metadata,
)
@validate_call
def get_context(
self,
*,
summary: bool = True,
tokens: int | None = Field(
None, gt=0, description="Maximum number of tokens to include in the context"
),
) -> SessionContext:
"""
Get optimized context for this session within a token limit.
Makes an API call to retrieve a curated list of messages that provides
optimal context for the conversation while staying within the specified
token limit. Uses tiktoken for token counting, so results should be
compatible with OpenAI models.
Args:
summary: Whether to include summary information
tokens: Maximum number of tokens to include in the context.
Defaults to HONCHO_default_context_tokens env var
Returns:
A SessionContext object containing the optimized message history
that maximizes conversational context while respecting the token limit
Note:
Token counting is performed using tiktoken. For models using different
tokenizers, you may need to adjust the token limit accordingly.
"""
if not tokens:
tokens = _default_context_tokens
context = self._client.workspaces.sessions.get_context(
session_id=self.id,
workspace_id=self.workspace_id,
tokens=tokens,
summary=summary,
)
return SessionContext(
session_id=self.id, messages=context.messages, summary=context.summary
)
@validate_call
def search(
self,
query: str = Field(..., min_length=1, description="The search query to use"),
) -> SyncPage[Message]:
"""
Search for messages in this session.
Makes an API call to search for messages in this session.
Args:
query: The search query to use
Returns:
A SyncPage of Message objects representing the search results.
Returns an empty page if no messages are found.
"""
messages_page = self._client.workspaces.sessions.search(
self.id, workspace_id=self.workspace_id, query=query
)
return SyncPage(messages_page)
def working_rep(
self,
peer: str | Peer,
*,
target: str | Peer | None = None,
) -> dict[str, object]:
"""
Get the current working representation of the peer in this session.
Args:
peer: Peer to get the working representation of.
target: Optional target peer to get the representation of. If provided,
queries what `peer` knows about the `target`.
Returns:
A dictionary containing information about the peer.
"""
from .peer import Peer
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,
)
def __repr__(self) -> str:
"""
Return a string representation of the Session.
Returns:
A string representation suitable for debugging
"""
return f"Session(id='{self.id}')"
def __str__(self) -> str:
"""
Return a human-readable string representation of the Session.
Returns:
The session's ID
"""
return self.id

View File

@ -0,0 +1,142 @@
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, Field, validate_call
from .peer import Peer
class SessionContext(BaseModel):
"""
Represents the context of a session containing a curated list of messages.
The SessionContext provides methods to convert message history into formats
compatible with different LLM providers while staying within token limits
and providing optimal conversation context.
Attributes:
messages: List of Message objects representing the conversation context
"""
session_id: str = Field(
..., description="ID of the session this context belongs to"
)
messages: list[Message] = Field(
..., description="List of Message objects to include in the context"
)
summary: str = Field(
..., description="Summary of the session history prior to the message cutoff"
)
@validate_call
def __init__(
self,
session_id: str = Field(
..., description="ID of the session this context belongs to"
),
messages: list[Message] = Field(
..., description="List of Message objects to include in the context"
),
summary: str = Field(
...,
description="Summary of the session history prior to the message cutoff",
),
) -> None:
"""
Initialize a new SessionContext.
Args:
messages: List of Message objects to include in the context
"""
super().__init__(
session_id=session_id,
messages=messages,
summary=summary,
)
def to_openai(
self,
*,
assistant: str | Peer,
) -> list[dict[str, object]]:
"""
Convert the context to OpenAI-compatible message format.
Transforms the message history into the format expected by OpenAI's
Chat Completions API, with proper role assignments based on the
assistant's identity.
Args:
assistant: The assistant peer (Peer object or peer ID string) to use
for determining message roles. Messages from this peer will
be marked as "assistant", others as "user"
Returns:
A list of dictionaries in OpenAI format, where each dictionary contains
"role" and "content" keys suitable for the OpenAI API
Raises:
ValidationError: If assistant parameter is invalid
"""
assistant_id = assistant.id if isinstance(assistant, Peer) else assistant
return [
{
"role": "assistant" if message.peer_id == assistant_id else "user",
"content": message.content,
}
for message in self.messages
]
def to_anthropic(
self,
*,
assistant: str | Peer,
) -> list[dict[str, object]]:
"""
Convert the context to Anthropic-compatible message format.
Transforms the message history into the format expected by Anthropic's
Claude API. TODO: Anthropic requires messages to alternate between
user and assistant roles, so this method may need to handle role
consolidation or filtering in the future.
Args:
assistant: The assistant peer (Peer object or peer ID string) to use
for determining message roles. Messages from this peer will
be marked as "assistant", others as "user"
Returns:
A list of dictionaries in Anthropic format, where each dictionary contains
"role" and "content" keys suitable for the Anthropic API
Raises:
ValidationError: If assistant parameter is invalid
Note:
Future versions may implement role alternation requirements for
Anthropic's API compatibility
"""
assistant_id = assistant.id if isinstance(assistant, Peer) else assistant
return [
{
"role": "assistant" if message.peer_id == assistant_id else "user",
"content": message.content,
}
for message in self.messages
]
def __len__(self) -> int:
"""
Return the number of messages in the context.
Returns:
The number of messages in this context
"""
return len(self.messages)
def __repr__(self) -> str:
"""
Return a string representation of the SessionContext.
Returns:
A string representation suitable for debugging
"""
return f"SessionContext(messages={len(self.messages)})"

509
sdks/python/uv.lock Normal file
View File

@ -0,0 +1,509 @@
version = 1
revision = 2
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.9.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.0", 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/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
[[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.0", 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.0.2"
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.5", 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.0.1" },
{ 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.0.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.9.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.5", 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.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/73/59cfd4769eb041e6bf47cfdfec609b7c913e7c6844c9732ad2c53c92bd6d/honcho_core-1.0.1.tar.gz", hash = "sha256:90c1a6ca79c0686453a764eee47ae8e74ca277849dbbe5bd7de9bbd0505ea6ad", size = 119013, upload-time = "2025-06-19T16:29:09.276Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/b1/0b7809b2635c60c16d0d53cedaaf8102b19ff9a5ef91578d8a713c970057/honcho_core-1.0.1-py3-none-any.whl", hash = "sha256:5f549dfadcb07a366c18d1feef6d4020e7b97ff9f3d98f454cb591b117e662c8", size = 110594, upload-time = "2025-06-19T16:29:08.153Z" },
]
[[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.9.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.5"
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.0", 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/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
]
[[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.0", 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.11.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" },
{ url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" },
{ url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" },
{ url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" },
{ url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" },
{ url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" },
{ url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" },
{ url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" },
{ url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" },
{ url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" },
{ url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" },
{ url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" },
{ url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" },
{ url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" },
{ url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" },
{ url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" },
]
[[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.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/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" },
]

2
sdks/typescript/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

40
sdks/typescript/README.md Normal file
View File

@ -0,0 +1,40 @@
# Honcho TypeScript SDK
A high-level, ergonomic TypeScript SDK for the Honcho conversational memory platform. This library wraps [honcho-node-core](../honcho-node-core) to provide a user-friendly, Pythonic API for managing peers, sessions, and conversational context.
## Installation
```
npm install @honcho-ai/sdk
```
## Usage
```ts
import { Honcho } from "@honcho-ai/sdk";
const honcho = new Honcho({
apiKey: process.env.HONCHO_API_KEY,
baseUrl: "http://localhost:8000",
workspaceId: "test",
});
const assistant = honcho.peer("bob");
const alice = honcho.peer("alice");
await honcho.getPeers();
const session = honcho.session("session_1");
await session.addPeers([alice, assistant]);
await session.addMessages([
assistant.message("What did you have for breakfast today, alice?"),
alice.message("I had oatmeal."),
]);
const response = await alice.chat("what did alice have for breakfast today?");
console.log(response);
```
See `examples/` for more.

View File

@ -0,0 +1,182 @@
# Honcho TypeScript SDK Test Suite
This directory contains an exhaustive and idiomatic test suite for the Honcho TypeScript SDK that covers every available endpoint in multiple ways.
## Test Structure
### Unit Tests
Each class has its own dedicated test file with comprehensive coverage:
- **`client.test.ts`** - Tests for the main `Honcho` client class
- **`peer.test.ts`** - Tests for the `Peer` class
- **`session.test.ts`** - Tests for the `Session` class
- **`session_context.test.ts`** - Tests for the `SessionContext` class
- **`pagination.test.ts`** - Tests for the `Page` class
### Integration Tests
- **`integration.test.ts`** - End-to-end workflow tests that demonstrate real-world usage patterns
### Test Configuration
- **`setup.ts`** - Global test setup and configuration
- **`jest.config.js`** - Jest configuration
- **`__mocks__/@honcho-ai/core.ts`** - Mock implementation of the core API client
## Test Coverage
### Honcho Client (`client.test.ts`)
- ✅ Constructor with all option variations
- ✅ Environment variable fallbacks
- ✅ Peer creation and validation
- ✅ Session creation and validation
- ✅ Workspace metadata operations
- ✅ Workspace listing
- ✅ Search functionality
- ✅ Error handling for all methods
- ✅ Edge cases and input validation
### Peer Class (`peer.test.ts`)
- ✅ Chat functionality with all option combinations
- ✅ Session management
- ✅ Message operations (add, get, create)
- ✅ Metadata operations
- ✅ Search within peer scope
- ✅ Different input types and formats
- ✅ Null/empty response handling
- ✅ Error scenarios
### Session Class (`session.test.ts`)
- ✅ Peer management (add, set, remove, list)
- ✅ Message operations with filtering
- ✅ Metadata operations
- ✅ Context retrieval with options
- ✅ Search within session scope
- ✅ Working representation queries
- ✅ Mixed input types (strings vs objects)
- ✅ Constructor options
- ✅ Error handling
### SessionContext Class (`session_context.test.ts`)
- ✅ Constructor variations
- ✅ OpenAI format conversion
- ✅ Anthropic format conversion
- ✅ Length and toString methods
- ✅ Empty/null message handling
- ✅ Complex message content
- ✅ Case sensitivity
- ✅ Missing field handling
- ✅ Edge cases and malformed data
### Page Class (`pagination.test.ts`)
- ✅ Async iteration
- ✅ Transform functions
- ✅ Data retrieval methods
- ✅ Pagination navigation
- ✅ Different page formats
- ✅ Error handling in transforms
- ✅ Large dataset handling
- ✅ Circular references
- ✅ Complex nested structures
### Integration Tests (`integration.test.ts`)
- ✅ Complete chat session workflow
- ✅ Workspace and peer management
- ✅ Multi-scope search functionality
- ✅ Error scenario handling
- ✅ Pagination workflows
- ✅ Working representation queries
- ✅ Type safety verification
- ✅ Empty/null response handling
## Test Patterns
### Comprehensive Mocking
All tests use comprehensive mocks of the underlying `@honcho-ai/core` API client to ensure:
- Tests run independently of external services
- Predictable and controllable test scenarios
- Fast test execution
- Ability to test error conditions
### Edge Case Coverage
Each test suite includes extensive edge case testing:
- Empty/null inputs and responses
- Invalid input types
- API error conditions
- Boundary conditions
- Malformed data handling
### Multiple Input Types
Tests verify that methods handle various input types correctly:
- String vs object parameters
- Single items vs arrays
- Optional vs required parameters
- Different data structures
### Error Scenarios
Comprehensive error testing including:
- API failures
- Invalid inputs
- Network errors
- Timeout scenarios
- Validation errors
### Async Operations
Proper testing of all asynchronous operations:
- Promise resolution/rejection
- Async iteration
- Concurrent operations
- Error propagation
## Running Tests
```bash
# Install dependencies
npm install
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test client.test.ts
# Run integration tests only
npm test integration.test.ts
```
## Coverage Goals
This test suite aims for:
- **100% function coverage** - Every function is called
- **100% branch coverage** - Every code path is tested
- **100% statement coverage** - Every line is executed
- **Comprehensive edge case coverage** - Every failure mode is tested
## Test Philosophy
1. **Exhaustive Testing**: Every public method and property is tested
2. **Multiple Scenarios**: Each method is tested with various inputs and conditions
3. **Real-world Usage**: Integration tests mirror actual usage patterns
4. **Error Resilience**: Extensive error condition testing
5. **Type Safety**: TypeScript types are verified throughout
6. **Performance Awareness**: Tests include large dataset scenarios
7. **Maintainability**: Clear, well-documented test cases
## Mock Strategy
The test suite uses a sophisticated mocking strategy:
1. **Core API Mocking**: The `@honcho-ai/core` module is completely mocked
2. **Flexible Responses**: Mock responses can be configured per test
3. **Error Simulation**: Easy simulation of API errors and edge cases
4. **Isolation**: Each test runs in complete isolation
5. **Deterministic**: Tests produce consistent, reproducible results
This ensures that the SDK layer is thoroughly tested while remaining independent of the underlying API implementation.

View File

@ -0,0 +1,45 @@
// Mock implementation of @honcho-ai/core for testing
export default class MockHonchoCore {
public workspaces = {
peers: {
list: jest.fn(),
chat: jest.fn(),
sessions: {
list: jest.fn(),
},
messages: {
create: jest.fn(),
list: jest.fn(),
},
getOrCreate: jest.fn(),
update: jest.fn(),
search: jest.fn(),
workingRepresentation: jest.fn(),
},
sessions: {
list: jest.fn(),
peers: {
add: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
list: jest.fn(),
},
messages: {
create: jest.fn(),
list: jest.fn(),
},
getOrCreate: jest.fn(),
update: jest.fn(),
getContext: jest.fn(),
search: jest.fn(),
},
getOrCreate: jest.fn().mockResolvedValue({ id: 'test-workspace', metadata: {} }),
update: jest.fn(),
list: jest.fn(),
search: jest.fn(),
};
constructor(options?: any) {
// Mock constructor
}
}

View File

@ -0,0 +1,385 @@
import { Honcho } from '../src/client';
import { Peer } from '../src/peer';
import { Session } from '../src/session';
import { Page } from '../src/pagination';
// Mock the @honcho-ai/core module
jest.mock('@honcho-ai/core', () => {
return jest.fn().mockImplementation(() => ({
workspaces: {
peers: {
list: jest.fn(),
},
sessions: {
list: jest.fn(),
},
getOrCreate: jest.fn(),
update: jest.fn(),
list: jest.fn(),
search: jest.fn(),
},
}));
});
describe('Honcho Client', () => {
let honcho: Honcho;
let mockClient: any;
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
honcho = new Honcho({
workspaceId: 'test-workspace',
apiKey: 'test-key',
environment: 'local',
});
mockClient = (honcho as any)._client;
});
describe('constructor', () => {
it('should initialize with provided options', () => {
const client = new Honcho({
workspaceId: 'custom-workspace',
apiKey: 'custom-key',
environment: 'production',
baseURL: 'https://custom-url.com',
timeout: 5000,
maxRetries: 3,
});
expect(client.workspaceId).toBe('custom-workspace');
});
it('should use environment variables as fallbacks', () => {
process.env.HONCHO_WORKSPACE_ID = 'env-workspace';
process.env.HONCHO_API_KEY = 'env-key';
process.env.HONCHO_URL = 'https://env-url.com';
const client = new Honcho({});
expect(client.workspaceId).toBe('env-workspace');
// Clean up environment variables
delete process.env.HONCHO_WORKSPACE_ID;
delete process.env.HONCHO_API_KEY;
delete process.env.HONCHO_URL;
});
it('should use default workspace ID when none provided', () => {
const client = new Honcho({});
expect(client.workspaceId).toBe('default');
});
it('should handle all constructor options', () => {
const client = new Honcho({
workspaceId: 'test',
apiKey: 'key',
environment: 'local',
baseURL: 'https://example.com',
timeout: 10000,
maxRetries: 5,
defaultHeaders: { 'X-Custom': 'header' },
defaultQuery: { param: 'value' },
});
expect(client.workspaceId).toBe('test');
});
});
describe('peer', () => {
it('should create a new Peer instance', () => {
const peer = honcho.peer('test-peer');
expect(peer).toBeInstanceOf(Peer);
expect(peer.id).toBe('test-peer');
});
it('should throw error for empty peer ID', () => {
expect(() => honcho.peer('')).toThrow('Peer ID must be a non-empty string');
});
it('should throw error for non-string peer ID', () => {
expect(() => honcho.peer(null as any)).toThrow('Peer ID must be a non-empty string');
expect(() => honcho.peer(undefined as any)).toThrow('Peer ID must be a non-empty string');
expect(() => honcho.peer(123 as any)).toThrow('Peer ID must be a non-empty string');
});
});
describe('getPeers', () => {
it('should return a Page of Peer instances', async () => {
const mockPeersData = {
items: [
{ id: 'peer1', metadata: {} },
{ id: 'peer2', metadata: {} },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData);
const peersPage = await honcho.getPeers();
expect(peersPage).toBeInstanceOf(Page);
expect(mockClient.workspaces.peers.list).toHaveBeenCalledWith('test-workspace');
});
it('should handle empty peers list', async () => {
const mockPeersData = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData);
const peersPage = await honcho.getPeers();
expect(peersPage).toBeInstanceOf(Page);
expect(mockClient.workspaces.peers.list).toHaveBeenCalledWith('test-workspace');
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.list.mockRejectedValue(new Error('API Error'));
await expect(honcho.getPeers()).rejects.toThrow('API Error');
});
});
describe('session', () => {
it('should create a new Session instance', () => {
const session = honcho.session('test-session');
expect(session).toBeInstanceOf(Session);
expect(session.id).toBe('test-session');
});
it('should throw error for empty session ID', () => {
expect(() => honcho.session('')).toThrow('Session ID must be a non-empty string');
});
it('should throw error for non-string session ID', () => {
expect(() => honcho.session(null as any)).toThrow('Session ID must be a non-empty string');
expect(() => honcho.session(undefined as any)).toThrow('Session ID must be a non-empty string');
expect(() => honcho.session(123 as any)).toThrow('Session ID must be a non-empty string');
});
});
describe('getSessions', () => {
it('should return a Page of Session instances', async () => {
const mockSessionsData = {
items: [
{ id: 'session1', metadata: {} },
{ id: 'session2', metadata: {} },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData);
const sessionsPage = await honcho.getSessions();
expect(sessionsPage).toBeInstanceOf(Page);
expect(mockClient.workspaces.sessions.list).toHaveBeenCalledWith('test-workspace');
});
it('should handle empty sessions list', async () => {
const mockSessionsData = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData);
const sessionsPage = await honcho.getSessions();
expect(sessionsPage).toBeInstanceOf(Page);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.list.mockRejectedValue(new Error('API Error'));
await expect(honcho.getSessions()).rejects.toThrow('API Error');
});
});
describe('getMetadata', () => {
it('should return workspace metadata', async () => {
const mockWorkspace = {
id: 'test-workspace',
metadata: { key: 'value', setting: 'config' },
};
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
const metadata = await honcho.getMetadata();
expect(metadata).toEqual({ key: 'value', setting: 'config' });
expect(mockClient.workspaces.getOrCreate).toHaveBeenCalledWith({ id: 'test-workspace' });
});
it('should return empty object when no metadata exists', async () => {
const mockWorkspace = {
id: 'test-workspace',
metadata: null,
};
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
const metadata = await honcho.getMetadata();
expect(metadata).toEqual({});
});
it('should handle API errors', async () => {
mockClient.workspaces.getOrCreate.mockRejectedValue(new Error('Workspace not found'));
await expect(honcho.getMetadata()).rejects.toThrow('Workspace not found');
});
});
describe('setMetadata', () => {
it('should update workspace metadata', async () => {
const metadata = { newKey: 'newValue', updated: true };
mockClient.workspaces.update.mockResolvedValue({});
await honcho.setMetadata(metadata);
expect(mockClient.workspaces.update).toHaveBeenCalledWith('test-workspace', { metadata });
});
it('should handle empty metadata object', async () => {
mockClient.workspaces.update.mockResolvedValue({});
await honcho.setMetadata({});
expect(mockClient.workspaces.update).toHaveBeenCalledWith('test-workspace', { metadata: {} });
});
it('should handle complex metadata objects', async () => {
const complexMetadata = {
nested: { object: { with: 'values' } },
array: [1, 2, 3],
boolean: true,
number: 42,
string: 'test',
};
mockClient.workspaces.update.mockResolvedValue({});
await honcho.setMetadata(complexMetadata);
expect(mockClient.workspaces.update).toHaveBeenCalledWith('test-workspace', { metadata: complexMetadata });
});
it('should handle API errors', async () => {
mockClient.workspaces.update.mockRejectedValue(new Error('Update failed'));
await expect(honcho.setMetadata({ key: 'value' })).rejects.toThrow('Update failed');
});
});
describe('getWorkspaces', () => {
it('should return array of workspace IDs', async () => {
const mockWorkspacesPage = {
[Symbol.asyncIterator]: async function* () {
yield { id: 'workspace1' };
yield { id: 'workspace2' };
yield { id: 'workspace3' };
},
};
mockClient.workspaces.list.mockResolvedValue(mockWorkspacesPage);
const workspaces = await honcho.getWorkspaces();
expect(workspaces).toEqual(['workspace1', 'workspace2', 'workspace3']);
expect(mockClient.workspaces.list).toHaveBeenCalled();
});
it('should handle empty workspaces list', async () => {
const mockWorkspacesPage = {
[Symbol.asyncIterator]: async function* () {
// Empty iterator
},
};
mockClient.workspaces.list.mockResolvedValue(mockWorkspacesPage);
const workspaces = await honcho.getWorkspaces();
expect(workspaces).toEqual([]);
});
it('should handle API errors', async () => {
mockClient.workspaces.list.mockRejectedValue(new Error('Failed to list workspaces'));
await expect(honcho.getWorkspaces()).rejects.toThrow('Failed to list workspaces');
});
});
describe('search', () => {
it('should search for messages and return Page', async () => {
const mockSearchResults = {
items: [
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.search.mockResolvedValue(mockSearchResults);
const results = await honcho.search('hello');
expect(results).toBeInstanceOf(Page);
expect(mockClient.workspaces.search).toHaveBeenCalledWith('test-workspace', 'hello');
});
it('should handle empty search results', async () => {
const mockSearchResults = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.search.mockResolvedValue(mockSearchResults);
const results = await honcho.search('nonexistent');
expect(results).toBeInstanceOf(Page);
});
it('should throw error for empty query', async () => {
await expect(honcho.search('')).rejects.toThrow('Search query must be a non-empty string');
await expect(honcho.search(' ')).rejects.toThrow('Search query must be a non-empty string');
});
it('should throw error for non-string query', async () => {
await expect(honcho.search(null as any)).rejects.toThrow('Search query must be a non-empty string');
await expect(honcho.search(undefined as any)).rejects.toThrow('Search query must be a non-empty string');
await expect(honcho.search(123 as any)).rejects.toThrow('Search query must be a non-empty string');
});
it('should handle complex search queries', async () => {
const mockSearchResults = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.search.mockResolvedValue(mockSearchResults);
const complexQuery = 'complex query with "quotes" and special characters!@#$%';
await honcho.search(complexQuery);
expect(mockClient.workspaces.search).toHaveBeenCalledWith('test-workspace', complexQuery);
});
it('should handle API errors', async () => {
mockClient.workspaces.search.mockRejectedValue(new Error('Search failed'));
await expect(honcho.search('test')).rejects.toThrow('Search failed');
});
});
});

View File

@ -0,0 +1,418 @@
import { Honcho } from '../src/client';
import { Peer } from '../src/peer';
import { Session } from '../src/session';
import { SessionContext } from '../src/session_context';
import { Page } from '../src/pagination';
// Mock the @honcho-ai/core module
let mockWorkspacesApi: any;
jest.mock('@honcho-ai/core', () => {
return jest.fn().mockImplementation(() => mockWorkspacesApi);
});
describe('Honcho SDK Integration Tests', () => {
let honcho: Honcho;
beforeEach(() => {
mockWorkspacesApi = {
workspaces: {
peers: {
list: jest.fn(),
chat: jest.fn(),
sessions: { list: jest.fn() },
messages: { create: jest.fn(), list: jest.fn() },
getOrCreate: jest.fn(),
update: jest.fn(),
search: jest.fn(),
workingRepresentation: jest.fn(),
},
sessions: {
list: jest.fn(),
peers: { add: jest.fn(), set: jest.fn(), remove: jest.fn(), list: jest.fn() },
messages: { create: jest.fn(), list: jest.fn() },
getOrCreate: jest.fn(),
update: jest.fn(),
getContext: jest.fn(),
search: jest.fn(),
},
getOrCreate: jest.fn(),
update: jest.fn(),
list: jest.fn(),
search: jest.fn(),
},
};
jest.clearAllMocks();
honcho = new Honcho({
workspaceId: 'integration-test-workspace',
apiKey: 'test-api-key',
environment: 'local',
});
});
describe('Complete Workflow Integration', () => {
it('should handle complete chat session workflow', async () => {
// Setup mock responses
const mockPeerData = { id: 'assistant', metadata: { role: 'ai' } };
const mockSessionData = { id: 'chat-session', metadata: { topic: 'general' } };
const mockMessages = [
{ id: 'msg1', content: 'Hello', peer_name: 'user' },
{ id: 'msg2', content: 'Hi there!', peer_name: 'assistant' },
];
const mockContextData = { messages: mockMessages, summary: 'Friendly greeting' };
mockWorkspacesApi.workspaces.peers.getOrCreate.mockResolvedValue(mockPeerData);
mockWorkspacesApi.workspaces.sessions.getOrCreate.mockResolvedValue(mockSessionData);
mockWorkspacesApi.workspaces.sessions.peers.add.mockResolvedValue({});
mockWorkspacesApi.workspaces.sessions.messages.create.mockResolvedValue({});
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue(mockContextData);
mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ content: 'AI response' });
// Step 1: Create peers
const user = honcho.peer('user');
const assistant = honcho.peer('assistant');
expect(user).toBeInstanceOf(Peer);
expect(assistant).toBeInstanceOf(Peer);
expect(user.id).toBe('user');
expect(assistant.id).toBe('assistant');
// Step 2: Create session
const session = honcho.session('chat-session');
expect(session).toBeInstanceOf(Session);
expect(session.id).toBe('chat-session');
// Step 3: Add peers to session
await session.addPeers([user, assistant]);
expect(mockWorkspacesApi.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'integration-test-workspace',
'chat-session',
{
'user': { observe_me: true, observe_others: false },
'assistant': { observe_me: true, observe_others: false }
}
);
// Step 4: Add messages to session
const userMessage = user.message('Hello');
const assistantMessage = assistant.message('Hi there!');
await session.addMessages([userMessage, assistantMessage]);
expect(mockWorkspacesApi.workspaces.sessions.messages.create).toHaveBeenCalledWith(
'integration-test-workspace',
'chat-session',
{
messages: [
{ peer_id: 'user', content: 'Hello', metadata: undefined },
{ peer_id: 'assistant', content: 'Hi there!', metadata: undefined },
]
}
);
// Step 5: Get session context
const context = await session.getContext();
expect(context).toBeInstanceOf(SessionContext);
expect(context.sessionId).toBe('chat-session');
expect(context.messages).toEqual(mockMessages);
expect(context.summary).toBe('Friendly greeting');
// Step 6: Convert context to different formats
const openAIFormat = context.toOpenAI('assistant');
const anthropicFormat = context.toAnthropic('assistant');
expect(openAIFormat).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' },
]);
expect(anthropicFormat).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' },
]);
// Step 7: Query assistant
const response = await assistant.chat('How are you?');
expect(response).toBe('AI response');
expect(mockWorkspacesApi.workspaces.peers.chat).toHaveBeenCalledWith(
'integration-test-workspace',
'assistant',
{ queries: 'How are you?', stream: undefined, target: undefined, session_id: undefined }
);
});
it('should handle workspace and peer management workflow', async () => {
// Setup mock responses
const mockWorkspaceMetadata = { name: 'Test Workspace', version: '1.0' };
const mockPeersList = {
items: [
{ id: 'peer1', metadata: { role: 'user' } },
{ id: 'peer2', metadata: { role: 'assistant' } },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockWorkspacesApi.workspaces.getOrCreate.mockResolvedValue({
id: 'integration-test-workspace',
metadata: mockWorkspaceMetadata,
});
mockWorkspacesApi.workspaces.update.mockResolvedValue({});
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(mockPeersList);
// Step 1: Get workspace metadata
const metadata = await honcho.getMetadata();
expect(metadata).toEqual(mockWorkspaceMetadata);
// Step 2: Update workspace metadata
const newMetadata = { ...mockWorkspaceMetadata, updated: true };
await honcho.setMetadata(newMetadata);
expect(mockWorkspacesApi.workspaces.update).toHaveBeenCalledWith(
'integration-test-workspace',
{ metadata: newMetadata }
);
// Step 3: Get all peers
const peersPage = await honcho.getPeers();
expect(peersPage).toBeInstanceOf(Page);
// Step 4: Iterate through peers
const peersList: Peer[] = [];
for await (const peer of peersPage) {
peersList.push(peer);
}
expect(peersList).toHaveLength(2);
expect(peersList[0]).toBeInstanceOf(Peer);
expect(peersList[1]).toBeInstanceOf(Peer);
expect(peersList[0].id).toBe('peer1');
expect(peersList[1].id).toBe('peer2');
});
it('should handle search functionality across different scopes', async () => {
// Setup mock responses
const mockWorkspaceSearchResults = {
items: [
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
],
total: 1,
size: 1,
hasNextPage: false,
};
const mockPeerSearchResults = {
items: [
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
],
total: 1,
size: 1,
hasNextPage: false,
};
const mockSessionSearchResults = {
items: [
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
],
total: 1,
size: 1,
hasNextPage: false,
};
mockWorkspacesApi.workspaces.search.mockResolvedValue(mockWorkspaceSearchResults);
mockWorkspacesApi.workspaces.peers.search.mockResolvedValue(mockPeerSearchResults);
mockWorkspacesApi.workspaces.sessions.search.mockResolvedValue(mockSessionSearchResults);
// Step 1: Search workspace
const workspaceResults = await honcho.search('test query');
expect(workspaceResults).toBeInstanceOf(Page);
expect(mockWorkspacesApi.workspaces.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test query'
);
// Step 2: Search peer
const peer = honcho.peer('test-peer');
const peerResults = await peer.search('peer query');
expect(peerResults).toBeInstanceOf(Page);
expect(mockWorkspacesApi.workspaces.peers.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test-peer',
{ query: 'peer query' }
);
// Step 3: Search session
const session = honcho.session('test-session');
const sessionResults = await session.search('session query');
expect(sessionResults).toBeInstanceOf(Page);
expect(mockWorkspacesApi.workspaces.sessions.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test-session',
'session query'
);
});
it('should handle error scenarios gracefully', async () => {
// Setup error scenarios
mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue(new Error('Chat API failed'));
mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue(new Error('Context API failed'));
const assistant = honcho.peer('assistant');
const session = honcho.session('error-session');
// Test error handling in chat
await expect(assistant.chat('Hello')).rejects.toThrow('Chat API failed');
// Test error handling in context
await expect(session.getContext()).rejects.toThrow('Context API failed');
});
it('should handle pagination correctly', async () => {
// Setup paginated response
const firstPageData = {
items: [
{ id: 'peer1', metadata: {} },
{ id: 'peer2', metadata: {} },
],
total: 4,
size: 2,
hasNextPage: true,
nextPage: jest.fn(),
};
const secondPageData = {
items: [
{ id: 'peer3', metadata: {} },
{ id: 'peer4', metadata: {} },
],
total: 4,
size: 2,
hasNextPage: false,
nextPage: jest.fn().mockResolvedValue(null),
};
firstPageData.nextPage.mockResolvedValue(secondPageData);
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(firstPageData);
// Step 1: Get first page
const firstPage = await honcho.getPeers();
expect(firstPage.total).toBe(4);
expect(firstPage.size).toBe(2);
expect(firstPage.hasNextPage).toBe(true);
// Step 2: Get data from first page
const firstPageData_ = await firstPage.data();
expect(firstPageData_).toHaveLength(2);
// Step 3: Get next page
const secondPage = await firstPage.nextPage();
expect(secondPage).not.toBeNull();
expect(secondPage!.hasNextPage).toBe(false);
// Step 4: Get data from second page
const secondPageData_ = await secondPage!.data();
expect(secondPageData_).toHaveLength(2);
// Step 5: Verify no more pages
const thirdPage = await secondPage!.nextPage();
expect(thirdPage).toBeNull();
});
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',
};
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(mockWorkingRep);
const session = honcho.session('working-rep-session');
const alice = honcho.peer('alice');
const bob = honcho.peer('bob');
// Test working representation without target
const globalRep = await session.workingRep('alice');
expect(globalRep).toEqual(mockWorkingRep);
expect(mockWorkspacesApi.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
'integration-test-workspace',
'alice',
{ session_id: 'working-rep-session', target: undefined }
);
// Test working representation with target
await session.workingRep(alice, bob);
expect(mockWorkspacesApi.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
'integration-test-workspace',
'alice',
{ session_id: 'working-rep-session', target: 'bob' }
);
});
});
describe('Edge Cases and Error Handling Integration', () => {
it('should handle empty and null responses gracefully', async () => {
// Setup empty/null responses
mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ content: null });
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue({ items: [], total: 0, hasNextPage: false });
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({ messages: [] });
const peer = honcho.peer('empty-peer');
const session = honcho.session('empty-session');
// Test null chat response
const chatResult = await peer.chat('Hello');
expect(chatResult).toBeNull();
// Test empty peers list
const peersPage = await honcho.getPeers();
const peersList = await peersPage.data();
expect(peersList).toEqual([]);
// Test empty context
const context = await session.getContext();
expect(context.messages).toEqual([]);
expect(context.length).toBe(0);
});
it('should maintain type safety throughout the workflow', async () => {
// This test verifies TypeScript types are maintained correctly
const peer: Peer = honcho.peer('typed-peer');
const session: Session = honcho.session('typed-session');
expect(typeof peer.id).toBe('string');
expect(typeof session.id).toBe('string');
const message = peer.message('typed message', { metadata: { type: 'test' } });
expect(typeof message.peerId).toBe('string');
expect(typeof message.content).toBe('string');
expect(typeof message.metadata).toBe('object');
// Mock successful operations
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'typed-peer' }],
summary: 'Test summary',
});
const context: SessionContext = await session.getContext();
expect(typeof context.sessionId).toBe('string');
expect(Array.isArray(context.messages)).toBe(true);
expect(typeof context.summary).toBe('string');
expect(typeof context.length).toBe('number');
expect(typeof context.toString()).toBe('string');
const openAI = context.toOpenAI(peer);
const anthropic = context.toAnthropic('assistant');
expect(Array.isArray(openAI)).toBe(true);
expect(Array.isArray(anthropic)).toBe(true);
if (openAI.length > 0) {
expect(typeof openAI[0].role).toBe('string');
expect(typeof openAI[0].content).toBe('string');
}
});
});
});

View File

@ -0,0 +1,453 @@
import { Page } from '../src/pagination';
describe('Page', () => {
let mockOriginalPage: any;
let mockItems: any[];
beforeEach(() => {
mockItems = [
{ id: 'item1', name: 'Item 1' },
{ id: 'item2', name: 'Item 2' },
{ id: 'item3', name: 'Item 3' },
];
mockOriginalPage = {
items: mockItems,
data: mockItems,
size: 3,
total: 10,
hasNextPage: true,
get: jest.fn(),
nextPage: jest.fn(),
};
});
describe('constructor', () => {
it('should initialize with original page', () => {
const page = new Page(mockOriginalPage);
expect(page['_originalPage']).toBe(mockOriginalPage);
expect(page['_transformFunc']).toBeUndefined();
});
it('should initialize with transform function', () => {
const transformFunc = (item: any) => ({ ...item, transformed: true });
const page = new Page(mockOriginalPage, transformFunc);
expect(page['_originalPage']).toBe(mockOriginalPage);
expect(page['_transformFunc']).toBe(transformFunc);
});
});
describe('Symbol.asyncIterator', () => {
it('should iterate through items without transform', async () => {
const page = new Page(mockOriginalPage);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual(mockItems);
});
it('should iterate through items with transform', async () => {
const transformFunc = (item: any) => ({ ...item, transformed: true });
const page = new Page(mockOriginalPage, transformFunc);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual([
{ id: 'item1', name: 'Item 1', transformed: true },
{ id: 'item2', name: 'Item 2', transformed: true },
{ id: 'item3', name: 'Item 3', transformed: true },
]);
});
it('should handle empty items array', async () => {
const emptyPage = { items: [], data: [], size: 0, total: 0, hasNextPage: false };
const page = new Page(emptyPage);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual([]);
});
it('should handle page with data field instead of items', async () => {
const pageWithData = {
data: mockItems,
size: 3,
total: 10,
hasNextPage: true,
};
const page = new Page(pageWithData);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual(mockItems);
});
it('should handle page with neither items nor data', async () => {
const pageWithoutItems = {
size: 0,
total: 0,
hasNextPage: false,
};
const page = new Page(pageWithoutItems);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual([]);
});
});
describe('get', () => {
it('should get item by index without transform', async () => {
mockOriginalPage.get.mockResolvedValue(mockItems[1]);
const page = new Page(mockOriginalPage);
const item = await page.get(1);
expect(item).toEqual(mockItems[1]);
expect(mockOriginalPage.get).toHaveBeenCalledWith(1);
});
it('should get item by index with transform', async () => {
const transformFunc = (item: any) => ({ ...item, transformed: true });
mockOriginalPage.get.mockResolvedValue(mockItems[1]);
const page = new Page(mockOriginalPage, transformFunc);
const item = await page.get(1);
expect(item).toEqual({ ...mockItems[1], transformed: true });
expect(mockOriginalPage.get).toHaveBeenCalledWith(1);
});
it('should handle out of bounds index', async () => {
mockOriginalPage.get.mockResolvedValue(undefined);
const page = new Page(mockOriginalPage);
const item = await page.get(999);
expect(item).toBeUndefined();
expect(mockOriginalPage.get).toHaveBeenCalledWith(999);
});
it('should handle negative index', async () => {
mockOriginalPage.get.mockResolvedValue(undefined);
const page = new Page(mockOriginalPage);
const item = await page.get(-1);
expect(item).toBeUndefined();
expect(mockOriginalPage.get).toHaveBeenCalledWith(-1);
});
});
describe('size getter', () => {
it('should return size from original page', () => {
const page = new Page(mockOriginalPage);
expect(page.size).toBe(3);
});
it('should handle missing size', () => {
const pageWithoutSize = { items: mockItems };
const page = new Page(pageWithoutSize);
expect(page.size).toBeUndefined();
});
});
describe('total getter', () => {
it('should return total from original page', () => {
const page = new Page(mockOriginalPage);
expect(page.total).toBe(10);
});
it('should handle missing total', () => {
const pageWithoutTotal = { items: mockItems };
const page = new Page(pageWithoutTotal);
expect(page.total).toBeUndefined();
});
});
describe('data', () => {
it('should return data array without transform', async () => {
const page = new Page(mockOriginalPage);
const data = await page.data();
expect(data).toEqual(mockItems);
});
it('should return data array with transform', async () => {
const transformFunc = (item: any) => ({ ...item, transformed: true });
const page = new Page(mockOriginalPage, transformFunc);
const data = await page.data();
expect(data).toEqual([
{ id: 'item1', name: 'Item 1', transformed: true },
{ id: 'item2', name: 'Item 2', transformed: true },
{ id: 'item3', name: 'Item 3', transformed: true },
]);
});
it('should handle data as function', async () => {
const mockDataFunction = jest.fn().mockResolvedValue(mockItems);
const pageWithDataFunction = {
data: mockDataFunction,
size: 3,
total: 10,
hasNextPage: true,
};
const page = new Page(pageWithDataFunction);
const data = await page.data();
expect(data).toEqual(mockItems);
expect(mockDataFunction).toHaveBeenCalled();
});
it('should handle items as function', async () => {
const mockItemsFunction = jest.fn().mockResolvedValue(mockItems);
const pageWithItemsFunction = {
items: mockItemsFunction,
size: 3,
total: 10,
hasNextPage: true,
};
const page = new Page(pageWithItemsFunction);
const data = await page.data();
expect(data).toEqual(mockItems);
expect(mockItemsFunction).toHaveBeenCalled();
});
it('should handle empty data', async () => {
const emptyPage = { items: [], size: 0, total: 0, hasNextPage: false };
const page = new Page(emptyPage);
const data = await page.data();
expect(data).toEqual([]);
});
it('should prioritize items over data field', async () => {
const pageWithBoth = {
items: [{ id: 'from-items' }],
data: [{ id: 'from-data' }],
size: 1,
total: 1,
hasNextPage: false,
};
const page = new Page(pageWithBoth);
const data = await page.data();
expect(data).toEqual([{ id: 'from-items' }]);
});
});
describe('hasNextPage getter', () => {
it('should return hasNextPage from original page', () => {
const page = new Page(mockOriginalPage);
expect(page.hasNextPage).toBe(true);
});
it('should handle missing hasNextPage', () => {
const pageWithoutHasNextPage = { items: mockItems };
const page = new Page(pageWithoutHasNextPage);
expect(page.hasNextPage).toBeUndefined();
});
it('should handle false hasNextPage', () => {
const lastPage = { ...mockOriginalPage, hasNextPage: false };
const page = new Page(lastPage);
expect(page.hasNextPage).toBe(false);
});
});
describe('nextPage', () => {
it('should return next page with same transform function', async () => {
const nextPageData = {
items: [{ id: 'item4', name: 'Item 4' }],
size: 1,
total: 10,
hasNextPage: false,
};
const transformFunc = (item: any) => ({ ...item, transformed: true });
mockOriginalPage.nextPage.mockResolvedValue(nextPageData);
const page = new Page(mockOriginalPage, transformFunc);
const nextPage = await page.nextPage();
expect(nextPage).toBeInstanceOf(Page);
expect(nextPage!['_transformFunc']).toBe(transformFunc);
expect(mockOriginalPage.nextPage).toHaveBeenCalled();
});
it('should return null when no next page', async () => {
mockOriginalPage.nextPage.mockResolvedValue(null);
const page = new Page(mockOriginalPage);
const nextPage = await page.nextPage();
expect(nextPage).toBeNull();
});
it('should handle next page returning undefined', async () => {
mockOriginalPage.nextPage.mockResolvedValue(undefined);
const page = new Page(mockOriginalPage);
const nextPage = await page.nextPage();
expect(nextPage).toBeNull();
});
it('should handle error from original page nextPage', async () => {
mockOriginalPage.nextPage.mockRejectedValue(new Error('Failed to get next page'));
const page = new Page(mockOriginalPage);
await expect(page.nextPage()).rejects.toThrow('Failed to get next page');
});
});
describe('edge cases and error handling', () => {
it('should handle null original page', () => {
const page = new Page(null);
expect(page['_originalPage']).toBeNull();
expect(page.size).toBeUndefined();
expect(page.total).toBeUndefined();
expect(page.hasNextPage).toBeUndefined();
});
it('should handle transform function that throws error', async () => {
const errorTransform = () => {
throw new Error('Transform error');
};
const page = new Page(mockOriginalPage, errorTransform);
await expect(async () => {
for await (const item of page) {
// This should throw
}
}).rejects.toThrow('Transform error');
});
it('should handle transform function returning null', async () => {
const nullTransform = () => null;
const page = new Page(mockOriginalPage, nullTransform);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual([null, null, null]);
});
it('should handle very large datasets', async () => {
const largeItems = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
const largePage = {
items: largeItems,
size: 10000,
total: 10000,
hasNextPage: false,
};
const page = new Page(largePage);
let count = 0;
for await (const item of page) {
count++;
if (count > 10) break; // Don't actually iterate through all 10k items
}
expect(count).toBe(11);
});
it('should handle circular reference in items', async () => {
const circularItem: any = { id: 'circular' };
circularItem.self = circularItem;
const pageWithCircular = {
items: [circularItem],
size: 1,
total: 1,
hasNextPage: false,
};
const page = new Page(pageWithCircular);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toHaveLength(1);
expect(items[0].id).toBe('circular');
expect(items[0].self).toBe(items[0]);
});
it('should handle items with complex nested structures', async () => {
const complexItems = [
{
id: 'complex1',
nested: {
deep: {
value: 'deeply nested',
array: [1, 2, { nested: 'array object' }],
},
},
metadata: new Map([['key', 'value']]),
},
];
const complexPage = {
items: complexItems,
size: 1,
total: 1,
hasNextPage: false,
};
const page = new Page(complexPage);
const items: any[] = [];
for await (const item of page) {
items.push(item);
}
expect(items).toEqual(complexItems);
expect(items[0].nested.deep.array[2].nested).toBe('array object');
});
it('should handle async transform function', async () => {
const asyncTransform = async (item: any) => {
await new Promise(resolve => setTimeout(resolve, 1));
return { ...item, asyncTransformed: true };
};
mockOriginalPage.get.mockResolvedValue(mockItems[0]);
const page = new Page(mockOriginalPage, asyncTransform);
const item = await page.get(0);
expect(item).toEqual({ ...mockItems[0], asyncTransformed: true });
});
});
});

View File

@ -0,0 +1,537 @@
import { Peer } from '../src/peer';
import { Session } from '../src/session';
import { Page } from '../src/pagination';
import { Honcho } from '../src/client';
// Mock the @honcho-ai/core module
jest.mock('@honcho-ai/core', () => {
return jest.fn().mockImplementation(() => ({
workspaces: {
peers: {
chat: jest.fn(),
sessions: {
list: jest.fn(),
},
messages: {
create: jest.fn(),
list: jest.fn(),
},
getOrCreate: jest.fn(),
update: jest.fn(),
search: jest.fn(),
},
},
}));
});
describe('Peer', () => {
let honcho: Honcho;
let peer: Peer;
let mockClient: any;
beforeEach(() => {
jest.clearAllMocks();
honcho = new Honcho({
workspaceId: 'test-workspace',
apiKey: 'test-key',
environment: 'local',
});
peer = new Peer('test-peer', honcho);
mockClient = (honcho as any)._client;
});
describe('constructor', () => {
it('should initialize with correct properties', () => {
const newPeer = new Peer('peer-id', honcho);
expect(newPeer.id).toBe('peer-id');
expect(newPeer['_honcho']).toBe(honcho);
});
});
describe('chat', () => {
it('should query peer representation and return response', async () => {
const mockResponse = { content: 'Hello, I am a peer response' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
const result = await peer.chat('Hello');
expect(result).toBe('Hello, I am a peer response');
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ queries: 'Hello', stream: undefined, target: undefined, session_id: undefined }
);
});
it('should return null for None content', async () => {
const mockResponse = { content: 'None' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
const result = await peer.chat('Hello');
expect(result).toBeNull();
});
it('should return null for empty content', async () => {
const mockResponse = { content: null };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
const result = await peer.chat('Hello');
expect(result).toBeNull();
});
it('should handle chat with streaming option', async () => {
const mockResponse = { content: 'Streamed response' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
await peer.chat('Hello', { stream: true });
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ queries: 'Hello', stream: true, target: undefined, session_id: undefined }
);
});
it('should handle chat with target peer', async () => {
const targetPeer = new Peer('target-peer', honcho);
const mockResponse = { content: 'Targeted response' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
await peer.chat('Hello', { target: targetPeer });
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ queries: 'Hello', stream: undefined, target: 'target-peer', session_id: undefined }
);
});
it('should handle chat with target as string', async () => {
const mockResponse = { content: 'Targeted response' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
await peer.chat('Hello', { target: 'string-target' });
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ queries: 'Hello', stream: undefined, target: 'string-target', session_id: undefined }
);
});
it('should handle chat with session ID', async () => {
const mockResponse = { content: 'Session-specific response' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
await peer.chat('Hello', { sessionId: 'session-123' });
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ queries: 'Hello', stream: undefined, target: undefined, session_id: 'session-123' }
);
});
it('should handle all options together', async () => {
const targetPeer = new Peer('target-peer', honcho);
const mockResponse = { content: 'Full options response' };
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
await peer.chat('Hello', {
stream: true,
target: targetPeer,
sessionId: 'session-456'
});
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ queries: 'Hello', stream: true, target: 'target-peer', session_id: 'session-456' }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.chat.mockRejectedValue(new Error('Chat failed'));
await expect(peer.chat('Hello')).rejects.toThrow('Chat failed');
});
});
describe('getSessions', () => {
it('should return Page of Session instances', async () => {
const mockSessionsData = {
items: [
{ id: 'session1', metadata: {} },
{ id: 'session2', metadata: {} },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.peers.sessions.list.mockResolvedValue(mockSessionsData);
const sessionsPage = await peer.getSessions();
expect(sessionsPage).toBeInstanceOf(Page);
expect(mockClient.workspaces.peers.sessions.list).toHaveBeenCalledWith(
'test-peer',
'test-workspace'
);
});
it('should handle empty sessions list', async () => {
const mockSessionsData = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.peers.sessions.list.mockResolvedValue(mockSessionsData);
const sessionsPage = await peer.getSessions();
expect(sessionsPage).toBeInstanceOf(Page);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.sessions.list.mockRejectedValue(new Error('Failed to get sessions'));
await expect(peer.getSessions()).rejects.toThrow('Failed to get sessions');
});
});
describe('addMessages', () => {
it('should add a single string message', async () => {
mockClient.workspaces.peers.messages.create.mockResolvedValue({});
await peer.addMessages('Hello world');
expect(mockClient.workspaces.peers.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ messages: [{ peer_id: 'test-peer', content: 'Hello world', metadata: undefined }] }
);
});
it('should add a single message object', async () => {
const message = {
peerId: 'test-peer',
content: 'Test message',
metadata: { type: 'test' },
};
mockClient.workspaces.peers.messages.create.mockResolvedValue({});
await peer.addMessages(message);
expect(mockClient.workspaces.peers.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ messages: [{ peer_id: 'test-peer', content: 'Test message', metadata: { type: 'test' } }] }
);
});
it('should add message object without specified peerId', async () => {
const message = {
content: 'Test message without peer ID',
metadata: { type: 'test' },
};
mockClient.workspaces.peers.messages.create.mockResolvedValue({});
await peer.addMessages(message);
expect(mockClient.workspaces.peers.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ messages: [{ peer_id: 'test-peer', content: 'Test message without peer ID', metadata: { type: 'test' } }] }
);
});
it('should add array of messages', async () => {
const messages = [
{ peerId: 'peer1', content: 'Message 1', metadata: { order: 1 } },
{ peerId: 'peer2', content: 'Message 2', metadata: { order: 2 } },
];
mockClient.workspaces.peers.messages.create.mockResolvedValue({});
await peer.addMessages(messages);
expect(mockClient.workspaces.peers.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{
messages: [
{ peer_id: 'peer1', content: 'Message 1', metadata: { order: 1 } },
{ peer_id: 'peer2', content: 'Message 2', metadata: { order: 2 } },
]
}
);
});
it('should handle empty array', async () => {
mockClient.workspaces.peers.messages.create.mockResolvedValue({});
await peer.addMessages([]);
expect(mockClient.workspaces.peers.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ messages: [] }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.messages.create.mockRejectedValue(new Error('Failed to add messages'));
await expect(peer.addMessages('test')).rejects.toThrow('Failed to add messages');
});
});
describe('getMessages', () => {
it('should get messages without options', async () => {
const mockMessagesData = {
items: [
{ id: 'msg1', content: 'Message 1', peer_id: 'test-peer' },
{ id: 'msg2', content: 'Message 2', peer_id: 'test-peer' },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.peers.messages.list.mockResolvedValue(mockMessagesData);
const messagesPage = await peer.getMessages();
expect(messagesPage).toBeInstanceOf(Page);
expect(mockClient.workspaces.peers.messages.list).toHaveBeenCalledWith(
'test-peer',
'test-workspace',
undefined
);
});
it('should get messages with filter options', async () => {
const mockMessagesData = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.peers.messages.list.mockResolvedValue(mockMessagesData);
const options = {
filter: { type: 'important', date: '2023-01-01' }
};
await peer.getMessages(options);
expect(mockClient.workspaces.peers.messages.list).toHaveBeenCalledWith(
'test-peer',
'test-workspace',
{ type: 'important', date: '2023-01-01' }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.messages.list.mockRejectedValue(new Error('Failed to get messages'));
await expect(peer.getMessages()).rejects.toThrow('Failed to get messages');
});
});
describe('message', () => {
it('should create message object without metadata', () => {
const message = peer.message('Test content');
expect(message).toEqual({
peerId: 'test-peer',
content: 'Test content',
metadata: undefined,
});
});
it('should create message object with metadata', () => {
const metadata = { importance: 'high', category: 'greeting' };
const message = peer.message('Hello there', { metadata });
expect(message).toEqual({
peerId: 'test-peer',
content: 'Hello there',
metadata: { importance: 'high', category: 'greeting' },
});
});
it('should handle empty content', () => {
const message = peer.message('');
expect(message).toEqual({
peerId: 'test-peer',
content: '',
metadata: undefined,
});
});
});
describe('getMetadata', () => {
it('should return peer metadata', async () => {
const mockPeer = {
id: 'test-peer',
metadata: { name: 'Test Peer', role: 'assistant' },
};
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
const metadata = await peer.getMetadata();
expect(metadata).toEqual({ name: 'Test Peer', role: 'assistant' });
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
'test-workspace',
{ id: 'test-peer' }
);
});
it('should return empty object when no metadata exists', async () => {
const mockPeer = {
id: 'test-peer',
metadata: null,
};
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
const metadata = await peer.getMetadata();
expect(metadata).toEqual({});
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.getOrCreate.mockRejectedValue(new Error('Peer not found'));
await expect(peer.getMetadata()).rejects.toThrow('Peer not found');
});
});
describe('setMetadata', () => {
it('should update peer metadata', async () => {
const metadata = { name: 'Updated Peer', status: 'active' };
mockClient.workspaces.peers.update.mockResolvedValue({});
await peer.setMetadata(metadata);
expect(mockClient.workspaces.peers.update).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ metadata }
);
});
it('should handle empty metadata', async () => {
mockClient.workspaces.peers.update.mockResolvedValue({});
await peer.setMetadata({});
expect(mockClient.workspaces.peers.update).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ metadata: {} }
);
});
it('should handle complex metadata objects', async () => {
const complexMetadata = {
profile: { name: 'Complex Peer', age: 25 },
settings: { theme: 'dark', notifications: true },
tags: ['ai', 'assistant', 'helpful'],
};
mockClient.workspaces.peers.update.mockResolvedValue({});
await peer.setMetadata(complexMetadata);
expect(mockClient.workspaces.peers.update).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ metadata: complexMetadata }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.update.mockRejectedValue(new Error('Update failed'));
await expect(peer.setMetadata({ key: 'value' })).rejects.toThrow('Update failed');
});
});
describe('search', () => {
it('should search peer messages and return Page', async () => {
const mockSearchResults = {
items: [
{ id: 'msg1', content: 'Hello world', peer_id: 'test-peer' },
{ id: 'msg2', content: 'Hello there', peer_id: 'test-peer' },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.peers.search.mockResolvedValue(mockSearchResults);
const results = await peer.search('hello');
expect(results).toBeInstanceOf(Page);
expect(mockClient.workspaces.peers.search).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ body: 'hello' }
);
});
it('should handle empty search results', async () => {
const mockSearchResults = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.peers.search.mockResolvedValue(mockSearchResults);
const results = await peer.search('nonexistent');
expect(results).toBeInstanceOf(Page);
});
it('should throw error for empty query', async () => {
await expect(peer.search('')).rejects.toThrow('Search query must be a non-empty string');
await expect(peer.search(' ')).rejects.toThrow('Search query must be a non-empty string');
});
it('should throw error for non-string query', async () => {
await expect(peer.search(null as any)).rejects.toThrow('Search query must be a non-empty string');
await expect(peer.search(undefined as any)).rejects.toThrow('Search query must be a non-empty string');
await expect(peer.search(123 as any)).rejects.toThrow('Search query must be a non-empty string');
});
it('should handle complex search queries', async () => {
const mockSearchResults = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.peers.search.mockResolvedValue(mockSearchResults);
const complexQuery = 'complex query with "quotes" and special characters!@#$%';
await peer.search(complexQuery);
expect(mockClient.workspaces.peers.search).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
{ body: complexQuery }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.search.mockRejectedValue(new Error('Search failed'));
await expect(peer.search('test')).rejects.toThrow('Search failed');
});
});
});

View File

@ -0,0 +1,692 @@
import { Session } from '../src/session';
import { Peer } from '../src/peer';
import { Page } from '../src/pagination';
import { SessionContext } from '../src/session_context';
import { Honcho } from '../src/client';
// Mock the @honcho-ai/core module
jest.mock('@honcho-ai/core', () => {
return jest.fn().mockImplementation(() => ({
workspaces: {
sessions: {
peers: {
add: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
list: jest.fn(),
},
messages: {
create: jest.fn(),
list: jest.fn(),
},
getOrCreate: jest.fn(),
update: jest.fn(),
getContext: jest.fn(),
search: jest.fn(),
},
peers: {
workingRepresentation: jest.fn(),
},
},
}));
});
describe('Session', () => {
let honcho: Honcho;
let session: Session;
let mockClient: any;
beforeEach(() => {
jest.clearAllMocks();
honcho = new Honcho({
workspaceId: 'test-workspace',
apiKey: 'test-key',
environment: 'local',
});
session = new Session('test-session', honcho);
mockClient = (honcho as any)._client;
});
describe('constructor', () => {
it('should initialize with correct properties', () => {
const newSession = new Session('session-id', honcho);
expect(newSession.id).toBe('session-id');
expect(newSession['_honcho']).toBe(honcho);
});
it('should handle constructor options', () => {
const newSession = new Session('session-id', honcho, {
anonymous: true,
summarize: false
});
expect(newSession.id).toBe('session-id');
});
});
describe('addPeers', () => {
it('should add single peer by string ID', async () => {
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
await session.addPeers('peer1');
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ 'peer1': { observe_me: true, observe_others: false } }
);
});
it('should add single peer by Peer object', async () => {
const peer = new Peer('peer1', honcho);
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
await session.addPeers(peer);
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ 'peer1': { observe_me: true, observe_others: false } }
);
});
it('should add array of peer strings', async () => {
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
await session.addPeers(['peer1', 'peer2', 'peer3']);
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
'peer1': { observe_me: true, observe_others: false },
'peer2': { observe_me: true, observe_others: false },
'peer3': { observe_me: true, observe_others: false }
}
);
});
it('should add array of Peer objects', async () => {
const peers = [
new Peer('peer1', honcho),
new Peer('peer2', honcho),
new Peer('peer3', honcho),
];
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
await session.addPeers(peers);
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
'peer1': { observe_me: true, observe_others: false },
'peer2': { observe_me: true, observe_others: false },
'peer3': { observe_me: true, observe_others: false }
}
);
});
it('should add mixed array of strings and Peer objects', async () => {
const peers = [
'string-peer',
new Peer('object-peer', honcho),
];
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
await session.addPeers(peers);
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
'string-peer': { observe_me: true, observe_others: false },
'object-peer': { observe_me: true, observe_others: false }
}
);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.peers.add.mockRejectedValue(new Error('Failed to add peers'));
await expect(session.addPeers('peer1')).rejects.toThrow('Failed to add peers');
});
});
describe('setPeers', () => {
it('should set single peer by string ID', async () => {
mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
await session.setPeers('peer1');
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ 'peer1': { observe_me: true, observe_others: false } }
);
});
it('should set single peer by Peer object', async () => {
const peer = new Peer('peer1', honcho);
mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
await session.setPeers(peer);
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ 'peer1': { observe_me: true, observe_others: false } }
);
});
it('should set array of peers', async () => {
const peers = ['peer1', new Peer('peer2', honcho)];
mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
await session.setPeers(peers);
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
'peer1': { observe_me: true, observe_others: false },
'peer2': { observe_me: true, observe_others: false }
}
);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.peers.set.mockRejectedValue(new Error('Failed to set peers'));
await expect(session.setPeers(['peer1'])).rejects.toThrow('Failed to set peers');
});
});
describe('removePeers', () => {
it('should remove single peer by string ID', async () => {
mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
await session.removePeers('peer1');
expect(mockClient.workspaces.sessions.peers.remove).toHaveBeenCalledWith(
'test-workspace',
'test-session',
['peer1']
);
});
it('should remove single peer by Peer object', async () => {
const peer = new Peer('peer1', honcho);
mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
await session.removePeers(peer);
expect(mockClient.workspaces.sessions.peers.remove).toHaveBeenCalledWith(
'test-workspace',
'test-session',
['peer1']
);
});
it('should remove array of peers', async () => {
const peers = ['peer1', new Peer('peer2', honcho)];
mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
await session.removePeers(peers);
expect(mockClient.workspaces.sessions.peers.remove).toHaveBeenCalledWith(
'test-workspace',
'test-session',
['peer1', 'peer2']
);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.peers.remove.mockRejectedValue(new Error('Failed to remove peers'));
await expect(session.removePeers(['peer1'])).rejects.toThrow('Failed to remove peers');
});
});
describe('getPeers', () => {
it('should return Page of Peer instances', async () => {
const mockPeersData = {
items: [
{ id: 'peer1', metadata: {} },
{ id: 'peer2', metadata: {} },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData);
const peers = await session.getPeers();
expect(peers).toBeInstanceOf(Array);
expect(mockClient.workspaces.sessions.peers.list).toHaveBeenCalledWith(
'test-workspace',
'test-session'
);
});
it('should handle empty peers list', async () => {
const mockPeersData = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData);
const peers = await session.getPeers();
expect(peers).toBeInstanceOf(Array);
expect(peers.length).toBe(0);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.peers.list.mockRejectedValue(new Error('Failed to get peers'));
await expect(session.getPeers()).rejects.toThrow('Failed to get peers');
});
});
describe('addMessages', () => {
it('should add single message', async () => {
const message = {
peerId: 'peer1',
content: 'Hello world',
metadata: { type: 'greeting' },
};
mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
await session.addMessages(message);
expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
messages: [{
peer_id: 'peer1',
content: 'Hello world',
metadata: { type: 'greeting' }
}]
}
);
});
it('should add array of messages', async () => {
const messages = [
{ peerId: 'peer1', content: 'Message 1', metadata: { order: 1 } },
{ peerId: 'peer2', content: 'Message 2', metadata: { order: 2 } },
];
mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
await session.addMessages(messages);
expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
messages: [
{ peer_id: 'peer1', content: 'Message 1', metadata: { order: 1 } },
{ peer_id: 'peer2', content: 'Message 2', metadata: { order: 2 } },
]
}
);
});
it('should handle messages without metadata', async () => {
const message = {
peerId: 'peer1',
content: 'Simple message',
};
mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
await session.addMessages(message);
expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ messages: [{ peer_id: 'peer1', content: 'Simple message', metadata: undefined }] }
);
});
it('should handle empty array', async () => {
mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
await session.addMessages([]);
expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ messages: [] }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.messages.create.mockRejectedValue(new Error('Failed to add messages'));
await expect(session.addMessages({ peerId: 'peer1', content: 'test' })).rejects.toThrow('Failed to add messages');
});
});
describe('getMessages', () => {
it('should get messages without options', async () => {
const mockMessagesData = {
items: [
{ id: 'msg1', content: 'Message 1', peer_id: 'peer1' },
{ id: 'msg2', content: 'Message 2', peer_id: 'peer2' },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.sessions.messages.list.mockResolvedValue(mockMessagesData);
const messagesPage = await session.getMessages();
expect(messagesPage).toBeInstanceOf(Page);
expect(mockClient.workspaces.sessions.messages.list).toHaveBeenCalledWith(
'test-workspace',
'test-session',
undefined
);
});
it('should get messages with filter options', async () => {
const mockMessagesData = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.sessions.messages.list.mockResolvedValue(mockMessagesData);
const options = {
filter: { peer_id: 'peer1', type: 'important' }
};
await session.getMessages(options);
expect(mockClient.workspaces.sessions.messages.list).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ peer_id: 'peer1', type: 'important' }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.messages.list.mockRejectedValue(new Error('Failed to get messages'));
await expect(session.getMessages()).rejects.toThrow('Failed to get messages');
});
});
describe('getMetadata', () => {
it('should return session metadata', async () => {
const mockSession = {
id: 'test-session',
metadata: { name: 'Test Session', active: true },
};
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
const metadata = await session.getMetadata();
expect(metadata).toEqual({ name: 'Test Session', active: true });
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
'test-workspace',
{ id: 'test-session' }
);
});
it('should return empty object when no metadata exists', async () => {
const mockSession = {
id: 'test-session',
metadata: null,
};
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
const metadata = await session.getMetadata();
expect(metadata).toEqual({});
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.getOrCreate.mockRejectedValue(new Error('Session not found'));
await expect(session.getMetadata()).rejects.toThrow('Session not found');
});
});
describe('setMetadata', () => {
it('should update session metadata', async () => {
const metadata = { name: 'Updated Session', status: 'active' };
mockClient.workspaces.sessions.update.mockResolvedValue({});
await session.setMetadata(metadata);
expect(mockClient.workspaces.sessions.update).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ metadata }
);
});
it('should handle empty metadata', async () => {
mockClient.workspaces.sessions.update.mockResolvedValue({});
await session.setMetadata({});
expect(mockClient.workspaces.sessions.update).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ metadata: {} }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.update.mockRejectedValue(new Error('Update failed'));
await expect(session.setMetadata({ key: 'value' })).rejects.toThrow('Update failed');
});
});
describe('getContext', () => {
it('should get session context without options', async () => {
const mockContext = {
messages: [
{ id: 'msg1', content: 'Hello', peer_name: 'peer1' },
{ id: 'msg2', content: 'Hi there', peer_name: 'peer2' },
],
summary: 'Conversation summary',
};
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
const context = await session.getContext();
expect(context).toBeInstanceOf(SessionContext);
expect(context.sessionId).toBe('test-session');
expect(context.messages).toEqual(mockContext.messages);
expect(context.summary).toBe('Conversation summary');
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ tokens: undefined, summary: undefined }
);
});
it('should get session context with options', async () => {
const mockContext = {
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'peer1' }],
summary: 'Brief summary',
};
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
const options = { summary: true, tokens: 1000 };
const context = await session.getContext(options);
expect(context).toBeInstanceOf(SessionContext);
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ tokens: 1000, summary: true }
);
});
it('should handle context without summary', async () => {
const mockContext = {
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'peer1' }],
};
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
const context = await session.getContext();
expect(context.summary).toBe('');
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.getContext.mockRejectedValue(new Error('Failed to get context'));
await expect(session.getContext()).rejects.toThrow('Failed to get context');
});
});
describe('search', () => {
it('should search session messages and return Page', async () => {
const mockSearchResults = {
items: [
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
],
total: 2,
size: 2,
hasNextPage: false,
};
mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults);
const results = await session.search('hello');
expect(results).toBeInstanceOf(Page);
expect(mockClient.workspaces.sessions.search).toHaveBeenCalledWith(
'test-workspace',
'test-session',
'hello'
);
});
it('should handle empty search results', async () => {
const mockSearchResults = {
items: [],
total: 0,
size: 0,
hasNextPage: false,
};
mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults);
const results = await session.search('nonexistent');
expect(results).toBeInstanceOf(Page);
});
it('should throw error for empty query', async () => {
await expect(session.search('')).rejects.toThrow('Search query must be a non-empty string');
await expect(session.search(' ')).rejects.toThrow('Search query must be a non-empty string');
});
it('should throw error for non-string query', async () => {
await expect(session.search(null as any)).rejects.toThrow('Search query must be a non-empty string');
await expect(session.search(undefined as any)).rejects.toThrow('Search query must be a non-empty string');
await expect(session.search(123 as any)).rejects.toThrow('Search query must be a non-empty string');
});
it('should handle API errors', async () => {
mockClient.workspaces.sessions.search.mockRejectedValue(new Error('Search failed'));
await expect(session.search('test')).rejects.toThrow('Search failed');
});
});
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'],
};
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
const result = await session.workingRep('peer1');
expect(result).toEqual(mockRepresentation);
expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
'test-workspace',
'peer1',
{ session_id: 'test-session', target: undefined }
);
});
it('should get working representation with Peer object', async () => {
const peer = new Peer('peer1', honcho);
const mockRepresentation = {
peer_id: 'peer1',
knowledge: 'Some knowledge',
};
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
const result = await session.workingRep(peer);
expect(result).toEqual(mockRepresentation);
expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
'test-workspace',
'peer1',
{ session_id: 'test-session', target: undefined }
);
});
it('should get working representation with target peer string', async () => {
const mockRepresentation = {
peer_id: 'peer1',
target_knowledge: 'What peer1 knows about target',
};
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
const result = await session.workingRep('peer1', 'target-peer');
expect(result).toEqual(mockRepresentation);
expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
'test-workspace',
'peer1',
{ session_id: 'test-session', target: 'target-peer' }
);
});
it('should get working representation with target Peer object', async () => {
const peer = new Peer('peer1', honcho);
const target = new Peer('target-peer', honcho);
const mockRepresentation = {
peer_id: 'peer1',
target_knowledge: 'What peer1 knows about target',
};
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
const result = await session.workingRep(peer, target);
expect(result).toEqual(mockRepresentation);
expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
'test-workspace',
'peer1',
{ session_id: 'test-session', target: 'target-peer' }
);
});
it('should handle API errors', async () => {
mockClient.workspaces.peers.workingRepresentation.mockRejectedValue(new Error('Failed to get working representation'));
await expect(session.workingRep('peer1')).rejects.toThrow('Failed to get working representation');
});
});
});

View File

@ -0,0 +1,327 @@
import { SessionContext } from '../src/session_context';
import { Peer } from '../src/peer';
describe('SessionContext', () => {
let sessionContext: SessionContext;
let mockMessages: any[];
beforeEach(() => {
mockMessages = [
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
{ id: 'msg2', content: 'Hi there', peer_name: 'user' },
{ id: 'msg3', content: 'How are you?', peer_name: 'user' },
{ id: 'msg4', content: 'I am doing well, thank you!', peer_name: 'assistant' },
];
sessionContext = new SessionContext('test-session', mockMessages, 'This is a summary');
});
describe('constructor', () => {
it('should initialize with all properties', () => {
expect(sessionContext.sessionId).toBe('test-session');
expect(sessionContext.messages).toEqual(mockMessages);
expect(sessionContext.summary).toBe('This is a summary');
});
it('should initialize with empty summary when not provided', () => {
const context = new SessionContext('session-id', mockMessages);
expect(context.sessionId).toBe('session-id');
expect(context.messages).toEqual(mockMessages);
expect(context.summary).toBe('');
});
it('should handle empty messages array', () => {
const context = new SessionContext('session-id', [], 'No messages');
expect(context.sessionId).toBe('session-id');
expect(context.messages).toEqual([]);
expect(context.summary).toBe('No messages');
});
it('should handle null/undefined summary', () => {
const context1 = new SessionContext('session-id', mockMessages, undefined as any);
const context2 = new SessionContext('session-id', mockMessages, null as any);
expect(context1.summary).toBe('');
expect(context2.summary).toBe('');
});
});
describe('toOpenAI', () => {
it('should convert messages to OpenAI format with string assistant', () => {
const openAIMessages = sessionContext.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
it('should convert messages to OpenAI format with Peer object', () => {
const mockHoncho = {} as any;
const assistantPeer = new Peer('assistant', mockHoncho);
const openAIMessages = sessionContext.toOpenAI(assistantPeer);
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
it('should handle messages where assistant is different peer', () => {
const openAIMessages = sessionContext.toOpenAI('different-assistant');
expect(openAIMessages).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'user', content: 'I am doing well, thank you!' },
]);
});
it('should handle empty messages array', () => {
const emptyContext = new SessionContext('session-id', []);
const openAIMessages = emptyContext.toOpenAI('assistant');
expect(openAIMessages).toEqual([]);
});
it('should handle messages with missing peer_name', () => {
const messagesWithMissingPeer = [
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
{ id: 'msg2', content: 'No peer' }, // missing peer_name
{ id: 'msg3', content: 'Another message', peer_name: null },
];
const context = new SessionContext('test', messagesWithMissingPeer);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'No peer' },
{ role: 'user', content: 'Another message' },
]);
});
it('should handle complex message content', () => {
const complexMessages = [
{ id: 'msg1', content: 'Message with\nnewlines and special chars!@#$%', peer_name: 'assistant' },
{ id: 'msg2', content: '', peer_name: 'user' }, // empty content
{ id: 'msg3', content: ' whitespace ', peer_name: 'assistant' },
];
const context = new SessionContext('test', complexMessages);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Message with\nnewlines and special chars!@#$%' },
{ role: 'user', content: '' },
{ role: 'assistant', content: ' whitespace ' },
]);
});
});
describe('toAnthropic', () => {
it('should convert messages to Anthropic format with string assistant', () => {
const anthropicMessages = sessionContext.toAnthropic('assistant');
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
it('should convert messages to Anthropic format with Peer object', () => {
const mockHoncho = {} as any;
const assistantPeer = new Peer('assistant', mockHoncho);
const anthropicMessages = sessionContext.toAnthropic(assistantPeer);
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
it('should handle messages where assistant is different peer', () => {
const anthropicMessages = sessionContext.toAnthropic('different-assistant');
expect(anthropicMessages).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'user', content: 'I am doing well, thank you!' },
]);
});
it('should handle empty messages array', () => {
const emptyContext = new SessionContext('session-id', []);
const anthropicMessages = emptyContext.toAnthropic('assistant');
expect(anthropicMessages).toEqual([]);
});
it('should handle messages with missing peer_name', () => {
const messagesWithMissingPeer = [
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
{ id: 'msg2', content: 'No peer' }, // missing peer_name
{ id: 'msg3', content: 'Another message', peer_name: undefined },
];
const context = new SessionContext('test', messagesWithMissingPeer);
const anthropicMessages = context.toAnthropic('assistant');
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'No peer' },
{ role: 'user', content: 'Another message' },
]);
});
});
describe('length getter', () => {
it('should return correct message count', () => {
expect(sessionContext.length).toBe(4);
});
it('should return zero for empty messages', () => {
const emptyContext = new SessionContext('session-id', []);
expect(emptyContext.length).toBe(0);
});
it('should return correct count for single message', () => {
const singleMessageContext = new SessionContext('session-id', [mockMessages[0]]);
expect(singleMessageContext.length).toBe(1);
});
});
describe('toString', () => {
it('should return correct string representation', () => {
const result = sessionContext.toString();
expect(result).toBe('SessionContext(messages=4)');
});
it('should handle empty messages', () => {
const emptyContext = new SessionContext('session-id', []);
const result = emptyContext.toString();
expect(result).toBe('SessionContext(messages=0)');
});
it('should handle large number of messages', () => {
const manyMessages = Array.from({ length: 1000 }, (_, i) => ({
id: `msg${i}`,
content: `Message ${i}`,
peer_name: i % 2 === 0 ? 'assistant' : 'user',
}));
const context = new SessionContext('session-id', manyMessages);
const result = context.toString();
expect(result).toBe('SessionContext(messages=1000)');
});
});
describe('edge cases and error handling', () => {
it('should handle messages with null content', () => {
const messagesWithNullContent = [
{ id: 'msg1', content: null, peer_name: 'assistant' },
{ id: 'msg2', content: undefined, peer_name: 'user' },
];
const context = new SessionContext('test', messagesWithNullContent);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: null },
{ role: 'user', content: undefined },
]);
});
it('should handle messages with non-string content', () => {
const messagesWithNonStringContent = [
{ id: 'msg1', content: 123, peer_name: 'assistant' },
{ id: 'msg2', content: { text: 'object content' }, peer_name: 'user' },
{ id: 'msg3', content: true, peer_name: 'assistant' },
];
const context = new SessionContext('test', messagesWithNonStringContent);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 123 },
{ role: 'user', content: { text: 'object content' } },
{ role: 'assistant', content: true },
]);
});
it('should handle very long session IDs and summaries', () => {
const longSessionId = 'x'.repeat(1000);
const longSummary = 'Very long summary that goes on and on...'.repeat(100);
const context = new SessionContext(longSessionId, mockMessages, longSummary);
expect(context.sessionId).toBe(longSessionId);
expect(context.summary).toBe(longSummary);
expect(context.length).toBe(4);
});
it('should handle messages with additional properties', () => {
const messagesWithExtraProps = [
{
id: 'msg1',
content: 'Hello',
peer_name: 'assistant',
timestamp: '2023-01-01T00:00:00Z',
metadata: { important: true },
extra_field: 'extra_value'
},
];
const context = new SessionContext('test', messagesWithExtraProps);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
]);
});
it('should handle case-sensitive peer names', () => {
const caseMessages = [
{ id: 'msg1', content: 'Hello', peer_name: 'Assistant' },
{ id: 'msg2', content: 'Hi', peer_name: 'ASSISTANT' },
{ id: 'msg3', content: 'Hey', peer_name: 'assistant' },
];
const context = new SessionContext('test', caseMessages);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'user', content: 'Hello' }, // 'Assistant' != 'assistant'
{ role: 'user', content: 'Hi' }, // 'ASSISTANT' != 'assistant'
{ role: 'assistant', content: 'Hey' }, // exact match
]);
});
it('should handle messages without id field', () => {
const messagesWithoutId = [
{ content: 'Message without ID', peer_name: 'assistant' },
{ peer_name: 'user', content: 'Another message' },
];
const context = new SessionContext('test', messagesWithoutId);
expect(context.length).toBe(2);
expect(context.toOpenAI('assistant')).toEqual([
{ role: 'assistant', content: 'Message without ID' },
{ role: 'user', content: 'Another message' },
]);
});
});
});

View File

@ -0,0 +1,19 @@
// Global test setup
import 'jest';
// Suppress console warnings during tests unless explicitly testing them
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
beforeAll(() => {
console.warn = jest.fn();
console.error = jest.fn();
});
afterAll(() => {
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
});
// Mock environment variables for tests
process.env.NODE_ENV = 'test';

788
sdks/typescript/bun.lock Normal file
View File

@ -0,0 +1,788 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "honcho-ai",
"dependencies": {
"@honcho-ai/core": "^1.0.0",
"@types/node": "^24.0.1",
},
"devDependencies": {
"@types/jest": "^29.5.14",
"eslint": "^8.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.0",
"typescript": "^5.0.0",
},
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@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.27.5", "", {}, "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg=="],
"@babel/core": ["@babel/core@7.27.4", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.4", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.4", "@babel/types": "^7.27.3", "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-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g=="],
"@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
"@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=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
"@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-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
"@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@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=="],
"@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="],
"@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="],
"@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="],
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="],
"@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="],
"@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
"@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="],
"@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="],
"@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="],
"@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="],
"@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="],
"@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="],
"@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="],
"@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
"@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.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
"@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
"@honcho-ai/core": ["@honcho-ai/core@1.0.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-WwpKTxMhkBEpiQ2UYlM9HH+MCAkWA8o+5w/J/bYKAo9traZF4UKsO2VouWIXiLsyIvLfqzLGgGrALoH2P4h9Uw=="],
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
"@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=="],
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
"@jest/console": ["@jest/console@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0" } }, "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg=="],
"@jest/core": ["@jest/core@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.7.0", "jest-config": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-resolve-dependencies": "^29.7.0", "jest-runner": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg=="],
"@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="],
"@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="],
"@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="],
"@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="],
"@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="],
"@jest/reporters": ["@jest/reporters@29.7.0", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg=="],
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
"@jest/source-map": ["@jest/source-map@29.6.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw=="],
"@jest/test-result": ["@jest/test-result@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA=="],
"@jest/test-sequencer": ["@jest/test-sequencer@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "slash": "^3.0.0" } }, "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw=="],
"@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="],
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
"@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="],
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
"@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=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="],
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
"@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="],
"@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.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
"@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-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"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-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="],
"babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="],
"babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="],
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="],
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"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.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="],
"bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="],
"bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"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=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001723", "", {}, "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="],
"collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="],
"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=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"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=="],
"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.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.170", "", {}, "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA=="],
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
"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=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="],
"expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="],
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
"filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"form-data": ["form-data@4.0.3", "", { "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-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="],
"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=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"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-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"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=="],
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
"istanbul-lib-instrument": ["istanbul-lib-instrument@6.0.3", "", { "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" } }, "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q=="],
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
"istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="],
"istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="],
"jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="],
"jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="],
"jest-changed-files": ["jest-changed-files@29.7.0", "", { "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0" } }, "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w=="],
"jest-circus": ["jest-circus@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", "jest-each": "^29.7.0", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0", "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw=="],
"jest-cli": ["jest-cli@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", "create-jest": "^29.7.0", "exit": "^0.1.2", "import-local": "^3.0.2", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg=="],
"jest-config": ["jest-config@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-runner": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "ts-node"] }, "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ=="],
"jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="],
"jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="],
"jest-each": ["jest-each@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "jest-util": "^29.7.0", "pretty-format": "^29.7.0" } }, "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ=="],
"jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="],
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
"jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="],
"jest-leak-detector": ["jest-leak-detector@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw=="],
"jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="],
"jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="],
"jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="],
"jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" }, "optionalPeers": ["jest-resolve"] }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="],
"jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="],
"jest-resolve": ["jest-resolve@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" } }, "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA=="],
"jest-resolve-dependencies": ["jest-resolve-dependencies@29.7.0", "", { "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" } }, "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA=="],
"jest-runner": ["jest-runner@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-leak-detector": "^29.7.0", "jest-message-util": "^29.7.0", "jest-resolve": "^29.7.0", "jest-runtime": "^29.7.0", "jest-util": "^29.7.0", "jest-watcher": "^29.7.0", "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ=="],
"jest-runtime": ["jest-runtime@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ=="],
"jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="],
"jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
"jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
"jest-watcher": ["jest-watcher@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.7.0", "string-length": "^4.0.1" } }, "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g=="],
"jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
"makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"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=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"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=="],
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"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-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="],
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
"tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"ts-jest": ["ts-jest@29.4.0", "", { "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"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=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"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=="],
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
"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=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@honcho-ai/core/@types/node": ["@types/node@18.19.112", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog=="],
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"@istanbuljs/load-nyc-config/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=="],
"@istanbuljs/load-nyc-config/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="],
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"wrap-ansi/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=="],
"@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
}
}

View File

@ -0,0 +1,109 @@
import { Honcho, SessionPeerConfig } from '../src';
/**
* Example usage of the Honcho TypeScript SDK.
*
* This demonstrates how to manage peers, sessions, and messages
* using the high-level SDK API.
*/
async function main() {
console.log('Initializing Honcho client...');
const honcho = new Honcho({
environment: 'local',
workspaceId: 'test',
});
console.log('Creating peers...');
const assistant = honcho.peer('bob');
const alice = honcho.peer('alice');
console.log('Fetching all peers in workspace...');
const peers = await honcho.getPeers();
for await (const peer of peers) {
console.log('Peer:', peer.id);
}
console.log('Fetching workspace metadata...');
const m = await honcho.getMetadata();
console.log('Current metadata:', m);
await honcho.setMetadata({ test: 'test' });
console.log('Set workspace metadata.');
console.log('Testing chat endpoint (should be null)...');
const response = await alice.chat('what did alice have for breakfast today?');
console.log('Chat response:', response);
console.log('Creating session...');
const mySession = honcho.session('session_1');
console.log('Adding peers to session...');
await mySession.addPeers([alice, [assistant, new SessionPeerConfig({ observe_me: false })]]);
console.log('Peers added to session.');
console.log('Fetching sessions for alice...');
const _sessions = await alice.getSessions();
for await (const session of _sessions) {
console.log('Session:', session.id);
}
console.log('Adding messages to session...');
await mySession.addMessages([
assistant.message('what did you have for breakfast today, alice?'),
alice.message('i had oatmeal.'),
]);
console.log('Messages added.');
const sessionMetadata = await mySession.getMetadata();
console.log('Session metadata:', sessionMetadata);
await mySession.setMetadata({ ...sessionMetadata, test: 'test2' });
console.log('Session metadata updated.');
console.log('Querying alice global representation...');
await alice.chat('what did the user have for breakfast today?');
console.log('Querying alice local representation of assistant...');
await alice.chat('does alice know what bob had for breakfast?', { target: assistant });
console.log('Querying assistant local representation of alice in session...');
await assistant.chat('does the assistant know what alice had for breakfast?', {
target: alice,
sessionId: mySession.id,
});
console.log('Adding non-message content to alice...');
await alice.addMessages('this might be a document about alice, say, a journal entry.');
console.log('Creating charlie peer and adding message...');
const charlie = honcho.peer('charlie');
await mySession.addMessages(charlie.message('hello world!'));
console.log('Fetching and updating charlie metadata...');
const charlieMetadata = await charlie.getMetadata();
await charlie.setMetadata({ ...charlieMetadata, location: 'the moon' });
console.log('Charlie metadata updated.');
console.log('Querying charlie for location...');
await charlie.chat('where is the user?');
console.log('Fetching all messages from session...');
const messages = await mySession.getMessages();
console.log('Messages:', messages.total);
console.log('Fetching session context...');
const context = await mySession.getContext();
const openaiMessages = context.toOpenAI(alice.id);
const anthropicMessages = context.toAnthropic(alice.id);
console.log('OpenAI context:', openaiMessages);
console.log('Anthropic context:', anthropicMessages);
console.log('Adding test message using property syntax...');
await mySession.addMessages(
assistant.message('This is a test message using the property syntax')
);
console.log('Sample code executed successfully!');
}
main().catch((err) => {
console.error('Error running example:', err);
});

View File

@ -0,0 +1,52 @@
import { Honcho } from '../src';
/**
* Example demonstrating how to get context from a session with summary and token limits.
*
* This creates a session with random messages and retrieves context
* with a low token limit to demonstrate the summarization feature.
*/
async function main() {
console.log('Creating Honcho client...');
// Create a Honcho client with the default workspace
const honcho = new Honcho({
environment: 'local'
});
console.log('Creating peers...');
const peers = [
honcho.peer('alice'),
honcho.peer('bob'),
honcho.peer('charlie'),
];
// Create a new session
const sessionId = `context_test_${crypto.randomUUID()}`;
const session = honcho.session(sessionId);
console.log(`Created session: ${sessionId}`);
console.log('Generating random messages...');
// Generate some random messages from alice, bob, and charlie and add them to the session
const messages = [];
for (let i = 0; i < 10; i++) {
const randomPeer = peers[Math.floor(Math.random() * peers.length)];
messages.push(
randomPeer.message(`Hello from ${randomPeer.id}! This is message ${i}.`)
);
}
await session.addMessages(messages);
console.log('Added 10 random messages to session.');
console.log('Getting context with summary and low token limit...');
// Get some context of the session
// Set the token limit super low so we only get a few of the tiny messages created
const context = await session.getContext({ summary: true, tokens: 50 });
console.log('Context returned:', context);
console.log('Example completed successfully!');
}
main().catch((err) => {
console.error('Error running get_context example:', err);
});

View File

@ -0,0 +1,59 @@
import { Honcho } from '../src';
/**
* Example demonstrating how to get peer representations.
*
* This creates a session with random messages and retrieves both
* global and local representations for a peer.
*/
async function main() {
console.log('Creating Honcho client...');
// Create a Honcho client with the default workspace
const honcho = new Honcho({
environment: 'local'
});
console.log('Creating peers...');
const peers = [
honcho.peer('alice'),
honcho.peer('bob'),
honcho.peer('charlie'),
];
// Create a new session
const sessionId = `context_test_${crypto.randomUUID()}`;
const session = honcho.session(sessionId);
console.log(`Created session: ${sessionId}`);
console.log('Generating random messages...');
// Generate some random messages from alice, bob, and charlie and add them to the session
const messages = [];
for (let i = 0; i < 10; i++) {
const randomPeer = peers[Math.floor(Math.random() * peers.length)];
messages.push(
randomPeer.message(`Hello from ${randomPeer.id}! This is message ${i}.`)
);
}
await session.addMessages(messages);
console.log('Added 10 random messages to session.');
const alice = peers[0];
const bob = peers[1];
console.log('Getting alice\'s working representation in session...');
// Get alice's working representation in the session
const workingRepresentation = await session.workingRep(alice);
console.log('Working representation returned:', workingRepresentation);
console.log('Getting alice\'s working representation *of bob* in session...');
// Get alice's working representation *of bob* in the session
const workingRepresentationOfBob = await session.workingRep(alice, bob);
console.log('Working representation returned:', workingRepresentationOfBob);
console.log('Example completed successfully!');
}
main().catch((err) => {
console.error('Error running get_representation example:', err);
});

View File

@ -0,0 +1,82 @@
import { Honcho } from '../src';
/**
* Example demonstrating search functionality across different scopes.
*
* This creates sessions with special keywords and demonstrates
* searching at session, workspace, and peer levels.
*/
async function main() {
console.log('Creating Honcho client...');
// Create a Honcho client with the default workspace
const honcho = new Honcho({
environment: 'local'
});
console.log('Creating peers...');
const peers = [
honcho.peer('alice'),
honcho.peer('bob'),
honcho.peer('charlie'),
];
// Create a new session
const sessionId = `search_test_${crypto.randomUUID()}`;
const session = honcho.session(sessionId);
console.log(`Created session: ${sessionId}`);
// Create a message with our special keyword
const keyword = `~special-${crypto.randomUUID()}~`;
console.log(`Using keyword: ${keyword}`);
await session.addMessages(peers[0].message(`I am a ${keyword} message`));
console.log('Generating random messages...');
// Generate some random messages from alice, bob, and charlie and add them to the session
const messages = [];
for (let i = 0; i < 10; i++) {
const randomPeer = peers[Math.floor(Math.random() * peers.length)];
messages.push(
randomPeer.message(`Hello from ${randomPeer.id}! This is message ${i}.`)
);
}
await session.addMessages(messages);
console.log('Added random messages to session.');
console.log('Searching the session...');
// Search the session for the special keyword
const sessionSearchResults = await session.search(keyword);
console.log(`Session search returned ${sessionSearchResults.total} results:`);
for await (const message of sessionSearchResults) {
console.log(` - ${message.content} (from ${message.peer_id})`);
}
const alice = peers[0];
// Add a different message to alice's global representation
const differentKeyword = `~different-${crypto.randomUUID()}~`;
console.log(`Using different keyword: ${differentKeyword}`);
await alice.addMessages(alice.message(`I am a ${differentKeyword} message`));
console.log('Searching the workspace...');
// Search the workspace for the special keyword
const workspaceSearchResults = await honcho.search(keyword);
console.log(`Workspace search returned ${workspaceSearchResults.total} results:`);
for await (const message of workspaceSearchResults) {
console.log(` - ${message.content} (from ${message.peer_id})`);
}
console.log('Searching alice\'s global representation...');
// Search alice's global representation for the different message
const aliceSearchResults = await alice.search(differentKeyword);
console.log(`Alice search returned ${aliceSearchResults.total} results:`);
for await (const message of aliceSearchResults) {
console.log(` - ${message.content} (from ${message.peer_id})`);
}
console.log('Example completed successfully!');
}
main().catch((err) => {
console.error('Error running search example:', err);
});

View File

@ -0,0 +1,20 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/__tests__/setup.ts'],
moduleNameMapper: {
'^@honcho-ai/core$': '<rootDir>/__tests__/__mocks__/@honcho-ai/core.ts',
},
testTimeout: 10000,
};

5156
sdks/typescript/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
{
"name": "@honcho-ai/sdk",
"version": "1.1.0",
"description": "Official DX Optimized TypeScript SDK for Honcho",
"author": "Plastic Labs <hello@plasticlabs.ai>",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc --project tsconfig.json",
"lint": "eslint src --ext .ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"dependencies": {
"@types/node": "^24.0.1",
"@honcho-ai/core": "^1.1.0"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"eslint": "^8.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.0",
"typescript": "^5.0.0"
}
}

View File

@ -0,0 +1,119 @@
import HonchoCore from '@honcho-ai/core';
import { Page } from './pagination';
import { Peer } from './peer';
import { Session } from './session';
/**
* Main client for the Honcho TypeScript SDK.
* Provides access to peers, sessions, and workspace operations.
*/
export class Honcho {
private _client: InstanceType<typeof HonchoCore>;
readonly workspaceId: string;
/**
* Initialize the Honcho client.
*/
constructor(options: {
apiKey?: string;
environment?: 'local' | 'production' | 'demo';
baseURL?: string;
workspaceId?: string;
timeout?: number;
maxRetries?: number;
defaultHeaders?: Record<string, string>;
defaultQuery?: Record<string, unknown>;
}) {
this.workspaceId = options.workspaceId || process.env.HONCHO_WORKSPACE_ID || 'default';
this._client = new HonchoCore({
apiKey: options.apiKey || process.env.HONCHO_API_KEY,
environment: options.environment,
baseURL: options.baseURL || process.env.HONCHO_URL,
timeout: options.timeout,
maxRetries: options.maxRetries,
defaultHeaders: options.defaultHeaders,
defaultQuery: options.defaultQuery as any,
}) as any;
this._client.workspaces.getOrCreate({ id: this.workspaceId })
}
/**
* Get or create a peer with the given ID.
*/
peer(id: string, options?: { config?: Record<string, unknown> }): Peer {
if (!id || typeof id !== 'string') {
throw new Error('Peer ID must be a non-empty string');
}
return new Peer(id, this, options?.config);
}
/**
* Get all peers in the current workspace.
*/
async getPeers(): Promise<Page<Peer>> {
const peersPage = await this._client.workspaces.peers.list(this.workspaceId);
return new Page(peersPage, (peer: any) => new Peer(peer.id, this));
}
/**
* Get or create a session with the given ID.
*/
session(id: string, options?: { config?: Record<string, unknown> }): Session {
if (!id || typeof id !== 'string') {
throw new Error('Session ID must be a non-empty string');
}
return new Session(id, this, options?.config);
}
/**
* Get all sessions in the current workspace.
*/
async getSessions(): Promise<Page<Session>> {
const sessionsPage = await this._client.workspaces.sessions.list(this.workspaceId);
return new Page(sessionsPage, (session: any) => new Session(session.id, this));
}
/**
* Get metadata for the current workspace.
*/
async getMetadata(): Promise<Record<string, unknown>> {
const workspace = await this._client.workspaces.getOrCreate({ id: this.workspaceId });
return workspace.metadata || {};
}
/**
* Set metadata for the current workspace.
*/
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
await this._client.workspaces.update(this.workspaceId, { metadata });
}
/**
* Get all workspace IDs from the Honcho instance.
*/
async getWorkspaces(): Promise<string[]> {
const workspacesPage = await this._client.workspaces.list();
const ids: string[] = [];
for await (const workspace of workspacesPage) {
ids.push(workspace.id);
}
return ids;
}
/**
* Search for messages in the current workspace.
*
* Makes an API call to search for messages in the current workspace.
*
* @param query The search query to use
* @returns A Page of Message objects representing the search results.
* Returns an empty page if no messages are found.
*/
async search(query: string): Promise<Page<any>> {
if (!query || typeof query !== 'string' || query.trim().length === 0) {
throw new Error('Search query must be a non-empty string');
}
const messagesPage = await this._client.workspaces.search(this.workspaceId, { body: query });
return new Page(messagesPage);
}
}

View File

@ -0,0 +1,8 @@
// Main entry point for the Honcho TypeScript SDK
// Exports all main classes and types
export { Honcho } from './client';
export { Peer } from './peer';
export { Session, SessionPeerConfig } from './session';
export { SessionContext } from './session_context';
export { Page } from './pagination';

View File

@ -0,0 +1,77 @@
/**
* Generic paginated result wrapper for Honcho SDK.
* Provides async iteration and transformation capabilities.
*/
export class Page<T> implements AsyncIterable<T> {
private _originalPage: any;
private _transformFunc?: (item: any) => T;
/**
* Initialize a new Page.
*/
constructor(originalPage: any, transformFunc?: (item: any) => T) {
this._originalPage = originalPage;
this._transformFunc = transformFunc;
}
/**
* Async iterator for the page's items.
*/
async *[Symbol.asyncIterator](): AsyncIterator<T> {
// Handle different page structure formats
const items = this._originalPage.items || this._originalPage.data || [];
for (const item of items) {
yield this._transformFunc ? this._transformFunc(item) : item;
}
}
/**
* Get an item by index.
*/
async get(index: number): Promise<T> {
if (!this._originalPage?.get || typeof this._originalPage.get !== 'function') {
throw new Error('Original page does not support indexed access');
}
const item = await this._originalPage.get(index);
return this._transformFunc ? this._transformFunc(item) : item;
}
/**
* Get the size of the page.
*/
get size(): number {
return this._originalPage?.size ?? 0;
}
/**
* Get the total number of items.
*/
get total(): number {
return this._originalPage?.total ?? 0;
}
/**
* Get all data as a list.
*/
async data(): Promise<T[]> {
const items = this._originalPage.items || this._originalPage.data || [];
const data = typeof items === 'function' ? await items() : items;
return this._transformFunc ? data.map(this._transformFunc) : data;
}
/**
* Check if there's a next page.
*/
get hasNextPage(): boolean {
return this._originalPage?.hasNextPage ?? false;
}
/**
* Get the next page.
*/
async nextPage(): Promise<Page<T> | null> {
const nextPage = await this._originalPage.nextPage();
if (!nextPage) return null;
return new Page(nextPage, this._transformFunc);
}
}

152
sdks/typescript/src/peer.ts Normal file
View File

@ -0,0 +1,152 @@
import { Session } from './session';
import { Page } from './pagination';
import type { Honcho } from './client';
/**
* Represents a peer in the Honcho system.
*/
export class Peer {
/**
* Unique identifier for this peer.
*/
readonly id: string;
private _honcho: Honcho;
/**
* Initialize a new Peer.
*/
constructor(id: string, honcho: Honcho, config?: Record<string, unknown>) {
this.id = id;
this._honcho = honcho;
if (config) {
this._honcho['_client'].workspaces.peers.getOrCreate(
this._honcho.workspaceId,
{ id: this.id, configuration: config }
);
}
}
/**
* Query the peer's representation with a natural language question.
*/
async chat(queries: string | string[], opts?: {
stream?: boolean;
target?: string | Peer;
sessionId?: string;
}): Promise<string | null> {
const response = await this._honcho['_client'].workspaces.peers.chat(
this._honcho.workspaceId,
this.id,
{ queries, stream: opts?.stream, target: opts?.target ? (typeof opts.target === 'string' ? opts.target : opts.target.id) : undefined, session_id: opts?.sessionId },
);
if (!response.content || response.content === 'None') {
return null;
}
return response.content;
}
/**
* Get all sessions this peer is a member of.
*/
async getSessions(): Promise<Page<Session>> {
const sessionsPage = await this._honcho['_client'].workspaces.peers.sessions.list(
this.id,
this._honcho.workspaceId,
);
return new Page(sessionsPage, (session: any) => new Session(session.id, this._honcho));
}
/**
* Add messages or content to this peer's global representation.
*/
async addMessages(content: string | any | any[]): Promise<void> {
let messages: any[];
if (typeof content === 'string') {
messages = [{ peer_id: this.id, content, metadata: undefined }];
} else if (Array.isArray(content)) {
messages = content.map((msg) => ({
peer_id: msg.peerId || this.id,
content: msg.content,
metadata: msg.metadata,
}));
} else {
messages = [{
peer_id: content.peerId || this.id,
content: content.content,
metadata: content.metadata,
}];
}
await this._honcho['_client'].workspaces.peers.messages.create(
this._honcho.workspaceId,
this.id,
{ messages }
);
}
/**
* Get messages saved to this peer outside of a session with optional filtering.
*/
async getMessages(opts?: { filter?: Record<string, unknown> }): Promise<Page<any>> {
const messagesPage = await this._honcho['_client'].workspaces.peers.messages.list(
this.id,
this._honcho.workspaceId,
opts?.filter,
);
return new Page(messagesPage);
}
/**
* Create a message attributed to this peer.
*/
message(content: string, opts?: { metadata?: Record<string, unknown> }): any {
return {
peerId: this.id,
content,
metadata: opts?.metadata,
};
}
/**
* Get the current metadata for this peer.
*/
async getMetadata(): Promise<Record<string, unknown>> {
const peer = await this._honcho['_client'].workspaces.peers.getOrCreate(
this._honcho.workspaceId,
{ id: this.id }
);
return peer.metadata || {};
}
/**
* Set the metadata for this peer.
*/
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
await this._honcho['_client'].workspaces.peers.update(
this._honcho.workspaceId,
this.id,
{ metadata },
);
}
/**
* Search for messages in this peer's global representation.
*
* Makes an API call to search for messages in this peer's global representation.
*
* @param query The search query to use
* @returns A Page of Message objects representing the search results.
* Returns an empty page if no messages are found.
*/
async search(query: string): Promise<Page<any>> {
if (!query || typeof query !== 'string' || query.trim().length === 0) {
throw new Error('Search query must be a non-empty string');
}
const messagesPage = await this._honcho['_client'].workspaces.peers.search(
this._honcho.workspaceId,
this.id,
{ query: query }
);
return new Page(messagesPage);
}
}

View File

@ -0,0 +1,245 @@
import { Peer } from './peer';
import { Page } from './pagination';
import { SessionContext } from './session_context';
import type { Honcho } from './client';
export class SessionPeerConfig {
observe_others: boolean;
observe_me: boolean;
constructor(opts?: { observe_others?: boolean; observe_me?: boolean }) {
this.observe_others = opts?.observe_others ?? false;
this.observe_me = opts?.observe_me ?? true;
}
}
/**
* Represents a session in Honcho.
*/
export class Session {
/**
* Unique identifier for this session.
*/
readonly id: string;
private _honcho: Honcho;
/**
* Initialize a new Session.
*/
constructor(id: string, honcho: Honcho, config?: Record<string, unknown>) {
this.id = id;
this._honcho = honcho;
if (config) {
this._honcho['_client'].workspaces.sessions.getOrCreate(
this._honcho.workspaceId,
{ id: this.id, configuration: config }
);
}
}
/**
* Add peers to this session.
*/
async addPeers(peers: string | Peer | Array<string | Peer> | [string | Peer, SessionPeerConfig] | Array<[string | Peer, SessionPeerConfig]> | Array<string | Peer | [string | Peer, SessionPeerConfig]>): Promise<void> {
const peerDict: Record<string, SessionPeerConfig> = {};
if (!Array.isArray(peers)) {
peers = [peers];
}
for (const peer of peers) {
if (typeof peer === 'string') {
peerDict[peer] = { observe_others: false, observe_me: true };
} else if (typeof peer === 'object' && 'id' in peer) {
peerDict[peer.id] = { observe_others: false, observe_me: true };
} else if (Array.isArray(peer)) {
const peerId = typeof peer[0] === 'string' ? peer[0] : peer[0].id;
peerDict[peerId] = peer[1];
} else if (typeof peer === 'object' && 'id' in peer && 'observe_others' in peer && 'observe_me' in peer) {
peerDict[(peer as any).id] = { observe_others: (peer as any).observe_others, observe_me: (peer as any).observe_me };
}
}
await (this._honcho['_client'] as any).workspaces.sessions.peers.add(
this._honcho.workspaceId,
this.id,
peerDict
);
}
/**
* Set the complete peer list for this session.
*/
async setPeers(peers: string | Peer | Array<string | Peer> | [string | Peer, SessionPeerConfig] | Array<[string | Peer, SessionPeerConfig]> | Array<string | Peer | [string | Peer, SessionPeerConfig]>): Promise<void> {
const peerDict: Record<string, SessionPeerConfig> = {};
if (!Array.isArray(peers)) {
peers = [peers];
}
for (const peer of peers) {
if (typeof peer === 'string') {
peerDict[peer] = { observe_others: false, observe_me: true };
} else if (typeof peer === 'object' && 'id' in peer) {
peerDict[peer.id] = { observe_others: false, observe_me: true };
} else if (Array.isArray(peer)) {
const peerId = typeof peer[0] === 'string' ? peer[0] : peer[0].id;
peerDict[peerId] = peer[1];
} else if (typeof peer === 'object' && 'id' in peer && 'observe_others' in peer && 'observe_me' in peer) {
peerDict[(peer as any).id] = { observe_others: (peer as any).observe_others, observe_me: (peer as any).observe_me };
}
}
await (this._honcho['_client'] as any).workspaces.sessions.peers.set(
this._honcho.workspaceId,
this.id,
peerDict
);
}
/**
* Remove peers from this session.
*/
async removePeers(peers: string | Peer | Array<string | Peer>): Promise<void> {
const peerIds = Array.isArray(peers)
? peers.map((p) => (typeof p === 'string' ? p : p.id))
: [typeof peers === 'string' ? peers : peers.id];
await (this._honcho['_client'] as any).workspaces.sessions.peers.remove(this._honcho.workspaceId, this.id, peerIds);
}
/**
* Get all peers in this session. Automatically converts the paginated response
* into a list for us -- the max number of peers in a session is usually 10.
*/
async getPeers(): Promise<Peer[]> {
const peersPage = await (this._honcho['_client'] as any).workspaces.sessions.peers.list(this._honcho.workspaceId, this.id);
return peersPage.items.map((peer: any) => new Peer(peer.id, this._honcho));
}
/**
* Get the configuration for a peer in this session.
*/
async getPeerConfig(peer: string | Peer): Promise<SessionPeerConfig> {
const peerId = typeof peer === 'string' ? peer : peer.id;
return await (this._honcho['_client'] as any).workspaces.sessions.peers.getConfig(
this._honcho.workspaceId,
this.id,
peerId
);
}
/**
* Set the configuration for a peer in this session.
*/
async setPeerConfig(peer: string | Peer, config: SessionPeerConfig): Promise<void> {
const peerId = typeof peer === 'string' ? peer : peer.id;
await (this._honcho['_client'] as any).workspaces.sessions.peers.setConfig(
this._honcho.workspaceId,
this.id,
peerId,
{
observe_others: config.observe_others,
observe_me: config.observe_me
}
);
}
/**
* Add one or more messages to this session.
*/
async addMessages(messages: any | any[]): Promise<void> {
const msgs = Array.isArray(messages) ? messages : [messages];
await (this._honcho['_client'] as any).workspaces.sessions.messages.create(
this._honcho.workspaceId,
this.id,
{
messages: msgs.map((msg) => ({
peer_id: msg.peerId,
content: msg.content,
metadata: msg.metadata,
}))
}
);
}
/**
* Get messages from this session with optional filtering.
*/
async getMessages(opts?: { filter?: Record<string, unknown> }): Promise<Page<any>> {
const messagesPage = await (this._honcho['_client'] as any).workspaces.sessions.messages.list(this._honcho.workspaceId, this.id, opts?.filter);
return new Page(messagesPage);
}
/**
* Get metadata for this session.
*/
async getMetadata(): Promise<Record<string, unknown>> {
const session = await (this._honcho['_client'] as any).workspaces.sessions.getOrCreate(
this._honcho.workspaceId,
{ id: this.id }
);
return session.metadata || {};
}
/**
* Set metadata for this session.
*/
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
await (this._honcho['_client'] as any).workspaces.sessions.update(this._honcho.workspaceId, this.id, { metadata });
}
/**
* Get optimized context for this session within a token limit.
*/
async getContext(opts?: { summary?: boolean; tokens?: number }): Promise<SessionContext> {
const context = await (this._honcho['_client'] as any).workspaces.sessions.getContext(
this._honcho.workspaceId,
this.id,
{
tokens: opts?.tokens,
summary: opts?.summary
}
);
return new SessionContext(this.id, context.messages, context.summary || '');
}
/**
* Search for messages in this session.
*
* Makes an API call to search for messages in this session.
*
* @param query The search query to use
* @returns A Page of Message objects representing the search results.
* Returns an empty page if no messages are found.
*/
async search(query: string): Promise<Page<any>> {
if (!query || typeof query !== 'string' || query.trim().length === 0) {
throw new Error('Search query must be a non-empty string');
}
const messagesPage = await (this._honcho['_client'] as any).workspaces.sessions.search(
this._honcho.workspaceId,
this.id,
{ query: query }
);
return new Page(messagesPage);
}
/**
* Get the current working representation of the peer in this session.
*
* @param peer The peer to get the working representation of.
* @param target The target peer to get the representation of. If provided,
* queries what `peer` knows about the `target`.
* @returns A dictionary containing information about the peer.
*/
async workingRep(peer: string | Peer, target?: string | Peer): Promise<Record<string, unknown>> {
const peerId = typeof peer === 'string' ? peer : peer.id;
const targetId = target ? (typeof target === 'string' ? target : target.id) : undefined;
return await (this._honcho['_client'] as any).workspaces.peers.workingRepresentation(
this._honcho.workspaceId,
peerId,
{
session_id: this.id,
target: targetId
}
);
}
}

View File

@ -0,0 +1,93 @@
import { Peer } from './peer';
/**
* Represents the context of a session containing a curated list of messages.
*
* The SessionContext provides methods to convert message history into formats
* compatible with different LLM providers while staying within token limits
* and providing optimal conversation context.
*/
export class SessionContext {
/**
* ID of the session this context belongs to.
*/
readonly sessionId: string;
/**
* List of Message objects representing the conversation context.
*/
readonly messages: any[];
/**
* Summary of the session history prior to the message cutoff.
*/
readonly summary: string;
/**
* Initialize a new SessionContext.
*
* @param sessionId ID of the session this context belongs to
* @param messages List of Message objects to include in the context
* @param summary Summary of the session history prior to the message cutoff
*/
constructor(sessionId: string, messages: any[], summary: string = '') {
this.sessionId = sessionId;
this.messages = messages;
this.summary = summary || '';
}
/**
* Convert the context to OpenAI-compatible message format.
*
* Transforms the message history into the format expected by OpenAI's
* Chat Completions API, with proper role assignments based on the
* assistant's identity.
*
* @param assistant The assistant peer (Peer object or peer ID string) to use
* for determining message roles. Messages from this peer will
* be marked as "assistant", others as "user"
* @returns A list of dictionaries in OpenAI format, where each dictionary contains
* "role" and "content" keys suitable for the OpenAI API
*/
toOpenAI(assistant: string | Peer): Array<{ role: string; content: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id;
return this.messages.map((message) => ({
role: message.peer_name === assistantId ? 'assistant' : 'user',
content: message.content,
}));
}
/**
* Convert the context to Anthropic-compatible message format.
*
* Transforms the message history into the format expected by Anthropic's
* Claude API.
*
* @param assistant The assistant peer (Peer object or peer ID string) to use
* for determining message roles. Messages from this peer will
* be marked as "assistant", others as "user"
* @returns A list of dictionaries in Anthropic format, where each dictionary contains
* "role" and "content" keys suitable for the Anthropic API
*/
toAnthropic(assistant: string | Peer): Array<{ role: string; content: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id;
return this.messages.map((message) => ({
role: message.peer_name === assistantId ? 'assistant' : 'user',
content: message.content,
}));
}
/**
* Return the number of messages in the context.
*/
get length(): number {
return this.messages.length;
}
/**
* Return a string representation of the SessionContext.
*/
toString(): string {
return `SessionContext(messages=${this.messages.length})`;
}
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"lib": [
"es6",
"dom"
]
},
"include": [
"src/**/*",
],
"exclude": [
"node_modules",
"dist"
],
"baseUrl": ".",
}

View File

@ -372,8 +372,13 @@ async def get_or_create_session(
f"Cannot create session {session.name} with {len(session.peer_names)} peers. Maximum allowed is {settings.SESSION_PEERS_LIMIT} peers per session."
)
# Create honcho session
# Get or create workspace to ensure it exists
await get_or_create_workspace(
db,
schemas.WorkspaceCreate(name=workspace_name),
)
# Create honcho session
honcho_session = models.Session(
workspace_name=workspace_name,
name=session.name,
@ -931,42 +936,53 @@ async def set_peer_config(
db: AsyncSession,
workspace_name: str,
session_name: str,
peer_id: str,
peer_name: str,
config: schemas.SessionPeerConfig,
) -> None:
"""
Set the configuration for a peer in a session.
Set the configuration for a specific peer in a session.
Args:
db: Database session
workspace_name: Name of the workspace
session_name: Name of the session
peer_id: Name of the peer
config: Configuration for the peer
Returns:
True if the peer config was set successfully
Raises:
ResourceNotFoundException: If the session or peer does not exist
peer_name: Name of the peer
config: The peer configuration to set
"""
# Get row from session_peer table
stmt = select(models.SessionPeer).where(
models.SessionPeer.workspace_name == workspace_name,
models.SessionPeer.session_name == session_name,
models.SessionPeer.peer_name == peer_id,
# First, get the session and peer to ensure they exist
await get_session(db, session_name, workspace_name)
await get_peer(db, workspace_name, schemas.PeerCreate(name=peer_name))
# Check if a SessionPeer entry already exists
stmt = (
select(models.SessionPeer)
.where(models.SessionPeer.session_name == session_name)
.where(models.SessionPeer.peer_name == peer_name)
.where(models.SessionPeer.workspace_name == workspace_name)
)
result = await db.execute(stmt)
session_peer = result.scalar_one_or_none()
if session_peer is None:
raise ResourceNotFoundException(
f"Session peer {peer_id} not found in session {session_name} in workspace {workspace_name}"
)
update_data = config.model_dump(exclude_none=True)
# Update peer config
session_peer.configuration["observe_others"] = config.observe_others
session_peer.configuration["observe_me"] = config.observe_me
if session_peer:
# Update existing configuration
if session_peer.configuration:
# Create a new dictionary and update it to ensure SQLAlchemy tracks the change
new_config = session_peer.configuration.copy()
new_config.update(update_data)
session_peer.configuration = new_config
else:
session_peer.configuration = update_data
else:
# Create a new SessionPeer entry
session_peer = models.SessionPeer(
session_name=session_name,
peer_name=peer_name,
workspace_name=workspace_name,
configuration=update_data,
)
db.add(session_peer)
await db.commit()

View File

@ -321,7 +321,7 @@ async def set_peer_config(
db,
workspace_name=workspace_id,
session_name=session_id,
peer_id=peer_id,
peer_name=peer_id,
config=config,
)
logger.info(

0
tests/sdk/__init__.py Normal file
View File

72
tests/sdk/conftest.py Normal file
View File

@ -0,0 +1,72 @@
import sys
from pathlib import Path
import httpx
import pytest
import pytest_asyncio
from fastapi.testclient import TestClient
# Add the SDK src to the path to allow imports
sdk_src_path = Path(__file__).parent.parent.parent / "sdks" / "python" / "src"
sys.path.insert(0, str(sdk_src_path))
# This is a bit of a hack to make the main conftest discoverable
sys.path.insert(0, str(Path(__file__).parent.parent))
from sdks.python.src.honcho.async_client.client import AsyncHoncho # noqa: E402
from sdks.python.src.honcho.client import Honcho # noqa: E402
@pytest.fixture
def honcho_sync_test_client(client: TestClient) -> Honcho:
"""
Returns a Honcho SDK client configured to talk to the test API.
"""
http_client = httpx.Client(
transport=client._transport, # pyright: ignore
base_url=str(client.base_url),
headers=client.headers,
)
honcho_client = Honcho(
workspace_id="sdk-test-workspace-sync", http_client=http_client
)
return honcho_client
@pytest_asyncio.fixture
async def honcho_async_test_client(
client: TestClient,
) -> AsyncHoncho:
"""
Returns an async Honcho SDK client configured to talk to the test API.
"""
async_http_client = httpx.AsyncClient(
transport=httpx.ASGITransport(app=client.app),
base_url=str(client.base_url),
headers=client.headers,
)
http_client = httpx.Client(
transport=client._transport, # pyright: ignore
base_url=str(client.base_url),
headers=client.headers,
)
honcho_client = AsyncHoncho(
workspace_id="sdk-test-workspace-async",
async_http_client=async_http_client,
http_client=http_client,
)
return honcho_client
@pytest.fixture(params=["sync", "async"])
def client_fixture(
request: pytest.FixtureRequest,
honcho_sync_test_client: Honcho,
honcho_async_test_client: AsyncHoncho,
) -> tuple[Honcho | AsyncHoncho, str]:
if request.param == "sync":
return honcho_sync_test_client, "sync"
return honcho_async_test_client, "async"

View File

@ -0,0 +1,106 @@
import sys
from collections.abc import Generator
from pathlib import Path
import httpx
import pytest
from fastapi.testclient import TestClient
# Add the SDK src to the path to allow imports
sdk_src_path = Path(__file__).parent.parent.parent / "sdks" / "python" / "src"
sys.path.insert(0, str(sdk_src_path))
from sdks.python.src.honcho.client import Honcho # noqa: E402
from sdks.python.src.honcho.peer import Peer # noqa: E402
from sdks.python.src.honcho.session import Session, SessionPeerConfig # noqa: E402
@pytest.fixture
def honcho_test_client(client: TestClient) -> Generator[Honcho, None, None]:
"""
Returns a Honcho SDK client configured to talk to the test API.
"""
http_client = httpx.Client(
transport=client._transport, # pyright: ignore
base_url=str(client.base_url),
headers=client.headers,
)
honcho_client = Honcho(workspace_id="sdk-test-workspace", http_client=http_client)
yield honcho_client
def test_peer_operations(honcho_test_client: Honcho):
"""
Tests creation and metadata operations for peers.
"""
peers_page = honcho_test_client.get_peers()
assert len(list(peers_page)) == 0
peer = honcho_test_client.peer(id="test-peer-1")
assert isinstance(peer, Peer)
# Peer is created on first use
metadata = peer.get_metadata()
assert metadata == {}
peers_page = honcho_test_client.get_peers()
assert len(list(peers_page)) == 1
peer.set_metadata({"foo": "bar"})
metadata = peer.get_metadata()
assert metadata == {"foo": "bar"}
def test_session_operations(honcho_test_client: Honcho):
"""
Tests creation, peer management, and metadata for sessions.
"""
sessions_page = honcho_test_client.get_sessions()
assert len(list(sessions_page)) == 0
session = honcho_test_client.session(id="test-session-1")
assert isinstance(session, Session)
# Session is created on first use
metadata = session.get_metadata()
assert metadata == {}
sessions_page = honcho_test_client.get_sessions()
assert len(list(sessions_page)) == 1
session.set_metadata({"bar": "baz"})
metadata = session.get_metadata()
assert metadata == {"bar": "baz"}
assistant = honcho_test_client.peer(id="assistant")
user = honcho_test_client.peer(id="user")
session.add_peers(
[assistant, (user, SessionPeerConfig(observe_others=False, observe_me=False))]
)
session_peers = session.get_peers()
assert len(session_peers) == 2
def test_message_and_chat_operations(honcho_test_client: Honcho):
"""
Tests adding messages to a session and using the chat functionality.
"""
session = honcho_test_client.session(id="test-chat-session")
assistant = honcho_test_client.peer(id="chat-assistant")
user = honcho_test_client.peer(id="chat-user")
session.add_messages(
[
user.message("What is the capital of France?"),
assistant.message("The capital of France is Paris."),
]
)
messages = session.get_messages()
assert len(list(messages)) == 2
# This is a mock response from the agent
_response = user.chat("What did I ask about?")

186
tests/sdk/test_client.py Normal file
View File

@ -0,0 +1,186 @@
import pytest
from fastapi.testclient import TestClient
from sdks.python.src.honcho.async_client.client import AsyncHoncho
from sdks.python.src.honcho.async_client.pagination import AsyncPage
from sdks.python.src.honcho.async_client.peer import AsyncPeer
from sdks.python.src.honcho.async_client.session import AsyncSession
from sdks.python.src.honcho.client import Honcho
from sdks.python.src.honcho.pagination import SyncPage
from sdks.python.src.honcho.peer import Peer
from sdks.python.src.honcho.session import Session
@pytest.mark.asyncio
async def test_client_init(
client_fixture: tuple[Honcho | AsyncHoncho, str], client: TestClient
):
"""
Tests that the Honcho SDK clients can be initialized and that they create a workspace.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
assert honcho_client.workspace_id == "sdk-test-workspace-async"
else:
assert isinstance(honcho_client, Honcho)
assert honcho_client.workspace_id == "sdk-test-workspace-sync"
# Check all pages to find the workspace
found_workspace = False
page = 1
while not found_workspace:
res = client.post("/v2/workspaces/list", json={}, params={"page": page})
assert res.status_code == 200
data = res.json()
workspaces = data["items"]
workspace_ids = [w["id"] for w in workspaces]
if honcho_client.workspace_id in workspace_ids:
found_workspace = True
break
# Check if there are more pages
if page >= data.get("pages", 1) or len(workspaces) == 0:
break
page += 1
assert found_workspace, (
f"Workspace {honcho_client.workspace_id} not found in any page of results"
)
@pytest.mark.asyncio
async def test_workspace_metadata(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests getting and setting metadata on a workspace.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
metadata = await honcho_client.get_metadata()
assert metadata == {}
await honcho_client.set_metadata({"foo": "bar"})
metadata = await honcho_client.get_metadata()
assert metadata == {"foo": "bar"}
else:
assert isinstance(honcho_client, Honcho)
metadata = honcho_client.get_metadata()
assert metadata == {}
honcho_client.set_metadata({"foo": "bar"})
metadata = honcho_client.get_metadata()
assert metadata == {"foo": "bar"}
@pytest.mark.asyncio
async def test_get_workspaces(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests listing available workspaces.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
workspaces = await honcho_client.get_workspaces()
else:
assert isinstance(honcho_client, Honcho)
workspaces = honcho_client.get_workspaces()
assert isinstance(workspaces, list)
assert honcho_client.workspace_id in workspaces
@pytest.mark.asyncio
async def test_client_list_peers_and_sessions(
client_fixture: tuple[Honcho | AsyncHoncho, str],
):
"""
Tests listing peers and sessions at the client level.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
peers_page = await honcho_client.get_peers()
assert isinstance(peers_page, AsyncPage)
assert len(peers_page.items) == 0
sessions_page = await honcho_client.get_sessions()
assert isinstance(sessions_page, AsyncPage)
assert len(sessions_page.items) == 0
peer = await honcho_client.peer(id="test-peer-client")
assert isinstance(peer, AsyncPeer)
await peer.get_metadata() # Creates the peer
peers_page = await honcho_client.get_peers()
assert len(peers_page.items) == 1
session = await honcho_client.session(id="test-session-client")
assert isinstance(session, AsyncSession)
await session.get_metadata() # Creates the session
sessions_page = await honcho_client.get_sessions()
assert len(sessions_page.items) == 1
else:
assert isinstance(honcho_client, Honcho)
peers_page = honcho_client.get_peers()
assert isinstance(peers_page, SyncPage)
assert len(list(peers_page)) == 0
sessions_page = honcho_client.get_sessions()
assert isinstance(sessions_page, SyncPage)
assert len(list(sessions_page)) == 0
peer = honcho_client.peer(id="test-peer-client")
assert isinstance(peer, Peer)
peer.get_metadata()
peers_page = honcho_client.get_peers()
assert len(list(peers_page)) == 1
session = honcho_client.session(id="test-session-client")
assert isinstance(session, Session)
session.get_metadata()
sessions_page = honcho_client.get_sessions()
assert len(list(sessions_page)) == 1
@pytest.mark.asyncio
async def test_workspace_search(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests searching for messages within a workspace.
"""
honcho_client, client_type = client_fixture
search_query = "a unique message for workspace search"
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="search-session-ws")
assert isinstance(session, AsyncSession)
user = await honcho_client.peer(id="search-user-ws")
assert isinstance(user, AsyncPeer)
await session.add_messages([user.message(search_query)])
search_results = await honcho_client.search(search_query)
results = search_results.items
assert len(results) >= 1
assert search_query in results[0].content
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="search-session-ws")
assert isinstance(session, Session)
user = honcho_client.peer(id="search-user-ws")
assert isinstance(user, Peer)
session.add_messages([user.message(search_query)])
search_results = honcho_client.search(search_query)
results = list(search_results)
assert len(results) >= 1
assert search_query in results[0].content

197
tests/sdk/test_peer.py Normal file
View File

@ -0,0 +1,197 @@
import pytest
from sdks.python.src.honcho.async_client.client import AsyncHoncho
from sdks.python.src.honcho.async_client.peer import AsyncPeer
from sdks.python.src.honcho.client import Honcho
from sdks.python.src.honcho.peer import Peer
@pytest.mark.asyncio
async def test_peer_metadata(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests creation and metadata operations for peers.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
peer = await honcho_client.peer(id="test-peer-meta")
assert isinstance(peer, AsyncPeer)
metadata = await peer.get_metadata()
assert metadata == {}
await peer.set_metadata({"foo": "bar"})
metadata = await peer.get_metadata()
assert metadata == {"foo": "bar"}
else:
assert isinstance(honcho_client, Honcho)
peer = honcho_client.peer(id="test-peer-meta")
assert isinstance(peer, Peer)
metadata = peer.get_metadata()
assert metadata == {}
peer.set_metadata({"foo": "bar"})
metadata = peer.get_metadata()
assert metadata == {"foo": "bar"}
@pytest.mark.asyncio
async def test_peer_add_and_get_messages(
client_fixture: tuple[Honcho | AsyncHoncho, str],
):
"""
Tests adding and getting messages from a peer's global representation.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
peer = await honcho_client.peer(id="test-peer-gms")
assert isinstance(peer, AsyncPeer)
await peer.add_messages("a simple string message")
message_obj = peer.message("a message object")
await peer.add_messages(message_obj)
await peer.add_messages([peer.message("a message in a list")])
messages_page = await peer.get_messages()
messages = messages_page.items
assert len(messages) == 3
contents = {m.content for m in messages}
assert "a simple string message" in contents
assert "a message object" in contents
assert "a message in a list" in contents
else:
assert isinstance(honcho_client, Honcho)
peer = honcho_client.peer(id="test-peer-gms")
assert isinstance(peer, Peer)
peer.add_messages("a simple string message")
message_obj = peer.message("a message object")
peer.add_messages(message_obj)
peer.add_messages([peer.message("a message in a list")])
messages_page = peer.get_messages()
messages = list(messages_page)
assert len(messages) == 3
contents = {m.content for m in messages}
assert "a simple string message" in contents
assert "a message object" in contents
assert "a message in a list" in contents
@pytest.mark.asyncio
async def test_peer_chat(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests the chat functionality of a peer, including target and session scoping.
"""
honcho_client, client_type = client_fixture
question = "What is my name?"
answer = "Your name is test-peer-chat"
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
peer = await honcho_client.peer(id="test-peer-chat")
assert isinstance(peer, AsyncPeer)
await peer.add_messages(answer)
_response = await peer.chat(question)
# Test target
target_peer = await honcho_client.peer(id="target-peer-chat")
_response = await peer.chat(
"Does the assistant know my name?", target=target_peer
)
# Test session_id
session = await honcho_client.session(id="chat-session-scope")
await session.add_messages(peer.message(answer))
_response = await peer.chat(question, session_id=session.id)
else:
assert isinstance(honcho_client, Honcho)
peer = honcho_client.peer(id="test-peer-chat")
assert isinstance(peer, Peer)
peer.add_messages(answer)
_response = peer.chat(question)
# Test target
target_peer = honcho_client.peer(id="target-peer-chat")
_response = peer.chat("Does the assistant know my name?", target=target_peer)
# Test session_id
session = honcho_client.session(id="chat-session-scope")
session.add_messages(peer.message(answer))
_response = peer.chat(question, session_id=session.id)
@pytest.mark.asyncio
async def test_peer_get_sessions(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests retrieving the sessions a peer is a member of.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
peer = await honcho_client.peer(id="test-peer-sessions")
session1 = await honcho_client.session(id="s1")
session2 = await honcho_client.session(id="s2")
await session1.add_peers(peer)
await session2.add_peers(peer)
sessions_page = await peer.get_sessions()
sessions = sessions_page.items
assert len(sessions) == 2
session_ids = {s.id for s in sessions}
assert "s1" in session_ids
assert "s2" in session_ids
else:
assert isinstance(honcho_client, Honcho)
peer = honcho_client.peer(id="test-peer-sessions")
session1 = honcho_client.session(id="s1")
session2 = honcho_client.session(id="s2")
session1.add_peers(peer)
session2.add_peers(peer)
sessions_page = peer.get_sessions()
sessions = list(sessions_page)
assert len(sessions) == 2
session_ids = {s.id for s in sessions}
assert "s1" in session_ids
assert "s2" in session_ids
@pytest.mark.asyncio
async def test_peer_search(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests searching for messages in a peer's global representation.
"""
honcho_client, client_type = client_fixture
search_query = "a unique message for peer search"
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
peer = await honcho_client.peer(id="search-peer")
assert isinstance(peer, AsyncPeer)
await peer.add_messages(search_query)
search_results = await peer.search(search_query)
results = search_results.items
assert len(results) >= 1
assert search_query in results[0].content
else:
assert isinstance(honcho_client, Honcho)
peer = honcho_client.peer(id="search-peer")
assert isinstance(peer, Peer)
peer.add_messages(search_query)
search_results = peer.search(search_query)
results = list(search_results)
assert len(results) >= 1
assert search_query in results[0].content

299
tests/sdk/test_session.py Normal file
View File

@ -0,0 +1,299 @@
import pytest
from sdks.python.src.honcho.async_client.client import AsyncHoncho
from sdks.python.src.honcho.async_client.peer import AsyncPeer
from sdks.python.src.honcho.async_client.session import (
AsyncSession,
)
from sdks.python.src.honcho.async_client.session import (
SessionPeerConfig as AsyncSessionPeerConfig,
)
from sdks.python.src.honcho.client import Honcho
from sdks.python.src.honcho.peer import Peer
from sdks.python.src.honcho.session import Session, SessionPeerConfig
@pytest.mark.asyncio
async def test_session_metadata(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests creation and metadata operations for sessions.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="test-session-meta")
assert isinstance(session, AsyncSession)
metadata = await session.get_metadata()
assert metadata == {}
await session.set_metadata({"foo": "bar"})
metadata = await session.get_metadata()
assert metadata == {"foo": "bar"}
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="test-session-meta")
assert isinstance(session, Session)
metadata = session.get_metadata()
assert metadata == {}
session.set_metadata({"foo": "bar"})
metadata = session.get_metadata()
assert metadata == {"foo": "bar"}
@pytest.mark.asyncio
async def test_session_peer_management(
client_fixture: tuple[Honcho | AsyncHoncho, str],
):
"""
Tests adding, setting, getting, and removing peers from a session.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="test-session-peers")
assert isinstance(session, AsyncSession)
peer1 = await honcho_client.peer(id="p1")
assert isinstance(peer1, AsyncPeer)
peer2 = await honcho_client.peer(id="p2")
assert isinstance(peer2, AsyncPeer)
peer3 = await honcho_client.peer(id="p3")
assert isinstance(peer3, AsyncPeer)
await session.add_peers([peer1, peer2])
peers = await session.get_peers()
assert len(peers) == 2
peer_ids = {p.id for p in peers}
assert "p1" in peer_ids and "p2" in peer_ids
await session.set_peers([peer2, peer3])
peers = await session.get_peers()
assert len(peers) == 2
peer_ids = {p.id for p in peers}
assert "p2" in peer_ids and "p3" in peer_ids
await session.remove_peers([peer2])
peers = await session.get_peers()
assert len(peers) == 1
assert peers[0].id == "p3"
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="test-session-peers")
assert isinstance(session, Session)
peer1 = honcho_client.peer(id="p1")
assert isinstance(peer1, Peer)
peer2 = honcho_client.peer(id="p2")
assert isinstance(peer2, Peer)
peer3 = honcho_client.peer(id="p3")
assert isinstance(peer3, Peer)
session.add_peers([peer1, peer2])
peers = session.get_peers()
assert len(peers) == 2
peer_ids = {p.id for p in peers}
assert "p1" in peer_ids and "p2" in peer_ids
session.set_peers([peer2, peer3])
peers = session.get_peers()
assert len(peers) == 2
peer_ids = {p.id for p in peers}
assert "p2" in peer_ids and "p3" in peer_ids
session.remove_peers([peer2])
peers = session.get_peers()
assert len(peers) == 1
assert peers[0].id == "p3"
@pytest.mark.asyncio
async def test_session_peer_config(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests getting and setting peer configurations in a session.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
config = AsyncSessionPeerConfig(observe_others=False, observe_me=False)
session = await honcho_client.session(id="test-session-config")
assert isinstance(session, AsyncSession)
peer = await honcho_client.peer(id="p-config")
assert isinstance(peer, AsyncPeer)
await session.add_peers([(peer, config)])
retrieved_config = await session.get_peer_config(peer)
assert retrieved_config.observe_me is False
assert retrieved_config.observe_others is False
await session.set_peer_config(
peer, AsyncSessionPeerConfig(observe_others=True, observe_me=True)
)
retrieved_config = await session.get_peer_config(peer)
assert retrieved_config.observe_me is True
assert retrieved_config.observe_others is True
else:
assert isinstance(honcho_client, Honcho)
config = SessionPeerConfig(observe_others=False, observe_me=False)
session = honcho_client.session(id="test-session-config")
assert isinstance(session, Session)
peer = honcho_client.peer(id="p-config")
assert isinstance(peer, Peer)
session.add_peers([(peer, config)])
retrieved_config = session.get_peer_config(peer)
assert retrieved_config.observe_me is False
assert retrieved_config.observe_others is False
session.set_peer_config(
peer, SessionPeerConfig(observe_others=True, observe_me=True)
)
retrieved_config = session.get_peer_config(peer)
assert retrieved_config.observe_me
assert retrieved_config.observe_others
@pytest.mark.asyncio
async def test_session_messages(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests adding and getting messages from a session.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="test-session-msg")
assert isinstance(session, AsyncSession)
user = await honcho_client.peer(id="user-msg")
assert isinstance(user, AsyncPeer)
assistant = await honcho_client.peer(id="assistant-msg")
assert isinstance(assistant, AsyncPeer)
await session.add_messages(
[
user.message("Hello assistant"),
assistant.message("Hello user"),
]
)
messages_page = await session.get_messages()
messages = messages_page.items
assert len(messages) == 2
messages_page = await session.get_messages(filters={"peer_id": user.id})
messages = messages_page.items
assert len(messages) == 1
assert messages[0].content == "Hello assistant"
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="test-session-msg")
assert isinstance(session, Session)
user = honcho_client.peer(id="user-msg")
assert isinstance(user, Peer)
assistant = honcho_client.peer(id="assistant-msg")
assert isinstance(assistant, Peer)
session.add_messages(
[
user.message("Hello assistant"),
assistant.message("Hello user"),
]
)
messages_page = session.get_messages()
messages = list(messages_page)
assert len(messages) == 2
messages_page = session.get_messages(filters={"peer_id": user.id})
messages = list(messages_page)
assert len(messages) == 1
assert messages[0].content == "Hello assistant"
@pytest.mark.asyncio
async def test_session_get_context(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests getting the context of a session.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="test-session-ctx")
assert isinstance(session, AsyncSession)
user = await honcho_client.peer(id="user-ctx")
assert isinstance(user, AsyncPeer)
await session.add_messages([user.message("This is a context test.")])
context = await session.get_context()
assert len(context.messages) == 1
assert "context test" in context.messages[0].content
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="test-session-ctx")
assert isinstance(session, Session)
user = honcho_client.peer(id="user-ctx")
assert isinstance(user, Peer)
session.add_messages([user.message("This is a context test.")])
context = session.get_context()
assert len(context.messages) == 1
assert "context test" in context.messages[0].content
@pytest.mark.asyncio
async def test_session_search(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests searching for messages in a session.
"""
honcho_client, client_type = client_fixture
search_query = "a unique message for session search"
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="search-session-s")
assert isinstance(session, AsyncSession)
user = await honcho_client.peer(id="search-user-s")
assert isinstance(user, AsyncPeer)
await session.add_messages([user.message(search_query)])
search_results = await session.search(search_query)
results = search_results.items
assert len(results) >= 1
assert search_query in results[0].content
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="search-session-s")
assert isinstance(session, Session)
user = honcho_client.peer(id="search-user-s")
assert isinstance(user, Peer)
session.add_messages([user.message(search_query)])
search_results = session.search(search_query)
results = list(search_results)
assert len(results) >= 1
assert search_query in results[0].content
@pytest.mark.asyncio
async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, str]):
"""
Tests getting the working representation of a peer in a session.
"""
honcho_client, client_type = client_fixture
if client_type == "async":
assert isinstance(honcho_client, AsyncHoncho)
session = await honcho_client.session(id="test-session-wr")
assert isinstance(session, AsyncSession)
peer = await honcho_client.peer(id="peer-wr")
assert isinstance(peer, AsyncPeer)
await session.add_messages([peer.message("test message for working rep")])
_working_rep = await session.working_rep(peer)
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="test-session-wr")
assert isinstance(session, Session)
peer = honcho_client.peer(id="peer-wr")
assert isinstance(peer, Peer)
session.add_messages([peer.message("test message for working rep")])
_working_rep = session.working_rep(peer)

25
uv.lock
View File

@ -505,6 +505,7 @@ dependencies = [
dev = [
{ name = "basedpyright" },
{ name = "coverage" },
{ name = "honcho-core" },
{ name = "interrogate" },
{ name = "py-spy" },
{ name = "pytest" },
@ -539,6 +540,7 @@ requires-dist = [
dev = [
{ name = "basedpyright", specifier = ">=1.29.4" },
{ name = "coverage", specifier = ">=7.6.0" },
{ name = "honcho-core", specifier = ">=1.1.0" },
{ name = "interrogate", specifier = ">=1.7.0" },
{ name = "py-spy", specifier = ">=0.3.14" },
{ name = "pytest", specifier = ">=8.2.2" },
@ -547,6 +549,23 @@ dev = [
{ name = "sqlalchemy-utils", specifier = ">=0.41.2" },
]
[[package]]
name = "honcho-core"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/2f/27739b0d8950da05743cba5085be66f1184195c07ba9b852988d45ba252d/honcho_core-1.1.0.tar.gz", hash = "sha256:d31dc932573b771056952d234cf0c615a4ca591a11eb29c543e9a2277fcc0926", size = 121388, upload-time = "2025-06-26T19:31:00.814Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/14/c2bc7dce35a76a7d89166c55f35f2be21fdb3a94ee3f6502aa83f399bb1a/honcho_core-1.1.0-py3-none-any.whl", hash = "sha256:5716aa572cf33416d1c2c475dbd714879e9d351f2e2b1c6c37751b2c91adc6ce", size = 112730, upload-time = "2025-06-26T19:30:59.455Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
@ -1024,7 +1043,7 @@ wheels = [
[[package]]
name = "openai"
version = "1.92.0"
version = "1.92.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -1036,9 +1055,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/37/3f/75ad8dfe1ee4e6daacebc5a8145878c00b22c4b89e3446228d3ffccbf726/openai-1.92.0.tar.gz", hash = "sha256:acaf4ee5fca8611a09035e37ceb69a352b4a84e25b103f0fc1ccc696bf9a16f0", size = 485264, upload-time = "2025-06-26T16:57:18.996Z" }
sdist = { url = "https://files.pythonhosted.org/packages/11/44/9dba6f521591c49d3ba70b6800b32cb3f8feec89648b27fcce766046c57c/openai-1.92.2.tar.gz", hash = "sha256:b571a79fc7e165e7d00e6963a8a95eb5f42b60ac89fd316f1dc0a2dac5c6fae1", size = 485428, upload-time = "2025-06-26T19:38:01.228Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/66/8f85019b7943e4976d931fdb0e2b973090b4dac999c3e90844d2336f1184/openai-1.92.0-py3-none-any.whl", hash = "sha256:99210715bad1a4de5a387993aca7e4e8e5a742f1e185eefe2f805001cc2c21f3", size = 753397, upload-time = "2025-06-26T16:57:17.038Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e4/e558d3049feedab15fabf3cb704837b21c8194c133b9fb9fe56b8b82ca30/openai-1.92.2-py3-none-any.whl", hash = "sha256:abb64bee7f2571709edf9a856f598ffe871730129a7d807a8a4d8d2958f5c842", size = 753297, upload-time = "2025-06-26T19:37:59.35Z" },
]
[[package]]