feat: Add get summaries endpoints & Custom Timestamps (#185)
* feat: add support for custom message timestamps in API - Introduced `created_at` parameter for message creation, allowing users to specify custom timestamps. - **Single source of truth for timestamp string format** - Updated SDK documentation to reflect this new feature and its use cases. - Enhanced validation schemas to include the optional `created_at` field. - Added tests to verify functionality for messages with and without custom timestamps, ensuring correct behavior and default timestamp usage. * feat: add timestamp option to sdks * feat: Add get summaries endpoints * feat: WIP basic SDK implementation blocked until stainless release * feat: Implement SDKs with honcho-core methods * fix (sdk): Used release 1.4.0 core sdks * fix: Code Rabbit * chore: Pytest errors --------- Co-authored-by: Benjamin McCormick <docterformer@protonmail.com>
This commit is contained in:
parent
0838a281c1
commit
e914dbe334
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -5,7 +5,23 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [2.2.0] - 2025-08-07
|
||||
## [2.3.0] — 2025-08-14
|
||||
|
||||
### Added
|
||||
|
||||
- `getSummaries` endpoint to get all available summaries for a session directly
|
||||
|
||||
### Changed
|
||||
|
||||
- Session Peer limit to be based on observers instead, renamed config value to
|
||||
`SESSION_OBSERVERS_LIMIT`
|
||||
- Deriver uses `get_context` internally to prevent context window limit errors
|
||||
- `Messages` can take a custom timestamp for the `created_at` field, defaulting
|
||||
to the current time
|
||||
- `get_context` endpoint returns detailed `Summary` object rather than just
|
||||
summary content
|
||||
|
||||
## [2.2.0] — 2025-08-07
|
||||
|
||||
### Added
|
||||
|
||||
|
|
@ -28,14 +44,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- Re-introduced Sentry tracing to deriver
|
||||
- Better integrity logic in get_or_create API methods
|
||||
|
||||
## [2.1.2] - 2025-07-30
|
||||
## [2.1.2] — 2025-07-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Summarizer module to ignore empty summaries and pass appropriate one to get_context
|
||||
- Structured Outputs calls with OpenAI provider to pass strict=True to Pydantic Schema
|
||||
|
||||
## [2.1.1] - 2025-07-23
|
||||
## [2.1.1] — 2025-07-23
|
||||
|
||||
### Added
|
||||
|
||||
|
|
@ -58,7 +74,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- Refactor summarization for performance and code clarity
|
||||
- Refactor queue payloads for clarity
|
||||
|
||||
## [2.1.0] - 2025-07-17
|
||||
## [2.1.0] — 2025-07-17
|
||||
|
||||
### Added
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🫡 Honcho
|
||||
|
||||

|
||||

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

|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
## Version Compatibility
|
||||
|
||||
### Honcho API v2.2.0 (Current)
|
||||
### Honcho API v2.3.0 (Current)
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Compatible Version:** v1.3.0
|
||||
**Compatible Version:** v1.4.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install @honcho-ai/sdk@1.3.0
|
||||
npm install @honcho-ai/sdk@1.4.0
|
||||
```
|
||||
</Card>
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Compatible Version:** v1.3.0
|
||||
**Compatible Version:** v1.4.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
pip install honcho-ai==1.3.0
|
||||
pip install honcho-ai==1.4.0
|
||||
```
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
@ -33,20 +33,20 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Compatible Version:** v1.2.1
|
||||
**Compatible Version:** v1.4.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install @honcho-ai/sdk@1.2.1
|
||||
npm install @honcho-ai/sdk@1.4.0
|
||||
```
|
||||
</Card>
|
||||
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Compatible Version:** v1.2.2
|
||||
**Compatible Version:** v1.4.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
pip install honcho-ai==1.2.2
|
||||
pip install honcho-ai==1.4.0
|
||||
```
|
||||
</Card>
|
||||
|
||||
|
|
@ -56,7 +56,8 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
| Honcho API Version | TypeScript SDK | Python SDK |
|
||||
|-------------------|---------------|------------|
|
||||
| v2.2.0 (Current) | v1.3.0 | v1.3.0 |
|
||||
| v2.3.0 (Current) | v1.4.0 | v1.4.0 |
|
||||
| v2.2.0 | v1.3.0 | v1.3.0 |
|
||||
| v2.1.2 | v1.2.1 | v1.2.2 |
|
||||
| v2.1.1 | v1.2.1 | v1.2.2 |
|
||||
| v2.1.0 | v1.2.1 | v1.2.2 |
|
||||
|
|
|
|||
|
|
@ -27,7 +27,23 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
### Honcho API and SDK Changelogs
|
||||
<Tabs>
|
||||
<Tab title="Honcho API">
|
||||
<Update label="v2.2.0 (Current)">
|
||||
<Update label="v2.3.0 (Current)">
|
||||
### Added
|
||||
|
||||
- `getSummaries` endpoint to get all available summaries for a session directly
|
||||
|
||||
### Changed
|
||||
|
||||
- Session Peer limit to be based on observers instead, renamed config value to
|
||||
`SESSION_OBSERVERS_LIMIT`
|
||||
- Deriver uses `get_context` internally to prevent context window limit errors
|
||||
- `Messages` can take a custom timestamp for the `created_at` field, defaulting
|
||||
to the current time
|
||||
- `get_context` endpoint returns detailed `Summary` object rather than just
|
||||
summary content
|
||||
</Update>
|
||||
|
||||
<Update label="v2.2.0">
|
||||
### Added
|
||||
|
||||
- Arbitrary filters now available on all search endpoints
|
||||
|
|
@ -239,6 +255,16 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
|
||||
<Tab title="Python SDK">
|
||||
[Python SDK](https://pypi.org/project/honcho-ai/)
|
||||
<Update label="v1.4.0 (Current)">
|
||||
### Added
|
||||
|
||||
- getSummaries API returning structured summaries
|
||||
|
||||
### Changed
|
||||
|
||||
- Messages can take an optional `created_at` value, defaulting to the current
|
||||
time (UTC ISO 8601)
|
||||
</Update>
|
||||
<Update label="v1.2.2">
|
||||
### Added
|
||||
|
||||
|
|
@ -275,6 +301,16 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
|
||||
<Tab title="TypeScript SDK">
|
||||
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
|
||||
<Update label="v1.4.0 (Current)">
|
||||
### Added
|
||||
|
||||
- getSummaries API returning structured summaries
|
||||
|
||||
### Changed
|
||||
|
||||
- Messages can take an optional `created_at` value, defaulting to the current
|
||||
time (UTC ISO 8601)
|
||||
</Update>
|
||||
<Update label="v1.2.1">
|
||||
### Added
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"navigation": {
|
||||
"versions": [
|
||||
{
|
||||
"version": "v2.2.0",
|
||||
"version": "v2.3.0",
|
||||
"api": {
|
||||
"openapi": ["openapi.documented.yml"]
|
||||
},
|
||||
|
|
@ -131,6 +131,7 @@
|
|||
"v2/api-reference/endpoint/sessions/get-peer-config",
|
||||
"v2/api-reference/endpoint/sessions/set-peer-config",
|
||||
"v2/api-reference/endpoint/sessions/get-session-context",
|
||||
"v2/api-reference/endpoint/sessions/get-session-summaries",
|
||||
"v2/api-reference/endpoint/sessions/search-session"
|
||||
]
|
||||
},
|
||||
|
|
@ -141,12 +142,21 @@
|
|||
"v2/api-reference/endpoint/messages/get-messages",
|
||||
"v2/api-reference/endpoint/messages/get-message",
|
||||
"v2/api-reference/endpoint/messages/update-message",
|
||||
"v2/api-reference/endpoint/messages/upload-file"
|
||||
"v2/api-reference/endpoint/messages/create-messages-with-file"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "keys",
|
||||
"pages": ["v2/api-reference/endpoint/keys/create-key"]
|
||||
},
|
||||
{
|
||||
"group": "webhooks",
|
||||
"pages": [
|
||||
"v2/api-reference/endpoint/webhooks/list-webhook-endpoints",
|
||||
"v2/api-reference/endpoint/webhooks/get-or-create-webhook-endpoint",
|
||||
"v2/api-reference/endpoint/webhooks/delete-webhook-endpoint",
|
||||
"v2/api-reference/endpoint/webhooks/test-emit"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"main": ".pnp.js",
|
||||
"scripts": {
|
||||
"dev": "mint dev",
|
||||
"openapi": "npx @mintlify/scraping openapi-file openapi.json -o api-reference/endpoint",
|
||||
"openapi": "npx @mintlify/scraping openapi-file openapi.documented.yml -o api-reference/endpoint",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v2/workspaces/{workspace_id}/sessions/{session_id}/summaries
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: delete /v2/workspaces/{workspace_id}/webhooks/{endpoint_id}
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/webhooks
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v2/workspaces/{workspace_id}/webhooks
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v2/workspaces/{workspace_id}/webhooks/test
|
||||
---
|
||||
|
|
@ -555,6 +555,33 @@ const response = await openai.chat.completions.create({
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Custom Message Timestamps
|
||||
|
||||
When creating messages, you can optionally specify a custom `created_at` timestamp instead of using the server's current time:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.honcho.dev/v2/workspaces/{workspace_id}/sessions/{session_id}/messages" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{
|
||||
"peer_id": "user123",
|
||||
"content": "This message happened yesterday",
|
||||
"created_at": "2024-01-01T12:00:00Z",
|
||||
"metadata": {"source": "historical_data"}
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- Importing historical conversation data
|
||||
- Backfilling messages from other systems
|
||||
- Maintaining accurate timeline ordering when processing batch data
|
||||
|
||||
If `created_at` is not provided, messages will use the server's current timestamp.
|
||||
|
||||
### Metadata and Filtering
|
||||
|
||||
See [Using Filters](/v2/guides/using-filters) for more examples on how to use filters.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ info:
|
|||
name: Plastic Labs
|
||||
url: https://honcho.dev/
|
||||
email: hello@plasticlabs.ai
|
||||
version: 2.1.1
|
||||
version: 2.3.0
|
||||
servers:
|
||||
- url: http://localhost:8000
|
||||
description: Local Development Server
|
||||
|
|
@ -231,42 +231,23 @@ paths:
|
|||
description: ID of the workspace to search
|
||||
title: Workspace Id
|
||||
description: ID of the workspace to search
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Page number
|
||||
default: 1
|
||||
title: Page
|
||||
description: Page number
|
||||
- name: size
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
description: Page size
|
||||
default: 50
|
||||
title: Size
|
||||
description: Page size
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Search query
|
||||
title: Query
|
||||
$ref: '#/components/schemas/MessageSearchOptions'
|
||||
description: 'Message search parameters '
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Page_Message_'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
title: Response Search Workspace V2 Workspaces Workspace Id Search Post
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
|
|
@ -282,10 +263,9 @@ paths:
|
|||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
// Automatically fetches more pages as needed.
|
||||
for await (const message of client.workspaces.search('workspace_id', { body: 'body' })) {
|
||||
console.log(message.id);
|
||||
}
|
||||
const messages = await client.workspaces.search('workspace_id', { query: 'query' });
|
||||
|
||||
console.log(messages);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
|
@ -293,12 +273,11 @@ paths:
|
|||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
page = client.workspaces.search(
|
||||
messages = client.workspaces.search(
|
||||
workspace_id="workspace_id",
|
||||
body="body",
|
||||
query="query",
|
||||
)
|
||||
page = page.items[0]
|
||||
print(page.id)
|
||||
print(messages)
|
||||
/v2/workspaces/{workspace_id}/deriver/status:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -891,27 +870,6 @@ paths:
|
|||
description: ID of the peer
|
||||
title: Peer Id
|
||||
description: ID of the peer
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Page number
|
||||
default: 1
|
||||
title: Page
|
||||
description: Page number
|
||||
- name: size
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
description: Page size
|
||||
default: 50
|
||||
title: Size
|
||||
description: Page size
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
@ -925,7 +883,10 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Page_Message_'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
title: Response Search Peer V2 Workspaces Workspace Id Peers Peer Id Search Post
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
|
|
@ -943,12 +904,11 @@ paths:
|
|||
});
|
||||
|
||||
|
||||
// Automatically fetches more pages as needed.
|
||||
const messages = await client.workspaces.peers.search('workspace_id', 'peer_id', { query: 'query'
|
||||
});
|
||||
|
||||
for await (const message of client.workspaces.peers.search('workspace_id', 'peer_id', { query:
|
||||
'query' })) {
|
||||
console.log(message.id);
|
||||
}
|
||||
|
||||
console.log(messages);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
|
@ -956,13 +916,12 @@ paths:
|
|||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
page = client.workspaces.peers.search(
|
||||
messages = client.workspaces.peers.search(
|
||||
peer_id="peer_id",
|
||||
workspace_id="workspace_id",
|
||||
query="query",
|
||||
)
|
||||
page = page.items[0]
|
||||
print(page.id)
|
||||
print(messages)
|
||||
/v2/workspaces/{workspace_id}/sessions:
|
||||
post:
|
||||
tags:
|
||||
|
|
@ -1699,20 +1658,20 @@ paths:
|
|||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: >-
|
||||
source: |-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
const sessionPeerConfig = await client.workspaces.sessions.peers.getConfig(
|
||||
'workspace_id',
|
||||
'session_id',
|
||||
'peer_id',
|
||||
);
|
||||
|
||||
const response = await client.workspaces.sessions.peers.getConfig('workspace_id', 'session_id',
|
||||
'peer_id');
|
||||
|
||||
|
||||
console.log(response.observe_me);
|
||||
console.log(sessionPeerConfig.observe_me);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
|
@ -1720,12 +1679,12 @@ paths:
|
|||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
response = client.workspaces.sessions.peers.get_config(
|
||||
session_peer_config = client.workspaces.sessions.peers.get_config(
|
||||
peer_id="peer_id",
|
||||
workspace_id="workspace_id",
|
||||
session_id="session_id",
|
||||
)
|
||||
print(response.observe_me)
|
||||
print(session_peer_config.observe_me)
|
||||
post:
|
||||
tags:
|
||||
- sessions
|
||||
|
|
@ -1814,15 +1773,16 @@ paths:
|
|||
- sessions
|
||||
summary: Get Session Context
|
||||
description: >-
|
||||
Produce a context object from the session. The caller provides a token limit which the entire context
|
||||
must fit into.
|
||||
Produce a context object from the session. The caller provides an optional token limit which the
|
||||
entire context must fit into.
|
||||
|
||||
To do this, we allocate 40% of the token limit to the summary, and 60% to recent messages -- as many
|
||||
as can fit.
|
||||
If not provided, the context will be exhaustive (within configured max tokens). To do this, we
|
||||
allocate 40% of the token limit
|
||||
|
||||
If the caller does not want a summary, we allocate all the tokens to recent messages.
|
||||
to the summary, and 60% to recent messages -- as many as can fit. Note that the summary will usually
|
||||
take up less space than
|
||||
|
||||
The default token limit if not provided is 2048. (TODO: make this configurable)
|
||||
this. If the caller does not want a summary, we allocate all the tokens to recent messages.
|
||||
operationId: get_session_context_v2_workspaces__workspace_id__sessions__session_id__context_get
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
|
|
@ -1850,19 +1810,24 @@ paths:
|
|||
schema:
|
||||
anyOf:
|
||||
- type: integer
|
||||
maximum: 100000
|
||||
- type: 'null'
|
||||
description: Number of tokens to use for the context. Includes summary if set to true
|
||||
description: >-
|
||||
Number of tokens to use for the context. Includes summary if set to true. If not provided, the
|
||||
context will be exhaustive (within 100000 tokens)
|
||||
title: Tokens
|
||||
description: Number of tokens to use for the context. Includes summary if set to true
|
||||
description: >-
|
||||
Number of tokens to use for the context. Includes summary if set to true. If not provided, the
|
||||
context will be exhaustive (within 100000 tokens)
|
||||
- name: summary
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
description: Whether to summarize the session history prior to the cutoff message
|
||||
default: false
|
||||
description: Whether or not to include a summary *if* one is available for the session
|
||||
default: true
|
||||
title: Summary
|
||||
description: Whether to summarize the session history prior to the cutoff message
|
||||
description: Whether or not to include a summary *if* one is available for the session
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
|
|
@ -1900,6 +1865,74 @@ paths:
|
|||
workspace_id="workspace_id",
|
||||
)
|
||||
print(response.id)
|
||||
/v2/workspaces/{workspace_id}/sessions/{session_id}/summaries:
|
||||
get:
|
||||
tags:
|
||||
- sessions
|
||||
summary: Get Session Summaries
|
||||
description: |-
|
||||
Get available summaries for a session.
|
||||
|
||||
Returns both short and long summaries if available, including metadata like
|
||||
the message ID they cover up to, creation timestamp, and token count.
|
||||
operationId: get_session_summaries_v2_workspaces__workspace_id__sessions__session_id__summaries_get
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
- {}
|
||||
parameters:
|
||||
- name: workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: ID of the workspace
|
||||
title: Workspace Id
|
||||
description: ID of the workspace
|
||||
- name: session_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: ID of the session
|
||||
title: Session Id
|
||||
description: ID of the session
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SessionSummaries'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: |-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
const response = await client.workspaces.sessions.summaries('workspace_id', 'session_id');
|
||||
|
||||
console.log(response.id);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
||||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
response = client.workspaces.sessions.summaries(
|
||||
session_id="session_id",
|
||||
workspace_id="workspace_id",
|
||||
)
|
||||
print(response.id)
|
||||
/v2/workspaces/{workspace_id}/sessions/{session_id}/search:
|
||||
post:
|
||||
tags:
|
||||
|
|
@ -1927,41 +1960,23 @@ paths:
|
|||
description: ID of the session
|
||||
title: Session Id
|
||||
description: ID of the session
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Page number
|
||||
default: 1
|
||||
title: Page
|
||||
description: Page number
|
||||
- name: size
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
description: Page size
|
||||
default: 50
|
||||
title: Size
|
||||
description: Page size
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MessageSearchOptions'
|
||||
description: 'Message search parameters '
|
||||
description: Message search parameters
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Page_Message_'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
title: Response Search Session V2 Workspaces Workspace Id Sessions Session Id Search Post
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
|
|
@ -1970,19 +1985,20 @@ paths:
|
|||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: |-
|
||||
source: >-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
// Automatically fetches more pages as needed.
|
||||
for await (const message of client.workspaces.sessions.search('workspace_id', 'session_id', {
|
||||
query: 'query',
|
||||
})) {
|
||||
console.log(message.id);
|
||||
}
|
||||
|
||||
const messages = await client.workspaces.sessions.search('workspace_id', 'session_id', { query:
|
||||
'query' });
|
||||
|
||||
|
||||
console.log(messages);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
|
@ -1990,13 +2006,12 @@ paths:
|
|||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
page = client.workspaces.sessions.search(
|
||||
messages = client.workspaces.sessions.search(
|
||||
session_id="session_id",
|
||||
workspace_id="workspace_id",
|
||||
query="query",
|
||||
)
|
||||
page = page.items[0]
|
||||
print(page.id)
|
||||
print(messages)
|
||||
/v2/workspaces/{workspace_id}/sessions/{session_id}/messages/:
|
||||
post:
|
||||
tags:
|
||||
|
|
@ -2505,6 +2520,267 @@ paths:
|
|||
)
|
||||
key = client.keys.create()
|
||||
print(key)
|
||||
/v2/workspaces/{workspace_id}/webhooks:
|
||||
post:
|
||||
tags:
|
||||
- webhooks
|
||||
summary: Get Or Create Webhook Endpoint
|
||||
description: Get or create a webhook endpoint URL.
|
||||
operationId: get_or_create_webhook_endpoint_v2_workspaces__workspace_id__webhooks_post
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
- {}
|
||||
parameters:
|
||||
- name: workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Workspace ID
|
||||
title: Workspace Id
|
||||
description: Workspace ID
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WebhookEndpointCreate'
|
||||
description: Webhook endpoint parameters
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WebhookEndpoint'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: >-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
|
||||
const webhookEndpoint = await client.workspaces.webhooks.getOrCreate('workspace_id', { url: 'url'
|
||||
});
|
||||
|
||||
|
||||
console.log(webhookEndpoint.id);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
||||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
webhook_endpoint = client.workspaces.webhooks.get_or_create(
|
||||
workspace_id="workspace_id",
|
||||
url="url",
|
||||
)
|
||||
print(webhook_endpoint.id)
|
||||
get:
|
||||
tags:
|
||||
- webhooks
|
||||
summary: List Webhook Endpoints
|
||||
description: List all webhook endpoints, optionally filtered by workspace.
|
||||
operationId: list_webhook_endpoints_v2_workspaces__workspace_id__webhooks_get
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
- {}
|
||||
parameters:
|
||||
- name: workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Workspace ID
|
||||
title: Workspace Id
|
||||
description: Workspace ID
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Page number
|
||||
default: 1
|
||||
title: Page
|
||||
description: Page number
|
||||
- name: size
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
description: Page size
|
||||
default: 50
|
||||
title: Size
|
||||
description: Page size
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Page_WebhookEndpoint_'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: |-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
// Automatically fetches more pages as needed.
|
||||
for await (const webhookEndpoint of client.workspaces.webhooks.list('workspace_id')) {
|
||||
console.log(webhookEndpoint.id);
|
||||
}
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
||||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
page = client.workspaces.webhooks.list(
|
||||
workspace_id="workspace_id",
|
||||
)
|
||||
page = page.items[0]
|
||||
print(page.id)
|
||||
/v2/workspaces/{workspace_id}/webhooks/{endpoint_id}:
|
||||
delete:
|
||||
tags:
|
||||
- webhooks
|
||||
summary: Delete Webhook Endpoint
|
||||
description: Delete a specific webhook endpoint.
|
||||
operationId: delete_webhook_endpoint_v2_workspaces__workspace_id__webhooks__endpoint_id__delete
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
- {}
|
||||
parameters:
|
||||
- name: workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Workspace ID
|
||||
title: Workspace Id
|
||||
description: Workspace ID
|
||||
- name: endpoint_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Webhook endpoint ID
|
||||
title: Endpoint Id
|
||||
description: Webhook endpoint ID
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: |-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
const webhook = await client.workspaces.webhooks.delete('workspace_id', 'endpoint_id');
|
||||
|
||||
console.log(webhook);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
||||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
webhook = client.workspaces.webhooks.delete(
|
||||
endpoint_id="endpoint_id",
|
||||
workspace_id="workspace_id",
|
||||
)
|
||||
print(webhook)
|
||||
/v2/workspaces/{workspace_id}/webhooks/test:
|
||||
get:
|
||||
tags:
|
||||
- webhooks
|
||||
summary: Test Emit
|
||||
description: Test publishing a webhook event.
|
||||
operationId: test_emit_v2_workspaces__workspace_id__webhooks_test_get
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
- {}
|
||||
parameters:
|
||||
- name: workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Workspace ID
|
||||
title: Workspace Id
|
||||
description: Workspace ID
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
x-codeSamples:
|
||||
- lang: JavaScript
|
||||
source: |-
|
||||
import Honcho from '@honcho-ai/core';
|
||||
|
||||
const client = new Honcho({
|
||||
apiKey: 'My API Key',
|
||||
});
|
||||
|
||||
const response = await client.workspaces.webhooks.testEmit('workspace_id');
|
||||
|
||||
console.log(response);
|
||||
- lang: Python
|
||||
source: |-
|
||||
from honcho_core import Honcho
|
||||
|
||||
client = Honcho(
|
||||
api_key="My API Key",
|
||||
)
|
||||
response = client.workspaces.webhooks.test_emit(
|
||||
"workspace_id",
|
||||
)
|
||||
print(response)
|
||||
components:
|
||||
schemas:
|
||||
Body_create_messages_with_file_v2_workspaces__workspace_id__sessions__session_id__messages_upload_post:
|
||||
|
|
@ -2612,9 +2888,7 @@ components:
|
|||
type: string
|
||||
title: Peer Id
|
||||
session_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
type: string
|
||||
title: Session Id
|
||||
metadata:
|
||||
additionalProperties: true
|
||||
|
|
@ -2670,6 +2944,12 @@ components:
|
|||
type: object
|
||||
- type: 'null'
|
||||
title: Metadata
|
||||
created_at:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
title: Created At
|
||||
type: object
|
||||
required:
|
||||
- content
|
||||
|
|
@ -2682,7 +2962,7 @@ components:
|
|||
- additionalProperties: true
|
||||
type: object
|
||||
- type: 'null'
|
||||
title: Filter
|
||||
title: Filters
|
||||
type: object
|
||||
title: MessageGet
|
||||
MessageSearchOptions:
|
||||
|
|
@ -2691,12 +2971,20 @@ components:
|
|||
type: string
|
||||
title: Query
|
||||
description: Search query
|
||||
semantic:
|
||||
filters:
|
||||
anyOf:
|
||||
- type: boolean
|
||||
- additionalProperties: true
|
||||
type: object
|
||||
- type: 'null'
|
||||
title: Semantic
|
||||
description: Whether to explicitly use semantic search to filter the results
|
||||
title: Filters
|
||||
description: Filters to scope the search
|
||||
limit:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
title: Limit
|
||||
description: Number of results to return
|
||||
default: 10
|
||||
type: object
|
||||
required:
|
||||
- query
|
||||
|
|
@ -2798,6 +3086,35 @@ components:
|
|||
- page
|
||||
- size
|
||||
title: Page[Session]
|
||||
Page_WebhookEndpoint_:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/WebhookEndpoint'
|
||||
type: array
|
||||
title: Items
|
||||
total:
|
||||
type: integer
|
||||
minimum: 0
|
||||
title: Total
|
||||
page:
|
||||
type: integer
|
||||
minimum: 1
|
||||
title: Page
|
||||
size:
|
||||
type: integer
|
||||
minimum: 1
|
||||
title: Size
|
||||
pages:
|
||||
type: integer
|
||||
minimum: 0
|
||||
title: Pages
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
- page
|
||||
- size
|
||||
title: Page[WebhookEndpoint]
|
||||
Page_Workspace_:
|
||||
properties:
|
||||
items:
|
||||
|
|
@ -2884,7 +3201,7 @@ components:
|
|||
- additionalProperties: true
|
||||
type: object
|
||||
- type: 'null'
|
||||
title: Filter
|
||||
title: Filters
|
||||
type: object
|
||||
title: PeerGet
|
||||
PeerRepresentationGet:
|
||||
|
|
@ -2960,13 +3277,14 @@ components:
|
|||
type: array
|
||||
title: Messages
|
||||
summary:
|
||||
type: string
|
||||
title: Summary
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/Summary'
|
||||
- type: 'null'
|
||||
description: The summary if available
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- messages
|
||||
- summary
|
||||
title: SessionContext
|
||||
SessionCreate:
|
||||
properties:
|
||||
|
|
@ -3037,7 +3355,7 @@ components:
|
|||
- additionalProperties: true
|
||||
type: object
|
||||
- type: 'null'
|
||||
title: Filter
|
||||
title: Filters
|
||||
type: object
|
||||
title: SessionGet
|
||||
SessionPeerConfig:
|
||||
|
|
@ -3059,6 +3377,25 @@ components:
|
|||
representation of this peer
|
||||
type: object
|
||||
title: SessionPeerConfig
|
||||
SessionSummaries:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
title: Id
|
||||
short_summary:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/Summary'
|
||||
- type: 'null'
|
||||
description: The short summary if available
|
||||
long_summary:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/Summary'
|
||||
- type: 'null'
|
||||
description: The long summary if available
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
title: SessionSummaries
|
||||
SessionUpdate:
|
||||
properties:
|
||||
metadata:
|
||||
|
|
@ -3075,6 +3412,36 @@ components:
|
|||
title: Configuration
|
||||
type: object
|
||||
title: SessionUpdate
|
||||
Summary:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
title: Content
|
||||
description: The summary text
|
||||
message_id:
|
||||
type: integer
|
||||
title: Message Id
|
||||
description: The ID of the message that this summary covers up to
|
||||
summary_type:
|
||||
type: string
|
||||
title: Summary Type
|
||||
description: The type of summary (short or long)
|
||||
created_at:
|
||||
type: string
|
||||
title: Created At
|
||||
description: The timestamp of when the summary was created (ISO format)
|
||||
token_count:
|
||||
type: integer
|
||||
title: Token Count
|
||||
description: The number of tokens in the summary text
|
||||
type: object
|
||||
required:
|
||||
- content
|
||||
- message_id
|
||||
- summary_type
|
||||
- created_at
|
||||
- token_count
|
||||
title: Summary
|
||||
ValidationError:
|
||||
properties:
|
||||
loc:
|
||||
|
|
@ -3096,6 +3463,39 @@ components:
|
|||
- msg
|
||||
- type
|
||||
title: ValidationError
|
||||
WebhookEndpoint:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
title: Id
|
||||
workspace_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: Workspace Id
|
||||
url:
|
||||
type: string
|
||||
title: Url
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Created At
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- workspace_id
|
||||
- url
|
||||
- created_at
|
||||
title: WebhookEndpoint
|
||||
WebhookEndpointCreate:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
title: Url
|
||||
type: object
|
||||
required:
|
||||
- url
|
||||
title: WebhookEndpointCreate
|
||||
Workspace:
|
||||
properties:
|
||||
id:
|
||||
|
|
@ -3147,7 +3547,7 @@ components:
|
|||
- additionalProperties: true
|
||||
type: object
|
||||
- type: 'null'
|
||||
title: Filter
|
||||
title: Filters
|
||||
type: object
|
||||
title: WorkspaceGet
|
||||
WorkspaceUpdate:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
description = "Honcho Server"
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
|
|
@ -32,8 +32,6 @@ dependencies = [
|
|||
]
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"honcho-core==1.3.0",
|
||||
"honcho-ai==1.3.0",
|
||||
"pytest>=8.2.2",
|
||||
"sqlalchemy-utils>=0.41.2",
|
||||
"pytest-asyncio>=0.23.7",
|
||||
|
|
@ -44,8 +42,17 @@ dev-dependencies = [
|
|||
"basedpyright>=1.29.4",
|
||||
"pre-commit>=4.2.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"honcho-ai",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"sdks/python",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
honcho-ai = { workspace = true }
|
||||
|
||||
[tool.ruff.lint]
|
||||
# from https://docs.astral.sh/ruff/linter/#rule-selection example
|
||||
select = [
|
||||
|
|
|
|||
|
|
@ -608,7 +608,7 @@ TYPESCRIPT_VERSION=
|
|||
)
|
||||
|
||||
# Add new row at the top
|
||||
new_row = f"| v{api_version} (Current) | v{typescript_version} | v{python_version} | Latest release |"
|
||||
new_row = f"| v{api_version} (Current) | v{typescript_version} | v{python_version} |"
|
||||
|
||||
# Reconstruct table
|
||||
new_table = header + new_row + "\n" + rows + after_table
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.4.0] - 2025-08-12
|
||||
|
||||
### Added
|
||||
|
||||
- getSummaries API returning structured summaries
|
||||
|
||||
### Changed
|
||||
|
||||
- Messages can take an optional `created_at` value, defaulting to the current
|
||||
time (UTC ISO 8601)
|
||||
|
||||
## [1.3.0] - 2025-08-04
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
"""
|
||||
Example of getting session summaries using the Honcho Python SDK.
|
||||
|
||||
This example demonstrates how to retrieve both short and long summaries
|
||||
for a session, including their metadata like message ID, creation timestamp,
|
||||
and token count.
|
||||
"""
|
||||
|
||||
from honcho import AsyncHoncho, Honcho, SessionSummaries
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
# Initialize the Honcho client
|
||||
api_key = os.getenv("HONCHO_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("Please set HONCHO_API_KEY environment variable")
|
||||
|
||||
client = Honcho(api_key=api_key)
|
||||
|
||||
# Get a session (replace with your actual session ID)
|
||||
session = client.session("my-conversation-session")
|
||||
|
||||
# Get summaries for the session
|
||||
summaries: SessionSummaries = session.get_summaries()
|
||||
|
||||
print(f"Session ID: {summaries.id}")
|
||||
print("-" * 50)
|
||||
|
||||
# Check and display short summary
|
||||
if summaries.short_summary:
|
||||
print("SHORT SUMMARY:")
|
||||
print(f" Content: {summaries.short_summary.content[:200]}...") # First 200 chars
|
||||
print(f" Covers up to message ID: {summaries.short_summary.message_id}")
|
||||
print(f" Created at: {summaries.short_summary.created_at}")
|
||||
print(f" Token count: {summaries.short_summary.token_count}")
|
||||
print(f" Type: {summaries.short_summary.summary_type}")
|
||||
else:
|
||||
print("No short summary available yet")
|
||||
|
||||
print("-" * 50)
|
||||
|
||||
# Check and display long summary
|
||||
if summaries.long_summary:
|
||||
print("LONG SUMMARY:")
|
||||
print(f" Content: {summaries.long_summary.content[:200]}...") # First 200 chars
|
||||
print(f" Covers up to message ID: {summaries.long_summary.message_id}")
|
||||
print(f" Created at: {summaries.long_summary.created_at}")
|
||||
print(f" Token count: {summaries.long_summary.token_count}")
|
||||
print(f" Type: {summaries.long_summary.summary_type}")
|
||||
else:
|
||||
print("No long summary available yet")
|
||||
|
||||
# Example with async client
|
||||
print("\n" + "=" * 50)
|
||||
print("ASYNC EXAMPLE:")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
async def get_summaries_async():
|
||||
async_client = AsyncHoncho(api_key=api_key)
|
||||
|
||||
# Get a session
|
||||
async_session = async_client.session("my-conversation-session")
|
||||
|
||||
# Get summaries asynchronously
|
||||
summaries = await async_session.get_summaries()
|
||||
|
||||
print(f"Session ID (async): {summaries.id}")
|
||||
|
||||
if summaries.short_summary:
|
||||
print(
|
||||
f"Short summary available with {summaries.short_summary.token_count} tokens"
|
||||
)
|
||||
|
||||
if summaries.long_summary:
|
||||
print(
|
||||
f"Long summary available with {summaries.long_summary.token_count} tokens"
|
||||
)
|
||||
|
||||
|
||||
# Run the async example
|
||||
asyncio.run(get_summaries_async())
|
||||
|
|
@ -5,11 +5,13 @@ This example shows how the SDK now uses Pydantic to validate inputs at runtime,
|
|||
providing better error messages and type safety.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from honcho import Honcho
|
||||
from pydantic import ValidationError
|
||||
|
||||
from honcho import Honcho
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
|
|
@ -119,6 +121,24 @@ def demonstrate_validation():
|
|||
except ValidationError as e:
|
||||
print(f"❌ Validation error: {e}")
|
||||
|
||||
# Example 10: Custom created_at timestamps
|
||||
print("10. Custom created_at timestamps:")
|
||||
try:
|
||||
honcho = Honcho(environment="local", workspace_id="test")
|
||||
peer = honcho.peer("alice")
|
||||
message = peer.message("Hello, world!", created_at=datetime.datetime.now())
|
||||
print(f"✅ Created message: {message}")
|
||||
message = peer.message(
|
||||
"Hello, world!",
|
||||
created_at=datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
print(f"✅ Created message: {message}")
|
||||
session = honcho.session("conversation_1")
|
||||
session.add_messages([message])
|
||||
print(f"✅ Added message to session: {session}")
|
||||
except ValidationError as e:
|
||||
print(f"❌ Validation error: {e}")
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
print("🎉 Pydantic validation examples completed!")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho-ai"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
description = "Official DX Optimized Python SDK for Honcho"
|
||||
dynamic = ["readme"]
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -8,7 +8,7 @@ authors = [
|
|||
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
|
||||
]
|
||||
dependencies = [
|
||||
"honcho-core==1.3.0",
|
||||
"honcho-core>=1.4.0",
|
||||
"httpx>=0.28.0, <1",
|
||||
"pydantic>=2.0.0, <3",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ from .client import Honcho
|
|||
from .pagination import SyncPage
|
||||
from .peer import Peer
|
||||
from .session import Session
|
||||
from .session_context import SessionContext
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.4.0"
|
||||
__author__ = "Plastic Labs"
|
||||
__email__ = "hello@plasticlabs.ai"
|
||||
|
||||
|
|
@ -59,5 +59,7 @@ __all__ = [
|
|||
"Peer",
|
||||
"Session",
|
||||
"SessionContext",
|
||||
"SessionSummaries",
|
||||
"Summary",
|
||||
"SyncPage",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
|
|
@ -175,6 +176,10 @@ class AsyncPeer(BaseModel):
|
|||
metadata: dict[str, object] | None = Field(
|
||||
None, description="Optional metadata dictionary"
|
||||
),
|
||||
created_at: datetime.datetime | str | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the message. Accepts a datetime which will be converted to an ISO 8601 string, or a preformatted string.",
|
||||
),
|
||||
) -> MessageCreateParam:
|
||||
"""
|
||||
Create a MessageCreateParam object attributed to this peer.
|
||||
|
|
@ -189,7 +194,18 @@ class AsyncPeer(BaseModel):
|
|||
Returns:
|
||||
A new MessageCreateParam object with this peer's ID and the provided content
|
||||
"""
|
||||
return MessageCreateParam(peer_id=self.id, content=content, metadata=metadata)
|
||||
created_at_str: str | None
|
||||
if isinstance(created_at, datetime.datetime):
|
||||
created_at_str = created_at.isoformat()
|
||||
else:
|
||||
created_at_str = created_at
|
||||
|
||||
return MessageCreateParam(
|
||||
peer_id=self.id,
|
||||
content=content,
|
||||
metadata=metadata,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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 ..session_context import SessionContext, SessionSummaries, Summary
|
||||
from ..utils import prepare_file_for_upload
|
||||
from .pagination import AsyncPage
|
||||
|
||||
|
|
@ -440,8 +440,72 @@ class AsyncSession(BaseModel):
|
|||
summary=summary,
|
||||
)
|
||||
|
||||
# Convert the honcho_core summary to our Summary if it exists
|
||||
session_summary = None
|
||||
if context.summary:
|
||||
session_summary = Summary(
|
||||
content=context.summary.content,
|
||||
message_id=context.summary.message_id,
|
||||
summary_type=context.summary.summary_type,
|
||||
created_at=context.summary.created_at,
|
||||
token_count=context.summary.token_count,
|
||||
)
|
||||
|
||||
return SessionContext(
|
||||
session_id=self.id, messages=context.messages, summary=context.summary
|
||||
session_id=self.id, messages=context.messages, summary=session_summary
|
||||
)
|
||||
|
||||
async def get_summaries(self) -> SessionSummaries:
|
||||
"""
|
||||
Get available summaries for this session.
|
||||
|
||||
Makes an async API call to retrieve both short and long summaries for this session,
|
||||
if they are available. Summaries are created asynchronously by the backend
|
||||
as messages are added to the session.
|
||||
|
||||
Returns:
|
||||
A SessionSummaries object containing:
|
||||
- id: The session ID
|
||||
- short_summary: The short summary if available, including metadata
|
||||
- long_summary: The long summary if available, including metadata
|
||||
|
||||
Note:
|
||||
Summaries may be None if:
|
||||
- Not enough messages have been added to trigger summary generation
|
||||
- The summary generation is still in progress
|
||||
- Summary generation is disabled for this session
|
||||
"""
|
||||
# Use the honcho_core client to get summaries
|
||||
response = await self._client.workspaces.sessions.summaries(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
)
|
||||
|
||||
# Create Summary objects from the response data
|
||||
short_summary = None
|
||||
if response.short_summary:
|
||||
short_summary = Summary(
|
||||
content=response.short_summary.content,
|
||||
message_id=response.short_summary.message_id,
|
||||
summary_type=response.short_summary.summary_type,
|
||||
created_at=response.short_summary.created_at,
|
||||
token_count=response.short_summary.token_count,
|
||||
)
|
||||
|
||||
long_summary = None
|
||||
if response.long_summary:
|
||||
long_summary = Summary(
|
||||
content=response.long_summary.content,
|
||||
message_id=response.long_summary.message_id,
|
||||
summary_type=response.long_summary.summary_type,
|
||||
created_at=response.long_summary.created_at,
|
||||
token_count=response.long_summary.token_count,
|
||||
)
|
||||
|
||||
return SessionSummaries(
|
||||
id=response.id or self.id,
|
||||
short_summary=short_summary,
|
||||
long_summary=long_summary,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
|
|
@ -158,6 +159,10 @@ class Peer(BaseModel):
|
|||
metadata: dict[str, object] | None = Field(
|
||||
None, description="Optional metadata dictionary"
|
||||
),
|
||||
created_at: datetime.datetime | str | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the message. Accepts a datetime which will be converted to an ISO 8601 string, or a preformatted string.",
|
||||
),
|
||||
) -> MessageCreateParam:
|
||||
"""
|
||||
Create a MessageCreateParam object attributed to this peer.
|
||||
|
|
@ -172,7 +177,18 @@ class Peer(BaseModel):
|
|||
Returns:
|
||||
A new MessageCreateParam object with this peer's ID and the provided content
|
||||
"""
|
||||
return MessageCreateParam(peer_id=self.id, content=content, metadata=metadata)
|
||||
created_at_str: str | None
|
||||
if isinstance(created_at, datetime.datetime):
|
||||
created_at_str = created_at.isoformat()
|
||||
else:
|
||||
created_at_str = created_at
|
||||
|
||||
return MessageCreateParam(
|
||||
peer_id=self.id,
|
||||
content=content,
|
||||
metadata=metadata,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
||||
def get_metadata(self) -> dict[str, object]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
from .utils import prepare_file_for_upload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -421,8 +421,72 @@ class Session(BaseModel):
|
|||
summary=summary,
|
||||
)
|
||||
|
||||
# Convert the honcho_core summary to our Summary if it exists
|
||||
session_summary = None
|
||||
if context.summary:
|
||||
session_summary = Summary(
|
||||
content=context.summary.content,
|
||||
message_id=context.summary.message_id,
|
||||
summary_type=context.summary.summary_type,
|
||||
created_at=context.summary.created_at,
|
||||
token_count=context.summary.token_count,
|
||||
)
|
||||
|
||||
return SessionContext(
|
||||
session_id=self.id, messages=context.messages, summary=context.summary
|
||||
session_id=self.id, messages=context.messages, summary=session_summary
|
||||
)
|
||||
|
||||
def get_summaries(self) -> SessionSummaries:
|
||||
"""
|
||||
Get available summaries for this session.
|
||||
|
||||
Makes an API call to retrieve both short and long summaries for this session,
|
||||
if they are available. Summaries are created asynchronously by the backend
|
||||
as messages are added to the session.
|
||||
|
||||
Returns:
|
||||
A SessionSummaries object containing:
|
||||
- id: The session ID
|
||||
- short_summary: The short summary if available, including metadata
|
||||
- long_summary: The long summary if available, including metadata
|
||||
|
||||
Note:
|
||||
Summaries may be None if:
|
||||
- Not enough messages have been added to trigger summary generation
|
||||
- The summary generation is still in progress
|
||||
- Summary generation is disabled for this session
|
||||
"""
|
||||
# Use the honcho_core client to get summaries
|
||||
response = self._client.workspaces.sessions.summaries(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
)
|
||||
|
||||
# Create Summary objects from the response data
|
||||
short_summary = None
|
||||
if response.short_summary:
|
||||
short_summary = Summary(
|
||||
content=response.short_summary.content,
|
||||
message_id=response.short_summary.message_id,
|
||||
summary_type=response.short_summary.summary_type,
|
||||
created_at=response.short_summary.created_at,
|
||||
token_count=response.short_summary.token_count,
|
||||
)
|
||||
|
||||
long_summary = None
|
||||
if response.long_summary:
|
||||
long_summary = Summary(
|
||||
content=response.long_summary.content,
|
||||
message_id=response.long_summary.message_id,
|
||||
summary_type=response.long_summary.summary_type,
|
||||
created_at=response.long_summary.created_at,
|
||||
token_count=response.long_summary.token_count,
|
||||
)
|
||||
|
||||
return SessionSummaries(
|
||||
id=response.id or self.id,
|
||||
short_summary=short_summary,
|
||||
long_summary=long_summary,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
|
|||
|
|
@ -9,6 +9,34 @@ if TYPE_CHECKING:
|
|||
from .peer import Peer
|
||||
|
||||
|
||||
class Summary(BaseModel):
|
||||
"""Represents a summary of a session's conversation."""
|
||||
|
||||
content: str = Field(..., description="The summary text")
|
||||
message_id: int = Field(
|
||||
..., description="The ID of the message that this summary covers up to"
|
||||
)
|
||||
summary_type: str = Field(..., description="The type of summary (short or long)")
|
||||
created_at: str = Field(
|
||||
..., description="The timestamp of when the summary was created (ISO format)"
|
||||
)
|
||||
token_count: int = Field(
|
||||
..., description="The number of tokens in the summary text"
|
||||
)
|
||||
|
||||
|
||||
class SessionSummaries(BaseModel):
|
||||
"""Contains both short and long summaries for a session."""
|
||||
|
||||
id: str = Field(..., description="The session ID")
|
||||
short_summary: Summary | None = Field(
|
||||
None, description="The short summary if available"
|
||||
)
|
||||
long_summary: Summary | None = Field(
|
||||
None, description="The long summary if available"
|
||||
)
|
||||
|
||||
|
||||
class SessionContext(BaseModel):
|
||||
"""
|
||||
Represents the context of a session containing a curated list of messages.
|
||||
|
|
@ -27,8 +55,8 @@ class SessionContext(BaseModel):
|
|||
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"
|
||||
summary: Summary | None = Field(
|
||||
None, description="Summary of the session history prior to the message cutoff"
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -40,8 +68,8 @@ class SessionContext(BaseModel):
|
|||
messages: list[Message] = Field(
|
||||
..., description="List of Message objects to include in the context"
|
||||
),
|
||||
summary: str = Field(
|
||||
...,
|
||||
summary: Summary | None = Field(
|
||||
None,
|
||||
description="Summary of the session history prior to the message cutoff",
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -50,6 +78,7 @@ class SessionContext(BaseModel):
|
|||
|
||||
Args:
|
||||
messages: List of Message objects to include in the context
|
||||
summary: Optional Summary object containing summary information
|
||||
"""
|
||||
super().__init__(
|
||||
session_id=session_id,
|
||||
|
|
@ -80,10 +109,6 @@ class SessionContext(BaseModel):
|
|||
"""
|
||||
|
||||
assistant_id = assistant if isinstance(assistant, str) else assistant.id
|
||||
summary_message = {
|
||||
"role": "system",
|
||||
"content": f"<summary>{self.summary}</summary>",
|
||||
}
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant" if message.peer_id == assistant_id else "user",
|
||||
|
|
@ -92,7 +117,14 @@ class SessionContext(BaseModel):
|
|||
}
|
||||
for message in self.messages
|
||||
]
|
||||
return [summary_message, *messages] if self.summary else messages
|
||||
|
||||
if self.summary:
|
||||
summary_message = {
|
||||
"role": "system",
|
||||
"content": f"<summary>{self.summary.content}</summary>",
|
||||
}
|
||||
return [summary_message, *messages]
|
||||
return messages
|
||||
|
||||
def to_anthropic(
|
||||
self,
|
||||
|
|
@ -120,10 +152,6 @@ class SessionContext(BaseModel):
|
|||
"""
|
||||
|
||||
assistant_id = assistant if isinstance(assistant, str) else assistant.id
|
||||
summary_message = {
|
||||
"role": "user",
|
||||
"content": f"<summary>{self.summary}</summary>",
|
||||
}
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
|
|
@ -136,7 +164,14 @@ class SessionContext(BaseModel):
|
|||
}
|
||||
for message in self.messages
|
||||
]
|
||||
return [summary_message, *messages] if self.summary else messages
|
||||
|
||||
if self.summary:
|
||||
summary_message = {
|
||||
"role": "user",
|
||||
"content": f"<summary>{self.summary.content}</summary>",
|
||||
}
|
||||
return [summary_message, *messages]
|
||||
return messages
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
|
|
@ -154,4 +189,4 @@ class SessionContext(BaseModel):
|
|||
Returns:
|
||||
A string representation suitable for debugging
|
||||
"""
|
||||
return f"SessionContext(messages={len(self.messages)}, summary={self.summary})"
|
||||
return f"SessionContext(messages={len(self.messages)}, summary={'present' if self.summary else 'None'})"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.8"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
|
|
@ -38,7 +38,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
|
|
@ -47,20 +47,20 @@ 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'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
version = "2025.8.3"
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -78,7 +78,7 @@ 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'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
|
||||
wheels = [
|
||||
|
|
@ -96,13 +96,13 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "honcho-ai"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "honcho-core" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
|
|
@ -112,7 +112,7 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = "==1.3.0" },
|
||||
{ name = "honcho-core", specifier = ">=1.4.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
]
|
||||
|
|
@ -122,22 +122,22 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
|||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "anyio", version = "4.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/49/1810de2351be1fdff330ffadcc5eef709889b2f698b0340966cae2c1eef9/honcho_core-1.3.0.tar.gz", hash = "sha256:276a73e8d523f7d22f06746922fda97e4a11fbbef4a51a0b43573bc817089cec", size = 123333, upload-time = "2025-08-06T16:42:10.941Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/c2/4d3737d5c0a75e2f59f324bed7428a9593c8edfcb7500cf402c46ad7378a/honcho_core-1.4.0.tar.gz", hash = "sha256:b30de9247763c01c3c37ed22b5d04d3c611440dd1983168f3d7d93f2cc0ecf7d", size = 126164, upload-time = "2025-08-12T19:06:54.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/3f/3beb750ed65bb1b604c585ae1c51a54f8b34ea3002f4525f93fe86549531/honcho_core-1.3.0-py3-none-any.whl", hash = "sha256:c7c0bf77b61162e6c65a580327fc054785aef794923abc35acd3dbd8287245db", size = 112895, upload-time = "2025-08-06T16:42:08.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ac/6eb2e38376736bb1dfd26bbdfd96490bfbb6f0ddcb00b39913e93799e7d8/honcho_core-1.4.0-py3-none-any.whl", hash = "sha256:a84397fd9daf546a04f5458d7233ebd5eeafcb97af75ec60a9327db89abdaf03", size = 117602, upload-time = "2025-08-12T19:06:53.667Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -159,7 +159,7 @@ 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 = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
|
|
@ -197,7 +197,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.5"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
|
|
@ -205,12 +205,12 @@ resolution-markers = [
|
|||
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-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-inspection", marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -334,7 +334,7 @@ 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'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
|
|
@ -440,27 +440,27 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.13"
|
||||
version = "0.12.8"
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -486,14 +486,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.0"
|
||||
version = "4.14.1"
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -501,7 +501,7 @@ 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'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.4.0] - 2025-08-12
|
||||
|
||||
### Added
|
||||
|
||||
- getSummaries API returning structured summaries
|
||||
|
||||
### Changed
|
||||
|
||||
- Messages can take an optional `created_at` value, defaulting to the current
|
||||
time (UTC ISO 8601)
|
||||
|
||||
## [1.3.0] - 2025-08-04
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
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';
|
||||
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;
|
||||
let mockWorkspacesApi: any
|
||||
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
return jest.fn().mockImplementation(() => mockWorkspacesApi);
|
||||
});
|
||||
return jest.fn().mockImplementation(() => mockWorkspacesApi)
|
||||
})
|
||||
|
||||
describe('Honcho SDK Integration Tests', () => {
|
||||
let honcho: Honcho;
|
||||
let honcho: Honcho
|
||||
|
||||
beforeEach(() => {
|
||||
mockWorkspacesApi = {
|
||||
|
|
@ -29,7 +29,12 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
},
|
||||
sessions: {
|
||||
list: jest.fn(),
|
||||
peers: { add: jest.fn(), set: jest.fn(), remove: jest.fn(), 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(),
|
||||
|
|
@ -41,112 +46,135 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
list: jest.fn(),
|
||||
search: jest.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
jest.clearAllMocks();
|
||||
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 mockPeerData = { id: 'assistant', metadata: { role: 'ai' } }
|
||||
const mockSessionData = {
|
||||
id: 'chat-session',
|
||||
metadata: { topic: 'general' },
|
||||
}
|
||||
const mockMessages = [
|
||||
{ id: 'msg1', content: 'Hello', peer_id: 'user' },
|
||||
{ id: 'msg2', content: 'Hi there!', peer_id: 'assistant' },
|
||||
];
|
||||
const mockContextData = { messages: mockMessages, summary: 'Friendly greeting' };
|
||||
]
|
||||
const mockContextData = {
|
||||
messages: mockMessages,
|
||||
summary: {
|
||||
content: 'Friendly greeting',
|
||||
message_id: 5,
|
||||
summary_type: 'short',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
token_count: 50,
|
||||
},
|
||||
}
|
||||
|
||||
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' });
|
||||
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 = await honcho.peer('user');
|
||||
const assistant = await honcho.peer('assistant');
|
||||
const user = await honcho.peer('user')
|
||||
const assistant = await honcho.peer('assistant')
|
||||
|
||||
expect(user).toBeInstanceOf(Peer);
|
||||
expect(assistant).toBeInstanceOf(Peer);
|
||||
expect(user.id).toBe('user');
|
||||
expect(assistant.id).toBe('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 = await honcho.session('chat-session');
|
||||
expect(session).toBeInstanceOf(Session);
|
||||
expect(session.id).toBe('chat-session');
|
||||
const session = await 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': {},
|
||||
'assistant': {}
|
||||
}
|
||||
);
|
||||
await session.addPeers([user, assistant])
|
||||
expect(
|
||||
mockWorkspacesApi.workspaces.sessions.peers.add
|
||||
).toHaveBeenCalledWith('integration-test-workspace', 'chat-session', {
|
||||
user: {},
|
||||
assistant: {},
|
||||
})
|
||||
|
||||
// Step 4: Add messages to session
|
||||
const userMessage = user.message('Hello');
|
||||
const assistantMessage = assistant.message('Hi there!');
|
||||
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 },
|
||||
]
|
||||
}
|
||||
);
|
||||
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');
|
||||
const context = await session.getContext()
|
||||
expect(context).toBeInstanceOf(SessionContext)
|
||||
expect(context.sessionId).toBe('chat-session')
|
||||
expect(context.messages).toEqual(mockMessages)
|
||||
expect(context.summary?.content).toBe('Friendly greeting')
|
||||
|
||||
// Step 6: Convert context to different formats
|
||||
const openAIFormat = context.toOpenAI('assistant');
|
||||
const anthropicFormat = context.toAnthropic('assistant');
|
||||
const openAIFormat = context.toOpenAI('assistant')
|
||||
const anthropicFormat = context.toAnthropic('assistant')
|
||||
|
||||
expect(openAIFormat).toEqual([
|
||||
{ role: 'system', content: '<summary>Friendly greeting</summary>' },
|
||||
{ role: 'user', content: 'Hello', name: 'user' },
|
||||
{ role: 'assistant', content: 'Hi there!', name: 'assistant' },
|
||||
]);
|
||||
])
|
||||
|
||||
expect(anthropicFormat).toEqual([
|
||||
{ role: 'user', content: '<summary>Friendly greeting</summary>' },
|
||||
{ role: 'user', content: 'user: Hello' },
|
||||
{ role: 'assistant', content: 'Hi there!' },
|
||||
]);
|
||||
])
|
||||
|
||||
// Step 7: Query assistant
|
||||
const response = await assistant.chat('How are you?');
|
||||
expect(response).toBe('AI response');
|
||||
const response = await assistant.chat('How are you?')
|
||||
expect(response).toBe('AI response')
|
||||
expect(mockWorkspacesApi.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
'assistant',
|
||||
{ query: 'How are you?', stream: undefined, target: undefined, session_id: undefined }
|
||||
);
|
||||
});
|
||||
{
|
||||
query: '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 mockWorkspaceMetadata = { name: 'Test Workspace', version: '1.0' }
|
||||
const mockPeersList = {
|
||||
items: [
|
||||
{ id: 'peer1', metadata: { role: 'user' } },
|
||||
|
|
@ -155,105 +183,115 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
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);
|
||||
})
|
||||
mockWorkspacesApi.workspaces.update.mockResolvedValue({})
|
||||
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(mockPeersList)
|
||||
|
||||
// Step 1: Get workspace metadata
|
||||
const metadata = await honcho.getMetadata();
|
||||
expect(metadata).toEqual(mockWorkspaceMetadata);
|
||||
const metadata = await honcho.getMetadata()
|
||||
expect(metadata).toEqual(mockWorkspaceMetadata)
|
||||
|
||||
// Step 2: Update workspace metadata
|
||||
const newMetadata = { ...mockWorkspaceMetadata, updated: true };
|
||||
await honcho.setMetadata(newMetadata);
|
||||
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);
|
||||
const peersPage = await honcho.getPeers()
|
||||
expect(peersPage).toBeInstanceOf(Page)
|
||||
|
||||
// Step 4: Iterate through peers
|
||||
const peersList: Peer[] = [];
|
||||
const peersList: Peer[] = []
|
||||
for await (const peer of peersPage) {
|
||||
peersList.push(peer);
|
||||
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');
|
||||
});
|
||||
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 = [
|
||||
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
|
||||
];
|
||||
]
|
||||
|
||||
const mockPeerSearchResults = [
|
||||
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
|
||||
];
|
||||
]
|
||||
|
||||
const mockSessionSearchResults = [
|
||||
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
|
||||
];
|
||||
]
|
||||
|
||||
mockWorkspacesApi.workspaces.search.mockResolvedValue(mockWorkspaceSearchResults);
|
||||
mockWorkspacesApi.workspaces.peers.search.mockResolvedValue(mockPeerSearchResults);
|
||||
mockWorkspacesApi.workspaces.sessions.search.mockResolvedValue(mockSessionSearchResults);
|
||||
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(Array.isArray(workspaceResults)).toBe(true);
|
||||
const workspaceResults = await honcho.search('test query')
|
||||
expect(Array.isArray(workspaceResults)).toBe(true)
|
||||
expect(mockWorkspacesApi.workspaces.search).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
{ query: 'test query', limit: undefined }
|
||||
);
|
||||
)
|
||||
|
||||
// Step 2: Search peer
|
||||
const peer = await honcho.peer('test-peer');
|
||||
const peerResults = await peer.search('peer query');
|
||||
expect(Array.isArray(peerResults)).toBe(true);
|
||||
const peer = await honcho.peer('test-peer')
|
||||
const peerResults = await peer.search('peer query')
|
||||
expect(Array.isArray(peerResults)).toBe(true)
|
||||
expect(mockWorkspacesApi.workspaces.peers.search).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'peer query', limit: undefined }
|
||||
);
|
||||
)
|
||||
|
||||
// Step 3: Search session
|
||||
const session = await honcho.session('test-session');
|
||||
const sessionResults = await session.search('session query');
|
||||
expect(Array.isArray(sessionResults)).toBe(true);
|
||||
const session = await honcho.session('test-session')
|
||||
const sessionResults = await session.search('session query')
|
||||
expect(Array.isArray(sessionResults)).toBe(true)
|
||||
expect(mockWorkspacesApi.workspaces.sessions.search).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
'test-session',
|
||||
{ query: 'session query', limit: undefined }
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
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'));
|
||||
mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue(
|
||||
new Error('Chat API failed')
|
||||
)
|
||||
mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue(
|
||||
new Error('Context API failed')
|
||||
)
|
||||
|
||||
const assistant = await honcho.peer('assistant');
|
||||
const session = await honcho.session('error-session');
|
||||
const assistant = await honcho.peer('assistant')
|
||||
const session = await honcho.session('error-session')
|
||||
|
||||
// Test error handling in chat
|
||||
await expect(assistant.chat('Hello')).rejects.toThrow();
|
||||
await expect(assistant.chat('Hello')).rejects.toThrow()
|
||||
|
||||
// Test error handling in context
|
||||
await expect(session.getContext()).rejects.toThrow();
|
||||
});
|
||||
await expect(session.getContext()).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should handle pagination correctly', async () => {
|
||||
// Setup paginated response
|
||||
|
|
@ -266,7 +304,7 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
size: 2,
|
||||
hasNextPage: true,
|
||||
nextPage: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const secondPageData = {
|
||||
items: [
|
||||
|
|
@ -277,34 +315,34 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
size: 2,
|
||||
hasNextPage: false,
|
||||
nextPage: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
}
|
||||
|
||||
firstPageData.nextPage.mockResolvedValue(secondPageData);
|
||||
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(firstPageData);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
const secondPageData_ = await secondPage!.data()
|
||||
expect(secondPageData_).toHaveLength(2)
|
||||
|
||||
// Step 5: Verify no more pages
|
||||
const thirdPage = await secondPage!.nextPage();
|
||||
expect(thirdPage).toBeNull();
|
||||
});
|
||||
const thirdPage = await secondPage!.nextPage()
|
||||
expect(thirdPage).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle working representation queries', async () => {
|
||||
const mockWorkingRep = {
|
||||
|
|
@ -312,94 +350,115 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
knowledge: 'Alice likes coffee and works as a developer',
|
||||
relationships: ['bob', 'charlie'],
|
||||
context: 'session-specific context',
|
||||
};
|
||||
}
|
||||
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(mockWorkingRep);
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockWorkingRep
|
||||
)
|
||||
|
||||
const session = await honcho.session('working-rep-session');
|
||||
const alice = await honcho.peer('alice');
|
||||
const bob = await honcho.peer('bob');
|
||||
const session = await honcho.session('working-rep-session')
|
||||
const alice = await honcho.peer('alice')
|
||||
const bob = await honcho.peer('bob')
|
||||
|
||||
// Test working representation without target
|
||||
const globalRep = await session.workingRep('alice');
|
||||
expect(globalRep).toEqual(mockWorkingRep);
|
||||
expect(mockWorkspacesApi.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
'alice',
|
||||
{ session_id: 'working-rep-session', target: undefined }
|
||||
);
|
||||
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' }
|
||||
);
|
||||
});
|
||||
});
|
||||
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: [] });
|
||||
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 = await honcho.peer('empty-peer');
|
||||
const session = await honcho.session('empty-session');
|
||||
const peer = await honcho.peer('empty-peer')
|
||||
const session = await honcho.session('empty-session')
|
||||
|
||||
// Test null chat response
|
||||
const chatResult = await peer.chat('Hello');
|
||||
expect(chatResult).toBeNull();
|
||||
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([]);
|
||||
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);
|
||||
});
|
||||
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 = await honcho.peer('typed-peer');
|
||||
const session: Session = await honcho.session('typed-session');
|
||||
const peer: Peer = await honcho.peer('typed-peer')
|
||||
const session: Session = await honcho.session('typed-session')
|
||||
|
||||
expect(typeof peer.id).toBe('string');
|
||||
expect(typeof session.id).toBe('string');
|
||||
expect(typeof peer.id).toBe('string')
|
||||
expect(typeof session.id).toBe('string')
|
||||
|
||||
const message = peer.message('typed message', { metadata: { type: 'test' } });
|
||||
expect(typeof message.peer_id).toBe('string');
|
||||
expect(typeof message.content).toBe('string');
|
||||
expect(typeof message.metadata).toBe('object');
|
||||
const message = peer.message('typed message', {
|
||||
metadata: { type: 'test' },
|
||||
})
|
||||
expect(typeof message.peer_id).toBe('string')
|
||||
expect(typeof message.content).toBe('string')
|
||||
expect(typeof message.metadata).toBe('object')
|
||||
|
||||
// Mock successful operations
|
||||
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'typed-peer' }],
|
||||
summary: 'Test summary',
|
||||
});
|
||||
summary: {
|
||||
content: 'Test summary',
|
||||
message_id: 1,
|
||||
summary_type: 'short',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
token_count: 20,
|
||||
},
|
||||
})
|
||||
|
||||
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 context: SessionContext = await session.getContext()
|
||||
expect(typeof context.sessionId).toBe('string')
|
||||
expect(Array.isArray(context.messages)).toBe(true)
|
||||
expect(context.summary).not.toBeNull()
|
||||
expect(typeof context.summary?.content).toBe('string')
|
||||
expect(typeof context.length).toBe('number')
|
||||
expect(typeof context.toString()).toBe('string')
|
||||
|
||||
const openAI = context.toOpenAI(peer);
|
||||
const anthropic = context.toAnthropic('assistant');
|
||||
const openAI = context.toOpenAI(peer)
|
||||
const anthropic = context.toAnthropic('assistant')
|
||||
|
||||
expect(Array.isArray(openAI)).toBe(true);
|
||||
expect(Array.isArray(anthropic)).toBe(true);
|
||||
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');
|
||||
expect(typeof openAI[0].role).toBe('string')
|
||||
expect(typeof openAI[0].content).toBe('string')
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -455,6 +455,107 @@ describe('Session', () => {
|
|||
|
||||
await expect(session.addMessages({ peer_id: 'peer1', content: 'test' })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should add message with custom timestamp', async () => {
|
||||
const message = {
|
||||
peer_id: 'peer1',
|
||||
content: 'Message with timestamp',
|
||||
created_at: '2023-01-01T12:00:00Z',
|
||||
metadata: { test: 'timestamp' },
|
||||
};
|
||||
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: 'Message with timestamp',
|
||||
created_at: '2023-01-01T12:00:00Z',
|
||||
metadata: { test: 'timestamp' }
|
||||
}]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should add message with null timestamp', async () => {
|
||||
const message = {
|
||||
peer_id: 'peer1',
|
||||
content: 'Message without timestamp',
|
||||
created_at: null,
|
||||
metadata: { test: 'no_timestamp' },
|
||||
};
|
||||
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: 'Message without timestamp',
|
||||
created_at: null,
|
||||
metadata: { test: 'no_timestamp' }
|
||||
}]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should add mixed messages with and without timestamps', async () => {
|
||||
const messages = [
|
||||
{
|
||||
peer_id: 'peer1',
|
||||
content: 'Message with timestamp',
|
||||
created_at: '2023-01-01T12:00:00Z',
|
||||
metadata: { type: 'historical' }
|
||||
},
|
||||
{
|
||||
peer_id: 'peer2',
|
||||
content: 'Message without timestamp',
|
||||
metadata: { type: 'current' }
|
||||
},
|
||||
{
|
||||
peer_id: 'peer3',
|
||||
content: 'Message with null timestamp',
|
||||
created_at: null,
|
||||
metadata: { type: 'default' }
|
||||
}
|
||||
];
|
||||
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 with timestamp',
|
||||
created_at: '2023-01-01T12:00:00Z',
|
||||
metadata: { type: 'historical' }
|
||||
},
|
||||
{
|
||||
peer_id: 'peer2',
|
||||
content: 'Message without timestamp',
|
||||
metadata: { type: 'current' }
|
||||
},
|
||||
{
|
||||
peer_id: 'peer3',
|
||||
content: 'Message with null timestamp',
|
||||
created_at: null,
|
||||
metadata: { type: 'default' }
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessages', () => {
|
||||
|
|
@ -582,7 +683,13 @@ describe('Session', () => {
|
|||
{ id: 'msg1', content: 'Hello', peer_id: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hi there', peer_id: 'peer2' },
|
||||
],
|
||||
summary: 'Conversation summary',
|
||||
summary: {
|
||||
content: 'Conversation summary',
|
||||
message_id: 10,
|
||||
summary_type: 'short',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
token_count: 100
|
||||
},
|
||||
};
|
||||
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
|
||||
|
||||
|
|
@ -591,7 +698,7 @@ describe('Session', () => {
|
|||
expect(context).toBeInstanceOf(SessionContext);
|
||||
expect(context.sessionId).toBe('test-session');
|
||||
expect(context.messages).toEqual(mockContext.messages);
|
||||
expect(context.summary).toBe('Conversation summary');
|
||||
expect(context.summary?.content).toBe('Conversation summary');
|
||||
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
|
|
@ -602,7 +709,13 @@ describe('Session', () => {
|
|||
it('should get session context with options', async () => {
|
||||
const mockContext = {
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'peer1' }],
|
||||
summary: 'Brief summary',
|
||||
summary: {
|
||||
content: 'Brief summary',
|
||||
message_id: 5,
|
||||
summary_type: 'short',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
token_count: 50
|
||||
},
|
||||
};
|
||||
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
|
||||
|
||||
|
|
@ -624,7 +737,7 @@ describe('Session', () => {
|
|||
|
||||
const context = await session.getContext();
|
||||
|
||||
expect(context.summary).toBe('');
|
||||
expect(context.summary).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { SessionContext } from '../src/session_context';
|
||||
import { SessionContext, Summary } from '../src/session_context';
|
||||
import { Peer } from '../src/peer';
|
||||
|
||||
/**
|
||||
|
|
@ -17,6 +17,19 @@ function createTestMessage(id: string, content: string, peer_id: string, additio
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a test Summary object
|
||||
*/
|
||||
function createTestSummary(content: string): Summary {
|
||||
return new Summary({
|
||||
content,
|
||||
message_id: 1,
|
||||
summary_type: 'short',
|
||||
created_at: new Date().toISOString(),
|
||||
token_count: content.length
|
||||
});
|
||||
}
|
||||
|
||||
describe('SessionContext', () => {
|
||||
let sessionContext: SessionContext;
|
||||
let mockMessages: any[];
|
||||
|
|
@ -29,38 +42,39 @@ describe('SessionContext', () => {
|
|||
createTestMessage('msg4', 'I am doing well, thank you!', 'assistant'),
|
||||
];
|
||||
|
||||
sessionContext = new SessionContext('test-session', mockMessages, '');
|
||||
sessionContext = new SessionContext('test-session', mockMessages, null);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with all properties', () => {
|
||||
expect(sessionContext.sessionId).toBe('test-session');
|
||||
expect(sessionContext.messages).toEqual(mockMessages);
|
||||
expect(sessionContext.summary).toBe('');
|
||||
expect(sessionContext.summary).toBe(null);
|
||||
});
|
||||
|
||||
it('should initialize with empty summary when not provided', () => {
|
||||
it('should initialize with null 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('');
|
||||
expect(context.summary).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle empty messages array', () => {
|
||||
const context = new SessionContext('session-id', [], 'No messages');
|
||||
const summary = createTestSummary('No messages');
|
||||
const context = new SessionContext('session-id', [], summary);
|
||||
|
||||
expect(context.sessionId).toBe('session-id');
|
||||
expect(context.messages).toEqual([]);
|
||||
expect(context.summary).toBe('No messages');
|
||||
expect(context.summary).toBe(summary);
|
||||
});
|
||||
|
||||
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);
|
||||
const context2 = new SessionContext('session-id', mockMessages, null);
|
||||
|
||||
expect(context1.summary).toBe('');
|
||||
expect(context2.summary).toBe('');
|
||||
expect(context1.summary).toBe(null);
|
||||
expect(context2.summary).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -109,7 +123,8 @@ describe('SessionContext', () => {
|
|||
});
|
||||
|
||||
it('should include summary message when summary exists', () => {
|
||||
const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
|
||||
const summary = createTestSummary('This is a summary');
|
||||
const contextWithSummary = new SessionContext('test-session', mockMessages, summary);
|
||||
const openAIMessages = contextWithSummary.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
|
|
@ -201,7 +216,8 @@ describe('SessionContext', () => {
|
|||
});
|
||||
|
||||
it('should include summary message when summary exists', () => {
|
||||
const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
|
||||
const summary = createTestSummary('This is a summary');
|
||||
const contextWithSummary = new SessionContext('test-session', mockMessages, summary);
|
||||
const anthropicMessages = contextWithSummary.toAnthropic('assistant');
|
||||
|
||||
expect(anthropicMessages).toEqual([
|
||||
|
|
@ -250,13 +266,13 @@ describe('SessionContext', () => {
|
|||
describe('toString', () => {
|
||||
it('should return correct string representation', () => {
|
||||
const result = sessionContext.toString();
|
||||
expect(result).toBe('SessionContext(messages=4, summary=)');
|
||||
expect(result).toBe('SessionContext(messages=4, summary=none)');
|
||||
});
|
||||
|
||||
it('should handle empty messages', () => {
|
||||
const emptyContext = new SessionContext('session-id', []);
|
||||
const result = emptyContext.toString();
|
||||
expect(result).toBe('SessionContext(messages=0, summary=)');
|
||||
expect(result).toBe('SessionContext(messages=0, summary=none)');
|
||||
});
|
||||
|
||||
it('should handle large number of messages', () => {
|
||||
|
|
@ -266,7 +282,7 @@ describe('SessionContext', () => {
|
|||
const context = new SessionContext('session-id', manyMessages);
|
||||
|
||||
const result = context.toString();
|
||||
expect(result).toBe('SessionContext(messages=1000, summary=)');
|
||||
expect(result).toBe('SessionContext(messages=1000, summary=none)');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -305,11 +321,13 @@ describe('SessionContext', () => {
|
|||
|
||||
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 longSummaryText = 'Very long summary that goes on and on...'.repeat(100);
|
||||
const longSummary = createTestSummary(longSummaryText);
|
||||
const context = new SessionContext(longSessionId, mockMessages, longSummary);
|
||||
|
||||
expect(context.sessionId).toBe(longSessionId);
|
||||
expect(context.summary).toBe(longSummary);
|
||||
expect(context.summary?.content).toBe(longSummaryText);
|
||||
expect(context.length).toBe(5); // 4 messages + 1 summary
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "1.3.0",
|
||||
"@honcho-ai/core": "^1.4.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.3.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-yhxpZ00sQhO24PrHoTlj1cwR1q7/8FKSFZoCImqsFFmdERiXtE74YXhhfqzseyuShu1WfM7IwD8T2oCOB3hu7A=="],
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.4.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-pHyUgkiIlf+pMfZJx7NmJ1UH8yOKWZ+vmPRummg3olgstMXApcBrW0bhJTPjHWoGGA+OFYcOPxJBoZKY6VCtMw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -644,7 +644,7 @@
|
|||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="],
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Example of getting session summaries using the Honcho TypeScript SDK.
|
||||
*
|
||||
* This example demonstrates how to retrieve both short and long summaries
|
||||
* for a session, including their metadata like message ID, creation timestamp,
|
||||
* and token count.
|
||||
*/
|
||||
|
||||
import { Honcho, type SessionSummaries, type Summary } from "../src";
|
||||
|
||||
async function main() {
|
||||
// Initialize the Honcho client
|
||||
const apiKey = process.env.HONCHO_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("Please set HONCHO_API_KEY environment variable");
|
||||
}
|
||||
|
||||
const client = new Honcho({ apiKey });
|
||||
|
||||
// Get a session (replace with your actual session ID)
|
||||
const session = await client.session("my-conversation-session");
|
||||
|
||||
// Get summaries for the session
|
||||
const summaries: SessionSummaries = await session.getSummaries();
|
||||
|
||||
console.log(`Session ID: ${summaries.id}`);
|
||||
console.log("-".repeat(50));
|
||||
|
||||
// Check and display short summary
|
||||
if (summaries.shortSummary) {
|
||||
console.log("SHORT SUMMARY:");
|
||||
console.log(
|
||||
` Content: ${summaries.shortSummary.content.slice(0, 200)}...`,
|
||||
); // First 200 chars
|
||||
console.log(
|
||||
` Covers up to message ID: ${summaries.shortSummary.messageId}`,
|
||||
);
|
||||
console.log(` Created at: ${summaries.shortSummary.createdAt}`);
|
||||
console.log(` Token count: ${summaries.shortSummary.tokenCount}`);
|
||||
console.log(` Type: ${summaries.shortSummary.summaryType}`);
|
||||
} else {
|
||||
console.log("No short summary available yet");
|
||||
}
|
||||
|
||||
console.log("-".repeat(50));
|
||||
|
||||
// Check and display long summary
|
||||
if (summaries.longSummary) {
|
||||
console.log("LONG SUMMARY:");
|
||||
console.log(` Content: ${summaries.longSummary.content.slice(0, 200)}...`); // First 200 chars
|
||||
console.log(
|
||||
` Covers up to message ID: ${summaries.longSummary.messageId}`,
|
||||
);
|
||||
console.log(` Created at: ${summaries.longSummary.createdAt}`);
|
||||
console.log(` Token count: ${summaries.longSummary.tokenCount}`);
|
||||
console.log(` Type: ${summaries.longSummary.summaryType}`);
|
||||
} else {
|
||||
console.log("No long summary available yet");
|
||||
}
|
||||
|
||||
// Example showing how to use summaries in application logic
|
||||
console.log("\n" + "=".repeat(50));
|
||||
console.log("USAGE EXAMPLE:");
|
||||
console.log("=".repeat(50));
|
||||
|
||||
// Check if we have any summaries
|
||||
if (summaries.shortSummary || summaries.longSummary) {
|
||||
// Prefer long summary if available and under token limit
|
||||
const MAX_TOKENS = 2000;
|
||||
let selectedSummary: Summary | null = null;
|
||||
|
||||
if (
|
||||
summaries.longSummary &&
|
||||
summaries.longSummary.tokenCount <= MAX_TOKENS
|
||||
) {
|
||||
selectedSummary = summaries.longSummary;
|
||||
console.log("Using long summary (within token limit)");
|
||||
} else if (summaries.shortSummary) {
|
||||
selectedSummary = summaries.shortSummary;
|
||||
console.log("Using short summary");
|
||||
}
|
||||
|
||||
if (selectedSummary) {
|
||||
console.log(
|
||||
`Selected summary covers up to message ${selectedSummary.messageId}`,
|
||||
);
|
||||
console.log(`Token count: ${selectedSummary.tokenCount}`);
|
||||
|
||||
// You could use this summary as context for an LLM call
|
||||
// For example, with OpenAI:
|
||||
// const systemPrompt = `Previous conversation summary: ${selectedSummary.content}`
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"No summaries available - would need to use full message history",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@honcho-ai/sdk",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"description": "Official DX Optimized TypeScript SDK for Honcho",
|
||||
"author": "Plastic Labs <hello@plasticlabs.ai>",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "1.3.0",
|
||||
"@honcho-ai/core": "^1.4.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ export { Honcho } from './client'
|
|||
export { Page } from './pagination'
|
||||
export { Peer } from './peer'
|
||||
export { Session, SessionPeerConfig } from './session'
|
||||
export { SessionContext } from './session_context'
|
||||
export {
|
||||
SessionContext,
|
||||
SessionSummaries,
|
||||
Summary,
|
||||
SummaryData,
|
||||
} from './session_context'
|
||||
|
||||
// Export validation types for advanced usage
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -131,21 +131,31 @@ export class Peer {
|
|||
*
|
||||
* @param content - The text content for the message
|
||||
* @param metadata - Optional metadata to associate with the message
|
||||
* @param created_at - Optional ISO 8601 timestamp for the message
|
||||
* @returns A new message object with this peer's ID and the provided content
|
||||
*/
|
||||
message(
|
||||
content: string,
|
||||
options?: { metadata?: Record<string, unknown> }
|
||||
options?: {
|
||||
metadata?: Record<string, unknown>
|
||||
created_at?: string | Date
|
||||
}
|
||||
): ValidatedMessageCreate {
|
||||
const validatedContent = MessageContentSchema.parse(content)
|
||||
const validatedMetadata = options?.metadata
|
||||
? MessageMetadataSchema.parse(options.metadata)
|
||||
: undefined
|
||||
|
||||
const createdAt =
|
||||
options?.created_at instanceof Date
|
||||
? options.created_at.toISOString()
|
||||
: options?.created_at
|
||||
|
||||
return {
|
||||
peer_id: this.id,
|
||||
content: validatedContent,
|
||||
metadata: validatedMetadata,
|
||||
created_at: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/
|
|||
import type { Uploadable } from '@honcho-ai/core/src/uploads'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import { SessionContext } from './session_context'
|
||||
import { SessionContext, SessionSummaries, Summary } from './session_context'
|
||||
import {
|
||||
ContextParamsSchema,
|
||||
FileUploadSchema,
|
||||
|
|
@ -350,6 +350,12 @@ export class Session {
|
|||
* { peer_id: 'user1', content: 'Hello!' },
|
||||
* { peer_id: 'assistant', content: 'Hi there!' }
|
||||
* ])
|
||||
* // Add message with custom ISO 8601 timestamp
|
||||
* await session.addMessages({
|
||||
* peer_id: 'user123',
|
||||
* content: 'Hello world!',
|
||||
* created_at: '2021-01-01T00:00:00.000Z'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async addMessages(messages: MessageAddition): Promise<void> {
|
||||
|
|
@ -457,7 +463,37 @@ export class Session {
|
|||
summary: contextParams.summary,
|
||||
}
|
||||
)
|
||||
return new SessionContext(this.id, context.messages, context.summary)
|
||||
// Convert the summary response to Summary object if present
|
||||
const summary = context.summary ? new Summary(context.summary) : null
|
||||
return new SessionContext(this.id, context.messages, summary)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available summaries for this session.
|
||||
*
|
||||
* Makes an API call to retrieve both short and long summaries for this session,
|
||||
* if they are available. Summaries are created asynchronously by the backend
|
||||
* as messages are added to the session.
|
||||
*
|
||||
* @returns Promise resolving to a SessionSummaries object containing:
|
||||
* - id: The session ID
|
||||
* - shortSummary: The short summary if available, including metadata
|
||||
* - longSummary: The long summary if available, including metadata
|
||||
*
|
||||
* @note Summaries may be null if:
|
||||
* - Not enough messages have been added to trigger summary generation
|
||||
* - The summary generation is still in progress
|
||||
* - Summary generation is disabled for this session
|
||||
*/
|
||||
async getSummaries(): Promise<SessionSummaries> {
|
||||
// Use the core SDK's summaries method
|
||||
const data = await this._client.workspaces.sessions.summaries(
|
||||
this.workspaceId,
|
||||
this.id
|
||||
)
|
||||
|
||||
// Return a SessionSummaries instance
|
||||
return new SessionSummaries(data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,84 @@
|
|||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { Peer } from './peer'
|
||||
|
||||
export interface SummaryData {
|
||||
content: string
|
||||
message_id: number
|
||||
summary_type: string
|
||||
created_at: string
|
||||
token_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a summary of a session's conversation.
|
||||
*/
|
||||
export class Summary {
|
||||
/**
|
||||
* The summary text.
|
||||
*/
|
||||
readonly content: string
|
||||
|
||||
/**
|
||||
* The ID of the message that this summary covers up to.
|
||||
*/
|
||||
readonly messageId: number
|
||||
|
||||
/**
|
||||
* The type of summary (short or long).
|
||||
*/
|
||||
readonly summaryType: string
|
||||
|
||||
/**
|
||||
* The timestamp of when the summary was created (ISO format).
|
||||
*/
|
||||
readonly createdAt: string
|
||||
|
||||
/**
|
||||
* The number of tokens in the summary text.
|
||||
*/
|
||||
readonly tokenCount: number
|
||||
|
||||
constructor(data: SummaryData) {
|
||||
this.content = data.content
|
||||
this.messageId = data.message_id
|
||||
this.summaryType = data.summary_type
|
||||
this.createdAt = data.created_at
|
||||
this.tokenCount = data.token_count
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains both short and long summaries for a session.
|
||||
*/
|
||||
export class SessionSummaries {
|
||||
/**
|
||||
* The session ID.
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
* The short summary if available.
|
||||
*/
|
||||
readonly shortSummary: Summary | null
|
||||
|
||||
/**
|
||||
* The long summary if available.
|
||||
*/
|
||||
readonly longSummary: Summary | null
|
||||
|
||||
constructor(data: {
|
||||
id: string
|
||||
short_summary?: SummaryData | null
|
||||
long_summary?: SummaryData | null
|
||||
}) {
|
||||
this.id = data.id
|
||||
this.shortSummary = data.short_summary
|
||||
? new Summary(data.short_summary)
|
||||
: null
|
||||
this.longSummary = data.long_summary ? new Summary(data.long_summary) : null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context of a session containing a curated list of messages.
|
||||
*
|
||||
|
|
@ -22,7 +100,7 @@ export class SessionContext {
|
|||
/**
|
||||
* Summary of the session history prior to the message cutoff.
|
||||
*/
|
||||
readonly summary: string
|
||||
readonly summary: Summary | null
|
||||
|
||||
/**
|
||||
* Initialize a new SessionContext.
|
||||
|
|
@ -31,10 +109,14 @@ export class SessionContext {
|
|||
* @param messages List of Message objects to include in the context
|
||||
* @param summary Summary of the session history prior to the message cutoff
|
||||
*/
|
||||
constructor(sessionId: string, messages: Message[], summary: string = '') {
|
||||
constructor(
|
||||
sessionId: string,
|
||||
messages: Message[],
|
||||
summary: Summary | null = null
|
||||
) {
|
||||
this.sessionId = sessionId
|
||||
this.messages = messages
|
||||
this.summary = summary || ''
|
||||
this.summary = summary
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,16 +136,18 @@ export class SessionContext {
|
|||
assistant: string | Peer
|
||||
): Array<{ role: string; content: string; name?: string }> {
|
||||
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
|
||||
const summaryMessage = {
|
||||
role: 'system',
|
||||
content: `<summary>${this.summary}</summary>`,
|
||||
}
|
||||
const summaryMessage = this.summary
|
||||
? {
|
||||
role: 'system',
|
||||
content: `<summary>${this.summary.content}</summary>`,
|
||||
}
|
||||
: null
|
||||
const messages = this.messages.map((message) => ({
|
||||
role: message.peer_id === assistantId ? 'assistant' : 'user',
|
||||
name: message.peer_id,
|
||||
content: message.content,
|
||||
}))
|
||||
return this.summary ? [summaryMessage, ...messages] : messages
|
||||
return summaryMessage ? [summaryMessage, ...messages] : messages
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,10 +170,12 @@ export class SessionContext {
|
|||
assistant: string | Peer
|
||||
): Array<{ role: string; content: string }> {
|
||||
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
|
||||
const summaryMessage = {
|
||||
role: 'user',
|
||||
content: `<summary>${this.summary}</summary>`,
|
||||
}
|
||||
const summaryMessage = this.summary
|
||||
? {
|
||||
role: 'user',
|
||||
content: `<summary>${this.summary.content}</summary>`,
|
||||
}
|
||||
: null
|
||||
const messages = this.messages.map((message) =>
|
||||
message.peer_id === assistantId
|
||||
? {
|
||||
|
|
@ -101,20 +187,20 @@ export class SessionContext {
|
|||
content: `${message.peer_id}: ${message.content}`,
|
||||
}
|
||||
)
|
||||
return this.summary ? [summaryMessage, ...messages] : messages
|
||||
return summaryMessage ? [summaryMessage, ...messages] : messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of messages in the context.
|
||||
*/
|
||||
get length(): number {
|
||||
return this.messages.length + (this.summary.length > 0 ? 1 : 0)
|
||||
return this.messages.length + (this.summary ? 1 : 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the SessionContext.
|
||||
*/
|
||||
toString(): string {
|
||||
return `SessionContext(messages=${this.messages.length}, summary=${this.summary})`
|
||||
return `SessionContext(messages=${this.messages.length}, summary=${this.summary ? 'present' : 'none'})`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export const MessageCreateSchema = z.object({
|
|||
peer_id: PeerIdSchema,
|
||||
content: MessageContentSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
created_at: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ async def create_messages(
|
|||
workspace_name=workspace_name,
|
||||
public_id=generate_nanoid(),
|
||||
token_count=len(message.encoded_message),
|
||||
created_at=message.created_at, # Use provided created_at if available
|
||||
)
|
||||
message_objects.append(message_obj)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from src.utils.formatting import (
|
|||
find_new_observations,
|
||||
format_context_for_prompt,
|
||||
format_new_turn_with_timestamp,
|
||||
utc_now_iso,
|
||||
)
|
||||
from src.utils.logging import (
|
||||
accumulate_metric,
|
||||
|
|
@ -630,7 +631,7 @@ async def save_working_representation_to_peer(
|
|||
working_rep_data = {
|
||||
"final_observations": final_obs_dict,
|
||||
"message_id": message_id,
|
||||
"created_at": datetime.datetime.now().isoformat(),
|
||||
"created_at": utc_now_iso(),
|
||||
}
|
||||
|
||||
# if session_name is supplied, save working representation to session peer
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from mirascope.llm import Stream
|
|||
from src import crud
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.routers.sessions import get_session_context
|
||||
from src.utils import summarizer
|
||||
from src.utils.clients import honcho_llm_call
|
||||
from src.utils.embedding_store import EmbeddingStore
|
||||
from src.utils.logging import (
|
||||
|
|
@ -262,25 +262,16 @@ async def chat(
|
|||
# If query is session-scoped, get recent conversation history from that session
|
||||
if session_name:
|
||||
async with tracked_db("chat.get_session_context") as db:
|
||||
session_context = await get_session_context(
|
||||
workspace_id=workspace_name,
|
||||
session_id=session_name,
|
||||
tokens=context_window_size,
|
||||
summary=True,
|
||||
db=db,
|
||||
recent_conversation_history = (
|
||||
await summarizer.get_session_context_formatted(
|
||||
db,
|
||||
workspace_name=workspace_name,
|
||||
session_name=session_name,
|
||||
token_limit=context_window_size,
|
||||
include_summary=True,
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
"Retrieved recent conversation history with %s messages",
|
||||
len(session_context.messages),
|
||||
)
|
||||
recent_conversation_history = f"""
|
||||
<summary>
|
||||
{session_context.summary}
|
||||
</summary>
|
||||
<recent_messages>
|
||||
{session_context.messages}
|
||||
</recent_messages>
|
||||
"""
|
||||
logger.info("Retrieved recent conversation history")
|
||||
else:
|
||||
recent_conversation_history = None
|
||||
logger.info("Query is not session-scoped, skipping recent conversation history")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from src.utils.clients import honcho_llm_call
|
|||
from src.utils.embedding_store import EmbeddingStore
|
||||
from src.utils.formatting import (
|
||||
format_premises_for_display,
|
||||
parse_datetime_iso,
|
||||
)
|
||||
from src.utils.logging import conditional_observe
|
||||
from src.utils.shared_models import SemanticQueries
|
||||
|
|
@ -190,11 +191,9 @@ def _format_observations(
|
|||
if last_accessed:
|
||||
# Format the last_accessed datetime for display
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
if isinstance(last_accessed, str):
|
||||
# Parse ISO format datetime string
|
||||
dt = datetime.fromisoformat(last_accessed.replace("Z", "+00:00"))
|
||||
dt = parse_datetime_iso(last_accessed)
|
||||
formatted_last_accessed: str = dt.strftime("%Y-%m-%d %H:%M")
|
||||
access_parts.append(f"last accessed {formatted_last_accessed}")
|
||||
except (ValueError, AttributeError):
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ app = FastAPI(
|
|||
title="Honcho API",
|
||||
summary="The Identity Layer for the Agentic World",
|
||||
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
|
||||
version="2.2.0",
|
||||
version="2.3.0",
|
||||
contact={
|
||||
"name": "Plastic Labs",
|
||||
"url": "https://honcho.dev",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from src.security import (
|
|||
create_jwt,
|
||||
require_auth,
|
||||
)
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ async def create_key(
|
|||
|
||||
key_str = create_jwt(
|
||||
JWTParams(
|
||||
exp=expires_at.isoformat() if expires_at else None,
|
||||
exp=format_datetime_utc(expires_at) if expires_at else None,
|
||||
w=workspace_id,
|
||||
p=peer_id,
|
||||
s=session_id,
|
||||
|
|
|
|||
|
|
@ -374,9 +374,11 @@ async def get_session_context(
|
|||
le=config.settings.GET_CONTEXT_MAX_TOKENS,
|
||||
description=f"Number of tokens to use for the context. Includes summary if set to true. If not provided, the context will be exhaustive (within {config.settings.GET_CONTEXT_MAX_TOKENS} tokens)",
|
||||
),
|
||||
summary: bool = Query(
|
||||
True,
|
||||
*,
|
||||
include_summary: bool = Query(
|
||||
default=True,
|
||||
description="Whether or not to include a summary *if* one is available for the session",
|
||||
alias="summary",
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
|
|
@ -389,18 +391,59 @@ async def get_session_context(
|
|||
token_limit = tokens or config.settings.GET_CONTEXT_MAX_TOKENS
|
||||
|
||||
# Use the shared get_session_context function from summarizer
|
||||
summary_content, messages = await summarizer.get_session_context(
|
||||
summary_obj, messages = await summarizer.get_session_context(
|
||||
db,
|
||||
workspace_name=workspace_id,
|
||||
session_name=session_id,
|
||||
token_limit=token_limit,
|
||||
include_summary=summary,
|
||||
include_summary=include_summary,
|
||||
)
|
||||
|
||||
return schemas.SessionContext(
|
||||
name=session_id,
|
||||
messages=messages, # pyright: ignore -- db message type and schema message type are different, but excess gets removed by schema
|
||||
summary=summary_content,
|
||||
summary=summary_obj,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{session_id}/summaries",
|
||||
response_model=schemas.SessionSummaries,
|
||||
dependencies=[
|
||||
Depends(require_auth(workspace_name="workspace_id", session_name="session_id"))
|
||||
],
|
||||
)
|
||||
async def get_session_summaries(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
db: AsyncSession = db,
|
||||
) -> schemas.SessionSummaries:
|
||||
"""
|
||||
Get available summaries for a session.
|
||||
|
||||
Returns both short and long summaries if available, including metadata like
|
||||
the message ID they cover up to, creation timestamp, and token count.
|
||||
"""
|
||||
# Reuse the logic from get_context to get both summaries
|
||||
short_summary, long_summary = await summarizer.get_both_summaries(
|
||||
db,
|
||||
workspace_name=workspace_id,
|
||||
session_name=session_id,
|
||||
)
|
||||
|
||||
# Convert the internal Summary TypedDict to our Pydantic schema
|
||||
short_summary_schema = None
|
||||
if short_summary:
|
||||
short_summary_schema = summarizer.to_schema_summary(short_summary)
|
||||
|
||||
long_summary_schema = None
|
||||
if long_summary:
|
||||
long_summary_schema = summarizer.to_schema_summary(long_summary)
|
||||
|
||||
return schemas.SessionSummaries(
|
||||
name=session_id,
|
||||
short_summary=short_summary_schema,
|
||||
long_summary=long_summary_schema,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ class MessageCreate(MessageBase):
|
|||
content: Annotated[str, Field(min_length=0, max_length=50000)]
|
||||
peer_name: str = Field(alias="peer_id")
|
||||
metadata: dict[str, Any] | None = None
|
||||
created_at: datetime.datetime | None = None
|
||||
|
||||
_encoded_message: list[int] = PrivateAttr(default=[])
|
||||
|
||||
|
|
@ -223,10 +224,38 @@ class Session(SessionBase):
|
|||
)
|
||||
|
||||
|
||||
class Summary(BaseModel):
|
||||
content: str = Field(description="The summary text")
|
||||
message_id: int = Field(
|
||||
description="The ID of the message that this summary covers up to"
|
||||
)
|
||||
summary_type: str = Field(description="The type of summary (short or long)")
|
||||
created_at: str = Field(
|
||||
description="The timestamp of when the summary was created (ISO format)"
|
||||
)
|
||||
token_count: int = Field(description="The number of tokens in the summary text")
|
||||
|
||||
|
||||
class SessionContext(SessionBase):
|
||||
name: str = Field(serialization_alias="id")
|
||||
messages: list[Message]
|
||||
summary: str
|
||||
summary: Summary | None = Field(
|
||||
default=None, description="The summary if available"
|
||||
)
|
||||
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class SessionSummaries(SessionBase):
|
||||
name: str = Field(serialization_alias="id")
|
||||
short_summary: Summary | None = Field(
|
||||
default=None, description="The short summary if available"
|
||||
)
|
||||
long_summary: Summary | None = Field(
|
||||
default=None, description="The long summary if available"
|
||||
)
|
||||
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.config import settings
|
||||
from src.utils.formatting import utc_now_iso
|
||||
|
||||
from .exceptions import AuthenticationException
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ class JWTParams(BaseModel):
|
|||
`s`: (string) session name
|
||||
"""
|
||||
|
||||
t: str = datetime.datetime.now().isoformat()
|
||||
t: str = utc_now_iso()
|
||||
exp: str | None = None
|
||||
ad: bool | None = None
|
||||
w: str | None = None
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from src import crud, models
|
|||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.embedding_client import embedding_client
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
from src.utils.logging import conditional_observe
|
||||
from src.utils.shared_models import (
|
||||
Observation,
|
||||
|
|
@ -115,7 +116,7 @@ class EmbeddingStore:
|
|||
"message_id": message_id,
|
||||
"session_name": session_name,
|
||||
"premises": obs.premises, # Store premises in metadata
|
||||
"created_at": message_created_at.isoformat()
|
||||
"created_at": format_datetime_utc(message_created_at)
|
||||
if message_created_at
|
||||
else None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from sqlalchemy import ColumnElement, Select, and_, case, cast, literal, not_, o
|
|||
from sqlalchemy.types import Numeric
|
||||
|
||||
from ..exceptions import FilterError
|
||||
from .formatting import parse_datetime_iso
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
|
@ -561,9 +562,9 @@ def _validate_datetime_string(value: str) -> datetime.datetime | None:
|
|||
except ValueError:
|
||||
continue
|
||||
|
||||
# Try fromisoformat as a fallback (Python 3.7+)
|
||||
# Try fromisoformat as a fallback (Python 3.7+) with Z format support
|
||||
try:
|
||||
return datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
return parse_datetime_iso(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,83 @@ This module contains helper functions for processing observations, formatting co
|
|||
and handling temporal metadata for the reasoning system.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Protocol, cast, runtime_checkable
|
||||
|
||||
from src.utils.logging import conditional_observe
|
||||
from src.utils.shared_models import ReasoningResponse
|
||||
|
||||
|
||||
def format_datetime_utc(dt: datetime) -> str:
|
||||
"""
|
||||
Format datetime to ISO 8601 string with Z suffix for UTC timezone.
|
||||
|
||||
This ensures consistent datetime formatting across the entire backend,
|
||||
using the Z format which is the ISO 8601 standard for UTC and matches
|
||||
Pydantic's JSON serialization behavior.
|
||||
|
||||
Args:
|
||||
dt: datetime object (should be timezone-aware)
|
||||
|
||||
Returns:
|
||||
ISO 8601 formatted string with Z suffix for UTC
|
||||
|
||||
Example:
|
||||
>>> dt = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
>>> format_datetime_utc(dt)
|
||||
'2023-01-01T12:00:00Z'
|
||||
"""
|
||||
if dt.tzinfo is None:
|
||||
# If no timezone info, assume UTC
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
# Convert to UTC if not already
|
||||
if dt.tzinfo != timezone.utc:
|
||||
dt = dt.astimezone(timezone.utc)
|
||||
|
||||
# Format and replace +00:00 with Z
|
||||
return dt.isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def utc_now_iso() -> str:
|
||||
"""
|
||||
Get current UTC time as ISO 8601 string with Z suffix.
|
||||
|
||||
Returns:
|
||||
Current UTC time in ISO 8601 format with Z suffix
|
||||
|
||||
Example:
|
||||
>>> utc_now_iso()
|
||||
'2023-01-01T12:34:56.789123Z'
|
||||
"""
|
||||
return format_datetime_utc(datetime.now(timezone.utc))
|
||||
|
||||
|
||||
def parse_datetime_iso(iso_string: str) -> datetime:
|
||||
"""
|
||||
Parse ISO 8601 datetime string, handling both Z and +00:00 UTC formats.
|
||||
|
||||
This function handles the fact that Python's fromisoformat() doesn't
|
||||
directly support the 'Z' suffix, which is the standard ISO 8601 way
|
||||
to represent UTC timezone.
|
||||
|
||||
Args:
|
||||
iso_string: ISO 8601 formatted datetime string
|
||||
|
||||
Returns:
|
||||
datetime object with timezone information
|
||||
|
||||
Example:
|
||||
>>> parse_datetime_iso('2023-01-01T12:00:00Z')
|
||||
datetime.datetime(2023, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
|
||||
>>> parse_datetime_iso('2023-01-01T12:00:00+00:00')
|
||||
datetime.datetime(2023, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
|
||||
"""
|
||||
# Convert Z format to +00:00 format for Python's fromisoformat
|
||||
normalized_string = iso_string.replace("Z", "+00:00")
|
||||
return datetime.fromisoformat(normalized_string)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class StructuredObservation(Protocol):
|
||||
"""Protocol for observations that have conclusion and premises attributes."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum
|
||||
|
|
@ -9,10 +8,12 @@ from sqlalchemy import update
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from src import schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.exceptions import ResourceNotFoundException
|
||||
from src.utils.clients import honcho_llm_call
|
||||
from src.utils.formatting import utc_now_iso
|
||||
from src.utils.logging import accumulate_metric
|
||||
|
||||
from .. import crud, models
|
||||
|
|
@ -40,6 +41,16 @@ class Summary(TypedDict):
|
|||
token_count: int
|
||||
|
||||
|
||||
def to_schema_summary(s: Summary) -> schemas.Summary:
|
||||
return schemas.Summary(
|
||||
content=s["content"],
|
||||
message_id=s["message_id"],
|
||||
summary_type=s["summary_type"],
|
||||
created_at=s["created_at"],
|
||||
token_count=s["token_count"],
|
||||
)
|
||||
|
||||
|
||||
# Export the public functions
|
||||
__all__ = [
|
||||
"get_summary",
|
||||
|
|
@ -49,6 +60,7 @@ __all__ = [
|
|||
"get_session_context_formatted",
|
||||
"SummaryType",
|
||||
"Summary",
|
||||
"to_schema_summary",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -388,7 +400,7 @@ async def _create_summary(
|
|||
content=summary_text,
|
||||
message_id=messages[-1].id if messages else 0,
|
||||
summary_type=summary_type.value,
|
||||
created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
created_at=utc_now_iso(),
|
||||
token_count=summary_tokens,
|
||||
)
|
||||
|
||||
|
|
@ -564,7 +576,7 @@ async def get_session_context(
|
|||
*,
|
||||
cutoff: int | None = None,
|
||||
include_summary: bool = True,
|
||||
) -> tuple[str, list[models.Message]]:
|
||||
) -> tuple[schemas.Summary | None, list[models.Message]]:
|
||||
"""
|
||||
Get session context similar to the API endpoint but for internal use.
|
||||
|
||||
|
|
@ -577,10 +589,10 @@ async def get_session_context(
|
|||
include_summary: Whether to include summary if available
|
||||
|
||||
Returns:
|
||||
Tuple of (summary_content, messages) where summary_content is the summary text (or empty string)
|
||||
Tuple of (summary, messages) where summary is a Summary pydantic model (or None)
|
||||
and messages is the list of message objects
|
||||
"""
|
||||
summary_content = ""
|
||||
summary = None
|
||||
messages_tokens = token_limit
|
||||
messages_start_id = 0
|
||||
|
||||
|
|
@ -601,13 +613,25 @@ async def get_session_context(
|
|||
and long_len <= summary_tokens_limit
|
||||
and long_len > short_len
|
||||
):
|
||||
summary_content = latest_long_summary["content"]
|
||||
summary = schemas.Summary(
|
||||
content=latest_long_summary["content"],
|
||||
message_id=latest_long_summary["message_id"],
|
||||
summary_type=latest_long_summary["summary_type"],
|
||||
created_at=latest_long_summary["created_at"],
|
||||
token_count=latest_long_summary["token_count"],
|
||||
)
|
||||
messages_tokens = token_limit - latest_long_summary["token_count"]
|
||||
messages_start_id = latest_long_summary["message_id"]
|
||||
elif (
|
||||
latest_short_summary and short_len <= summary_tokens_limit and short_len > 0
|
||||
):
|
||||
summary_content = latest_short_summary["content"]
|
||||
summary = schemas.Summary(
|
||||
content=latest_short_summary["content"],
|
||||
message_id=latest_short_summary["message_id"],
|
||||
summary_type=latest_short_summary["summary_type"],
|
||||
created_at=latest_short_summary["created_at"],
|
||||
token_count=latest_short_summary["token_count"],
|
||||
)
|
||||
messages_tokens = token_limit - latest_short_summary["token_count"]
|
||||
messages_start_id = latest_short_summary["message_id"]
|
||||
else:
|
||||
|
|
@ -628,7 +652,7 @@ async def get_session_context(
|
|||
token_limit=messages_tokens,
|
||||
)
|
||||
|
||||
return summary_content, messages
|
||||
return summary, messages
|
||||
|
||||
|
||||
async def get_session_context_formatted(
|
||||
|
|
@ -646,7 +670,7 @@ async def get_session_context_formatted(
|
|||
This is a convenience wrapper around get_session_context that formats
|
||||
the output as a string.
|
||||
"""
|
||||
summary_content, messages = await get_session_context(
|
||||
summary, messages = await get_session_context(
|
||||
db,
|
||||
workspace_name,
|
||||
session_name,
|
||||
|
|
@ -657,6 +681,7 @@ async def get_session_context_formatted(
|
|||
|
||||
# Format the messages
|
||||
messages_text = _format_messages(messages)
|
||||
summary_content = summary.content if summary else ""
|
||||
|
||||
if summary_content and messages_text:
|
||||
return f"""<summary>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import hashlib
|
|||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -11,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src.config import settings
|
||||
from src.crud.webhook import list_webhook_endpoints
|
||||
from src.deriver.queue_payload import WebhookPayload
|
||||
from src.utils.formatting import utc_now_iso
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ async def deliver_webhook(db: AsyncSession, payload: WebhookPayload) -> None:
|
|||
event_payload = {
|
||||
"type": payload.event_type,
|
||||
"data": payload.data,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp": utc_now_iso(),
|
||||
}
|
||||
event_json = json.dumps(
|
||||
event_payload, separators=(",", ":"), sort_keys=True
|
||||
|
|
|
|||
|
|
@ -509,8 +509,12 @@ Evaluate whether the actual response contains the core correct information from
|
|||
summary=summary, tokens=max_tokens
|
||||
)
|
||||
|
||||
summary_content = ""
|
||||
if session_context.summary:
|
||||
summary_content = session_context.summary.content
|
||||
|
||||
tokenizer = tiktoken.get_encoding("cl100k_base")
|
||||
summary_tokens = len(tokenizer.encode(session_context.summary))
|
||||
summary_tokens = len(tokenizer.encode(summary_content))
|
||||
print(f" summary: {session_context.summary}")
|
||||
|
||||
got_tokens = summary_tokens
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -921,3 +922,219 @@ async def test_create_messages_with_file_too_large(
|
|||
|
||||
# Should raise FileTooLargeError which gets converted to 413
|
||||
assert response.status_code == 413
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_message_with_timestamp(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating a message with custom timestamp"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Use a specific timestamp for testing
|
||||
custom_timestamp = datetime.datetime(
|
||||
2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
||||
json={
|
||||
"messages": [
|
||||
{
|
||||
"content": "Test message with timestamp",
|
||||
"peer_id": test_peer.name,
|
||||
"created_at": custom_timestamp.isoformat(),
|
||||
"metadata": {"test": "with_timestamp"},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
message = data[0]
|
||||
assert message["content"] == "Test message with timestamp"
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["metadata"] == {"test": "with_timestamp"}
|
||||
|
||||
# Verify the created_at field matches our custom timestamp
|
||||
# Pydantic serializes UTC timezone as 'Z' format (ISO 8601 standard)
|
||||
expected_timestamp = "2023-01-01T12:00:00Z"
|
||||
assert message["created_at"] == expected_timestamp
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_message_without_timestamp_uses_default(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating a message without timestamp uses default timestamp"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Record time before request
|
||||
before_request = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
||||
json={
|
||||
"messages": [
|
||||
{
|
||||
"content": "Test message without timestamp",
|
||||
"peer_id": test_peer.name,
|
||||
"metadata": {"test": "no_timestamp"},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Record time after request
|
||||
after_request = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
message = data[0]
|
||||
assert message["content"] == "Test message without timestamp"
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["metadata"] == {"test": "no_timestamp"}
|
||||
|
||||
# Verify the created_at field is between our before/after times
|
||||
message_created_at = datetime.datetime.fromisoformat(
|
||||
message["created_at"].replace("Z", "+00:00")
|
||||
)
|
||||
assert before_request <= message_created_at <= after_request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages_with_mixed_timestamps(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating batch messages with some having custom timestamps and others using default"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Use specific timestamps for testing
|
||||
timestamp1 = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
timestamp2 = datetime.datetime(2023, 1, 2, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Record time before request for default timestamp
|
||||
before_request = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
||||
json={
|
||||
"messages": [
|
||||
{
|
||||
"content": "Message 1 with timestamp",
|
||||
"peer_id": test_peer.name,
|
||||
"created_at": timestamp1.isoformat(),
|
||||
"metadata": {"type": "custom_timestamp"},
|
||||
},
|
||||
{
|
||||
"content": "Message 2 without timestamp",
|
||||
"peer_id": test_peer.name,
|
||||
"metadata": {"type": "default_timestamp"},
|
||||
},
|
||||
{
|
||||
"content": "Message 3 with timestamp",
|
||||
"peer_id": test_peer.name,
|
||||
"created_at": timestamp2.isoformat(),
|
||||
"metadata": {"type": "custom_timestamp"},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
after_request = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 3
|
||||
|
||||
# Check first message with custom timestamp
|
||||
assert data[0]["content"] == "Message 1 with timestamp"
|
||||
assert (
|
||||
data[0]["created_at"] == "2023-01-01T12:00:00Z"
|
||||
) # Pydantic converts to Z format
|
||||
assert data[0]["metadata"] == {"type": "custom_timestamp"}
|
||||
|
||||
# Check second message with default timestamp
|
||||
assert data[1]["content"] == "Message 2 without timestamp"
|
||||
message2_created_at = datetime.datetime.fromisoformat(
|
||||
data[1]["created_at"].replace("Z", "+00:00")
|
||||
)
|
||||
assert before_request <= message2_created_at <= after_request
|
||||
assert data[1]["metadata"] == {"type": "default_timestamp"}
|
||||
|
||||
# Check third message with custom timestamp
|
||||
assert data[2]["content"] == "Message 3 with timestamp"
|
||||
assert (
|
||||
data[2]["created_at"] == "2023-01-02T12:00:00Z"
|
||||
) # Pydantic converts to Z format
|
||||
assert data[2]["metadata"] == {"type": "custom_timestamp"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_message_with_null_timestamp(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating a message with null timestamp uses default"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Record time before request
|
||||
before_request = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
||||
json={
|
||||
"messages": [
|
||||
{
|
||||
"content": "Test message with null timestamp",
|
||||
"peer_id": test_peer.name,
|
||||
"created_at": None,
|
||||
"metadata": {"test": "null_timestamp"},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
after_request = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
message = data[0]
|
||||
assert message["content"] == "Test message with null timestamp"
|
||||
assert message["metadata"] == {"test": "null_timestamp"}
|
||||
|
||||
# Verify the created_at field uses default (current time)
|
||||
message_created_at = datetime.datetime.fromisoformat(
|
||||
message["created_at"].replace("Z", "+00:00")
|
||||
)
|
||||
assert before_request <= message_created_at <= after_request
|
||||
|
|
|
|||
|
|
@ -784,7 +784,7 @@ def test_get_session_context(client: TestClient, sample_data: tuple[Workspace, P
|
|||
assert "summary" in data
|
||||
assert data["id"] == session_id
|
||||
assert isinstance(data["messages"], list)
|
||||
assert data["summary"] == "" # Default is empty when summary=False
|
||||
assert data["summary"] is None # No summary available
|
||||
|
||||
|
||||
def test_get_session_context_with_summary(
|
||||
|
|
@ -855,6 +855,55 @@ def test_get_session_context_with_all_params(
|
|||
assert "summary" in data
|
||||
|
||||
|
||||
def test_get_session_summaries(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
) -> None:
|
||||
"""Test getting summaries for a valid session"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
||||
# Create session
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions",
|
||||
json={"id": session_id, "peers": {test_peer.name: {}}},
|
||||
)
|
||||
|
||||
# Get summaries
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/summaries",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Validate response structure
|
||||
assert "id" in data
|
||||
assert data["id"] == session_id
|
||||
assert "short_summary" in data
|
||||
assert "long_summary" in data
|
||||
# Summaries will be None since they're created asynchronously
|
||||
assert data["short_summary"] is None
|
||||
assert data["long_summary"] is None
|
||||
|
||||
|
||||
def test_get_session_summaries_nonexistent_session(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
) -> None:
|
||||
"""Test getting summaries for a non-existent session"""
|
||||
test_workspace, _ = sample_data
|
||||
nonexistent_session_id = str(generate_nanoid())
|
||||
|
||||
# Try to get summaries for non-existent session
|
||||
# Should still return 200 with null summaries
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{nonexistent_session_id}/summaries",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == nonexistent_session_id
|
||||
assert data["short_summary"] is None
|
||||
assert data["long_summary"] is None
|
||||
|
||||
|
||||
def test_search_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test the session search functionality"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
|
|||
41
uv.lock
41
uv.lock
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
|
|
@ -7,6 +7,12 @@ resolution-markers = [
|
|||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[manifest]
|
||||
members = [
|
||||
"honcho",
|
||||
"honcho-ai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.16.4"
|
||||
|
|
@ -822,7 +828,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "honcho"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
|
|
@ -853,7 +859,6 @@ dev = [
|
|||
{ name = "basedpyright" },
|
||||
{ name = "coverage" },
|
||||
{ name = "honcho-ai" },
|
||||
{ name = "honcho-core" },
|
||||
{ name = "interrogate" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "py-spy" },
|
||||
|
|
@ -893,8 +898,7 @@ requires-dist = [
|
|||
dev = [
|
||||
{ name = "basedpyright", specifier = ">=1.29.4" },
|
||||
{ name = "coverage", specifier = ">=7.6.0" },
|
||||
{ name = "honcho-ai", specifier = "==1.3.0" },
|
||||
{ name = "honcho-core", specifier = "==1.3.0" },
|
||||
{ name = "honcho-ai", editable = "sdks/python" },
|
||||
{ name = "interrogate", specifier = ">=1.7.0" },
|
||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||
{ name = "py-spy", specifier = ">=0.3.14" },
|
||||
|
|
@ -907,21 +911,32 @@ dev = [
|
|||
|
||||
[[package]]
|
||||
name = "honcho-ai"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
version = "1.4.0"
|
||||
source = { editable = "sdks/python" }
|
||||
dependencies = [
|
||||
{ name = "honcho-core" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/d3/c882f95637e30124c57a3496ac8221865c2449323f5bd6823009658ce8f7/honcho_ai-1.3.0.tar.gz", hash = "sha256:674440071ae66f3382c7a2355e1c9e8a2c9f0f9482ca0633151d30d42b20ea2c", size = 25042, upload-time = "2025-08-06T21:49:15.655Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/f9/0d3c71f4a400da433119eabfb03837a00f1badd31994d578e4fdc9f95c1f/honcho_ai-1.3.0-py3-none-any.whl", hash = "sha256:cb610384956752a8a33f7dee1a01f10b7732c76417b72c87f0d91dea2acd1174", size = 32599, upload-time = "2025-08-06T21:49:14.418Z" },
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = ">=1.4.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
||||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -931,9 +946,9 @@ dependencies = [
|
|||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/49/1810de2351be1fdff330ffadcc5eef709889b2f698b0340966cae2c1eef9/honcho_core-1.3.0.tar.gz", hash = "sha256:276a73e8d523f7d22f06746922fda97e4a11fbbef4a51a0b43573bc817089cec", size = 123333, upload-time = "2025-08-06T16:42:10.941Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/c2/4d3737d5c0a75e2f59f324bed7428a9593c8edfcb7500cf402c46ad7378a/honcho_core-1.4.0.tar.gz", hash = "sha256:b30de9247763c01c3c37ed22b5d04d3c611440dd1983168f3d7d93f2cc0ecf7d", size = 126164, upload-time = "2025-08-12T19:06:54.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/3f/3beb750ed65bb1b604c585ae1c51a54f8b34ea3002f4525f93fe86549531/honcho_core-1.3.0-py3-none-any.whl", hash = "sha256:c7c0bf77b61162e6c65a580327fc054785aef794923abc35acd3dbd8287245db", size = 112895, upload-time = "2025-08-06T16:42:08.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ac/6eb2e38376736bb1dfd26bbdfd96490bfbb6f0ddcb00b39913e93799e7d8/honcho_core-1.4.0-py3-none-any.whl", hash = "sha256:a84397fd9daf546a04f5458d7233ebd5eeafcb97af75ec60a9327db89abdaf03", size = 117602, upload-time = "2025-08-12T19:06:53.667Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Reference in New Issue