sdks: add set peer card function (#371)
* feat: add set peer card to SDK, bump version, document * fix: pytest -> pytest -x * chore: deprecate .card(), move to .getCard() / .get_card() * fix: get_or_create when crudding peer cards * chore: review nits * chore: document .get_card / .set_card * chore: (docs) update language from deriver to dreamer agent * fix: get_peer_card should not create peer/workspace * fix: change api contract to return ResourceNotFoundException (#375) * fix: PR nitpicks --------- Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
This commit is contained in:
parent
3eab54374c
commit
2522cc5ee6
|
|
@ -65,7 +65,7 @@ repos:
|
|||
# Run main application tests
|
||||
- id: pytest-main
|
||||
name: pytest (main app)
|
||||
entry: uv run pytest tests/ --ignore=tests/alembic/
|
||||
entry: uv run pytest -x tests/ --ignore=tests/alembic/
|
||||
language: system
|
||||
files: ^(src/|tests/).*\.py$
|
||||
stages: [pre-push]
|
||||
|
|
|
|||
|
|
@ -4,27 +4,23 @@ description: "Compatibility guide for Honcho's SDKs and API"
|
|||
icon: "shield-check"
|
||||
---
|
||||
|
||||
This guide helps you understand which versions of Honcho's API are compatible with which SDK versions.
|
||||
This guide helps you match the right SDK version to your Honcho API version. Newer SDK patch versions are always backward-compatible within the same major version — install the latest patch for your range.
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
### Honcho API v3.0.2 (Current)
|
||||
## Current Versions
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Compatible Version:** v2.0.0
|
||||
**Latest:** v2.0.1
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install @honcho-ai/sdk@2.0.0
|
||||
npm install @honcho-ai/sdk
|
||||
```
|
||||
</Card>
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Compatible Version:** v2.0.0
|
||||
**Latest:** v2.0.1
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
pip install honcho-ai==2.0.0
|
||||
pip install honcho-ai
|
||||
```
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
@ -34,9 +30,9 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
| Honcho API Version | TypeScript SDK | Python SDK |
|
||||
|-------------------|---------------|------------|
|
||||
| v3.0.2 (Current) | v2.0.0 | v2.0.0 |
|
||||
| v3.0.1 | v2.0.0 | v2.0.0 |
|
||||
| v3.0.0 | v2.0.0 | v2.0.0 |
|
||||
| v3.0.2 (Current) | v2.0.0+ | v2.0.0+ |
|
||||
| v3.0.1 | v2.0.0+ | v2.0.0+ |
|
||||
| v3.0.0 | v2.0.0+ | v2.0.0+ |
|
||||
| v2.5.1 | v1.6.0 | v1.6.0 |
|
||||
| v2.5.0 | v1.6.0 | v1.6.0 |
|
||||
| v2.4.3 | v1.5.0 | v1.5.0 |
|
||||
|
|
|
|||
|
|
@ -280,6 +280,12 @@ metadata = alice.get_metadata()
|
|||
metadata["location"] = "Paris"
|
||||
alice.set_metadata(metadata)
|
||||
|
||||
# Peer card management
|
||||
card = alice.get_card() # Get peer card
|
||||
card = alice.get_card(target="bob") # Get card about another peer
|
||||
updated = alice.set_card(["Likes Python", "Lives in NYC"]) # Set peer card
|
||||
updated = alice.set_card(["Works at Acme"], target="bob") # Set card about another peer
|
||||
|
||||
# Get peer context (representation + peer card in one call)
|
||||
context = alice.context()
|
||||
context = alice.context(target="bob") # What alice knows about bob
|
||||
|
|
@ -334,6 +340,12 @@ await alice.setMetadata({
|
|||
location: "Paris"
|
||||
});
|
||||
|
||||
// Peer card management
|
||||
const card = await alice.getCard(); // Get peer card
|
||||
const targetCard = await alice.getCard("bob"); // Get card about another peer
|
||||
const updated = await alice.setCard(["Likes TypeScript", "Lives in NYC"]); // Set peer card
|
||||
const updatedTarget = await alice.setCard(["Works at Acme"], "bob"); // Set card about another peer
|
||||
|
||||
// Get peer context (representation + peer card in one call)
|
||||
const context = await alice.context();
|
||||
const targetContext = await alice.context({ target: "bob" }); // What alice knows about bob
|
||||
|
|
@ -396,6 +408,46 @@ const searchedContext = await alice.context({
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Peer Card
|
||||
|
||||
The peer card contains stable biographical facts about a peer (name, preferences, background). Use `get_card()` / `getCard()` to retrieve it and `set_card()` / `setCard()` to overwrite it:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get peer's own card
|
||||
card = alice.get_card()
|
||||
print(card) # ["Likes Python", "Lives in NYC", ...]
|
||||
|
||||
# Get card about another peer (local representation)
|
||||
bob_card = alice.get_card(target="bob")
|
||||
|
||||
# Set peer's own card
|
||||
updated = alice.set_card(["Likes Python", "Lives in NYC"])
|
||||
|
||||
# Set card about another peer
|
||||
updated = alice.set_card(["Works at Acme", "Enjoys hiking"], target="bob")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get peer's own card
|
||||
const card = await alice.getCard();
|
||||
console.log(card); // ["Likes TypeScript", "Lives in NYC", ...]
|
||||
|
||||
// Get card about another peer (local representation)
|
||||
const bobCard = await alice.getCard("bob");
|
||||
|
||||
// Set peer's own card
|
||||
const updated = await alice.setCard(["Likes TypeScript", "Lives in NYC"]);
|
||||
|
||||
// Set card about another peer
|
||||
const updatedBob = await alice.setCard(["Works at Acme", "Enjoys hiking"], "bob");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
Peer cards are automatically maintained by the dreaming agent during message processing. Use `set_card()` / `setCard()` when you need to manually override or seed the card — the peer will be created automatically if it doesn't already exist.
|
||||
</Info>
|
||||
|
||||
### Conclusions
|
||||
|
||||
Peers can access their conclusions (facts derived from messages) through the `conclusions` property and `conclusions_of()` method:
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ Additional features with **no Mem0 equivalent**:
|
|||
|
||||
| Honcho Method | Description | Use Case |
|
||||
|---------------|-------------|----------|
|
||||
| `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
|
||||
| `peer.get_card()` / `peer.set_card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
|
||||
| `session.representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation |
|
||||
| `session.summaries()` | Auto-generated short/long session summaries | Conversation continuity |
|
||||
| `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning |
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ Additional features with **no Mem0 equivalent**:
|
|||
|
||||
| Honcho Method | Description | Use Case |
|
||||
|---------------|-------------|----------|
|
||||
| `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
|
||||
| `peer.get_card()` / `peer.set_card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
|
||||
| `session.representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation |
|
||||
| `session.summaries()` | Auto-generated short/long session summaries | Conversation continuity |
|
||||
| `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho-ai"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
description = "Official DX Optimized Python SDK for Honcho"
|
||||
dynamic = ["readme"]
|
||||
license = "Apache-2.0"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ from .types import (
|
|||
DialecticStreamResponse,
|
||||
)
|
||||
|
||||
__version__ = "2.0.0"
|
||||
__version__ = "2.0.1"
|
||||
__author__ = "Plastic Labs"
|
||||
__email__ = "hello@plasticlabs.ai"
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
||||
|
|
@ -588,7 +589,7 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
]
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def card(
|
||||
async def get_card(
|
||||
self,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> list[str] | None:
|
||||
|
|
@ -604,6 +605,51 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
response = PeerCardResponse.model_validate(data)
|
||||
return response.peer_card
|
||||
|
||||
async def card(
|
||||
self,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> list[str] | None:
|
||||
"""Deprecated: use get_card() instead."""
|
||||
|
||||
warnings.warn(
|
||||
"card() is deprecated, use get_card() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return await self.get_card(target=target)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def set_card(
|
||||
self,
|
||||
peer_card: list[str],
|
||||
target: str | PeerBase | None = None,
|
||||
) -> list[str] | None:
|
||||
"""
|
||||
Set the peer card for this peer.
|
||||
|
||||
Makes an API call to set the peer card. If a target is provided, sets this
|
||||
peer's local card of the target peer.
|
||||
|
||||
Args:
|
||||
peer_card: A list of strings to set as the peer card.
|
||||
target: Optional target peer for local card. If provided, sets this
|
||||
peer's card of the target peer. Can be a Peer object or peer ID string.
|
||||
|
||||
Returns:
|
||||
A list of strings representing the updated peer card, or None if none is available
|
||||
"""
|
||||
await self._peer._honcho._ensure_workspace_async()
|
||||
target_id = resolve_id(target)
|
||||
|
||||
query = {"target": target_id} if target_id else None
|
||||
data = await self._peer._honcho._async_http_client.put(
|
||||
routes.peer_card(self._peer.workspace_id, self._peer.id),
|
||||
body={"peer_card": peer_card},
|
||||
query=query,
|
||||
)
|
||||
response = PeerCardResponse.model_validate(data)
|
||||
return response.peer_card
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def representation(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Generator
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
|
|
@ -446,7 +447,7 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
]
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def card(
|
||||
def get_card(
|
||||
self,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> list[str] | None:
|
||||
|
|
@ -476,6 +477,50 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
|
||||
return response.peer_card
|
||||
|
||||
def card(
|
||||
self,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> list[str] | None:
|
||||
"""Deprecated: use get_card() instead."""
|
||||
warnings.warn(
|
||||
"card() is deprecated, use get_card() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.get_card(target=target)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def set_card(
|
||||
self,
|
||||
peer_card: list[str],
|
||||
target: str | PeerBase | None = None,
|
||||
) -> list[str] | None:
|
||||
"""
|
||||
Set the peer card for this peer.
|
||||
|
||||
Makes an API call to set the peer card. If a target is provided, sets this
|
||||
peer's local card of the target peer.
|
||||
|
||||
Args:
|
||||
peer_card: A list of strings to set as the peer card.
|
||||
target: Optional target peer for local card. If provided, sets this
|
||||
peer's card of the target peer. Can be a Peer object or peer ID string.
|
||||
|
||||
Returns:
|
||||
A list of strings representing the updated peer card, or None if none is available
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
target_id = resolve_id(target)
|
||||
|
||||
query = {"target": target_id} if target_id else None
|
||||
data = self._honcho._http.put(
|
||||
routes.peer_card(self.workspace_id, self.id),
|
||||
body={"peer_card": peer_card},
|
||||
query=query,
|
||||
)
|
||||
response = PeerCardResponse.model_validate(data)
|
||||
return response.peer_card
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def representation(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -389,6 +389,9 @@ describe('Peer', () => {
|
|||
describe('POST /peers/:id/card', () => {
|
||||
test('card returns string array or null', async () => {
|
||||
const peer = await client.peer('card-peer')
|
||||
const session = await client.session('card-session')
|
||||
await session.addPeers([peer.id])
|
||||
await session.addMessages([peer.message('hello')])
|
||||
|
||||
const card = await peer.card()
|
||||
|
||||
|
|
@ -399,6 +402,9 @@ describe('Peer', () => {
|
|||
test('card with target peer', async () => {
|
||||
const observer = await client.peer('card-observer')
|
||||
const observed = await client.peer('card-observed')
|
||||
const session = await client.session('card-target-session')
|
||||
await session.addPeers([observer.id, observed.id])
|
||||
await session.addMessages([observer.message('hello'), observed.message('hi')])
|
||||
|
||||
const card = await observer.card(observed)
|
||||
|
||||
|
|
@ -408,6 +414,10 @@ describe('Peer', () => {
|
|||
|
||||
test('card with target ID string', async () => {
|
||||
const peer = await client.peer('card-string-peer')
|
||||
const target = await client.peer('some-target-id')
|
||||
const session = await client.session('card-string-session')
|
||||
await session.addPeers([peer.id, target.id])
|
||||
await session.addMessages([peer.message('hello'), target.message('hi')])
|
||||
|
||||
const card = await peer.card('some-target-id')
|
||||
|
||||
|
|
@ -430,6 +440,48 @@ describe('Peer', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Set Peer Card (PUT /peers/:id/card)
|
||||
// ===========================================================================
|
||||
|
||||
describe('PUT /peers/:id/card', () => {
|
||||
test('setCard sets and returns peer card', async () => {
|
||||
const peer = await client.peer('setcard-peer')
|
||||
|
||||
const cardData = ['fact one', 'fact two']
|
||||
const result = await peer.setCard(cardData)
|
||||
|
||||
expect(result).toEqual(cardData)
|
||||
|
||||
// Verify with get
|
||||
const card = await peer.getCard()
|
||||
expect(card).toEqual(cardData)
|
||||
})
|
||||
|
||||
test('setCard with target peer', async () => {
|
||||
const observer = await client.peer('setcard-observer')
|
||||
const observed = await client.peer('setcard-observed')
|
||||
|
||||
const cardData = ['target likes TypeScript', 'target is clever']
|
||||
const result = await observer.setCard(cardData, observed)
|
||||
|
||||
expect(result).toEqual(cardData)
|
||||
|
||||
// Verify with get
|
||||
const card = await observer.getCard(observed)
|
||||
expect(card).toEqual(cardData)
|
||||
})
|
||||
|
||||
test('setCard with target ID string', async () => {
|
||||
const peer = await client.peer('setcard-string-peer')
|
||||
|
||||
const cardData = ['some fact']
|
||||
const result = await peer.setCard(cardData, 'setcard-string-target')
|
||||
|
||||
expect(result).toEqual(cardData)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Peer Context (POST /peers/:id/context)
|
||||
// ===========================================================================
|
||||
|
|
@ -437,6 +489,9 @@ describe('Peer', () => {
|
|||
describe('POST /peers/:id/context', () => {
|
||||
test('context returns representation and card', async () => {
|
||||
const peer = await client.peer('context-peer')
|
||||
const session = await client.session('context-session')
|
||||
await session.addPeers([peer.id])
|
||||
await session.addMessages([peer.message('hello')])
|
||||
|
||||
const context = await peer.context()
|
||||
|
||||
|
|
@ -450,6 +505,9 @@ describe('Peer', () => {
|
|||
test('context with target peer', async () => {
|
||||
const observer = await client.peer('context-observer')
|
||||
const observed = await client.peer('context-observed')
|
||||
const session = await client.session('context-target-session')
|
||||
await session.addPeers([observer.id, observed.id])
|
||||
await session.addMessages([observer.message('hello'), observed.message('hi')])
|
||||
|
||||
const context = await observer.context({ target: observed })
|
||||
|
||||
|
|
@ -459,6 +517,9 @@ describe('Peer', () => {
|
|||
|
||||
test('context with options', async () => {
|
||||
const peer = await client.peer('context-options-peer')
|
||||
const session = await client.session('context-options-session')
|
||||
await session.addPeers([peer.id])
|
||||
await session.addMessages([peer.message('hello')])
|
||||
|
||||
const context = await peer.context({
|
||||
searchQuery: 'interests',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@honcho-ai/sdk",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Official DX Optimized TypeScript SDK for Honcho",
|
||||
"author": "Plastic Labs <hello@plasticlabs.ai>",
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
MessageConfigurationSchema,
|
||||
MessageContentSchema,
|
||||
MessageMetadataSchema,
|
||||
PeerCardContentSchema,
|
||||
type PeerConfig,
|
||||
PeerConfigSchema,
|
||||
PeerGetRepresentationParamsSchema,
|
||||
|
|
@ -295,6 +296,18 @@ export class Peer {
|
|||
)
|
||||
}
|
||||
|
||||
private async _setCard(params: {
|
||||
peer_card: string[]
|
||||
target?: string
|
||||
}): Promise<PeerCardResponse> {
|
||||
await this._ensureWorkspace()
|
||||
const { peer_card, ...query } = params
|
||||
return this._http.put<PeerCardResponse>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/peers/${this.id}/card`,
|
||||
{ body: { peer_card }, query }
|
||||
)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Public Methods
|
||||
// ===========================================================================
|
||||
|
|
@ -652,7 +665,7 @@ export class Peer {
|
|||
* @returns Promise resolving to an array of strings containing the peer card items,
|
||||
* or null if no peer card exists
|
||||
*/
|
||||
async card(target?: string | Peer): Promise<string[] | null> {
|
||||
async getCard(target?: string | Peer): Promise<string[] | null> {
|
||||
const validatedTarget = CardTargetSchema.parse(target)
|
||||
|
||||
const response = await this._getCard({
|
||||
|
|
@ -662,6 +675,38 @@ export class Peer {
|
|||
return response.peer_card
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link getCard} instead.
|
||||
*/
|
||||
async card(target?: string | Peer): Promise<string[] | null> {
|
||||
return this.getCard(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the peer card for this peer.
|
||||
*
|
||||
* Makes an API call to set the peer card. If a target is provided, sets this
|
||||
* peer's local card of the target peer.
|
||||
*
|
||||
* @param peerCard - An array of strings to set as the peer card.
|
||||
* @param target - Optional target peer for local card. If provided, sets this
|
||||
* peer's card of the target peer. Can be a Peer object or peer ID string.
|
||||
* @returns Promise resolving to an array of strings containing the updated peer card items,
|
||||
* or null if no peer card exists
|
||||
*/
|
||||
async setCard(
|
||||
peerCard: string[],
|
||||
target?: string | Peer
|
||||
): Promise<string[] | null> {
|
||||
const validatedPeerCard = PeerCardContentSchema.parse(peerCard)
|
||||
const validatedTarget = CardTargetSchema.parse(target)
|
||||
const response = await this._setCard({
|
||||
peer_card: validatedPeerCard,
|
||||
target: validatedTarget,
|
||||
})
|
||||
return response.peer_card
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subset of Honcho's Representation of a peer.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -376,6 +376,11 @@ export const CardTargetSchema = z
|
|||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
)
|
||||
|
||||
/**
|
||||
* Schema for peer card content (array of strings).
|
||||
*/
|
||||
export const PeerCardContentSchema = z.array(z.string())
|
||||
|
||||
/**
|
||||
* Schema for peer addition to session.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from src import exceptions, models, schemas
|
||||
from src.cache.client import cache
|
||||
from src.crud.peer import get_peer, peer_cache_key
|
||||
from src.crud.peer import get_or_create_peers, get_peer, peer_cache_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -32,18 +32,18 @@ async def get_peer_card(
|
|||
observer: Peer name of the observer
|
||||
|
||||
Returns:
|
||||
The peer's card text if present, otherwise None (also None if peer not found).
|
||||
The peer's card text if present, otherwise None.
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the peer does not exist.
|
||||
"""
|
||||
try:
|
||||
peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
|
||||
return cast(
|
||||
list[str] | None,
|
||||
peer.internal_metadata.get(
|
||||
construct_peer_card_label(observer=observer, observed=observed)
|
||||
),
|
||||
)
|
||||
except exceptions.ResourceNotFoundException:
|
||||
return None
|
||||
peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
|
||||
return cast(
|
||||
list[str] | None,
|
||||
peer.internal_metadata.get(
|
||||
construct_peer_card_label(observer=observer, observed=observed)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def set_peer_card(
|
||||
|
|
@ -66,9 +66,10 @@ async def set_peer_card(
|
|||
observed: Peer name of the peer described in the peer card
|
||||
observer: Peer name of the observer
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the peer does not exist
|
||||
"""
|
||||
# Ensure the peer exists (get-or-create)
|
||||
await get_or_create_peers(db, workspace_name, [schemas.PeerCreate(name=observer)])
|
||||
|
||||
stmt = (
|
||||
update(models.Peer)
|
||||
.where(models.Peer.workspace_name == workspace_name)
|
||||
|
|
@ -82,11 +83,9 @@ async def set_peer_card(
|
|||
}
|
||||
)
|
||||
)
|
||||
.returning(models.Peer)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
updated_peer = result.scalar_one_or_none()
|
||||
if updated_peer is None:
|
||||
if result.rowcount == 0:
|
||||
raise exceptions.ResourceNotFoundException(
|
||||
f"Peer {observer} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using the DialecticAgent.
|
|||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from src import crud
|
||||
from src import crud, schemas
|
||||
from src.config import ReasoningLevel
|
||||
from src.dependencies import tracked_db
|
||||
from src.dialectic.core import DialecticAgent
|
||||
|
|
@ -40,6 +40,11 @@ async def agentic_chat(
|
|||
The synthesized answer string
|
||||
"""
|
||||
async with tracked_db("dialectic.agentic_chat") as db:
|
||||
# Validate that the peers exist before proceeding
|
||||
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
|
||||
if observer != observed:
|
||||
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
|
||||
|
||||
# Resolve configuration to check if peer cards should be used
|
||||
session = None
|
||||
if session_name:
|
||||
|
|
@ -101,6 +106,11 @@ async def agentic_chat_stream(
|
|||
Chunks of the response text as they are generated
|
||||
"""
|
||||
async with tracked_db("dialectic.agentic_chat_stream") as db:
|
||||
# Validate that the peers exist before proceeding
|
||||
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
|
||||
if observer != observed:
|
||||
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
|
||||
|
||||
# Resolve configuration to check if peer cards should be used
|
||||
session = None
|
||||
if session_name:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from typing import Any
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud
|
||||
from src import crud, schemas
|
||||
from src.config import settings
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.telemetry import prometheus_metrics
|
||||
|
|
@ -142,6 +142,11 @@ class BaseSpecialist(ABC):
|
|||
task_name = f"dreamer_{self.name}_{run_id}"
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Validate that the peers exist before proceeding
|
||||
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
|
||||
if observer != observed:
|
||||
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
|
||||
|
||||
# Determine if peer card tools should be included
|
||||
peer_card_enabled = configuration is None or configuration.peer_card.create
|
||||
|
||||
|
|
|
|||
|
|
@ -338,11 +338,7 @@ async def set_peer_card(
|
|||
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)
|
||||
return schemas.PeerCardResponse(peer_card=peer_card_data.peer_card)
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
|
|||
|
|
@ -51,62 +51,30 @@ async def test_peer_card_get_set_roundtrip(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_peer_card_missing_peer_raises(
|
||||
async def test_get_peer_card_missing_peer_raises(
|
||||
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Setting a peer card for a non-existent peer should raise ResourceNotFoundException."""
|
||||
"""Getting a peer card for a non-existent peer should raise ResourceNotFoundException."""
|
||||
workspace, _existing_peer = sample_data
|
||||
with pytest.raises(ResourceNotFoundException):
|
||||
await set_peer_card(
|
||||
await get_peer_card(
|
||||
db_session,
|
||||
workspace.name,
|
||||
["card"],
|
||||
observer="missing-peer",
|
||||
observed="missing-peer",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_peer_card_missing_peer_returns_none(
|
||||
async def test_get_peer_card_missing_workspace_raises(
|
||||
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Getting a peer card for a non-existent peer should return None."""
|
||||
workspace, _existing_peer = sample_data
|
||||
result = await get_peer_card(
|
||||
db_session,
|
||||
workspace.name,
|
||||
observer="missing-peer",
|
||||
observed="missing-peer",
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_peer_card_missing_workspace_returns_none(
|
||||
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Getting a peer card for a non-existent workspace should return None."""
|
||||
_workspace, peer = sample_data
|
||||
result = await get_peer_card(
|
||||
db_session,
|
||||
"missing-workspace",
|
||||
observer=peer.name,
|
||||
observed=peer.name,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_peer_card_missing_workspace_raises(
|
||||
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Setting a peer card for a non-existent workspace should raise ResourceNotFoundException."""
|
||||
"""Getting a peer card for a non-existent workspace should raise ResourceNotFoundException."""
|
||||
_workspace, peer = sample_data
|
||||
with pytest.raises(ResourceNotFoundException):
|
||||
await set_peer_card(
|
||||
await get_peer_card(
|
||||
db_session,
|
||||
"missing-workspace",
|
||||
["card"],
|
||||
observer=peer.name,
|
||||
observed=peer.name,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -909,3 +909,52 @@ async def test_get_peer_card_with_data(
|
|||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["peer_card"] == target_card_content
|
||||
|
||||
|
||||
def test_set_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test setting peer cards via the PUT endpoint."""
|
||||
test_workspace, observer_peer = sample_data
|
||||
|
||||
# Create a target peer
|
||||
target_peer_name = str(generate_nanoid())
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/peers",
|
||||
json={"name": target_peer_name},
|
||||
)
|
||||
assert response.status_code in [200, 201]
|
||||
|
||||
# Set the observer's own card
|
||||
self_card = ["I am a test peer", "I like writing tests"]
|
||||
response = client.put(
|
||||
f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
|
||||
json={"peer_card": self_card},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["peer_card"] == self_card
|
||||
|
||||
# Verify with GET
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["peer_card"] == self_card
|
||||
|
||||
# Set a card for the target peer
|
||||
target_card = ["Target is helpful", "Target knows Python"]
|
||||
response = client.put(
|
||||
f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
|
||||
params={"target": target_peer_name},
|
||||
json={"peer_card": target_card},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["peer_card"] == target_card
|
||||
|
||||
# Verify with GET
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
|
||||
params={"target": target_peer_name},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["peer_card"] == target_card
|
||||
|
|
|
|||
|
|
@ -104,3 +104,41 @@ def test_message_and_chat_operations(honcho_test_client: Honcho):
|
|||
|
||||
# This is a mock response from the agent
|
||||
_response = user.chat("What did I ask about?")
|
||||
|
||||
|
||||
def test_peer_card_operations(honcho_test_client: Honcho):
|
||||
"""
|
||||
Tests setting and getting peer cards.
|
||||
"""
|
||||
peer = honcho_test_client.peer(id="card-test-peer")
|
||||
target = honcho_test_client.peer(id="card-test-target")
|
||||
|
||||
# Create the peers on the server by adding a message
|
||||
session = honcho_test_client.session(id="card-test-session")
|
||||
session.add_messages([peer.message("hello"), target.message("hi")])
|
||||
|
||||
# Initially card should be None
|
||||
card = peer.get_card()
|
||||
assert card is None
|
||||
|
||||
# Set own card
|
||||
own_card = ["I am a helpful assistant", "I enjoy learning"]
|
||||
result = peer.set_card(own_card)
|
||||
assert result == own_card
|
||||
|
||||
# Verify with get
|
||||
card = peer.get_card()
|
||||
assert card == own_card
|
||||
|
||||
# Set card for target
|
||||
target_card = ["Target likes Python", "Target is friendly"]
|
||||
result = peer.set_card(target_card, target=target)
|
||||
assert result == target_card
|
||||
|
||||
# Verify with get
|
||||
card = peer.get_card(target=target)
|
||||
assert card == target_card
|
||||
|
||||
# Own card should still be unchanged
|
||||
card = peer.get_card()
|
||||
assert card == own_card
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async def test_peer_card_global(client_fixture: tuple[Honcho, str]):
|
|||
await session.aio.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Get global peer card
|
||||
card_response = await peer.aio.card()
|
||||
card_response = await peer.aio.get_card()
|
||||
assert card_response is None or isinstance(card_response, list)
|
||||
else:
|
||||
peer = honcho_client.peer(id="test-card-global-peer")
|
||||
|
|
@ -103,7 +103,7 @@ async def test_peer_card_global(client_fixture: tuple[Honcho, str]):
|
|||
session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Get global peer card
|
||||
card_response = peer.card()
|
||||
card_response = peer.get_card()
|
||||
assert card_response is None or isinstance(card_response, list)
|
||||
|
||||
|
||||
|
|
@ -125,11 +125,11 @@ async def test_peer_card_local(client_fixture: tuple[Honcho, str]):
|
|||
)
|
||||
|
||||
# Get local peer card with target as Peer object
|
||||
card_response = await observer.aio.card(target=target)
|
||||
card_response = await observer.aio.get_card(target=target)
|
||||
assert card_response is None or isinstance(card_response, list)
|
||||
|
||||
# Get local peer card with target as string
|
||||
card_response = await observer.aio.card(target=target.id)
|
||||
card_response = await observer.aio.get_card(target=target.id)
|
||||
assert card_response is None or isinstance(card_response, list)
|
||||
else:
|
||||
observer = honcho_client.peer(id="test-card-local-observer")
|
||||
|
|
@ -140,11 +140,11 @@ async def test_peer_card_local(client_fixture: tuple[Honcho, str]):
|
|||
session.add_messages([observer.message("Hello"), target.message("Hi there")])
|
||||
|
||||
# Get local peer card with target as Peer object
|
||||
card_response = observer.card(target=target)
|
||||
card_response = observer.get_card(target=target)
|
||||
assert card_response is None or isinstance(card_response, list)
|
||||
|
||||
# Get local peer card with target as string
|
||||
card_response = observer.card(target=target.id)
|
||||
card_response = observer.get_card(target=target.id)
|
||||
assert card_response is None or isinstance(card_response, list)
|
||||
|
||||
|
||||
|
|
@ -159,15 +159,23 @@ async def test_peer_card_with_empty_target(client_fixture: tuple[Honcho, str]):
|
|||
|
||||
if client_type == "async":
|
||||
peer = await honcho_client.aio.peer(id="test-card-validation-peer")
|
||||
session = await honcho_client.aio.session(id="test-card-validation-session")
|
||||
|
||||
# Create the peer on the server by adding a message
|
||||
await session.aio.add_messages([peer.message("hello")])
|
||||
|
||||
# Empty target is treated as no target (same as None)
|
||||
result = await peer.aio.card(target="")
|
||||
result = await peer.aio.get_card(target="")
|
||||
assert result is None or isinstance(result, list)
|
||||
else:
|
||||
peer = honcho_client.peer(id="test-card-validation-peer")
|
||||
session = honcho_client.session(id="test-card-validation-session")
|
||||
|
||||
# Create the peer on the server by adding a message
|
||||
session.add_messages([peer.message("hello")])
|
||||
|
||||
# Empty target is treated as no target (same as None)
|
||||
result = peer.card(target="")
|
||||
result = peer.get_card(target="")
|
||||
assert result is None or isinstance(result, list)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ class UnifiedTestExecutor:
|
|||
raise ValueError("peer_id required for get_peer_card")
|
||||
|
||||
peer = await self.client.aio.peer(id=step.observer_peer_id)
|
||||
card = await peer.aio.card(
|
||||
card = await peer.aio.get_card(
|
||||
step.observed_peer_id
|
||||
if step.observed_peer_id
|
||||
else step.observer_peer_id
|
||||
|
|
|
|||
Loading…
Reference in New Issue