From 2522cc5ee6507ee01e2a8b9520d0c3ceff935c30 Mon Sep 17 00:00:00 2001 From: doria <93405247+dr-frmr@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:35:57 -0500 Subject: [PATCH] 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> --- .pre-commit-config.yaml | 2 +- docs/changelog/compatibility-guide.mdx | 22 ++++----- docs/v3/documentation/reference/sdk.mdx | 52 +++++++++++++++++++++ docs/v3/guides/migrations/mem0.mdx | 2 +- docs/v3/migrations/from-mem0.mdx | 2 +- sdks/python/pyproject.toml | 2 +- sdks/python/src/honcho/__init__.py | 2 +- sdks/python/src/honcho/aio.py | 48 ++++++++++++++++++- sdks/python/src/honcho/peer.py | 47 ++++++++++++++++++- sdks/typescript/__tests__/peer.test.ts | 61 +++++++++++++++++++++++++ sdks/typescript/package.json | 2 +- sdks/typescript/src/peer.ts | 47 ++++++++++++++++++- sdks/typescript/src/validation.ts | 5 ++ src/crud/peer_card.py | 33 +++++++------ src/dialectic/chat.py | 12 ++++- src/dreamer/specialists.py | 7 ++- src/routers/peers.py | 6 +-- tests/crud/test_peer_card.py | 44 +++--------------- tests/routes/test_peers.py | 49 ++++++++++++++++++++ tests/sdk/sdk_integration_test.py | 38 +++++++++++++++ tests/sdk/test_peer.py | 24 ++++++---- tests/unified/runner.py | 2 +- uv.lock | 2 +- 23 files changed, 417 insertions(+), 94 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51428930..4ba8ab43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] diff --git a/docs/changelog/compatibility-guide.mdx b/docs/changelog/compatibility-guide.mdx index 33e01f7f..719ea184 100644 --- a/docs/changelog/compatibility-guide.mdx +++ b/docs/changelog/compatibility-guide.mdx @@ -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 - **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 ``` - **Compatible Version:** v2.0.0 + **Latest:** v2.0.1 - Install with: ```bash - pip install honcho-ai==2.0.0 + pip install honcho-ai ``` @@ -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 | diff --git a/docs/v3/documentation/reference/sdk.mdx b/docs/v3/documentation/reference/sdk.mdx index 967f39ff..9aa0f509 100644 --- a/docs/v3/documentation/reference/sdk.mdx +++ b/docs/v3/documentation/reference/sdk.mdx @@ -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({ ``` +### 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: + + +```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"); +``` + + + +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. + + ### Conclusions Peers can access their conclusions (facts derived from messages) through the `conclusions` property and `conclusions_of()` method: diff --git a/docs/v3/guides/migrations/mem0.mdx b/docs/v3/guides/migrations/mem0.mdx index 6a2bbd22..1d56bde6 100644 --- a/docs/v3/guides/migrations/mem0.mdx +++ b/docs/v3/guides/migrations/mem0.mdx @@ -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 | diff --git a/docs/v3/migrations/from-mem0.mdx b/docs/v3/migrations/from-mem0.mdx index 6a2bbd22..1d56bde6 100644 --- a/docs/v3/migrations/from-mem0.mdx +++ b/docs/v3/migrations/from-mem0.mdx @@ -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 | diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 20204d04..dd389d96 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -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" diff --git a/sdks/python/src/honcho/__init__.py b/sdks/python/src/honcho/__init__.py index cf8d8bb5..35f42dce 100644 --- a/sdks/python/src/honcho/__init__.py +++ b/sdks/python/src/honcho/__init__.py @@ -66,7 +66,7 @@ from .types import ( DialecticStreamResponse, ) -__version__ = "2.0.0" +__version__ = "2.0.1" __author__ = "Plastic Labs" __email__ = "hello@plasticlabs.ai" diff --git a/sdks/python/src/honcho/aio.py b/sdks/python/src/honcho/aio.py index 1d72946a..aa851889 100644 --- a/sdks/python/src/honcho/aio.py +++ b/sdks/python/src/honcho/aio.py @@ -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, diff --git a/sdks/python/src/honcho/peer.py b/sdks/python/src/honcho/peer.py index ea71fc79..5984737b 100644 --- a/sdks/python/src/honcho/peer.py +++ b/sdks/python/src/honcho/peer.py @@ -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, diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts index 9a3b3b61..77953168 100644 --- a/sdks/typescript/__tests__/peer.test.ts +++ b/sdks/typescript/__tests__/peer.test.ts @@ -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', diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 956066ed..618a0acf 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -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 ", "license": "Apache-2.0", diff --git a/sdks/typescript/src/peer.ts b/sdks/typescript/src/peer.ts index 2ce07468..65567ad5 100644 --- a/sdks/typescript/src/peer.ts +++ b/sdks/typescript/src/peer.ts @@ -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 { + await this._ensureWorkspace() + const { peer_card, ...query } = params + return this._http.put( + `/${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 { + async getCard(target?: string | Peer): Promise { 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 { + 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 { + 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. * diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts index 3a5e8584..829699ca 100644 --- a/sdks/typescript/src/validation.ts +++ b/sdks/typescript/src/validation.ts @@ -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. */ diff --git a/src/crud/peer_card.py b/src/crud/peer_card.py index f04205ed..37c2749c 100644 --- a/src/crud/peer_card.py +++ b/src/crud/peer_card.py @@ -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}" ) diff --git a/src/dialectic/chat.py b/src/dialectic/chat.py index e2783db8..5ea0e11f 100644 --- a/src/dialectic/chat.py +++ b/src/dialectic/chat.py @@ -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: diff --git a/src/dreamer/specialists.py b/src/dreamer/specialists.py index 0c499594..cb690420 100644 --- a/src/dreamer/specialists.py +++ b/src/dreamer/specialists.py @@ -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 diff --git a/src/routers/peers.py b/src/routers/peers.py index 3bff98a3..e9eea331 100644 --- a/src/routers/peers.py +++ b/src/routers/peers.py @@ -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( diff --git a/tests/crud/test_peer_card.py b/tests/crud/test_peer_card.py index c057323b..ada64bd9 100644 --- a/tests/crud/test_peer_card.py +++ b/tests/crud/test_peer_card.py @@ -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, ) diff --git a/tests/routes/test_peers.py b/tests/routes/test_peers.py index 84f23887..c8047f02 100644 --- a/tests/routes/test_peers.py +++ b/tests/routes/test_peers.py @@ -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 diff --git a/tests/sdk/sdk_integration_test.py b/tests/sdk/sdk_integration_test.py index f5a14fe8..a8a90203 100644 --- a/tests/sdk/sdk_integration_test.py +++ b/tests/sdk/sdk_integration_test.py @@ -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 diff --git a/tests/sdk/test_peer.py b/tests/sdk/test_peer.py index dae19398..258fb989 100644 --- a/tests/sdk/test_peer.py +++ b/tests/sdk/test_peer.py @@ -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) diff --git a/tests/unified/runner.py b/tests/unified/runner.py index 2168d1c4..0d07d5fe 100644 --- a/tests/unified/runner.py +++ b/tests/unified/runner.py @@ -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 diff --git a/uv.lock b/uv.lock index 8817b2e7..ec6c0ab0 100644 --- a/uv.lock +++ b/uv.lock @@ -1147,7 +1147,7 @@ dev = [ [[package]] name = "honcho-ai" -version = "2.0.0" +version = "2.0.1" source = { editable = "sdks/python" } dependencies = [ { name = "httpx" },