From fb66630142b2452f03707ea89d357da4f61c017b Mon Sep 17 00:00:00 2001 From: Rajat Ahuja Date: Wed, 24 Sep 2025 12:56:45 -0400 Subject: [PATCH] feat: get peer cards endpoint (#209) * feat: get peer cards endpoint * fix: rm try/catch * refactor: POST -> GET * fix: /peer-cards -> /peer-card * fix: /peer-cards -> /peer-card --------- Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> --- src/routers/peers.py | 35 ++++++++++++--- src/schemas.py | 6 +++ tests/routes/test_peers.py | 88 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/src/routers/peers.py b/src/routers/peers.py index b022a945..31d11d79 100644 --- a/src/routers/peers.py +++ b/src/routers/peers.py @@ -1,12 +1,7 @@ import logging from collections.abc import AsyncGenerator, AsyncIterator -from fastapi import ( - APIRouter, - Body, - Depends, - Path, -) +from fastapi import APIRouter, Body, Depends, Path, Query from fastapi.exceptions import HTTPException from fastapi.responses import StreamingResponse from fastapi_pagination import Page @@ -239,6 +234,34 @@ async def get_working_representation( raise ResourceNotFoundException("Peer or session not found") from e +@router.get( + "/{peer_id}/card", + response_model=schemas.PeerCardResponse, + dependencies=[ + Depends(require_auth(workspace_name="workspace_id", peer_name="peer_id")) + ], +) +async def get_peer_card( + workspace_id: str = Path(..., description="ID of the workspace"), + peer_id: str = Path(..., description="ID of the observer peer"), + target: str | None = Query( + None, + description="The peer whose card to retrieve. If not provided, returns the observer's own card", + ), + db: AsyncSession = db, +): + """Get a peer card for a specific peer relationship. + + Returns the peer card that the observer peer has for the target peer if it exists. + If no target is specified, returns the observer's own peer card. + """ + # If no target specified, get the observer's own card + target_peer = target if target is not None else peer_id + + peer_card = await crud.get_peer_card(db, workspace_id, target_peer, peer_id) + return schemas.PeerCardResponse(peer_card=peer_card) + + @router.post( "/{peer_id}/search", response_model=list[schemas.Message], diff --git a/src/schemas.py b/src/schemas.py index fec16563..ac1b226c 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -104,6 +104,12 @@ class PeerRepresentationGet(BaseModel): ) +class PeerCardResponse(BaseModel): + peer_card: list[str] | None = Field( + None, description="The peer card content, or None if not found" + ) + + class PeerConfig(BaseModel): observe_me: bool = Field( default=True, diff --git a/tests/routes/test_peers.py b/tests/routes/test_peers.py index 0b1928c7..2b4f3c1e 100644 --- a/tests/routes/test_peers.py +++ b/tests/routes/test_peers.py @@ -1,10 +1,12 @@ from typing import Any from unittest.mock import AsyncMock, patch +import pytest from fastapi.testclient import TestClient from nanoid import generate as generate_nanoid from sqlalchemy.ext.asyncio import AsyncSession +from src import crud from src.models import Peer, Workspace @@ -569,3 +571,89 @@ def test_update_peer_all_fields( data = response.json() assert data["metadata"] == metadata assert data["configuration"] == configuration + + +def test_get_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]): + """Test the peer cards endpoint""" + test_workspace, observer_peer = sample_data + + # Create a second peer (the target/observed peer) + target_peer_name = str(generate_nanoid()) + response = client.post( + f"/v2/workspaces/{test_workspace.name}/peers", + json={"name": target_peer_name}, + ) + assert response.status_code == 200 + + # Test getting observer's own card (should return null initially) + response = client.get( + f"/v2/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card" + ) + assert response.status_code == 200 + data = response.json() + assert data["peer_card"] is None + + # Test getting card for target peer (should return null initially) + response = client.get( + f"/v2/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", + params={"target": target_peer_name}, + ) + assert response.status_code == 200 + data = response.json() + assert data["peer_card"] is None + + +@pytest.mark.asyncio +async def test_get_peer_card_with_data( + client: TestClient, + db_session: AsyncSession, + sample_data: tuple[Workspace, Peer], +): + """Test the peer cards endpoint with actual peer card data""" + test_workspace, observer_peer = sample_data + + # Create a second peer (the target/observed peer) + target_peer_name = str(generate_nanoid()) + response = client.post( + f"/v2/workspaces/{test_workspace.name}/peers", + json={"name": target_peer_name}, + ) + assert response.status_code == 200 + + # Set up peer cards using the database directly + # Set a self-card for the observer peer + self_card_content = ["I am a helpful AI assistant", "I enjoy learning about users"] + await crud.set_peer_card( + db_session, + test_workspace.name, + observer_peer.name, + observer_peer.name, + self_card_content, + ) + + # Set a card for the observer describing the target peer + target_card_content = ["This peer seems friendly", "They ask good questions"] + await crud.set_peer_card( + db_session, + test_workspace.name, + target_peer_name, + observer_peer.name, + target_card_content, + ) + + # Test getting observer's own card + response = client.get( + f"/v2/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card" + ) + assert response.status_code == 200 + data = response.json() + assert data["peer_card"] == self_card_content + + # Test getting card for target peer + response = client.get( + f"/v2/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", + params={"target": target_peer_name}, + ) + assert response.status_code == 200 + data = response.json() + assert data["peer_card"] == target_card_content