API/SDK updates: configurability, more parameters. Unified test harness (#283)
* feat: add better params to working representation fetch in SDKs, return messages when added * fix: working representation routes now accepting all parameters properly, with tests * feat: add metadata/config fields to SDK objects where viable * fix: tests * feat: refactor SDKs to use representation config; [TEMP STAINLESS BUILD] update API * feat: add representation object to sdks * fix: use stainless sdk on branch * fix: update TypeScript SDK tsconfig to use node16 module resolution * fix: add isolatedModules = true to tsconfig * fix: lol * chore: coderabbit review * feat: make delete session real * feat: add observations routes with delete endpoints for documents. make session deletion real. * chore: type cleanup * fix: tests * chore: coderabbit review * fix: namespace by workspace * feat: add ability to customize messages_per_summary at both workspace and session level * chore: tests for summary config * chore: coderabbit cleanup * feat: make session and workspace config totally customizeable * feat: add search by peer knowledge (#250) * feat: search by peer perspective * fix: enforce workspace in filters, make messages distinct in join * fix: batch and merge migration steps * fix: add refresh, add config to workspace, add refresh function, make fields readonly * fix: search distinct * fix: merge migrations * fix: merge migrations * fix: batch deletions, improve comments, limit consolidate dream to 100 docs at a time, auth on observations routes * chore: review * chore: coderabbit * chore: review * chore: broken comment * feat: add set peer card route to API * feat: create advanced configuration parameters with message>session>workspace hierarchy * [wip] build unified testing harness * chore: lint * fix: cache invalidation, naming things, etc * feat: longmem tests * chore: peer config refactor * feat: consolidate dream working, refactor representation * fix: Various CR Comment Fixes * feat: Allow configurable Redis port for harness instances and update cleanup methods to be asynchronous. * fix: version bump, api/sdk updates * fix: observation endpoints, deletion queue, sdk observation implementation * chore: Fix migration order * fix: Use published stainless sdks * chore: (docs) update api-reference * fix: (docs) update based on api and sdk changes * fix: Code Rabbit Comments * fix: Code Rabbit Final Nits * fix: dream scheduler * fix: SDK model type consistency --------- Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
This commit is contained in:
parent
b3e715d1d6
commit
e3d345b961
|
|
@ -101,6 +101,8 @@ jobs:
|
|||
SENTRY_ENABLED: false
|
||||
LLM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLM_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
LLM_OPENAI_COMPATIBLE_API_KEY: test-key
|
||||
LLM_OPENAI_COMPATIBLE_BASE_URL: http://localhost:8000
|
||||
DERIVER_PROVIDER: openai
|
||||
DERIVER_MODEL: test
|
||||
DIALECTIC_PROVIDER: openai
|
||||
|
|
|
|||
|
|
@ -9,12 +9,7 @@
|
|||
},
|
||||
"favicon": "/favicon.svg",
|
||||
"contextual": {
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
"chatgpt",
|
||||
"claude"
|
||||
]
|
||||
"options": ["copy", "view", "chatgpt", "claude"]
|
||||
},
|
||||
"navigation": {
|
||||
"versions": [
|
||||
|
|
@ -68,20 +63,19 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"v2/guides/overview"
|
||||
]
|
||||
"pages": ["v2/guides/overview"]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": ["v2/integrations/crewai", "v2/integrations/langgraph", "v2/integrations/mcp"]
|
||||
"pages": [
|
||||
"v2/integrations/crewai",
|
||||
"v2/integrations/langgraph",
|
||||
"v2/integrations/mcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Application Interfaces",
|
||||
"pages": [
|
||||
"v2/guides/discord",
|
||||
"v2/guides/telegram"
|
||||
]
|
||||
"pages": ["v2/guides/discord", "v2/guides/telegram"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -90,9 +84,7 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": [
|
||||
"v2/api-reference/introduction"
|
||||
]
|
||||
"pages": ["v2/api-reference/introduction"]
|
||||
},
|
||||
{
|
||||
"group": "workspaces",
|
||||
|
|
@ -102,7 +94,8 @@
|
|||
"v2/api-reference/endpoint/workspaces/update-workspace",
|
||||
"v2/api-reference/endpoint/workspaces/delete-workspace",
|
||||
"v2/api-reference/endpoint/workspaces/search-workspace",
|
||||
"v2/api-reference/endpoint/workspaces/get-deriver-status"
|
||||
"v2/api-reference/endpoint/workspaces/get-deriver-status",
|
||||
"v2/api-reference/endpoint/workspaces/trigger-dream"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -114,8 +107,10 @@
|
|||
"v2/api-reference/endpoint/peers/get-sessions-for-peer",
|
||||
"v2/api-reference/endpoint/peers/chat",
|
||||
"v2/api-reference/endpoint/peers/get-working-representation",
|
||||
"v2/api-reference/endpoint/peers/search-peer",
|
||||
"v2/api-reference/endpoint/peers/get-peer-card"
|
||||
"v2/api-reference/endpoint/peers/get-peer-card",
|
||||
"v2/api-reference/endpoint/peers/set-peer-card",
|
||||
"v2/api-reference/endpoint/peers/get-peer-context",
|
||||
"v2/api-reference/endpoint/peers/search-peer"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -147,6 +142,14 @@
|
|||
"v2/api-reference/endpoint/messages/create-messages-with-file"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "observations",
|
||||
"pages": [
|
||||
"v2/api-reference/endpoint/observations/list-observations",
|
||||
"v2/api-reference/endpoint/observations/query-observations",
|
||||
"v2/api-reference/endpoint/observations/delete-observation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "webhooks",
|
||||
"pages": [
|
||||
|
|
@ -196,9 +199,7 @@
|
|||
{
|
||||
"version": "v1.1.0",
|
||||
"api": {
|
||||
"openapi": [
|
||||
"openapi.json"
|
||||
]
|
||||
"openapi": ["openapi.json"]
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
|
|
@ -228,23 +229,15 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"v1/guides/overview",
|
||||
"v1/guides/streaming-response"
|
||||
]
|
||||
"pages": ["v1/guides/overview", "v1/guides/streaming-response"]
|
||||
},
|
||||
{
|
||||
"group": "Application Interfaces",
|
||||
"pages": [
|
||||
"v1/guides/discord",
|
||||
"v1/guides/honcho-mcp"
|
||||
]
|
||||
"pages": ["v1/guides/discord", "v1/guides/honcho-mcp"]
|
||||
},
|
||||
{
|
||||
"group": "Personal Memory",
|
||||
"pages": [
|
||||
"v1/guides/dialectic-endpoint"
|
||||
]
|
||||
"pages": ["v1/guides/dialectic-endpoint"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -253,9 +246,7 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": [
|
||||
"v1/api-reference/introduction"
|
||||
]
|
||||
"pages": ["v1/api-reference/introduction"]
|
||||
},
|
||||
{
|
||||
"group": "apps",
|
||||
|
|
@ -303,9 +294,7 @@
|
|||
},
|
||||
{
|
||||
"group": "keys",
|
||||
"pages": [
|
||||
"v1/api-reference/endpoint/keys/create-key"
|
||||
]
|
||||
"pages": ["v1/api-reference/endpoint/keys/create-key"]
|
||||
},
|
||||
{
|
||||
"group": "metamessages",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"main": ".pnp.js",
|
||||
"scripts": {
|
||||
"dev": "mint dev",
|
||||
"openapi": "npx @mintlify/scraping openapi-file openapi.documented.yml -o api-reference/endpoint",
|
||||
"openapi": "npx @mintlify/scraping openapi-file v2/openapi.json -o v2/api-reference/endpoint",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: delete /v2/workspaces/{workspace_id}/observations/{observation_id}
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/observations/list
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/observations/query
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v2/workspaces/{workspace_id}/peers/{peer_id}/context
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: put /v2/workspaces/{workspace_id}/peers/{peer_id}/card
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/trigger_dream
|
||||
---
|
||||
|
|
@ -1,14 +1,144 @@
|
|||
---
|
||||
title: 'Configure Reasoning'
|
||||
description: 'Customizing how Honcho handles peers and sessions'
|
||||
description: 'Customizing how Honcho handles peers, sessions, and messages'
|
||||
icon: 'wrench'
|
||||
---
|
||||
|
||||
Entities in Honcho can sometimes be configured to change the behavior of the deriver, which is responsible for generating and storing facts, summaries, and user representations.
|
||||
Honcho's reasoning engine (the "deriver") can be configured at multiple levels to control how it processes messages, generates facts, creates summaries, and builds peer representations.
|
||||
|
||||
These configurations can be set at the peer, session, and session-peer level (AKA the state of a peer within a specific session).
|
||||
Configuration follows a hierarchy: **message > session > workspace > global defaults**. Settings at lower levels override those at higher levels, giving you fine-grained control over behavior.
|
||||
|
||||
### Peer Configuration
|
||||
## Configuration Hierarchy
|
||||
|
||||
Honcho uses a hierarchical configuration system where more specific settings override more general ones:
|
||||
|
||||
1. **Global Defaults**: Built-in system defaults
|
||||
2. **Workspace Configuration**: Settings that apply to all sessions in a workspace
|
||||
3. **Session Configuration**: Settings that apply to all messages in a session
|
||||
4. **Message Configuration**: Settings that apply to a specific message
|
||||
|
||||
<Info>
|
||||
All configuration fields are optional. If not specified, the value is inherited from the next level up in the hierarchy.
|
||||
</Info>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Deriver Configuration
|
||||
|
||||
Controls the core reasoning engine that extracts facts and insights from messages.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `enabled` | `bool` | Whether to enable deriver functionality. When disabled, no facts or representations are generated. |
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
||||
honcho = Honcho()
|
||||
|
||||
# Disable deriver at session level
|
||||
session = honcho.session("private-session", config={
|
||||
"deriver": {"enabled": False}
|
||||
})
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Disable deriver at session level
|
||||
const session = await honcho.session("private-session", {
|
||||
config: {
|
||||
deriver: { enabled: false }
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Peer Card Configuration
|
||||
|
||||
Controls how peer cards (concise summaries of what's known about a peer) are generated and used.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `use` | `bool` | Whether to use peer cards during the deriver process. |
|
||||
| `create` | `bool` | Whether to generate peer cards based on message content. |
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Disable peer card generation but still use existing cards
|
||||
session = honcho.session("my-session", config={
|
||||
"peer_card": {"create": False, "use": True}
|
||||
})
|
||||
```
|
||||
```typescript TypeScript
|
||||
// Disable peer card generation but still use existing cards
|
||||
const session = await honcho.session("my-session", {
|
||||
config: {
|
||||
peer_card: { create: false, use: true }
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Summary Configuration
|
||||
|
||||
Controls automatic conversation summarization. Available at workspace and session levels only.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `enabled` | `bool` | Whether to enable summary functionality. |
|
||||
| `messages_per_short_summary` | `int` | Number of messages between short summaries. Must be ≥ 10. |
|
||||
| `messages_per_long_summary` | `int` | Number of messages between long summaries. Must be ≥ 20 and greater than `messages_per_short_summary`. |
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Customize summary frequency
|
||||
session = honcho.session("verbose-session", config={
|
||||
"summary": {
|
||||
"enabled": True,
|
||||
"messages_per_short_summary": 15,
|
||||
"messages_per_long_summary": 45
|
||||
}
|
||||
})
|
||||
```
|
||||
```typescript TypeScript
|
||||
// Customize summary frequency
|
||||
const session = await honcho.session("verbose-session", {
|
||||
config: {
|
||||
summary: {
|
||||
enabled: true,
|
||||
messages_per_short_summary: 15,
|
||||
messages_per_long_summary: 45
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Dream Configuration
|
||||
|
||||
Controls the "dreaming" process that consolidates and refines representations. Available at workspace and session levels only.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `enabled` | `bool` | Whether to enable dream functionality. Automatically disabled if deriver is disabled. |
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Disable dreams for a workspace
|
||||
# (done via API when creating/updating workspace)
|
||||
```
|
||||
```typescript TypeScript
|
||||
// Disable dreams for a workspace
|
||||
// (done via API when creating/updating workspace)
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
---
|
||||
|
||||
## Peer Configuration
|
||||
|
||||
By default, all peers are "observed" by Honcho. This means that Honcho will derive facts from messages sent by the peer and generate a representation of them. In most cases, this is why you use Honcho! However, sometimes an application requires a peer that should not be observed: for example, an assistant or game NPC that your program will never need to ask questions about.
|
||||
|
||||
|
|
@ -27,7 +157,7 @@ honcho = Honcho()
|
|||
peer = honcho.peer("my-peer", config={"observe_me": False})
|
||||
|
||||
# Change peer's configuration
|
||||
peer.set_peer_config({"observe_me": True})
|
||||
peer.set_config({"observe_me": True})
|
||||
|
||||
# Note: creating the same peer again will also replace the configuration
|
||||
peer = honcho.peer("my-peer", config={"observe_me": False})
|
||||
|
|
@ -43,7 +173,7 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
const peer = await honcho.peer("my-peer", { config: { observe_me: false } });
|
||||
|
||||
// Change peer's configuration
|
||||
await peer.setPeerConfig({ observe_me: true });
|
||||
await peer.setConfig({ observe_me: true });
|
||||
|
||||
// Note: creating the same peer again will also replace the configuration
|
||||
await honcho.peer("my-peer", { config: { observe_me: false } });
|
||||
|
|
@ -51,9 +181,9 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Session Configuration
|
||||
## Session Configuration
|
||||
|
||||
By default, all sessions have the deriver enabled, much like peers. You may create a session that escapes the deriver's watchful eye by setting the `deriver_disabled` flag to `true`. You can update the flag by calling `get_or_create` on the session with a new configuration.
|
||||
Sessions support the full configuration schema. You can disable the deriver entirely for a session, customize summary behavior, or adjust peer card settings.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
|
|
@ -62,8 +192,18 @@ from honcho import Honcho
|
|||
# Initialize client
|
||||
honcho = Honcho()
|
||||
|
||||
# Create session with configuration
|
||||
session = honcho.session("my-session", config={"deriver_disabled": True})
|
||||
# Create session with deriver disabled
|
||||
session = honcho.session("my-session", config={
|
||||
"deriver": {"enabled": False}
|
||||
})
|
||||
|
||||
# Create session with custom summary settings
|
||||
session = honcho.session("detailed-session", config={
|
||||
"summary": {
|
||||
"messages_per_short_summary": 10,
|
||||
"messages_per_long_summary": 30
|
||||
}
|
||||
})
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
|
@ -72,15 +212,73 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Create session with configuration
|
||||
const session = await honcho.session("my-session", { config: { deriver_disabled: true } });
|
||||
// Create session with deriver disabled
|
||||
const session = await honcho.session("my-session", {
|
||||
config: { deriver: { enabled: false } }
|
||||
});
|
||||
|
||||
// Create session with custom summary settings
|
||||
const detailedSession = await honcho.session("detailed-session", {
|
||||
config: {
|
||||
summary: {
|
||||
messages_per_short_summary: 10,
|
||||
messages_per_long_summary: 30
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Session-Peer Configuration
|
||||
## Message Configuration
|
||||
|
||||
Configuration at the session-peer level is the most common use case for configuration flags. You will often want to arrange a session such that certain peers observe others in order to form "local representations" of them. There are two flags that can be set at the session-peer level:
|
||||
Individual messages can override session and workspace configuration for fine-grained control. This is useful for excluding specific messages from processing or adjusting behavior on a per-message basis.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
||||
honcho = Honcho()
|
||||
session = honcho.session("my-session")
|
||||
user = honcho.peer("user")
|
||||
|
||||
# Create a message that skips deriver processing
|
||||
session.add_messages([
|
||||
user.message("This message won't be analyzed", config={
|
||||
"deriver": {"enabled": False}
|
||||
})
|
||||
])
|
||||
|
||||
# Create a message with custom peer card settings
|
||||
session.add_messages([
|
||||
user.message("Use existing card but don't update it", config={
|
||||
"peer_card": {"use": True, "create": False}
|
||||
})
|
||||
])
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
(async () => {
|
||||
const honcho = new Honcho({});
|
||||
const session = await honcho.session("my-session");
|
||||
const user = await honcho.peer("user");
|
||||
|
||||
// Create a message that skips deriver processing
|
||||
await session.addMessages([
|
||||
user.message("This message won't be analyzed", {
|
||||
configuration: { deriver: { enabled: false } }
|
||||
})
|
||||
]);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Session-Peer Configuration
|
||||
|
||||
Configuration at the session-peer level controls how peers observe each other within a specific session. This is the most common use case for enabling "local representations" — where one peer forms a model of another peer based only on what they observe in that session.
|
||||
|
||||
There are two flags that can be set at the session-peer level:
|
||||
|
||||
- `observe_me`: Whether this peer should *be observed* by others in the session. By default, this is `true`. This overrides the peer-level `observe_me` flag.
|
||||
|
||||
|
|
@ -96,7 +294,7 @@ You can dynamically change the configuration of a session-peer by calling `set_p
|
|||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
from honcho import Honcho, SessionPeerConfig
|
||||
|
||||
# Initialize client
|
||||
honcho = Honcho()
|
||||
|
|
@ -113,11 +311,11 @@ session.add_peers([alice, bob])
|
|||
|
||||
# Add another peer to the session with a custom configuration
|
||||
charlie = honcho.peer("charlie")
|
||||
session.add_peers([charlie, {"observe_me": False, "observe_others": True}])
|
||||
session.add_peers([(charlie, SessionPeerConfig(observe_me=False, observe_others=True))])
|
||||
|
||||
# Set session-peer configuration
|
||||
session.set_peer_config(alice, {"observe_others": True})
|
||||
session.set_peer_config(bob, {"observe_me": False})
|
||||
session.set_peer_config(alice, SessionPeerConfig(observe_others=True))
|
||||
session.set_peer_config(bob, SessionPeerConfig(observe_me=False))
|
||||
|
||||
# Get session-peer configuration
|
||||
charlie_config = session.get_peer_config(charlie)
|
||||
|
|
@ -142,7 +340,7 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
|
||||
// Add another peer to the session with a custom configuration
|
||||
const charlie = await honcho.peer("charlie");
|
||||
await session.addPeers([charlie, { observe_me: false, observe_others: true }]);
|
||||
await session.addPeers([[charlie, { observe_me: false, observe_others: true }]]);
|
||||
|
||||
// Set session-peer configuration
|
||||
await session.setPeerConfig(alice, { observe_others: true });
|
||||
|
|
@ -154,3 +352,45 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Full Configuration Schema Reference
|
||||
|
||||
### Workspace & Session Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"deriver": {
|
||||
"enabled": true
|
||||
},
|
||||
"peer_card": {
|
||||
"use": true,
|
||||
"create": true
|
||||
},
|
||||
"summary": {
|
||||
"enabled": true,
|
||||
"messages_per_short_summary": 20,
|
||||
"messages_per_long_summary": 60
|
||||
},
|
||||
"dream": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Message Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"deriver": {
|
||||
"enabled": true
|
||||
},
|
||||
"peer_card": {
|
||||
"use": true,
|
||||
"create": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Message configuration only supports `deriver` and `peer_card` settings. Summary and dream configurations are session/workspace-level only.
|
||||
</Note>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,127 @@ context = session.get_context(summary=False, tokens=2000)
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Peer Representation in Context
|
||||
|
||||
You can include a peer's representation and peer card in the context by specifying `peer_target`. This is useful for providing the LLM with knowledge about a specific peer.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get context with peer representation included
|
||||
context = session.get_context(
|
||||
tokens=2000,
|
||||
peer_target="user-123" # Include representation of user-123
|
||||
)
|
||||
|
||||
# Access the representation and peer card
|
||||
print(context.peer_representation) # String representation
|
||||
print(context.peer_card) # List of peer card items
|
||||
|
||||
# Get representation from a specific peer's perspective
|
||||
context = session.get_context(
|
||||
tokens=2000,
|
||||
peer_target="user-123",
|
||||
peer_perspective="assistant" # From assistant's viewpoint
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
(async () => {
|
||||
// Get context with peer representation included
|
||||
const context = await session.getContext({
|
||||
tokens: 2000,
|
||||
peerTarget: "user-123" // Include representation of user-123
|
||||
});
|
||||
|
||||
// Access the representation and peer card
|
||||
console.log(context.peerRepresentation); // String representation
|
||||
console.log(context.peerCard); // Array of peer card items
|
||||
|
||||
// Get representation from a specific peer's perspective
|
||||
const perspectiveContext = await session.getContext({
|
||||
tokens: 2000,
|
||||
peerTarget: "user-123",
|
||||
peerPerspective: "assistant" // From assistant's viewpoint
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Semantic Search with Last Message
|
||||
|
||||
Use `last_user_message` to fetch semantically relevant observations based on the most recent message:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get context with semantic search based on last message
|
||||
context = session.get_context(
|
||||
tokens=2000,
|
||||
peer_target="user-123",
|
||||
last_user_message="What are my account preferences?",
|
||||
search_top_k=10, # Number of relevant observations
|
||||
search_max_distance=0.8, # Max semantic distance (0.0-1.0)
|
||||
include_most_derived=True, # Include most recent observations
|
||||
max_observations=25 # Cap total observations
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
(async () => {
|
||||
// Get context with semantic search based on last message
|
||||
const context = await session.getContext({
|
||||
tokens: 2000,
|
||||
peerTarget: "user-123",
|
||||
lastUserMessage: "What are my account preferences?",
|
||||
searchTopK: 10, // Number of relevant observations
|
||||
searchMaxDistance: 0.8, // Max semantic distance (0.0-1.0)
|
||||
includeMostDerived: true, // Include most recent observations
|
||||
maxObservations: 25 // Cap total observations
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Session-Scoped Representations
|
||||
|
||||
Use `limit_to_session` to only include observations from the current session:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get context limited to this session's observations only
|
||||
context = session.get_context(
|
||||
tokens=2000,
|
||||
peer_target="user-123",
|
||||
limit_to_session=True # Only observations from this session
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
(async () => {
|
||||
// Get context limited to this session's observations only
|
||||
const context = await session.getContext({
|
||||
tokens: 2000,
|
||||
peerTarget: "user-123",
|
||||
limitToSession: true // Only observations from this session
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### All Parameters Reference
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `summary` | `bool` | Include summary in context (default: true) |
|
||||
| `tokens` | `int` | Maximum tokens to include |
|
||||
| `peer_target` | `str` | Peer ID to include representation for |
|
||||
| `peer_perspective` | `str` | Peer ID for perspective (requires peer_target) |
|
||||
| `last_user_message` | `str` | Message for semantic search (requires peer_target) |
|
||||
| `limit_to_session` | `bool` | Limit to session observations only |
|
||||
| `search_top_k` | `int` | Semantic search results to include (1-100) |
|
||||
| `search_max_distance` | `float` | Max semantic distance (0.0-1.0) |
|
||||
| `include_most_derived` | `bool` | Include most recently derived observations |
|
||||
| `max_observations` | `int` | Maximum observations to include (1-100) |
|
||||
|
||||
## Converting to LLM Formats
|
||||
|
||||
The `SessionContext` object provides methods to convert the context into formats compatible with popular LLM APIs. When converting to OpenAI format, you must specify the assistant peer to format the context in such a way that the LLM can understand it.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ A `Message` is sent by a `Peer` and saved in a `Session`
|
|||
|
||||
session = honcho.session("sample-session")
|
||||
|
||||
message = peer.message("Hello, world!", session_id=session.id)
|
||||
message = peer.message("Hello, world!")
|
||||
|
||||
session.add_messages([message])
|
||||
```
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Working representations are automatically generated and cached through Honcho's
|
|||
|
||||
## Basic Usage
|
||||
|
||||
Working representations are accessed through the `working_rep()` method on Session objects:
|
||||
Working representations are accessed through the `working_rep()` method on Session or Peer objects:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
|
|
@ -50,6 +50,9 @@ response = user.chat("What is this user's main concern right now?", session_id=s
|
|||
# Retrieve the cached working representation for the user
|
||||
user_representation = session.working_rep("user-123")
|
||||
print("Cached user representation:", user_representation)
|
||||
|
||||
# Or access from the peer directly
|
||||
peer_representation = user.working_rep()
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
@ -77,7 +80,76 @@ const response = await user.chat("What is this user's main concern right now?",
|
|||
// Retrieve the cached working representation for the user
|
||||
const userRepresentation = await session.workingRep("user-123");
|
||||
console.log("Cached user representation:", userRepresentation);
|
||||
// Returns: { representation: Object }
|
||||
|
||||
// Or access from the peer directly
|
||||
const peerRepresentation = await user.workingRep();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Semantic Search in Representations
|
||||
|
||||
Working representations support semantic search to retrieve the most relevant observations for a given query. This is useful when you want to focus the representation on specific topics.
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `search_query` | `str` | Semantic search query to filter relevant observations |
|
||||
| `search_top_k` | `int` | Number of semantic search results to include (1-100) |
|
||||
| `search_max_distance` | `float` | Maximum semantic distance threshold (0.0-1.0) |
|
||||
| `include_most_derived` | `bool` | Whether to include the most recently derived observations |
|
||||
| `max_observations` | `int` | Maximum number of observations to include (1-100) |
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get representation focused on a specific topic
|
||||
billing_rep = session.working_rep(
|
||||
"user-123",
|
||||
search_query="billing and payment issues",
|
||||
search_top_k=10,
|
||||
search_max_distance=0.8,
|
||||
include_most_derived=True,
|
||||
max_observations=25
|
||||
)
|
||||
|
||||
# Get representation from peer with target
|
||||
# What user-123 knows about the assistant
|
||||
local_rep = session.working_rep(
|
||||
"user-123",
|
||||
target="ai-assistant",
|
||||
search_query="support interactions"
|
||||
)
|
||||
|
||||
# Access from peer object with semantic search
|
||||
user_rep = user.working_rep(
|
||||
session=session,
|
||||
search_query="preferences",
|
||||
search_top_k=5
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get representation focused on a specific topic
|
||||
const billingRep = await session.workingRep("user-123", {
|
||||
searchQuery: "billing and payment issues",
|
||||
searchTopK: 10,
|
||||
searchMaxDistance: 0.8,
|
||||
includeMostDerived: true,
|
||||
maxObservations: 25
|
||||
});
|
||||
|
||||
// Get representation from peer with target
|
||||
// What user-123 knows about the assistant
|
||||
const localRep = await session.workingRep("user-123", {
|
||||
target: "ai-assistant",
|
||||
searchQuery: "support interactions"
|
||||
});
|
||||
|
||||
// Access from peer object with semantic search
|
||||
const userRep = await user.workingRep(session, undefined, {
|
||||
searchQuery: "preferences",
|
||||
searchTopK: 5
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,17 @@ results = alice.search("programming")
|
|||
metadata = alice.get_metadata()
|
||||
metadata["location"] = "Paris"
|
||||
alice.set_metadata(metadata)
|
||||
|
||||
# Get peer context (representation + peer card in one call)
|
||||
context = alice.get_context()
|
||||
context = alice.get_context(target="bob") # What alice knows about bob
|
||||
|
||||
# Get working representation with semantic search
|
||||
rep = alice.working_rep(search_query="preferences", search_top_k=10)
|
||||
|
||||
# Access observations
|
||||
self_observations = alice.observations.list() # Self-observations
|
||||
bob_observations = alice.observations_of("bob").list() # Observations of bob
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
@ -318,6 +329,109 @@ await alice.setMetadata({
|
|||
...metadata,
|
||||
location: "Paris"
|
||||
});
|
||||
|
||||
// Get peer context (representation + peer card in one call)
|
||||
const context = await alice.getContext();
|
||||
const targetContext = await alice.getContext("bob"); // What alice knows about bob
|
||||
|
||||
// Get working representation with semantic search
|
||||
const rep = await alice.workingRep(undefined, undefined, {
|
||||
searchQuery: "preferences",
|
||||
searchTopK: 10
|
||||
});
|
||||
|
||||
// Access observations
|
||||
const selfObs = await alice.observations.list(); // Self-observations
|
||||
const bobObs = await alice.observationsOf("bob").list(); // Observations of bob
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Peer Context
|
||||
|
||||
The `get_context()` method on peers retrieves both the working representation and peer card in a single API call:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get peer's own context
|
||||
context = alice.get_context()
|
||||
print(context.representation) # Working representation
|
||||
print(context.peer_card) # Peer card as list of strings
|
||||
|
||||
# Get context about another peer (what alice knows about bob)
|
||||
bob_context = alice.get_context(target="bob")
|
||||
|
||||
# Get context with semantic search
|
||||
context = alice.get_context(
|
||||
target="bob",
|
||||
search_query="work preferences",
|
||||
search_top_k=10,
|
||||
search_max_distance=0.8,
|
||||
include_most_derived=True,
|
||||
max_observations=50
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get peer's own context
|
||||
const context = await alice.getContext();
|
||||
console.log(context.representation); // Working representation
|
||||
console.log(context.peerCard); // Peer card as array of strings
|
||||
|
||||
// Get context about another peer (what alice knows about bob)
|
||||
const bobContext = await alice.getContext("bob");
|
||||
|
||||
// Get context with semantic search
|
||||
const searchedContext = await alice.getContext("bob", {
|
||||
searchQuery: "work preferences",
|
||||
searchTopK: 10,
|
||||
searchMaxDistance: 0.8,
|
||||
includeMostDerived: true,
|
||||
maxObservations: 50
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Observations
|
||||
|
||||
Peers can access their observations (facts derived from messages) through the `observations` property and `observations_of()` method:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Access self-observations (what honcho knows about alice)
|
||||
self_obs = alice.observations
|
||||
|
||||
# List self-observations
|
||||
obs_list = self_obs.list()
|
||||
|
||||
# Search self-observations semantically
|
||||
results = self_obs.query("food preferences")
|
||||
|
||||
# Delete an observation
|
||||
self_obs.delete("observation-id")
|
||||
|
||||
# Access observations of another peer (what alice knows about bob)
|
||||
bob_obs = alice.observations_of("bob")
|
||||
bob_obs_list = bob_obs.list()
|
||||
bob_search = bob_obs.query("work history")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Access self-observations (what honcho knows about alice)
|
||||
const selfObs = alice.observations;
|
||||
|
||||
// List self-observations
|
||||
const obsList = await selfObs.list();
|
||||
|
||||
// Search self-observations semantically
|
||||
const results = await selfObs.query("food preferences");
|
||||
|
||||
// Delete an observation
|
||||
await selfObs.delete("observation-id");
|
||||
|
||||
// Access observations of another peer (what alice knows about bob)
|
||||
const bobObs = alice.observationsOf("bob");
|
||||
const bobObsList = await bobObs.list();
|
||||
const bobSearch = await bobObs.query("work history");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -361,12 +475,42 @@ messages = session.get_messages()
|
|||
# Get conversation context
|
||||
context = session.get_context(summary=True, tokens=2000)
|
||||
|
||||
# Get context with peer representation included
|
||||
context = session.get_context(
|
||||
tokens=2000,
|
||||
peer_target="user",
|
||||
peer_perspective="assistant",
|
||||
last_user_message="What are my preferences?",
|
||||
limit_to_session=True,
|
||||
search_top_k=10,
|
||||
search_max_distance=0.8,
|
||||
include_most_derived=True,
|
||||
max_observations=25
|
||||
)
|
||||
|
||||
# Search session content
|
||||
results = session.search("help")
|
||||
|
||||
# Working representation queries
|
||||
# Working representation queries with semantic search
|
||||
global_rep = session.working_rep("alice")
|
||||
targeted_rep = session.working_rep(alice, bob)
|
||||
targeted_rep = session.working_rep(alice, target=bob)
|
||||
searched_rep = session.working_rep(
|
||||
"alice",
|
||||
search_query="preferences",
|
||||
search_top_k=10,
|
||||
include_most_derived=True
|
||||
)
|
||||
|
||||
# Upload a file to create messages
|
||||
messages = session.upload_file(
|
||||
file=open("document.pdf", "rb"),
|
||||
peer_id="user",
|
||||
metadata={"source": "upload"},
|
||||
created_at="2024-01-15T10:30:00Z"
|
||||
)
|
||||
|
||||
# Delete session (async - returns 202)
|
||||
session.delete()
|
||||
|
||||
# Metadata management
|
||||
session.set_metadata({"topic": "product planning", "status": "active"})
|
||||
|
|
@ -402,12 +546,43 @@ const messages = await session.getMessages();
|
|||
// Get conversation context
|
||||
const context = await session.getContext({ summary: true, tokens: 2000 });
|
||||
|
||||
// Get context with peer representation included
|
||||
const richContext = await session.getContext({
|
||||
tokens: 2000,
|
||||
peerTarget: "user",
|
||||
peerPerspective: "assistant",
|
||||
lastUserMessage: "What are my preferences?",
|
||||
limitToSession: true,
|
||||
searchTopK: 10,
|
||||
searchMaxDistance: 0.8,
|
||||
includeMostDerived: true,
|
||||
maxObservations: 25
|
||||
});
|
||||
|
||||
// Search session content
|
||||
const results = await session.search("help");
|
||||
|
||||
// Working representation queries
|
||||
// Working representation queries with semantic search
|
||||
const globalRep = await session.workingRep("alice");
|
||||
const targetedRep = await session.workingRep(alice, bob);
|
||||
const targetedRep = await session.workingRep(alice, { target: bob });
|
||||
const searchedRep = await session.workingRep("alice", undefined, {
|
||||
searchQuery: "preferences",
|
||||
searchTopK: 10,
|
||||
includeMostDerived: true
|
||||
});
|
||||
|
||||
// Upload a file to create messages
|
||||
const messages = await session.uploadFile(
|
||||
fileBuffer,
|
||||
"user",
|
||||
{
|
||||
metadata: { source: "upload" },
|
||||
createdAt: "2024-01-15T10:30:00Z"
|
||||
}
|
||||
);
|
||||
|
||||
// Delete session (async - returns 202)
|
||||
await session.delete();
|
||||
|
||||
// Metadata management
|
||||
await session.setMetadata({
|
||||
|
|
@ -494,10 +669,27 @@ The SessionContext object has the following structure:
|
|||
"message_id": 123,
|
||||
"summary_type": "short|long",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"peer_representation": "string (optional)",
|
||||
"peer_card": ["string"] // optional, included when peer_target is provided
|
||||
}
|
||||
```
|
||||
|
||||
**Session Context Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `summary` | `bool` | Whether to include summary (default: true) |
|
||||
| `tokens` | `int` | Maximum tokens to include |
|
||||
| `peer_target` | `str` | Peer ID to get representation for |
|
||||
| `peer_perspective` | `str` | Peer ID for perspective (requires peer_target) |
|
||||
| `last_user_message` | `str` | Most recent message for semantic search |
|
||||
| `limit_to_session` | `bool` | Limit representation to session only |
|
||||
| `search_top_k` | `int` | Number of semantic search results (1-100) |
|
||||
| `search_max_distance` | `float` | Max semantic distance (0.0-1.0) |
|
||||
| `include_most_derived` | `bool` | Include most derived observations |
|
||||
| `max_observations` | `int` | Max observations to include (1-100) |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multi-Party Conversations
|
||||
|
|
|
|||
2651
docs/v2/openapi.json
2651
docs/v2/openapi.json
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,114 @@
|
|||
"""rename_deriver_disabled_to_deriver_enabled
|
||||
|
||||
Revision ID: 110bdf470272
|
||||
Revises: baa22cad81e2
|
||||
Create Date: 2025-10-31 13:04:31.029856
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from migrations.utils import get_schema
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "110bdf470272"
|
||||
down_revision: str | None = "baa22cad81e2"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
schema = get_schema()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""
|
||||
Convert deriver_disabled to deriver_enabled in configuration JSONB.
|
||||
|
||||
- deriver_disabled: true -> deriver_enabled: false
|
||||
- deriver_disabled: false -> deriver_enabled: true
|
||||
- Remove deriver_disabled key from configuration
|
||||
"""
|
||||
# Update sessions table in batches
|
||||
# Combine all cases in one pass: set deriver_enabled based on deriver_disabled value,
|
||||
# or just remove deriver_disabled if it's null or other value
|
||||
bind = op.get_bind()
|
||||
batch_size = 5000
|
||||
|
||||
while True:
|
||||
result = bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
WITH batch AS (
|
||||
SELECT id
|
||||
FROM "{schema}".sessions
|
||||
WHERE configuration ? 'deriver_disabled'
|
||||
LIMIT :batch_size
|
||||
)
|
||||
UPDATE "{schema}".sessions s
|
||||
SET configuration = CASE
|
||||
WHEN s.configuration->>'deriver_disabled' = 'true' THEN
|
||||
s.configuration - 'deriver_disabled' || jsonb_build_object('deriver_enabled', false)
|
||||
WHEN s.configuration->>'deriver_disabled' = 'false' THEN
|
||||
s.configuration - 'deriver_disabled' || jsonb_build_object('deriver_enabled', true)
|
||||
ELSE
|
||||
s.configuration - 'deriver_disabled'
|
||||
END
|
||||
FROM batch b
|
||||
WHERE s.id = b.id
|
||||
AND s.configuration ? 'deriver_disabled'
|
||||
"""
|
||||
),
|
||||
{"batch_size": batch_size},
|
||||
)
|
||||
rowcount = result.rowcount
|
||||
result.close()
|
||||
if rowcount == 0:
|
||||
break
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Convert deriver_enabled back to deriver_disabled in configuration JSONB.
|
||||
|
||||
- deriver_enabled: false -> deriver_disabled: true
|
||||
- deriver_enabled: true -> deriver_disabled: false
|
||||
- Remove deriver_enabled key from configuration
|
||||
"""
|
||||
# Update sessions table in batches
|
||||
# Combine all cases in one pass: set deriver_disabled based on deriver_enabled value,
|
||||
# or just remove deriver_enabled if it's null or other value
|
||||
bind = op.get_bind()
|
||||
batch_size = 5000
|
||||
|
||||
while True:
|
||||
result = bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
WITH batch AS (
|
||||
SELECT id
|
||||
FROM "{schema}".sessions
|
||||
WHERE configuration ? 'deriver_enabled'
|
||||
LIMIT :batch_size
|
||||
)
|
||||
UPDATE "{schema}".sessions s
|
||||
SET configuration = CASE
|
||||
WHEN s.configuration->>'deriver_enabled' = 'false' THEN
|
||||
s.configuration - 'deriver_enabled' || jsonb_build_object('deriver_disabled', true)
|
||||
WHEN s.configuration->>'deriver_enabled' = 'true' THEN
|
||||
s.configuration - 'deriver_enabled' || jsonb_build_object('deriver_disabled', false)
|
||||
ELSE
|
||||
s.configuration - 'deriver_enabled'
|
||||
END
|
||||
FROM batch b
|
||||
WHERE s.id = b.id
|
||||
AND s.configuration ? 'deriver_enabled'
|
||||
"""
|
||||
),
|
||||
{"batch_size": batch_size},
|
||||
)
|
||||
rowcount = result.rowcount
|
||||
result.close()
|
||||
if rowcount == 0:
|
||||
break
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho"
|
||||
version = "2.4.3"
|
||||
version = "2.5.0"
|
||||
description = "Honcho Server"
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import random
|
||||
import uuid
|
||||
|
||||
from honcho import Honcho
|
||||
|
||||
# Create a Honcho client with the default workspace
|
||||
honcho = Honcho(environment="local")
|
||||
|
||||
peers = [
|
||||
honcho.peer("alice"),
|
||||
honcho.peer("bob"),
|
||||
honcho.peer("charlie"),
|
||||
]
|
||||
|
||||
# Create a new session
|
||||
session = honcho.session("context_test_" + str(uuid.uuid4()))
|
||||
|
||||
# Generate some random messages from alice, bob, and charlie and add them to the session
|
||||
messages = []
|
||||
for i in range(10):
|
||||
random_peer = random.choice(peers)
|
||||
messages.append(
|
||||
random_peer.message(f"Hello from {random_peer}! This is message {i}.")
|
||||
)
|
||||
|
||||
session.add_messages(messages)
|
||||
|
||||
alice = peers[0]
|
||||
bob = peers[1]
|
||||
|
||||
# Get alice's working representation in the session
|
||||
representation = session.working_rep(alice)
|
||||
print("working representation returned:", representation)
|
||||
|
||||
# Get alice's working representation *of bob* in the session
|
||||
representation = session.working_rep(alice, target=bob)
|
||||
print("working representation returned:", representation)
|
||||
|
|
@ -8,7 +8,7 @@ authors = [
|
|||
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
|
||||
]
|
||||
dependencies = [
|
||||
"honcho-core>=1.5.1",
|
||||
"honcho-core>=1.6.0",
|
||||
"httpx>=0.28.0, <1",
|
||||
"pydantic>=2.0.0, <3",
|
||||
"typing-extensions>=4.12.0; python_version < \"3.12\"",
|
||||
|
|
|
|||
|
|
@ -41,11 +41,18 @@ from .async_client import (
|
|||
AsyncSession,
|
||||
)
|
||||
from .client import Honcho
|
||||
from .observations import AsyncObservationScope, Observation, ObservationScope
|
||||
from .pagination import SyncPage
|
||||
from .peer import Peer
|
||||
from .session import Session
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
from .types import DialecticStreamResponse
|
||||
from .types import (
|
||||
DeductiveObservation,
|
||||
DialecticStreamResponse,
|
||||
ExplicitObservation,
|
||||
PeerContext,
|
||||
Representation,
|
||||
)
|
||||
|
||||
__version__ = "1.5.0"
|
||||
__author__ = "Plastic Labs"
|
||||
|
|
@ -53,15 +60,22 @@ __email__ = "hello@plasticlabs.ai"
|
|||
|
||||
__all__ = [
|
||||
"AsyncHoncho",
|
||||
"AsyncObservationScope",
|
||||
"AsyncPeer",
|
||||
"AsyncSession",
|
||||
"AsyncPage",
|
||||
"Honcho",
|
||||
"Observation",
|
||||
"ObservationScope",
|
||||
"Peer",
|
||||
"PeerContext",
|
||||
"Session",
|
||||
"SessionContext",
|
||||
"SessionSummaries",
|
||||
"Summary",
|
||||
"SyncPage",
|
||||
"DialecticStreamResponse",
|
||||
"Representation",
|
||||
"ExplicitObservation",
|
||||
"DeductiveObservation",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@ class AsyncHoncho(BaseModel):
|
|||
`core` property to use functionality not exposed through this SDK.
|
||||
|
||||
Attributes:
|
||||
api_key: API key for authentication
|
||||
base_url: Base URL for the Honcho API
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this workspace. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this workspace. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
core: Access to the underlying honcho_core client for advanced usage
|
||||
"""
|
||||
|
||||
|
|
@ -46,8 +48,20 @@ class AsyncHoncho(BaseModel):
|
|||
min_length=1,
|
||||
description="Workspace ID for scoping operations",
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this workspace. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this workspace. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@property
|
||||
def core(self) -> AsyncHonchoCore:
|
||||
"""
|
||||
|
|
@ -218,7 +232,14 @@ class AsyncHoncho(BaseModel):
|
|||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return AsyncPage(
|
||||
peers_page, lambda peer: AsyncPeer(peer.id, self.workspace_id, self._client)
|
||||
peers_page,
|
||||
lambda peer: AsyncPeer(
|
||||
peer.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=peer.metadata,
|
||||
config=peer.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -283,7 +304,13 @@ class AsyncHoncho(BaseModel):
|
|||
)
|
||||
return AsyncPage(
|
||||
sessions_page,
|
||||
lambda session: AsyncSession(session.id, self.workspace_id, self._client),
|
||||
lambda session: AsyncSession(
|
||||
session.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=session.metadata,
|
||||
config=session.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
|
|
@ -292,14 +319,16 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
Makes an async API call to retrieve metadata associated with the current workspace.
|
||||
Workspace metadata can include settings, configuration, or any other
|
||||
key-value data associated with the workspace.
|
||||
key-value data associated with the workspace. This method also updates the
|
||||
cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's metadata. Returns an empty
|
||||
dictionary if no metadata is set
|
||||
"""
|
||||
workspace = await self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
return workspace.metadata or {}
|
||||
self._metadata = workspace.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
async def set_metadata(
|
||||
|
|
@ -311,12 +340,64 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
Makes an async API call to update the metadata associated with the current workspace.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.update(self.workspace_id, metadata=metadata)
|
||||
self._metadata = metadata
|
||||
|
||||
async def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for the current workspace.
|
||||
|
||||
Makes an async API call to retrieve configuration associated with the current workspace.
|
||||
Configuration includes settings that control workspace behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's configuration. Returns an empty
|
||||
dictionary if no configuration is set
|
||||
"""
|
||||
workspace = await self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._configuration = workspace.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
async def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for the current workspace.
|
||||
|
||||
Makes an async API call to update the configuration associated with the current workspace.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.update(
|
||||
self.workspace_id, configuration=configuration
|
||||
)
|
||||
self._configuration = configuration
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for the current workspace.
|
||||
|
||||
Makes a single async API call to retrieve the latest metadata and configuration
|
||||
associated with the current workspace and updates the cached attributes.
|
||||
"""
|
||||
workspace = await self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._metadata = workspace.metadata or {}
|
||||
self._configuration = workspace.configuration or {}
|
||||
|
||||
async def get_workspaces(
|
||||
self, filters: dict[str, object] | None = None
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types.workspaces import PeerCardResponse
|
||||
from honcho_core.types.workspaces.peer_working_representation_response import (
|
||||
PeerWorkingRepresentationResponse,
|
||||
)
|
||||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..types import DialecticStreamResponse
|
||||
from .pagination import AsyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..observations import AsyncObservationScope
|
||||
from ..types import PeerContext, Representation
|
||||
from .session import AsyncSession
|
||||
|
||||
|
||||
|
|
@ -29,15 +35,31 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this peer
|
||||
_client: Reference to the parent AsyncHoncho client instance
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this peer. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this peer. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this peer")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this peer. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this peer. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -52,6 +74,9 @@ class AsyncPeer(BaseModel):
|
|||
client: AsyncHonchoCore = Field(
|
||||
..., description="Reference to the parent AsyncHoncho client instance"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = None,
|
||||
config: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new AsyncPeer.
|
||||
|
|
@ -60,9 +85,16 @@ class AsyncPeer(BaseModel):
|
|||
peer_id: Unique identifier for this peer within the workspace
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
client: Reference to the parent AsyncHoncho client instance
|
||||
metadata: Optional metadata to initialize the cached value
|
||||
config: Optional configuration to initialize the cached value
|
||||
"""
|
||||
super().__init__(id=peer_id, workspace_id=workspace_id)
|
||||
super().__init__(
|
||||
id=peer_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
|
|
@ -92,17 +124,22 @@ class AsyncPeer(BaseModel):
|
|||
Returns:
|
||||
A new AsyncPeer instance
|
||||
"""
|
||||
peer = cls(peer_id, workspace_id, client)
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
await client.workspaces.peers.get_or_create(
|
||||
peer_data = await client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
return cls(
|
||||
peer_id,
|
||||
workspace_id,
|
||||
client,
|
||||
metadata=peer_data.metadata,
|
||||
config=peer_data.configuration,
|
||||
)
|
||||
|
||||
return peer
|
||||
return cls(peer_id, workspace_id, client)
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
|
|
@ -208,6 +245,10 @@ class AsyncPeer(BaseModel):
|
|||
..., min_length=1, description="The text content for the message"
|
||||
),
|
||||
*,
|
||||
config: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the message",
|
||||
),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None, description="Optional metadata dictionary"
|
||||
),
|
||||
|
|
@ -238,6 +279,7 @@ class AsyncPeer(BaseModel):
|
|||
return MessageCreateParam(
|
||||
peer_id=self.id,
|
||||
content=content,
|
||||
configuration=config,
|
||||
metadata=metadata,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
|
@ -248,7 +290,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Makes an async API call to retrieve metadata associated with this peer. Metadata
|
||||
can include custom attributes, settings, or any other key-value data
|
||||
associated with the peer.
|
||||
associated with the peer. This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's metadata. Returns an empty dictionary
|
||||
|
|
@ -258,7 +300,8 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.metadata or {}
|
||||
self._metadata = peer.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
async def set_metadata(
|
||||
|
|
@ -272,6 +315,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Makes an async API call to update the metadata associated with this peer.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this peer.
|
||||
|
|
@ -282,13 +326,15 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
async def get_peer_config(self) -> dict[str, object]:
|
||||
async def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
Makes an API call to retrieve configuration associated with this peer.
|
||||
Configuration currently includes one optional flag, `observe_me`.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
|
|
@ -297,10 +343,11 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.configuration or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
async def set_peer_config(
|
||||
async def set_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
|
|
@ -313,6 +360,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Makes an API call to update the configuration associated with this peer.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer.
|
||||
|
|
@ -323,6 +371,51 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
configuration=config,
|
||||
)
|
||||
self._configuration = config
|
||||
|
||||
async def get_peer_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`get_config` instead.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
"""
|
||||
return await self.get_config()
|
||||
|
||||
@validate_call
|
||||
async def set_peer_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration for this peer.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`set_config` instead.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer
|
||||
"""
|
||||
return await self.set_config(config)
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this peer.
|
||||
|
||||
Makes a single async API call to retrieve the latest metadata and configuration
|
||||
associated with this peer and updates the cached attributes.
|
||||
"""
|
||||
peer = await self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = peer.metadata or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
|
||||
@validate_call
|
||||
async def search(
|
||||
|
|
@ -391,6 +484,212 @@ class AsyncPeer(BaseModel):
|
|||
items: list[str] = response.peer_card
|
||||
return "\n".join(items)
|
||||
|
||||
async def working_rep(
|
||||
self,
|
||||
session: str | AsyncSession | None = None,
|
||||
target: str | AsyncPeer | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get a working representation for this peer.
|
||||
|
||||
Args:
|
||||
session: Optional session to scope the representation to.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
returns the representation of the target from the perspective of this peer.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get global representation
|
||||
rep = await peer.working_rep()
|
||||
print(rep)
|
||||
|
||||
# Get representation scoped to a session
|
||||
session_rep = await peer.working_rep(session='session-123')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = await peer.working_rep(
|
||||
search_query='preferences',
|
||||
search_top_k=10,
|
||||
max_observations=50
|
||||
)
|
||||
```
|
||||
"""
|
||||
from ..types import Representation as _Representation
|
||||
|
||||
session_id = (
|
||||
None
|
||||
if session is None
|
||||
else session
|
||||
if isinstance(session, str)
|
||||
else session.id
|
||||
)
|
||||
|
||||
data: PeerWorkingRepresentationResponse = (
|
||||
await self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=session_id,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations
|
||||
if max_observations is not None
|
||||
else omit,
|
||||
)
|
||||
)
|
||||
representation = data.get("representation")
|
||||
if representation is not None:
|
||||
return _Representation.from_dict(cast(dict[str, object], representation))
|
||||
else:
|
||||
return _Representation.from_dict(data)
|
||||
|
||||
async def get_context(
|
||||
self,
|
||||
target: str | AsyncPeer | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "PeerContext":
|
||||
"""
|
||||
Get context for this peer, including representation and peer card.
|
||||
|
||||
This is a convenience method that retrieves both the working representation
|
||||
and peer card in a single API call.
|
||||
|
||||
Args:
|
||||
target: Optional target peer to get context for. If provided, returns
|
||||
the context for the target from this peer's perspective.
|
||||
Can be an AsyncPeer object or peer ID string.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A PeerContext object containing the representation and peer card
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get own context
|
||||
context = await peer.get_context()
|
||||
print(context.representation)
|
||||
print(context.peer_card)
|
||||
|
||||
# Get context for another peer
|
||||
context = await peer.get_context(target='other-peer-id')
|
||||
|
||||
# Get context with semantic search
|
||||
context = await peer.get_context(
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from ..types import PeerContext as _PeerContext
|
||||
|
||||
target_id = str(target.id) if isinstance(target, AsyncPeer) else target
|
||||
|
||||
response = await self._client.workspaces.peers.get_context(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
return _PeerContext.from_api_response(response)
|
||||
|
||||
@property
|
||||
def observations(self) -> "AsyncObservationScope":
|
||||
"""
|
||||
Access this peer's self-observations (where observer == observed == self).
|
||||
|
||||
This property provides a convenient way to access observations that this peer
|
||||
has made about themselves. Use this for self-observation scenarios.
|
||||
|
||||
Returns:
|
||||
An AsyncObservationScope scoped to this peer's self-observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# List self-observations
|
||||
obs_list = await peer.observations.list()
|
||||
|
||||
# Search self-observations
|
||||
results = await peer.observations.query("preferences")
|
||||
|
||||
# Delete a self-observation
|
||||
await peer.observations.delete("obs-123")
|
||||
```
|
||||
"""
|
||||
from ..observations import AsyncObservationScope as _AsyncObservationScope
|
||||
|
||||
return _AsyncObservationScope(self._client, self.workspace_id, self.id, self.id)
|
||||
|
||||
def observations_of(self, target: str | AsyncPeer) -> "AsyncObservationScope":
|
||||
"""
|
||||
Access observations this peer has made about another peer.
|
||||
|
||||
This method provides scoped access to observations where this peer is the
|
||||
observer and the target is the observed peer.
|
||||
|
||||
Args:
|
||||
target: The target peer (either an AsyncPeer object or peer ID string)
|
||||
|
||||
Returns:
|
||||
An AsyncObservationScope scoped to this peer's observations of the target
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
|
||||
# List observations
|
||||
obs_list = await bob_observations.list()
|
||||
|
||||
# Search observations
|
||||
results = await bob_observations.query("work history")
|
||||
|
||||
# Get the representation from these observations
|
||||
rep = await bob_observations.get_representation()
|
||||
```
|
||||
"""
|
||||
from ..observations import AsyncObservationScope as _AsyncObservationScope
|
||||
|
||||
target_id = target.id if isinstance(target, AsyncPeer) else target
|
||||
return _AsyncObservationScope(
|
||||
self._client, self.workspace_id, self.id, target_id
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the AsyncPeer.
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..session_context import SessionContext, SessionSummaries, Summary
|
||||
|
|
@ -17,6 +20,7 @@ from ..utils import prepare_file_for_upload
|
|||
from .pagination import AsyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types import Representation
|
||||
from .peer import AsyncPeer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -43,17 +47,31 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this session
|
||||
_client: Reference to the parent AsyncHoncho client instance
|
||||
anonymous: Whether this is an anonymous session
|
||||
summarize: Whether automatic summarization is enabled
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this session. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this session. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this session")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this session. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this session. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -66,6 +84,9 @@ class AsyncSession(BaseModel):
|
|||
client: AsyncHonchoCore = Field(
|
||||
..., description="Reference to the parent AsyncHoncho client instance"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = None,
|
||||
config: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new AsyncSession.
|
||||
|
|
@ -74,12 +95,16 @@ class AsyncSession(BaseModel):
|
|||
session_id: Unique identifier for this session within the workspace
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
client: Reference to the parent AsyncHoncho client instance
|
||||
metadata: Optional metadata to initialize the cached value
|
||||
config: Optional configuration to initialize the cached value
|
||||
"""
|
||||
super().__init__(
|
||||
id=session_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
|
|
@ -109,17 +134,22 @@ class AsyncSession(BaseModel):
|
|||
Returns:
|
||||
A new AsyncSession instance
|
||||
"""
|
||||
session = cls(session_id, workspace_id, client)
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
await client.workspaces.sessions.get_or_create(
|
||||
session_data = await client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
return cls(
|
||||
session_id,
|
||||
workspace_id,
|
||||
client,
|
||||
metadata=session_data.metadata,
|
||||
config=session_data.configuration,
|
||||
)
|
||||
|
||||
return session
|
||||
return cls(session_id, workspace_id, client)
|
||||
|
||||
async def add_peers(
|
||||
self,
|
||||
|
|
@ -314,7 +344,7 @@ class AsyncSession(BaseModel):
|
|||
messages: MessageCreateParam | list[MessageCreateParam] = Field(
|
||||
..., description="Messages to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Add one or more messages to this session.
|
||||
|
||||
|
|
@ -330,7 +360,7 @@ class AsyncSession(BaseModel):
|
|||
if not isinstance(messages, list):
|
||||
messages = [messages]
|
||||
|
||||
await self._client.workspaces.sessions.messages.create(
|
||||
return await self._client.workspaces.sessions.messages.create(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
messages=[MessageCreateParam(**message) for message in messages],
|
||||
|
|
@ -370,9 +400,16 @@ class AsyncSession(BaseModel):
|
|||
|
||||
async def delete(self) -> None:
|
||||
"""
|
||||
Delete this session.
|
||||
Delete this session and all associated data.
|
||||
|
||||
Makes an async API call to delete this session.
|
||||
Makes an async API call to permanently delete this session and all related data including:
|
||||
- Messages
|
||||
- Message embeddings
|
||||
- Observations
|
||||
- Session-Peer associations
|
||||
- Background processing queue items
|
||||
|
||||
This action cannot be undone.
|
||||
"""
|
||||
await self._client.workspaces.sessions.delete(
|
||||
session_id=self.id,
|
||||
|
|
@ -385,6 +422,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Makes an async API call to retrieve the current metadata associated with this session.
|
||||
Metadata can include custom attributes, settings, or any other key-value data.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's metadata. Returns an empty dictionary
|
||||
|
|
@ -394,7 +432,8 @@ class AsyncSession(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return session.metadata or {}
|
||||
self._metadata = session.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
async def set_metadata(
|
||||
|
|
@ -408,6 +447,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Makes an async API call to update the metadata associated with this session.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this session.
|
||||
|
|
@ -418,6 +458,65 @@ class AsyncSession(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
async def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for this session.
|
||||
|
||||
Makes an async API call to retrieve the current configuration associated with this session.
|
||||
Configuration includes settings that control session behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's configuration. Returns an empty dictionary
|
||||
if no configuration is set
|
||||
"""
|
||||
session = await self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._configuration = session.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
async def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this session"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for this session.
|
||||
|
||||
Makes an async API call to update the configuration associated with this session.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with this session.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.sessions.update(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
configuration=configuration,
|
||||
)
|
||||
self._configuration = configuration
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this session.
|
||||
|
||||
Makes a single async API call to retrieve the latest metadata and configuration
|
||||
associated with this session and updates the cached attributes.
|
||||
"""
|
||||
session = await self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = session.metadata or {}
|
||||
self._configuration = session.configuration or {}
|
||||
|
||||
@validate_call
|
||||
async def get_context(
|
||||
|
|
@ -439,6 +538,32 @@ class AsyncSession(BaseModel):
|
|||
None,
|
||||
description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.",
|
||||
),
|
||||
limit_to_session: bool = Field(
|
||||
False,
|
||||
description="Whether to limit the representation to this session only. If True, only observations from this session will be included.",
|
||||
),
|
||||
search_top_k: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Number of semantically relevant facts to return when searching with `last_user_message`.",
|
||||
),
|
||||
search_max_distance: float | None = Field(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.",
|
||||
),
|
||||
include_most_derived: bool | None = Field(
|
||||
None,
|
||||
description="Whether to include the most derived observations in the representation.",
|
||||
),
|
||||
max_observations: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of observations to include in the representation.",
|
||||
),
|
||||
) -> SessionContext:
|
||||
"""
|
||||
Get optimized context for this session within a token limit.
|
||||
|
|
@ -455,6 +580,11 @@ class AsyncSession(BaseModel):
|
|||
peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.
|
||||
last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.
|
||||
peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.
|
||||
limit_to_session: Whether to limit the representation to this session only. If True, only observations from this session will be included.
|
||||
search_top_k: Number of semantically relevant facts to return when searching with `last_user_message`.
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.
|
||||
include_most_derived: Whether to include the most derived observations in the representation.
|
||||
max_observations: Maximum number of observations to include in the representation.
|
||||
|
||||
Returns:
|
||||
A SessionContext object containing the optimized message history and
|
||||
|
|
@ -491,6 +621,15 @@ class AsyncSession(BaseModel):
|
|||
else omit,
|
||||
peer_target=peer_target if peer_target is not None else omit,
|
||||
peer_perspective=peer_perspective if peer_perspective is not None else omit,
|
||||
limit_to_session=limit_to_session,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
# Convert the honcho_core summary to our Summary if it exists
|
||||
|
|
@ -608,6 +747,18 @@ class AsyncSession(BaseModel):
|
|||
description="File to upload. Can be a file object, (filename, bytes, content_type) tuple, or (filename, fileobj, content_type) tuple.",
|
||||
),
|
||||
peer_id: str = Field(..., description="ID of the peer creating the messages"),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with the messages",
|
||||
),
|
||||
configuration: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the messages",
|
||||
),
|
||||
created_at: str | datetime | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.",
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Upload file to create message(s) in this session.
|
||||
|
|
@ -625,6 +776,9 @@ class AsyncSession(BaseModel):
|
|||
- a tuple (filename, bytes, content_type)
|
||||
- a tuple (filename, fileobj, content_type)
|
||||
peer_id: ID of the peer who will be attributed as the creator of the messages
|
||||
metadata: Optional metadata dictionary to associate with the messages
|
||||
configuration: Optional configuration dictionary to associate with the messages
|
||||
created_at: Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.
|
||||
|
||||
Returns:
|
||||
A list of Message objects representing the created messages
|
||||
|
|
@ -638,12 +792,26 @@ class AsyncSession(BaseModel):
|
|||
# Prepare file for upload using shared utility
|
||||
filename, content_bytes, content_type = prepare_file_for_upload(file)
|
||||
|
||||
# Call the upload endpoint
|
||||
# Build extra_body dict with optional fields as JSON strings (backend expects Form fields)
|
||||
extra_body_data: dict[str, str] = {}
|
||||
if metadata is not None:
|
||||
extra_body_data["metadata"] = json.dumps(metadata)
|
||||
if configuration is not None:
|
||||
extra_body_data["configuration"] = json.dumps(configuration)
|
||||
if created_at is not None:
|
||||
# Ensure created_at is a string (ISO format)
|
||||
if isinstance(created_at, datetime):
|
||||
extra_body_data["created_at"] = created_at.isoformat()
|
||||
else:
|
||||
extra_body_data["created_at"] = created_at
|
||||
|
||||
# Call the upload endpoint with extra_body for the additional form fields
|
||||
response = await self._client.workspaces.sessions.messages.upload(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
file=(filename, content_bytes, content_type),
|
||||
peer_id=peer_id,
|
||||
extra_body=extra_body_data if extra_body_data else None,
|
||||
)
|
||||
|
||||
return [Message.model_validate(msg) for msg in response]
|
||||
|
|
@ -653,7 +821,12 @@ class AsyncSession(BaseModel):
|
|||
peer: str | AsyncPeer,
|
||||
*,
|
||||
target: str | AsyncPeer | None = None,
|
||||
) -> dict[str, object]:
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the current working representation of the peer in this session.
|
||||
|
||||
|
|
@ -661,18 +834,51 @@ class AsyncSession(BaseModel):
|
|||
peer: Peer to get the working representation of.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
queries what `peer` knows about the `target`.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A dictionary containing information about the peer.
|
||||
"""
|
||||
from .peer import AsyncPeer
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
return await self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
Example:
|
||||
```python
|
||||
# Get peer's representation in this session
|
||||
rep = await session.working_rep('user123')
|
||||
print(rep)
|
||||
|
||||
# Get what user123 knows about assistant in this session
|
||||
local_rep = await session.working_rep('user123', target='assistant')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = await session.working_rep(
|
||||
'user123',
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from ..types import Representation as _Representation
|
||||
from .peer import AsyncPeer as _AsyncPeer
|
||||
|
||||
data = await self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, _AsyncPeer) else peer,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
target=str(target.id) if isinstance(target, _AsyncPeer) else target,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
return _Representation.from_dict(data) # type: ignore
|
||||
|
||||
@validate_call
|
||||
async def get_deriver_status(
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ class Honcho(BaseModel):
|
|||
`core` property to use functionality not exposed through this SDK.
|
||||
|
||||
Attributes:
|
||||
api_key: API key for authentication
|
||||
base_url: Base URL for the Honcho API
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this workspace. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this workspace. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
core: Access to the underlying honcho_core client for advanced usage
|
||||
"""
|
||||
|
||||
|
|
@ -44,8 +46,20 @@ class Honcho(BaseModel):
|
|||
min_length=1,
|
||||
description="Workspace ID for scoping operations",
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this workspace. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this workspace. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@property
|
||||
def core(self) -> HonchoCore:
|
||||
"""
|
||||
|
|
@ -183,6 +197,7 @@ class Honcho(BaseModel):
|
|||
Raises:
|
||||
ValidationError: If the peer ID is empty or invalid
|
||||
"""
|
||||
# Peer constructor handles API call and caching when metadata/config provided
|
||||
return Peer(
|
||||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
|
|
@ -204,7 +219,14 @@ class Honcho(BaseModel):
|
|||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return SyncPage(
|
||||
peers_page, lambda peer: Peer(peer.id, self.workspace_id, self._client)
|
||||
peers_page,
|
||||
lambda peer: Peer(
|
||||
peer.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=peer.metadata,
|
||||
config=peer.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -267,7 +289,13 @@ class Honcho(BaseModel):
|
|||
)
|
||||
return SyncPage(
|
||||
sessions_page,
|
||||
lambda session: Session(session.id, self.workspace_id, self._client),
|
||||
lambda session: Session(
|
||||
session.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=session.metadata,
|
||||
config=session.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
def get_metadata(self) -> dict[str, object]:
|
||||
|
|
@ -276,14 +304,16 @@ class Honcho(BaseModel):
|
|||
|
||||
Makes an API call to retrieve metadata associated with the current workspace.
|
||||
Workspace metadata can include settings, configuration, or any other
|
||||
key-value data associated with the workspace.
|
||||
key-value data associated with the workspace. This method also updates the
|
||||
cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's metadata. Returns an empty
|
||||
dictionary if no metadata is set
|
||||
"""
|
||||
workspace = self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
return workspace.metadata or {}
|
||||
self._metadata = workspace.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
def set_metadata(
|
||||
|
|
@ -295,12 +325,62 @@ class Honcho(BaseModel):
|
|||
|
||||
Makes an API call to update the metadata associated with the current workspace.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.update(self.workspace_id, metadata=metadata)
|
||||
self._metadata = metadata
|
||||
|
||||
def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for the current workspace.
|
||||
|
||||
Makes an API call to retrieve configuration associated with the current workspace.
|
||||
Configuration includes settings that control workspace behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's configuration. Returns an empty
|
||||
dictionary if no configuration is set
|
||||
"""
|
||||
workspace = self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._configuration = workspace.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for the current workspace.
|
||||
|
||||
Makes an API call to update the configuration associated with the current workspace.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.update(self.workspace_id, configuration=configuration)
|
||||
self._configuration = configuration
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for the current workspace.
|
||||
|
||||
Makes a single API call to retrieve the latest metadata and configuration
|
||||
associated with the current workspace and updates the cached attributes.
|
||||
"""
|
||||
workspace = self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._metadata = workspace.metadata or {}
|
||||
self._configuration = workspace.configuration or {}
|
||||
|
||||
def get_workspaces(self, filters: dict[str, object] | None = None) -> list[str]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,450 @@
|
|||
"""Observation types and scoped access for the Honcho SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import Representation
|
||||
|
||||
|
||||
class Observation:
|
||||
"""
|
||||
An observation from the theory-of-mind system.
|
||||
|
||||
Observations are facts derived from messages that help build a representation
|
||||
of a peer.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this observation
|
||||
content: The observation content/text
|
||||
observer_id: The peer who made the observation
|
||||
observed_id: The peer being observed
|
||||
session_id: The session where this observation was made
|
||||
created_at: When the observation was created
|
||||
"""
|
||||
|
||||
id: str
|
||||
content: str
|
||||
observer_id: str
|
||||
observed_id: str
|
||||
session_id: str
|
||||
created_at: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
content: str,
|
||||
observer_id: str,
|
||||
observed_id: str,
|
||||
session_id: str,
|
||||
created_at: str,
|
||||
):
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.observer_id = observer_id
|
||||
self.observed_id = observed_id
|
||||
self.session_id = session_id
|
||||
self.created_at = created_at
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "Observation":
|
||||
"""Create an Observation from an API response dict."""
|
||||
return cls(
|
||||
id=data.get("id", ""),
|
||||
content=data.get("content", ""),
|
||||
observer_id=data.get("observer_id", ""),
|
||||
observed_id=data.get("observed_id", ""),
|
||||
session_id=data.get("session_id", ""),
|
||||
created_at=data.get("created_at", ""),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
truncated = (
|
||||
f"{self.content[:50]}..." if len(self.content) > 50 else self.content
|
||||
)
|
||||
return f"Observation(id={self.id!r}, content={truncated!r})"
|
||||
|
||||
|
||||
class ObservationScope:
|
||||
"""
|
||||
Scoped access to observations for a specific observer/observed relationship.
|
||||
|
||||
This class provides convenient methods to list, query, and delete observations
|
||||
that are automatically scoped to a specific observer/observed pair.
|
||||
|
||||
Typically accessed via `peer.observations` (for self-observations) or
|
||||
`peer.observations_of(target)` (for observations about another peer).
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get self-observations
|
||||
observations = peer.observations
|
||||
obs_list = observations.list()
|
||||
search_results = observations.query("preferences")
|
||||
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
bob_list = bob_observations.list()
|
||||
```
|
||||
|
||||
Note:
|
||||
This class requires the core Honcho SDK to support observation endpoints.
|
||||
The observation endpoints are:
|
||||
- POST /workspaces/{workspace_id}/observations/list
|
||||
- POST /workspaces/{workspace_id}/observations/query
|
||||
- DELETE /workspaces/{workspace_id}/observations/{observation_id}
|
||||
"""
|
||||
|
||||
_client: Any
|
||||
workspace_id: str
|
||||
observer: str
|
||||
observed: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Any,
|
||||
workspace_id: str,
|
||||
observer: str,
|
||||
observed: str,
|
||||
):
|
||||
"""
|
||||
Initialize an ObservationScope.
|
||||
|
||||
Args:
|
||||
client: The Honcho client instance
|
||||
workspace_id: The workspace ID
|
||||
observer: The observer peer ID
|
||||
observed: The observed peer ID
|
||||
"""
|
||||
self._client = client
|
||||
self.workspace_id = workspace_id
|
||||
self.observer = observer
|
||||
self.observed = observed
|
||||
|
||||
def list(
|
||||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
session_id: str | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
List observations in this scope.
|
||||
|
||||
Args:
|
||||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session_id: Optional session ID to filter by
|
||||
|
||||
Returns:
|
||||
List of Observation objects
|
||||
"""
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
if session_id:
|
||||
filters["session_id"] = session_id
|
||||
|
||||
# Note: This requires the core SDK to support observations.list()
|
||||
response = self._client.workspaces.observations.list(
|
||||
workspace_id=self.workspace_id,
|
||||
filters=filters,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response.items]
|
||||
|
||||
def query(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int = 10,
|
||||
distance: float | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Semantic search for observations in this scope.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
top_k: Maximum number of results to return
|
||||
distance: Maximum cosine distance threshold (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
List of matching Observation objects
|
||||
"""
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
|
||||
# Note: This requires the core SDK to support observations.query()
|
||||
response = self._client.workspaces.observations.query(
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
top_k=top_k,
|
||||
distance=distance,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response]
|
||||
|
||||
def delete(self, observation_id: str) -> None:
|
||||
"""
|
||||
Delete an observation by ID.
|
||||
|
||||
Args:
|
||||
observation_id: The ID of the observation to delete
|
||||
"""
|
||||
# Note: This requires the core SDK to support observations.delete()
|
||||
self._client.workspaces.observations.delete(
|
||||
workspace_id=self.workspace_id,
|
||||
observation_id=observation_id,
|
||||
)
|
||||
|
||||
def get_representation(
|
||||
self,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the computed representation for this scope.
|
||||
|
||||
This returns the working representation (narrative) built from the
|
||||
observations in this scope.
|
||||
|
||||
Args:
|
||||
search_query: Optional semantic search query to curate the representation
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
"""
|
||||
from honcho_core._types import omit
|
||||
|
||||
from .types import Representation
|
||||
|
||||
response = self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.observer,
|
||||
workspace_id=self.workspace_id,
|
||||
target=self.observed,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
representation = response.get("representation")
|
||||
if representation is not None:
|
||||
return Representation.from_dict(cast(dict[str, Any], representation))
|
||||
else:
|
||||
return Representation.from_dict(response)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"ObservationScope(workspace_id={self.workspace_id!r}, "
|
||||
f"observer={self.observer!r}, observed={self.observed!r})"
|
||||
)
|
||||
|
||||
|
||||
class AsyncObservationScope:
|
||||
"""
|
||||
Async scoped access to observations for a specific observer/observed relationship.
|
||||
|
||||
This class provides convenient async methods to list, query, and delete observations
|
||||
that are automatically scoped to a specific observer/observed pair.
|
||||
|
||||
Typically accessed via `peer.observations` (for self-observations) or
|
||||
`peer.observations_of(target)` (for observations about another peer).
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get self-observations
|
||||
observations = peer.observations
|
||||
obs_list = await observations.list()
|
||||
search_results = await observations.query("preferences")
|
||||
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
bob_list = await bob_observations.list()
|
||||
```
|
||||
|
||||
Note:
|
||||
This class requires the core Honcho SDK to support observation endpoints.
|
||||
The observation endpoints are:
|
||||
- POST /workspaces/{workspace_id}/observations/list
|
||||
- POST /workspaces/{workspace_id}/observations/query
|
||||
- DELETE /workspaces/{workspace_id}/observations/{observation_id}
|
||||
"""
|
||||
|
||||
_client: Any
|
||||
workspace_id: str
|
||||
observer: str
|
||||
observed: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Any,
|
||||
workspace_id: str,
|
||||
observer: str,
|
||||
observed: str,
|
||||
):
|
||||
"""
|
||||
Initialize an AsyncObservationScope.
|
||||
|
||||
Args:
|
||||
client: The AsyncHoncho client instance
|
||||
workspace_id: The workspace ID
|
||||
observer: The observer peer ID
|
||||
observed: The observed peer ID
|
||||
"""
|
||||
self._client = client
|
||||
self.workspace_id = workspace_id
|
||||
self.observer = observer
|
||||
self.observed = observed
|
||||
|
||||
async def list(
|
||||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
session_id: str | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
List observations in this scope.
|
||||
|
||||
Args:
|
||||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session_id: Optional session ID to filter by
|
||||
|
||||
Returns:
|
||||
List of Observation objects
|
||||
"""
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
if session_id:
|
||||
filters["session_id"] = session_id
|
||||
|
||||
# Note: This requires the core SDK to support observations.list()
|
||||
response = await self._client.workspaces.observations.list(
|
||||
workspace_id=self.workspace_id,
|
||||
filters=filters,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response.items]
|
||||
|
||||
async def query(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int = 10,
|
||||
distance: float | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Semantic search for observations in this scope.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
top_k: Maximum number of results to return
|
||||
distance: Maximum cosine distance threshold (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
List of matching Observation objects
|
||||
"""
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
|
||||
# Note: This requires the core SDK to support observations.query()
|
||||
response = await self._client.workspaces.observations.query(
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
top_k=top_k,
|
||||
distance=distance,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response]
|
||||
|
||||
async def delete(self, observation_id: str) -> None:
|
||||
"""
|
||||
Delete an observation by ID.
|
||||
|
||||
Args:
|
||||
observation_id: The ID of the observation to delete
|
||||
"""
|
||||
# Note: This requires the core SDK to support observations.delete()
|
||||
await self._client.workspaces.observations.delete(
|
||||
workspace_id=self.workspace_id,
|
||||
observation_id=observation_id,
|
||||
)
|
||||
|
||||
async def get_representation(
|
||||
self,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the computed representation for this scope.
|
||||
|
||||
This returns the working representation (narrative) built from the
|
||||
observations in this scope.
|
||||
|
||||
Args:
|
||||
search_query: Optional semantic search query to curate the representation
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
"""
|
||||
from honcho_core._types import omit
|
||||
|
||||
from .types import Representation
|
||||
|
||||
response = await self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.observer,
|
||||
workspace_id=self.workspace_id,
|
||||
target=self.observed,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
representation = response.get("representation")
|
||||
if representation is not None:
|
||||
return Representation.from_dict(cast(dict[str, Any], representation))
|
||||
else:
|
||||
return Representation.from_dict(response)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"AsyncObservationScope(workspace_id={self.workspace_id!r}, "
|
||||
f"observer={self.observer!r}, observed={self.observed!r})"
|
||||
)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Generator
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import omit
|
||||
|
|
@ -10,13 +10,16 @@ from honcho_core.types.workspaces import PeerCardResponse
|
|||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .types import DialecticStreamResponse
|
||||
from .pagination import SyncPage
|
||||
from .types import DialecticStreamResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .observations import ObservationScope
|
||||
from .session import Session
|
||||
from .types import PeerContext, Representation
|
||||
|
||||
|
||||
class Peer(BaseModel):
|
||||
|
|
@ -29,15 +32,31 @@ class Peer(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this peer
|
||||
_client: Reference to the parent Honcho client instance
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this peer. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this peer. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this peer")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this peer. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this peer. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -77,16 +96,24 @@ class Peer(BaseModel):
|
|||
config: Optional configuration to set for this peer.
|
||||
If set, will get/create peer immediately with flags.
|
||||
"""
|
||||
super().__init__(id=peer_id, workspace_id=workspace_id)
|
||||
super().__init__(
|
||||
id=peer_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
self._client.workspaces.peers.get_or_create(
|
||||
peer_data = self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
# Update cached values with API response
|
||||
self._metadata = peer_data.metadata
|
||||
self._configuration = peer_data.configuration
|
||||
|
||||
def chat(
|
||||
self,
|
||||
|
|
@ -194,6 +221,10 @@ class Peer(BaseModel):
|
|||
metadata: dict[str, object] | None = Field(
|
||||
None, description="Optional metadata dictionary"
|
||||
),
|
||||
config: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the message",
|
||||
),
|
||||
created_at: datetime.datetime | str | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the message. Accepts a datetime which will be converted to an ISO 8601 string, or a preformatted string.",
|
||||
|
|
@ -221,6 +252,7 @@ class Peer(BaseModel):
|
|||
return MessageCreateParam(
|
||||
peer_id=self.id,
|
||||
content=content,
|
||||
configuration=config,
|
||||
metadata=metadata,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
|
@ -231,7 +263,7 @@ class Peer(BaseModel):
|
|||
|
||||
Makes an API call to retrieve metadata associated with this peer. Metadata
|
||||
can include custom attributes, settings, or any other key-value data
|
||||
associated with the peer.
|
||||
associated with the peer. This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's metadata. Returns an empty dictionary
|
||||
|
|
@ -241,7 +273,8 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.metadata or {}
|
||||
self._metadata = peer.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
def set_metadata(
|
||||
|
|
@ -255,6 +288,7 @@ class Peer(BaseModel):
|
|||
|
||||
Makes an API call to update the metadata associated with this peer.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this peer.
|
||||
|
|
@ -265,13 +299,15 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
def get_peer_config(self) -> dict[str, object]:
|
||||
def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
Makes an API call to retrieve configuration associated with this peer.
|
||||
Configuration currently includes one optional flag, `observe_me`.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
|
|
@ -280,10 +316,11 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.configuration or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
def set_peer_config(
|
||||
def set_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
|
|
@ -296,6 +333,7 @@ class Peer(BaseModel):
|
|||
|
||||
Makes an API call to update the configuration associated with this peer.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer.
|
||||
|
|
@ -306,6 +344,51 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
configuration=config,
|
||||
)
|
||||
self._configuration = config
|
||||
|
||||
def get_peer_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`get_config` instead.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
"""
|
||||
return self.get_config()
|
||||
|
||||
@validate_call
|
||||
def set_peer_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration for this peer.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`set_config` instead.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer
|
||||
"""
|
||||
return self.set_config(config)
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this peer.
|
||||
|
||||
Makes a single API call to retrieve the latest metadata and configuration
|
||||
associated with this peer and updates the cached attributes.
|
||||
"""
|
||||
peer = self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = peer.metadata or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
|
||||
@validate_call
|
||||
def search(
|
||||
|
|
@ -374,6 +457,206 @@ class Peer(BaseModel):
|
|||
|
||||
return "\n".join(items)
|
||||
|
||||
def working_rep(
|
||||
self,
|
||||
session: str | Session | None = None,
|
||||
target: str | Peer | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get a working representation for this peer.
|
||||
|
||||
Args:
|
||||
session: Optional session to scope the representation to.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
returns the representation of the target from the perspective of this peer.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get global representation
|
||||
rep = peer.working_rep()
|
||||
print(rep)
|
||||
|
||||
# Get representation scoped to a session
|
||||
session_rep = peer.working_rep(session='session-123')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = peer.working_rep(
|
||||
search_query='preferences',
|
||||
search_top_k=10,
|
||||
max_observations=50
|
||||
)
|
||||
```
|
||||
"""
|
||||
from .types import Representation as _Representation
|
||||
|
||||
session_id = (
|
||||
None
|
||||
if session is None
|
||||
else session
|
||||
if isinstance(session, str)
|
||||
else session.id
|
||||
)
|
||||
|
||||
data = self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=session_id,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
representation = data.get("representation")
|
||||
if representation is not None:
|
||||
return _Representation.from_dict(cast(dict[str, object], representation))
|
||||
else:
|
||||
return _Representation.from_dict(data)
|
||||
|
||||
def get_context(
|
||||
self,
|
||||
target: str | Peer | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "PeerContext":
|
||||
"""
|
||||
Get context for this peer, including representation and peer card.
|
||||
|
||||
This is a convenience method that retrieves both the working representation
|
||||
and peer card in a single API call.
|
||||
|
||||
Args:
|
||||
target: Optional target peer to get context for. If provided, returns
|
||||
the context for the target from this peer's perspective.
|
||||
Can be a Peer object or peer ID string.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A PeerContext object containing the representation and peer card
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get own context
|
||||
context = peer.get_context()
|
||||
print(context.representation)
|
||||
print(context.peer_card)
|
||||
|
||||
# Get context for another peer
|
||||
context = peer.get_context(target='other-peer-id')
|
||||
|
||||
# Get context with semantic search
|
||||
context = peer.get_context(
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from .types import PeerContext as _PeerContext
|
||||
|
||||
target_id = str(target.id) if isinstance(target, Peer) else target
|
||||
|
||||
response = self._client.workspaces.peers.get_context(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
return _PeerContext.from_api_response(response)
|
||||
|
||||
@property
|
||||
def observations(self) -> "ObservationScope":
|
||||
"""
|
||||
Access this peer's self-observations (where observer == observed == self).
|
||||
|
||||
This property provides a convenient way to access observations that this peer
|
||||
has made about themselves. Use this for self-observation scenarios.
|
||||
|
||||
Returns:
|
||||
An ObservationScope scoped to this peer's self-observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# List self-observations
|
||||
obs_list = peer.observations.list()
|
||||
|
||||
# Search self-observations
|
||||
results = peer.observations.query("preferences")
|
||||
|
||||
# Delete a self-observation
|
||||
peer.observations.delete("obs-123")
|
||||
```
|
||||
"""
|
||||
from .observations import ObservationScope as _ObservationScope
|
||||
|
||||
return _ObservationScope(self._client, self.workspace_id, self.id, self.id)
|
||||
|
||||
def observations_of(self, target: str | Peer) -> "ObservationScope":
|
||||
"""
|
||||
Access observations this peer has made about another peer.
|
||||
|
||||
This method provides scoped access to observations where this peer is the
|
||||
observer and the target is the observed peer.
|
||||
|
||||
Args:
|
||||
target: The target peer (either a Peer object or peer ID string)
|
||||
|
||||
Returns:
|
||||
An ObservationScope scoped to this peer's observations of the target
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
|
||||
# List observations
|
||||
obs_list = bob_observations.list()
|
||||
|
||||
# Search observations
|
||||
results = bob_observations.query("work history")
|
||||
|
||||
# Get the representation from these observations
|
||||
rep = bob_observations.get_representation()
|
||||
```
|
||||
"""
|
||||
from .observations import ObservationScope as _ObservationScope
|
||||
|
||||
target_id = target.id if isinstance(target, Peer) else target
|
||||
return _ObservationScope(self._client, self.workspace_id, self.id, target_id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Peer.
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ from __future__ import annotations
|
|||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .pagination import SyncPage
|
||||
|
|
@ -17,6 +20,7 @@ from .utils import prepare_file_for_upload
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from .peer import Peer
|
||||
from .types import Representation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -42,17 +46,31 @@ class Session(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this session
|
||||
_honcho: Reference to the parent Honcho client instance
|
||||
anonymous: Whether this is an anonymous session
|
||||
summarize: Whether automatic summarization is enabled
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this session. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this session. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this session")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this session. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this session. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -95,14 +113,19 @@ class Session(BaseModel):
|
|||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
self._client.workspaces.sessions.get_or_create(
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
# Update cached values with API response
|
||||
self._metadata = session_data.metadata
|
||||
self._configuration = session_data.configuration
|
||||
|
||||
def add_peers(
|
||||
self,
|
||||
|
|
@ -292,7 +315,7 @@ class Session(BaseModel):
|
|||
messages: MessageCreateParam | list[MessageCreateParam] = Field(
|
||||
..., description="Messages to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Add one or more messages to this session.
|
||||
|
||||
|
|
@ -308,7 +331,7 @@ class Session(BaseModel):
|
|||
if not isinstance(messages, list):
|
||||
messages = [messages]
|
||||
|
||||
self._client.workspaces.sessions.messages.create(
|
||||
return self._client.workspaces.sessions.messages.create(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
messages=[MessageCreateParam(**message) for message in messages],
|
||||
|
|
@ -352,24 +375,31 @@ class Session(BaseModel):
|
|||
|
||||
Makes an API call to retrieve the current metadata associated with this session.
|
||||
Metadata can include custom attributes, settings, or any other key-value data.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's metadata. Returns an empty dictionary
|
||||
if no metadata is set
|
||||
"""
|
||||
return (
|
||||
self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
).metadata
|
||||
or {}
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = session_data.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
def delete(self) -> None:
|
||||
"""
|
||||
Delete this session
|
||||
Delete this session and all associated data.
|
||||
|
||||
Makes an API call to mark this session as inactive.
|
||||
Makes an API call to permanently delete this session and all related data including:
|
||||
- Messages
|
||||
- Message embeddings
|
||||
- Observations
|
||||
- Session-Peer associations
|
||||
- Background processing queue items
|
||||
|
||||
This action cannot be undone.
|
||||
"""
|
||||
self._client.workspaces.sessions.delete(
|
||||
session_id=self.id,
|
||||
|
|
@ -388,6 +418,7 @@ class Session(BaseModel):
|
|||
|
||||
Makes an API call to update the metadata associated with this session.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this session.
|
||||
|
|
@ -398,6 +429,65 @@ class Session(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for this session.
|
||||
|
||||
Makes an API call to retrieve the current configuration associated with this session.
|
||||
Configuration includes settings that control session behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's configuration. Returns an empty dictionary
|
||||
if no configuration is set
|
||||
"""
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._configuration = session_data.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this session"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for this session.
|
||||
|
||||
Makes an API call to update the configuration associated with this session.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with this session.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.sessions.update(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
configuration=configuration,
|
||||
)
|
||||
self._configuration = configuration
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this session.
|
||||
|
||||
Makes a single API call to retrieve the latest metadata and configuration
|
||||
associated with this session and updates the cached attributes.
|
||||
"""
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = session_data.metadata or {}
|
||||
self._configuration = session_data.configuration or {}
|
||||
|
||||
@validate_call
|
||||
def get_context(
|
||||
|
|
@ -419,6 +509,32 @@ class Session(BaseModel):
|
|||
None,
|
||||
description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.",
|
||||
),
|
||||
limit_to_session: bool = Field(
|
||||
False,
|
||||
description="Whether to limit the representation to this session only. If True, only observations from this session will be included.",
|
||||
),
|
||||
search_top_k: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Number of semantically relevant facts to return when searching with `last_user_message`.",
|
||||
),
|
||||
search_max_distance: float | None = Field(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.",
|
||||
),
|
||||
include_most_derived: bool | None = Field(
|
||||
None,
|
||||
description="Whether to include the most derived observations in the representation.",
|
||||
),
|
||||
max_observations: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of observations to include in the representation.",
|
||||
),
|
||||
) -> SessionContext:
|
||||
"""
|
||||
Get optimized context for this session within a token limit.
|
||||
|
|
@ -435,6 +551,11 @@ class Session(BaseModel):
|
|||
peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.
|
||||
last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.
|
||||
peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.
|
||||
limit_to_session: Whether to limit the representation to this session only. If True, only observations from this session will be included.
|
||||
search_top_k: Number of semantically relevant facts to return when searching with `last_user_message`.
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.
|
||||
include_most_derived: Whether to include the most derived observations in the representation.
|
||||
max_observations: Maximum number of observations to include in the representation.
|
||||
|
||||
Returns:
|
||||
A SessionContext object containing the optimized message history and
|
||||
|
|
@ -471,6 +592,15 @@ class Session(BaseModel):
|
|||
else omit,
|
||||
peer_target=peer_target if peer_target is not None else omit,
|
||||
peer_perspective=peer_perspective if peer_perspective is not None else omit,
|
||||
limit_to_session=limit_to_session,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
# Convert the honcho_core summary to our Summary if it exists
|
||||
|
|
@ -588,6 +718,18 @@ class Session(BaseModel):
|
|||
description="File to upload. Can be a file object, (filename, bytes, content_type) tuple, or (filename, fileobj, content_type) tuple.",
|
||||
),
|
||||
peer_id: str = Field(..., description="ID of the peer creating the messages"),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with the messages",
|
||||
),
|
||||
configuration: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the messages",
|
||||
),
|
||||
created_at: str | datetime | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.",
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Upload file to create message(s) in this session.
|
||||
|
|
@ -605,6 +747,9 @@ class Session(BaseModel):
|
|||
- a tuple (filename, bytes, content_type)
|
||||
- a tuple (filename, fileobj, content_type)
|
||||
peer_id: ID of the peer who will be attributed as the creator of the messages
|
||||
metadata: Optional metadata dictionary to associate with the messages
|
||||
configuration: Optional configuration dictionary to associate with the messages
|
||||
created_at: Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.
|
||||
|
||||
Returns:
|
||||
A list of Message objects representing the created messages
|
||||
|
|
@ -618,12 +763,26 @@ class Session(BaseModel):
|
|||
# Prepare file for upload using shared utility
|
||||
filename, content_bytes, content_type = prepare_file_for_upload(file)
|
||||
|
||||
# Call the upload endpoint
|
||||
# Build extra_body dict with optional fields as JSON strings (backend expects Form fields)
|
||||
extra_body_data: dict[str, str] = {}
|
||||
if metadata is not None:
|
||||
extra_body_data["metadata"] = json.dumps(metadata)
|
||||
if configuration is not None:
|
||||
extra_body_data["configuration"] = json.dumps(configuration)
|
||||
if created_at is not None:
|
||||
# Ensure created_at is a string (ISO format)
|
||||
if isinstance(created_at, datetime):
|
||||
extra_body_data["created_at"] = created_at.isoformat()
|
||||
else:
|
||||
extra_body_data["created_at"] = created_at
|
||||
|
||||
# Call the upload endpoint with extra_body for the additional form fields
|
||||
response = self._client.workspaces.sessions.messages.upload(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
file=(filename, content_bytes, content_type),
|
||||
peer_id=peer_id,
|
||||
extra_body=extra_body_data if extra_body_data else None,
|
||||
)
|
||||
|
||||
return [Message.model_validate(msg) for msg in response]
|
||||
|
|
@ -633,7 +792,12 @@ class Session(BaseModel):
|
|||
peer: str | Peer,
|
||||
*,
|
||||
target: str | Peer | None = None,
|
||||
) -> dict[str, object]:
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the current working representation of the peer in this session.
|
||||
|
||||
|
|
@ -641,18 +805,51 @@ class Session(BaseModel):
|
|||
peer: Peer to get the working representation of.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
queries what `peer` knows about the `target`.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A dictionary containing information about the peer.
|
||||
"""
|
||||
from .peer import Peer
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
return self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
Example:
|
||||
```python
|
||||
# Get peer's representation in this session
|
||||
rep = session.working_rep('user123')
|
||||
print(rep)
|
||||
|
||||
# Get what user123 knows about assistant in this session
|
||||
local_rep = session.working_rep('user123', target='assistant')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = session.working_rep(
|
||||
'user123',
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from .peer import Peer as _Peer
|
||||
from .types import Representation as _Representation
|
||||
|
||||
data = self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, _Peer) else peer,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
target=str(target.id) if isinstance(target, _Peer) else target,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
return _Representation.from_dict(data) # type: ignore
|
||||
|
||||
@validate_call
|
||||
def get_deriver_status(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,322 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, AsyncIterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from datetime import datetime
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Re-export observation types from dedicated module
|
||||
from .observations import AsyncObservationScope, Observation, ObservationScope
|
||||
|
||||
__all__ = [
|
||||
"AsyncObservationScope",
|
||||
"DeductiveObservation",
|
||||
"DeductiveObservationBase",
|
||||
"DialecticStreamResponse",
|
||||
"ExplicitObservation",
|
||||
"ExplicitObservationBase",
|
||||
"Observation",
|
||||
"ObservationMetadata",
|
||||
"ObservationScope",
|
||||
"PeerContext",
|
||||
"Representation",
|
||||
]
|
||||
|
||||
|
||||
class ObservationMetadata(BaseModel):
|
||||
"""Metadata associated with an observation."""
|
||||
|
||||
created_at: datetime
|
||||
message_ids: list[int]
|
||||
session_name: str
|
||||
|
||||
|
||||
class ExplicitObservationBase(BaseModel):
|
||||
"""Base model for explicit observations - facts literally stated."""
|
||||
|
||||
content: str = Field(description="The explicit observation")
|
||||
|
||||
|
||||
class DeductiveObservationBase(BaseModel):
|
||||
"""Base model for deductive observations - logical conclusions."""
|
||||
|
||||
premises: list[str] = Field(
|
||||
description="Supporting premises or evidence for this conclusion",
|
||||
default_factory=list,
|
||||
)
|
||||
conclusion: str = Field(description="The deductive conclusion")
|
||||
|
||||
|
||||
class ExplicitObservation(ExplicitObservationBase, ObservationMetadata):
|
||||
"""
|
||||
Explicit observation with content and metadata.
|
||||
Represents facts LITERALLY stated - direct quotes or clear paraphrases only.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format observation with timestamp and content."""
|
||||
return f"[{self.created_at.replace(microsecond=0)}] {self.content}"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""
|
||||
Make ExplicitObservation hashable for use in sets.
|
||||
"""
|
||||
return hash((self.content, self.created_at, self.session_name))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""
|
||||
Define equality for ExplicitObservation objects.
|
||||
Two observations are equal if content, created_at, and session_name match.
|
||||
NOTE: message_ids are not included in the equality check.
|
||||
"""
|
||||
if not isinstance(other, ExplicitObservation):
|
||||
return False
|
||||
return (
|
||||
self.content == other.content
|
||||
and self.created_at == other.created_at
|
||||
and self.session_name == other.session_name
|
||||
)
|
||||
|
||||
|
||||
class DeductiveObservation(DeductiveObservationBase, ObservationMetadata):
|
||||
"""
|
||||
Deductive observation with multiple premises and one conclusion, plus metadata.
|
||||
Represents conclusions that MUST be true given explicit facts and premises.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format observation with timestamp, conclusion, and premises."""
|
||||
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
|
||||
return f"[{self.created_at.replace(microsecond=0)}] {self.conclusion}\n{premises_text}"
|
||||
|
||||
def str_no_timestamps(self) -> str:
|
||||
"""Format observation without timestamps."""
|
||||
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
|
||||
return f"{self.conclusion}\n{premises_text}"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""
|
||||
Make DeductiveObservation hashable for use in sets.
|
||||
NOTE: premises are not included in the hash.
|
||||
"""
|
||||
return hash((self.conclusion, self.created_at, self.session_name))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""
|
||||
Define equality for DeductiveObservation objects.
|
||||
Two observations are equal if all their fields match.
|
||||
NOTE: premises are not included in the equality check.
|
||||
"""
|
||||
if not isinstance(other, DeductiveObservation):
|
||||
return False
|
||||
return (
|
||||
self.conclusion == other.conclusion
|
||||
and self.created_at == other.created_at
|
||||
and self.session_name == other.session_name
|
||||
)
|
||||
|
||||
|
||||
class Representation(BaseModel):
|
||||
"""
|
||||
A Representation is a traversable and diffable map of observations.
|
||||
|
||||
At the base, we have a list of explicit observations, derived from a peer's messages.
|
||||
From there, deductive observations can be made by establishing logical relationships
|
||||
between explicit observations.
|
||||
|
||||
All of a peer's observations are stored as documents in a collection. These documents
|
||||
can be queried in various ways to produce this Representation object.
|
||||
|
||||
A "working representation" is a version of this data structure representing the most
|
||||
recent observations within a single session.
|
||||
|
||||
A representation can have a maximum number of observations, which is applied
|
||||
individually to each level of reasoning. If a maximum is set, observations are
|
||||
added and removed in FIFO order.
|
||||
"""
|
||||
|
||||
explicit: list[ExplicitObservation] = Field(
|
||||
description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference",
|
||||
default_factory=list,
|
||||
)
|
||||
deductive: list[DeductiveObservation] = Field(
|
||||
description="Conclusions that MUST be true given explicit facts and premises - strict logical necessities",
|
||||
default_factory=list,
|
||||
)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
Check if the representation is empty.
|
||||
"""
|
||||
return len(self.explicit) == 0 and len(self.deductive) == 0
|
||||
|
||||
def diff_representation(self, other: "Representation") -> "Representation":
|
||||
"""
|
||||
Given this and another representation, return a new representation with only
|
||||
observations that are unique to the other.
|
||||
|
||||
Note: This only removes literal duplicates, not semantically equivalent ones.
|
||||
|
||||
Args:
|
||||
other: The representation to compare against
|
||||
|
||||
Returns:
|
||||
A new Representation containing only observations unique to other
|
||||
"""
|
||||
diff = Representation()
|
||||
diff.explicit = [o for o in other.explicit if o not in self.explicit]
|
||||
diff.deductive = [o for o in other.deductive if o not in self.deductive]
|
||||
return diff
|
||||
|
||||
def merge_representation(
|
||||
self, other: "Representation", max_observations: int | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Merge another representation object into this one.
|
||||
|
||||
This will automatically deduplicate explicit and deductive observations.
|
||||
This *preserves order* of observations so that they retain FIFO order.
|
||||
|
||||
NOTE: observations with the *same* timestamp will not have order preserved.
|
||||
That's fine though, because they are from the same timestamp...
|
||||
|
||||
Args:
|
||||
other: The representation to merge into this one
|
||||
max_observations: Optional maximum number of observations to keep per type
|
||||
"""
|
||||
# removing duplicates by going list->set->list
|
||||
self.explicit = list(set(self.explicit + other.explicit))
|
||||
self.deductive = list(set(self.deductive + other.deductive))
|
||||
# sort by created_at
|
||||
self.explicit.sort(key=lambda x: x.created_at)
|
||||
self.deductive.sort(key=lambda x: x.created_at)
|
||||
|
||||
if max_observations:
|
||||
self.explicit = self.explicit[-max_observations:]
|
||||
self.deductive = self.deductive[-max_observations:]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format representation into a clean, readable string for LLM prompts.
|
||||
NOTE: we always strip subsecond precision from the timestamps.
|
||||
|
||||
Returns:
|
||||
Formatted string with clear sections and bullet points including temporal metadata
|
||||
|
||||
Example:
|
||||
EXPLICIT:
|
||||
1. [2025-01-01 12:00:00] The user has a dog named Rover
|
||||
2. [2025-01-01 12:01:00] The user's dog is 5 years old
|
||||
|
||||
DEDUCTIVE:
|
||||
1. [2025-01-01 12:01:00] Rover is 5 years old
|
||||
- The user has a dog named Rover
|
||||
- The user's dog is 5 years old
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def str_no_timestamps(self) -> str:
|
||||
"""
|
||||
Format representation into a clean, readable string for LLM prompts... but without timestamps.
|
||||
|
||||
Returns:
|
||||
Formatted string with clear sections and bullet points without temporal metadata
|
||||
|
||||
Example:
|
||||
EXPLICIT:
|
||||
1. The user has a dog named Rover
|
||||
2. The user's dog is 5 years old
|
||||
|
||||
DEDUCTIVE:
|
||||
1. Rover is 5 years old
|
||||
- The user has a dog named Rover
|
||||
- The user's dog is 5 years old
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation.content}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation.str_no_timestamps()}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def format_as_markdown(self) -> str:
|
||||
"""
|
||||
Format a Representation object as markdown.
|
||||
NOTE: we always strip subsecond precision from the timestamps.
|
||||
|
||||
Returns:
|
||||
Formatted markdown string with headers and lists
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
# Add explicit observations
|
||||
parts.append("## Explicit Observations\n")
|
||||
for i, obs in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {obs}")
|
||||
parts.append("")
|
||||
|
||||
# Add deductive observations
|
||||
parts.append("## Deductive Observations\n")
|
||||
for i, obs in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. **Conclusion**: {obs.conclusion}")
|
||||
if obs.premises:
|
||||
parts.append(" **Premises**:")
|
||||
for premise in obs.premises:
|
||||
parts.append(f" - {premise}")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Representation":
|
||||
"""
|
||||
Create a Representation from a dictionary (typically from API response).
|
||||
|
||||
Args:
|
||||
data: Dictionary containing 'explicit' and 'deductive' observation lists
|
||||
|
||||
Returns:
|
||||
A new Representation instance
|
||||
|
||||
Raises:
|
||||
ValidationError: If observation data is missing required fields
|
||||
"""
|
||||
explicit_data: Any = data.get("explicit", [])
|
||||
deductive_data: Any = data.get("deductive", [])
|
||||
|
||||
explicit_list = cast(
|
||||
list[Any], explicit_data if isinstance(explicit_data, list) else []
|
||||
)
|
||||
deductive_list = cast(
|
||||
list[Any], deductive_data if isinstance(deductive_data, list) else []
|
||||
)
|
||||
|
||||
return cls(
|
||||
explicit=[ExplicitObservation(**obs) for obs in explicit_list],
|
||||
deductive=[DeductiveObservation(**obs) for obs in deductive_list],
|
||||
)
|
||||
|
||||
|
||||
class DialecticStreamResponse:
|
||||
|
|
@ -62,7 +377,7 @@ class DialecticStreamResponse:
|
|||
try:
|
||||
if not isinstance(self._iterator, Iterator):
|
||||
raise TypeError("iterator must be an sync iterator, got async iterator")
|
||||
chunk = next(self._iterator) # type: ignore
|
||||
chunk = next(self._iterator)
|
||||
self._accumulated_content.append(chunk)
|
||||
return chunk
|
||||
except StopIteration:
|
||||
|
|
@ -80,7 +395,7 @@ class DialecticStreamResponse:
|
|||
try:
|
||||
if not isinstance(self._iterator, AsyncIterator):
|
||||
raise TypeError("iterator must be an async iterator, got sync iterator")
|
||||
chunk = await self._iterator.__anext__() # type: ignore
|
||||
chunk = await self._iterator.__anext__()
|
||||
self._accumulated_content.append(chunk)
|
||||
return chunk
|
||||
except StopAsyncIteration:
|
||||
|
|
@ -104,3 +419,81 @@ class DialecticStreamResponse:
|
|||
def is_complete(self) -> bool:
|
||||
"""Check if the stream has finished."""
|
||||
return self._is_complete
|
||||
|
||||
|
||||
class PeerContext:
|
||||
"""
|
||||
Context for a peer, including representation and peer card.
|
||||
|
||||
This class holds both the working representation and peer card for a peer,
|
||||
typically returned from the get_context API call.
|
||||
|
||||
Attributes:
|
||||
peer_id: The ID of the observer peer
|
||||
target_id: The ID of the target peer being observed
|
||||
representation: The working representation (may be None if no observations exist)
|
||||
peer_card: List of peer card strings (may be None if no card exists)
|
||||
"""
|
||||
|
||||
peer_id: str
|
||||
target_id: str
|
||||
representation: Representation | None
|
||||
peer_card: list[str] | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
peer_id: str,
|
||||
target_id: str,
|
||||
representation: Representation | None = None,
|
||||
peer_card: list[str] | None = None,
|
||||
):
|
||||
self.peer_id = peer_id
|
||||
self.target_id = target_id
|
||||
self.representation = representation
|
||||
self.peer_card = peer_card
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: Any) -> "PeerContext":
|
||||
"""
|
||||
Create a PeerContext from an API response.
|
||||
|
||||
Args:
|
||||
response: API response object with peer_id, target_id, representation, and peer_card
|
||||
|
||||
Returns:
|
||||
A new PeerContext instance
|
||||
"""
|
||||
peer_id = getattr(response, "peer_id", "") or ""
|
||||
target_id = getattr(response, "target_id", "") or ""
|
||||
|
||||
representation = None
|
||||
rep_data = getattr(response, "representation", None)
|
||||
if rep_data is not None:
|
||||
if isinstance(rep_data, dict):
|
||||
representation = Representation.from_dict(
|
||||
cast(dict[str, Any], rep_data)
|
||||
)
|
||||
elif hasattr(rep_data, "explicit") and hasattr(rep_data, "deductive"):
|
||||
representation = Representation.from_dict(
|
||||
{
|
||||
"explicit": rep_data.explicit,
|
||||
"deductive": rep_data.deductive,
|
||||
}
|
||||
)
|
||||
|
||||
peer_card = getattr(response, "peer_card", None)
|
||||
|
||||
return cls(
|
||||
peer_id=peer_id,
|
||||
target_id=target_id,
|
||||
representation=representation,
|
||||
peer_card=peer_card,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
has_rep = self.representation is not None
|
||||
has_card = self.peer_card is not None and len(self.peer_card) > 0
|
||||
return (
|
||||
f"PeerContext(peer_id={self.peer_id!r}, target_id={self.target_id!r}, "
|
||||
f"has_representation={has_rep}, has_peer_card={has_card})"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,509 +0,0 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.8"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.9'" },
|
||||
{ name = "idna", marker = "python_full_version < '3.9'" },
|
||||
{ name = "sniffio", marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293, upload-time = "2024-10-13T22:18:03.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766, upload-time = "2024-10-13T22:18:01.524Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
||||
{ name = "idna", marker = "python_full_version >= '3.9'" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "honcho-ai"
|
||||
version = "1.4.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "honcho-core" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = ">=1.4.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
||||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/c2/4d3737d5c0a75e2f59f324bed7428a9593c8edfcb7500cf402c46ad7378a/honcho_core-1.4.0.tar.gz", hash = "sha256:b30de9247763c01c3c37ed22b5d04d3c611440dd1983168f3d7d93f2cc0ecf7d", size = 126164, upload-time = "2025-08-12T19:06:54.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ac/6eb2e38376736bb1dfd26bbdfd96490bfbb6f0ddcb00b39913e93799e7d8/honcho_core-1.4.0-py3-none-any.whl", hash = "sha256:a84397fd9daf546a04f5458d7233ebd5eeafcb97af75ec60a9327db89abdaf03", size = 117602, upload-time = "2025-08-12T19:06:53.667Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "annotated-types", marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic-core", version = "2.27.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "annotated-types", marker = "python_full_version >= '3.9'" },
|
||||
{ name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-inspection", marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938, upload-time = "2024-12-18T11:27:14.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684, upload-time = "2024-12-18T11:27:16.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169, upload-time = "2024-12-18T11:27:22.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227, upload-time = "2024-12-18T11:27:25.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695, upload-time = "2024-12-18T11:27:28.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662, upload-time = "2024-12-18T11:27:30.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370, upload-time = "2024-12-18T11:27:33.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813, upload-time = "2024-12-18T11:27:37.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287, upload-time = "2024-12-18T11:27:40.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414, upload-time = "2024-12-18T11:27:43.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301, upload-time = "2024-12-18T11:27:47.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685, upload-time = "2024-12-18T11:27:50.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876, upload-time = "2024-12-18T11:27:53.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421, upload-time = "2024-12-18T11:27:55.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998, upload-time = "2024-12-18T11:27:57.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167, upload-time = "2024-12-18T11:27:59.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071, upload-time = "2024-12-18T11:28:02.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244, upload-time = "2024-12-18T11:28:04.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470, upload-time = "2024-12-18T11:28:07.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291, upload-time = "2024-12-18T11:28:10.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613, upload-time = "2024-12-18T11:28:13.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355, upload-time = "2024-12-18T11:28:16.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661, upload-time = "2024-12-18T11:28:18.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261, upload-time = "2024-12-18T11:28:21.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709, upload-time = "2024-12-18T11:29:03.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273, upload-time = "2024-12-18T11:29:05.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027, upload-time = "2024-12-18T11:29:07.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888, upload-time = "2024-12-18T11:29:09.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738, upload-time = "2024-12-18T11:29:11.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138, upload-time = "2024-12-18T11:29:16.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025, upload-time = "2024-12-18T11:29:20.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633, upload-time = "2024-12-18T11:29:23.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404, upload-time = "2024-12-18T11:29:25.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130, upload-time = "2024-12-18T11:29:29.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946, upload-time = "2024-12-18T11:29:31.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387, upload-time = "2024-12-18T11:29:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453, upload-time = "2024-12-18T11:29:35.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186, upload-time = "2024-12-18T11:29:37.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/53/13e9917fc69c0a4aea06fd63ed6a8d6cda9cf140ca9584d49c1650b0ef5e/pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", size = 1899595, upload-time = "2024-12-18T11:29:40.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/20/26c549249769ed84877f862f7bb93f89a6ee08b4bee1ed8781616b7fbb5e/pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", size = 1775010, upload-time = "2024-12-18T11:29:44.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/eb/8234e05452d92d2b102ffa1b56d801c3567e628fdc63f02080fdfc68fd5e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", size = 1830727, upload-time = "2024-12-18T11:29:46.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/df/59f915c8b929d5f61e5a46accf748a87110ba145156f9326d1a7d28912b2/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", size = 1868393, upload-time = "2024-12-18T11:29:49.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/52/81cf4071dca654d485c277c581db368b0c95b2b883f4d7b736ab54f72ddf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", size = 2040300, upload-time = "2024-12-18T11:29:51.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/00/05197ce1614f5c08d7a06e1d39d5d8e704dc81971b2719af134b844e2eaf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", size = 2738785, upload-time = "2024-12-18T11:29:55.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a3/5f19bc495793546825ab160e530330c2afcee2281c02b5ffafd0b32ac05e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", size = 1996493, upload-time = "2024-12-18T11:29:57.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e8/e0102c2ec153dc3eed88aea03990e1b06cfbca532916b8a48173245afe60/pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", size = 1998544, upload-time = "2024-12-18T11:30:00.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a3/4be70845b555bd80aaee9f9812a7cf3df81550bce6dadb3cfee9c5d8421d/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", size = 2007449, upload-time = "2024-12-18T11:30:02.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9f/b779ed2480ba355c054e6d7ea77792467631d674b13d8257085a4bc7dcda/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", size = 2129460, upload-time = "2024-12-18T11:30:06.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f0/a6ab0681f6e95260c7fbf552874af7302f2ea37b459f9b7f00698f875492/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", size = 2159609, upload-time = "2024-12-18T11:30:09.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2b/e1059506795104349712fbca647b18b3f4a7fd541c099e6259717441e1e0/pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", size = 1819886, upload-time = "2024-12-18T11:30:11.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/6d/df49c17f024dfc58db0bacc7b03610058018dd2ea2eaf748ccbada4c3d06/pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad", size = 1980773, upload-time = "2024-12-18T11:30:14.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475, upload-time = "2024-12-18T11:30:18.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279, upload-time = "2024-12-18T11:30:20.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112, upload-time = "2024-12-18T11:30:23.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780, upload-time = "2024-12-18T11:30:25.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943, upload-time = "2024-12-18T11:30:28.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492, upload-time = "2024-12-18T11:30:30.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714, upload-time = "2024-12-18T11:30:34.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163, upload-time = "2024-12-18T11:30:37.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217, upload-time = "2024-12-18T11:30:40.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899, upload-time = "2024-12-18T11:30:42.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726, upload-time = "2024-12-18T11:30:45.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219, upload-time = "2024-12-18T11:30:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382, upload-time = "2024-12-18T11:30:51.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159, upload-time = "2024-12-18T11:30:54.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331, upload-time = "2024-12-18T11:30:58.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467, upload-time = "2024-12-18T11:31:00.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797, upload-time = "2024-12-18T11:31:07.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839, upload-time = "2024-12-18T11:31:09.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861, upload-time = "2024-12-18T11:31:13.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582, upload-time = "2024-12-18T11:31:17.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985, upload-time = "2024-12-18T11:31:19.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715, upload-time = "2024-12-18T11:31:22.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733, upload-time = "2024-12-18T11:31:26.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375, upload-time = "2024-12-18T11:31:29.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307, upload-time = "2024-12-18T11:31:33.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971, upload-time = "2024-12-18T11:31:35.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616, upload-time = "2024-12-18T11:31:38.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943, upload-time = "2024-12-18T11:31:41.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654, upload-time = "2024-12-18T11:31:44.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292, upload-time = "2024-12-18T11:31:48.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961, upload-time = "2024-12-18T11:31:52.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
|
@ -104,6 +104,12 @@ describe('Honcho Client', () => {
|
|||
const metadata = { name: 'Test Peer' };
|
||||
const config = { observe_me: false };
|
||||
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
await honcho.peer('test-peer', { metadata, config });
|
||||
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
|
||||
|
|
@ -176,6 +182,12 @@ describe('Honcho Client', () => {
|
|||
const metadata = { name: 'Test Session' };
|
||||
const config = { anonymous: true };
|
||||
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue({
|
||||
id: 'test-session',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
await honcho.session('test-session', { metadata, config });
|
||||
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Peer } from '../src/peer'
|
|||
import { Session } from '../src/session'
|
||||
import { SessionContext } from '../src/session_context'
|
||||
import { Page } from '../src/pagination'
|
||||
import { Representation } from '../src/representation'
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
let mockWorkspacesApi: any
|
||||
|
|
@ -369,16 +370,35 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
})
|
||||
|
||||
it('should handle working representation queries', async () => {
|
||||
const mockWorkingRep = {
|
||||
peer_id: 'alice',
|
||||
knowledge: 'Alice likes coffee and works as a developer',
|
||||
relationships: ['bob', 'charlie'],
|
||||
context: 'session-specific context',
|
||||
const mockWorkingRepData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Alice likes coffee',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'working-rep-session',
|
||||
},
|
||||
{
|
||||
content: 'Alice works as a developer',
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
message_ids: [[3, 4]],
|
||||
session_name: 'working-rep-session',
|
||||
},
|
||||
],
|
||||
deductive: [
|
||||
{
|
||||
conclusion: 'Alice is a coffee-drinking developer',
|
||||
premises: ['Alice likes coffee', 'Alice works as a developer'],
|
||||
created_at: '2024-01-01T00:02:00Z',
|
||||
message_ids: [[5, 6]],
|
||||
session_name: 'working-rep-session',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockWorkingRep
|
||||
)
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockWorkingRepData,
|
||||
})
|
||||
|
||||
const session = await honcho.session('working-rep-session')
|
||||
const alice = await honcho.peer('alice')
|
||||
|
|
@ -386,7 +406,12 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
// Test working representation without target
|
||||
const globalRep = await session.workingRep('alice')
|
||||
expect(globalRep).toEqual(mockWorkingRep)
|
||||
expect(globalRep).toBeInstanceOf(Representation)
|
||||
expect(globalRep.explicit).toHaveLength(2)
|
||||
expect(globalRep.explicit[0].content).toBe('Alice likes coffee')
|
||||
expect(globalRep.explicit[1].content).toBe('Alice works as a developer')
|
||||
expect(globalRep.deductive).toHaveLength(1)
|
||||
expect(globalRep.deductive[0].conclusion).toBe('Alice is a coffee-drinking developer')
|
||||
expect(
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('integration-test-workspace', 'alice', {
|
||||
|
|
@ -395,7 +420,8 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
})
|
||||
|
||||
// Test working representation with target
|
||||
await session.workingRep(alice, bob)
|
||||
const targetRep = await session.workingRep(alice, bob)
|
||||
expect(targetRep).toBeInstanceOf(Representation)
|
||||
expect(
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('integration-test-workspace', 'alice', {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,565 @@
|
|||
import { Honcho } from '../src/client';
|
||||
import { Peer } from '../src/peer';
|
||||
import { Session } from '../src/session';
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
workspaces: {
|
||||
peers: {
|
||||
list: jest.fn(),
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
sessions: {
|
||||
list: jest.fn(),
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Metadata and Configuration Caching', () => {
|
||||
let honcho: Honcho;
|
||||
let mockClient: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
honcho = new Honcho({
|
||||
workspaceId: 'test-workspace',
|
||||
apiKey: 'test-key',
|
||||
environment: 'local',
|
||||
});
|
||||
|
||||
mockClient = (honcho as any)._client;
|
||||
});
|
||||
|
||||
describe('Workspace Metadata Caching', () => {
|
||||
it('should initialize with undefined metadata', () => {
|
||||
expect(honcho.metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should cache metadata after getMetadata call', async () => {
|
||||
const mockWorkspace = {
|
||||
id: 'test-workspace',
|
||||
metadata: { theme: 'dark', version: '1.0' },
|
||||
};
|
||||
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
|
||||
|
||||
const metadata = await honcho.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({ theme: 'dark', version: '1.0' });
|
||||
expect(honcho.metadata).toEqual({ theme: 'dark', version: '1.0' });
|
||||
});
|
||||
|
||||
it('should cache empty object when metadata is null', async () => {
|
||||
const mockWorkspace = {
|
||||
id: 'test-workspace',
|
||||
metadata: null,
|
||||
};
|
||||
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
|
||||
|
||||
const metadata = await honcho.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({});
|
||||
expect(honcho.metadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached metadata after setMetadata call', async () => {
|
||||
mockClient.workspaces.update.mockResolvedValue({});
|
||||
|
||||
const newMetadata = { theme: 'light', version: '2.0' };
|
||||
await honcho.setMetadata(newMetadata);
|
||||
|
||||
expect(honcho.metadata).toEqual(newMetadata);
|
||||
});
|
||||
|
||||
it('should maintain cached value across multiple calls', async () => {
|
||||
const mockWorkspace = {
|
||||
id: 'test-workspace',
|
||||
metadata: { count: 1 },
|
||||
};
|
||||
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
|
||||
|
||||
await honcho.getMetadata();
|
||||
expect(honcho.metadata).toEqual({ count: 1 });
|
||||
|
||||
// Update cache
|
||||
mockClient.workspaces.update.mockResolvedValue({});
|
||||
await honcho.setMetadata({ count: 2 });
|
||||
expect(honcho.metadata).toEqual({ count: 2 });
|
||||
|
||||
// Verify cache persists
|
||||
expect(honcho.metadata).toEqual({ count: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer Metadata and Configuration Caching', () => {
|
||||
describe('Peer Constructor with metadata/config', () => {
|
||||
it('should initialize peer with provided metadata and config', async () => {
|
||||
const metadata = { name: 'Test Peer', role: 'assistant' };
|
||||
const config = { observe_me: false };
|
||||
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'peer1',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
const peer = await honcho.peer('peer1', { metadata, config });
|
||||
|
||||
expect(peer.metadata).toEqual(metadata);
|
||||
expect(peer.configuration).toEqual(config);
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'peer1', metadata, configuration: config }
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize peer without metadata/config', async () => {
|
||||
const peer = await honcho.peer('peer1');
|
||||
|
||||
expect(peer.metadata).toBeUndefined();
|
||||
expect(peer.configuration).toBeUndefined();
|
||||
expect(mockClient.workspaces.peers.getOrCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer Metadata Caching', () => {
|
||||
let peer: Peer;
|
||||
|
||||
beforeEach(() => {
|
||||
peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache metadata after getMetadata call', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
metadata: { name: 'Alice', role: 'user' },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const metadata = await peer.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({ name: 'Alice', role: 'user' });
|
||||
expect(peer.metadata).toEqual({ name: 'Alice', role: 'user' });
|
||||
});
|
||||
|
||||
it('should cache empty object when metadata is null', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
metadata: null,
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const metadata = await peer.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({});
|
||||
expect(peer.metadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached metadata after setMetadata call', async () => {
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
const newMetadata = { name: 'Bob', role: 'admin' };
|
||||
await peer.setMetadata(newMetadata);
|
||||
|
||||
expect(peer.metadata).toEqual(newMetadata);
|
||||
});
|
||||
|
||||
it('should maintain cached value across operations', async () => {
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: { score: 100 },
|
||||
});
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
// Get initial metadata
|
||||
await peer.getMetadata();
|
||||
expect(peer.metadata).toEqual({ score: 100 });
|
||||
|
||||
// Update metadata
|
||||
await peer.setMetadata({ score: 200 });
|
||||
expect(peer.metadata).toEqual({ score: 200 });
|
||||
|
||||
// Verify cache persists
|
||||
expect(peer.metadata).toEqual({ score: 200 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer Configuration Caching', () => {
|
||||
let peer: Peer;
|
||||
|
||||
beforeEach(() => {
|
||||
peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache configuration after getConfig call', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: { observe_me: true, observe_others: false },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getConfig();
|
||||
|
||||
expect(config).toEqual({ observe_me: true, observe_others: false });
|
||||
expect(peer.configuration).toEqual({ observe_me: true, observe_others: false });
|
||||
});
|
||||
|
||||
it('should cache empty object when configuration is null', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: null,
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
expect(peer.configuration).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached configuration after setConfig call', async () => {
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
const newConfig = { observe_me: false, observe_others: true };
|
||||
await peer.setConfig(newConfig);
|
||||
|
||||
expect(peer.configuration).toEqual(newConfig);
|
||||
});
|
||||
|
||||
it('should support deprecated getPeerConfig method', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: { observe_me: true },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getPeerConfig();
|
||||
|
||||
expect(config).toEqual({ observe_me: true });
|
||||
expect(peer.configuration).toEqual({ observe_me: true });
|
||||
});
|
||||
|
||||
it('should support deprecated setPeerConfig method', async () => {
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
const newConfig = { observe_me: false };
|
||||
await peer.setPeerConfig(newConfig);
|
||||
|
||||
expect(peer.configuration).toEqual(newConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer List with Cached Data', () => {
|
||||
it('should populate metadata and config when listing peers', async () => {
|
||||
const mockPeersData = {
|
||||
items: [
|
||||
{
|
||||
id: 'peer1',
|
||||
metadata: { name: 'Alice' },
|
||||
configuration: { observe_me: true },
|
||||
},
|
||||
{
|
||||
id: 'peer2',
|
||||
metadata: { name: 'Bob' },
|
||||
configuration: { observe_me: false },
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData);
|
||||
|
||||
const peersPage = await honcho.getPeers();
|
||||
const peers = peersPage.items;
|
||||
|
||||
expect(peers[0].metadata).toEqual({ name: 'Alice' });
|
||||
expect(peers[0].configuration).toEqual({ observe_me: true });
|
||||
expect(peers[1].metadata).toEqual({ name: 'Bob' });
|
||||
expect(peers[1].configuration).toEqual({ observe_me: false });
|
||||
});
|
||||
|
||||
it('should handle null metadata and config in peer list', async () => {
|
||||
const mockPeersData = {
|
||||
items: [
|
||||
{
|
||||
id: 'peer1',
|
||||
metadata: null,
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData);
|
||||
|
||||
const peersPage = await honcho.getPeers();
|
||||
const peers = peersPage.items;
|
||||
|
||||
expect(peers[0].metadata).toBeUndefined();
|
||||
expect(peers[0].configuration).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Metadata and Configuration Caching', () => {
|
||||
describe('Session Constructor with metadata/config', () => {
|
||||
it('should initialize session with provided metadata and config', async () => {
|
||||
const metadata = { title: 'Test Session', tags: ['important'] };
|
||||
const config = { anonymous: false };
|
||||
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue({
|
||||
id: 'session1',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
const session = await honcho.session('session1', { metadata, config });
|
||||
|
||||
expect(session.metadata).toEqual(metadata);
|
||||
expect(session.configuration).toEqual(config);
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'session1', metadata, configuration: config }
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize session without metadata/config', async () => {
|
||||
const session = await honcho.session('session1');
|
||||
|
||||
expect(session.metadata).toBeUndefined();
|
||||
expect(session.configuration).toBeUndefined();
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Metadata Caching', () => {
|
||||
let session: Session;
|
||||
|
||||
beforeEach(() => {
|
||||
session = new Session('test-session', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache metadata after getMetadata call', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
metadata: { title: 'Chat Session', active: true },
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const metadata = await session.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({ title: 'Chat Session', active: true });
|
||||
expect(session.metadata).toEqual({ title: 'Chat Session', active: true });
|
||||
});
|
||||
|
||||
it('should cache empty object when metadata is null', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
metadata: null,
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const metadata = await session.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({});
|
||||
expect(session.metadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached metadata after setMetadata call', async () => {
|
||||
mockClient.workspaces.sessions.update.mockResolvedValue({});
|
||||
|
||||
const newMetadata = { title: 'Updated Session', active: false };
|
||||
await session.setMetadata(newMetadata);
|
||||
|
||||
expect(session.metadata).toEqual(newMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Configuration Caching', () => {
|
||||
let session: Session;
|
||||
|
||||
beforeEach(() => {
|
||||
session = new Session('test-session', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache configuration after getConfig call', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
configuration: { anonymous: true, summarize: false },
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const config = await session.getConfig();
|
||||
|
||||
expect(config).toEqual({ anonymous: true, summarize: false });
|
||||
expect(session.configuration).toEqual({ anonymous: true, summarize: false });
|
||||
});
|
||||
|
||||
it('should cache empty object when configuration is null', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
configuration: null,
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const config = await session.getConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
expect(session.configuration).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached configuration after setConfig call', async () => {
|
||||
mockClient.workspaces.sessions.update.mockResolvedValue({});
|
||||
|
||||
const newConfig = { anonymous: false, summarize: true };
|
||||
await session.setConfig(newConfig);
|
||||
|
||||
expect(session.configuration).toEqual(newConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session List with Cached Data', () => {
|
||||
it('should populate metadata and config when listing sessions', async () => {
|
||||
const mockSessionsData = {
|
||||
items: [
|
||||
{
|
||||
id: 'session1',
|
||||
metadata: { title: 'Session 1' },
|
||||
configuration: { anonymous: true },
|
||||
},
|
||||
{
|
||||
id: 'session2',
|
||||
metadata: { title: 'Session 2' },
|
||||
configuration: { anonymous: false },
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData);
|
||||
|
||||
const sessionsPage = await honcho.getSessions();
|
||||
const sessions = sessionsPage.items;
|
||||
|
||||
expect(sessions[0].metadata).toEqual({ title: 'Session 1' });
|
||||
expect(sessions[0].configuration).toEqual({ anonymous: true });
|
||||
expect(sessions[1].metadata).toEqual({ title: 'Session 2' });
|
||||
expect(sessions[1].configuration).toEqual({ anonymous: false });
|
||||
});
|
||||
|
||||
it('should handle null metadata and config in session list', async () => {
|
||||
const mockSessionsData = {
|
||||
items: [
|
||||
{
|
||||
id: 'session1',
|
||||
metadata: null,
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData);
|
||||
|
||||
const sessionsPage = await honcho.getSessions();
|
||||
const sessions = sessionsPage.items;
|
||||
|
||||
expect(sessions[0].metadata).toBeUndefined();
|
||||
expect(sessions[0].configuration).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration: Combined Metadata and Configuration Operations', () => {
|
||||
it('should cache both metadata and config for peers independently', async () => {
|
||||
const peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
|
||||
// Set up mocks
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: { name: 'Test' },
|
||||
configuration: { observe_me: true },
|
||||
});
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
// Get both metadata and config
|
||||
await peer.getMetadata();
|
||||
await peer.getConfig();
|
||||
|
||||
expect(peer.metadata).toEqual({ name: 'Test' });
|
||||
expect(peer.configuration).toEqual({ observe_me: true });
|
||||
|
||||
// Update metadata only
|
||||
await peer.setMetadata({ name: 'Updated' });
|
||||
|
||||
expect(peer.metadata).toEqual({ name: 'Updated' });
|
||||
expect(peer.configuration).toEqual({ observe_me: true }); // Should remain unchanged
|
||||
|
||||
// Update config only
|
||||
await peer.setConfig({ observe_me: false });
|
||||
|
||||
expect(peer.metadata).toEqual({ name: 'Updated' }); // Should remain unchanged
|
||||
expect(peer.configuration).toEqual({ observe_me: false });
|
||||
});
|
||||
|
||||
it('should cache both metadata and config for sessions independently', async () => {
|
||||
const session = new Session('test-session', 'test-workspace', mockClient);
|
||||
|
||||
// Set up mocks
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue({
|
||||
id: 'test-session',
|
||||
metadata: { title: 'Test' },
|
||||
configuration: { anonymous: true },
|
||||
});
|
||||
mockClient.workspaces.sessions.update.mockResolvedValue({});
|
||||
|
||||
// Get both metadata and config
|
||||
await session.getMetadata();
|
||||
await session.getConfig();
|
||||
|
||||
expect(session.metadata).toEqual({ title: 'Test' });
|
||||
expect(session.configuration).toEqual({ anonymous: true });
|
||||
|
||||
// Update metadata only
|
||||
await session.setMetadata({ title: 'Updated' });
|
||||
|
||||
expect(session.metadata).toEqual({ title: 'Updated' });
|
||||
expect(session.configuration).toEqual({ anonymous: true }); // Should remain unchanged
|
||||
|
||||
// Update config only
|
||||
await session.setConfig({ anonymous: false });
|
||||
|
||||
expect(session.metadata).toEqual({ title: 'Updated' }); // Should remain unchanged
|
||||
expect(session.configuration).toEqual({ anonymous: false });
|
||||
});
|
||||
|
||||
it('should reduce API calls by using cached values', async () => {
|
||||
const peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
|
||||
// Initial fetch
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: { name: 'Test' },
|
||||
});
|
||||
|
||||
await peer.getMetadata();
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Access cached value directly (without API call)
|
||||
const cachedMetadata = peer.metadata;
|
||||
expect(cachedMetadata).toEqual({ name: 'Test' });
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledTimes(1); // Still only 1 call
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import { Peer } from '../src/peer';
|
|||
import { Session } from '../src/session';
|
||||
import { Page } from '../src/pagination';
|
||||
import { Honcho } from '../src/client';
|
||||
import { Representation } from '../src/representation';
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
|
|
@ -208,6 +209,8 @@ describe('Peer', () => {
|
|||
peer_id: 'test-peer',
|
||||
content: 'Test content',
|
||||
metadata: undefined,
|
||||
configuration: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -219,6 +222,39 @@ describe('Peer', () => {
|
|||
peer_id: 'test-peer',
|
||||
content: 'Hello there',
|
||||
metadata: { importance: 'high', category: 'greeting' },
|
||||
configuration: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create message object with configuration', () => {
|
||||
const configuration = { deriver: { enabled: false } };
|
||||
const message = peer.message('Test content', { configuration });
|
||||
|
||||
expect(message).toEqual({
|
||||
peer_id: 'test-peer',
|
||||
content: 'Test content',
|
||||
metadata: undefined,
|
||||
configuration: { deriver: { enabled: false } },
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create message object with metadata, configuration, and timestamp', () => {
|
||||
const metadata = { importance: 'high' };
|
||||
const configuration = { deriver: { enabled: false }, peer_card: { create: false } };
|
||||
const message = peer.message('Full options test', {
|
||||
metadata,
|
||||
configuration,
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
});
|
||||
|
||||
expect(message).toEqual({
|
||||
peer_id: 'test-peer',
|
||||
content: 'Full options test',
|
||||
metadata: { importance: 'high' },
|
||||
configuration: { deriver: { enabled: false }, peer_card: { create: false } },
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -229,6 +265,8 @@ describe('Peer', () => {
|
|||
peer_id: 'test-peer',
|
||||
content: '',
|
||||
metadata: undefined,
|
||||
configuration: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -514,4 +552,423 @@ describe('Peer', () => {
|
|||
await expect(peer.card()).rejects.toThrow('Card fetch failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('workingRep', () => {
|
||||
beforeEach(() => {
|
||||
mockClient.workspaces.peers.workingRepresentation = jest.fn();
|
||||
});
|
||||
|
||||
it('should get working representation with no parameters', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Observation 1',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
{
|
||||
content: 'Observation 2',
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
message_ids: [[3, 4]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [
|
||||
{
|
||||
conclusion: 'Conclusion 1',
|
||||
premises: ['Observation 1', 'Observation 2'],
|
||||
created_at: '2024-01-01T00:02:00Z',
|
||||
message_ids: [[5, 6]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep();
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(2);
|
||||
expect(result.explicit[0].content).toBe('Observation 1');
|
||||
expect(result.explicit[1].content).toBe('Observation 2');
|
||||
expect(result.deductive).toHaveLength(1);
|
||||
expect(result.deductive[0].conclusion).toBe('Conclusion 1');
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with session as string', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Session-scoped observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep('session-123');
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Session-scoped observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-123',
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with session as Session object', async () => {
|
||||
const session = new Session('session-123', 'test-workspace', mockClient);
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Session object observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(session);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Session object observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-123',
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with target as string', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: "Observer's view of target",
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(undefined, 'target-peer');
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe("Observer's view of target");
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: 'target-peer',
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with target as Peer object', async () => {
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: "Observer's view of target peer object",
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(undefined, targetPeer);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe("Observer's view of target peer object");
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: 'target-peer',
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with search query', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Query-curated observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(
|
||||
undefined,
|
||||
undefined,
|
||||
{ searchQuery: 'programming' }
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Query-curated observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: 'programming',
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with custom size', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Limited observations',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(undefined, undefined, { maxObservations: 10 });
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Limited observations');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with all parameters', async () => {
|
||||
const session = new Session('session-123', 'test-workspace', mockClient);
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Fully parameterized observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
deductive: [
|
||||
{
|
||||
conclusion: 'Conclusion with all params',
|
||||
premises: ['Fully parameterized observation'],
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
message_ids: [[3, 4]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(
|
||||
session,
|
||||
targetPeer,
|
||||
{ searchQuery: 'Python programming', maxObservations: 25 }
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Fully parameterized observation');
|
||||
expect(result.deductive).toHaveLength(1);
|
||||
expect(result.deductive[0].conclusion).toBe('Conclusion with all params');
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-123',
|
||||
target: 'target-peer',
|
||||
search_query: 'Python programming',
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 25,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with string session and string target', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'String params observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-456',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(
|
||||
'session-456',
|
||||
'target-peer-123',
|
||||
{ searchQuery: 'machine learning', maxObservations: 50 }
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('String params observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-456',
|
||||
target: 'target-peer-123',
|
||||
search_query: 'machine learning',
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle boundary size values', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Boundary test',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
// Test size = 1
|
||||
const result1 = await peer.workingRep(undefined, undefined, { maxObservations: 1 });
|
||||
expect(result1).toBeInstanceOf(Representation);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenLastCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 1,
|
||||
});
|
||||
|
||||
// Test size = 100
|
||||
const result2 = await peer.workingRep(undefined, undefined, { maxObservations: 100 });
|
||||
expect(result2).toBeInstanceOf(Representation);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenLastCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.workingRepresentation.mockRejectedValue(
|
||||
new Error('Working representation fetch failed')
|
||||
);
|
||||
|
||||
await expect(peer.workingRep()).rejects.toThrow(
|
||||
'Working representation fetch failed'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Peer } from '../src/peer'
|
|||
import { Page } from '../src/pagination'
|
||||
import { SessionContext } from '../src/session_context'
|
||||
import { Honcho } from '../src/client'
|
||||
import { Representation } from '../src/representation'
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
|
|
@ -568,6 +569,62 @@ describe('Session', () => {
|
|||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should return list of messages when created', async () => {
|
||||
const mockMessages = [
|
||||
{
|
||||
id: 'msg1',
|
||||
peer_id: 'peer1',
|
||||
content: 'Hello',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
id: 'msg2',
|
||||
peer_id: 'peer2',
|
||||
content: 'Hi there',
|
||||
created_at: '2023-01-01T00:00:01Z',
|
||||
metadata: {},
|
||||
},
|
||||
]
|
||||
mockClient.workspaces.sessions.messages.create.mockResolvedValue(
|
||||
mockMessages
|
||||
)
|
||||
|
||||
const messages = [
|
||||
{ peer_id: 'peer1', content: 'Hello' },
|
||||
{ peer_id: 'peer2', content: 'Hi there' },
|
||||
]
|
||||
const result = await session.addMessages(messages)
|
||||
|
||||
expect(result).toEqual(mockMessages)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe('msg1')
|
||||
expect(result[0].content).toBe('Hello')
|
||||
expect(result[1].id).toBe('msg2')
|
||||
expect(result[1].content).toBe('Hi there')
|
||||
})
|
||||
|
||||
it('should return single message when adding single message', async () => {
|
||||
const mockMessage = {
|
||||
id: 'msg1',
|
||||
peer_id: 'peer1',
|
||||
content: 'Hello',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
metadata: {},
|
||||
}
|
||||
mockClient.workspaces.sessions.messages.create.mockResolvedValue([
|
||||
mockMessage,
|
||||
])
|
||||
|
||||
const message = { peer_id: 'peer1', content: 'Hello' }
|
||||
const result = await session.addMessages(message)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual(mockMessage)
|
||||
expect(result[0].id).toBe('msg1')
|
||||
expect(result[0].content).toBe('Hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMessages', () => {
|
||||
|
|
@ -847,18 +904,27 @@ describe('Session', () => {
|
|||
|
||||
describe('workingRep', () => {
|
||||
it('should get working representation with peer string', async () => {
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
knowledge: 'Some knowledge about the peer',
|
||||
relationships: ['peer2', 'peer3'],
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Some knowledge about the peer',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep('peer1')
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('Some knowledge about the peer')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -869,17 +935,27 @@ describe('Session', () => {
|
|||
|
||||
it('should get working representation with Peer object', async () => {
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient)
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
knowledge: 'Some knowledge',
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Some knowledge',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep(peer)
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('Some knowledge')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -889,17 +965,27 @@ describe('Session', () => {
|
|||
})
|
||||
|
||||
it('should get working representation with target peer string', async () => {
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
target_knowledge: 'What peer1 knows about target',
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'What peer1 knows about target',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep('peer1', 'target-peer')
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('What peer1 knows about target')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -911,17 +997,27 @@ describe('Session', () => {
|
|||
it('should get working representation with target Peer object', async () => {
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient)
|
||||
const target = new Peer('target-peer', 'test-workspace', mockClient)
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
target_knowledge: 'What peer1 knows about target',
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'What peer1 knows about target',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep(peer, target)
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('What peer1 knows about target')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.5.1",
|
||||
"@honcho-ai/core": "^1.6.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
|
|
@ -20,11 +20,11 @@
|
|||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
|
|
@ -38,13 +38,13 @@
|
|||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
"@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="],
|
||||
|
||||
|
|
@ -82,33 +82,31 @@
|
|||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.5.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-lbYtMTcL2AxdcIl5ZKenogeTlVMnE7buJWvAFOCLp0yQxcezyA/R9FPvLz2UGRApLsiplLNWhftML+3QBjIIJA=="],
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.6.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-NqUWIs9FLt8dG6LF7rEx/uSY7HPdS67AGTceZPm48l60daddU3+e64glQHkGlmsNQ6TJc/z/BGrVVtQxyNilyw=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],
|
||||
|
||||
|
|
@ -158,40 +156,6 @@
|
|||
|
||||
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
|
||||
|
||||
"@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
|
||||
|
||||
"@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
|
||||
|
||||
"@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
|
||||
|
||||
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
|
@ -210,22 +174,18 @@
|
|||
|
||||
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw=="],
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
|
||||
|
||||
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
|
||||
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
|
||||
|
||||
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
|
@ -236,8 +196,6 @@
|
|||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
|
@ -254,13 +212,13 @@
|
|||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.26.2", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
|
||||
"browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="],
|
||||
|
||||
"bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="],
|
||||
|
||||
|
|
@ -274,7 +232,7 @@
|
|||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001746", "", {}, "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
|
|
@ -288,7 +246,7 @@
|
|||
|
||||
"co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="],
|
||||
|
||||
"collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="],
|
||||
"collect-v8-coverage": ["collect-v8-coverage@1.0.3", "", {}, "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
|
|
@ -302,8 +260,6 @@
|
|||
|
||||
"create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="],
|
||||
|
||||
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
|
@ -316,13 +272,11 @@
|
|||
|
||||
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
|
||||
|
||||
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
|
||||
|
||||
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.228", "", {}, "sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.263", "", {}, "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg=="],
|
||||
|
||||
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
|
||||
|
||||
|
|
@ -360,7 +314,7 @@
|
|||
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
|
|
@ -492,7 +446,7 @@
|
|||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
|
|
@ -546,7 +500,7 @@
|
|||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
|
|
@ -590,7 +544,7 @@
|
|||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="],
|
||||
|
||||
|
|
@ -598,7 +552,7 @@
|
|||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
|
|
@ -642,9 +596,7 @@
|
|||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-jest": ["ts-jest@29.4.4", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw=="],
|
||||
|
||||
"ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
|
||||
"ts-jest": ["ts-jest@29.4.6", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA=="],
|
||||
|
||||
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
|
||||
|
||||
|
|
@ -654,11 +606,9 @@
|
|||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
|
||||
|
||||
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
|
||||
|
||||
|
|
@ -688,8 +638,6 @@
|
|||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.0.0", "", {}, "sha512-9diLdTPc/L7w/5jI4C3gHYNiGHDV9IZYxo1e5LSD8cabi65WVTWWb+g2BGPEpUUCOxR4D+6O5B0AzyMdUAXwrw=="],
|
||||
|
|
@ -698,9 +646,7 @@
|
|||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.129", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A=="],
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -20,7 +20,7 @@
|
|||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.5.1",
|
||||
"@honcho-ai/core": "^1.6.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import {
|
|||
SessionIdSchema,
|
||||
type SessionMetadata,
|
||||
SessionMetadataSchema,
|
||||
type WorkspaceConfig,
|
||||
WorkspaceConfigSchema,
|
||||
type WorkspaceMetadata,
|
||||
WorkspaceMetadataSchema,
|
||||
} from './validation'
|
||||
|
|
@ -62,6 +64,36 @@ export class Honcho {
|
|||
* Reference to the core Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
/**
|
||||
* Private cached metadata for this workspace.
|
||||
*/
|
||||
private _metadata?: Record<string, unknown>
|
||||
/**
|
||||
* Private cached configuration for this workspace.
|
||||
*/
|
||||
private _configuration?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Cached metadata for this workspace. May be stale if the workspace
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getMetadata() to get the latest metadata from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get metadata(): Record<string, unknown> | undefined {
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached configuration for this workspace. May be stale if the workspace
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getConfig() to get the latest configuration from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get configuration(): Record<string, unknown> | undefined {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the underlying @honcho-ai/core client. The @honcho-ai/core client is the raw Stainless-generated client,
|
||||
|
|
@ -155,17 +187,26 @@ export class Honcho {
|
|||
const validatedConfig = options?.config
|
||||
? PeerConfigSchema.parse(options.config)
|
||||
: undefined
|
||||
const peer = new Peer(validatedId, this.workspaceId, this._client)
|
||||
|
||||
if (validatedConfig || validatedMetadata) {
|
||||
await this._client.workspaces.peers.getOrCreate(this.workspaceId, {
|
||||
id: peer.id,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
const peerData = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{
|
||||
id: validatedId,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
}
|
||||
)
|
||||
return new Peer(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
peerData.metadata ?? undefined,
|
||||
peerData.configuration ?? undefined
|
||||
)
|
||||
}
|
||||
|
||||
return peer
|
||||
return new Peer(validatedId, this.workspaceId, this._client)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -185,7 +226,14 @@ export class Honcho {
|
|||
)
|
||||
return new Page(
|
||||
peersPage,
|
||||
(peer) => new Peer(peer.id, this.workspaceId, this._client)
|
||||
(peer) =>
|
||||
new Peer(
|
||||
peer.id,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
peer.metadata ?? undefined,
|
||||
peer.configuration ?? undefined
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -224,17 +272,26 @@ export class Honcho {
|
|||
const validatedConfig = options?.config
|
||||
? SessionConfigSchema.parse(options.config)
|
||||
: undefined
|
||||
const session = new Session(validatedId, this.workspaceId, this._client)
|
||||
|
||||
if (validatedConfig || validatedMetadata) {
|
||||
await this._client.workspaces.sessions.getOrCreate(this.workspaceId, {
|
||||
id: session.id,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
const sessionData = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{
|
||||
id: validatedId,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
}
|
||||
)
|
||||
return new Session(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
sessionData.metadata ?? undefined,
|
||||
sessionData.configuration ?? undefined
|
||||
)
|
||||
}
|
||||
|
||||
return session
|
||||
return new Session(validatedId, this.workspaceId, this._client)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -255,7 +312,14 @@ export class Honcho {
|
|||
)
|
||||
return new Page(
|
||||
sessionsPage,
|
||||
(session) => new Session(session.id, this.workspaceId, this._client)
|
||||
(session) =>
|
||||
new Session(
|
||||
session.id,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
session.metadata ?? undefined,
|
||||
session.configuration ?? undefined
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +328,8 @@ export class Honcho {
|
|||
*
|
||||
* Makes an API call to retrieve metadata associated with the current workspace.
|
||||
* Workspace metadata can include settings, configuration, or any other
|
||||
* key-value data associated with the workspace.
|
||||
* key-value data associated with the workspace. This method also updates the
|
||||
* cached metadata property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the workspace's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
|
|
@ -273,7 +338,8 @@ export class Honcho {
|
|||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
id: this.workspaceId,
|
||||
})
|
||||
return workspace.metadata || {}
|
||||
this._metadata = workspace.metadata || {}
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,6 +347,7 @@ export class Honcho {
|
|||
*
|
||||
* Makes an API call to update the metadata associated with the current workspace.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
* This method also updates the cached metadata property.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with the workspace.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
|
|
@ -290,6 +357,57 @@ export class Honcho {
|
|||
await this._client.workspaces.update(this.workspaceId, {
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
this._metadata = validatedMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for the current workspace.
|
||||
*
|
||||
* Makes an API call to retrieve configuration associated with the current workspace.
|
||||
* Configuration includes settings that control workspace behavior.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the workspace's configuration.
|
||||
* Returns an empty dictionary if no configuration is set
|
||||
*/
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
id: this.workspaceId,
|
||||
})
|
||||
this._configuration = workspace.configuration || {}
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration for the current workspace.
|
||||
*
|
||||
* Makes an API call to update the configuration associated with the current workspace.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @param configuration - A dictionary of configuration to associate with the workspace.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setConfig(configuration: WorkspaceConfig): Promise<void> {
|
||||
const validatedConfig = WorkspaceConfigSchema.parse(configuration)
|
||||
await this._client.workspaces.update(this.workspaceId, {
|
||||
configuration: validatedConfig,
|
||||
})
|
||||
this._configuration = validatedConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached metadata and configuration for the current workspace.
|
||||
*
|
||||
* Makes a single API call to retrieve the latest metadata and configuration
|
||||
* associated with the current workspace and updates the cached properties.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
id: this.workspaceId,
|
||||
})
|
||||
this._metadata = workspace.metadata || {}
|
||||
this._configuration = workspace.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@
|
|||
|
||||
export type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
export { Honcho } from './client'
|
||||
export { Observation, ObservationScope } from './observations'
|
||||
export { Page } from './pagination'
|
||||
export { Peer } from './peer'
|
||||
export { Peer, PeerContext } from './peer'
|
||||
export { Session, SessionPeerConfig } from './session'
|
||||
export {
|
||||
SessionContext,
|
||||
SessionSummaries,
|
||||
Summary,
|
||||
SummaryData,
|
||||
type SummaryData,
|
||||
} from './session_context'
|
||||
export {
|
||||
type DialecticStreamChunk,
|
||||
type DialecticStreamDelta,
|
||||
DialecticStreamResponse,
|
||||
type Observation as ObservationData,
|
||||
type ObservationQueryParams,
|
||||
} from './types'
|
||||
|
||||
// Export validation types for advanced usage
|
||||
|
|
@ -35,5 +38,6 @@ export type {
|
|||
SessionConfig,
|
||||
SessionMetadata,
|
||||
WorkingRepParams,
|
||||
WorkspaceConfig,
|
||||
WorkspaceMetadata,
|
||||
} from './validation'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
import type HonchoCore from '@honcho-ai/core'
|
||||
import {
|
||||
Representation,
|
||||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
|
||||
// Re-export for consumers who import from this module
|
||||
export type { RepresentationOptions }
|
||||
|
||||
/**
|
||||
* An observation from the theory-of-mind system.
|
||||
*
|
||||
* Observations are facts derived from messages that help build a representation
|
||||
* of a peer.
|
||||
*/
|
||||
export class Observation {
|
||||
/**
|
||||
* Unique identifier for this observation.
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
* The observation content/text.
|
||||
*/
|
||||
readonly content: string
|
||||
|
||||
/**
|
||||
* The peer who made the observation.
|
||||
*/
|
||||
readonly observerId: string
|
||||
|
||||
/**
|
||||
* The peer being observed.
|
||||
*/
|
||||
readonly observedId: string
|
||||
|
||||
/**
|
||||
* The session where this observation was made.
|
||||
*/
|
||||
readonly sessionId: string
|
||||
|
||||
/**
|
||||
* When the observation was created.
|
||||
*/
|
||||
readonly createdAt: string
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
content: string,
|
||||
observerId: string,
|
||||
observedId: string,
|
||||
sessionId: string,
|
||||
createdAt: string
|
||||
) {
|
||||
this.id = id
|
||||
this.content = content
|
||||
this.observerId = observerId
|
||||
this.observedId = observedId
|
||||
this.sessionId = sessionId
|
||||
this.createdAt = createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Observation from an API response object.
|
||||
*
|
||||
* @param data - API response data
|
||||
* @returns A new Observation instance
|
||||
*/
|
||||
static fromApiResponse(data: Record<string, unknown>): Observation {
|
||||
return new Observation(
|
||||
(data.id as string) ?? '',
|
||||
(data.content as string) ?? '',
|
||||
(data.observer_id as string) ?? '',
|
||||
(data.observed_id as string) ?? '',
|
||||
(data.session_id as string) ?? '',
|
||||
(data.created_at as string) ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Observation.
|
||||
*/
|
||||
toString(): string {
|
||||
const truncatedContent =
|
||||
this.content.length > 50
|
||||
? `${this.content.slice(0, 50)}...`
|
||||
: this.content
|
||||
return `Observation(id='${this.id}', content='${truncatedContent}')`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scoped access to observations for a specific observer/observed relationship.
|
||||
*
|
||||
* This class provides convenient methods to list, query, and delete observations
|
||||
* that are automatically scoped to a specific observer/observed pair.
|
||||
*
|
||||
* Typically accessed via `peer.observations` (for self-observations) or
|
||||
* `peer.observationsOf(target)` (for observations about another peer).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get self-observations
|
||||
* const observations = peer.observations
|
||||
* const obsList = await observations.list()
|
||||
* const searchResults = await observations.query('preferences')
|
||||
*
|
||||
* // Get observations about another peer
|
||||
* const bobObservations = peer.observationsOf('bob')
|
||||
* const bobList = await bobObservations.list()
|
||||
* ```
|
||||
*
|
||||
* @note
|
||||
* This class requires the core Honcho SDK to support observation endpoints.
|
||||
* The observation endpoints are:
|
||||
* - POST /workspaces/{workspace_id}/observations/list
|
||||
* - POST /workspaces/{workspace_id}/observations/query
|
||||
* - DELETE /workspaces/{workspace_id}/observations/{observation_id}
|
||||
*/
|
||||
export class ObservationScope {
|
||||
private _client: HonchoCore
|
||||
|
||||
/**
|
||||
* The workspace ID.
|
||||
*/
|
||||
readonly workspaceId: string
|
||||
|
||||
/**
|
||||
* The observer peer ID.
|
||||
*/
|
||||
readonly observer: string
|
||||
|
||||
/**
|
||||
* The observed peer ID.
|
||||
*/
|
||||
readonly observed: string
|
||||
|
||||
/**
|
||||
* Initialize an ObservationScope.
|
||||
*
|
||||
* @param client - The Honcho client instance
|
||||
* @param workspaceId - The workspace ID
|
||||
* @param observer - The observer peer ID
|
||||
* @param observed - The observed peer ID
|
||||
*/
|
||||
constructor(
|
||||
client: HonchoCore,
|
||||
workspaceId: string,
|
||||
observer: string,
|
||||
observed: string
|
||||
) {
|
||||
this._client = client
|
||||
this.workspaceId = workspaceId
|
||||
this.observer = observer
|
||||
this.observed = observed
|
||||
}
|
||||
|
||||
/**
|
||||
* List observations in this scope.
|
||||
*
|
||||
* @param page - Page number (1-indexed)
|
||||
* @param size - Number of results per page
|
||||
* @param sessionId - Optional session ID to filter by
|
||||
* @returns Promise resolving to list of Observation objects
|
||||
*/
|
||||
async list(
|
||||
page: number = 1,
|
||||
size: number = 50,
|
||||
sessionId?: string
|
||||
): Promise<Observation[]> {
|
||||
const filters: Record<string, unknown> = {
|
||||
observer: this.observer,
|
||||
observed: this.observed,
|
||||
}
|
||||
if (sessionId) {
|
||||
filters.session_id = sessionId
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
const response = await (this._client.workspaces as any).observations.list(
|
||||
this.workspaceId,
|
||||
{
|
||||
filters,
|
||||
page,
|
||||
size,
|
||||
}
|
||||
)
|
||||
|
||||
return (response.items ?? []).map((item: unknown) =>
|
||||
Observation.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic search for observations in this scope.
|
||||
*
|
||||
* @param query - The search query string
|
||||
* @param topK - Maximum number of results to return
|
||||
* @param distance - Maximum cosine distance threshold (0.0-1.0)
|
||||
* @returns Promise resolving to list of matching Observation objects
|
||||
*/
|
||||
async query(
|
||||
query: string,
|
||||
topK: number = 10,
|
||||
distance?: number
|
||||
): Promise<Observation[]> {
|
||||
const filters: Record<string, unknown> = {
|
||||
observer: this.observer,
|
||||
observed: this.observed,
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
const response = await (this._client.workspaces as any).observations.query(
|
||||
this.workspaceId,
|
||||
{
|
||||
query,
|
||||
top_k: topK,
|
||||
distance,
|
||||
filters,
|
||||
}
|
||||
)
|
||||
|
||||
return (response ?? []).map((item: unknown) =>
|
||||
Observation.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an observation by ID.
|
||||
*
|
||||
* @param observationId - The ID of the observation to delete
|
||||
*/
|
||||
async delete(observationId: string): Promise<void> {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
await (this._client.workspaces as any).observations.delete(
|
||||
this.workspaceId,
|
||||
observationId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computed representation for this scope.
|
||||
*
|
||||
* This returns the working representation (narrative) built from the
|
||||
* observations in this scope.
|
||||
*
|
||||
* @param options - Optional options to configure the representation
|
||||
* @returns Promise resolving to a Representation object
|
||||
*/
|
||||
async getRepresentation(
|
||||
options?: RepresentationOptions
|
||||
): Promise<Representation> {
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
this.observer,
|
||||
{
|
||||
target: this.observed,
|
||||
search_query: options?.searchQuery,
|
||||
search_top_k: options?.searchTopK,
|
||||
search_max_distance: options?.searchMaxDistance,
|
||||
include_most_derived: options?.includeMostDerived,
|
||||
max_observations: options?.maxObservations,
|
||||
}
|
||||
)
|
||||
|
||||
const maybe = response as
|
||||
| RepresentationData
|
||||
| { representation?: RepresentationData | null }
|
||||
| null
|
||||
const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe
|
||||
? (maybe as { representation?: RepresentationData | null }).representation
|
||||
: maybe) ?? { explicit: [], deductive: [] }
|
||||
return Representation.fromData(rep as RepresentationData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the ObservationScope.
|
||||
*/
|
||||
toString(): string {
|
||||
return `ObservationScope(workspaceId='${this.workspaceId}', observer='${this.observer}', observed='${this.observed}')`
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import type { Page as CorePage } from '@honcho-ai/core/pagination'
|
|||
* Provides async iteration and transformation capabilities while preserving
|
||||
* pagination functionality from the underlying core Page.
|
||||
*/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Generic type parameter with reasonable default for internal transform
|
||||
export class Page<T, TOriginal = any> implements AsyncIterable<T> {
|
||||
private _originalPage: CorePage<TOriginal>
|
||||
private _transformFunc?: (item: TOriginal) => T
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import type HonchoCore from '@honcho-ai/core'
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
import { ObservationScope } from './observations'
|
||||
import { Page } from './pagination'
|
||||
import {
|
||||
Representation,
|
||||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
import { Session } from './session'
|
||||
import { type DialecticStreamChunk, DialecticStreamResponse } from './types'
|
||||
import {
|
||||
|
|
@ -10,6 +16,7 @@ import {
|
|||
LimitSchema,
|
||||
MessageContentSchema,
|
||||
MessageMetadataSchema,
|
||||
PeerWorkingRepParamsSchema,
|
||||
SearchQuerySchema,
|
||||
type MessageCreate as ValidatedMessageCreate,
|
||||
} from './validation'
|
||||
|
|
@ -34,6 +41,36 @@ export class Peer {
|
|||
* Reference to the parent Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
/**
|
||||
* Private cached metadata for this peer.
|
||||
*/
|
||||
private _metadata?: Record<string, unknown>
|
||||
/**
|
||||
* Private cached configuration for this peer.
|
||||
*/
|
||||
private _configuration?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Cached metadata for this peer. May be stale if the peer
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getMetadata() to get the latest metadata from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get metadata(): Record<string, unknown> | undefined {
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached configuration for this peer. May be stale if the peer
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getConfig() to get the latest configuration from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get configuration(): Record<string, unknown> | undefined {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Peer. **Do not call this directly, use the client.peer() method instead.**
|
||||
|
|
@ -41,11 +78,21 @@ export class Peer {
|
|||
* @param id - Unique identifier for this peer within the workspace
|
||||
* @param workspaceId - Workspace ID for scoping operations
|
||||
* @param client - Reference to the parent Honcho client instance
|
||||
* @param metadata - Optional metadata to initialize the cached value
|
||||
* @param configuration - Optional configuration to initialize the cached value
|
||||
*/
|
||||
constructor(id: string, workspaceId: string, client: HonchoCore) {
|
||||
constructor(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
client: HonchoCore,
|
||||
metadata?: Record<string, unknown>,
|
||||
configuration?: Record<string, unknown>
|
||||
) {
|
||||
this.id = id
|
||||
this.workspaceId = workspaceId
|
||||
this._client = client
|
||||
this._metadata = metadata
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -211,14 +258,16 @@ export class Peer {
|
|||
* The created message object can then be added to sessions or used in other operations.
|
||||
*
|
||||
* @param content - The text content for the message
|
||||
* @param metadata - Optional metadata to associate with the message
|
||||
* @param created_at - Optional ISO 8601 timestamp for the message
|
||||
* @param options.metadata - Optional metadata to associate with the message
|
||||
* @param options.configuration - Optional message-level configuration (e.g., deriver settings)
|
||||
* @param options.created_at - Optional ISO 8601 timestamp for the message
|
||||
* @returns A new message object with this peer's ID and the provided content
|
||||
*/
|
||||
message(
|
||||
content: string,
|
||||
options?: {
|
||||
metadata?: Record<string, unknown>
|
||||
configuration?: Record<string, unknown>
|
||||
created_at?: string | Date
|
||||
}
|
||||
): ValidatedMessageCreate {
|
||||
|
|
@ -236,6 +285,7 @@ export class Peer {
|
|||
peer_id: this.id,
|
||||
content: validatedContent,
|
||||
metadata: validatedMetadata,
|
||||
configuration: options?.configuration,
|
||||
created_at: createdAt,
|
||||
}
|
||||
}
|
||||
|
|
@ -245,7 +295,7 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to retrieve metadata associated with this peer. Metadata
|
||||
* can include custom attributes, settings, or any other key-value data
|
||||
* associated with the peer.
|
||||
* associated with the peer. This method also updates the cached metadata property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the peer's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
|
|
@ -255,7 +305,8 @@ export class Peer {
|
|||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return peer.metadata || {}
|
||||
this._metadata = peer.metadata || {}
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -263,6 +314,7 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to update the metadata associated with this peer.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
* This method also updates the cached metadata property.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with this peer.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
|
|
@ -271,6 +323,7 @@ export class Peer {
|
|||
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
|
||||
metadata,
|
||||
})
|
||||
this._metadata = metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -278,15 +331,17 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to retrieve configuration associated with this peer.
|
||||
* Configuration currently includes one optional flag, `observe_me`.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the peer's configuration
|
||||
*/
|
||||
async getPeerConfig(): Promise<Record<string, unknown>> {
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
const peer = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return peer.configuration || {}
|
||||
this._configuration = peer.configuration || {}
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -296,14 +351,51 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to update the configuration associated with this peer.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @param config - A dictionary of configuration to associate with this peer.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setPeerConfig(config: Record<string, unknown>): Promise<void> {
|
||||
async setConfig(config: Record<string, unknown>): Promise<void> {
|
||||
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
|
||||
configuration: config,
|
||||
})
|
||||
this._configuration = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current workspace-level configuration for this peer.
|
||||
*
|
||||
* @deprecated Use getConfig() instead
|
||||
* @returns Promise resolving to a dictionary containing the peer's configuration
|
||||
*/
|
||||
async getPeerConfig(): Promise<Record<string, unknown>> {
|
||||
return this.getConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the configuration for this peer.
|
||||
*
|
||||
* @deprecated Use setConfig() instead
|
||||
* @param config - A dictionary of configuration to associate with this peer
|
||||
*/
|
||||
async setPeerConfig(config: Record<string, unknown>): Promise<void> {
|
||||
return this.setConfig(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached metadata and configuration for this peer.
|
||||
*
|
||||
* Makes a single API call to retrieve the latest metadata and configuration
|
||||
* associated with this peer and updates the cached properties.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const peer = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
this._metadata = peer.metadata || {}
|
||||
this._configuration = peer.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -383,6 +475,197 @@ export class Peer {
|
|||
return items.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a working representation for this peer.
|
||||
*
|
||||
* Makes an API call to retrieve the working representation for this peer.
|
||||
*
|
||||
* @param session - Optional session to scope the representation to.
|
||||
* @param target - Optional target peer to get the representation of. If provided,
|
||||
* returns the representation of the target from the perspective of this peer.
|
||||
* @param options - Optional representation options to filter and configure the results
|
||||
* @returns Promise resolving to a Representation object containing explicit and deductive observations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get global representation
|
||||
* const globalRep = await peer.workingRep()
|
||||
* console.log(globalRep.toString())
|
||||
*
|
||||
* // Get representation scoped to a session
|
||||
* const sessionRep = await peer.workingRep('session-123')
|
||||
*
|
||||
* // Get representation with semantic search
|
||||
* const searchedRep = await peer.workingRep(undefined, undefined, {
|
||||
* searchQuery: 'preferences',
|
||||
* searchTopK: 10,
|
||||
* maxObservations: 50
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async workingRep(
|
||||
session?: string | Session,
|
||||
target?: string | Peer,
|
||||
options?: RepresentationOptions
|
||||
): Promise<Representation> {
|
||||
const workingRepParams = PeerWorkingRepParamsSchema.parse({
|
||||
session,
|
||||
target,
|
||||
options,
|
||||
})
|
||||
const sessionId = workingRepParams.session
|
||||
? typeof workingRepParams.session === 'string'
|
||||
? workingRepParams.session
|
||||
: workingRepParams.session.id
|
||||
: undefined
|
||||
const targetId = workingRepParams.target
|
||||
? typeof workingRepParams.target === 'string'
|
||||
? workingRepParams.target
|
||||
: workingRepParams.target.id
|
||||
: undefined
|
||||
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
session_id: sessionId,
|
||||
target: targetId,
|
||||
search_query: workingRepParams.options?.searchQuery,
|
||||
search_top_k: workingRepParams.options?.searchTopK,
|
||||
search_max_distance: workingRepParams.options?.searchMaxDistance,
|
||||
include_most_derived: workingRepParams.options?.includeMostDerived,
|
||||
max_observations: workingRepParams.options?.maxObservations,
|
||||
}
|
||||
)
|
||||
const maybe = response as
|
||||
| RepresentationData
|
||||
| { representation?: RepresentationData | null }
|
||||
| null
|
||||
const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe
|
||||
? (maybe as { representation?: RepresentationData | null }).representation
|
||||
: maybe) ?? { explicit: [], deductive: [] }
|
||||
return Representation.fromData(rep as RepresentationData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for this peer, including representation and peer card.
|
||||
*
|
||||
* This is a convenience method that retrieves both the working representation
|
||||
* and peer card in a single API call.
|
||||
*
|
||||
* @param target - Optional target peer to get context for. If provided, returns
|
||||
* the context for the target from this peer's perspective.
|
||||
* @param options - Optional representation options to filter and configure the results
|
||||
* @returns Promise resolving to a PeerContext object containing representation and peer card
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get own context
|
||||
* const context = await peer.getContext()
|
||||
* console.log(context.representation?.toString())
|
||||
* console.log(context.peerCard)
|
||||
*
|
||||
* // Get context for another peer
|
||||
* const context = await peer.getContext('other-peer-id')
|
||||
*
|
||||
* // Get context with semantic search
|
||||
* const context = await peer.getContext(undefined, {
|
||||
* searchQuery: 'preferences',
|
||||
* searchTopK: 10
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async getContext(
|
||||
target?: string | Peer,
|
||||
options?: RepresentationOptions
|
||||
): Promise<PeerContext> {
|
||||
const targetId = target
|
||||
? typeof target === 'string'
|
||||
? target
|
||||
: target.id
|
||||
: undefined
|
||||
|
||||
const response = await this._client.workspaces.peers.getContext(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
target: targetId,
|
||||
search_query: options?.searchQuery,
|
||||
search_top_k: options?.searchTopK,
|
||||
search_max_distance: options?.searchMaxDistance,
|
||||
include_most_derived: options?.includeMostDerived,
|
||||
max_observations: options?.maxObservations,
|
||||
}
|
||||
)
|
||||
|
||||
return PeerContext.fromApiResponse(
|
||||
response as unknown as Record<string, unknown>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Access this peer's self-observations (where observer == observed == self).
|
||||
*
|
||||
* This property provides a convenient way to access observations that this peer
|
||||
* has made about themselves. Use this for self-observation scenarios.
|
||||
*
|
||||
* @returns An ObservationScope scoped to this peer's self-observations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // List self-observations
|
||||
* const obsList = await peer.observations.list()
|
||||
*
|
||||
* // Search self-observations
|
||||
* const results = await peer.observations.query('preferences')
|
||||
*
|
||||
* // Delete a self-observation
|
||||
* await peer.observations.delete('obs-123')
|
||||
* ```
|
||||
*/
|
||||
get observations(): ObservationScope {
|
||||
return new ObservationScope(
|
||||
this._client,
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
this.id
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Access observations this peer has made about another peer.
|
||||
*
|
||||
* This method provides scoped access to observations where this peer is the
|
||||
* observer and the target is the observed peer.
|
||||
*
|
||||
* @param target - The target peer (either a Peer object or peer ID string)
|
||||
* @returns An ObservationScope scoped to this peer's observations of the target
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get observations about another peer
|
||||
* const bobObservations = peer.observationsOf('bob')
|
||||
*
|
||||
* // List observations
|
||||
* const obsList = await bobObservations.list()
|
||||
*
|
||||
* // Search observations
|
||||
* const results = await bobObservations.query('work history')
|
||||
*
|
||||
* // Get the representation from these observations
|
||||
* const rep = await bobObservations.getRepresentation()
|
||||
* ```
|
||||
*/
|
||||
observationsOf(target: string | Peer): ObservationScope {
|
||||
const targetId = typeof target === 'string' ? target : target.id
|
||||
return new ObservationScope(
|
||||
this._client,
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
targetId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Peer.
|
||||
*
|
||||
|
|
@ -392,3 +675,76 @@ export class Peer {
|
|||
return `Peer(id='${this.id}')`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for a peer, including representation and peer card.
|
||||
*
|
||||
* This class holds both the working representation and peer card for a peer,
|
||||
* typically returned from the getContext API call.
|
||||
*/
|
||||
export class PeerContext {
|
||||
/**
|
||||
* The ID of the observer peer.
|
||||
*/
|
||||
readonly peerId: string
|
||||
|
||||
/**
|
||||
* The ID of the target peer being observed.
|
||||
*/
|
||||
readonly targetId: string
|
||||
|
||||
/**
|
||||
* The working representation (may be null if no observations exist).
|
||||
*/
|
||||
readonly representation: Representation | null
|
||||
|
||||
/**
|
||||
* List of peer card strings (may be null if no card exists).
|
||||
*/
|
||||
readonly peerCard: string[] | null
|
||||
|
||||
constructor(
|
||||
peerId: string,
|
||||
targetId: string,
|
||||
representation: Representation | null,
|
||||
peerCard: string[] | null
|
||||
) {
|
||||
this.peerId = peerId
|
||||
this.targetId = targetId
|
||||
this.representation = representation
|
||||
this.peerCard = peerCard
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PeerContext from an API response.
|
||||
*
|
||||
* @param response - API response object with peer_id, target_id, representation, and peer_card
|
||||
* @returns A new PeerContext instance
|
||||
*/
|
||||
static fromApiResponse(response: Record<string, unknown>): PeerContext {
|
||||
const peerId = (response.peer_id as string | undefined) ?? ''
|
||||
const targetId = (response.target_id as string | undefined) ?? ''
|
||||
|
||||
let representation: Representation | null = null
|
||||
if (response.representation) {
|
||||
representation = Representation.fromData(
|
||||
response.representation as RepresentationData
|
||||
)
|
||||
}
|
||||
|
||||
const peerCard = (response.peer_card as string[] | undefined) ?? null
|
||||
|
||||
return new PeerContext(peerId, targetId, representation, peerCard)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the PeerContext.
|
||||
*
|
||||
* @returns A string representation suitable for debugging
|
||||
*/
|
||||
toString(): string {
|
||||
const hasRep = this.representation !== null
|
||||
const hasCard = this.peerCard !== null && this.peerCard.length > 0
|
||||
return `PeerContext(peerId='${this.peerId}', targetId='${this.targetId}', hasRepresentation=${hasRep}, hasPeerCard=${hasCard})`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* Options for representation retrieval.
|
||||
*/
|
||||
export interface RepresentationOptions {
|
||||
/**
|
||||
* Semantic search query to filter relevant observations.
|
||||
*/
|
||||
searchQuery?: string
|
||||
|
||||
/**
|
||||
* Number of semantically relevant facts to return.
|
||||
*/
|
||||
searchTopK?: number
|
||||
|
||||
/**
|
||||
* Maximum semantic distance for search results (0.0-1.0).
|
||||
*/
|
||||
searchMaxDistance?: number
|
||||
|
||||
/**
|
||||
* Whether to include the most derived observations.
|
||||
*/
|
||||
includeMostDerived?: boolean
|
||||
|
||||
/**
|
||||
* Maximum number of observations to include.
|
||||
*/
|
||||
maxObservations?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata associated with an observation.
|
||||
*/
|
||||
export interface ObservationMetadata {
|
||||
created_at: string
|
||||
message_ids: Array<[number, number]>
|
||||
session_name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* An explicit observation with full metadata.
|
||||
* Represents facts LITERALLY stated - direct quotes or clear paraphrases only.
|
||||
*/
|
||||
export interface ExplicitObservationBase {
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for deductive observations - logical conclusions.
|
||||
*/
|
||||
export interface DeductiveObservationBase {
|
||||
premises: string[]
|
||||
conclusion: string
|
||||
}
|
||||
|
||||
export interface ExplicitObservation
|
||||
extends ExplicitObservationBase,
|
||||
ObservationMetadata {}
|
||||
|
||||
/**
|
||||
* A deductive observation with full metadata.
|
||||
* Represents conclusions that MUST be true given explicit facts and premises.
|
||||
*/
|
||||
export interface DeductiveObservation
|
||||
extends DeductiveObservationBase,
|
||||
ObservationMetadata {}
|
||||
|
||||
/**
|
||||
* Raw representation data structure returned from the API.
|
||||
*/
|
||||
export interface RepresentationData {
|
||||
explicit: ExplicitObservation[]
|
||||
deductive: DeductiveObservation[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A Representation is a traversable and diffable map of observations.
|
||||
*
|
||||
* At the base, we have a list of explicit observations, derived from a peer's messages.
|
||||
* From there, deductive observations can be made by establishing logical relationships
|
||||
* between explicit observations.
|
||||
*
|
||||
* All of a peer's observations are stored as documents in a collection. These documents
|
||||
* can be queried in various ways to produce this Representation object.
|
||||
*
|
||||
* A "working representation" is a version of this data structure representing the most
|
||||
* recent observations within a single session.
|
||||
*/
|
||||
export class Representation {
|
||||
/**
|
||||
* Facts LITERALLY stated - direct quotes or clear paraphrases only, no interpretation or inference.
|
||||
*/
|
||||
explicit: ExplicitObservation[]
|
||||
|
||||
/**
|
||||
* Conclusions that MUST be true given explicit facts and premises - strict logical necessities.
|
||||
*/
|
||||
deductive: DeductiveObservation[]
|
||||
|
||||
/**
|
||||
* Create a new Representation from observation lists.
|
||||
*
|
||||
* @param explicit - List of explicit observations
|
||||
* @param deductive - List of deductive observations
|
||||
*/
|
||||
constructor(
|
||||
explicit: ExplicitObservation[] = [],
|
||||
deductive: DeductiveObservation[] = []
|
||||
) {
|
||||
this.explicit = explicit
|
||||
this.deductive = deductive
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the representation is empty.
|
||||
*
|
||||
* @returns True if both explicit and deductive observation lists are empty
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.explicit.length === 0 && this.deductive.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Given this and another representation, return a new representation with only
|
||||
* observations that are unique to the other.
|
||||
*
|
||||
* Note: This only removes literal duplicates based on stringified comparison,
|
||||
* not semantically equivalent ones.
|
||||
*
|
||||
* @param other - The representation to compare against
|
||||
* @returns A new Representation containing only observations unique to other
|
||||
*/
|
||||
diff(other: Representation): Representation {
|
||||
const thisExplicitSet = new Set(
|
||||
this.explicit.map((obs) => this._hashExplicit(obs))
|
||||
)
|
||||
const thisDeductiveSet = new Set(
|
||||
this.deductive.map((obs) => this._hashDeductive(obs))
|
||||
)
|
||||
|
||||
const uniqueExplicit = other.explicit.filter(
|
||||
(obs) => !thisExplicitSet.has(this._hashExplicit(obs))
|
||||
)
|
||||
const uniqueDeductive = other.deductive.filter(
|
||||
(obs) => !thisDeductiveSet.has(this._hashDeductive(obs))
|
||||
)
|
||||
|
||||
return new Representation(uniqueExplicit, uniqueDeductive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge another representation into this one.
|
||||
*
|
||||
* This automatically deduplicates explicit and deductive observations.
|
||||
* Preserves order of observations to retain FIFO order.
|
||||
*
|
||||
* Note: Observations with the same timestamp may not have order preserved,
|
||||
* but that's acceptable since they're from the same timestamp.
|
||||
*
|
||||
* @param other - The representation to merge into this one
|
||||
* @param maxObservations - Optional maximum number of observations to keep per type
|
||||
*/
|
||||
merge(other: Representation, maxObservations?: number): void {
|
||||
// Deduplicate by converting to Set using hash, then back to array
|
||||
const explicitMap = new Map<string, ExplicitObservation>()
|
||||
const deductiveMap = new Map<string, DeductiveObservation>()
|
||||
|
||||
// Add existing observations
|
||||
for (const obs of this.explicit) {
|
||||
explicitMap.set(this._hashExplicit(obs), obs)
|
||||
}
|
||||
for (const obs of this.deductive) {
|
||||
deductiveMap.set(this._hashDeductive(obs), obs)
|
||||
}
|
||||
|
||||
// Add new observations (overwrites duplicates)
|
||||
for (const obs of other.explicit) {
|
||||
explicitMap.set(this._hashExplicit(obs), obs)
|
||||
}
|
||||
for (const obs of other.deductive) {
|
||||
deductiveMap.set(this._hashDeductive(obs), obs)
|
||||
}
|
||||
|
||||
// Convert back to arrays and sort by created_at
|
||||
this.explicit = Array.from(explicitMap.values()).sort(
|
||||
(a, b) =>
|
||||
this._parseTimestampForSort(a.created_at) -
|
||||
this._parseTimestampForSort(b.created_at)
|
||||
)
|
||||
this.deductive = Array.from(deductiveMap.values()).sort(
|
||||
(a, b) =>
|
||||
this._parseTimestampForSort(a.created_at) -
|
||||
this._parseTimestampForSort(b.created_at)
|
||||
)
|
||||
|
||||
// Apply max observations limit if specified
|
||||
if (maxObservations !== undefined) {
|
||||
this.explicit = this.explicit.slice(-maxObservations)
|
||||
this.deductive = this.deductive.slice(-maxObservations)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format representation into a clean, readable string for LLM prompts.
|
||||
*
|
||||
* Timestamps are stripped of subsecond precision for cleaner display.
|
||||
*
|
||||
* @returns Formatted string with clear sections and numbered items including timestamps
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* EXPLICIT:
|
||||
* 1. [2025-01-01T12:00:00Z] The user has a dog named Rover
|
||||
* 2. [2025-01-01T12:01:00Z] The user's dog is 5 years old
|
||||
*
|
||||
* DEDUCTIVE:
|
||||
* 1. [2025-01-01T12:01:00Z] Rover is 5 years old
|
||||
* - The user has a dog named Rover
|
||||
* - The user's dog is 5 years old
|
||||
* ```
|
||||
*/
|
||||
toString(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push('EXPLICIT:\n')
|
||||
for (let i = 0; i < this.explicit.length; i++) {
|
||||
const obs = this.explicit[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. [${timestamp}] ${obs.content}`)
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
parts.push('DEDUCTIVE:\n')
|
||||
for (let i = 0; i < this.deductive.length; i++) {
|
||||
const obs = this.deductive[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. [${timestamp}] ${obs.conclusion}`)
|
||||
for (const premise of obs.premises) {
|
||||
parts.push(` - ${premise}`)
|
||||
}
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format representation into a clean, readable string without timestamps.
|
||||
*
|
||||
* @returns Formatted string with clear sections and numbered items without temporal metadata
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* EXPLICIT:
|
||||
* 1. The user has a dog named Rover
|
||||
* 2. The user's dog is 5 years old
|
||||
*
|
||||
* DEDUCTIVE:
|
||||
* 1. Rover is 5 years old
|
||||
* - The user has a dog named Rover
|
||||
* - The user's dog is 5 years old
|
||||
* ```
|
||||
*/
|
||||
toStringNoTimestamps(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push('EXPLICIT:\n')
|
||||
for (let i = 0; i < this.explicit.length; i++) {
|
||||
parts.push(`${i + 1}. ${this.explicit[i].content}`)
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
parts.push('DEDUCTIVE:\n')
|
||||
for (let i = 0; i < this.deductive.length; i++) {
|
||||
const obs = this.deductive[i]
|
||||
parts.push(`${i + 1}. ${obs.conclusion}`)
|
||||
for (const premise of obs.premises) {
|
||||
parts.push(` - ${premise}`)
|
||||
}
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Representation object as markdown.
|
||||
*
|
||||
* Timestamps are stripped of subsecond precision for cleaner display.
|
||||
*
|
||||
* @returns Formatted markdown string with headers and lists
|
||||
*/
|
||||
toMarkdown(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push('## Explicit Observations\n')
|
||||
for (let i = 0; i < this.explicit.length; i++) {
|
||||
const obs = this.explicit[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. [${timestamp}] ${obs.content}`)
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
parts.push('## Deductive Observations\n')
|
||||
for (let i = 0; i < this.deductive.length; i++) {
|
||||
const obs = this.deductive[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. **Conclusion**: ${obs.conclusion}`)
|
||||
parts.push(` **Created**: ${timestamp}`)
|
||||
if (obs.premises.length > 0) {
|
||||
parts.push(' **Premises**:')
|
||||
for (const premise of obs.premises) {
|
||||
parts.push(` - ${premise}`)
|
||||
}
|
||||
}
|
||||
parts.push('')
|
||||
}
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Representation from raw API response data.
|
||||
*
|
||||
* @param data - Raw representation data from the API
|
||||
* @returns A new Representation instance
|
||||
*/
|
||||
static fromData(data: RepresentationData): Representation {
|
||||
return new Representation(data.explicit, data.deductive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash string for an explicit observation for deduplication.
|
||||
* Based on content, created_at, and session_name.
|
||||
*/
|
||||
private _hashExplicit(obs: ExplicitObservation): string {
|
||||
return JSON.stringify({
|
||||
content: obs.content,
|
||||
created_at: obs.created_at,
|
||||
session_name: obs.session_name,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash string for a deductive observation for deduplication.
|
||||
* Based on conclusion, created_at, and session_name (premises not included).
|
||||
*/
|
||||
private _hashDeductive(obs: DeductiveObservation): string {
|
||||
return JSON.stringify({
|
||||
conclusion: obs.conclusion,
|
||||
created_at: obs.created_at,
|
||||
session_name: obs.session_name,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip microseconds from ISO timestamp for cleaner display.
|
||||
*/
|
||||
private _stripMicroseconds(timestamp: string): string {
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
return date.toISOString().replace(/\.\d{3}Z$/, 'Z')
|
||||
} catch {
|
||||
return timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse a timestamp and return milliseconds since epoch for sorting.
|
||||
* Handles microsecond precision by truncating to milliseconds before parsing.
|
||||
*
|
||||
* @param timestamp - ISO 8601 timestamp string (may include microseconds)
|
||||
* @returns Milliseconds since epoch, or 0 if parsing fails
|
||||
*/
|
||||
private _parseTimestampForSort(timestamp: string): number {
|
||||
try {
|
||||
// Normalize fractional seconds to 3 digits (milliseconds)
|
||||
// Match pattern: YYYY-MM-DDTHH:mm:ss.SSSSSS(Z or timezone)
|
||||
const normalized = timestamp.replace(
|
||||
/(\.\d{3})\d+(Z|[+-]\d{2}:\d{2})$/,
|
||||
'$1$2'
|
||||
)
|
||||
const time = new Date(normalized).getTime()
|
||||
// Return 0 if parsing failed (NaN)
|
||||
return Number.isNaN(time) ? 0 : time
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,14 @@ import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/mess
|
|||
import type { Uploadable } from '@honcho-ai/core/uploads'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import {
|
||||
Representation,
|
||||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
import { SessionContext, SessionSummaries, Summary } from './session_context'
|
||||
// Disabled: observations not ready for release
|
||||
// import type { Observation, ObservationQueryParams } from './types'
|
||||
import {
|
||||
ContextParamsSchema,
|
||||
type DeriverStatusOptions,
|
||||
|
|
@ -18,6 +25,7 @@ import {
|
|||
LimitSchema,
|
||||
type MessageAddition,
|
||||
MessageAdditionSchema,
|
||||
// ObservationQueryParamsSchema, // Disabled: observations not ready for release
|
||||
type PeerAddition,
|
||||
PeerAdditionSchema,
|
||||
type PeerRemoval,
|
||||
|
|
@ -47,7 +55,7 @@ export class SessionPeerConfig {
|
|||
* of other peers in the session. When false, this peer will not build local
|
||||
* representations of other peers within this session.
|
||||
*/
|
||||
observe_others?: boolean
|
||||
observe_others?: boolean | null
|
||||
|
||||
/**
|
||||
* Initialize SessionPeerConfig with observation settings.
|
||||
|
|
@ -55,7 +63,7 @@ export class SessionPeerConfig {
|
|||
* @param observe_me - Whether other peers should observe this peer in the session
|
||||
* @param observe_others - Whether this peer should observe others in the session
|
||||
*/
|
||||
constructor(observe_me?: boolean | null, observe_others?: boolean) {
|
||||
constructor(observe_me?: boolean | null, observe_others?: boolean | null) {
|
||||
const validatedConfig = SessionPeerConfigSchema.parse({
|
||||
observe_me,
|
||||
observe_others,
|
||||
|
|
@ -112,6 +120,36 @@ export class Session {
|
|||
* Reference to the parent Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
/**
|
||||
* Private cached metadata for this session.
|
||||
*/
|
||||
private _metadata?: Record<string, unknown>
|
||||
/**
|
||||
* Private cached configuration for this session.
|
||||
*/
|
||||
private _configuration?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Cached metadata for this session. May be stale if the session
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getMetadata() to get the latest metadata from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get metadata(): Record<string, unknown> | undefined {
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached configuration for this session. May be stale if the session
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getConfig() to get the latest configuration from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get configuration(): Record<string, unknown> | undefined {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Session. **Do not call this directly, use the client.session() method instead.**
|
||||
|
|
@ -119,11 +157,21 @@ export class Session {
|
|||
* @param id - Unique identifier for this session within the workspace
|
||||
* @param workspaceId - Workspace ID for scoping operations
|
||||
* @param client - Reference to the parent Honcho client instance
|
||||
* @param metadata - Optional metadata to initialize the cached value
|
||||
* @param configuration - Optional configuration to initialize the cached value
|
||||
*/
|
||||
constructor(id: string, workspaceId: string, client: HonchoCore) {
|
||||
constructor(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
client: HonchoCore,
|
||||
metadata?: Record<string, unknown>,
|
||||
configuration?: Record<string, unknown>
|
||||
) {
|
||||
this.id = id
|
||||
this.workspaceId = workspaceId
|
||||
this._client = client
|
||||
this._metadata = metadata
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -364,12 +412,12 @@ export class Session {
|
|||
* })
|
||||
* ```
|
||||
*/
|
||||
async addMessages(messages: MessageAddition): Promise<void> {
|
||||
async addMessages(messages: MessageAddition): Promise<Message[]> {
|
||||
const validatedMessages = MessageAdditionSchema.parse(messages)
|
||||
const messagesList = Array.isArray(validatedMessages)
|
||||
? validatedMessages
|
||||
: [validatedMessages]
|
||||
await this._client.workspaces.sessions.messages.create(
|
||||
return await this._client.workspaces.sessions.messages.create(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
|
|
@ -403,7 +451,8 @@ export class Session {
|
|||
*
|
||||
* Makes an API call to retrieve the current metadata associated with this session.
|
||||
* Metadata can include custom attributes, settings, or any other key-value data
|
||||
* that provides context about the session.
|
||||
* that provides context about the session. This method also updates the cached
|
||||
* metadata property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the session's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
|
|
@ -413,7 +462,8 @@ export class Session {
|
|||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return session.metadata || {}
|
||||
this._metadata = session.metadata || {}
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -422,7 +472,8 @@ export class Session {
|
|||
* Makes an API call to update the metadata associated with this session.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
* Metadata is useful for storing custom attributes, configuration, or
|
||||
* contextual information about the session.
|
||||
* contextual information about the session. This method also updates the
|
||||
* cached metadata property.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with this session.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
|
|
@ -431,12 +482,71 @@ export class Session {
|
|||
await this._client.workspaces.sessions.update(this.workspaceId, this.id, {
|
||||
metadata,
|
||||
})
|
||||
this._metadata = metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this session.
|
||||
* Get configuration for this session.
|
||||
*
|
||||
* Makes an API call to mark this session as inactive.
|
||||
* Makes an API call to retrieve the current configuration associated with this session.
|
||||
* Configuration includes settings that control session behavior. This method also
|
||||
* updates the cached configuration property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the session's configuration.
|
||||
* Returns an empty dictionary if no configuration is set
|
||||
*/
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
const session = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
this._configuration = session.configuration || {}
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration for this session.
|
||||
*
|
||||
* Makes an API call to update the configuration associated with this session.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @param configuration - A dictionary of configuration to associate with this session.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setConfig(configuration: Record<string, unknown>): Promise<void> {
|
||||
await this._client.workspaces.sessions.update(this.workspaceId, this.id, {
|
||||
configuration,
|
||||
})
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached metadata and configuration for this session.
|
||||
*
|
||||
* Makes a single API call to retrieve the latest metadata and configuration
|
||||
* associated with this session and updates the cached properties.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const session = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
this._metadata = session.metadata || {}
|
||||
this._configuration = session.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this session and all associated data.
|
||||
*
|
||||
* Makes an API call to permanently delete this session and all related data including:
|
||||
* - Messages
|
||||
* - Message embeddings
|
||||
* - Observations
|
||||
* - Session-Peer associations
|
||||
* - Background processing queue items
|
||||
*
|
||||
* This action cannot be undone.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this._client.workspaces.sessions.delete(this.workspaceId, this.id)
|
||||
|
|
@ -478,7 +588,8 @@ export class Session {
|
|||
tokens?: number,
|
||||
peerTarget?: string | Peer,
|
||||
lastUserMessage?: string | Message,
|
||||
peerPerspective?: string | Peer
|
||||
peerPerspective?: string | Peer,
|
||||
representationOptions?: RepresentationOptions
|
||||
): Promise<SessionContext>
|
||||
async getContext(options?: {
|
||||
summary?: boolean
|
||||
|
|
@ -486,6 +597,8 @@ export class Session {
|
|||
peerTarget?: string | Peer
|
||||
lastUserMessage?: string | Message
|
||||
peerPerspective?: string | Peer
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
}): Promise<SessionContext>
|
||||
async getContext(
|
||||
summaryOrOptions?:
|
||||
|
|
@ -496,11 +609,14 @@ export class Session {
|
|||
peerTarget?: string | Peer
|
||||
lastUserMessage?: string | Message
|
||||
peerPerspective?: string | Peer
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
},
|
||||
tokens?: number,
|
||||
peerTarget?: string | Peer,
|
||||
lastUserMessage?: string | Message,
|
||||
peerPerspective?: string | Peer
|
||||
peerPerspective?: string | Peer,
|
||||
representationOptions?: RepresentationOptions
|
||||
): Promise<SessionContext> {
|
||||
// Normalize positional arguments into options object
|
||||
let options: {
|
||||
|
|
@ -509,10 +625,13 @@ export class Session {
|
|||
peerTarget?: string
|
||||
lastUserMessage?: string
|
||||
peerPerspective?: string
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
}
|
||||
|
||||
if (
|
||||
typeof summaryOrOptions === 'boolean' ||
|
||||
// biome-ignore lint/complexity/noArguments: Need to detect which overload pattern is being used
|
||||
(summaryOrOptions === undefined && arguments.length > 1)
|
||||
) {
|
||||
// Positional arguments pattern
|
||||
|
|
@ -528,6 +647,7 @@ export class Session {
|
|||
typeof peerPerspective === 'object'
|
||||
? peerPerspective.id
|
||||
: peerPerspective,
|
||||
representationOptions,
|
||||
}
|
||||
} else {
|
||||
// Options object pattern
|
||||
|
|
@ -540,6 +660,8 @@ export class Session {
|
|||
peerTarget: options.peerTarget,
|
||||
lastUserMessage: options.lastUserMessage,
|
||||
peerPerspective: options.peerPerspective,
|
||||
limitToSession: options.limitToSession,
|
||||
representationOptions: options.representationOptions,
|
||||
})
|
||||
|
||||
// Extract message ID if lastUserMessage is a Message object
|
||||
|
|
@ -557,6 +679,13 @@ export class Session {
|
|||
last_message: lastMessageId,
|
||||
peer_target: contextParams.peerTarget,
|
||||
peer_perspective: contextParams.peerPerspective,
|
||||
limit_to_session: contextParams.limitToSession,
|
||||
search_top_k: contextParams.representationOptions?.searchTopK,
|
||||
search_max_distance:
|
||||
contextParams.representationOptions?.searchMaxDistance,
|
||||
include_most_derived:
|
||||
contextParams.representationOptions?.includeMostDerived,
|
||||
max_observations: contextParams.representationOptions?.maxObservations,
|
||||
}
|
||||
)
|
||||
// Convert the summary response to Summary object if present
|
||||
|
|
@ -636,6 +765,91 @@ export class Session {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* List all observations for this session.
|
||||
*
|
||||
* Observations are theory-of-mind data (documents) that peers have formed about each other.
|
||||
* Returns paginated results that can be filtered by observer_id and observed_id.
|
||||
*
|
||||
* @param filters - Optional filters to scope the observations: see [filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @returns A paginated list of Observation objects.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const observations = await session.listObservations()
|
||||
* for await (const observation of observations) {
|
||||
* console.log(`${observation.observer_id} observed: ${observation.content}`)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
// Disabled: observations not ready for release
|
||||
// async listObservations(filters?: Filters): Promise<Page<Observation>> {
|
||||
// const validatedFilters = filters ? FilterSchema.parse(filters) : undefined
|
||||
// const response = await this._client.workspaces.sessions.observations.list(
|
||||
// this.workspaceId,
|
||||
// this.id,
|
||||
// { filters: validatedFilters }
|
||||
// )
|
||||
// return new Page(response)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Query observations using semantic search.
|
||||
*
|
||||
* Performs vector similarity search on observations to find semantically relevant results.
|
||||
* Use this to find observations related to a specific topic or concept.
|
||||
*
|
||||
* @param params - Query parameters
|
||||
* @param params.query - The semantic search query
|
||||
* @param params.top_k - Number of results to return (1-100, default: 10)
|
||||
* @param params.distance - Maximum cosine distance threshold for results (0.0-1.0)
|
||||
* @param params.filters - Optional filters to scope the query
|
||||
* @returns A list of Observation objects matching the query
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const observations = await session.queryObservations({
|
||||
* query: "user preferences about music",
|
||||
* top_k: 5,
|
||||
* distance: 0.8
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
// Disabled: observations not ready for release
|
||||
// async queryObservations(
|
||||
// params: ObservationQueryParams
|
||||
// ): Promise<Observation[]> {
|
||||
// const validated = ObservationQueryParamsSchema.parse(params)
|
||||
// return await this._client.workspaces.sessions.observations.query(
|
||||
// this.workspaceId,
|
||||
// this.id,
|
||||
// validated
|
||||
// )
|
||||
// }
|
||||
|
||||
/**
|
||||
* Delete a specific observation by ID.
|
||||
*
|
||||
* This permanently deletes the observation (document) from the theory-of-mind system.
|
||||
* This action cannot be undone.
|
||||
*
|
||||
* @param observationId - The ID of the observation to delete
|
||||
* @returns A promise that resolves when the observation is deleted
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await session.deleteObservation('obs_123abc')
|
||||
* ```
|
||||
*/
|
||||
// Disabled: observations not ready for release
|
||||
// async deleteObservation(observationId: string): Promise<void> {
|
||||
// await this._client.workspaces.sessions.observations.delete(
|
||||
// this.workspaceId,
|
||||
// this.id,
|
||||
// observationId
|
||||
// )
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get the deriver processing status for this session, optionally scoped to an observer or sender.
|
||||
*
|
||||
|
|
@ -755,6 +969,10 @@ export class Session {
|
|||
* - Buffer or Uint8Array with filename and content_type
|
||||
* - { filename: string, content: Buffer | Uint8Array, content_type: string }
|
||||
* @param peerId - The peer ID to attribute the created messages to
|
||||
* @param options - Optional parameters for the uploaded messages
|
||||
* @param options.metadata - Optional metadata dictionary to associate with the messages
|
||||
* @param options.configuration - Optional configuration dictionary to associate with the messages
|
||||
* @param options.created_at - Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.
|
||||
* @returns Promise resolving to a list of Message objects representing the created messages
|
||||
*
|
||||
* @note Supported file types include PDFs, text files, and JSON documents.
|
||||
|
|
@ -766,17 +984,57 @@ export class Session {
|
|||
* // Upload a file
|
||||
* const messages = await session.uploadFile(fileInput.files[0], 'user123')
|
||||
* console.log(`Created ${messages.length} messages from file`)
|
||||
*
|
||||
* // Upload a file with metadata and timestamp
|
||||
* const messages = await session.uploadFile(fileInput.files[0], 'user123', {
|
||||
* metadata: { source: 'upload' },
|
||||
* created_at: '2021-01-01T00:00:00.000Z'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async uploadFile(file: Uploadable, peerId: string): Promise<Message[]> {
|
||||
const uploadParams = FileUploadSchema.parse({ file, peerId })
|
||||
async uploadFile(
|
||||
file: Uploadable,
|
||||
peerId: string,
|
||||
options?: {
|
||||
metadata?: Record<string, unknown>
|
||||
configuration?: Record<string, unknown>
|
||||
created_at?: string | Date
|
||||
}
|
||||
): Promise<Message[]> {
|
||||
const createdAt =
|
||||
options?.created_at instanceof Date
|
||||
? options.created_at.toISOString()
|
||||
: options?.created_at
|
||||
|
||||
const uploadParams = FileUploadSchema.parse({
|
||||
file,
|
||||
peerId,
|
||||
metadata: options?.metadata,
|
||||
configuration: options?.configuration,
|
||||
created_at: createdAt,
|
||||
})
|
||||
|
||||
// Build body with file and peer_id, plus optional fields as JSON strings
|
||||
const body = {
|
||||
file: uploadParams.file,
|
||||
peer_id: uploadParams.peerId,
|
||||
...(uploadParams.metadata !== undefined && uploadParams.metadata !== null
|
||||
? { metadata: JSON.stringify(uploadParams.metadata) }
|
||||
: {}),
|
||||
...(uploadParams.configuration !== undefined &&
|
||||
uploadParams.configuration !== null
|
||||
? { configuration: JSON.stringify(uploadParams.configuration) }
|
||||
: {}),
|
||||
...(uploadParams.created_at !== undefined &&
|
||||
uploadParams.created_at !== null
|
||||
? { created_at: uploadParams.created_at }
|
||||
: {}),
|
||||
}
|
||||
|
||||
const response = await this._client.workspaces.sessions.messages.upload(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
file: uploadParams.file,
|
||||
peer_id: uploadParams.peerId,
|
||||
}
|
||||
body
|
||||
)
|
||||
|
||||
return response
|
||||
|
|
@ -792,23 +1050,41 @@ export class Session {
|
|||
* @param peer - The peer to get the working representation of. Can be peer ID string or Peer object
|
||||
* @param target - Optional target peer. If provided, returns what `peer` knows about
|
||||
* `target` within this session context rather than `peer`'s global representation
|
||||
* @returns Promise resolving to a dictionary containing the peer's representation information,
|
||||
* including facts, characteristics, and contextual knowledge
|
||||
* @param options - Optional representation options to filter and configure the results
|
||||
* @returns Promise resolving to a Representation object containing explicit and deductive observations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get peer's global representation in this session
|
||||
* const globalRep = await session.workingRep('user123')
|
||||
* console.log(globalRep.toString())
|
||||
*
|
||||
* // Get what user123 knows about assistant in this session
|
||||
* const localRep = await session.workingRep('user123', 'assistant')
|
||||
*
|
||||
* // Get representation with semantic search
|
||||
* const searchedRep = await session.workingRep('user123', undefined, {
|
||||
* searchQuery: 'preferences',
|
||||
* searchTopK: 10
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async workingRep(
|
||||
peer: string | Peer,
|
||||
target?: string | Peer
|
||||
): Promise<Record<string, unknown>> {
|
||||
const workingRepParams = WorkingRepParamsSchema.parse({ peer, target })
|
||||
target?: string | Peer,
|
||||
options?: {
|
||||
searchQuery?: string
|
||||
searchTopK?: number
|
||||
searchMaxDistance?: number
|
||||
includeMostDerived?: boolean
|
||||
maxObservations?: number
|
||||
}
|
||||
): Promise<Representation> {
|
||||
const workingRepParams = WorkingRepParamsSchema.parse({
|
||||
peer,
|
||||
target,
|
||||
options,
|
||||
})
|
||||
const peerId =
|
||||
typeof workingRepParams.peer === 'string'
|
||||
? workingRepParams.peer
|
||||
|
|
@ -819,14 +1095,27 @@ export class Session {
|
|||
: workingRepParams.target.id
|
||||
: undefined
|
||||
|
||||
return await this._client.workspaces.peers.workingRepresentation(
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
peerId,
|
||||
{
|
||||
session_id: this.id,
|
||||
target: targetId,
|
||||
search_query: workingRepParams.options?.searchQuery,
|
||||
search_top_k: workingRepParams.options?.searchTopK,
|
||||
search_max_distance: workingRepParams.options?.searchMaxDistance,
|
||||
include_most_derived: workingRepParams.options?.includeMostDerived,
|
||||
max_observations: workingRepParams.options?.maxObservations,
|
||||
}
|
||||
)
|
||||
const maybe = response as
|
||||
| RepresentationData
|
||||
| { representation?: RepresentationData | null }
|
||||
| null
|
||||
const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe
|
||||
? (maybe as { representation?: RepresentationData | null }).representation
|
||||
: maybe) ?? { explicit: [], deductive: [] }
|
||||
return Representation.fromData(rep as RepresentationData)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,6 +2,28 @@
|
|||
* Shared types for the Honcho TypeScript SDK.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Observation - external view of a document (theory-of-mind data).
|
||||
*/
|
||||
export interface Observation {
|
||||
id: string
|
||||
content: string
|
||||
observer_id: string
|
||||
observed_id: string
|
||||
session_id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for semantic search of observations.
|
||||
*/
|
||||
export interface ObservationQueryParams {
|
||||
query: string
|
||||
top_k?: number
|
||||
distance?: number
|
||||
filters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Delta object for streaming dialectic responses.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const SessionIdSchema = z
|
|||
*/
|
||||
export const SessionPeerConfigSchema = z.object({
|
||||
observe_me: z.boolean().nullable().optional(),
|
||||
observe_others: z.boolean().optional(),
|
||||
observe_others: z.boolean().nullable().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +88,15 @@ export const MessageMetadataSchema = z
|
|||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
|
||||
/**
|
||||
* Schema for message configuration.
|
||||
* Configuration can include deriver and peer_card settings.
|
||||
*/
|
||||
export const MessageConfigurationSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.nullable()
|
||||
.optional()
|
||||
|
||||
/**
|
||||
* Schema for message creation.
|
||||
*/
|
||||
|
|
@ -95,6 +104,7 @@ export const MessageCreateSchema = z.object({
|
|||
peer_id: PeerIdSchema,
|
||||
content: MessageContentSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: MessageConfigurationSchema,
|
||||
created_at: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
|
|
@ -138,6 +148,30 @@ const MessageSchema: z.ZodType<Message> = z.object({
|
|||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
}) as z.ZodType<Message>
|
||||
|
||||
/**
|
||||
* Schema for representation options.
|
||||
*/
|
||||
export const RepresentationOptionsSchema = z.object({
|
||||
searchTopK: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'searchTopK must be at least 1')
|
||||
.max(100, 'searchTopK must be at most 100')
|
||||
.optional(),
|
||||
searchMaxDistance: z
|
||||
.number()
|
||||
.min(0.0, 'searchMaxDistance must be at least 0.0')
|
||||
.max(1.0, 'searchMaxDistance must be at most 1.0')
|
||||
.optional(),
|
||||
includeMostDerived: z.boolean().optional(),
|
||||
maxObservations: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'maxObservations must be at least 1')
|
||||
.max(100, 'maxObservations must be at most 100')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for context retrieval parameters.
|
||||
*/
|
||||
|
|
@ -156,6 +190,8 @@ export const ContextParamsSchema = z
|
|||
.optional(),
|
||||
peerTarget: PeerIdSchema.optional(),
|
||||
peerPerspective: PeerIdSchema.optional(),
|
||||
limitToSession: z.boolean().optional(),
|
||||
representationOptions: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.lastUserMessage && !data.peerTarget) {
|
||||
|
|
@ -217,6 +253,9 @@ export const FileUploadSchema = z.object({
|
|||
),
|
||||
]),
|
||||
peerId: PeerIdSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: z.record(z.string(), z.unknown()).optional(),
|
||||
created_at: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -225,6 +264,20 @@ export const FileUploadSchema = z.object({
|
|||
export const WorkingRepParamsSchema = z.object({
|
||||
peer: z.union([z.string(), z.object({ id: z.string() })]),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
options: RepresentationOptionsSchema.extend({
|
||||
searchQuery: SearchQuerySchema.optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for peer working representation parameters.
|
||||
*/
|
||||
export const PeerWorkingRepParamsSchema = z.object({
|
||||
session: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
options: RepresentationOptionsSchema.extend({
|
||||
searchQuery: SearchQuerySchema.optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -271,6 +324,11 @@ export const MessageAdditionSchema = z.union([
|
|||
*/
|
||||
export const WorkspaceMetadataSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for workspace configuration.
|
||||
*/
|
||||
export const WorkspaceConfigSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for limit.
|
||||
*/
|
||||
|
|
@ -280,6 +338,25 @@ export const LimitSchema = z
|
|||
.min(1, 'Limit must be a positive integer')
|
||||
.max(100, 'Limit must be less than or equal to 100')
|
||||
|
||||
/**
|
||||
* Schema for observation query parameters.
|
||||
*/
|
||||
export const ObservationQueryParamsSchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
top_k: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'top_k must be at least 1')
|
||||
.max(100, 'top_k must be at most 100')
|
||||
.optional(),
|
||||
distance: z
|
||||
.number()
|
||||
.min(0.0, 'distance must be at least 0.0')
|
||||
.max(1.0, 'distance must be at most 1.0')
|
||||
.optional(),
|
||||
filters: FilterSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Type exports for use throughout the SDK.
|
||||
*/
|
||||
|
|
@ -296,8 +373,13 @@ export type ContextParams = z.infer<typeof ContextParamsSchema>
|
|||
export type DeriverStatusOptions = z.infer<typeof DeriverStatusOptionsSchema>
|
||||
export type FileUpload = z.infer<typeof FileUploadSchema>
|
||||
export type WorkingRepParams = z.infer<typeof WorkingRepParamsSchema>
|
||||
export type PeerWorkingRepParams = z.infer<typeof PeerWorkingRepParamsSchema>
|
||||
export type PeerAddition = z.infer<typeof PeerAdditionSchema>
|
||||
export type PeerRemoval = z.infer<typeof PeerRemovalSchema>
|
||||
export type MessageAddition = z.infer<typeof MessageAdditionSchema>
|
||||
export type WorkspaceMetadata = z.infer<typeof WorkspaceMetadataSchema>
|
||||
export type WorkspaceConfig = z.infer<typeof WorkspaceConfigSchema>
|
||||
export type Limit = z.infer<typeof LimitSchema>
|
||||
export type ObservationQueryParams = z.infer<
|
||||
typeof ObservationQueryParamsSchema
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ class BackupLLMSettingsMixin:
|
|||
both fields are set together or both are None.
|
||||
"""
|
||||
|
||||
BACKUP_PROVIDER: SupportedProviders | None = None
|
||||
BACKUP_MODEL: str | None = None
|
||||
BACKUP_PROVIDER: SupportedProviders | None = "custom"
|
||||
BACKUP_MODEL: str | None = "x-ai/grok-4-fast"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_backup_configuration(self):
|
||||
|
|
@ -236,7 +236,7 @@ class DeriverSettings(BackupLLMSettingsMixin, HonchoSettings):
|
|||
# Thinking budget tokens are only applied when using Anthropic as provider
|
||||
THINKING_BUDGET_TOKENS: Annotated[int, Field(default=1024, gt=0, le=5000)] = 1024
|
||||
|
||||
# Maximum number of observations to store in working representation
|
||||
# Maximum number of observations to return in working representation
|
||||
# This is applied to both explicit and deductive observations
|
||||
WORKING_REPRESENTATION_MAX_OBSERVATIONS: Annotated[
|
||||
int, Field(default=50, gt=0, le=500)
|
||||
|
|
@ -351,8 +351,8 @@ class DreamSettings(BackupLLMSettingsMixin, HonchoSettings):
|
|||
ENABLED_TYPES: list[str] = ["consolidate"]
|
||||
|
||||
# LLM settings for dream processing
|
||||
PROVIDER: SupportedProviders = "openai"
|
||||
MODEL: str = "gpt-4o-mini-2024-07-18"
|
||||
PROVIDER: SupportedProviders = "google"
|
||||
MODEL: str = "gemini-2.5-flash"
|
||||
MAX_OUTPUT_TOKENS: Annotated[int, Field(default=2000, gt=0, le=10_000)] = 2000
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ from .collection import get_collection, get_or_create_collection
|
|||
from .deriver import get_deriver_status
|
||||
from .document import (
|
||||
create_documents,
|
||||
delete_document,
|
||||
delete_document_by_id,
|
||||
get_all_documents,
|
||||
get_documents_with_filters,
|
||||
query_documents,
|
||||
)
|
||||
from .message import (
|
||||
|
|
@ -47,6 +50,7 @@ from .workspace import (
|
|||
delete_workspace,
|
||||
get_all_workspaces,
|
||||
get_or_create_workspace,
|
||||
get_workspace,
|
||||
update_workspace,
|
||||
)
|
||||
|
||||
|
|
@ -59,7 +63,10 @@ __all__ = [
|
|||
# Document
|
||||
"create_documents",
|
||||
"get_all_documents",
|
||||
"get_documents_with_filters",
|
||||
"query_documents",
|
||||
"delete_document",
|
||||
"delete_document_by_id",
|
||||
# Message
|
||||
"create_messages",
|
||||
"get_messages",
|
||||
|
|
@ -98,6 +105,7 @@ __all__ = [
|
|||
# Workspace
|
||||
"delete_workspace",
|
||||
"get_or_create_workspace",
|
||||
"get_workspace",
|
||||
"get_all_workspaces",
|
||||
"update_workspace",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -98,10 +98,12 @@ def _build_queue_status_query(
|
|||
models.QueueItem.work_unit_key == models.ActiveQueueSession.work_unit_key,
|
||||
)
|
||||
|
||||
stmt = stmt.join(models.Session, models.QueueItem.session_id == models.Session.id)
|
||||
stmt = stmt.where(models.Session.workspace_name == workspace_name)
|
||||
stmt = stmt.where(models.QueueItem.workspace_name == workspace_name)
|
||||
|
||||
if session_name is not None:
|
||||
stmt = stmt.join(
|
||||
models.Session, models.QueueItem.session_id == models.Session.id
|
||||
)
|
||||
stmt = stmt.where(models.Session.name == session_name)
|
||||
|
||||
peer_conditions = []
|
||||
|
|
|
|||
|
|
@ -2,42 +2,101 @@ from collections.abc import Sequence
|
|||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.sql import Select
|
||||
|
||||
from src import models, schemas
|
||||
from src.config import settings
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import ValidationException
|
||||
from src.exceptions import ResourceNotFoundException, ValidationException
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
async def get_all_documents(
|
||||
db: AsyncSession,
|
||||
def get_all_documents(
|
||||
workspace_name: str,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
limit: int = 1000,
|
||||
) -> Sequence[models.Document]:
|
||||
filters: dict[str, Any] | None = None,
|
||||
reverse: bool = False,
|
||||
limit: int | None = None,
|
||||
) -> Select[tuple[models.Document]]:
|
||||
"""
|
||||
Get all documents in a collection.
|
||||
|
||||
NOTE: Order is nondeterministic. Also this may return a massive amount of documents. Don't use this on large collections.
|
||||
TODO: add pagination and update dreaming logic to deduplicate more effectively
|
||||
Returns a Select query for pagination support via apaginate().
|
||||
Results are ordered by created_at timestamp.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
observer: Name of the observing peer
|
||||
observed: Name of the observed peer
|
||||
filters: Optional filters to apply
|
||||
reverse: Whether to reverse the order (oldest first)
|
||||
|
||||
Returns:
|
||||
Select query for documents
|
||||
"""
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.limit(limit)
|
||||
.where(models.Document.workspace_name == workspace_name)
|
||||
.where(models.Document.observer == observer)
|
||||
.where(models.Document.observed == observed)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
# Apply additional filters if provided
|
||||
stmt = apply_filter(stmt, models.Document, filters)
|
||||
|
||||
# Order by created_at (newest first by default)
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Document.created_at.asc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Document.created_at.desc())
|
||||
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def get_documents_with_filters(
|
||||
workspace_name: str,
|
||||
*,
|
||||
filters: dict[str, Any] | None = None,
|
||||
reverse: bool = False,
|
||||
) -> Select[tuple[models.Document]]:
|
||||
"""
|
||||
Get all documents using custom filters.
|
||||
|
||||
Returns a Select query for pagination support via apaginate().
|
||||
Results are ordered by created_at timestamp.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
filters: Optional filters to apply
|
||||
reverse: Whether to reverse the order (oldest first)
|
||||
|
||||
Returns:
|
||||
Select query for documents
|
||||
"""
|
||||
stmt = select(models.Document).where(
|
||||
models.Document.workspace_name == workspace_name
|
||||
)
|
||||
|
||||
# Apply additional filters if provided
|
||||
stmt = apply_filter(stmt, models.Document, filters)
|
||||
|
||||
# Order by created_at (newest first by default)
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Document.created_at.asc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Document.created_at.desc())
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
async def query_documents(
|
||||
|
|
@ -162,6 +221,79 @@ async def create_documents(
|
|||
return len(honcho_documents)
|
||||
|
||||
|
||||
async def delete_document(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
document_id: str,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
session_name: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a single document by ID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
workspace_name: Name of the workspace
|
||||
document_id: ID of the document to delete
|
||||
observer: Name of the observing peer (for authorization)
|
||||
observed: Name of the observed peer (for authorization)
|
||||
session_name: Optional session name to verify document belongs to session
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If document not found or doesn't match criteria
|
||||
"""
|
||||
stmt = delete(models.Document).where(
|
||||
models.Document.id == document_id,
|
||||
models.Document.workspace_name == workspace_name,
|
||||
models.Document.observer == observer,
|
||||
models.Document.observed == observed,
|
||||
)
|
||||
|
||||
# If session is specified, ensure document belongs to that session
|
||||
if session_name is not None:
|
||||
stmt = stmt.where(models.Document.session_name == session_name)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
if result.rowcount == 0:
|
||||
raise ResourceNotFoundException(
|
||||
f"Document {document_id} not found or does not belong to the specified collection/session"
|
||||
)
|
||||
|
||||
|
||||
async def delete_document_by_id(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
document_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a single document by ID and workspace.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
workspace_name: Name of the workspace
|
||||
document_id: ID of the document to delete
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If document not found or doesn't belong to the workspace
|
||||
"""
|
||||
stmt = delete(models.Document).where(
|
||||
models.Document.id == document_id,
|
||||
models.Document.workspace_name == workspace_name,
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
if result.rowcount == 0:
|
||||
raise ResourceNotFoundException(
|
||||
f"Document {document_id} not found or does not belong to workspace {workspace_name}"
|
||||
)
|
||||
|
||||
|
||||
async def is_rejected_duplicate(
|
||||
db: AsyncSession,
|
||||
doc: schemas.DocumentCreate,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from src.config import settings
|
|||
from src.dependencies import tracked_db
|
||||
from src.dreamer.dream_scheduler import check_and_schedule_dream
|
||||
from src.embedding_client import embedding_client
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
from src.utils.logging import accumulate_metric
|
||||
from src.utils.representation import (
|
||||
|
|
@ -44,9 +45,10 @@ class RepresentationManager:
|
|||
async def save_representation(
|
||||
self,
|
||||
representation: Representation,
|
||||
message_id_range: tuple[int, int],
|
||||
message_ids: list[int],
|
||||
session_name: str,
|
||||
message_created_at: datetime.datetime,
|
||||
message_level_configuration: ResolvedConfiguration,
|
||||
) -> int:
|
||||
"""
|
||||
Save Representation objects to the collection as a set of documents.
|
||||
|
|
@ -85,7 +87,7 @@ class RepresentationManager:
|
|||
|
||||
batch_embed_duration = (time.perf_counter() - batch_embed_start) * 1000
|
||||
accumulate_metric(
|
||||
f"deriver_{message_id_range[1]}_{self.observer}",
|
||||
f"deriver_{message_ids[-1]}_{self.observer}",
|
||||
"embed_new_observations",
|
||||
batch_embed_duration,
|
||||
"ms",
|
||||
|
|
@ -98,14 +100,15 @@ class RepresentationManager:
|
|||
db,
|
||||
all_observations,
|
||||
embeddings,
|
||||
message_id_range,
|
||||
message_ids,
|
||||
session_name,
|
||||
message_created_at,
|
||||
message_level_configuration,
|
||||
)
|
||||
|
||||
create_document_duration = (time.perf_counter() - create_document_start) * 1000
|
||||
accumulate_metric(
|
||||
f"deriver_{message_id_range[1]}_{self.observer}",
|
||||
f"deriver_{message_ids[-1]}_{self.observer}",
|
||||
"save_new_observations",
|
||||
create_document_duration,
|
||||
"ms",
|
||||
|
|
@ -118,9 +121,10 @@ class RepresentationManager:
|
|||
db: AsyncSession,
|
||||
all_observations: list[ExplicitObservation | DeductiveObservation],
|
||||
embeddings: list[list[float]],
|
||||
message_id_range: tuple[int, int],
|
||||
message_ids: list[int],
|
||||
session_name: str,
|
||||
message_created_at: datetime.datetime,
|
||||
message_level_configuration: ResolvedConfiguration,
|
||||
) -> int:
|
||||
# get_or_create_collection already handles IntegrityError with rollback and a retry
|
||||
collection = await crud.get_or_create_collection(
|
||||
|
|
@ -144,7 +148,7 @@ class RepresentationManager:
|
|||
obs_premises = None
|
||||
|
||||
metadata: schemas.DocumentMetadata = schemas.DocumentMetadata(
|
||||
message_ids=[message_id_range],
|
||||
message_ids=message_ids,
|
||||
premises=obs_premises,
|
||||
message_created_at=format_datetime_utc(message_created_at),
|
||||
)
|
||||
|
|
@ -169,10 +173,11 @@ class RepresentationManager:
|
|||
deduplicate=settings.DERIVER.DEDUPLICATE,
|
||||
)
|
||||
|
||||
try:
|
||||
await check_and_schedule_dream(db, collection)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check dream scheduling: {e}")
|
||||
if message_level_configuration.dream.enabled:
|
||||
try:
|
||||
await check_and_schedule_dream(db, collection)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check dream scheduling: {e}")
|
||||
|
||||
return new_documents
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Any
|
|||
|
||||
from cashews import NOT_NONE
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy import Select, case, cast, func, insert, select, update
|
||||
from sqlalchemy import Select, and_, case, cast, delete, func, insert, select, update
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -83,9 +83,13 @@ async def get_sessions(
|
|||
filters: dict[str, Any] | None = None,
|
||||
) -> Select[tuple[models.Session]]:
|
||||
"""
|
||||
Get all sessions in a workspace.
|
||||
Get all active sessions in a workspace.
|
||||
"""
|
||||
stmt = select(models.Session).where(models.Session.workspace_name == workspace_name)
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.where(models.Session.workspace_name == workspace_name)
|
||||
.where(models.Session.is_active == True) # noqa: E712
|
||||
)
|
||||
|
||||
stmt = apply_filter(stmt, models.Session, filters)
|
||||
|
||||
|
|
@ -126,6 +130,12 @@ async def get_or_create_session(
|
|||
if honcho_session is not None:
|
||||
honcho_session = await db.merge(honcho_session, load=False)
|
||||
|
||||
# Reject operations on inactive sessions (marked for deletion)
|
||||
if not honcho_session.is_active:
|
||||
raise ResourceNotFoundException(
|
||||
f"Session {session.name} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
||||
# Track if we need to update cache
|
||||
needs_cache_update = False
|
||||
|
||||
|
|
@ -148,7 +158,9 @@ async def get_or_create_session(
|
|||
workspace_name=workspace_name,
|
||||
name=session.name,
|
||||
h_metadata=session.metadata or {},
|
||||
configuration=session.configuration or {},
|
||||
configuration=session.configuration.model_dump(exclude_none=True)
|
||||
if session.configuration
|
||||
else {},
|
||||
)
|
||||
try:
|
||||
db.add(honcho_session)
|
||||
|
|
@ -174,12 +186,14 @@ async def get_or_create_session(
|
|||
):
|
||||
honcho_session.h_metadata = session.metadata
|
||||
needs_cache_update = True
|
||||
if (
|
||||
session.configuration is not None
|
||||
and honcho_session.configuration != session.configuration
|
||||
):
|
||||
honcho_session.configuration = session.configuration
|
||||
needs_cache_update = True
|
||||
if session.configuration is not None:
|
||||
# Merge configuration instead of replacing to preserve existing keys
|
||||
existing_config = (honcho_session.configuration or {}).copy()
|
||||
incoming_config = session.configuration.model_dump(exclude_none=True)
|
||||
merged_config = {**existing_config, **incoming_config}
|
||||
if honcho_session.configuration != merged_config:
|
||||
honcho_session.configuration = merged_config
|
||||
needs_cache_update = True
|
||||
|
||||
# Add all peers to session
|
||||
if session.peer_names:
|
||||
|
|
@ -217,6 +231,8 @@ async def get_session(
|
|||
db: AsyncSession,
|
||||
session_name: str,
|
||||
workspace_name: str,
|
||||
*,
|
||||
include_inactive: bool = False,
|
||||
) -> models.Session:
|
||||
"""
|
||||
Get a session in a workspace.
|
||||
|
|
@ -225,12 +241,14 @@ async def get_session(
|
|||
db: Database session
|
||||
session_name: Name of the session
|
||||
workspace_name: Name of the workspace
|
||||
include_inactive: If True, return sessions even if they are marked for deletion.
|
||||
This should only be used for internal operations like the deletion task.
|
||||
|
||||
Returns:
|
||||
The session
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the session does not exist
|
||||
ResourceNotFoundException: If the session does not exist or is inactive
|
||||
"""
|
||||
session = await _fetch_session(db, workspace_name, session_name)
|
||||
|
||||
|
|
@ -239,6 +257,12 @@ async def get_session(
|
|||
f"Session {session_name} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
||||
# Check if session is active (unless include_inactive is True)
|
||||
if not include_inactive and not session.is_active:
|
||||
raise ResourceNotFoundException(
|
||||
f"Session {session_name} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
||||
# Merge cached object into session (cached objects are detached)
|
||||
session = await db.merge(session, load=False)
|
||||
|
||||
|
|
@ -277,12 +301,16 @@ async def update_session(
|
|||
honcho_session.h_metadata = session.metadata
|
||||
needs_update = True
|
||||
|
||||
if (
|
||||
session.configuration is not None
|
||||
and honcho_session.configuration != session.configuration
|
||||
):
|
||||
honcho_session.configuration = session.configuration
|
||||
needs_update = True
|
||||
if session.configuration is not None:
|
||||
# Merge configuration instead of replacing to preserve existing keys
|
||||
base_config = (honcho_session.configuration or {}).copy()
|
||||
merged_config = {
|
||||
**base_config,
|
||||
**session.configuration.model_dump(exclude_none=True),
|
||||
}
|
||||
if honcho_session.configuration != merged_config:
|
||||
honcho_session.configuration = merged_config
|
||||
needs_update = True
|
||||
|
||||
if not needs_update:
|
||||
logger.debug(
|
||||
|
|
@ -303,11 +331,56 @@ async def update_session(
|
|||
return honcho_session
|
||||
|
||||
|
||||
async def _batch_delete_matching(
|
||||
db: AsyncSession,
|
||||
model: Any,
|
||||
filter_conditions: list[Any],
|
||||
batch_size: int = 5000,
|
||||
) -> int:
|
||||
"""
|
||||
Delete records in batches that match the given filter conditions.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
model: SQLAlchemy model class
|
||||
filter_conditions: List of SQLAlchemy filter conditions
|
||||
batch_size: Number of records to delete per batch
|
||||
|
||||
Returns:
|
||||
Total number of records deleted
|
||||
"""
|
||||
total_deleted = 0
|
||||
primary_key_column = model.__table__.primary_key.columns.values()[0]
|
||||
|
||||
while True:
|
||||
subquery = (
|
||||
select(primary_key_column).where(and_(*filter_conditions)).limit(batch_size)
|
||||
)
|
||||
delete_stmt = delete(model).where(primary_key_column.in_(subquery))
|
||||
delete_result = await db.execute(delete_stmt)
|
||||
batch_deleted = delete_result.rowcount or 0
|
||||
total_deleted += batch_deleted
|
||||
|
||||
if batch_deleted == 0:
|
||||
break
|
||||
|
||||
return total_deleted
|
||||
|
||||
|
||||
async def delete_session(
|
||||
db: AsyncSession, workspace_name: str, session_name: str
|
||||
) -> bool:
|
||||
"""
|
||||
Mark a session as inactive (soft delete).
|
||||
Delete a session and all associated data (hard delete).
|
||||
|
||||
This performs cascading deletes for all session-related data including:
|
||||
- Active queue sessions
|
||||
- Queue items
|
||||
- Message embeddings (batched)
|
||||
- Documents (theory-of-mind data, batched)
|
||||
- Messages (batched)
|
||||
- Session peer associations
|
||||
- The session itself
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
|
@ -320,17 +393,83 @@ async def delete_session(
|
|||
Raises:
|
||||
ResourceNotFoundException: If the session does not exist
|
||||
"""
|
||||
honcho_session = await get_session(db, session_name, workspace_name)
|
||||
honcho_session = await get_session(
|
||||
db, session_name, workspace_name, include_inactive=True
|
||||
)
|
||||
|
||||
honcho_session.is_active = False
|
||||
await db.commit()
|
||||
await db.refresh(honcho_session)
|
||||
# Perform cascading deletes in order
|
||||
# Order is important to avoid foreign key constraint violations
|
||||
try:
|
||||
# Delete ActiveQueueSession entries
|
||||
# Work unit keys have format: {task_type}:{workspace_name}:{session_name}:{...}
|
||||
await db.execute(
|
||||
delete(models.ActiveQueueSession).where(
|
||||
and_(
|
||||
func.split_part(models.ActiveQueueSession.work_unit_key, ":", 2)
|
||||
== workspace_name,
|
||||
func.split_part(models.ActiveQueueSession.work_unit_key, ":", 3)
|
||||
== session_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Invalidate cache - read-through pattern
|
||||
cache_key = session_cache_key(workspace_name, session_name)
|
||||
await cache.delete(cache_key)
|
||||
# Delete QueueItem entries
|
||||
await db.execute(
|
||||
delete(models.QueueItem).where(
|
||||
models.QueueItem.session_id == honcho_session.id
|
||||
)
|
||||
)
|
||||
|
||||
# Delete MessageEmbedding entries in batches
|
||||
await _batch_delete_matching(
|
||||
db,
|
||||
models.MessageEmbedding,
|
||||
[
|
||||
models.MessageEmbedding.session_name == session_name,
|
||||
models.MessageEmbedding.workspace_name == workspace_name,
|
||||
],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
# Delete Document entries associated with this session in batches
|
||||
await _batch_delete_matching(
|
||||
db,
|
||||
models.Document,
|
||||
[
|
||||
models.Document.session_name == session_name,
|
||||
models.Document.workspace_name == workspace_name,
|
||||
],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
# Delete Message entries in batches
|
||||
await _batch_delete_matching(
|
||||
db,
|
||||
models.Message,
|
||||
[
|
||||
models.Message.session_name == session_name,
|
||||
models.Message.workspace_name == workspace_name,
|
||||
],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
# Delete SessionPeer associations
|
||||
await db.execute(
|
||||
delete(models.SessionPeer).where(
|
||||
models.SessionPeer.session_name == session_name,
|
||||
models.SessionPeer.workspace_name == workspace_name,
|
||||
)
|
||||
)
|
||||
|
||||
# Finally, delete the session itself
|
||||
await db.delete(honcho_session)
|
||||
await db.commit()
|
||||
logger.debug("Session %s and all associated data deleted", session_name)
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete session %s: %s", session_name, e)
|
||||
await db.rollback()
|
||||
raise e
|
||||
|
||||
logger.debug("Session %s marked as inactive", session_name)
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -353,11 +492,12 @@ async def clone_session(
|
|||
Returns:
|
||||
The newly created session
|
||||
"""
|
||||
# Get the original session
|
||||
# Get the original session (must be active)
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.where(models.Session.workspace_name == workspace_name)
|
||||
.where(models.Session.name == original_session_name)
|
||||
.where(models.Session.is_active == True) # noqa: E712
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
original_session = result.scalar_one_or_none()
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ async def get_or_create_workspace(
|
|||
honcho_workspace = models.Workspace(
|
||||
name=workspace.name,
|
||||
h_metadata=workspace.metadata,
|
||||
configuration=workspace.configuration,
|
||||
configuration=workspace.configuration.model_dump(exclude_none=True),
|
||||
)
|
||||
try:
|
||||
db.add(honcho_workspace)
|
||||
|
|
@ -182,12 +182,16 @@ async def update_workspace(
|
|||
honcho_workspace.h_metadata = workspace.metadata
|
||||
needs_update = True
|
||||
|
||||
if (
|
||||
workspace.configuration is not None
|
||||
and honcho_workspace.configuration != workspace.configuration
|
||||
):
|
||||
honcho_workspace.configuration = workspace.configuration
|
||||
needs_update = True
|
||||
if workspace.configuration is not None:
|
||||
# Merge configuration instead of replacing to preserve existing keys
|
||||
base_config = (honcho_workspace.configuration or {}).copy()
|
||||
merged_config = {
|
||||
**base_config,
|
||||
**workspace.configuration.model_dump(exclude_none=True),
|
||||
}
|
||||
if honcho_workspace.configuration != merged_config:
|
||||
honcho_workspace.configuration = merged_config
|
||||
needs_update = True
|
||||
|
||||
# Early exit if unchanged
|
||||
if not needs_update:
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ from pydantic import ValidationError
|
|||
from rich.console import Console
|
||||
from sqlalchemy import select
|
||||
|
||||
from src import models
|
||||
from src import crud, models
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
from src.dreamer.dreamer import process_dream
|
||||
from src.exceptions import ResourceNotFoundException
|
||||
from src.models import Message
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.utils import summarizer
|
||||
from src.utils.logging import log_performance_metrics
|
||||
from src.utils.queue_payload import (
|
||||
DeletionPayload,
|
||||
DreamPayload,
|
||||
SummaryPayload,
|
||||
WebhookPayload,
|
||||
|
|
@ -89,6 +92,7 @@ async def process_item(queue_item: models.QueueItem) -> None:
|
|||
message_id,
|
||||
validated.message_seq_in_session,
|
||||
message_public_id,
|
||||
validated.configuration,
|
||||
)
|
||||
log_performance_metrics("summary", f"{workspace_name}_{message_id}")
|
||||
|
||||
|
|
@ -104,12 +108,27 @@ async def process_item(queue_item: models.QueueItem) -> None:
|
|||
)
|
||||
raise ValueError(f"Invalid payload structure: {str(e)}") from e
|
||||
await process_dream(validated, workspace_name)
|
||||
|
||||
elif task_type == "deletion":
|
||||
with sentry_sdk.start_transaction(name="process_deletion_task", op="deriver"):
|
||||
try:
|
||||
validated = DeletionPayload(**queue_payload)
|
||||
except ValidationError as e:
|
||||
logger.error(
|
||||
"Invalid deletion payload received: %s. Payload: %s",
|
||||
str(e),
|
||||
queue_payload,
|
||||
)
|
||||
raise ValueError(f"Invalid payload structure: {str(e)}") from e
|
||||
await process_deletion(validated, workspace_name)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Invalid task type: {task_type}")
|
||||
|
||||
|
||||
async def process_representation_batch(
|
||||
messages: list[Message],
|
||||
message_level_configuration: ResolvedConfiguration | None,
|
||||
*,
|
||||
observer: str | None,
|
||||
observed: str | None,
|
||||
|
|
@ -133,5 +152,76 @@ async def process_representation_batch(
|
|||
)
|
||||
|
||||
await process_representation_tasks_batch(
|
||||
messages, observer=observer, observed=observed
|
||||
messages,
|
||||
message_level_configuration,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
||||
|
||||
async def process_deletion(
|
||||
payload: DeletionPayload,
|
||||
workspace_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Process a deletion task from the queue.
|
||||
|
||||
This function handles the actual deletion of resources based on the deletion type.
|
||||
It is designed to be idempotent - deleting an already-deleted resource is a no-op.
|
||||
|
||||
Args:
|
||||
payload: The deletion payload containing deletion_type and resource_id
|
||||
workspace_name: The workspace name for scoping the deletion
|
||||
|
||||
Raises:
|
||||
ValueError: If the deletion type is not supported
|
||||
"""
|
||||
deletion_type = payload.deletion_type
|
||||
resource_id = payload.resource_id
|
||||
|
||||
logger.info(
|
||||
"Processing deletion task: type=%s, resource_id=%s, workspace=%s",
|
||||
deletion_type,
|
||||
resource_id,
|
||||
workspace_name,
|
||||
)
|
||||
|
||||
async with tracked_db("process_deletion") as db:
|
||||
if deletion_type == "session":
|
||||
try:
|
||||
await crud.delete_session(
|
||||
db, workspace_name=workspace_name, session_name=resource_id
|
||||
)
|
||||
logger.info(
|
||||
"Successfully deleted session %s in workspace %s",
|
||||
resource_id,
|
||||
workspace_name,
|
||||
)
|
||||
except ResourceNotFoundException as e:
|
||||
# Session not found - may have already been deleted, treat as success
|
||||
logger.warning(
|
||||
"Session %s not found during deletion (may already be deleted): %s",
|
||||
resource_id,
|
||||
str(e),
|
||||
)
|
||||
|
||||
elif deletion_type == "observation":
|
||||
try:
|
||||
await crud.delete_document_by_id(
|
||||
db, workspace_name=workspace_name, document_id=resource_id
|
||||
)
|
||||
logger.info(
|
||||
"Successfully deleted observation %s in workspace %s",
|
||||
resource_id,
|
||||
workspace_name,
|
||||
)
|
||||
except ResourceNotFoundException as e:
|
||||
# Document not found - may have already been deleted, treat as success
|
||||
logger.warning(
|
||||
"Observation %s not found during deletion (may already be deleted): %s",
|
||||
resource_id,
|
||||
str(e),
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported deletion type: {deletion_type}")
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ from src.config import settings
|
|||
from src.crud.representation import RepresentationManager
|
||||
from src.dependencies import tracked_db
|
||||
from src.models import Message
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.utils import summarizer
|
||||
from src.utils.clients import honcho_llm_call
|
||||
from src.utils.config_helpers import get_configuration
|
||||
from src.utils.formatting import format_new_turn_with_timestamp
|
||||
from src.utils.logging import (
|
||||
accumulate_metric,
|
||||
|
|
@ -126,6 +128,7 @@ async def peer_card_call(
|
|||
@with_sentry_transaction("process_representation_tasks_batch", op="deriver")
|
||||
async def process_representation_tasks_batch(
|
||||
messages: list[Message],
|
||||
message_level_configuration: ResolvedConfiguration | None,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
|
|
@ -171,16 +174,29 @@ async def process_representation_tasks_batch(
|
|||
# include_most_derived=False,
|
||||
)
|
||||
|
||||
if settings.PEER_CARD.ENABLED:
|
||||
async with tracked_db("deriver.get_peer_card") as db:
|
||||
speaker_peer_card: list[str] | None = await crud.get_peer_card(
|
||||
async with tracked_db("deriver.get_peer_card") as db:
|
||||
if message_level_configuration is None:
|
||||
message_level_configuration = get_configuration(
|
||||
None,
|
||||
await crud.get_session(
|
||||
db, latest_message.session_name, latest_message.workspace_name
|
||||
),
|
||||
await crud.get_workspace(
|
||||
db, workspace_name=latest_message.workspace_name
|
||||
),
|
||||
)
|
||||
if message_level_configuration.peer_card.use is False:
|
||||
speaker_peer_card = None
|
||||
else:
|
||||
speaker_peer_card = await crud.get_peer_card(
|
||||
db,
|
||||
latest_message.workspace_name,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
else:
|
||||
speaker_peer_card = None
|
||||
|
||||
if message_level_configuration.deriver.enabled is False:
|
||||
return
|
||||
|
||||
# Estimate tokens for deriver input
|
||||
peer_card_tokens = estimate_tokens(speaker_peer_card)
|
||||
|
|
@ -279,6 +295,7 @@ async def process_representation_tasks_batch(
|
|||
ctx=messages,
|
||||
observed=observed,
|
||||
observer=observer,
|
||||
message_level_configuration=message_level_configuration,
|
||||
)
|
||||
|
||||
# Run single-pass reasoning
|
||||
|
|
@ -321,6 +338,7 @@ class CertaintyReasoner:
|
|||
ctx: list[Message]
|
||||
observer: str
|
||||
observed: str
|
||||
message_level_configuration: ResolvedConfiguration
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -329,11 +347,13 @@ class CertaintyReasoner:
|
|||
*,
|
||||
observed: str,
|
||||
observer: str,
|
||||
message_level_configuration: ResolvedConfiguration,
|
||||
) -> None:
|
||||
self.representation_manager = representation_manager
|
||||
self.ctx = ctx
|
||||
self.observed = observed
|
||||
self.observer = observer
|
||||
self.message_level_configuration = message_level_configuration
|
||||
|
||||
@conditional_observe(name="Deriver")
|
||||
@sentry_sdk.trace
|
||||
|
|
@ -352,6 +372,7 @@ class CertaintyReasoner:
|
|||
"""
|
||||
analysis_start = time.perf_counter()
|
||||
|
||||
message_ids = [m.id for m in self.ctx]
|
||||
earliest_message = self.ctx[0]
|
||||
latest_message = self.ctx[-1]
|
||||
|
||||
|
|
@ -385,7 +406,7 @@ class CertaintyReasoner:
|
|||
|
||||
reasoning_response = Representation.from_prompt_representation(
|
||||
reasoning_response,
|
||||
(earliest_message.id, latest_message.id),
|
||||
[earliest_message.id, latest_message.id],
|
||||
latest_message.session_name,
|
||||
latest_message.created_at,
|
||||
)
|
||||
|
|
@ -405,20 +426,13 @@ class CertaintyReasoner:
|
|||
if not new_observations.is_empty():
|
||||
await self.representation_manager.save_representation(
|
||||
new_observations,
|
||||
(earliest_message.id, latest_message.id),
|
||||
message_ids,
|
||||
latest_message.session_name,
|
||||
latest_message.created_at,
|
||||
self.message_level_configuration,
|
||||
)
|
||||
|
||||
# not currently deduplicating at the save_representation step, so this isn't useful
|
||||
# accumulate_metric(
|
||||
# f"deriver_{latest_payload.message_id}_{latest_payload.observer}",
|
||||
# "new_observation_count",
|
||||
# new_observations_saved,
|
||||
# "count",
|
||||
# )
|
||||
|
||||
if settings.PEER_CARD.ENABLED:
|
||||
if self.message_level_configuration.peer_card.create:
|
||||
update_peer_card_start = time.perf_counter()
|
||||
if not new_observations.is_empty():
|
||||
await self._update_peer_card(speaker_peer_card, new_observations)
|
||||
|
|
@ -458,7 +472,9 @@ class CertaintyReasoner:
|
|||
]
|
||||
accumulate_metric(
|
||||
f"deriver_{self.ctx[-1].id}_{self.observer}",
|
||||
"new_peer_card",
|
||||
"new_peer_card"
|
||||
if self.observer == self.observed
|
||||
else f"new_{self.observed}_peer_card",
|
||||
"\n".join(new_peer_card),
|
||||
"blob",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Literal
|
||||
|
||||
from sqlalchemy import insert
|
||||
from sqlalchemy import insert, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, schemas
|
||||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.dreamer.dream_scheduler import get_affected_dream_keys, get_dream_scheduler
|
||||
from src.exceptions import ValidationException
|
||||
from src.models import QueueItem
|
||||
from src.utils.queue_payload import create_payload
|
||||
from src.schemas import MessageConfiguration, ResolvedConfiguration
|
||||
from src.utils.config_helpers import get_configuration
|
||||
from src.utils.queue_payload import (
|
||||
create_deletion_payload,
|
||||
create_dream_payload,
|
||||
create_payload,
|
||||
)
|
||||
from src.utils.work_unit import construct_work_unit_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -92,7 +99,11 @@ async def handle_session(
|
|||
workspace_name=workspace_name,
|
||||
)
|
||||
|
||||
deriver_disabled = bool(session.configuration.get("deriver_disabled"))
|
||||
# Fetch workspace for configuration resolution
|
||||
workspace = await crud.get_workspace(db_session, workspace_name=workspace_name)
|
||||
|
||||
# Resolve summary configuration with hierarchical fallback
|
||||
session_level_configuration = get_configuration(None, session, workspace)
|
||||
|
||||
peers_with_configuration = await get_peers_with_configuration(
|
||||
db_session, workspace_name, session_name
|
||||
|
|
@ -101,13 +112,20 @@ async def handle_session(
|
|||
queue_records: list[dict[str, Any]] = []
|
||||
|
||||
for message in payload:
|
||||
message_config: MessageConfiguration | None = message.get("configuration")
|
||||
if message_config is not None:
|
||||
message_level_configuration = get_configuration(
|
||||
message_config, session, workspace
|
||||
)
|
||||
else:
|
||||
message_level_configuration = session_level_configuration
|
||||
queue_records.extend(
|
||||
await generate_queue_records(
|
||||
db_session,
|
||||
message,
|
||||
peers_with_configuration,
|
||||
session.id,
|
||||
deriver_disabled=deriver_disabled,
|
||||
message_level_configuration,
|
||||
)
|
||||
)
|
||||
return queue_records
|
||||
|
|
@ -144,6 +162,7 @@ async def get_peers_with_configuration(
|
|||
|
||||
def create_representation_record(
|
||||
message: dict[str, Any],
|
||||
conf: ResolvedConfiguration,
|
||||
session_id: str | None = None,
|
||||
*,
|
||||
observer: str,
|
||||
|
|
@ -154,9 +173,10 @@ def create_representation_record(
|
|||
|
||||
Args:
|
||||
message: The message payload
|
||||
conf: Resolved configuration for this particular message
|
||||
session_id: Optional session ID
|
||||
observed: Name of the sender
|
||||
observer: Name of the target
|
||||
session_id: Optional session ID
|
||||
|
||||
Returns:
|
||||
Queue record dictionary with workspace_name and message_id as separate fields
|
||||
|
|
@ -169,8 +189,9 @@ def create_representation_record(
|
|||
if not isinstance(message_id, int):
|
||||
raise TypeError("message_id is required and must be an integer")
|
||||
|
||||
processed_payload = create_payload(
|
||||
processed_payload: dict[str, Any] = create_payload(
|
||||
message=message,
|
||||
configuration=conf,
|
||||
task_type="representation",
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
|
|
@ -187,6 +208,7 @@ def create_representation_record(
|
|||
|
||||
def create_summary_record(
|
||||
message: dict[str, Any],
|
||||
configuration: ResolvedConfiguration,
|
||||
session_id: str,
|
||||
message_seq_in_session: int,
|
||||
) -> dict[str, Any]:
|
||||
|
|
@ -211,6 +233,7 @@ def create_summary_record(
|
|||
|
||||
processed_payload = create_payload(
|
||||
message=message,
|
||||
configuration=configuration,
|
||||
task_type="summary",
|
||||
message_seq_in_session=message_seq_in_session,
|
||||
)
|
||||
|
|
@ -255,7 +278,11 @@ def get_effective_observe_me(
|
|||
return sender_session_peer_config.observe_me
|
||||
|
||||
# Otherwise use peer config
|
||||
return sender_peer_config.observe_me
|
||||
return (
|
||||
sender_peer_config.observe_me
|
||||
if sender_peer_config.observe_me is not None
|
||||
else True
|
||||
)
|
||||
|
||||
|
||||
async def generate_queue_records(
|
||||
|
|
@ -263,8 +290,7 @@ async def generate_queue_records(
|
|||
message: dict[str, Any],
|
||||
peers_with_configuration: dict[str, list[dict[str, Any]]],
|
||||
session_id: str,
|
||||
*,
|
||||
deriver_disabled: bool,
|
||||
conf: ResolvedConfiguration,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Process a single message and generate queue records based on configurations.
|
||||
|
|
@ -272,10 +298,9 @@ async def generate_queue_records(
|
|||
Args:
|
||||
db_session: The database session
|
||||
message: The message payload
|
||||
deriver_disabled: Whether deriver is disabled for the session
|
||||
peers_with_configuration: Dictionary of peer configurations
|
||||
session_id: Session ID
|
||||
message_seq_map: Optional pre-fetched mapping of message_id to sequence number
|
||||
configuration: Resolved configuration for this particular message
|
||||
|
||||
Returns:
|
||||
List of queue records for this message
|
||||
|
|
@ -295,19 +320,20 @@ async def generate_queue_records(
|
|||
|
||||
records: list[dict[str, Any]] = []
|
||||
|
||||
if settings.SUMMARY.ENABLED and (
|
||||
message_seq_in_session % settings.SUMMARY.MESSAGES_PER_SHORT_SUMMARY == 0
|
||||
or message_seq_in_session % settings.SUMMARY.MESSAGES_PER_LONG_SUMMARY == 0
|
||||
if conf.summary.enabled and (
|
||||
message_seq_in_session % conf.summary.messages_per_short_summary == 0
|
||||
or message_seq_in_session % conf.summary.messages_per_long_summary == 0
|
||||
):
|
||||
records.append(
|
||||
create_summary_record(
|
||||
message,
|
||||
configuration=conf,
|
||||
session_id=session_id,
|
||||
message_seq_in_session=message_seq_in_session,
|
||||
)
|
||||
)
|
||||
|
||||
if deriver_disabled:
|
||||
if not conf.deriver.enabled:
|
||||
return records
|
||||
|
||||
if get_effective_observe_me(observed, peers_with_configuration):
|
||||
|
|
@ -315,24 +341,23 @@ async def generate_queue_records(
|
|||
records.append(
|
||||
create_representation_record(
|
||||
message,
|
||||
conf,
|
||||
observed=observed,
|
||||
observer=observed,
|
||||
session_id=session_id,
|
||||
)
|
||||
)
|
||||
|
||||
for peer_name, configuration in peers_with_configuration.items():
|
||||
for peer_name, peer_conf in peers_with_configuration.items():
|
||||
if peer_name == observed:
|
||||
continue
|
||||
|
||||
# If the observer peer has left the session, we don't need to enqueue a representation task for them.
|
||||
if not configuration[2]:
|
||||
if not peer_conf[2]:
|
||||
continue
|
||||
|
||||
session_peer_config = (
|
||||
schemas.SessionPeerConfig(**configuration[1])
|
||||
if configuration[1]
|
||||
else None
|
||||
schemas.SessionPeerConfig(**peer_conf[1]) if peer_conf[1] else None
|
||||
)
|
||||
|
||||
if session_peer_config is None or not session_peer_config.observe_others:
|
||||
|
|
@ -342,6 +367,7 @@ async def generate_queue_records(
|
|||
# peer representation task
|
||||
create_representation_record(
|
||||
message,
|
||||
conf,
|
||||
observed=observed,
|
||||
observer=peer_name,
|
||||
session_id=session_id,
|
||||
|
|
@ -361,3 +387,198 @@ async def generate_queue_records(
|
|||
)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def create_dream_record(
|
||||
workspace_name: str,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
dream_type: schemas.DreamType,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Create a queue record for a dream task.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
observer: Name of the observer peer
|
||||
observed: Name of the observed peer
|
||||
dream_type: Type of dream to execute
|
||||
|
||||
Returns:
|
||||
Queue record dictionary with workspace_name and other fields
|
||||
"""
|
||||
dream_payload = create_dream_payload(
|
||||
dream_type,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
||||
return {
|
||||
"work_unit_key": construct_work_unit_key(workspace_name, dream_payload),
|
||||
"payload": dream_payload,
|
||||
"session_id": None,
|
||||
"task_type": "dream",
|
||||
"workspace_name": workspace_name,
|
||||
"message_id": None,
|
||||
}
|
||||
|
||||
|
||||
async def enqueue_dream(
|
||||
workspace_name: str,
|
||||
observer: str,
|
||||
observed: str,
|
||||
dream_type: schemas.DreamType,
|
||||
document_count: int,
|
||||
) -> None:
|
||||
"""
|
||||
Enqueue a dream task for immediate processing by the deriver.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
observer: Name of the observer peer
|
||||
observed: Name of the observed peer
|
||||
dream_type: Type of dream to execute
|
||||
document_count: Current document count for metadata update
|
||||
"""
|
||||
async with tracked_db("dream_enqueue") as db_session:
|
||||
try:
|
||||
# Create the dream queue record
|
||||
dream_record = create_dream_record(
|
||||
workspace_name,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
dream_type=dream_type,
|
||||
)
|
||||
|
||||
# Insert into queue
|
||||
stmt = insert(QueueItem).returning(QueueItem)
|
||||
await db_session.execute(stmt, [dream_record])
|
||||
|
||||
# Update collection metadata
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
update_stmt = (
|
||||
update(models.Collection)
|
||||
.where(
|
||||
models.Collection.workspace_name == workspace_name,
|
||||
models.Collection.observer == observer,
|
||||
models.Collection.observed == observed,
|
||||
)
|
||||
.values(
|
||||
internal_metadata=models.Collection.internal_metadata.op("||")(
|
||||
{
|
||||
"dream": {
|
||||
"last_dream_document_count": document_count,
|
||||
"last_dream_at": now_iso,
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
await db_session.execute(update_stmt)
|
||||
await db_session.commit()
|
||||
|
||||
logger.info(
|
||||
"Enqueued dream task for %s/%s/%s (type: %s)",
|
||||
workspace_name,
|
||||
observer,
|
||||
observed,
|
||||
dream_type.value,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to enqueue dream task!")
|
||||
if settings.SENTRY.ENABLED:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.capture_exception(e)
|
||||
raise
|
||||
|
||||
|
||||
def create_deletion_record(
|
||||
workspace_name: str,
|
||||
deletion_type: Literal["session", "observation"],
|
||||
resource_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Create a queue record for a deletion task.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
deletion_type: Type of resource to delete ("session" or "observation")
|
||||
resource_id: ID of the resource to delete
|
||||
|
||||
Returns:
|
||||
Queue record dictionary for insertion into the queue
|
||||
"""
|
||||
deletion_payload = create_deletion_payload(
|
||||
deletion_type=deletion_type,
|
||||
resource_id=resource_id,
|
||||
)
|
||||
|
||||
return {
|
||||
"work_unit_key": construct_work_unit_key(workspace_name, deletion_payload),
|
||||
"payload": deletion_payload,
|
||||
"session_id": None,
|
||||
"task_type": "deletion",
|
||||
"workspace_name": workspace_name,
|
||||
"message_id": None,
|
||||
}
|
||||
|
||||
|
||||
async def enqueue_deletion(
|
||||
workspace_name: str,
|
||||
deletion_type: Literal["session", "observation"],
|
||||
resource_id: str,
|
||||
db_session: AsyncSession | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Enqueue a deletion task for processing by the deriver.
|
||||
|
||||
This function adds a deletion task to the queue for asynchronous processing.
|
||||
The deletion will be handled by the queue consumer with retry support.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
deletion_type: Type of resource to delete ("session" or "observation")
|
||||
resource_id: ID of the resource to delete
|
||||
db_session: Optional database session. If provided, uses this session
|
||||
instead of creating a new one. The caller is responsible for committing.
|
||||
"""
|
||||
|
||||
async def _do_enqueue(session: AsyncSession, should_commit: bool) -> None:
|
||||
deletion_record = create_deletion_record(
|
||||
workspace_name,
|
||||
deletion_type,
|
||||
resource_id,
|
||||
)
|
||||
|
||||
stmt = insert(QueueItem).returning(QueueItem)
|
||||
await session.execute(stmt, [deletion_record])
|
||||
|
||||
if should_commit:
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Enqueued deletion task: type=%s, resource_id=%s, workspace=%s",
|
||||
deletion_type,
|
||||
resource_id,
|
||||
workspace_name,
|
||||
)
|
||||
|
||||
try:
|
||||
if db_session is not None:
|
||||
# Use the provided session - caller is responsible for committing
|
||||
await _do_enqueue(db_session, should_commit=False)
|
||||
else:
|
||||
# Create a new session and commit
|
||||
async with tracked_db("deletion_enqueue") as new_session:
|
||||
await _do_enqueue(new_session, should_commit=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to enqueue deletion task!")
|
||||
if settings.SENTRY.ENABLED:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.capture_exception(e)
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from src.dreamer.dream_scheduler import (
|
|||
set_dream_scheduler,
|
||||
)
|
||||
from src.models import QueueItem
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.sentry import initialize_sentry
|
||||
from src.utils.work_unit import parse_work_unit_key
|
||||
from src.webhooks.events import (
|
||||
|
|
@ -423,6 +424,7 @@ class QueueManager:
|
|||
(
|
||||
messages_context,
|
||||
items_to_process,
|
||||
message_level_configuration,
|
||||
) = await self.get_queue_item_batch(
|
||||
work_unit.task_type, work_unit_key, ownership.aqs_id
|
||||
)
|
||||
|
|
@ -438,6 +440,7 @@ class QueueManager:
|
|||
try:
|
||||
await process_representation_batch(
|
||||
messages_context,
|
||||
message_level_configuration,
|
||||
observer=work_unit.observer,
|
||||
observed=work_unit.observed,
|
||||
)
|
||||
|
|
@ -574,7 +577,7 @@ class QueueManager:
|
|||
task_type: str,
|
||||
work_unit_key: str,
|
||||
aqs_id: str,
|
||||
) -> tuple[list[models.Message], list[QueueItem]]:
|
||||
) -> tuple[list[models.Message], list[QueueItem], ResolvedConfiguration | None]:
|
||||
"""
|
||||
Representation-only: returns a tuple of (messages_context, items_to_process).
|
||||
- messages_context: unique Message rows (conversation turns) forming the context window
|
||||
|
|
@ -598,7 +601,7 @@ class QueueManager:
|
|||
if not ownership_check.scalar_one_or_none():
|
||||
# Worker lost ownership, return empty
|
||||
await db.commit()
|
||||
return [], []
|
||||
return [], [], None
|
||||
|
||||
# Step 2: Build a single SQL query that:
|
||||
# 1. Finds the earliest unprocessed message for this work_unit_key
|
||||
|
|
@ -670,7 +673,7 @@ class QueueManager:
|
|||
rows = result.all()
|
||||
if not rows:
|
||||
await db.commit()
|
||||
return [], []
|
||||
return [], [], None
|
||||
|
||||
messages_context: list[models.Message] = []
|
||||
items_to_process: list[QueueItem] = []
|
||||
|
|
@ -682,6 +685,33 @@ class QueueManager:
|
|||
if qi is not None:
|
||||
items_to_process.append(qi)
|
||||
|
||||
if items_to_process:
|
||||
# Enforce homogeneous peer_card_config in the batch
|
||||
# We stop collecting items as soon as we encounter a different configuration
|
||||
payload = items_to_process[0].payload
|
||||
|
||||
raw_config = payload.get("configuration")
|
||||
if raw_config is None:
|
||||
resolved_config = None
|
||||
else:
|
||||
resolved_config = ResolvedConfiguration.model_validate(raw_config)
|
||||
|
||||
valid_items: list[QueueItem] = []
|
||||
for item in items_to_process:
|
||||
item_raw_config = item.payload.get("configuration")
|
||||
if item_raw_config is None:
|
||||
item_config = None
|
||||
else:
|
||||
item_config = ResolvedConfiguration.model_validate(
|
||||
item_raw_config
|
||||
)
|
||||
if item_config != resolved_config:
|
||||
break
|
||||
valid_items.append(item)
|
||||
items_to_process = valid_items
|
||||
else:
|
||||
resolved_config = None
|
||||
|
||||
if items_to_process:
|
||||
max_queue_item_message_id = max(
|
||||
[
|
||||
|
|
@ -696,7 +726,7 @@ class QueueManager:
|
|||
|
||||
await db.commit()
|
||||
|
||||
return messages_context, items_to_process
|
||||
return messages_context, items_to_process, resolved_config
|
||||
|
||||
async def mark_queue_items_as_processed(
|
||||
self, items: list[QueueItem], work_unit_key: str
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import logging
|
||||
|
||||
from src.utils.queue_payload import DreamPayload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def process_agent_dream(payload: DreamPayload, workspace_name: str) -> None:
|
||||
"""
|
||||
Process an agent dream task.
|
||||
|
||||
Args:
|
||||
payload: The dream task payload containing workspace, peer, and dream type information
|
||||
"""
|
||||
logger.info(
|
||||
f"Processing agent dream for {workspace_name}/{payload.observer}/{payload.observed}"
|
||||
)
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
import logging
|
||||
from inspect import cleandoc as c
|
||||
|
||||
from sqlalchemy import delete
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import ResourceNotFoundException
|
||||
from src.utils.clients import honcho_llm_call
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
from src.utils.logging import conditional_observe
|
||||
from src.utils.queue_payload import DreamPayload
|
||||
from src.utils.representation import (
|
||||
ExplicitObservation,
|
||||
Representation,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def consolidation_prompt(
|
||||
representation: Representation,
|
||||
) -> str:
|
||||
"""
|
||||
Generate the prompt for user representation consolidation.
|
||||
|
||||
Args:
|
||||
representation: The user representation to consolidate
|
||||
|
||||
Returns:
|
||||
A prompt string for the LLM to consolidate the representation
|
||||
"""
|
||||
representation_as_json = representation.model_dump_json(indent=2)
|
||||
|
||||
return c(
|
||||
f"""
|
||||
You are an agent that consolidates observations about an entity. You will be presented with a list of EXPLICIT and DEDUCTIVE observations. **Reduce** the number of observations, if possible, by combining similar observations. **ONLY** include information that is **GIVEN**. Create the highest-quality observations with the given information. Observations must always be maximally concise.
|
||||
|
||||
{representation_as_json}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@conditional_observe(name="[Dream] Consolidate Call")
|
||||
async def _consolidate_call(
|
||||
representation: Representation,
|
||||
) -> Representation:
|
||||
prompt = consolidation_prompt(representation)
|
||||
|
||||
response = await honcho_llm_call(
|
||||
llm_settings=settings.DREAM,
|
||||
prompt=prompt,
|
||||
max_tokens=settings.DREAM.MAX_OUTPUT_TOKENS,
|
||||
track_name="Dream Call",
|
||||
response_model=Representation,
|
||||
enable_retry=True,
|
||||
retry_attempts=3,
|
||||
)
|
||||
|
||||
return response.content
|
||||
|
||||
|
||||
async def process_consolidate_dream(payload: DreamPayload, workspace_name: str) -> None:
|
||||
"""
|
||||
Process a consolidation dream task.
|
||||
|
||||
Consolidation means taking all the documents in a collection and merging
|
||||
similar observations into a single, best-quality observation document.
|
||||
"""
|
||||
|
||||
logger.info(
|
||||
"Starting consolidate dream for workspace=%s, observer=%s, observed=%s",
|
||||
workspace_name,
|
||||
payload.observer,
|
||||
payload.observed,
|
||||
)
|
||||
|
||||
# grab 100 recent documents in the collection
|
||||
# in the future, we can perform clustering on documents by semantic similarity and do
|
||||
# multiple clusters at once. for now, can just sample documents and do what we can.
|
||||
async with tracked_db("dream_consolidate") as db:
|
||||
# First verify the collection exists
|
||||
try:
|
||||
collection = await crud.get_collection(
|
||||
db,
|
||||
workspace_name,
|
||||
observer=payload.observer,
|
||||
observed=payload.observed,
|
||||
)
|
||||
logger.debug(
|
||||
"Found collection id=%s for workspace=%s, observer=%s, observed=%s",
|
||||
collection.id,
|
||||
workspace_name,
|
||||
payload.observer,
|
||||
payload.observed,
|
||||
)
|
||||
except ResourceNotFoundException:
|
||||
logger.warning(
|
||||
"Collection does not exist for workspace=%s, observer=%s, observed=%s",
|
||||
workspace_name,
|
||||
payload.observer,
|
||||
payload.observed,
|
||||
)
|
||||
return
|
||||
|
||||
documents_query = crud.get_all_documents(
|
||||
workspace_name,
|
||||
observer=payload.observer,
|
||||
observed=payload.observed,
|
||||
limit=100,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Executing document query: %s",
|
||||
str(documents_query.compile(compile_kwargs={"literal_binds": True})),
|
||||
)
|
||||
|
||||
result = await db.execute(documents_query)
|
||||
documents = result.scalars().all()
|
||||
|
||||
if not documents:
|
||||
return
|
||||
|
||||
logger.info("consolidating %d documents", len(documents))
|
||||
|
||||
# Pre-calculate data structures needed for processing so we don't need attached objects
|
||||
cluster_representation = Representation.from_documents(documents)
|
||||
document_ids = [doc.id for doc in documents]
|
||||
total_times_derived = sum(doc.times_derived for doc in documents)
|
||||
|
||||
# We treat all fetched documents as a single cluster for now
|
||||
clusters = [(cluster_representation, document_ids, total_times_derived)]
|
||||
|
||||
# for each cluster, call llm to consolidate the representation if possible
|
||||
for representation, doc_ids, times_derived in clusters:
|
||||
await _consolidate_cluster(
|
||||
representation,
|
||||
doc_ids,
|
||||
times_derived,
|
||||
workspace_name,
|
||||
observer=payload.observer,
|
||||
observed=payload.observed,
|
||||
)
|
||||
|
||||
|
||||
async def _consolidate_cluster(
|
||||
representation: Representation,
|
||||
document_ids: list[str],
|
||||
total_times_derived: int,
|
||||
workspace_name: str,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
) -> None:
|
||||
"""
|
||||
Consolidate a cluster of documents, treated as a Representation, into a smaller one.
|
||||
Removes old documents and replaces them with consolidated versions while preserving metadata.
|
||||
"""
|
||||
if len(document_ids) <= 1:
|
||||
logger.info(
|
||||
"Cluster has %d documents, skipping consolidation", len(document_ids)
|
||||
)
|
||||
return
|
||||
|
||||
logger.info("unconsolidated representation:\n%s", representation)
|
||||
|
||||
consolidated_representation = await _consolidate_call(representation)
|
||||
logger.info("consolidated representation:\n%s", consolidated_representation)
|
||||
|
||||
new_documents = [
|
||||
*consolidated_representation.explicit,
|
||||
*consolidated_representation.deductive,
|
||||
]
|
||||
|
||||
if not new_documents:
|
||||
return
|
||||
|
||||
# Collect all contents for batch embedding
|
||||
contents: list[str] = []
|
||||
for obs in new_documents:
|
||||
if isinstance(obs, ExplicitObservation):
|
||||
contents.append(obs.content)
|
||||
else:
|
||||
contents.append(obs.conclusion)
|
||||
|
||||
# Batch embed all contents at once for better performance
|
||||
embeddings = await embedding_client.simple_batch_embed(contents)
|
||||
|
||||
documents_to_create: list[schemas.DocumentCreate] = []
|
||||
|
||||
for i, obs in enumerate(new_documents):
|
||||
if isinstance(obs, ExplicitObservation):
|
||||
content = obs.content
|
||||
level = "explicit"
|
||||
premises = None
|
||||
else:
|
||||
content = obs.conclusion
|
||||
level = "deductive"
|
||||
premises = obs.premises
|
||||
# NOTE: other kinds of observations here in the future
|
||||
|
||||
metadata = schemas.DocumentMetadata(
|
||||
message_ids=obs.message_ids,
|
||||
message_created_at=format_datetime_utc(obs.created_at),
|
||||
premises=premises,
|
||||
)
|
||||
|
||||
documents_to_create.append(
|
||||
schemas.DocumentCreate(
|
||||
content=content,
|
||||
session_name=obs.session_name,
|
||||
level=level,
|
||||
times_derived=total_times_derived,
|
||||
metadata=metadata,
|
||||
embedding=embeddings[i],
|
||||
)
|
||||
)
|
||||
|
||||
async with tracked_db("dream_consolidate_write") as db:
|
||||
# bulk create documents
|
||||
await crud.create_documents(
|
||||
db,
|
||||
documents_to_create,
|
||||
workspace_name,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
||||
# delete old documents
|
||||
await db.execute(
|
||||
delete(models.Document).where(models.Document.id.in_(document_ids))
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"consolidated %d documents into %d new documents",
|
||||
len(document_ids),
|
||||
len(new_documents),
|
||||
)
|
||||
|
|
@ -5,13 +5,13 @@ from logging import getLogger
|
|||
from typing import Any
|
||||
|
||||
import sentry_sdk
|
||||
from sqlalchemy import func, insert, select, update
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.utils.queue_payload import create_dream_payload
|
||||
from src.schemas import DreamType
|
||||
from src.utils.work_unit import construct_work_unit_key, parse_work_unit_key
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
|
@ -47,17 +47,21 @@ def get_affected_dream_keys(message: dict[str, Any]) -> list[str]:
|
|||
if not workspace_name or not peer_name:
|
||||
return []
|
||||
|
||||
# Generate dream work unit key for this peer's collection
|
||||
dream_key = construct_work_unit_key(
|
||||
workspace_name,
|
||||
{
|
||||
"task_type": "dream",
|
||||
"observer": peer_name,
|
||||
"observed": peer_name,
|
||||
},
|
||||
)
|
||||
# Generate dream work unit keys for each enabled dream type
|
||||
dream_keys: list[str] = []
|
||||
for dream_type in settings.DREAM.ENABLED_TYPES:
|
||||
dream_key = construct_work_unit_key(
|
||||
workspace_name,
|
||||
{
|
||||
"task_type": "dream",
|
||||
"observer": peer_name,
|
||||
"observed": peer_name,
|
||||
"dream_type": dream_type,
|
||||
},
|
||||
)
|
||||
dream_keys.append(dream_key)
|
||||
|
||||
return [dream_key]
|
||||
return dream_keys
|
||||
|
||||
|
||||
class DreamScheduler:
|
||||
|
|
@ -87,6 +91,7 @@ class DreamScheduler:
|
|||
workspace_name: str,
|
||||
document_count: int,
|
||||
delay_minutes: int,
|
||||
dream_type: DreamType,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
|
|
@ -104,6 +109,7 @@ class DreamScheduler:
|
|||
workspace_name,
|
||||
document_count,
|
||||
delay_minutes,
|
||||
dream_type,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
|
@ -128,6 +134,7 @@ class DreamScheduler:
|
|||
workspace_name: str,
|
||||
document_count: int,
|
||||
delay_minutes: int,
|
||||
dream_type: DreamType,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
|
|
@ -139,10 +146,10 @@ class DreamScheduler:
|
|||
if await self._should_execute_dream(
|
||||
workspace_name, observer=observer, observed=observed
|
||||
):
|
||||
await self._execute_dream(
|
||||
work_unit_key,
|
||||
await self.execute_dream(
|
||||
workspace_name,
|
||||
document_count,
|
||||
dream_type,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
|
@ -182,63 +189,25 @@ class DreamScheduler:
|
|||
|
||||
return True
|
||||
|
||||
async def _execute_dream(
|
||||
async def execute_dream(
|
||||
self,
|
||||
work_unit_key: str,
|
||||
workspace_name: str,
|
||||
document_count: int,
|
||||
dream_type: DreamType,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
) -> None:
|
||||
"""Execute the dream by enqueueing it and updating collection metadata."""
|
||||
dream_payload = create_dream_payload(
|
||||
dream_type="consolidate",
|
||||
# Import here to avoid circular dependency
|
||||
from src.deriver.enqueue import enqueue_dream
|
||||
|
||||
await enqueue_dream(
|
||||
workspace_name,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
||||
async with tracked_db("dream_execute") as db:
|
||||
dream_record = {
|
||||
"work_unit_key": work_unit_key,
|
||||
"payload": dream_payload,
|
||||
"session_id": None,
|
||||
"task_type": "dream",
|
||||
"workspace_name": workspace_name,
|
||||
"message_id": None, # Dreams don't have a message_id
|
||||
}
|
||||
|
||||
await db.execute(insert(models.QueueItem), [dream_record])
|
||||
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
stmt = (
|
||||
update(models.Collection)
|
||||
.where(
|
||||
models.Collection.workspace_name == workspace_name,
|
||||
models.Collection.observer == observer,
|
||||
models.Collection.observed == observed,
|
||||
)
|
||||
.values(
|
||||
internal_metadata=models.Collection.internal_metadata.op("||")(
|
||||
{
|
||||
"dream": {
|
||||
"last_dream_document_count": document_count,
|
||||
"last_dream_at": now_iso,
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"Enqueued dream task",
|
||||
extra={
|
||||
"workspace_name": workspace_name,
|
||||
"observer": observer,
|
||||
"observed": observed,
|
||||
},
|
||||
dream_type=dream_type,
|
||||
document_count=document_count,
|
||||
)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
|
|
@ -326,33 +295,38 @@ async def check_and_schedule_dream(
|
|||
|
||||
dream_scheduler = get_dream_scheduler()
|
||||
if dream_scheduler:
|
||||
collection_work_unit_key = construct_work_unit_key(
|
||||
collection.workspace_name,
|
||||
{
|
||||
"task_type": "dream",
|
||||
"observer": collection.observer,
|
||||
"observed": collection.observed,
|
||||
},
|
||||
)
|
||||
|
||||
await dream_scheduler.schedule_dream(
|
||||
collection_work_unit_key,
|
||||
collection.workspace_name,
|
||||
current_document_count,
|
||||
settings.DREAM.IDLE_TIMEOUT_MINUTES,
|
||||
observer=collection.observer,
|
||||
observed=collection.observed,
|
||||
)
|
||||
logger.info(
|
||||
"Scheduled dream",
|
||||
extra={
|
||||
"workspace_name": collection.workspace_name,
|
||||
"observer": collection.observer,
|
||||
"observed": collection.observed,
|
||||
"documents_since_last_dream": documents_since_last_dream,
|
||||
"document_threshold": settings.DREAM.DOCUMENT_THRESHOLD,
|
||||
},
|
||||
)
|
||||
enabled_dream_types = settings.DREAM.ENABLED_TYPES
|
||||
for dream_type in enabled_dream_types:
|
||||
# Include dream_type in key so each dream type can be tracked independently
|
||||
dream_work_unit_key = construct_work_unit_key(
|
||||
collection.workspace_name,
|
||||
{
|
||||
"task_type": "dream",
|
||||
"observer": collection.observer,
|
||||
"observed": collection.observed,
|
||||
"dream_type": dream_type,
|
||||
},
|
||||
)
|
||||
await dream_scheduler.schedule_dream(
|
||||
dream_work_unit_key,
|
||||
collection.workspace_name,
|
||||
current_document_count,
|
||||
settings.DREAM.IDLE_TIMEOUT_MINUTES,
|
||||
dream_type=DreamType(dream_type),
|
||||
observer=collection.observer,
|
||||
observed=collection.observed,
|
||||
)
|
||||
logger.info(
|
||||
"Scheduled dream",
|
||||
extra={
|
||||
"workspace_name": collection.workspace_name,
|
||||
"observer": collection.observer,
|
||||
"observed": collection.observed,
|
||||
"documents_since_last_dream": documents_since_last_dream,
|
||||
"document_threshold": settings.DREAM.DOCUMENT_THRESHOLD,
|
||||
"dream_type": dream_type,
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
import logging
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sentry_sdk
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.dreamer.prompts import consolidation_prompt
|
||||
from src.embedding_client import embedding_client
|
||||
from src.utils.clients import honcho_llm_call
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
from src.utils.logging import conditional_observe
|
||||
from src.dreamer.agent import process_agent_dream
|
||||
from src.dreamer.consolidate import process_consolidate_dream
|
||||
from src.schemas import DreamType
|
||||
from src.utils.queue_payload import DreamPayload
|
||||
from src.utils.representation import (
|
||||
ExplicitObservation,
|
||||
Representation,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -33,13 +23,18 @@ async def process_dream(
|
|||
payload: The dream task payload containing workspace, peer, and dream type information
|
||||
"""
|
||||
logger.info(
|
||||
f"Processing dream task: {payload.dream_type} for {workspace_name}/{payload.observer}/{payload.observed}"
|
||||
f"""
|
||||
(っ- ‸ - ς)ᶻ z 𐰁 ᶻ z 𐰁 ᶻ z 𐰁\n
|
||||
DREAM: {payload.dream_type} documents for {workspace_name}/{payload.observer}/{payload.observed}\n
|
||||
𐰁 z ᶻ 𐰁 z ᶻ 𐰁 z ᶻ(っ- ‸ - ς)"""
|
||||
)
|
||||
|
||||
try:
|
||||
if payload.dream_type == "consolidate":
|
||||
await _process_consolidate_dream(payload, workspace_name)
|
||||
## TODO other dream types
|
||||
match payload.dream_type:
|
||||
case DreamType.CONSOLIDATE:
|
||||
await process_consolidate_dream(payload, workspace_name)
|
||||
case DreamType.AGENT:
|
||||
await process_agent_dream(payload, workspace_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
|
@ -49,149 +44,3 @@ async def process_dream(
|
|||
if settings.SENTRY.ENABLED:
|
||||
sentry_sdk.capture_exception(e)
|
||||
# Don't re-raise - we want to mark the dream task as processed even if it fails
|
||||
|
||||
|
||||
async def _process_consolidate_dream(
|
||||
payload: DreamPayload, workspace_name: str
|
||||
) -> None:
|
||||
"""
|
||||
Process a consolidation dream task.
|
||||
|
||||
Consolidation means taking all the documents in a collection and merging
|
||||
similar observations into a single, best-quality observation document.
|
||||
|
||||
TODO: need to determine a way to do this on a subset of documents since
|
||||
collections will grow very large.
|
||||
"""
|
||||
logger.info(
|
||||
f"""
|
||||
(っ- ‸ - ς)ᶻ z 𐰁 ᶻ z 𐰁 ᶻ z 𐰁\n
|
||||
DREAM: consolidating documents for {workspace_name}/{payload.observer}/{payload.observed}\n
|
||||
𐰁 z ᶻ 𐰁 z ᶻ 𐰁 z ᶻ(っ- ‸ - ς)"""
|
||||
)
|
||||
|
||||
# get all documents in the collection
|
||||
async with tracked_db("dream_consolidate") as db:
|
||||
documents = await crud.get_all_documents(
|
||||
db,
|
||||
workspace_name,
|
||||
observer=payload.observer,
|
||||
observed=payload.observed,
|
||||
)
|
||||
|
||||
logger.info("found %d documents to consolidate", len(documents))
|
||||
|
||||
# TODO: create clusters of documents based on cosine similarity
|
||||
# clusters = await create_document_clusters(documents)
|
||||
|
||||
# logger.info("created %d clusters", len(clusters))
|
||||
clusters = [documents]
|
||||
|
||||
# for each cluster, call llm to consolidate the representation if possible
|
||||
for cluster in clusters:
|
||||
await _consolidate_cluster(
|
||||
cluster,
|
||||
workspace_name,
|
||||
db,
|
||||
observer=payload.observer,
|
||||
observed=payload.observed,
|
||||
)
|
||||
|
||||
|
||||
async def _consolidate_cluster(
|
||||
cluster: Sequence[models.Document],
|
||||
workspace_name: str,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
) -> None:
|
||||
"""
|
||||
Consolidate a cluster of documents, treated as a Representation, into a smaller one.
|
||||
Removes old documents and replaces them with consolidated versions while preserving metadata.
|
||||
"""
|
||||
if len(cluster) <= 1:
|
||||
logger.info("Cluster has %d documents, skipping consolidation", len(cluster))
|
||||
return
|
||||
|
||||
cluster_representation = Representation.from_documents(cluster)
|
||||
logger.info("unconsolidated representation:\n%s", cluster_representation)
|
||||
|
||||
consolidated_representation = await consolidate_call(cluster_representation)
|
||||
logger.info("consolidated representation:\n%s", consolidated_representation)
|
||||
|
||||
# TODO: less hacky preservation of times_derived
|
||||
total_times_derived = sum(doc.times_derived for doc in cluster)
|
||||
|
||||
new_documents = [
|
||||
*consolidated_representation.explicit,
|
||||
*consolidated_representation.deductive,
|
||||
]
|
||||
|
||||
documents_to_create: list[schemas.DocumentCreate] = []
|
||||
|
||||
for obs in new_documents:
|
||||
if isinstance(obs, ExplicitObservation):
|
||||
content = obs.content
|
||||
level = "explicit"
|
||||
premises = None
|
||||
else:
|
||||
content = obs.conclusion
|
||||
level = "deductive"
|
||||
premises = obs.premises
|
||||
# NOTE: other kinds of observations here in the future
|
||||
|
||||
metadata = schemas.DocumentMetadata(
|
||||
message_ids=obs.message_ids,
|
||||
message_created_at=format_datetime_utc(obs.created_at),
|
||||
premises=premises,
|
||||
)
|
||||
|
||||
embedding = await embedding_client.embed(content)
|
||||
|
||||
documents_to_create.append(
|
||||
schemas.DocumentCreate(
|
||||
content=content,
|
||||
session_name=obs.session_name,
|
||||
level=level,
|
||||
times_derived=total_times_derived,
|
||||
metadata=metadata,
|
||||
embedding=embedding,
|
||||
)
|
||||
)
|
||||
|
||||
# bulk create documents
|
||||
await crud.create_documents(
|
||||
db, documents_to_create, workspace_name, observer=observer, observed=observed
|
||||
)
|
||||
|
||||
# delete old documents
|
||||
for doc in cluster:
|
||||
await db.delete(doc)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"consolidated %d documents into %d new documents",
|
||||
len(cluster),
|
||||
len(new_documents),
|
||||
)
|
||||
|
||||
|
||||
@conditional_observe(name="[Dream] Consolidate Call")
|
||||
async def consolidate_call(
|
||||
representation: Representation,
|
||||
) -> Representation:
|
||||
prompt = consolidation_prompt(representation)
|
||||
|
||||
response = await honcho_llm_call(
|
||||
llm_settings=settings.DREAM,
|
||||
prompt=prompt,
|
||||
max_tokens=settings.DREAM.MAX_OUTPUT_TOKENS,
|
||||
track_name="Dream Call",
|
||||
response_model=Representation,
|
||||
enable_retry=True,
|
||||
retry_attempts=3,
|
||||
)
|
||||
|
||||
return response.content
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
from inspect import cleandoc as c
|
||||
|
||||
from src.utils.representation import Representation
|
||||
|
||||
|
||||
def consolidation_prompt(
|
||||
representation: Representation,
|
||||
) -> str:
|
||||
"""
|
||||
Generate the prompt for user representation consolidation.
|
||||
|
||||
Args:
|
||||
representation: The user representation to consolidate
|
||||
|
||||
Returns:
|
||||
A consolidated user representation
|
||||
"""
|
||||
representation_as_json = representation.model_dump_json(indent=2)
|
||||
|
||||
return c(
|
||||
f"""
|
||||
You are an agent that consolidates observations about an entity. You will be presented with a list of EXPLICIT and DEDUCTIVE observations. **Reduce** the number of observations, if possible, by combining similar observations. **ONLY** include information that is **GIVEN**. Create the highest-quality observations with the given information. Observations must always be maximally concise.
|
||||
|
||||
{representation_as_json}
|
||||
"""
|
||||
)
|
||||
|
|
@ -23,6 +23,7 @@ from src.exceptions import HonchoException
|
|||
from src.routers import (
|
||||
keys,
|
||||
messages,
|
||||
observations,
|
||||
peers,
|
||||
sessions,
|
||||
webhooks,
|
||||
|
|
@ -138,7 +139,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.4.3",
|
||||
version="2.5.0",
|
||||
contact={
|
||||
"name": "Plastic Labs",
|
||||
"url": "https://honcho.dev",
|
||||
|
|
@ -173,6 +174,7 @@ app.include_router(workspaces.router, prefix="/v2")
|
|||
app.include_router(peers.router, prefix="/v2")
|
||||
app.include_router(sessions.router, prefix="/v2")
|
||||
app.include_router(messages.router, prefix="/v2")
|
||||
app.include_router(observations.router, prefix="/v2")
|
||||
app.include_router(keys.router, prefix="/v2")
|
||||
app.include_router(webhooks.router, prefix="/v2")
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,48 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
|
||||
async def parse_upload_form(peer_id: str = Form(...)) -> schemas.MessageUploadCreate:
|
||||
async def parse_upload_form(
|
||||
peer_id: str = Form(...),
|
||||
metadata: str | None = Form(None),
|
||||
configuration: str | None = Form(None),
|
||||
created_at: str | None = Form(None),
|
||||
) -> schemas.MessageUploadCreate:
|
||||
"""Parse form data for file upload requests"""
|
||||
return schemas.MessageUploadCreate(peer_id=peer_id)
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
parsed_metadata = None
|
||||
if metadata:
|
||||
try:
|
||||
parsed_metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse metadata JSON: {metadata}")
|
||||
parsed_metadata = None
|
||||
|
||||
parsed_configuration = None
|
||||
if configuration:
|
||||
try:
|
||||
parsed_configuration = json.loads(configuration)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse configuration JSON: {configuration}")
|
||||
parsed_configuration = None
|
||||
|
||||
parsed_created_at = None
|
||||
if created_at:
|
||||
try:
|
||||
parsed_created_at = datetime.fromisoformat(
|
||||
created_at.replace("Z", "+00:00")
|
||||
)
|
||||
except (ValueError, AttributeError):
|
||||
logger.warning(f"Failed to parse created_at: {created_at}")
|
||||
parsed_created_at = None
|
||||
|
||||
return schemas.MessageUploadCreate(
|
||||
peer_id=peer_id,
|
||||
metadata=parsed_metadata,
|
||||
configuration=parsed_configuration,
|
||||
created_at=parsed_created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=list[schemas.Message])
|
||||
|
|
@ -48,7 +87,7 @@ async def create_messages_for_session(
|
|||
session_id: str = Path(...),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Create messages for a session with JSON data (original functionality)."""
|
||||
"""Add new message(s) to a session."""
|
||||
try:
|
||||
created_messages = await crud.create_messages(
|
||||
db,
|
||||
|
|
@ -72,8 +111,11 @@ async def create_messages_for_session(
|
|||
"created_at": message.created_at,
|
||||
"message_public_id": message.public_id,
|
||||
"seq_in_session": message.seq_in_session,
|
||||
"configuration": original.configuration,
|
||||
}
|
||||
for message in created_messages
|
||||
for message, original in zip(
|
||||
created_messages, messages.messages, strict=True
|
||||
)
|
||||
]
|
||||
|
||||
# Enqueue all messages in one call
|
||||
|
|
@ -106,6 +148,9 @@ async def create_messages_with_file(
|
|||
all_message_data = await process_file_uploads_for_messages(
|
||||
file=file,
|
||||
peer_id=form_data.peer_id,
|
||||
metadata=form_data.metadata,
|
||||
configuration=form_data.configuration,
|
||||
created_at=form_data.created_at,
|
||||
)
|
||||
|
||||
# Create messages
|
||||
|
|
@ -136,6 +181,7 @@ async def create_messages_with_file(
|
|||
"created_at": message.created_at,
|
||||
"message_public_id": message.public_id,
|
||||
"seq_in_session": message.seq_in_session,
|
||||
"configuration": form_data.configuration,
|
||||
}
|
||||
for message in created_messages
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import logging
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import apaginate
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, schemas
|
||||
from src.dependencies import db
|
||||
from src.exceptions import ResourceNotFoundException, ValidationException
|
||||
from src.security import require_auth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/workspaces/{workspace_id}/observations",
|
||||
tags=["observations"],
|
||||
dependencies=[Depends(require_auth(workspace_name="workspace_id"))],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/list",
|
||||
response_model=Page[schemas.Observation],
|
||||
)
|
||||
async def list_observations(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
options: schemas.ObservationGet | None = Body(
|
||||
None, description="Filtering options for the observations list"
|
||||
),
|
||||
reverse: bool | None = Query(
|
||||
False, description="Whether to reverse the order of results"
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
List all observations using custom filters. Observations are listed by recency unless `reverse` is set to `true`.
|
||||
|
||||
Observations can be filtered by session_id, observer_id and observed_id using the filters parameter.
|
||||
"""
|
||||
try:
|
||||
filters = None
|
||||
if options and hasattr(options, "filters"):
|
||||
filters = options.filters
|
||||
if filters == {}:
|
||||
filters = None
|
||||
|
||||
stmt = crud.get_documents_with_filters(
|
||||
workspace_name=workspace_id,
|
||||
filters=filters,
|
||||
reverse=reverse or False,
|
||||
)
|
||||
|
||||
return await apaginate(db, stmt)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to list observations: {str(e)}")
|
||||
raise ResourceNotFoundException("Session not found") from e
|
||||
|
||||
|
||||
@router.post(
|
||||
"/query",
|
||||
response_model=list[schemas.Observation],
|
||||
)
|
||||
async def query_observations(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
body: schemas.ObservationQuery = Body(
|
||||
..., description="Semantic search parameters for observations"
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
) -> list[schemas.Observation]:
|
||||
"""
|
||||
Query observations using semantic search.
|
||||
|
||||
Performs vector similarity search on observations to find semantically relevant results.
|
||||
Observer and observed are required for semantic search and must be provided in filters.
|
||||
"""
|
||||
# Extract observer and observed from filters if provided
|
||||
observer = None
|
||||
observed = None
|
||||
if body.filters:
|
||||
observer = body.filters.get("observer") or body.filters.get("observer_id")
|
||||
observed = body.filters.get("observed") or body.filters.get("observed_id")
|
||||
|
||||
# If no observer/observed specified, we need to query across all session documents
|
||||
# For now, we'll require these to be specified for semantic search
|
||||
if not observer or not observed:
|
||||
raise ValidationException(
|
||||
"observer and observed must be specified for semantic search"
|
||||
)
|
||||
else:
|
||||
# Query specific observer/observed pair
|
||||
documents = await crud.query_documents(
|
||||
db,
|
||||
workspace_name=workspace_id,
|
||||
query=body.query,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
filters=body.filters,
|
||||
max_distance=body.distance,
|
||||
top_k=body.top_k,
|
||||
)
|
||||
return [schemas.Observation.model_validate(doc) for doc in documents]
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{observation_id}",
|
||||
)
|
||||
async def delete_observation(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
observation_id: str = Path(..., description="ID of the observation to delete"),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Delete a specific observation.
|
||||
|
||||
This permanently deletes the observation (document) from the theory-of-mind system.
|
||||
This action cannot be undone.
|
||||
"""
|
||||
try:
|
||||
await crud.delete_document_by_id(
|
||||
db,
|
||||
workspace_name=workspace_id,
|
||||
document_id=observation_id,
|
||||
)
|
||||
|
||||
logger.debug("Observation %s deleted successfully", observation_id)
|
||||
return {"message": "Observation deleted successfully"}
|
||||
except ResourceNotFoundException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to delete observation {observation_id}: {str(e)}")
|
||||
raise ResourceNotFoundException("Observation not found") from e
|
||||
|
|
@ -9,6 +9,7 @@ from fastapi_pagination.ext.sqlalchemy import apaginate
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, prometheus, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import db, tracked_db
|
||||
from src.dialectic import chat as dialectic_chat
|
||||
from src.exceptions import AuthenticationException, ResourceNotFoundException
|
||||
|
|
@ -253,6 +254,15 @@ async def get_working_representation(
|
|||
observer=peer_id,
|
||||
observed=options.target if options.target is not None else peer_id,
|
||||
session_name=options.session_id,
|
||||
include_semantic_query=options.search_query,
|
||||
semantic_search_top_k=options.search_top_k,
|
||||
semantic_search_max_distance=options.search_max_distance,
|
||||
include_most_derived=options.include_most_derived
|
||||
if options.include_most_derived is not None
|
||||
else False,
|
||||
max_observations=options.max_observations
|
||||
if options.max_observations is not None
|
||||
else settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
|
||||
)
|
||||
return {"representation": representation}
|
||||
except ValueError as e:
|
||||
|
|
@ -292,6 +302,136 @@ async def get_peer_card(
|
|||
return schemas.PeerCardResponse(peer_card=peer_card)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{peer_id}/card",
|
||||
response_model=schemas.PeerCardResponse,
|
||||
dependencies=[
|
||||
Depends(require_auth(workspace_name="workspace_id", peer_name="peer_id"))
|
||||
],
|
||||
)
|
||||
async def set_peer_card(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the observer peer"),
|
||||
peer_card_data: schemas.PeerCardSet = Body(
|
||||
..., description="Peer card data to set"
|
||||
),
|
||||
target: str | None = Query(
|
||||
None,
|
||||
description="The peer whose card to set. If not provided, sets the observer's own card",
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Set a peer card for a specific peer relationship.
|
||||
|
||||
Sets the peer card that the observer peer has for the target peer.
|
||||
If no target is specified, sets the observer's own peer card.
|
||||
"""
|
||||
# If no target specified, set the observer's own card
|
||||
observed = target if target is not None else peer_id
|
||||
|
||||
await crud.set_peer_card(
|
||||
db,
|
||||
workspace_id,
|
||||
peer_card=peer_card_data.peer_card,
|
||||
observer=peer_id,
|
||||
observed=observed,
|
||||
)
|
||||
|
||||
# Return the updated peer card
|
||||
peer_card = await crud.get_peer_card(
|
||||
db, workspace_id, observer=peer_id, observed=observed
|
||||
)
|
||||
return schemas.PeerCardResponse(peer_card=peer_card)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{peer_id}/context",
|
||||
response_model=schemas.PeerContext,
|
||||
dependencies=[
|
||||
Depends(require_auth(workspace_name="workspace_id", peer_name="peer_id"))
|
||||
],
|
||||
)
|
||||
async def get_peer_context(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the peer (observer)"),
|
||||
target: str | None = Query(
|
||||
None,
|
||||
description="The target peer to get context for. If not provided, returns the peer's own context (self-observation)",
|
||||
),
|
||||
search_query: str | None = Query(
|
||||
None,
|
||||
description="Optional query to curate the representation around semantic search results",
|
||||
),
|
||||
search_top_k: int | None = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Only used if `search_query` is provided. Number of semantic-search-retrieved observations to include",
|
||||
),
|
||||
search_max_distance: float | None = Query(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Only used if `search_query` is provided. Maximum distance for semantically relevant observations",
|
||||
),
|
||||
include_most_derived: bool = Query(
|
||||
default=True,
|
||||
description="Whether to include the most derived observations in the representation",
|
||||
),
|
||||
max_observations: int | None = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of observations to include in the representation",
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Get context for a peer, including their representation and peer card.
|
||||
|
||||
This endpoint returns the working representation and peer card for a peer.
|
||||
If a target is specified, returns the context for the target from the
|
||||
observer peer's perspective. If no target is specified, returns the
|
||||
peer's own context (self-observation).
|
||||
|
||||
This is useful for getting all the context needed about a peer without
|
||||
making multiple API calls.
|
||||
"""
|
||||
# If no target specified, get the peer's own context (self-observation)
|
||||
observed = target if target is not None else peer_id
|
||||
|
||||
try:
|
||||
# Get the working representation
|
||||
representation = await crud.get_working_representation(
|
||||
workspace_id,
|
||||
observer=peer_id,
|
||||
observed=observed,
|
||||
session_name=None, # Peer context is global, not session-scoped
|
||||
include_semantic_query=search_query,
|
||||
semantic_search_top_k=search_top_k,
|
||||
semantic_search_max_distance=search_max_distance,
|
||||
include_most_derived=include_most_derived,
|
||||
max_observations=max_observations
|
||||
if max_observations is not None
|
||||
else settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
|
||||
)
|
||||
|
||||
# Get the peer card
|
||||
peer_card = await crud.get_peer_card(
|
||||
db, workspace_id, observer=peer_id, observed=observed
|
||||
)
|
||||
|
||||
return schemas.PeerContext(
|
||||
peer_id=peer_id,
|
||||
target_id=observed,
|
||||
representation=representation,
|
||||
peer_card=peer_card,
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to get context for peer {peer_id}: {str(e)}")
|
||||
raise ResourceNotFoundException("Peer not found") from e
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{peer_id}/search",
|
||||
response_model=list[schemas.Message],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Response
|
||||
from fastapi_pagination import Page
|
||||
|
|
@ -9,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from src import config, crud, schemas
|
||||
from src.dependencies import db, tracked_db
|
||||
from src.deriver.enqueue import enqueue_deletion
|
||||
from src.exceptions import (
|
||||
AuthenticationException,
|
||||
ResourceNotFoundException,
|
||||
|
|
@ -34,6 +33,11 @@ async def _get_working_representation_task(
|
|||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
session_name: str | None,
|
||||
search_top_k: int | None,
|
||||
search_max_distance: float | None,
|
||||
include_most_derived: bool,
|
||||
max_observations: int | None,
|
||||
) -> Representation:
|
||||
"""
|
||||
Atomic task to get working representation using tracked_db.
|
||||
|
|
@ -43,16 +47,27 @@ async def _get_working_representation_task(
|
|||
last_message: Optional last message for semantic query
|
||||
observer: Name of the observer peer
|
||||
observed: Name of the observed peer
|
||||
session_name: Optional session to filter by
|
||||
search_top_k: Number of semantic-search-retrieved observations to include in the representation
|
||||
search_max_distance: Maximum distance to search for semantically relevant observations
|
||||
include_most_derived: Whether to include the most derived observations in the representation
|
||||
max_observations: Maximum number of observations to include in the representation
|
||||
|
||||
Returns:
|
||||
The working representation
|
||||
"""
|
||||
return await crud.get_working_representation(
|
||||
workspace_name=workspace_id,
|
||||
include_semantic_query=last_message,
|
||||
include_most_derived=True,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
session_name=session_name,
|
||||
include_semantic_query=last_message,
|
||||
semantic_search_top_k=search_top_k,
|
||||
semantic_search_max_distance=search_max_distance,
|
||||
include_most_derived=include_most_derived,
|
||||
max_observations=max_observations
|
||||
if max_observations is not None
|
||||
else config.settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -214,6 +229,7 @@ async def update_session(
|
|||
|
||||
@router.delete(
|
||||
"/{session_id}",
|
||||
status_code=202,
|
||||
dependencies=[
|
||||
Depends(require_auth(workspace_name="workspace_id", session_name="session_id"))
|
||||
],
|
||||
|
|
@ -223,12 +239,32 @@ async def delete_session(
|
|||
session_id: str = Path(..., description="ID of the session to delete"),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Delete a session by marking it as inactive"""
|
||||
"""
|
||||
Delete a session and all associated data.
|
||||
|
||||
The session is marked as inactive immediately and returns 202 Accepted. The actual
|
||||
deletion of all related data (messages, embeddings, documents, etc.) happens
|
||||
asynchronously via the queue with retry support.
|
||||
|
||||
This action cannot be undone.
|
||||
"""
|
||||
try:
|
||||
await crud.delete_session(
|
||||
db, workspace_name=workspace_id, session_name=session_id
|
||||
# Mark session as inactive immediately (fast operation)
|
||||
session = await crud.get_session(db, session_id, workspace_id)
|
||||
session.is_active = False
|
||||
|
||||
# Enqueue deletion task for processing with retry support
|
||||
# Pass db session so it's all in one transaction
|
||||
await enqueue_deletion(
|
||||
workspace_name=workspace_id,
|
||||
deletion_type="session",
|
||||
resource_id=session_id,
|
||||
db_session=db,
|
||||
)
|
||||
logger.debug("Session %s deleted successfully", session_id)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.debug("Session %s marked as inactive, deletion enqueued", session_id)
|
||||
return {"message": "Session deleted successfully"}
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to delete session {session_id}: {str(e)}")
|
||||
|
|
@ -481,6 +517,32 @@ async def get_session_context(
|
|||
None,
|
||||
description="A peer to get context for. If given, response will attempt to include representation and card from the perspective of that peer. Must be provided with `peer_target`.",
|
||||
),
|
||||
limit_to_session: bool = Query(
|
||||
default=False,
|
||||
description="Only used if `last_message` is provided. Whether to limit the representation to the session (as opposed to everything known about the target peer)",
|
||||
),
|
||||
search_top_k: int | None = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Only used if `last_message` is provided. The number of semantic-search-retrieved observations to include in the representation",
|
||||
),
|
||||
search_max_distance: float | None = Query(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Only used if `last_message` is provided. The maximum distance to search for semantically relevant observations",
|
||||
),
|
||||
include_most_derived: bool = Query(
|
||||
default=False,
|
||||
description="Only used if `last_message` is provided. Whether to include the most derived observations in the representation",
|
||||
),
|
||||
max_observations: int | None = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Only used if `last_message` is provided. The maximum number of observations to include in the representation",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Produce a context object from the session. The caller provides an optional token limit which the entire context must fit into.
|
||||
|
|
@ -511,24 +573,20 @@ async def get_session_context(
|
|||
observer = peer_perspective or peer_target
|
||||
observed = peer_target
|
||||
|
||||
# Run representation and card tasks in parallel
|
||||
representation, card = await asyncio.gather(
|
||||
_get_working_representation_task(
|
||||
workspace_id, last_message, observer=observer, observed=observed
|
||||
),
|
||||
_get_peer_card_task(workspace_id, observer=observer, observed=observed),
|
||||
return_exceptions=True,
|
||||
# Run representation and card tasks sequentially to avoid event loop issues
|
||||
# with tracked_db creating separate database sessions
|
||||
representation = await _get_working_representation_task(
|
||||
workspace_id,
|
||||
last_message,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
session_name=session_id if limit_to_session else None,
|
||||
search_top_k=search_top_k,
|
||||
search_max_distance=search_max_distance,
|
||||
include_most_derived=include_most_derived,
|
||||
max_observations=max_observations,
|
||||
)
|
||||
|
||||
# Handle any exceptions from the parallel tasks
|
||||
if isinstance(representation, Exception):
|
||||
raise representation
|
||||
if isinstance(card, Exception):
|
||||
raise card
|
||||
|
||||
# At this point, we know the types are correct - cast to help type checker
|
||||
representation = cast(Representation, representation)
|
||||
card = cast(list[str] | None, card)
|
||||
card = await _get_peer_card_task(workspace_id, observer=observer, observed=observed)
|
||||
|
||||
# adjust token limit downward to account for approximate token count of representation and card
|
||||
# TODO determine if this impacts performance too much
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ import logging
|
|||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import apaginate
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, schemas
|
||||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import db
|
||||
from src.deriver.enqueue import enqueue_dream
|
||||
from src.exceptions import AuthenticationException
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.utils.search import search
|
||||
|
|
@ -152,3 +155,55 @@ async def get_deriver_status(
|
|||
except ValueError as e:
|
||||
logger.warning(f"Invalid request parameters: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{workspace_id}/trigger_dream",
|
||||
status_code=204,
|
||||
dependencies=[Depends(require_auth(workspace_name="workspace_id"))],
|
||||
)
|
||||
async def trigger_dream(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
request: schemas.TriggerDreamRequest = Body(
|
||||
..., description="Dream trigger parameters"
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Manually trigger a dream task immediately for a specific collection.
|
||||
|
||||
This endpoint bypasses all automatic dream conditions (document threshold,
|
||||
minimum hours between dreams) and executes the dream task immediately without delay.
|
||||
"""
|
||||
# Check if dreams are enabled
|
||||
if not settings.DREAM.ENABLED:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Dreams are not enabled in the system configuration",
|
||||
)
|
||||
|
||||
# Default observed to observer if not provided
|
||||
observer = request.observer
|
||||
observed = request.observed if request.observed is not None else request.observer
|
||||
dream_type = request.dream_type
|
||||
|
||||
# Count documents in the collection
|
||||
count_stmt = select(func.count(models.Document.id)).where(
|
||||
models.Document.workspace_name == workspace_id,
|
||||
models.Document.observer == observer,
|
||||
models.Document.observed == observed,
|
||||
)
|
||||
document_count = int(await db.scalar(count_stmt) or 0)
|
||||
|
||||
# Enqueue the dream task for immediate processing
|
||||
await enqueue_dream(
|
||||
workspace_id,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
dream_type=dream_type,
|
||||
document_count=document_count,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Manually triggered dream: {dream_type.value} for {workspace_id}/{observer}/{observed}"
|
||||
)
|
||||
|
|
|
|||
302
src/schemas.py
302
src/schemas.py
|
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
import ipaddress
|
||||
from enum import Enum
|
||||
from typing import Annotated, Any, Self
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
|
@ -20,6 +21,171 @@ from src.utils.types import DocumentLevel
|
|||
RESOURCE_NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
|
||||
|
||||
|
||||
class DreamType(str, Enum):
|
||||
"""Types of dreams that can be triggered."""
|
||||
|
||||
CONSOLIDATE = "consolidate"
|
||||
AGENT = "agent"
|
||||
|
||||
|
||||
class DeriverConfiguration(BaseModel):
|
||||
enabled: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable deriver functionality.",
|
||||
)
|
||||
custom_instructions: str | None = Field(
|
||||
default=None,
|
||||
description="TODO: currently unused. Custom instructions to use for the deriver on this workspace/session/message.",
|
||||
)
|
||||
|
||||
|
||||
class PeerCardConfiguration(BaseModel):
|
||||
use: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to use peer card related to this peer during deriver process.",
|
||||
)
|
||||
create: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to generate peer card based on content.",
|
||||
)
|
||||
|
||||
|
||||
class SummaryConfiguration(BaseModel):
|
||||
enabled: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable summary functionality.",
|
||||
)
|
||||
messages_per_short_summary: int | None = Field(
|
||||
default=None,
|
||||
ge=10,
|
||||
description="Number of messages per short summary. Must be positive, greater than or equal to 10, and less than messages_per_long_summary.",
|
||||
)
|
||||
messages_per_long_summary: int | None = Field(
|
||||
default=None,
|
||||
ge=20,
|
||||
description="Number of messages per long summary. Must be positive, greater than or equal to 20, and greater than messages_per_short_summary.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_summary_thresholds(self) -> Self:
|
||||
"""Validate that short summary threshold <= long summary threshold."""
|
||||
short = self.messages_per_short_summary
|
||||
long = self.messages_per_long_summary
|
||||
|
||||
if short is not None and long is not None and short >= long:
|
||||
raise ValueError(
|
||||
"messages_per_short_summary must be less than messages_per_long_summary"
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class DreamConfiguration(BaseModel):
|
||||
enabled: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable dream functionality. If deriver is disabled, dreams will also be disabled and this setting will be ignored.",
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceConfiguration(BaseModel):
|
||||
"""
|
||||
The set of options that can be in a workspace DB-level configuration dictionary.
|
||||
|
||||
All fields are optional. Session-level configuration overrides workspace-level configuration, which overrides global configuration.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow") # pyright: ignore
|
||||
|
||||
deriver: DeriverConfiguration | None = Field(
|
||||
default=None,
|
||||
description="Configuration for deriver functionality.",
|
||||
)
|
||||
peer_card: PeerCardConfiguration | None = Field(
|
||||
default=None,
|
||||
description="Configuration for peer card functionality. If deriver is disabled, peer cards will also be disabled and these settings will be ignored.",
|
||||
)
|
||||
summary: SummaryConfiguration | None = Field(
|
||||
default=None,
|
||||
description="Configuration for summary functionality.",
|
||||
)
|
||||
dream: DreamConfiguration | None = Field(
|
||||
default=None,
|
||||
description="Configuration for dream functionality. If deriver is disabled, dreams will also be disabled and these settings will be ignored.",
|
||||
)
|
||||
|
||||
|
||||
class SessionConfiguration(WorkspaceConfiguration):
|
||||
"""
|
||||
The set of options that can be in a session DB-level configuration dictionary.
|
||||
|
||||
All fields are optional. Session-level configuration overrides workspace-level configuration, which overrides global configuration.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MessageConfiguration(BaseModel):
|
||||
"""
|
||||
The set of options that can be in a message DB-level configuration dictionary.
|
||||
|
||||
All fields are optional. Message-level configuration overrides all other configurations.
|
||||
"""
|
||||
|
||||
deriver: DeriverConfiguration | None = Field(
|
||||
default=None,
|
||||
description="Configuration for deriver functionality.",
|
||||
)
|
||||
peer_card: PeerCardConfiguration | None = Field(
|
||||
default=None,
|
||||
description="Configuration for peer card functionality. If deriver is disabled, peer cards will also be disabled and these settings will be ignored.",
|
||||
)
|
||||
|
||||
|
||||
class ResolvedDeriverConfiguration(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class ResolvedPeerCardConfiguration(BaseModel):
|
||||
use: bool
|
||||
create: bool
|
||||
|
||||
|
||||
class ResolvedSummaryConfiguration(BaseModel):
|
||||
enabled: bool
|
||||
messages_per_short_summary: int
|
||||
messages_per_long_summary: int
|
||||
|
||||
|
||||
class ResolvedDreamConfiguration(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class ResolvedConfiguration(BaseModel):
|
||||
"""
|
||||
The final resolved configuration for a given message.
|
||||
Hierarchy: message > session > workspace > global configuration
|
||||
"""
|
||||
|
||||
deriver: ResolvedDeriverConfiguration
|
||||
peer_card: ResolvedPeerCardConfiguration
|
||||
summary: ResolvedSummaryConfiguration
|
||||
dream: ResolvedDreamConfiguration
|
||||
|
||||
|
||||
class PeerConfig(BaseModel):
|
||||
observe_me: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether honcho should form a global theory-of-mind representation of this peer",
|
||||
)
|
||||
|
||||
|
||||
class SessionPeerConfig(PeerConfig):
|
||||
observe_others: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether this peer should form a session-level theory-of-mind representation of other peers in the session",
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
|
@ -30,7 +196,9 @@ class WorkspaceCreate(WorkspaceBase):
|
|||
Field(alias="id", min_length=1, max_length=100, pattern=RESOURCE_NAME_PATTERN),
|
||||
]
|
||||
metadata: dict[str, Any] = {}
|
||||
configuration: dict[str, Any] = {}
|
||||
configuration: WorkspaceConfiguration = Field(
|
||||
default_factory=WorkspaceConfiguration
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True) # pyright: ignore
|
||||
|
||||
|
|
@ -41,7 +209,7 @@ class WorkspaceGet(WorkspaceBase):
|
|||
|
||||
class WorkspaceUpdate(WorkspaceBase):
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
configuration: WorkspaceConfiguration | None = None
|
||||
|
||||
|
||||
class Workspace(WorkspaceBase):
|
||||
|
|
@ -103,6 +271,32 @@ class PeerRepresentationGet(BaseModel):
|
|||
None,
|
||||
description="Optional peer ID to get the representation for, from the perspective of this peer",
|
||||
)
|
||||
search_query: str | None = Field(
|
||||
None,
|
||||
description="Optional input to curate the representation around semantic search results",
|
||||
)
|
||||
search_top_k: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Only used if `search_query` is provided. Number of semantic-search-retrieved observations to include in the representation",
|
||||
)
|
||||
search_max_distance: float | None = Field(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Only used if `search_query` is provided. Maximum distance to search for semantically relevant observations",
|
||||
)
|
||||
include_most_derived: bool | None = Field(
|
||||
default=None,
|
||||
description="Only used if `search_query` is provided. Whether to include the most derived observations in the representation",
|
||||
)
|
||||
max_observations: int | None = Field(
|
||||
default=25,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Only used if `search_query` is provided. Maximum number of observations to include in the representation",
|
||||
)
|
||||
|
||||
|
||||
class PeerCardResponse(BaseModel):
|
||||
|
|
@ -111,11 +305,8 @@ class PeerCardResponse(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class PeerConfig(BaseModel):
|
||||
observe_me: bool = Field(
|
||||
default=True,
|
||||
description="Whether honcho should form a global theory-of-mind representation of this peer",
|
||||
)
|
||||
class PeerCardSet(BaseModel):
|
||||
peer_card: list[str] = Field(..., description="The peer card content to set")
|
||||
|
||||
|
||||
class MessageBase(BaseModel):
|
||||
|
|
@ -126,6 +317,7 @@ class MessageCreate(MessageBase):
|
|||
content: Annotated[str, Field(min_length=0, max_length=settings.MAX_MESSAGE_SIZE)]
|
||||
peer_name: str = Field(alias="peer_id")
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: MessageConfiguration | None = None
|
||||
created_at: datetime.datetime | None = None
|
||||
|
||||
_encoded_message: list[int] = PrivateAttr(default=[])
|
||||
|
|
@ -178,6 +370,9 @@ class MessageUploadCreate(BaseModel):
|
|||
"""Schema for message creation from file uploads"""
|
||||
|
||||
peer_id: str = Field(..., description="ID of the peer creating the message")
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: MessageConfiguration | None = None
|
||||
created_at: datetime.datetime | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True) # pyright: ignore
|
||||
|
||||
|
|
@ -186,17 +381,6 @@ class SessionBase(BaseModel):
|
|||
pass
|
||||
|
||||
|
||||
class SessionPeerConfig(BaseModel):
|
||||
observe_others: bool = Field(
|
||||
default=False,
|
||||
description="Whether this peer should form a session-level theory-of-mind representation of other peers in the session",
|
||||
)
|
||||
observe_me: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether other peers in this session should try to form a session-level theory-of-mind representation of this peer",
|
||||
)
|
||||
|
||||
|
||||
class SessionCreate(SessionBase):
|
||||
name: Annotated[
|
||||
str,
|
||||
|
|
@ -204,7 +388,7 @@ class SessionCreate(SessionBase):
|
|||
]
|
||||
metadata: dict[str, Any] | None = None
|
||||
peer_names: dict[str, SessionPeerConfig] | None = Field(default=None, alias="peers")
|
||||
configuration: dict[str, Any] | None = None
|
||||
configuration: SessionConfiguration | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True) # pyright: ignore
|
||||
|
||||
|
|
@ -215,7 +399,7 @@ class SessionGet(SessionBase):
|
|||
|
||||
class SessionUpdate(SessionBase):
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
configuration: SessionConfiguration | None = None
|
||||
|
||||
|
||||
class Session(SessionBase):
|
||||
|
|
@ -270,6 +454,21 @@ class SessionContext(SessionBase):
|
|||
)
|
||||
|
||||
|
||||
class PeerContext(BaseModel):
|
||||
"""Context for a peer, including representation and peer card."""
|
||||
|
||||
peer_id: str = Field(description="The ID of the peer")
|
||||
target_id: str = Field(description="The ID of the target peer being observed")
|
||||
representation: Representation | None = Field(
|
||||
default=None,
|
||||
description="The working representation of the target peer from the observer's perspective",
|
||||
)
|
||||
peer_card: list[str] | None = Field(
|
||||
default=None,
|
||||
description="The peer card for the target peer from the observer's perspective",
|
||||
)
|
||||
|
||||
|
||||
class SessionSummaries(SessionBase):
|
||||
name: str = Field(serialization_alias="id")
|
||||
short_summary: Summary | None = Field(
|
||||
|
|
@ -289,7 +488,7 @@ class DocumentBase(BaseModel):
|
|||
|
||||
|
||||
class DocumentMetadata(BaseModel):
|
||||
message_ids: list[tuple[int, int]] = Field(
|
||||
message_ids: list[int] = Field(
|
||||
description="The ID range(s) of the messages that this document was derived from. Acts as a link to the primary source of the document. Note that as a document gets deduplicated, additional ranges will be added, because the same document could be derived from completely separate message ranges."
|
||||
)
|
||||
message_created_at: str = Field(
|
||||
|
|
@ -319,6 +518,50 @@ class DocumentCreate(DocumentBase):
|
|||
embedding: list[float] = Field()
|
||||
|
||||
|
||||
class ObservationGet(BaseModel):
|
||||
"""Schema for listing observations with optional filters"""
|
||||
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class Observation(BaseModel):
|
||||
"""Observation response - external view of a document"""
|
||||
|
||||
id: str
|
||||
content: str
|
||||
observer: str = Field(
|
||||
description="The peer who made the observation",
|
||||
serialization_alias="observer_id",
|
||||
)
|
||||
observed: str = Field(
|
||||
description="The peer being observed", serialization_alias="observed_id"
|
||||
)
|
||||
session_name: str = Field(serialization_alias="session_id")
|
||||
created_at: datetime.datetime
|
||||
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class ObservationQuery(BaseModel):
|
||||
"""Query parameters for semantic search of observations"""
|
||||
|
||||
query: str = Field(..., description="Semantic search query")
|
||||
top_k: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
)
|
||||
distance: float | None = Field(
|
||||
default=None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Maximum cosine distance threshold for results",
|
||||
)
|
||||
filters: dict[str, Any] | None = Field(
|
||||
default=None, description="Additional filters to apply"
|
||||
)
|
||||
|
||||
|
||||
class MessageSearchOptions(BaseModel):
|
||||
query: str = Field(..., description="Search query")
|
||||
filters: dict[str, Any] | None = Field(
|
||||
|
|
@ -399,14 +642,6 @@ class QueueStatusRow(BaseModel):
|
|||
session_pending: int
|
||||
|
||||
|
||||
class PeerConfigResult(BaseModel):
|
||||
"""Result from querying peer configuration data."""
|
||||
|
||||
peer_name: str
|
||||
peer_configuration: dict[str, Any]
|
||||
session_peer_configuration: dict[str, Any]
|
||||
|
||||
|
||||
class SessionPeerData(BaseModel):
|
||||
"""Data for managing session peer relationships."""
|
||||
|
||||
|
|
@ -445,6 +680,15 @@ class DeriverStatus(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
# Dream trigger schema
|
||||
class TriggerDreamRequest(BaseModel):
|
||||
observer: str = Field(..., description="Observer peer name")
|
||||
observed: str | None = Field(
|
||||
None, description="Observed peer name (defaults to observer if not specified)"
|
||||
)
|
||||
dream_type: DreamType = Field(..., description="Type of dream to trigger")
|
||||
|
||||
|
||||
# Webhook endpoint schemas
|
||||
class WebhookEndpointBase(BaseModel):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -76,6 +76,27 @@ for provider_name, provider_value in SELECTED_PROVIDERS:
|
|||
if provider_value not in CLIENTS:
|
||||
raise ValueError(f"Missing client for {provider_name}: {provider_value}")
|
||||
|
||||
# Validate backup providers are initialized if configured
|
||||
BACKUP_PROVIDERS = [
|
||||
("Deriver", settings.DERIVER),
|
||||
("PeerCard", settings.PEER_CARD),
|
||||
("Dialectic", settings.DIALECTIC),
|
||||
("Summary", settings.SUMMARY),
|
||||
("Dream", settings.DREAM),
|
||||
]
|
||||
|
||||
for component_name, component_settings in BACKUP_PROVIDERS:
|
||||
if (
|
||||
hasattr(component_settings, "BACKUP_PROVIDER")
|
||||
and component_settings.BACKUP_PROVIDER is not None
|
||||
and component_settings.BACKUP_PROVIDER not in CLIENTS
|
||||
):
|
||||
raise ValueError(
|
||||
f"Backup provider for {component_name} is set to {component_settings.BACKUP_PROVIDER}, "
|
||||
+ "but this provider is not initialized. Please set the required API key/URL environment "
|
||||
+ "variables or remove the backup configuration."
|
||||
)
|
||||
|
||||
|
||||
class HonchoLLMCallResponse(BaseModel, Generic[T]):
|
||||
"""
|
||||
|
|
@ -232,8 +253,8 @@ async def honcho_llm_call(
|
|||
):
|
||||
provider: SupportedProviders = llm_settings.BACKUP_PROVIDER
|
||||
model: str = llm_settings.BACKUP_MODEL
|
||||
logger.info(
|
||||
f"Final retry attempt {attempt}: switching from "
|
||||
logger.warning(
|
||||
f"Final retry attempt {attempt}/{retry_attempts}: switching from "
|
||||
+ f"{llm_settings.PROVIDER}/{llm_settings.MODEL} to "
|
||||
+ f"backup {provider}/{model}"
|
||||
)
|
||||
|
|
@ -306,14 +327,20 @@ async def honcho_llm_call(
|
|||
if enable_retry:
|
||||
|
||||
def before_retry_callback(retry_state: Any) -> None:
|
||||
"""Update attempt counter before each retry."""
|
||||
_current_attempt.set(retry_state.attempt_number)
|
||||
"""Update attempt counter before each retry.
|
||||
|
||||
Note: before_sleep is called AFTER an attempt fails and BEFORE sleeping,
|
||||
so we need to increment to the next attempt number.
|
||||
"""
|
||||
next_attempt = retry_state.attempt_number + 1
|
||||
_current_attempt.set(next_attempt)
|
||||
exc = retry_state.outcome.exception() if retry_state.outcome else None
|
||||
if exc:
|
||||
logger.warning(
|
||||
f"Error on attempt {retry_state.attempt_number} with "
|
||||
f"Error on attempt {retry_state.attempt_number}/{retry_attempts} with "
|
||||
+ f"{llm_settings.PROVIDER}/{llm_settings.MODEL}: {exc}"
|
||||
)
|
||||
logger.info(f"Will retry with attempt {next_attempt}/{retry_attempts}")
|
||||
|
||||
decorated = retry(
|
||||
stop=stop_after_attempt(retry_attempts),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
"""Configuration resolution utilities for hierarchical settings."""
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from src import models
|
||||
from src.config import settings
|
||||
from src.schemas import (
|
||||
MessageConfiguration,
|
||||
ResolvedConfiguration,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def deep_update(base: dict[str, Any], update: dict[str, Any]) -> None:
|
||||
"""
|
||||
Recursive update of a dictionary.
|
||||
Skips None values in the update dictionary.
|
||||
"""
|
||||
for key, value in update.items():
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, dict) and key in base and isinstance(base[key], dict):
|
||||
deep_update(cast(dict[str, Any], base[key]), cast(dict[str, Any], value))
|
||||
else:
|
||||
base[key] = value
|
||||
|
||||
|
||||
def get_configuration(
|
||||
message_configuration: MessageConfiguration | None,
|
||||
session: models.Session,
|
||||
workspace: models.Workspace | None = None,
|
||||
) -> ResolvedConfiguration:
|
||||
"""
|
||||
Resolve session configuration with hierarchical fallback.
|
||||
|
||||
Resolution hierarchy:
|
||||
1. Message configuration
|
||||
2. Session configuration
|
||||
3. Workspace configuration
|
||||
4. Global defaults from settings
|
||||
|
||||
Args:
|
||||
session: The session model
|
||||
workspace: Optional workspace model (if not provided, only session and global config are used)
|
||||
|
||||
Returns:
|
||||
ResolvedConfiguration
|
||||
"""
|
||||
# Start with defaults
|
||||
config_dict: dict[str, Any] = {
|
||||
"deriver": {"enabled": True},
|
||||
"peer_card": {
|
||||
"use": settings.PEER_CARD.ENABLED,
|
||||
"create": settings.PEER_CARD.ENABLED,
|
||||
},
|
||||
"summary": {
|
||||
"enabled": settings.SUMMARY.ENABLED,
|
||||
"messages_per_short_summary": settings.SUMMARY.MESSAGES_PER_SHORT_SUMMARY,
|
||||
"messages_per_long_summary": settings.SUMMARY.MESSAGES_PER_LONG_SUMMARY,
|
||||
},
|
||||
"dream": {"enabled": settings.DREAM.ENABLED},
|
||||
}
|
||||
|
||||
# Apply overrides in order (Workspace -> Session -> Message)
|
||||
# Note: deep_update modifies config_dict in place
|
||||
|
||||
if workspace is not None:
|
||||
deep_update(config_dict, workspace.configuration)
|
||||
|
||||
deep_update(config_dict, session.configuration)
|
||||
|
||||
if message_configuration is not None:
|
||||
deep_update(config_dict, message_configuration.model_dump(exclude_none=True))
|
||||
|
||||
return ResolvedConfiguration(**config_dict)
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Protocol
|
||||
|
|
@ -159,6 +160,9 @@ async def process_file_uploads_for_messages(
|
|||
file: UploadFile,
|
||||
peer_id: str,
|
||||
max_chars: int = settings.MAX_MESSAGE_SIZE,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
configuration: schemas.MessageConfiguration | None = None,
|
||||
created_at: datetime.datetime | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Process an uploaded file and prepare message creation data.
|
||||
|
|
@ -170,6 +174,9 @@ async def process_file_uploads_for_messages(
|
|||
file: Uploaded file to process
|
||||
peer_id: ID of the peer creating the messages
|
||||
max_chars: Maximum characters per message chunk
|
||||
metadata: Optional metadata to associate with all messages created from this file
|
||||
configuration: Optional configuration to associate with all messages created from this file
|
||||
created_at: Optional created_at timestamp to use for all messages created from this file
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing message_create and file_metadata
|
||||
|
|
@ -192,10 +199,13 @@ async def process_file_uploads_for_messages(
|
|||
# Build message content properly handling empty files
|
||||
message_content = chunk or ""
|
||||
|
||||
# Create message
|
||||
# Create message with optional metadata, configuration, and created_at
|
||||
message_create = schemas.MessageCreate(
|
||||
content=message_content,
|
||||
peer_id=peer_id,
|
||||
metadata=metadata,
|
||||
configuration=configuration,
|
||||
created_at=created_at,
|
||||
)
|
||||
|
||||
# Store file metadata separately to add to internal_metadata later
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ ALLOWED_EXTERNAL_TO_INTERNAL_COLUMN_MAPPING_MESSAGES = {
|
|||
"metadata": "h_metadata",
|
||||
}
|
||||
|
||||
ALLOWED_EXTERNAL_TO_INTERNAL_COLUMN_MAPPING_DOCUMENTS = {
|
||||
"session_id": "session_name",
|
||||
"workspace_id": "workspace_name",
|
||||
"observer_id": "observer",
|
||||
"observed_id": "observed",
|
||||
"metadata": "internal_metadata",
|
||||
}
|
||||
|
||||
|
||||
def apply_filter(
|
||||
stmt: Select[tuple[T]], model_class: type[T], filters: dict[str, Any] | None = None
|
||||
|
|
@ -193,8 +201,10 @@ def _build_field_condition(
|
|||
if model_class.__name__ == "Message":
|
||||
column_name = ALLOWED_EXTERNAL_TO_INTERNAL_COLUMN_MAPPING_MESSAGES.get(key)
|
||||
elif model_class.__name__ == "Document":
|
||||
# documents are fully internal so we can use any column name directly
|
||||
column_name = key
|
||||
column_name = ALLOWED_EXTERNAL_TO_INTERNAL_COLUMN_MAPPING_DOCUMENTS.get(
|
||||
key,
|
||||
key, # fallback to the key itself if not found in the mapping for internal use here
|
||||
)
|
||||
else:
|
||||
column_name = ALLOWED_EXTERNAL_TO_INTERNAL_COLUMN_MAPPING.get(key)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ from typing import Any, Literal
|
|||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from src.schemas import DreamType, ResolvedConfiguration
|
||||
|
||||
|
||||
class BasePayload(BaseModel):
|
||||
"""Base payload with common fields."""
|
||||
|
|
@ -19,6 +21,7 @@ class RepresentationPayload(BasePayload):
|
|||
observer: str
|
||||
observed: str
|
||||
created_at: datetime
|
||||
configuration: ResolvedConfiguration
|
||||
|
||||
|
||||
class RepresentationPayloads(BasePayload):
|
||||
|
|
@ -33,6 +36,7 @@ class SummaryPayload(BasePayload):
|
|||
task_type: Literal["summary"] = "summary"
|
||||
session_name: str
|
||||
message_seq_in_session: int
|
||||
configuration: ResolvedConfiguration
|
||||
# Optional for backward compatibility with older queue items
|
||||
message_public_id: str | None = None
|
||||
|
||||
|
|
@ -49,11 +53,19 @@ class DreamPayload(BasePayload):
|
|||
"""Payload for dream tasks."""
|
||||
|
||||
task_type: Literal["dream"] = "dream"
|
||||
dream_type: Literal["consolidate"] = "consolidate"
|
||||
dream_type: DreamType
|
||||
observer: str
|
||||
observed: str
|
||||
|
||||
|
||||
class DeletionPayload(BasePayload):
|
||||
"""Payload for deletion tasks."""
|
||||
|
||||
task_type: Literal["deletion"] = "deletion"
|
||||
deletion_type: Literal["session", "observation"]
|
||||
resource_id: str
|
||||
|
||||
|
||||
def create_webhook_payload(
|
||||
event_type: str,
|
||||
data: dict[str, Any],
|
||||
|
|
@ -65,7 +77,7 @@ def create_webhook_payload(
|
|||
|
||||
|
||||
def create_dream_payload(
|
||||
dream_type: Literal["consolidate"] = "consolidate",
|
||||
dream_type: DreamType,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
|
|
@ -78,8 +90,20 @@ def create_dream_payload(
|
|||
).model_dump(mode="json", exclude_none=True)
|
||||
|
||||
|
||||
def create_deletion_payload(
|
||||
deletion_type: Literal["session", "observation"],
|
||||
resource_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a deletion payload."""
|
||||
return DeletionPayload(
|
||||
deletion_type=deletion_type,
|
||||
resource_id=resource_id,
|
||||
).model_dump(mode="json", exclude_none=True)
|
||||
|
||||
|
||||
def create_payload(
|
||||
message: dict[str, Any],
|
||||
configuration: ResolvedConfiguration,
|
||||
task_type: Literal["representation", "summary"],
|
||||
message_seq_in_session: int | None = None,
|
||||
*,
|
||||
|
|
@ -96,9 +120,10 @@ def create_payload(
|
|||
Args:
|
||||
message: The original message dictionary
|
||||
task_type: Type of task ('representation' or 'summary')
|
||||
message_seq_in_session: Required for summary tasks, must be None for representation
|
||||
observer: Name of the observer peer (required for representation tasks)
|
||||
observed: Name of the observed peer (*always* the peer who sent the message) (required for representation tasks)
|
||||
message_seq_in_session: Required for summary tasks, must be None for representation
|
||||
|
||||
|
||||
Returns:
|
||||
Processed payload dictionary ready for queue processing (without workspace_name and message_id)
|
||||
|
|
@ -143,6 +168,7 @@ def create_payload(
|
|||
created_at=created_at,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
configuration=configuration,
|
||||
)
|
||||
elif task_type == "summary":
|
||||
if message_seq_in_session is None:
|
||||
|
|
@ -158,6 +184,7 @@ def create_payload(
|
|||
validated_payload = SummaryPayload(
|
||||
session_name=session_name,
|
||||
message_seq_in_session=message_seq_in_session,
|
||||
configuration=configuration,
|
||||
message_public_id=message_public_id,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from src.utils.formatting import parse_datetime_iso
|
|||
|
||||
class ObservationMetadata(BaseModel):
|
||||
created_at: datetime
|
||||
message_ids: list[tuple[int, int]]
|
||||
message_ids: list[int]
|
||||
session_name: str
|
||||
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ class Representation(BaseModel):
|
|||
doc.internal_metadata, doc.created_at
|
||||
),
|
||||
content=doc.content,
|
||||
message_ids=doc.internal_metadata.get("message_ids", [(0, 0)]),
|
||||
message_ids=doc.internal_metadata.get("message_ids", []),
|
||||
session_name=doc.session_name,
|
||||
)
|
||||
for doc in documents
|
||||
|
|
@ -279,7 +279,7 @@ class Representation(BaseModel):
|
|||
doc.internal_metadata, doc.created_at
|
||||
),
|
||||
conclusion=doc.content,
|
||||
message_ids=doc.internal_metadata.get("message_ids", [(0, 0)]),
|
||||
message_ids=doc.internal_metadata.get("message_ids", []),
|
||||
session_name=doc.session_name,
|
||||
premises=doc.internal_metadata.get("premises", []),
|
||||
)
|
||||
|
|
@ -292,7 +292,7 @@ class Representation(BaseModel):
|
|||
def from_prompt_representation(
|
||||
cls,
|
||||
prompt_representation: "PromptRepresentation",
|
||||
message_ids: tuple[int, int],
|
||||
message_ids: list[int],
|
||||
session_name: str,
|
||||
created_at: datetime,
|
||||
) -> "Representation":
|
||||
|
|
@ -301,7 +301,7 @@ class Representation(BaseModel):
|
|||
ExplicitObservation(
|
||||
content=e.content,
|
||||
created_at=created_at,
|
||||
message_ids=[message_ids],
|
||||
message_ids=message_ids,
|
||||
session_name=session_name,
|
||||
)
|
||||
for e in prompt_representation.explicit
|
||||
|
|
@ -310,7 +310,7 @@ class Representation(BaseModel):
|
|||
DeductiveObservation(
|
||||
conclusion=d.conclusion,
|
||||
created_at=created_at,
|
||||
message_ids=[message_ids],
|
||||
message_ids=message_ids,
|
||||
session_name=session_name,
|
||||
premises=d.premises,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ of each item's rank in each list, then summing these reciprocal ranks.
|
|||
import re
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy import Select, func, or_, select
|
||||
from sqlalchemy import Select, and_, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models
|
||||
from src.config import settings
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import ValidationException
|
||||
from src.models import session_peers_table
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
T = TypeVar("T")
|
||||
|
|
@ -173,7 +174,9 @@ async def search(
|
|||
Args:
|
||||
db: Database session
|
||||
query: Search query to match against message content
|
||||
filters: Optional filters to scope search
|
||||
filters: Optional filters to scope search. Special filter 'peer_perspective' will search
|
||||
across all messages from sessions that the peer is/was a member of, filtered
|
||||
by the time window when they were actually in the session.
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
|
|
@ -184,6 +187,35 @@ async def search(
|
|||
"""
|
||||
# Base query conditions
|
||||
stmt = select(models.Message)
|
||||
|
||||
# Handle special peer_perspective filter
|
||||
if filters and "peer_perspective" in filters:
|
||||
peer_name = filters["peer_perspective"]
|
||||
# Remove from filters dict so apply_filter doesn't try to handle it
|
||||
filters = {k: v for k, v in filters.items() if k != "peer_perspective"}
|
||||
# Safety: peer_perspective must be scoped to a workspace
|
||||
if not filters or (
|
||||
"workspace_id" not in filters and "workspace_name" not in filters
|
||||
):
|
||||
raise ValidationException(
|
||||
"peer_perspective requires a workspace scope (workspace_id or workspace_name)."
|
||||
)
|
||||
|
||||
# Join with session_peers_table to get messages from sessions the peer was in
|
||||
# Only include messages created during the time window the peer was active
|
||||
stmt = stmt.join(
|
||||
session_peers_table,
|
||||
and_(
|
||||
models.Message.session_name == session_peers_table.c.session_name,
|
||||
models.Message.workspace_name == session_peers_table.c.workspace_name,
|
||||
models.Message.created_at >= session_peers_table.c.joined_at,
|
||||
or_(
|
||||
session_peers_table.c.left_at.is_(None),
|
||||
models.Message.created_at <= session_peers_table.c.left_at,
|
||||
),
|
||||
),
|
||||
).where(session_peers_table.c.peer_name == peer_name)
|
||||
|
||||
stmt = apply_filter(stmt, models.Message, filters)
|
||||
|
||||
search_results: list[list[models.Message]] = []
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ async def summarize_if_needed(
|
|||
message_id: int,
|
||||
message_seq_in_session: int,
|
||||
message_public_id: str,
|
||||
configuration: schemas.ResolvedConfiguration,
|
||||
) -> None:
|
||||
"""
|
||||
Create short/long summaries if thresholds met.
|
||||
|
|
@ -248,9 +249,22 @@ async def summarize_if_needed(
|
|||
workspace_name: The workspace name
|
||||
session_name: The session name
|
||||
message_id: The message ID
|
||||
message_seq_in_session: The sequence number of the message in the session
|
||||
message_public_id: The public ID of the message
|
||||
configuration: The resolved configuration for the message
|
||||
"""
|
||||
should_create_long: bool = message_seq_in_session % MESSAGES_PER_LONG_SUMMARY == 0
|
||||
should_create_short: bool = message_seq_in_session % MESSAGES_PER_SHORT_SUMMARY == 0
|
||||
if configuration.summary.enabled is False:
|
||||
return
|
||||
|
||||
should_create_long: bool = (
|
||||
message_seq_in_session % configuration.summary.messages_per_long_summary == 0
|
||||
)
|
||||
should_create_short: bool = (
|
||||
message_seq_in_session % configuration.summary.messages_per_short_summary == 0
|
||||
)
|
||||
|
||||
if should_create_long is False and should_create_short is False:
|
||||
return
|
||||
|
||||
# If both summaries need to be created, run them in parallel with separate database sessions
|
||||
if should_create_long and should_create_short:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from typing import Literal
|
||||
|
||||
SupportedProviders = Literal["anthropic", "openai", "google", "groq", "custom", "vllm"]
|
||||
TaskType = Literal["webhook", "summary", "representation", "dream"]
|
||||
TaskType = Literal["webhook", "summary", "representation", "dream", "deletion"]
|
||||
DocumentLevel = Literal["explicit", "deductive"]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class ParsedWorkUnit(BaseModel):
|
|||
session_name: str | None
|
||||
observer: str | None
|
||||
observed: str | None
|
||||
dream_type: str | None = None
|
||||
|
||||
|
||||
def construct_work_unit_key(
|
||||
|
|
@ -45,12 +46,24 @@ def construct_work_unit_key(
|
|||
observed = payload.get("observed", "None")
|
||||
session_name = payload.get("session_name", "None")
|
||||
if task_type == "dream":
|
||||
return f"{task_type}:{workspace_name}:{observer}:{observed}"
|
||||
dream_type = payload.get("dream_type")
|
||||
if not dream_type:
|
||||
raise ValueError("dream_type is required for dream tasks")
|
||||
return f"{task_type}:{dream_type}:{workspace_name}:{observer}:{observed}"
|
||||
return f"{task_type}:{workspace_name}:{session_name}:{observer}:{observed}"
|
||||
|
||||
if task_type == "webhook":
|
||||
return f"webhook:{workspace_name}"
|
||||
|
||||
if task_type == "deletion":
|
||||
deletion_type = payload.get("deletion_type")
|
||||
resource_id = payload.get("resource_id")
|
||||
if not deletion_type or not resource_id:
|
||||
raise ValueError(
|
||||
"deletion_type and resource_id are required for deletion tasks"
|
||||
)
|
||||
return f"deletion:{workspace_name}:{deletion_type}:{resource_id}"
|
||||
|
||||
raise ValueError(f"Invalid task type: {task_type}")
|
||||
|
||||
|
||||
|
|
@ -84,16 +97,17 @@ def parse_work_unit_key(work_unit_key: str) -> ParsedWorkUnit:
|
|||
)
|
||||
|
||||
if task_type == "dream":
|
||||
if len(parts) != 4:
|
||||
if len(parts) != 5:
|
||||
raise ValueError(
|
||||
f"Invalid work_unit_key format for task_type {task_type}: {work_unit_key}"
|
||||
)
|
||||
return ParsedWorkUnit(
|
||||
task_type=task_type,
|
||||
workspace_name=parts[1],
|
||||
workspace_name=parts[2],
|
||||
session_name=None,
|
||||
observer=parts[2],
|
||||
observed=parts[3],
|
||||
observer=parts[3],
|
||||
observed=parts[4],
|
||||
dream_type=parts[1],
|
||||
)
|
||||
|
||||
if task_type == "webhook":
|
||||
|
|
@ -109,4 +123,17 @@ def parse_work_unit_key(work_unit_key: str) -> ParsedWorkUnit:
|
|||
observed=None,
|
||||
)
|
||||
|
||||
if task_type == "deletion":
|
||||
if len(parts) != 4:
|
||||
raise ValueError(
|
||||
f"Invalid work_unit_key format for task_type {task_type}: {work_unit_key}"
|
||||
)
|
||||
return ParsedWorkUnit(
|
||||
task_type=task_type,
|
||||
workspace_name=parts[1],
|
||||
session_name=None,
|
||||
observer=None,
|
||||
observed=None,
|
||||
)
|
||||
|
||||
raise ValueError(f"Invalid task type in work_unit_key: {task_type}")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from . import (
|
|||
test_66e63cf2cf77_add_indexes_to_documents_table,
|
||||
test_76ffba56fe8c_add_error_field_to_queueitem,
|
||||
test_88b0fb10906f_add_webhooks_table,
|
||||
test_110bdf470272_rename_deriver_disabled_to_deriver_,
|
||||
test_556a16564f50_add_user_id_and_app_id_to_tables,
|
||||
test_564ba40505c5_add_session_name_column_to_documents,
|
||||
test_917195d9b5e9_add_messageembedding_table,
|
||||
|
|
@ -27,6 +28,7 @@ __all__ = [
|
|||
"test_05486ce795d5_make_session_name_required_on_messages",
|
||||
"test_066e87ca5b07_align_schema_with_declarative_models",
|
||||
"test_08894082221a_replace_collection_name_with_observer_",
|
||||
"test_110bdf470272_rename_deriver_disabled_to_deriver_",
|
||||
"test_20f89a421aff_rename_metamessage_type_to_label",
|
||||
"test_29ade7350c19_remove_document_level_valid_constraint",
|
||||
"test_556a16564f50_add_user_id_and_app_id_to_tables",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
"""Hooks for revision 110bdf470272 (rename_deriver_disabled_to_deriver_)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy import text
|
||||
|
||||
from tests.alembic.registry import register_after_upgrade, register_before_upgrade
|
||||
from tests.alembic.verifier import MigrationVerifier
|
||||
|
||||
WORKSPACE_ID = generate_nanoid()
|
||||
WORKSPACE_NAME = "workspace-name"
|
||||
PEER_ID = generate_nanoid()
|
||||
PEER_NAME = "peer-name"
|
||||
SESSION_ID_DISABLED_TRUE = generate_nanoid()
|
||||
SESSION_ID_DISABLED_FALSE = generate_nanoid()
|
||||
SESSION_ID_NO_KEY = generate_nanoid()
|
||||
|
||||
|
||||
@register_before_upgrade("110bdf470272")
|
||||
def prepare_rename_deriver_disabled_to_deriver(verifier: MigrationVerifier) -> None:
|
||||
"""Seed sessions with deriver_disabled configuration before upgrading to 110bdf470272."""
|
||||
|
||||
schema = verifier.schema
|
||||
connection = verifier.conn
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO "{schema}"."workspaces" ("id", "name")
|
||||
VALUES (:workspace_id, :workspace_name)
|
||||
"""
|
||||
),
|
||||
{"workspace_id": WORKSPACE_ID, "workspace_name": WORKSPACE_NAME},
|
||||
)
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO "{schema}"."peers" ("id", "name", "workspace_name")
|
||||
VALUES (:peer_id, :peer_name, :workspace_name)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"peer_id": PEER_ID,
|
||||
"peer_name": PEER_NAME,
|
||||
"workspace_name": WORKSPACE_NAME,
|
||||
},
|
||||
)
|
||||
|
||||
configuration_disabled_true = json.dumps({"deriver_disabled": True})
|
||||
configuration_disabled_false = json.dumps({"deriver_disabled": False})
|
||||
configuration_no_key = json.dumps({"other_setting": "value"})
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO "{schema}"."sessions"
|
||||
("id", "name", "workspace_name", "configuration")
|
||||
VALUES (:session_id, :session_name, :workspace_name, :configuration)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"session_id": SESSION_ID_DISABLED_TRUE,
|
||||
"session_name": "session-disabled-true",
|
||||
"workspace_name": WORKSPACE_NAME,
|
||||
"configuration": configuration_disabled_true,
|
||||
},
|
||||
)
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO "{schema}"."sessions"
|
||||
("id", "name", "workspace_name", "configuration")
|
||||
VALUES (:session_id, :session_name, :workspace_name, :configuration)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"session_id": SESSION_ID_DISABLED_FALSE,
|
||||
"session_name": "session-disabled-false",
|
||||
"workspace_name": WORKSPACE_NAME,
|
||||
"configuration": configuration_disabled_false,
|
||||
},
|
||||
)
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO "{schema}"."sessions"
|
||||
("id", "name", "workspace_name", "configuration")
|
||||
VALUES (:session_id, :session_name, :workspace_name, :configuration)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"session_id": SESSION_ID_NO_KEY,
|
||||
"session_name": "session-no-key",
|
||||
"workspace_name": WORKSPACE_NAME,
|
||||
"configuration": configuration_no_key,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@register_after_upgrade("110bdf470272")
|
||||
def verify_rename_deriver_disabled_to_deriver(verifier: MigrationVerifier) -> None:
|
||||
"""Verify deriver_disabled was converted to deriver_enabled correctly."""
|
||||
|
||||
schema = verifier.schema
|
||||
conn = verifier.conn
|
||||
|
||||
session_disabled_true = conn.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT configuration FROM "{schema}"."sessions"
|
||||
WHERE "id" = :session_id
|
||||
"""
|
||||
),
|
||||
{"session_id": SESSION_ID_DISABLED_TRUE},
|
||||
).one()
|
||||
config = session_disabled_true.configuration
|
||||
assert "deriver_disabled" not in config, "deriver_disabled should be removed"
|
||||
assert (
|
||||
config.get("deriver_enabled") is False
|
||||
), "deriver_disabled: true should become deriver_enabled: false"
|
||||
|
||||
session_disabled_false = conn.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT configuration FROM "{schema}"."sessions"
|
||||
WHERE "id" = :session_id
|
||||
"""
|
||||
),
|
||||
{"session_id": SESSION_ID_DISABLED_FALSE},
|
||||
).one()
|
||||
config = session_disabled_false.configuration
|
||||
assert "deriver_disabled" not in config, "deriver_disabled should be removed"
|
||||
assert (
|
||||
config.get("deriver_enabled") is True
|
||||
), "deriver_disabled: false should become deriver_enabled: true"
|
||||
|
||||
session_no_key = conn.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT configuration FROM "{schema}"."sessions"
|
||||
WHERE "id" = :session_id
|
||||
"""
|
||||
),
|
||||
{"session_id": SESSION_ID_NO_KEY},
|
||||
).one()
|
||||
config = session_no_key.configuration
|
||||
assert "deriver_disabled" not in config, "deriver_disabled should not exist"
|
||||
assert (
|
||||
"deriver_enabled" not in config
|
||||
), "deriver_enabled should not be added when deriver_disabled was absent"
|
||||
assert (
|
||||
config.get("other_setting") == "value"
|
||||
), "Other configuration should be preserved"
|
||||
|
|
@ -32,19 +32,26 @@ class HonchoHarness:
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, db_port: int, api_port: int, project_root: Path, instance_id: int = 0
|
||||
self,
|
||||
db_port: int,
|
||||
api_port: int,
|
||||
redis_port: int,
|
||||
project_root: Path,
|
||||
instance_id: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the harness with database port, API port, and project root.
|
||||
Initialize the harness with database port, API port, Redis port, and project root.
|
||||
|
||||
Args:
|
||||
db_port: Port for the PostgreSQL database
|
||||
api_port: Port for the FastAPI server
|
||||
redis_port: Port for the Redis server
|
||||
project_root: Path to the Honcho project root
|
||||
instance_id: Instance identifier for pool management
|
||||
"""
|
||||
self.db_port: int = db_port
|
||||
self.api_port: int = api_port
|
||||
self.redis_port: int = redis_port
|
||||
self.project_root: Path = project_root
|
||||
self.instance_id: int = instance_id
|
||||
self.temp_dir: Path | None = None
|
||||
|
|
@ -86,8 +93,7 @@ class HonchoHarness:
|
|||
compose_data["services"]["database"]["command"] = cmd
|
||||
|
||||
# Update the Redis port
|
||||
# TODO: Make this configurable if running multiple instances
|
||||
compose_data["services"]["redis"]["ports"] = ["6379:6379"]
|
||||
compose_data["services"]["redis"]["ports"] = [f"{self.redis_port}:6379"]
|
||||
|
||||
# Add a unique project name to avoid conflicts
|
||||
compose_data["name"] = f"honcho_harness_{self.db_port}"
|
||||
|
|
@ -161,7 +167,7 @@ class HonchoHarness:
|
|||
return {
|
||||
"DB_CONNECTION_URI": f"postgresql+psycopg://testuser:testpwd@localhost:{self.db_port}/honcho",
|
||||
"CACHE_ENABLED": "true",
|
||||
"CACHE_URL": "redis://localhost:6379/0",
|
||||
"CACHE_URL": f"redis://localhost:{self.redis_port}/0",
|
||||
}
|
||||
|
||||
def start_database(self) -> None:
|
||||
|
|
@ -216,24 +222,7 @@ class HonchoHarness:
|
|||
"""
|
||||
Start the Redis cache server using Docker Compose.
|
||||
"""
|
||||
print("Starting Redis cache server on port 6379...")
|
||||
|
||||
# Ensure clean state by removing any existing containers/volumes
|
||||
subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"compose",
|
||||
"-f",
|
||||
str(self.docker_compose_file),
|
||||
"-p",
|
||||
f"honcho_harness_{self.db_port}",
|
||||
"down",
|
||||
"--volumes",
|
||||
"--remove-orphans",
|
||||
],
|
||||
cwd=self.temp_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
print(f"Starting Redis cache server on port {self.redis_port}...")
|
||||
|
||||
# Change to the temp directory and start the redis service
|
||||
result = subprocess.run(
|
||||
|
|
@ -271,7 +260,7 @@ class HonchoHarness:
|
|||
"""
|
||||
print("Waiting for Redis to be ready...")
|
||||
start_time = time.time()
|
||||
redis_port = 6379
|
||||
redis_port = self.redis_port
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
|
|
@ -440,7 +429,7 @@ class HonchoHarness:
|
|||
sys.exit(1)
|
||||
|
||||
print(
|
||||
f"[Instance {self.instance_id}] ✅ Database verification passed: Database is empty"
|
||||
f"[Instance {self.instance_id}] Database verification passed: Database is empty"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -686,7 +675,7 @@ except Exception as e:
|
|||
|
||||
print("=" * 60)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up resources and stop all processes.
|
||||
"""
|
||||
|
|
@ -752,7 +741,7 @@ except Exception as e:
|
|||
|
||||
# Close cache
|
||||
try:
|
||||
asyncio.run(self.close_cache())
|
||||
await self.close_cache()
|
||||
except Exception as e:
|
||||
print(f"Error closing cache: {e}")
|
||||
|
||||
|
|
@ -855,7 +844,7 @@ except Exception as e:
|
|||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
await self.cleanup()
|
||||
|
||||
|
||||
class HonchoHarnessPool:
|
||||
|
|
@ -864,7 +853,12 @@ class HonchoHarnessPool:
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, pool_size: int, base_db_port: int, base_api_port: int, project_root: Path
|
||||
self,
|
||||
pool_size: int,
|
||||
base_db_port: int,
|
||||
base_api_port: int,
|
||||
base_redis_port: int,
|
||||
project_root: Path,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a pool of Honcho harnesses.
|
||||
|
|
@ -873,11 +867,13 @@ class HonchoHarnessPool:
|
|||
pool_size: Number of Honcho instances to create
|
||||
base_db_port: Base port for PostgreSQL databases (each instance gets base + instance_id)
|
||||
base_api_port: Base port for FastAPI servers (each instance gets base + instance_id)
|
||||
base_redis_port: Base port for Redis servers (each instance gets base + instance_id)
|
||||
project_root: Path to the Honcho project root
|
||||
"""
|
||||
self.pool_size: int = pool_size
|
||||
self.base_db_port: int = base_db_port
|
||||
self.base_api_port: int = base_api_port
|
||||
self.base_redis_port: int = base_redis_port
|
||||
self.project_root: Path = project_root
|
||||
self.harnesses: list[HonchoHarness] = []
|
||||
|
||||
|
|
@ -886,6 +882,7 @@ class HonchoHarnessPool:
|
|||
harness = HonchoHarness(
|
||||
db_port=base_db_port + i,
|
||||
api_port=base_api_port + i,
|
||||
redis_port=base_redis_port + i,
|
||||
project_root=project_root,
|
||||
instance_id=i,
|
||||
)
|
||||
|
|
@ -1017,16 +1014,16 @@ class HonchoHarnessPool:
|
|||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
await self.cleanup()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up all harnesses in the pool.
|
||||
"""
|
||||
print("\nCleaning up pool...")
|
||||
for harness in self.harnesses:
|
||||
print(f"\n--- Cleaning up Instance {harness.instance_id} ---")
|
||||
harness.cleanup()
|
||||
await harness.cleanup()
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -1058,6 +1055,13 @@ Examples:
|
|||
help="Base port for the FastAPI server (default: 8000)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--redis-port",
|
||||
type=int,
|
||||
default=6379,
|
||||
help="Base port for the Redis server (default: 6379)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--pool-size",
|
||||
type=int,
|
||||
|
|
@ -1105,6 +1109,7 @@ Examples:
|
|||
pool_size=args.pool_size,
|
||||
base_db_port=args.port,
|
||||
base_api_port=args.api_port,
|
||||
base_redis_port=args.redis_port,
|
||||
project_root=args.project_root,
|
||||
)
|
||||
asyncio.run(pool.run())
|
||||
|
|
@ -1112,6 +1117,7 @@ Examples:
|
|||
harness = HonchoHarness(
|
||||
db_port=args.port,
|
||||
api_port=args.api_port,
|
||||
redis_port=args.redis_port,
|
||||
project_root=args.project_root,
|
||||
instance_id=0,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ async def run_benchmark(candidates: list[Candidate], cases: list[Case]) -> int:
|
|||
ExplicitObservation(
|
||||
content=o,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
message_ids=[(0, 0)],
|
||||
message_ids=[0],
|
||||
session_name=case.name,
|
||||
)
|
||||
for o in case.new_observations
|
||||
|
|
|
|||
|
|
@ -564,12 +564,12 @@ def mock_tracked_db(db_session: AsyncSession):
|
|||
|
||||
with (
|
||||
patch("src.dependencies.tracked_db", mock_tracked_db_context),
|
||||
patch("src.deriver.deriver.tracked_db", mock_tracked_db_context),
|
||||
patch("src.deriver.queue_manager.tracked_db", mock_tracked_db_context),
|
||||
patch("src.routers.sessions.tracked_db", mock_tracked_db_context),
|
||||
patch("src.routers.peers.tracked_db", mock_tracked_db_context),
|
||||
patch("src.crud.representation.tracked_db", mock_tracked_db_context),
|
||||
patch("src.routers.peers.tracked_db", mock_tracked_db_context),
|
||||
patch("src.dreamer.dreamer.tracked_db", mock_tracked_db_context),
|
||||
patch("src.dreamer.consolidate.tracked_db", mock_tracked_db_context),
|
||||
patch("src.dreamer.dream_scheduler.tracked_db", mock_tracked_db_context),
|
||||
patch("src.dialectic.chat.tracked_db", mock_tracked_db_context),
|
||||
patch("src.utils.summarizer.tracked_db", mock_tracked_db_context),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,260 @@
|
|||
import pytest
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src.exceptions import ResourceNotFoundException
|
||||
|
||||
|
||||
class TestDocumentCRUD:
|
||||
"""Test suite for document CRUD operations"""
|
||||
|
||||
async def _setup_test_data(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_workspace: models.Workspace,
|
||||
test_peer: models.Peer,
|
||||
) -> tuple[models.Peer, models.Session, models.Collection]:
|
||||
"""Helper to set up test data with collection"""
|
||||
# Create another peer to observe
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.flush()
|
||||
|
||||
# Create collection (required for documents foreign key)
|
||||
collection = models.Collection(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
)
|
||||
db_session.add(collection)
|
||||
await db_session.flush()
|
||||
|
||||
return test_peer2, test_session, collection
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_documents_returns_query(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test get_all_documents returns a Select query for pagination"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_peer2, test_session, _ = await self._setup_test_data(
|
||||
db_session, test_workspace, test_peer
|
||||
)
|
||||
|
||||
# Create test documents
|
||||
doc1 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Test observation 1",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
doc2 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Test observation 2",
|
||||
embedding=[0.2] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add_all([doc1, doc2])
|
||||
await db_session.flush()
|
||||
|
||||
# Get documents query
|
||||
stmt = crud.get_all_documents(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
)
|
||||
|
||||
# Execute query
|
||||
result = await db_session.execute(stmt)
|
||||
documents = result.scalars().all()
|
||||
|
||||
assert len(documents) == 2
|
||||
assert documents[0].content in ["Test observation 1", "Test observation 2"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_documents(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test query_documents with semantic search"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_peer2, test_session, _ = await self._setup_test_data(
|
||||
db_session, test_workspace, test_peer
|
||||
)
|
||||
|
||||
# Create test documents with different embeddings
|
||||
doc1 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="User likes pizza",
|
||||
embedding=[0.9] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
doc2 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="User dislikes vegetables",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add_all([doc1, doc2])
|
||||
await db_session.flush()
|
||||
|
||||
# Query documents
|
||||
results = await crud.query_documents(
|
||||
db_session,
|
||||
workspace_name=test_workspace.name,
|
||||
query="food preferences",
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
top_k=10,
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_document_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test delete_document successfully deletes a document"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_peer2, test_session, _ = await self._setup_test_data(
|
||||
db_session, test_workspace, test_peer
|
||||
)
|
||||
|
||||
# Create a document
|
||||
doc = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Test observation",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.flush()
|
||||
|
||||
doc_id = doc.id
|
||||
|
||||
# Verify document exists
|
||||
stmt = select(models.Document).where(models.Document.id == doc_id)
|
||||
result = await db_session.execute(stmt)
|
||||
assert result.scalar_one_or_none() is not None
|
||||
|
||||
# Delete document
|
||||
await crud.delete_document(
|
||||
db_session,
|
||||
workspace_name=test_workspace.name,
|
||||
document_id=doc_id,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
)
|
||||
|
||||
# Verify document is deleted
|
||||
result = await db_session.execute(stmt)
|
||||
assert result.scalar_one_or_none() is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_document_not_found(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test delete_document raises exception for non-existent document"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_peer2, _, _ = await self._setup_test_data(
|
||||
db_session, test_workspace, test_peer
|
||||
)
|
||||
|
||||
# Try to delete non-existent document
|
||||
with pytest.raises(ResourceNotFoundException):
|
||||
await crud.delete_document(
|
||||
db_session,
|
||||
workspace_name=test_workspace.name,
|
||||
document_id="nonexistent_id",
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_documents(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test create_documents creates multiple documents"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_peer2, test_session, _ = await self._setup_test_data(
|
||||
db_session, test_workspace, test_peer
|
||||
)
|
||||
|
||||
# Prepare document creation schemas
|
||||
doc_schemas = [
|
||||
schemas.DocumentCreate(
|
||||
content="Observation 1",
|
||||
session_name=test_session.name,
|
||||
embedding=[0.1] * 1536,
|
||||
level="explicit",
|
||||
metadata=schemas.DocumentMetadata(
|
||||
message_ids=[1, 2, 3, 4, 5],
|
||||
message_created_at="2024-01-01T00:00:00Z",
|
||||
),
|
||||
),
|
||||
schemas.DocumentCreate(
|
||||
content="Observation 2",
|
||||
session_name=test_session.name,
|
||||
embedding=[0.2] * 1536,
|
||||
level="deductive",
|
||||
metadata=schemas.DocumentMetadata(
|
||||
message_ids=[6, 7, 8, 9, 10],
|
||||
message_created_at="2024-01-01T00:01:00Z",
|
||||
premises=["Premise 1", "Premise 2"],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
# Create documents
|
||||
count = await crud.create_documents(
|
||||
db_session,
|
||||
documents=doc_schemas,
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
)
|
||||
|
||||
assert count == 2
|
||||
|
||||
# Verify documents were created
|
||||
stmt = select(models.Document).where(
|
||||
models.Document.workspace_name == test_workspace.name,
|
||||
models.Document.observer == test_peer.name,
|
||||
models.Document.observed == test_peer2.name,
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
documents = result.scalars().all()
|
||||
|
||||
assert len(documents) == 2
|
||||
assert documents[0].content in ["Observation 1", "Observation 2"]
|
||||
assert documents[1].content in ["Observation 1", "Observation 2"]
|
||||
|
|
@ -144,8 +144,20 @@ def create_queue_payload() -> Callable[..., Any]:
|
|||
"message_public_id": message.public_id,
|
||||
}
|
||||
|
||||
configuration = schemas.ResolvedConfiguration(
|
||||
deriver=schemas.ResolvedDeriverConfiguration(enabled=True),
|
||||
peer_card=schemas.ResolvedPeerCardConfiguration(use=True, create=True),
|
||||
summary=schemas.ResolvedSummaryConfiguration(
|
||||
enabled=True,
|
||||
messages_per_short_summary=10,
|
||||
messages_per_long_summary=20,
|
||||
),
|
||||
dream=schemas.ResolvedDreamConfiguration(enabled=True),
|
||||
)
|
||||
|
||||
return create_payload(
|
||||
message=message_dict,
|
||||
configuration=configuration,
|
||||
task_type=task_type,
|
||||
message_seq_in_session=message_seq_in_session,
|
||||
observer=observer,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from typing import Any
|
|||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
|
|
@ -108,6 +109,8 @@ class TestDeriverProcessing:
|
|||
|
||||
async def test_representation_batch_uses_earliest_cutoff(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
sample_session_with_peers: tuple[models.Session, list[models.Peer]],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Ensure batching history cutoff uses the earliest payload in the batch."""
|
||||
|
|
@ -134,50 +137,43 @@ class TestDeriverProcessing:
|
|||
),
|
||||
)
|
||||
|
||||
# Avoid DB access for collection and peer card
|
||||
monkeypatch.setattr(
|
||||
"src.crud.get_or_create_collection",
|
||||
AsyncMock(return_value=type("Collection", (), {"name": "dummy"})()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.crud.get_peer_card",
|
||||
AsyncMock(return_value=[]),
|
||||
)
|
||||
# Short-circuit tracked_db context manager
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def _no_db(_label: str):
|
||||
yield object()
|
||||
|
||||
monkeypatch.setattr("src.deriver.deriver.tracked_db", _no_db)
|
||||
|
||||
# Avoid executing the full reasoning pipeline; we only care about cutoff behavior.
|
||||
monkeypatch.setattr(
|
||||
"src.deriver.deriver.CertaintyReasoner.reason",
|
||||
AsyncMock(return_value=Representation(explicit=[], deductive=[])),
|
||||
)
|
||||
|
||||
# Create test messages with different IDs (earlier message has lower ID)
|
||||
# Use the real session and workspace from fixtures
|
||||
session, peers = sample_session_with_peers
|
||||
alice = peers[0]
|
||||
|
||||
# Create test messages with different IDs in the database
|
||||
now = datetime.now(timezone.utc)
|
||||
messages: list[models.Message] = []
|
||||
for i in range(8):
|
||||
message_id = 100 + i # 100, 101, 102, ..., 107
|
||||
messages.append(
|
||||
models.Message(
|
||||
id=message_id,
|
||||
workspace_name="test_workspace",
|
||||
session_name="test_session",
|
||||
peer_name="alice",
|
||||
content=f"message {message_id}",
|
||||
seq_in_session=i + 1,
|
||||
token_count=0,
|
||||
created_at=now - timedelta(minutes=7 - i),
|
||||
)
|
||||
message = models.Message(
|
||||
workspace_name=session.workspace_name,
|
||||
session_name=session.name,
|
||||
peer_name=alice.name,
|
||||
content=f"message {i}",
|
||||
seq_in_session=i + 1,
|
||||
token_count=10,
|
||||
created_at=now - timedelta(minutes=7 - i),
|
||||
)
|
||||
db_session.add(message)
|
||||
messages.append(message)
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
# Refresh messages to get their IDs
|
||||
for message in messages:
|
||||
await db_session.refresh(message)
|
||||
|
||||
await process_representation_tasks_batch(
|
||||
observer="alice", observed="alice", messages=messages
|
||||
observer=alice.name,
|
||||
message_level_configuration=None,
|
||||
observed=alice.name,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
# Verify that the earliest message ID was used as the cutoff
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(aqs)
|
||||
|
||||
_, items_to_process = await qm.get_queue_item_batch(
|
||||
_, items_to_process, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=first.work_unit_key,
|
||||
aqs_id=aqs.id,
|
||||
|
|
@ -184,7 +184,7 @@ class TestQueueProcessing:
|
|||
# Mark first processed, next should be the second
|
||||
first.processed = True
|
||||
await db_session.commit()
|
||||
_, items_to_process2 = await qm.get_queue_item_batch(
|
||||
_, items_to_process2, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=first.work_unit_key,
|
||||
aqs_id=aqs.id,
|
||||
|
|
@ -343,6 +343,8 @@ class TestQueueProcessing:
|
|||
|
||||
async def mock_process_representation_batch(
|
||||
messages: list[models.Message],
|
||||
_message_level_configuration: Any,
|
||||
*,
|
||||
observed: str | None = None, # pyright: ignore[reportUnusedParameter]
|
||||
observer: str | None = None, # pyright: ignore[reportUnusedParameter]
|
||||
) -> None:
|
||||
|
|
@ -471,7 +473,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(alice_aqs)
|
||||
|
||||
alice_messages, alice_items = await qm.get_queue_item_batch(
|
||||
alice_messages, alice_items, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=alice_work_unit_key,
|
||||
aqs_id=alice_aqs.id,
|
||||
|
|
@ -499,7 +501,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(bob_aqs)
|
||||
|
||||
bob_messages, bob_items = await qm.get_queue_item_batch(
|
||||
bob_messages, bob_items, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=bob_work_unit_key,
|
||||
aqs_id=bob_aqs.id,
|
||||
|
|
@ -525,7 +527,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(steve_aqs)
|
||||
|
||||
steve_messages, steve_items = await qm.get_queue_item_batch(
|
||||
steve_messages, steve_items, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=steve_work_unit_key,
|
||||
aqs_id=steve_aqs.id,
|
||||
|
|
@ -641,7 +643,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(alice_aqs)
|
||||
|
||||
alice_messages2, _ = await qm.get_queue_item_batch(
|
||||
alice_messages2, _, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=alice_work_unit_key,
|
||||
aqs_id=alice_aqs.id,
|
||||
|
|
@ -664,7 +666,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(bob_aqs)
|
||||
|
||||
bob_messages2, _ = await qm.get_queue_item_batch(
|
||||
bob_messages2, _, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=bob_work_unit_key,
|
||||
aqs_id=bob_aqs.id,
|
||||
|
|
@ -683,7 +685,7 @@ class TestQueueProcessing:
|
|||
await db_session.commit()
|
||||
await db_session.refresh(steve_aqs)
|
||||
|
||||
steve_messages2, _ = await qm.get_queue_item_batch(
|
||||
steve_messages2, _, _ = await qm.get_queue_item_batch(
|
||||
task_type="representation",
|
||||
work_unit_key=steve_work_unit_key,
|
||||
aqs_id=steve_aqs.id,
|
||||
|
|
@ -897,6 +899,8 @@ class TestQueueProcessing:
|
|||
|
||||
async def mock_process_representation_batch(
|
||||
messages: list[models.Message],
|
||||
_message_level_configuration: Any,
|
||||
*,
|
||||
observed: str | None = None, # pyright: ignore[reportUnusedParameter]
|
||||
observer: str | None = None, # pyright: ignore[reportUnusedParameter]
|
||||
) -> None:
|
||||
|
|
@ -1013,6 +1017,8 @@ class TestQueueProcessing:
|
|||
|
||||
async def mock_process_representation_batch(
|
||||
messages: list[models.Message],
|
||||
_message_level_configuration: Any,
|
||||
*,
|
||||
observed: str | None = None, # pyright: ignore[reportUnusedParameter]
|
||||
observer: str | None = None, # pyright: ignore[reportUnusedParameter]
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ def test_representation_is_empty_and_diff():
|
|||
exp_shared_1 = ExplicitObservation(
|
||||
content="A",
|
||||
created_at=shared_time,
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="s",
|
||||
)
|
||||
exp_shared_2 = ExplicitObservation(
|
||||
content="B",
|
||||
created_at=shared_time,
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="s",
|
||||
)
|
||||
rep1 = Representation(explicit=[exp_shared_1], deductive=[])
|
||||
|
|
@ -32,7 +32,7 @@ def test_representation_is_empty_and_diff():
|
|||
ExplicitObservation(
|
||||
content="A",
|
||||
created_at=shared_time,
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="s",
|
||||
),
|
||||
exp_shared_2,
|
||||
|
|
@ -53,12 +53,12 @@ def test_representation_formatting_methods():
|
|||
e = ExplicitObservation(
|
||||
content="has a dog",
|
||||
created_at=now,
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="s",
|
||||
)
|
||||
d = DeductiveObservation(
|
||||
created_at=now,
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="s",
|
||||
conclusion="owns a pet",
|
||||
premises=[e.content],
|
||||
|
|
@ -85,7 +85,7 @@ def test_prompt_representation_conversion():
|
|||
timestamp = datetime.datetime(2025, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
rep = Representation.from_prompt_representation(
|
||||
pr,
|
||||
message_ids=(1, 1),
|
||||
message_ids=[1],
|
||||
session_name="s",
|
||||
created_at=timestamp,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class TestEnqueueFunction:
|
|||
test_session = models.Session(
|
||||
workspace_name=test_workspace.name,
|
||||
name=str(generate_nanoid()),
|
||||
configuration={"deriver_disabled": True},
|
||||
configuration={"deriver": {"enabled": False}},
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
|
@ -1431,12 +1431,22 @@ class TestGenerateQueueRecordsSeqInSession:
|
|||
{"observe_others": True},
|
||||
]
|
||||
}
|
||||
resolved_configuration = schemas.ResolvedConfiguration(
|
||||
deriver=schemas.ResolvedDeriverConfiguration(enabled=True),
|
||||
summary=schemas.ResolvedSummaryConfiguration(
|
||||
enabled=True,
|
||||
messages_per_short_summary=20,
|
||||
messages_per_long_summary=60,
|
||||
),
|
||||
peer_card=schemas.ResolvedPeerCardConfiguration(use=True, create=True),
|
||||
dream=schemas.ResolvedDreamConfiguration(enabled=True),
|
||||
)
|
||||
records = await generate_queue_records(
|
||||
db_session=mock_db_session,
|
||||
message=message_payload,
|
||||
peers_with_configuration=peers_config,
|
||||
session_id=test_session.id,
|
||||
deriver_disabled=False,
|
||||
conf=resolved_configuration,
|
||||
)
|
||||
|
||||
mock_crud.assert_not_called()
|
||||
|
|
@ -1501,12 +1511,22 @@ class TestGenerateQueueRecordsSeqInSession:
|
|||
{"observe_others": True},
|
||||
]
|
||||
}
|
||||
resolved_configuration = schemas.ResolvedConfiguration(
|
||||
deriver=schemas.ResolvedDeriverConfiguration(enabled=True),
|
||||
summary=schemas.ResolvedSummaryConfiguration(
|
||||
enabled=True,
|
||||
messages_per_short_summary=20,
|
||||
messages_per_long_summary=60,
|
||||
),
|
||||
peer_card=schemas.ResolvedPeerCardConfiguration(use=True, create=True),
|
||||
dream=schemas.ResolvedDreamConfiguration(enabled=True),
|
||||
)
|
||||
records = await generate_queue_records(
|
||||
db_session=mock_db_session,
|
||||
message=message_payload,
|
||||
peers_with_configuration=peers_config,
|
||||
session_id=test_session.id,
|
||||
deriver_disabled=False,
|
||||
conf=resolved_configuration,
|
||||
)
|
||||
|
||||
# The CRUD function SHOULD have been called as fallback
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ class TestRepresentationWorkflow:
|
|||
explicit_obs1 = ExplicitObservation(
|
||||
content="User likes dogs",
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="test_session",
|
||||
)
|
||||
explicit_obs2 = ExplicitObservation(
|
||||
content="User has a pet named Rover",
|
||||
created_at=datetime(2025, 1, 1, 12, 1, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(2, 2)],
|
||||
message_ids=[2],
|
||||
session_name="test_session",
|
||||
)
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ class TestRepresentationWorkflow:
|
|||
conclusion="User probably has a dog named Rover",
|
||||
premises=["User likes dogs", "User has a pet named Rover"],
|
||||
created_at=datetime(2025, 1, 1, 12, 2, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(3, 3)],
|
||||
message_ids=[3],
|
||||
session_name="test_session",
|
||||
)
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ class TestRepresentationWorkflow:
|
|||
ExplicitObservation(
|
||||
content="User likes cats",
|
||||
created_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
]
|
||||
|
|
@ -156,13 +156,13 @@ class TestRepresentationWorkflow:
|
|||
ExplicitObservation(
|
||||
content="User likes cats", # Duplicate
|
||||
created_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
),
|
||||
ExplicitObservation(
|
||||
content="User likes dogs", # New
|
||||
created_at=datetime(2025, 1, 1, 11, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(2, 2)],
|
||||
message_ids=[2],
|
||||
session_name="session1",
|
||||
),
|
||||
]
|
||||
|
|
@ -186,7 +186,7 @@ class TestRepresentationWorkflow:
|
|||
ExplicitObservation(
|
||||
content="User likes birds",
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(3, 3)],
|
||||
message_ids=[3],
|
||||
session_name="session1",
|
||||
)
|
||||
]
|
||||
|
|
@ -224,7 +224,7 @@ class TestDocumentCreationWorkflow:
|
|||
session_name="test_session",
|
||||
level="explicit",
|
||||
internal_metadata={
|
||||
"message_ids": [(1, 1)],
|
||||
"message_ids": [1],
|
||||
"session_name": "test_session",
|
||||
},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
|
|
@ -321,7 +321,7 @@ class TestDocumentCreationWorkflow:
|
|||
content="User said they like programming",
|
||||
level="explicit",
|
||||
internal_metadata={
|
||||
"message_ids": [(1, 1)],
|
||||
"message_ids": [1],
|
||||
},
|
||||
session_name="test_session",
|
||||
embedding=[0.1] * 1536,
|
||||
|
|
@ -335,7 +335,7 @@ class TestDocumentCreationWorkflow:
|
|||
content="User is likely a software developer",
|
||||
level="deductive",
|
||||
internal_metadata={
|
||||
"message_ids": [(1, 1)],
|
||||
"message_ids": [1],
|
||||
"premises": ["User said they like programming"],
|
||||
},
|
||||
session_name="test_session",
|
||||
|
|
@ -351,13 +351,13 @@ class TestDocumentCreationWorkflow:
|
|||
|
||||
explicit_obs = representation.explicit[0]
|
||||
assert explicit_obs.content == "User said they like programming"
|
||||
assert explicit_obs.message_ids == [(1, 1)]
|
||||
assert explicit_obs.message_ids == [1]
|
||||
assert explicit_obs.session_name == "test_session"
|
||||
|
||||
deductive_obs = representation.deductive[0]
|
||||
assert deductive_obs.conclusion == "User is likely a software developer"
|
||||
assert deductive_obs.premises == ["User said they like programming"]
|
||||
assert deductive_obs.message_ids == [(1, 1)]
|
||||
assert deductive_obs.message_ids == [1]
|
||||
assert deductive_obs.session_name == "test_session"
|
||||
|
||||
async def create_test_workspace_and_peer(
|
||||
|
|
@ -424,7 +424,7 @@ class TestPromptRepresentationConversion:
|
|||
|
||||
representation = Representation.from_prompt_representation(
|
||||
prompt_rep,
|
||||
message_ids=(123, 123),
|
||||
message_ids=[123],
|
||||
session_name="test_session",
|
||||
created_at=timestamp,
|
||||
)
|
||||
|
|
@ -434,7 +434,7 @@ class TestPromptRepresentationConversion:
|
|||
|
||||
# Check explicit observations
|
||||
assert representation.explicit[0].content == "User likes coffee"
|
||||
assert representation.explicit[0].message_ids == [(123, 123)]
|
||||
assert representation.explicit[0].message_ids == [123]
|
||||
assert representation.explicit[0].session_name == "test_session"
|
||||
assert representation.explicit[1].content == "User works remotely"
|
||||
assert representation.explicit[0].created_at == timestamp
|
||||
|
|
@ -446,7 +446,7 @@ class TestPromptRepresentationConversion:
|
|||
== "User probably works from a coffee shop sometimes"
|
||||
)
|
||||
assert deductive_obs.premises == ["User likes coffee", "User works remotely"]
|
||||
assert deductive_obs.message_ids == [(123, 123)]
|
||||
assert deductive_obs.message_ids == [123]
|
||||
assert deductive_obs.session_name == "test_session"
|
||||
assert deductive_obs.created_at == timestamp
|
||||
|
||||
|
|
@ -455,7 +455,7 @@ class TestPromptRepresentationConversion:
|
|||
empty_prompt_rep = PromptRepresentation()
|
||||
representation = Representation.from_prompt_representation(
|
||||
empty_prompt_rep,
|
||||
message_ids=(1, 1),
|
||||
message_ids=[1],
|
||||
session_name="test",
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
|
@ -474,21 +474,21 @@ class TestRepresentationHashingAndEquality:
|
|||
obs1 = ExplicitObservation(
|
||||
content="Test content",
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
|
||||
obs2 = ExplicitObservation(
|
||||
content="Test content",
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
|
||||
obs3 = ExplicitObservation(
|
||||
content="Different content",
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ class TestRepresentationHashingAndEquality:
|
|||
conclusion="Test conclusion",
|
||||
premises=["premise1", "premise2"],
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
|
||||
|
|
@ -515,7 +515,7 @@ class TestRepresentationHashingAndEquality:
|
|||
conclusion="Test conclusion",
|
||||
premises=["premise1", "premise2"],
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
|
||||
|
|
@ -523,7 +523,7 @@ class TestRepresentationHashingAndEquality:
|
|||
conclusion="Different conclusion",
|
||||
premises=["premise1", "premise2"],
|
||||
created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
message_ids=[(1, 1)],
|
||||
message_ids=[1],
|
||||
session_name="session1",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -333,3 +333,256 @@ async def test_file_too_large_rejected(
|
|||
|
||||
# Should reject the file with 413 (Request Entity Too Large)
|
||||
assert response.status_code == 413
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_metadata(
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test file upload with metadata parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for session endpoint
|
||||
test_session = await _create_test_session(db_session, test_workspace)
|
||||
session_name = test_session.name
|
||||
|
||||
# Create a mock text file
|
||||
file_content = "Test file with metadata"
|
||||
file_data = io.BytesIO(file_content.encode("utf-8"))
|
||||
|
||||
# Prepare metadata
|
||||
metadata = {"source": "test", "category": "upload", "priority": 1}
|
||||
|
||||
files = {"file": ("test_metadata.txt", file_data, "text/plain")}
|
||||
form_data = {
|
||||
"peer_id": test_peer.name,
|
||||
"metadata": json.dumps(metadata),
|
||||
}
|
||||
|
||||
url = _get_upload_url(test_workspace.name, session_name)
|
||||
response = client.post(url, files=files, data=form_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
|
||||
message = data[0]
|
||||
assert file_content in message["content"]
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["session_id"] == session_name
|
||||
# Check that metadata was applied
|
||||
assert message["metadata"] == metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_configuration(
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test file upload with configuration parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for session endpoint
|
||||
test_session = await _create_test_session(db_session, test_workspace)
|
||||
session_name = test_session.name
|
||||
|
||||
# Create a mock text file
|
||||
file_content = "Test file with configuration"
|
||||
file_data = io.BytesIO(file_content.encode("utf-8"))
|
||||
|
||||
# Prepare configuration
|
||||
configuration = {"skip_deriver": True, "custom_flag": "test"}
|
||||
|
||||
files = {"file": ("test_config.txt", file_data, "text/plain")}
|
||||
form_data = {
|
||||
"peer_id": test_peer.name,
|
||||
"configuration": json.dumps(configuration),
|
||||
}
|
||||
|
||||
url = _get_upload_url(test_workspace.name, session_name)
|
||||
response = client.post(url, files=files, data=form_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
|
||||
message = data[0]
|
||||
assert file_content in message["content"]
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["session_id"] == session_name
|
||||
# Note: Configuration is used during processing, may not be directly stored
|
||||
# This test confirms the endpoint accepts it without error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_created_at(
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test file upload with created_at parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for session endpoint
|
||||
test_session = await _create_test_session(db_session, test_workspace)
|
||||
session_name = test_session.name
|
||||
|
||||
# Create a mock text file
|
||||
file_content = "Test file with created_at"
|
||||
file_data = io.BytesIO(file_content.encode("utf-8"))
|
||||
|
||||
# Prepare created_at timestamp (ISO 8601 format)
|
||||
from datetime import datetime, timezone
|
||||
|
||||
test_timestamp = datetime(2023, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
|
||||
created_at_str = test_timestamp.isoformat()
|
||||
|
||||
files = {"file": ("test_timestamp.txt", file_data, "text/plain")}
|
||||
form_data = {
|
||||
"peer_id": test_peer.name,
|
||||
"created_at": created_at_str,
|
||||
}
|
||||
|
||||
url = _get_upload_url(test_workspace.name, session_name)
|
||||
response = client.post(url, files=files, data=form_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
|
||||
message = data[0]
|
||||
assert file_content in message["content"]
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["session_id"] == session_name
|
||||
# Check that created_at was applied (compare timestamps, allowing for small differences)
|
||||
message_timestamp = datetime.fromisoformat(
|
||||
message["created_at"].replace("Z", "+00:00")
|
||||
)
|
||||
assert abs((message_timestamp - test_timestamp).total_seconds()) < 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_all_parameters(
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test file upload with metadata, configuration, and created_at all together"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for session endpoint
|
||||
test_session = await _create_test_session(db_session, test_workspace)
|
||||
session_name = test_session.name
|
||||
|
||||
# Create a mock text file
|
||||
file_content = "Test file with all parameters"
|
||||
file_data = io.BytesIO(file_content.encode("utf-8"))
|
||||
|
||||
# Prepare all parameters
|
||||
metadata = {"source": "comprehensive_test", "version": "1.0"}
|
||||
configuration = {"skip_deriver": False, "test_mode": True}
|
||||
from datetime import datetime, timezone
|
||||
|
||||
test_timestamp = datetime(2023, 6, 20, 14, 15, 30, tzinfo=timezone.utc)
|
||||
created_at_str = test_timestamp.isoformat()
|
||||
|
||||
files = {"file": ("test_all_params.txt", file_data, "text/plain")}
|
||||
form_data = {
|
||||
"peer_id": test_peer.name,
|
||||
"metadata": json.dumps(metadata),
|
||||
"configuration": json.dumps(configuration),
|
||||
"created_at": created_at_str,
|
||||
}
|
||||
|
||||
url = _get_upload_url(test_workspace.name, session_name)
|
||||
response = client.post(url, files=files, data=form_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
|
||||
message = data[0]
|
||||
assert file_content in message["content"]
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["session_id"] == session_name
|
||||
# Check metadata
|
||||
assert message["metadata"] == metadata
|
||||
# Check created_at
|
||||
message_timestamp = datetime.fromisoformat(
|
||||
message["created_at"].replace("Z", "+00:00")
|
||||
)
|
||||
assert abs((message_timestamp - test_timestamp).total_seconds()) < 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_invalid_metadata_json(
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test file upload with invalid JSON in metadata parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for session endpoint
|
||||
test_session = await _create_test_session(db_session, test_workspace)
|
||||
session_name = test_session.name
|
||||
|
||||
file_content = "Test file"
|
||||
file_data = io.BytesIO(file_content.encode("utf-8"))
|
||||
|
||||
files = {"file": ("test.txt", file_data, "text/plain")}
|
||||
form_data = {
|
||||
"peer_id": test_peer.name,
|
||||
"metadata": "invalid json {", # Invalid JSON
|
||||
}
|
||||
|
||||
url = _get_upload_url(test_workspace.name, session_name)
|
||||
response = client.post(url, files=files, data=form_data)
|
||||
|
||||
# Should still succeed but metadata will be None (backend handles gracefully)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# Metadata parsing failure is logged but doesn't fail the request
|
||||
assert len(data) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_large_file_upload_with_metadata(
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that large files with metadata get split correctly and metadata is applied to all chunks"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for session endpoint
|
||||
test_session = await _create_test_session(db_session, test_workspace)
|
||||
session_name = test_session.name
|
||||
|
||||
# Create a large text file that will require chunking
|
||||
large_content = "This is a test line.\n" * 3000 # Should exceed 49500 chars
|
||||
file_data = io.BytesIO(large_content.encode("utf-8"))
|
||||
|
||||
metadata = {"source": "chunked_test", "chunked": True}
|
||||
|
||||
files = {"file": ("large_metadata.txt", file_data, "text/plain")}
|
||||
form_data = {
|
||||
"peer_id": test_peer.name,
|
||||
"metadata": json.dumps(metadata),
|
||||
}
|
||||
|
||||
url = _get_upload_url(test_workspace.name, session_name)
|
||||
response = client.post(url, files=files, data=form_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) > 1 # Should be multiple messages due to chunking
|
||||
|
||||
# All messages should have the same metadata, peer_id and session_id
|
||||
for message in data:
|
||||
assert message["peer_id"] == test_peer.name
|
||||
assert message["session_id"] == session_name
|
||||
assert message["metadata"] == metadata
|
||||
|
|
|
|||
|
|
@ -0,0 +1,755 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models
|
||||
from src.models import Peer, Workspace
|
||||
|
||||
|
||||
class TestObservationRoutes:
|
||||
"""Test suite for observation API endpoints"""
|
||||
|
||||
async def _create_collection(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
workspace_name: str,
|
||||
observer: str,
|
||||
observed: str,
|
||||
) -> models.Collection:
|
||||
"""Helper to create collection for tests"""
|
||||
collection = models.Collection(
|
||||
workspace_name=workspace_name,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
db_session.add(collection)
|
||||
await db_session.flush()
|
||||
return collection
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_observations_success(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test listing observations for a session"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create test observations (documents)
|
||||
doc1 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="User prefers dark mode",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
doc2 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="User works late at night",
|
||||
embedding=[0.2] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add_all([doc1, doc2])
|
||||
await db_session.commit()
|
||||
|
||||
# List observations
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list",
|
||||
json={"filters": {"session_id": test_session.name}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) == 2
|
||||
|
||||
# Check observation structure
|
||||
observation = data["items"][0]
|
||||
assert "id" in observation
|
||||
assert "content" in observation
|
||||
assert "observer_id" in observation
|
||||
assert "observed_id" in observation
|
||||
assert "session_id" in observation
|
||||
assert "created_at" in observation
|
||||
|
||||
# Verify content
|
||||
contents = [item["content"] for item in data["items"]]
|
||||
assert "User prefers dark mode" in contents
|
||||
assert "User works late at night" in contents
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_observations_empty_session(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test listing observations for a session with no observations"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
# Create a session without any observations
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# List observations
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list",
|
||||
json={"filters": {"session_id": test_session.name}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_observations_with_filters(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test listing observations with observer/observed filters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create two more peers
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
test_peer3 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add_all([test_peer2, test_peer3])
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collections for both observer/observed pairs
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer2.name, test_peer3.name
|
||||
)
|
||||
|
||||
# Create observations with different observer/observed pairs
|
||||
doc1 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Peer1 observes Peer2",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
doc2 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer2.name,
|
||||
observed=test_peer3.name,
|
||||
content="Peer2 observes Peer3",
|
||||
embedding=[0.2] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add_all([doc1, doc2])
|
||||
await db_session.commit()
|
||||
|
||||
# List observations filtered by observer
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list",
|
||||
json={
|
||||
"filters": {"observer": test_peer.name, "session_id": test_session.name}
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) == 1
|
||||
assert data["items"][0]["content"] == "Peer1 observes Peer2"
|
||||
assert data["items"][0]["observer_id"] == test_peer.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_observations_reverse_order(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test listing observations in reverse chronological order"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create observations
|
||||
doc1 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="First observation",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc1)
|
||||
await db_session.flush()
|
||||
|
||||
doc2 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Second observation",
|
||||
embedding=[0.2] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc2)
|
||||
await db_session.commit()
|
||||
|
||||
# List observations in reverse (oldest first)
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list?reverse=true",
|
||||
json={"filters": {"session_id": test_session.name}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) == 2
|
||||
assert data["items"][0]["content"] == "First observation"
|
||||
assert data["items"][1]["content"] == "Second observation"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_observations_pagination(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test pagination of observations list"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create multiple observations
|
||||
for i in range(15):
|
||||
doc = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content=f"Observation {i}",
|
||||
embedding=[0.1 * i] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
# Get first page (default size)
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list?page=1&size=10",
|
||||
json={"filters": {"session_id": test_session.name}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) == 10
|
||||
assert data["total"] == 15
|
||||
|
||||
# Get second page
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list?page=2&size=10",
|
||||
json={"filters": {"session_id": test_session.name}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) == 5
|
||||
assert data["total"] == 15
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_observations_success(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test querying observations with semantic search"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create test observations
|
||||
doc1 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="User loves pizza and pasta",
|
||||
embedding=[0.9] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
doc2 = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="User dislikes vegetables",
|
||||
embedding=[0.5] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add_all([doc1, doc2])
|
||||
await db_session.commit()
|
||||
|
||||
# Query observations
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/query",
|
||||
json={
|
||||
"query": "food preferences",
|
||||
"filters": {
|
||||
"observer": test_peer.name,
|
||||
"observed": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1 # pyright: ignore
|
||||
|
||||
# Check observation structure
|
||||
observation = data[0] # pyright: ignore
|
||||
assert "id" in observation
|
||||
assert "content" in observation
|
||||
assert "observer_id" in observation
|
||||
assert "observed_id" in observation
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_observations_with_top_k(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test querying observations with top_k limit"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create multiple observations
|
||||
for i in range(5):
|
||||
doc = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content=f"Observation about topic {i}",
|
||||
embedding=[0.1 * i] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
# Query with top_k=2
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/query",
|
||||
json={
|
||||
"query": "relevant topic",
|
||||
"top_k": 2,
|
||||
"filters": {
|
||||
"observer": test_peer.name,
|
||||
"observed": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) <= 2 # pyright: ignore
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_observations_with_distance_threshold(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test querying observations with distance threshold"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create test observation
|
||||
doc = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Test observation",
|
||||
embedding=[0.5] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
# Query with distance threshold
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/query",
|
||||
json={
|
||||
"query": "test",
|
||||
"distance": 0.8,
|
||||
"filters": {
|
||||
"observer": test_peer.name,
|
||||
"observed": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_observations_requires_observer_observed(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test query observations requires observer and observed in filters"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Query without observer/observed filters should fail
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/query",
|
||||
json={"query": "test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_observations_invalid_top_k(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test query observations validates top_k range"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Query with invalid top_k (too high)
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/query",
|
||||
json={
|
||||
"query": "test",
|
||||
"top_k": 101, # Max is 100
|
||||
"filters": {
|
||||
"observer": test_peer.name,
|
||||
"observed": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_observation_success(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test deleting an observation"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create a test observation
|
||||
doc = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Test observation to delete",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
observation_id = doc.id
|
||||
|
||||
# Delete observation
|
||||
response = client.delete(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/{observation_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "Observation deleted successfully"
|
||||
|
||||
# Verify observation is deleted
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(models.Document).where(models.Document.id == observation_id)
|
||||
result = await db_session.execute(stmt)
|
||||
assert result.scalar_one_or_none() is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_observation_not_found(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test deleting a non-existent observation"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Try to delete non-existent observation
|
||||
response = client.delete(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/nonexistent_id"
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_observations_nonexistent_session(
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test listing observations for non-existent session"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
# Try to list observations for non-existent session
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list",
|
||||
json={"filters": {"session_id": "nonexistent_session"}},
|
||||
)
|
||||
|
||||
# Should return empty result, not error (session might exist but no observations)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observations_field_mapping(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that observation fields are properly mapped from document model"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
test_peer2 = models.Peer(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_peer2)
|
||||
await db_session.flush()
|
||||
|
||||
# Create a session
|
||||
test_session = models.Session(
|
||||
name=str(generate_nanoid()), workspace_name=test_workspace.name
|
||||
)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create collection
|
||||
await self._create_collection(
|
||||
db_session, test_workspace.name, test_peer.name, test_peer2.name
|
||||
)
|
||||
|
||||
# Create test observation
|
||||
doc = models.Document(
|
||||
workspace_name=test_workspace.name,
|
||||
observer=test_peer.name,
|
||||
observed=test_peer2.name,
|
||||
content="Test observation content",
|
||||
embedding=[0.1] * 1536,
|
||||
session_name=test_session.name,
|
||||
)
|
||||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
# List observations
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list",
|
||||
json={"filters": {"session_id": test_session.name}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
observation = data["items"][0]
|
||||
|
||||
# Verify field mappings
|
||||
assert observation["id"] == doc.id
|
||||
assert observation["content"] == doc.content
|
||||
assert observation["observer_id"] == doc.observer
|
||||
assert observation["observed_id"] == doc.observed
|
||||
assert observation["session_id"] == doc.session_name
|
||||
assert "created_at" in observation
|
||||
|
||||
# Verify internal fields are NOT exposed
|
||||
assert "embedding" not in observation
|
||||
assert "internal_metadata" not in observation
|
||||
assert "collection" not in observation
|
||||
|
|
@ -376,10 +376,278 @@ def test_get_peer_representation_with_session(
|
|||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"session_id": session_id,
|
||||
"queries": "Hello, how are you?",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
assert isinstance(data["representation"], dict)
|
||||
|
||||
|
||||
def test_get_peer_representation_global(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation without session_id (global representation)"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test global representation (no session_id)
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
assert isinstance(data["representation"], dict)
|
||||
|
||||
|
||||
def test_get_peer_representation_with_target(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with target parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a second peer to be the target
|
||||
target_peer_name = str(generate_nanoid())
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers",
|
||||
json={"name": target_peer_name, "metadata": {}},
|
||||
)
|
||||
|
||||
# Test representation of target from observer's perspective
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"target": target_peer_name,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
assert isinstance(data["representation"], dict)
|
||||
|
||||
|
||||
def test_get_peer_representation_with_search_query(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with search_query parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test representation with semantic search query
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "What are my interests and hobbies?",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
|
||||
def test_get_peer_representation_with_search_top_k(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with search_top_k parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test with valid search_top_k values
|
||||
for top_k in [1, 10, 50, 100]:
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test query",
|
||||
"search_top_k": top_k,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
|
||||
def test_get_peer_representation_with_search_max_distance(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with search_max_distance parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test with valid search_max_distance values (0.0 to 1.0)
|
||||
for max_distance in [0.0, 0.5, 0.8, 1.0]:
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test query",
|
||||
"search_max_distance": max_distance,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
|
||||
def test_get_peer_representation_with_include_most_derived(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with include_most_derived parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test with include_most_derived=True
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test query",
|
||||
"include_most_derived": True,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
# Test with include_most_derived=False
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test query",
|
||||
"include_most_derived": False,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
|
||||
def test_get_peer_representation_with_max_observations(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with max_observations parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test with various max_observations values
|
||||
for max_obs in [1, 25, 50, 100]:
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test query",
|
||||
"max_observations": max_obs,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
|
||||
def test_get_peer_representation_with_all_parameters(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with all optional parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
||||
# Create a session and target peer
|
||||
target_peer_name = str(generate_nanoid())
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers",
|
||||
json={"name": target_peer_name, "metadata": {}},
|
||||
)
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions",
|
||||
json={
|
||||
"id": session_id,
|
||||
"peer_names": {test_peer.name: {}, target_peer_name: {}},
|
||||
},
|
||||
)
|
||||
|
||||
# Test with all parameters
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"session_id": session_id,
|
||||
"target": target_peer_name,
|
||||
"search_query": "What do I know about this peer?",
|
||||
"search_top_k": 15,
|
||||
"search_max_distance": 0.75,
|
||||
"include_most_derived": True,
|
||||
"max_observations": 30,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
assert isinstance(data["representation"], dict)
|
||||
|
||||
|
||||
def test_get_peer_representation_structure(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test that peer representation response has correct structure"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Get representation and validate structure
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Validate response structure
|
||||
assert "representation" in data
|
||||
assert isinstance(data["representation"], dict)
|
||||
|
||||
# Representation should have expected keys based on Representation type
|
||||
representation = data["representation"]
|
||||
# The exact keys depend on the Representation implementation,
|
||||
# but we can verify it's a dict
|
||||
assert isinstance(representation, dict)
|
||||
|
||||
|
||||
def test_get_peer_representation_boundary_values(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with boundary values for numeric parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test minimum values
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test",
|
||||
"search_top_k": 1,
|
||||
"search_max_distance": 0.0,
|
||||
"max_observations": 1,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test maximum values
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test",
|
||||
"search_top_k": 100,
|
||||
"search_max_distance": 1.0,
|
||||
"max_observations": 100,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_get_peer_representation_default_max_observations(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test that max_observations defaults to 25 when not provided"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Test without max_observations - should use default of 25
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
|
||||
json={
|
||||
"search_query": "test query",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "representation" in data
|
||||
|
||||
|
||||
def test_search_peer(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
|
|
|
|||
|
|
@ -270,6 +270,39 @@ def test_update_session(client: TestClient, sample_data: tuple[Workspace, Peer])
|
|||
assert data["metadata"] == {"new_key": "new_value"}
|
||||
|
||||
|
||||
def test_delete_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test deleting a session"""
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions",
|
||||
json={
|
||||
"id": session_id,
|
||||
"peer_names": {test_peer.name: {}},
|
||||
"metadata": {"test_key": "test_value"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Delete the session
|
||||
response = client.delete(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}",
|
||||
)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert data["message"] == "Session deleted successfully"
|
||||
|
||||
# Verify the session is deleted by trying to list it
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/list",
|
||||
json={"filters": {"id": session_id}},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) == 0
|
||||
|
||||
|
||||
def test_update_session_with_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
|
|
@ -347,36 +380,6 @@ def test_update_session_with_null_configuration(
|
|||
assert "configuration" in data
|
||||
|
||||
|
||||
def test_delete_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions",
|
||||
json={
|
||||
"id": session_id,
|
||||
"peer_names": {test_peer.name: {}},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.delete(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check that session is marked as inactive
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/list",
|
||||
json={"filters": {"is_active": False}},
|
||||
)
|
||||
data = response.json()
|
||||
# Find our session in the inactive sessions
|
||||
inactive_session = next((s for s in data["items"] if s["id"] == session_id), None)
|
||||
assert inactive_session is not None
|
||||
assert inactive_session["is_active"] is False
|
||||
|
||||
|
||||
def test_clone_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
|
|
@ -1052,3 +1055,332 @@ def test_search_session_with_limit(
|
|||
assert isinstance(data, list)
|
||||
# Should not exceed the limit
|
||||
assert len(data) <= 2
|
||||
|
||||
|
||||
def test_get_session_context_with_peer_target(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with peer_target parameter"""
|
||||
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: {}}},
|
||||
)
|
||||
|
||||
# Add some messages
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages",
|
||||
json={
|
||||
"messages": [
|
||||
{"content": "Test message 1", "peer_id": test_peer.name},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Get context with peer_target
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context?peer_target={test_peer.name}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert "messages" in data
|
||||
assert "peer_representation" in data
|
||||
assert "peer_card" in data
|
||||
# Representation should be present
|
||||
assert data["peer_representation"] is not None
|
||||
assert isinstance(data["peer_representation"], dict)
|
||||
|
||||
|
||||
def test_get_session_context_with_peer_perspective(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with both peer_target and peer_perspective"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
||||
# Create another peer
|
||||
peer2_name = str(generate_nanoid())
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers",
|
||||
json={"name": peer2_name, "metadata": {}},
|
||||
)
|
||||
|
||||
# Create session with both peers
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions",
|
||||
json={"id": session_id, "peers": {test_peer.name: {}, peer2_name: {}}},
|
||||
)
|
||||
|
||||
# Get context with peer_perspective
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context?peer_target={test_peer.name}&peer_perspective={peer2_name}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "peer_representation" in data
|
||||
assert "peer_card" in data
|
||||
|
||||
|
||||
def test_get_session_context_peer_perspective_without_target_fails(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test that peer_perspective without peer_target raises validation error"""
|
||||
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: {}}},
|
||||
)
|
||||
|
||||
# Try to get context with peer_perspective but no peer_target (should fail)
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context?peer_perspective={test_peer.name}",
|
||||
)
|
||||
# FastAPI returns 422 for validation errors, or 400 if it's a custom ValidationException
|
||||
assert response.status_code in [400, 422]
|
||||
error_detail = response.json()["detail"]
|
||||
assert "peer_target" in error_detail.lower()
|
||||
|
||||
|
||||
def test_get_session_context_with_last_message(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with last_message parameter for semantic search"""
|
||||
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 context with last_message and peer_target
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
params={
|
||||
"peer_target": test_peer.name,
|
||||
"last_message": "What is my favorite color?",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "peer_representation" in data
|
||||
|
||||
|
||||
def test_get_session_context_with_limit_to_session(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with limit_to_session parameter"""
|
||||
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 context with limit_to_session=true
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
params={
|
||||
"peer_target": test_peer.name,
|
||||
"last_message": "Test query",
|
||||
"limit_to_session": True,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "peer_representation" in data
|
||||
|
||||
|
||||
def test_get_session_context_with_search_parameters(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with search_top_k and search_max_distance parameters"""
|
||||
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 context with search parameters
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
params={
|
||||
"peer_target": test_peer.name,
|
||||
"last_message": "Test query",
|
||||
"search_top_k": 5,
|
||||
"search_max_distance": 0.8, # float value (semantic distance 0.0-1.0)
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "peer_representation" in data
|
||||
|
||||
|
||||
def test_get_session_context_with_include_most_derived(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with include_most_derived parameter"""
|
||||
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 context with include_most_derived
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
params={
|
||||
"peer_target": test_peer.name,
|
||||
"last_message": "Test query",
|
||||
"include_most_derived": True,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "peer_representation" in data
|
||||
|
||||
|
||||
def test_get_session_context_with_max_observations(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with max_observations parameter"""
|
||||
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 context with max_observations
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
params={
|
||||
"peer_target": test_peer.name,
|
||||
"last_message": "Test query",
|
||||
"max_observations": 10,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "peer_representation" in data
|
||||
|
||||
|
||||
def test_get_session_context_with_all_representation_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with all representation-related parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
||||
# Create another peer
|
||||
peer2_name = str(generate_nanoid())
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers",
|
||||
json={"name": peer2_name, "metadata": {}},
|
||||
)
|
||||
|
||||
# Create session
|
||||
client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions",
|
||||
json={"id": session_id, "peers": {test_peer.name: {}, peer2_name: {}}},
|
||||
)
|
||||
|
||||
# Get context with all representation parameters
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
params={
|
||||
"tokens": 500,
|
||||
"peer_target": test_peer.name,
|
||||
"peer_perspective": peer2_name,
|
||||
"last_message": "What do you know about me?",
|
||||
"limit_to_session": True,
|
||||
"search_top_k": 10,
|
||||
"search_max_distance": 0.9, # float value (semantic distance 0.0-1.0)
|
||||
"include_most_derived": True,
|
||||
"max_observations": 15,
|
||||
"summary": True,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["id"] == session_id
|
||||
assert "messages" in data
|
||||
assert isinstance(data["messages"], list)
|
||||
assert "summary" in data
|
||||
assert "peer_representation" in data
|
||||
assert "peer_card" in data
|
||||
# Validate representation structure
|
||||
assert isinstance(data["peer_representation"], dict)
|
||||
|
||||
|
||||
def test_get_session_context_response_structure(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test that session context response has correct structure"""
|
||||
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: {}}},
|
||||
)
|
||||
|
||||
# Add messages
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages",
|
||||
json={
|
||||
"messages": [
|
||||
{"content": "Message 1", "peer_id": test_peer.name},
|
||||
{"content": "Message 2", "peer_id": test_peer.name},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Get context and validate response structure
|
||||
response = client.get(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Validate SessionContext schema
|
||||
assert "id" in data
|
||||
assert data["id"] == session_id
|
||||
assert "messages" in data
|
||||
assert isinstance(data["messages"], list)
|
||||
assert len(data["messages"]) >= 2
|
||||
|
||||
# Validate Message schema
|
||||
for message in data["messages"]:
|
||||
assert "id" in message
|
||||
assert "content" in message
|
||||
assert "peer_id" in message
|
||||
assert "session_id" in message
|
||||
assert "workspace_id" in message
|
||||
assert "created_at" in message
|
||||
assert "token_count" in message
|
||||
|
||||
# When no peer_target, these should not be present or be None
|
||||
assert data.get("peer_representation") is None
|
||||
assert data.get("peer_card") is None
|
||||
|
|
|
|||
|
|
@ -252,3 +252,244 @@ async def test_file_upload_with_tuple_input(
|
|||
assert content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_metadata(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests uploading a file with metadata parameter.
|
||||
"""
|
||||
honcho_client, _client_type = client_fixture
|
||||
|
||||
text_content = "Test file with metadata"
|
||||
from io import BytesIO
|
||||
|
||||
text_file = BytesIO(text_content.encode("utf-8"))
|
||||
text_file.name = "test_metadata.txt"
|
||||
|
||||
metadata: dict[str, object] = {
|
||||
"source": "sdk_test",
|
||||
"category": "upload",
|
||||
"priority": 1,
|
||||
}
|
||||
|
||||
if isinstance(honcho_client, Honcho):
|
||||
session = honcho_client.session(id="test-session-metadata")
|
||||
user = honcho_client.peer(id="user-metadata")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
metadata=metadata,
|
||||
)
|
||||
else:
|
||||
session = await honcho_client.session(id="test-session-metadata")
|
||||
user = await honcho_client.peer(id="user-metadata")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
assert len(messages) >= 1
|
||||
assert text_content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
# Check that metadata was applied
|
||||
assert messages[0].metadata == metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_configuration(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests uploading a file with configuration parameter.
|
||||
"""
|
||||
honcho_client, _client_type = client_fixture
|
||||
|
||||
text_content = "Test file with configuration"
|
||||
from io import BytesIO
|
||||
|
||||
text_file = BytesIO(text_content.encode("utf-8"))
|
||||
text_file.name = "test_config.txt"
|
||||
|
||||
from typing import cast
|
||||
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
|
||||
configuration = cast(
|
||||
Configuration, cast(object, {"skip_deriver": True, "custom_flag": "test"})
|
||||
)
|
||||
|
||||
if isinstance(honcho_client, Honcho):
|
||||
session = honcho_client.session(id="test-session-config")
|
||||
user = honcho_client.peer(id="user-config")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
configuration=configuration,
|
||||
)
|
||||
else:
|
||||
session = await honcho_client.session(id="test-session-config")
|
||||
user = await honcho_client.peer(id="user-config")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
configuration=configuration,
|
||||
)
|
||||
|
||||
assert len(messages) >= 1
|
||||
assert text_content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
# Configuration is used during processing, not directly stored in message
|
||||
# This test confirms the endpoint accepts it without error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_created_at(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests uploading a file with created_at parameter.
|
||||
"""
|
||||
honcho_client, _client_type = client_fixture
|
||||
|
||||
text_content = "Test file with created_at"
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
|
||||
text_file = BytesIO(text_content.encode("utf-8"))
|
||||
text_file.name = "test_timestamp.txt"
|
||||
|
||||
test_timestamp = datetime(2023, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
|
||||
created_at_str = test_timestamp.isoformat()
|
||||
|
||||
if isinstance(honcho_client, Honcho):
|
||||
session = honcho_client.session(id="test-session-timestamp")
|
||||
user = honcho_client.peer(id="user-timestamp")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
created_at=test_timestamp.isoformat(),
|
||||
)
|
||||
else:
|
||||
session = await honcho_client.session(id="test-session-timestamp")
|
||||
user = await honcho_client.peer(id="user-timestamp")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
||||
assert len(messages) >= 1
|
||||
assert text_content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
# Check that created_at was applied (compare timestamps, allowing for small differences)
|
||||
# Message.created_at from honcho_core is a datetime object
|
||||
message_timestamp = messages[0].created_at
|
||||
assert abs((message_timestamp - test_timestamp).total_seconds()) < 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_all_parameters(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests uploading a file with metadata, configuration, and created_at all together.
|
||||
"""
|
||||
honcho_client, _client_type = client_fixture
|
||||
|
||||
text_content = "Test file with all parameters"
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
|
||||
text_file = BytesIO(text_content.encode("utf-8"))
|
||||
text_file.name = "test_all_params.txt"
|
||||
|
||||
from typing import cast
|
||||
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
|
||||
metadata: dict[str, object] = {"source": "comprehensive_test", "version": "1.0"}
|
||||
configuration = cast(
|
||||
Configuration, cast(object, {"skip_deriver": False, "test_mode": True})
|
||||
)
|
||||
test_timestamp = datetime(2023, 6, 20, 14, 15, 30, tzinfo=timezone.utc)
|
||||
created_at_str = test_timestamp.isoformat()
|
||||
|
||||
if isinstance(honcho_client, Honcho):
|
||||
session = honcho_client.session(id="test-session-all")
|
||||
user = honcho_client.peer(id="user-all")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
metadata=metadata,
|
||||
configuration=configuration,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
else:
|
||||
session = await honcho_client.session(id="test-session-all")
|
||||
user = await honcho_client.peer(id="user-all")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
metadata=metadata,
|
||||
configuration=configuration,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
||||
assert len(messages) >= 1
|
||||
assert text_content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
# Check metadata
|
||||
assert messages[0].metadata == metadata
|
||||
# Check created_at
|
||||
# Message.created_at from honcho_core is a datetime object
|
||||
message_timestamp = messages[0].created_at
|
||||
assert abs((message_timestamp - test_timestamp).total_seconds()) < 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_upload_with_datetime_object(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests uploading a file with created_at as a datetime object (Python only).
|
||||
"""
|
||||
honcho_client, _client_type = client_fixture
|
||||
|
||||
text_content = "Test file with datetime object"
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
|
||||
text_file = BytesIO(text_content.encode("utf-8"))
|
||||
text_file.name = "test_datetime.txt"
|
||||
|
||||
test_timestamp = datetime(2023, 3, 10, 8, 45, 20, tzinfo=timezone.utc)
|
||||
|
||||
if isinstance(honcho_client, Honcho):
|
||||
session = honcho_client.session(id="test-session-datetime")
|
||||
user = honcho_client.peer(id="user-datetime")
|
||||
messages = session.upload_file(
|
||||
file=text_file, peer_id=user.id, created_at=test_timestamp
|
||||
)
|
||||
else:
|
||||
session = await honcho_client.session(id="test-session-datetime")
|
||||
user = await honcho_client.peer(id="user-datetime")
|
||||
messages = await session.upload_file(
|
||||
file=text_file, peer_id=user.id, created_at=test_timestamp
|
||||
)
|
||||
|
||||
assert len(messages) >= 1
|
||||
assert text_content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
# Check that created_at was applied
|
||||
# Message.created_at from honcho_core is a datetime object
|
||||
message_timestamp = messages[0].created_at
|
||||
assert abs((message_timestamp - test_timestamp).total_seconds()) < 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,592 @@
|
|||
"""Tests for metadata and configuration caching in Honcho SDK."""
|
||||
|
||||
import pytest
|
||||
|
||||
from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
||||
from sdks.python.src.honcho.async_client.pagination import AsyncPage
|
||||
from sdks.python.src.honcho.async_client.peer import AsyncPeer
|
||||
from sdks.python.src.honcho.async_client.session import AsyncSession
|
||||
from sdks.python.src.honcho.client import Honcho
|
||||
from sdks.python.src.honcho.pagination import SyncPage
|
||||
from sdks.python.src.honcho.peer import Peer
|
||||
from sdks.python.src.honcho.session import Session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_metadata_caching(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that workspace metadata is properly cached after get/set operations.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Should initialize with None metadata
|
||||
assert honcho_client.metadata is None
|
||||
|
||||
# Get metadata should cache it
|
||||
metadata = await honcho_client.get_metadata()
|
||||
assert isinstance(metadata, dict)
|
||||
assert honcho_client.metadata == metadata
|
||||
|
||||
# Set metadata should update cache
|
||||
await honcho_client.set_metadata({"theme": "dark", "version": "1.0"})
|
||||
assert honcho_client.metadata == {"theme": "dark", "version": "1.0"}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = await honcho_client.get_metadata()
|
||||
assert retrieved == {"theme": "dark", "version": "1.0"}
|
||||
assert honcho_client.metadata == {"theme": "dark", "version": "1.0"}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Should initialize with None metadata
|
||||
assert honcho_client.metadata is None
|
||||
|
||||
# Get metadata should cache it
|
||||
metadata = honcho_client.get_metadata()
|
||||
assert isinstance(metadata, dict)
|
||||
assert honcho_client.metadata == metadata
|
||||
|
||||
# Set metadata should update cache
|
||||
honcho_client.set_metadata({"theme": "dark", "version": "1.0"})
|
||||
assert honcho_client.metadata == {"theme": "dark", "version": "1.0"}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = honcho_client.get_metadata()
|
||||
assert retrieved == {"theme": "dark", "version": "1.0"}
|
||||
assert honcho_client.metadata == {"theme": "dark", "version": "1.0"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_metadata_caching(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that peer metadata is properly cached after get/set operations.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-peer-meta-cache")
|
||||
assert isinstance(peer, AsyncPeer)
|
||||
|
||||
# Get metadata should cache it
|
||||
metadata = await peer.get_metadata()
|
||||
assert isinstance(metadata, dict)
|
||||
assert peer.metadata == metadata
|
||||
|
||||
# Set metadata should update cache
|
||||
await peer.set_metadata({"name": "Alice", "role": "user"})
|
||||
assert peer.metadata == {"name": "Alice", "role": "user"}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = await peer.get_metadata()
|
||||
assert retrieved == {"name": "Alice", "role": "user"}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-peer-meta-cache")
|
||||
assert isinstance(peer, Peer)
|
||||
|
||||
# Get metadata should cache it
|
||||
metadata = peer.get_metadata()
|
||||
assert isinstance(metadata, dict)
|
||||
assert peer.metadata == metadata
|
||||
|
||||
# Set metadata should update cache
|
||||
peer.set_metadata({"name": "Alice", "role": "user"})
|
||||
assert peer.metadata == {"name": "Alice", "role": "user"}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = peer.get_metadata()
|
||||
assert retrieved == {"name": "Alice", "role": "user"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_config_caching(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that peer configuration is properly cached after get/set operations.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-peer-config-cache")
|
||||
assert isinstance(peer, AsyncPeer)
|
||||
|
||||
# Get config should cache it
|
||||
config = await peer.get_config()
|
||||
assert isinstance(config, dict)
|
||||
assert peer.configuration == config
|
||||
|
||||
# Set config should update cache
|
||||
await peer.set_config({"observe_me": True, "observe_others": False})
|
||||
assert peer.configuration == {"observe_me": True, "observe_others": False}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = await peer.get_config()
|
||||
assert retrieved == {"observe_me": True, "observe_others": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-peer-config-cache")
|
||||
assert isinstance(peer, Peer)
|
||||
|
||||
# Get config should cache it
|
||||
config = peer.get_config()
|
||||
assert isinstance(config, dict)
|
||||
assert peer.configuration == config
|
||||
|
||||
# Set config should update cache
|
||||
peer.set_config({"observe_me": True, "observe_others": False})
|
||||
assert peer.configuration == {"observe_me": True, "observe_others": False}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = peer.get_config()
|
||||
assert retrieved == {"observe_me": True, "observe_others": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_deprecated_config_methods(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that deprecated getPeerConfig/setPeerConfig methods work and cache properly.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-peer-deprecated-config")
|
||||
assert isinstance(peer, AsyncPeer)
|
||||
|
||||
# Deprecated get method should work and cache
|
||||
config = await peer.get_peer_config()
|
||||
assert isinstance(config, dict)
|
||||
assert peer.configuration == config
|
||||
|
||||
# Deprecated set method should work and update cache
|
||||
await peer.set_peer_config({"observe_me": False})
|
||||
assert peer.configuration == {"observe_me": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-peer-deprecated-config")
|
||||
assert isinstance(peer, Peer)
|
||||
|
||||
# Deprecated get method should work and cache
|
||||
config = peer.get_peer_config()
|
||||
assert isinstance(config, dict)
|
||||
assert peer.configuration == config
|
||||
|
||||
# Deprecated set method should work and update cache
|
||||
peer.set_peer_config({"observe_me": False})
|
||||
assert peer.configuration == {"observe_me": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_metadata_and_config_independence(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that peer metadata and config are cached independently.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-peer-independent-cache")
|
||||
assert isinstance(peer, AsyncPeer)
|
||||
|
||||
# Set both metadata and config
|
||||
await peer.set_metadata({"name": "Test"})
|
||||
await peer.set_config({"observe_me": True})
|
||||
|
||||
assert peer.metadata == {"name": "Test"}
|
||||
assert peer.configuration == {"observe_me": True}
|
||||
|
||||
# Update metadata only
|
||||
await peer.set_metadata({"name": "Updated"})
|
||||
|
||||
assert peer.metadata == {"name": "Updated"}
|
||||
assert peer.configuration == {"observe_me": True} # Should remain unchanged
|
||||
|
||||
# Update config only
|
||||
await peer.set_config({"observe_me": False})
|
||||
|
||||
assert peer.metadata == {"name": "Updated"} # Should remain unchanged
|
||||
assert peer.configuration == {"observe_me": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-peer-independent-cache")
|
||||
assert isinstance(peer, Peer)
|
||||
|
||||
# Set both metadata and config
|
||||
peer.set_metadata({"name": "Test"})
|
||||
peer.set_config({"observe_me": True})
|
||||
|
||||
assert peer.metadata == {"name": "Test"}
|
||||
assert peer.configuration == {"observe_me": True}
|
||||
|
||||
# Update metadata only
|
||||
peer.set_metadata({"name": "Updated"})
|
||||
|
||||
assert peer.metadata == {"name": "Updated"}
|
||||
assert peer.configuration == {"observe_me": True} # Should remain unchanged
|
||||
|
||||
# Update config only
|
||||
peer.set_config({"observe_me": False})
|
||||
|
||||
assert peer.metadata == {"name": "Updated"} # Should remain unchanged
|
||||
assert peer.configuration == {"observe_me": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_list_with_metadata_and_config(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that listed peers have metadata and config populated from API response.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Create peers with metadata and config
|
||||
peer1 = await honcho_client.peer(id="test-list-peer1")
|
||||
await peer1.set_metadata({"name": "Alice"})
|
||||
await peer1.set_config({"observe_me": True})
|
||||
|
||||
peer2 = await honcho_client.peer(id="test-list-peer2")
|
||||
await peer2.set_metadata({"name": "Bob"})
|
||||
await peer2.set_config({"observe_me": False})
|
||||
|
||||
# List peers and check cached data
|
||||
peers_page = await honcho_client.get_peers()
|
||||
assert isinstance(peers_page, AsyncPage)
|
||||
|
||||
peers = peers_page.items
|
||||
peer_map = {p.id: p for p in peers}
|
||||
|
||||
if "test-list-peer1" in peer_map:
|
||||
p1 = peer_map["test-list-peer1"]
|
||||
assert p1.metadata == {"name": "Alice"}
|
||||
assert p1.configuration == {"observe_me": True}
|
||||
|
||||
if "test-list-peer2" in peer_map:
|
||||
p2 = peer_map["test-list-peer2"]
|
||||
assert p2.metadata == {"name": "Bob"}
|
||||
assert p2.configuration == {"observe_me": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Create peers with metadata and config
|
||||
peer1 = honcho_client.peer(id="test-list-peer1")
|
||||
peer1.set_metadata({"name": "Alice"})
|
||||
peer1.set_config({"observe_me": True})
|
||||
|
||||
peer2 = honcho_client.peer(id="test-list-peer2")
|
||||
peer2.set_metadata({"name": "Bob"})
|
||||
peer2.set_config({"observe_me": False})
|
||||
|
||||
# List peers and check cached data
|
||||
peers_page = honcho_client.get_peers()
|
||||
assert isinstance(peers_page, SyncPage)
|
||||
|
||||
peers = list(peers_page)
|
||||
peer_map = {p.id: p for p in peers}
|
||||
|
||||
if "test-list-peer1" in peer_map:
|
||||
p1 = peer_map["test-list-peer1"]
|
||||
assert p1.metadata == {"name": "Alice"}
|
||||
assert p1.configuration == {"observe_me": True}
|
||||
|
||||
if "test-list-peer2" in peer_map:
|
||||
p2 = peer_map["test-list-peer2"]
|
||||
assert p2.metadata == {"name": "Bob"}
|
||||
assert p2.configuration == {"observe_me": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_metadata_caching(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that session metadata is properly cached after get/set operations.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-meta-cache")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
# Get metadata should cache it
|
||||
metadata = await session.get_metadata()
|
||||
assert isinstance(metadata, dict)
|
||||
assert session.metadata == metadata
|
||||
|
||||
# Set metadata should update cache
|
||||
await session.set_metadata({"title": "Chat Session", "active": True})
|
||||
assert session.metadata == {"title": "Chat Session", "active": True}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = await session.get_metadata()
|
||||
assert retrieved == {"title": "Chat Session", "active": True}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-meta-cache")
|
||||
assert isinstance(session, Session)
|
||||
|
||||
# Get metadata should cache it
|
||||
metadata = session.get_metadata()
|
||||
assert isinstance(metadata, dict)
|
||||
assert session.metadata == metadata
|
||||
|
||||
# Set metadata should update cache
|
||||
session.set_metadata({"title": "Chat Session", "active": True})
|
||||
assert session.metadata == {"title": "Chat Session", "active": True}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = session.get_metadata()
|
||||
assert retrieved == {"title": "Chat Session", "active": True}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_config_caching(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that session configuration is properly cached after get/set operations.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-config-cache")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
# Get config should cache it
|
||||
config = await session.get_config()
|
||||
assert isinstance(config, dict)
|
||||
assert session.configuration == config
|
||||
|
||||
# Set config should update cache
|
||||
await session.set_config({"anonymous": True, "summarize": False})
|
||||
assert session.configuration == {"anonymous": True, "summarize": False}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = await session.get_config()
|
||||
assert retrieved == {"anonymous": True, "summarize": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-config-cache")
|
||||
assert isinstance(session, Session)
|
||||
|
||||
# Get config should cache it
|
||||
config = session.get_config()
|
||||
assert isinstance(config, dict)
|
||||
assert session.configuration == config
|
||||
|
||||
# Set config should update cache
|
||||
session.set_config({"anonymous": True, "summarize": False})
|
||||
assert session.configuration == {"anonymous": True, "summarize": False}
|
||||
|
||||
# Get should return cached value
|
||||
retrieved = session.get_config()
|
||||
assert retrieved == {"anonymous": True, "summarize": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_metadata_and_config_independence(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that session metadata and config are cached independently.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-independent-cache")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
# Set both metadata and config
|
||||
await session.set_metadata({"title": "Test"})
|
||||
await session.set_config({"anonymous": True})
|
||||
|
||||
assert session.metadata == {"title": "Test"}
|
||||
assert session.configuration == {"anonymous": True}
|
||||
|
||||
# Update metadata only
|
||||
await session.set_metadata({"title": "Updated"})
|
||||
|
||||
assert session.metadata == {"title": "Updated"}
|
||||
assert session.configuration == {"anonymous": True} # Should remain unchanged
|
||||
|
||||
# Update config only
|
||||
await session.set_config({"anonymous": False})
|
||||
|
||||
assert session.metadata == {"title": "Updated"} # Should remain unchanged
|
||||
assert session.configuration == {"anonymous": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-independent-cache")
|
||||
assert isinstance(session, Session)
|
||||
|
||||
# Set both metadata and config
|
||||
session.set_metadata({"title": "Test"})
|
||||
session.set_config({"anonymous": True})
|
||||
|
||||
assert session.metadata == {"title": "Test"}
|
||||
assert session.configuration == {"anonymous": True}
|
||||
|
||||
# Update metadata only
|
||||
session.set_metadata({"title": "Updated"})
|
||||
|
||||
assert session.metadata == {"title": "Updated"}
|
||||
assert session.configuration == {"anonymous": True} # Should remain unchanged
|
||||
|
||||
# Update config only
|
||||
session.set_config({"anonymous": False})
|
||||
|
||||
assert session.metadata == {"title": "Updated"} # Should remain unchanged
|
||||
assert session.configuration == {"anonymous": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_list_with_metadata_and_config(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that listed sessions have metadata and config populated from API response.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Create sessions with metadata and config
|
||||
session1 = await honcho_client.session(id="test-list-session1")
|
||||
await session1.set_metadata({"title": "Session 1"})
|
||||
await session1.set_config({"anonymous": True})
|
||||
|
||||
session2 = await honcho_client.session(id="test-list-session2")
|
||||
await session2.set_metadata({"title": "Session 2"})
|
||||
await session2.set_config({"anonymous": False})
|
||||
|
||||
# List sessions and check cached data
|
||||
sessions_page = await honcho_client.get_sessions()
|
||||
assert isinstance(sessions_page, AsyncPage)
|
||||
|
||||
sessions = sessions_page.items
|
||||
session_map = {s.id: s for s in sessions}
|
||||
|
||||
if "test-list-session1" in session_map:
|
||||
s1 = session_map["test-list-session1"]
|
||||
assert s1.metadata == {"title": "Session 1"}
|
||||
assert s1.configuration == {"anonymous": True}
|
||||
|
||||
if "test-list-session2" in session_map:
|
||||
s2 = session_map["test-list-session2"]
|
||||
assert s2.metadata == {"title": "Session 2"}
|
||||
assert s2.configuration == {"anonymous": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Create sessions with metadata and config
|
||||
session1 = honcho_client.session(id="test-list-session1")
|
||||
session1.set_metadata({"title": "Session 1"})
|
||||
session1.set_config({"anonymous": True})
|
||||
|
||||
session2 = honcho_client.session(id="test-list-session2")
|
||||
session2.set_metadata({"title": "Session 2"})
|
||||
session2.set_config({"anonymous": False})
|
||||
|
||||
# List sessions and check cached data
|
||||
sessions_page = honcho_client.get_sessions()
|
||||
assert isinstance(sessions_page, SyncPage)
|
||||
|
||||
sessions = list(sessions_page)
|
||||
session_map = {s.id: s for s in sessions}
|
||||
|
||||
if "test-list-session1" in session_map:
|
||||
s1 = session_map["test-list-session1"]
|
||||
assert s1.metadata == {"title": "Session 1"}
|
||||
assert s1.configuration == {"anonymous": True}
|
||||
|
||||
if "test-list-session2" in session_map:
|
||||
s2 = session_map["test-list-session2"]
|
||||
assert s2.metadata == {"title": "Session 2"}
|
||||
assert s2.configuration == {"anonymous": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_initialization_with_metadata_and_config(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that peers can be initialized with metadata and config.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
peer = await honcho_client.peer(
|
||||
id="test-init-peer",
|
||||
metadata={"name": "Test Peer", "role": "assistant"},
|
||||
config={"observe_me": False},
|
||||
)
|
||||
assert isinstance(peer, AsyncPeer)
|
||||
assert peer.metadata == {"name": "Test Peer", "role": "assistant"}
|
||||
assert peer.configuration == {"observe_me": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
peer = honcho_client.peer(
|
||||
id="test-init-peer",
|
||||
metadata={"name": "Test Peer", "role": "assistant"},
|
||||
config={"observe_me": False},
|
||||
)
|
||||
assert isinstance(peer, Peer)
|
||||
assert peer.metadata == {"name": "Test Peer", "role": "assistant"}
|
||||
assert peer.configuration == {"observe_me": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_initialization_with_metadata_and_config(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
) -> None:
|
||||
"""
|
||||
Tests that sessions can be initialized with metadata and config.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
session = await honcho_client.session(
|
||||
id="test-init-session",
|
||||
metadata={"title": "Test Session", "tags": ["important"]},
|
||||
config={"anonymous": False},
|
||||
)
|
||||
assert isinstance(session, AsyncSession)
|
||||
assert session.metadata == {"title": "Test Session", "tags": ["important"]}
|
||||
assert session.configuration == {"anonymous": False}
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
session = honcho_client.session(
|
||||
id="test-init-session",
|
||||
metadata={"title": "Test Session", "tags": ["important"]},
|
||||
config={"anonymous": False},
|
||||
)
|
||||
assert isinstance(session, Session)
|
||||
assert session.metadata == {"title": "Test Session", "tags": ["important"]}
|
||||
assert session.configuration == {"anonymous": False}
|
||||
|
|
@ -6,7 +6,7 @@ from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
|||
from sdks.python.src.honcho.async_client.peer import AsyncPeer
|
||||
from sdks.python.src.honcho.client import Honcho
|
||||
from sdks.python.src.honcho.peer import Peer
|
||||
from sdks.python.src.honcho.types import DialecticStreamResponse
|
||||
from sdks.python.src.honcho.types import DialecticStreamResponse, Representation
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -197,8 +197,12 @@ async def test_peer_chat_streaming(client_fixture: tuple[Honcho | AsyncHoncho, s
|
|||
yield 'data: {"delta": {"content": " async"}}'
|
||||
yield 'data: {"done": true}'
|
||||
|
||||
mock_http_response = Mock()
|
||||
mock_http_response.raise_for_status = Mock()
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.iter_lines = mock_aiter_lines
|
||||
mock_response.http_response = mock_http_response
|
||||
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
|
||||
mock_response.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
|
|
@ -285,3 +289,411 @@ async def test_peer_chat_non_streaming(
|
|||
response = peer.chat("What do I like?", stream=False)
|
||||
# Response can be None or a string
|
||||
assert response is None or isinstance(response, str)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_no_params(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with no parameters (default behavior).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-working-rep-no-params")
|
||||
session = await honcho_client.session(id="test-working-rep-session-no-params")
|
||||
|
||||
# Add some messages to create context
|
||||
await session.add_messages([peer.message("I enjoy hiking and nature")])
|
||||
|
||||
# Get working representation with no parameters
|
||||
result = await peer.working_rep()
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-working-rep-no-params")
|
||||
session = honcho_client.session(id="test-working-rep-session-no-params")
|
||||
|
||||
# Add some messages to create context
|
||||
session.add_messages([peer.message("I enjoy hiking and nature")])
|
||||
|
||||
# Get working representation with no parameters
|
||||
result = peer.working_rep()
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_session_string(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with session parameter as string.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-working-rep-session-str")
|
||||
session = await honcho_client.session(id="test-working-rep-session-str-sess")
|
||||
|
||||
# Add some messages to the session
|
||||
await session.add_messages([peer.message("I like reading books")])
|
||||
|
||||
# Get working representation scoped to session (as string)
|
||||
result = await peer.working_rep(session=session.id)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-working-rep-session-str")
|
||||
session = honcho_client.session(id="test-working-rep-session-str-sess")
|
||||
|
||||
# Add some messages to the session
|
||||
session.add_messages([peer.message("I like reading books")])
|
||||
|
||||
# Get working representation scoped to session (as string)
|
||||
result = peer.working_rep(session=session.id)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_session_object(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with session parameter as Session object.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-working-rep-session-obj")
|
||||
session = await honcho_client.session(id="test-working-rep-session-obj-sess")
|
||||
from sdks.python.src.honcho.async_client.session import AsyncSession
|
||||
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
# Add some messages to the session
|
||||
await session.add_messages([peer.message("I prefer tea over coffee")])
|
||||
|
||||
# Get working representation scoped to session (as Session object)
|
||||
result = await peer.working_rep(session=session)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-working-rep-session-obj")
|
||||
session = honcho_client.session(id="test-working-rep-session-obj-sess")
|
||||
from sdks.python.src.honcho.session import Session
|
||||
|
||||
assert isinstance(session, Session)
|
||||
|
||||
# Add some messages to the session
|
||||
session.add_messages([peer.message("I prefer tea over coffee")])
|
||||
|
||||
# Get working representation scoped to session (as Session object)
|
||||
result = peer.working_rep(session=session)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_target_string(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with target parameter as string.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-working-rep-target-str-observer")
|
||||
target = await honcho_client.peer(id="test-working-rep-target-str-target")
|
||||
session = await honcho_client.session(id="test-working-rep-target-str-sess")
|
||||
|
||||
# Add messages from both peers
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello there"),
|
||||
target.message("Hi, how are you?"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation of target from observer's perspective (as string)
|
||||
result = await observer.working_rep(target=target.id)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-working-rep-target-str-observer")
|
||||
target = honcho_client.peer(id="test-working-rep-target-str-target")
|
||||
session = honcho_client.session(id="test-working-rep-target-str-sess")
|
||||
|
||||
# Add messages from both peers
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello there"),
|
||||
target.message("Hi, how are you?"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation of target from observer's perspective (as string)
|
||||
result = observer.working_rep(target=target.id)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_target_object(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with target parameter as Peer object.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-working-rep-target-obj-observer")
|
||||
target = await honcho_client.peer(id="test-working-rep-target-obj-target")
|
||||
session = await honcho_client.session(id="test-working-rep-target-obj-sess")
|
||||
from sdks.python.src.honcho.async_client.peer import AsyncPeer
|
||||
|
||||
assert isinstance(target, AsyncPeer)
|
||||
|
||||
# Add messages from both peers
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("What do you think?"),
|
||||
target.message("I think it's great!"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation of target from observer's perspective (as Peer object)
|
||||
result = await observer.working_rep(target=target)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-working-rep-target-obj-observer")
|
||||
target = honcho_client.peer(id="test-working-rep-target-obj-target")
|
||||
session = honcho_client.session(id="test-working-rep-target-obj-sess")
|
||||
from sdks.python.src.honcho.peer import Peer
|
||||
|
||||
assert isinstance(target, Peer)
|
||||
|
||||
# Add messages from both peers
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("What do you think?"),
|
||||
target.message("I think it's great!"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation of target from observer's perspective (as Peer object)
|
||||
result = observer.working_rep(target=target)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_search_query(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with search_query parameter.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-working-rep-search-query")
|
||||
session = await honcho_client.session(id="test-working-rep-search-query-sess")
|
||||
|
||||
# Add some messages with different topics
|
||||
await session.add_messages(
|
||||
[
|
||||
peer.message("I love programming in Python"),
|
||||
peer.message("I also enjoy playing basketball"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation with search query
|
||||
result = await peer.working_rep(search_query="programming")
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-working-rep-search-query")
|
||||
session = honcho_client.session(id="test-working-rep-search-query-sess")
|
||||
|
||||
# Add some messages with different topics
|
||||
session.add_messages(
|
||||
[
|
||||
peer.message("I love programming in Python"),
|
||||
peer.message("I also enjoy playing basketball"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation with search query
|
||||
result = peer.working_rep(search_query="programming")
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_size(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with size parameter.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-working-rep-size")
|
||||
session = await honcho_client.session(id="test-working-rep-size-sess")
|
||||
|
||||
# Add multiple messages
|
||||
await session.add_messages(
|
||||
[peer.message(f"Message number {i}") for i in range(10)]
|
||||
)
|
||||
|
||||
# Get working representation with custom max_observations
|
||||
result = await peer.working_rep(max_observations=5)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
# Test with different max_observations values
|
||||
result = await peer.working_rep(max_observations=1)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
result = await peer.working_rep(max_observations=100)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-working-rep-size")
|
||||
session = honcho_client.session(id="test-working-rep-size-sess")
|
||||
|
||||
# Add multiple messages
|
||||
session.add_messages([peer.message(f"Message number {i}") for i in range(10)])
|
||||
|
||||
# Get working representation with custom size
|
||||
result = peer.working_rep(max_observations=5)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
# Test with different max_observations values
|
||||
result = peer.working_rep(max_observations=1)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
result = peer.working_rep(max_observations=100)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_working_rep_with_all_params(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests peer.working_rep() with all parameters combined.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-working-rep-all-observer")
|
||||
target = await honcho_client.peer(id="test-working-rep-all-target")
|
||||
session = await honcho_client.session(id="test-working-rep-all-sess")
|
||||
|
||||
# Add messages from both peers
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("I think Python is great for data science"),
|
||||
target.message("I agree, especially with libraries like pandas"),
|
||||
observer.message("What about machine learning?"),
|
||||
target.message("TensorFlow and PyTorch are excellent choices"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation with all parameters
|
||||
result = await observer.working_rep(
|
||||
session=session, target=target, search_query="Python", max_observations=10
|
||||
)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
# Test with session as string and target as string
|
||||
result = await observer.working_rep(
|
||||
session=session.id,
|
||||
target=target.id,
|
||||
search_query="machine learning",
|
||||
max_observations=5,
|
||||
)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-working-rep-all-observer")
|
||||
target = honcho_client.peer(id="test-working-rep-all-target")
|
||||
session = honcho_client.session(id="test-working-rep-all-sess")
|
||||
|
||||
# Add messages from both peers
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message(content="I think Python is great for data science"),
|
||||
target.message("I agree, especially with libraries like pandas"),
|
||||
observer.message("What about machine learning?"),
|
||||
target.message("TensorFlow and PyTorch are excellent choices"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get working representation with all parameters
|
||||
result = observer.working_rep(
|
||||
session=session, target=target, search_query="Python", max_observations=10
|
||||
)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
||||
# Test with session as string and target as string
|
||||
result = observer.working_rep(
|
||||
session=session.id,
|
||||
target=target.id,
|
||||
search_query="machine learning",
|
||||
max_observations=5,
|
||||
)
|
||||
assert isinstance(result, Representation)
|
||||
assert hasattr(result, "explicit")
|
||||
assert hasattr(result, "deductive")
|
||||
|
|
|
|||
|
|
@ -277,6 +277,83 @@ async def test_session_search(client_fixture: tuple[Honcho | AsyncHoncho, str]):
|
|||
assert search_query in search_results[0].content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_add_messages_return_value(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that add_messages returns a list of Message objects.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-add-msg-return")
|
||||
assert isinstance(session, AsyncSession)
|
||||
user = await honcho_client.peer(id="user-add-msg-return")
|
||||
assert isinstance(user, AsyncPeer)
|
||||
assistant = await honcho_client.peer(id="assistant-add-msg-return")
|
||||
assert isinstance(assistant, AsyncPeer)
|
||||
|
||||
# Test single message return value
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
|
||||
result = await session.add_messages(user.message("Hello assistant"))
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Message)
|
||||
assert result[0].content == "Hello assistant"
|
||||
assert result[0].peer_id == user.id
|
||||
|
||||
# Test multiple messages return value
|
||||
result = await session.add_messages(
|
||||
[
|
||||
user.message("How are you?"),
|
||||
assistant.message("I'm doing well, thank you!"),
|
||||
]
|
||||
)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(msg, Message) for msg in result)
|
||||
assert result[0].content == "How are you?"
|
||||
assert result[0].peer_id == user.id
|
||||
assert result[1].content == "I'm doing well, thank you!"
|
||||
assert result[1].peer_id == assistant.id
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-add-msg-return")
|
||||
assert isinstance(session, Session)
|
||||
user = honcho_client.peer(id="user-add-msg-return")
|
||||
assert isinstance(user, Peer)
|
||||
assistant = honcho_client.peer(id="assistant-add-msg-return")
|
||||
assert isinstance(assistant, Peer)
|
||||
|
||||
# Test single message return value
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
|
||||
result = session.add_messages(user.message("Hello assistant"))
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Message)
|
||||
assert result[0].content == "Hello assistant"
|
||||
assert result[0].peer_id == user.id
|
||||
|
||||
# Test multiple messages return value
|
||||
result = session.add_messages(
|
||||
[
|
||||
user.message("How are you?"),
|
||||
assistant.message("I'm doing well, thank you!"),
|
||||
]
|
||||
)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(msg, Message) for msg in result)
|
||||
assert result[0].content == "How are you?"
|
||||
assert result[0].peer_id == user.id
|
||||
assert result[1].content == "I'm doing well, thank you!"
|
||||
assert result[1].peer_id == assistant.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, str]):
|
||||
"""
|
||||
|
|
@ -305,7 +382,7 @@ async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, s
|
|||
@pytest.mark.asyncio
|
||||
async def test_session_delete(client_fixture: tuple[Honcho | AsyncHoncho, str]) -> None:
|
||||
"""
|
||||
Tests deleting a session.
|
||||
Tests deleting a session and verifying all associated data is removed.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
|
|
@ -314,17 +391,60 @@ async def test_session_delete(client_fixture: tuple[Honcho | AsyncHoncho, str])
|
|||
session = await honcho_client.session(id="test-session-delete")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
# Add a peer to make the session exist
|
||||
# Add a peer and messages to make the session have data
|
||||
user = await honcho_client.peer(id="user-delete")
|
||||
await session.add_peers([user])
|
||||
await session.add_messages(
|
||||
[user.message("Test message that should be deleted")]
|
||||
)
|
||||
|
||||
# Verify messages exist before deletion
|
||||
messages_page = await session.get_messages()
|
||||
messages = messages_page.items
|
||||
assert len(messages) == 1
|
||||
|
||||
# Delete should not raise an exception
|
||||
await session.delete()
|
||||
|
||||
# Verify session is removed from active sessions list
|
||||
all_sessions_page = await honcho_client.get_sessions({"is_active": True})
|
||||
all_sessions = all_sessions_page.items
|
||||
all_session_ids = [s.id for s in all_sessions]
|
||||
assert "test-session-delete" not in all_session_ids
|
||||
|
||||
# Verify session is also removed from all sessions (hard delete, not soft)
|
||||
all_sessions_page = await honcho_client.get_sessions()
|
||||
all_sessions = all_sessions_page.items
|
||||
all_session_ids = [s.id for s in all_sessions]
|
||||
assert "test-session-delete" not in all_session_ids
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-delete")
|
||||
assert isinstance(session, Session)
|
||||
|
||||
# Add a peer and messages to make the session have data
|
||||
user = honcho_client.peer(id="user-delete")
|
||||
session.add_peers([user])
|
||||
session.add_messages([user.message("Test message that should be deleted")])
|
||||
|
||||
# Verify messages exist before deletion
|
||||
messages_page = session.get_messages()
|
||||
messages = list(messages_page)
|
||||
assert len(messages) == 1
|
||||
|
||||
# Delete should not raise an exception
|
||||
session.delete()
|
||||
|
||||
# Verify session is removed from active sessions list
|
||||
all_sessions_page = honcho_client.get_sessions({"is_active": True})
|
||||
all_sessions = list(all_sessions_page)
|
||||
all_session_ids = [s.id for s in all_sessions]
|
||||
assert "test-session-delete" not in all_session_ids
|
||||
|
||||
# Verify session is also removed from all sessions (hard delete, not soft)
|
||||
all_sessions_page = honcho_client.get_sessions()
|
||||
all_sessions = list(all_sessions_page)
|
||||
all_session_ids = [s.id for s in all_sessions]
|
||||
assert "test-session-delete" not in all_session_ids
|
||||
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue