From 7cc39d2b5631cb3d36005dfe69e1928c963157aa Mon Sep 17 00:00:00 2001 From: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:04:37 -0500 Subject: [PATCH] fix: change api contract to return ResourceNotFoundException (#375) --- sdks/typescript/__tests__/peer.test.ts | 19 +++++++++++++ src/crud/peer_card.py | 17 +++++------- src/dialectic/chat.py | 12 ++++++++- src/dreamer/specialists.py | 7 ++++- tests/crud/test_peer_card.py | 37 +++++++++++++------------- tests/sdk/sdk_integration_test.py | 4 +++ tests/sdk/test_peer.py | 8 ++++++ 7 files changed, 74 insertions(+), 30 deletions(-) diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts index b51a8483..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') @@ -479,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() @@ -492,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 }) @@ -501,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/src/crud/peer_card.py b/src/crud/peer_card.py index 254cdb4e..79e64bb0 100644 --- a/src/crud/peer_card.py +++ b/src/crud/peer_card.py @@ -3,12 +3,12 @@ from __future__ import annotations import logging from typing import cast -from sqlalchemy import select, update +from sqlalchemy import update from sqlalchemy.ext.asyncio import AsyncSession from src import models, schemas from src.cache.client import cache -from src.crud.peer import get_or_create_peers, peer_cache_key +from src.crud.peer import get_or_create_peers, get_peer, peer_cache_key logger = logging.getLogger(__name__) @@ -32,15 +32,12 @@ 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. """ - peer = await db.scalar( - select(models.Peer) - .where(models.Peer.workspace_name == workspace_name) - .where(models.Peer.name == observer) - ) - if peer is None: - return None + peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer)) return cast( list[str] | None, peer.internal_metadata.get( 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/tests/crud/test_peer_card.py b/tests/crud/test_peer_card.py index 2b00d56e..ada64bd9 100644 --- a/tests/crud/test_peer_card.py +++ b/tests/crud/test_peer_card.py @@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from src import models from src.crud.peer_card import construct_peer_card_label, get_peer_card, set_peer_card +from src.exceptions import ResourceNotFoundException @pytest.mark.asyncio @@ -50,33 +51,33 @@ async def test_peer_card_get_set_roundtrip( @pytest.mark.asyncio -async def test_get_peer_card_missing_peer_returns_none( +async def test_get_peer_card_missing_peer_raises( db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer] ): - """Getting a peer card for a non-existent peer should return None while creating the peer.""" + """Getting a peer card for a non-existent peer should raise ResourceNotFoundException.""" workspace, _existing_peer = sample_data - result = await get_peer_card( - db_session, - workspace.name, - observer="missing-peer", - observed="missing-peer", - ) - assert result is None + with pytest.raises(ResourceNotFoundException): + await get_peer_card( + db_session, + workspace.name, + observer="missing-peer", + observed="missing-peer", + ) @pytest.mark.asyncio -async def test_get_peer_card_missing_workspace_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 workspace should return None.""" + """Getting a peer card for a non-existent workspace should raise ResourceNotFoundException.""" _workspace, peer = sample_data - result = await get_peer_card( - db_session, - "missing-workspace", - observer=peer.name, - observed=peer.name, - ) - assert result is None + with pytest.raises(ResourceNotFoundException): + await get_peer_card( + db_session, + "missing-workspace", + observer=peer.name, + observed=peer.name, + ) @pytest.mark.asyncio diff --git a/tests/sdk/sdk_integration_test.py b/tests/sdk/sdk_integration_test.py index d9c5906f..a8a90203 100644 --- a/tests/sdk/sdk_integration_test.py +++ b/tests/sdk/sdk_integration_test.py @@ -113,6 +113,10 @@ def test_peer_card_operations(honcho_test_client: Honcho): 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 diff --git a/tests/sdk/test_peer.py b/tests/sdk/test_peer.py index 62b3a54f..258fb989 100644 --- a/tests/sdk/test_peer.py +++ b/tests/sdk/test_peer.py @@ -159,12 +159,20 @@ 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.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.get_card(target="")