Resolve SDK Inconsistencies and Add Observation Creation Endpoints (#288)
* feat: Add Observation Creation Endpoints and SDK Cleanup * fix: Resolve linting errors * chore: Code Rabbit Nits * chore: Code Rabbit Nits
This commit is contained in:
parent
72eb0827ec
commit
ca702cfd10
|
|
@ -145,6 +145,7 @@
|
|||
{
|
||||
"group": "observations",
|
||||
"pages": [
|
||||
"v2/api-reference/endpoint/observations/create-observations",
|
||||
"v2/api-reference/endpoint/observations/list-observations",
|
||||
"v2/api-reference/endpoint/observations/query-observations",
|
||||
"v2/api-reference/endpoint/observations/delete-observation"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/observations
|
||||
---
|
||||
|
|
@ -111,7 +111,7 @@ response = alice.chat("What does the user know about weather?")
|
|||
response = alice.chat("What does the user know about the assistant?", target=assistant)
|
||||
|
||||
# Query scoped to a specific session
|
||||
response = alice.chat("What happened in our conversation?", session_id=session.id)
|
||||
response = alice.chat("What happened in our conversation?", session=session.id)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
@ -258,7 +258,7 @@ print(f"Workspace: {alice.workspace_id}")
|
|||
# Chat with peer's representations (supports streaming)
|
||||
response = alice.chat("What did I have for breakfast?")
|
||||
response = alice.chat("What do I know about Bob?", target="bob")
|
||||
response = alice.chat("What happened in session-1?", session_id="session-1")
|
||||
response = alice.chat("What happened in session-1?", session="session-1")
|
||||
|
||||
# Add content to a session with a peer
|
||||
session = honcho.session("session-1")
|
||||
|
|
@ -435,6 +435,59 @@ const bobSearch = await bobObs.query("work history");
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
#### Creating Observations Manually
|
||||
|
||||
You can also create observations directly, which is useful for importing data or adding explicit facts:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Create observations for what alice knows about bob
|
||||
bob_obs = alice.observations_of("bob")
|
||||
|
||||
# Create a single observation
|
||||
created = bob_obs.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session-1"}
|
||||
])
|
||||
|
||||
# Create multiple observations in batch
|
||||
created = bob_obs.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session-1"},
|
||||
{"content": "User works late at night", "session_id": "session-1"},
|
||||
{"content": "User enjoys programming", "session_id": "session-1"},
|
||||
])
|
||||
|
||||
# Returns list of created Observation objects with IDs
|
||||
for obs in created:
|
||||
print(f"Created observation: {obs.id} - {obs.content}")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Create observations for what alice knows about bob
|
||||
const bobObs = alice.observationsOf("bob");
|
||||
|
||||
// Create a single observation
|
||||
const created = await bobObs.create([
|
||||
{ content: "User prefers dark mode", sessionId: "session-1" }
|
||||
]);
|
||||
|
||||
// Create multiple observations in batch
|
||||
const batchCreated = await bobObs.create([
|
||||
{ content: "User prefers dark mode", sessionId: "session-1" },
|
||||
{ content: "User works late at night", sessionId: "session-1" },
|
||||
{ content: "User enjoys programming", sessionId: "session-1" },
|
||||
]);
|
||||
|
||||
// Returns array of created Observation objects with IDs
|
||||
for (const obs of batchCreated) {
|
||||
console.log(`Created observation: ${obs.id} - ${obs.content}`);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
Manually created observations are marked as "explicit" and are treated the same as system-derived observations. Each observation must be tied to a session and the content length is validated against the embedding token limit.
|
||||
</Info>
|
||||
|
||||
### Session
|
||||
|
||||
Manages multi-party conversations:
|
||||
|
|
@ -504,7 +557,7 @@ searched_rep = session.working_rep(
|
|||
# Upload a file to create messages
|
||||
messages = session.upload_file(
|
||||
file=open("document.pdf", "rb"),
|
||||
peer_id="user",
|
||||
peer="user",
|
||||
metadata={"source": "upload"},
|
||||
created_at="2024-01-15T10:30:00Z"
|
||||
)
|
||||
|
|
@ -729,7 +782,7 @@ group_chat.add_messages([
|
|||
|
||||
# Query different perspectives
|
||||
user_perspective = users[0].chat("What are people's concerns?")
|
||||
moderator_view = moderator.chat("What feedback am I getting?", session_id=group_chat.id)
|
||||
moderator_view = moderator.chat("What feedback am I getting?", session=group_chat.id)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
|
|||
|
|
@ -2475,6 +2475,61 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v2/workspaces/{workspace_id}/observations": {
|
||||
"post": {
|
||||
"tags": ["observations"],
|
||||
"summary": "Create Observations",
|
||||
"description": "Create one or more observations.\n\nCreates observations (theory-of-mind facts) for the specified observer/observed peer pairs.\nEach observation must reference existing peers and a session within the workspace.\nEmbeddings are automatically generated for semantic search.\n\nMaximum of 100 observations per request.",
|
||||
"operationId": "create_observations_v2_workspaces__workspace_id__observations_post",
|
||||
"security": [{ "HTTPBearer": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "ID of the workspace",
|
||||
"title": "Workspace Id"
|
||||
},
|
||||
"description": "ID of the workspace"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ObservationBatchCreate",
|
||||
"description": "Batch of observations to create"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/Observation" },
|
||||
"title": "Response Create Observations V2 Workspaces Workspace Id Observations Post"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/workspaces/{workspace_id}/observations/list": {
|
||||
"post": {
|
||||
"tags": ["observations"],
|
||||
|
|
@ -3334,6 +3389,50 @@
|
|||
"title": "Observation",
|
||||
"description": "Observation response - external view of a document"
|
||||
},
|
||||
"ObservationBatchCreate": {
|
||||
"properties": {
|
||||
"observations": {
|
||||
"items": { "$ref": "#/components/schemas/ObservationCreate" },
|
||||
"type": "array",
|
||||
"maxItems": 100,
|
||||
"minItems": 1,
|
||||
"title": "Observations"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["observations"],
|
||||
"title": "ObservationBatchCreate",
|
||||
"description": "Schema for batch observation creation with a max of 100 observations"
|
||||
},
|
||||
"ObservationCreate": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 1,
|
||||
"title": "Content"
|
||||
},
|
||||
"observer_id": {
|
||||
"type": "string",
|
||||
"title": "Observer Id",
|
||||
"description": "The peer making the observation"
|
||||
},
|
||||
"observed_id": {
|
||||
"type": "string",
|
||||
"title": "Observed Id",
|
||||
"description": "The peer being observed"
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string",
|
||||
"title": "Session Id",
|
||||
"description": "The session this observation relates to"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["content", "observer_id", "observed_id", "session_id"],
|
||||
"title": "ObservationCreate",
|
||||
"description": "Schema for creating a single observation"
|
||||
},
|
||||
"ObservationGet": {
|
||||
"properties": {
|
||||
"filters": {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ response = alice.chat("what did alice have for breakfast today?")
|
|||
print("response returned:", response)
|
||||
|
||||
# Chat with alice in the session
|
||||
response = alice.chat("what did alice have for breakfast today?", session_id=session.id)
|
||||
response = alice.chat("what did alice have for breakfast today?", session=session.id)
|
||||
print("response returned:", response)
|
||||
|
||||
# Chat with alice in the session with a target
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ session = honcho.session("file_upload_test_" + str(uuid.uuid4()))
|
|||
|
||||
# Upload the current file directly using a file object
|
||||
with open(__file__, "rb") as file:
|
||||
session.upload_file(file, peer_id="alice")
|
||||
session.upload_file(file, peer="alice")
|
||||
|
||||
# get the messages from the session
|
||||
# should contain the contents of this file!
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ async def get_summaries_async():
|
|||
async_client = AsyncHoncho(api_key=api_key)
|
||||
|
||||
# Get a session
|
||||
async_session = async_client.session("my-conversation-session")
|
||||
async_session = await async_client.session("my-conversation-session")
|
||||
|
||||
# Get summaries asynchronously
|
||||
summaries = await async_session.get_summaries()
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ print(
|
|||
"\n\n\033[1m asking bob what alice had for breakfast -- scoped to session 1 \033[0m"
|
||||
)
|
||||
response = bob.chat(
|
||||
"what did alice have for breakfast today?", target=alice, session_id=session.id
|
||||
"what did alice have for breakfast today?", target=alice, session=session.id
|
||||
)
|
||||
print("response:", response)
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ print(
|
|||
"\n\n\033[1m asking bob what alice had for breakfast -- scoped to session 2 \033[0m"
|
||||
)
|
||||
response = bob.chat(
|
||||
"what did alice have for breakfast today?", target=alice, session_id=session2.id
|
||||
"what did alice have for breakfast today?", target=alice, session=session2.id
|
||||
)
|
||||
print("response:", response)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ authors = [
|
|||
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
|
||||
]
|
||||
dependencies = [
|
||||
"honcho-core>=1.6.0",
|
||||
"honcho-core>=1.6.1",
|
||||
"httpx>=0.28.0, <1",
|
||||
"pydantic>=2.0.0, <3",
|
||||
"typing-extensions>=4.12.0; python_version < \"3.12\"",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from .async_client import (
|
|||
AsyncPeer,
|
||||
AsyncSession,
|
||||
)
|
||||
from .base import PeerBase, SessionBase
|
||||
from .client import Honcho
|
||||
from .observations import AsyncObservationScope, Observation, ObservationScope
|
||||
from .pagination import SyncPage
|
||||
|
|
@ -68,8 +69,10 @@ __all__ = [
|
|||
"Observation",
|
||||
"ObservationScope",
|
||||
"Peer",
|
||||
"PeerBase",
|
||||
"PeerContext",
|
||||
"Session",
|
||||
"SessionBase",
|
||||
"SessionContext",
|
||||
"SessionSummaries",
|
||||
"Summary",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from honcho_core.types.workspaces.session import Session as SessionCore
|
|||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..base import PeerBase, SessionBase
|
||||
from .pagination import AsyncPage
|
||||
from .peer import AsyncPeer
|
||||
from .session import AsyncSession
|
||||
|
|
@ -470,29 +471,50 @@ class AsyncHoncho(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
session: Optional session (ID string or Session object) to scope the status check
|
||||
"""
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
session_id=session_id,
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
@validate_call
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -507,9 +529,9 @@ class AsyncHoncho(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
session_id: Optional session ID to scope the status check
|
||||
observer: Optional observer (ID string or AsyncPeer object) to scope the status check
|
||||
sender: Optional sender (ID string or AsyncPeer object) to scope the status check
|
||||
session: Optional session (ID string or AsyncSession object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -523,9 +545,7 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = await self.get_deriver_status(
|
||||
observer_id, sender_id, session_id
|
||||
)
|
||||
status = await self.get_deriver_status(observer, sender, session)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
@ -578,10 +598,9 @@ class AsyncHoncho(BaseModel):
|
|||
metadata: dict[str, object] = Field(
|
||||
..., description="The metadata to update for the message"
|
||||
),
|
||||
session_id: str | None = Field(
|
||||
session: str | SessionBase | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
description="The ID of the session (required if message is a string ID)",
|
||||
description="The session (ID string or Session object) - required if message is a string ID",
|
||||
),
|
||||
) -> Message:
|
||||
"""
|
||||
|
|
@ -592,7 +611,7 @@ class AsyncHoncho(BaseModel):
|
|||
Args:
|
||||
message: Either a Message object or a message ID string
|
||||
metadata: The metadata to update for the message
|
||||
session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
session: The session (ID string or Session object) - required if message is a string ID, ignored if message is a Message object
|
||||
|
||||
Returns:
|
||||
The updated Message object
|
||||
|
|
@ -605,9 +624,9 @@ class AsyncHoncho(BaseModel):
|
|||
resolved_session_id = message.session_id
|
||||
else:
|
||||
message_id = message
|
||||
if not session_id:
|
||||
raise ValueError("session_id is required when message is a string ID")
|
||||
resolved_session_id = session_id
|
||||
if not session:
|
||||
raise ValueError("session is required when message is a string ID")
|
||||
resolved_session_id = session if isinstance(session, str) else session.id
|
||||
|
||||
return await self._client.workspaces.sessions.messages.update(
|
||||
message_id=message_id,
|
||||
|
|
|
|||
|
|
@ -14,18 +14,20 @@ 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 pydantic import ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..base import PeerBase, SessionBase
|
||||
from ..types import DialecticStreamResponse
|
||||
from .pagination import AsyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..observations import AsyncObservationScope
|
||||
from ..types import PeerContext, Representation
|
||||
from .session import AsyncSession
|
||||
|
||||
from .session import AsyncSession
|
||||
|
||||
|
||||
class AsyncPeer(BaseModel):
|
||||
class AsyncPeer(PeerBase):
|
||||
"""
|
||||
Represents a peer in the Honcho system with async operations.
|
||||
|
||||
|
|
@ -42,10 +44,6 @@ class AsyncPeer(BaseModel):
|
|||
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()
|
||||
|
|
@ -146,8 +144,8 @@ class AsyncPeer(BaseModel):
|
|||
query: str,
|
||||
*,
|
||||
stream: bool = False,
|
||||
target: str | AsyncPeer | None = None,
|
||||
session_id: str | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> str | DialecticStreamResponse | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
|
@ -161,14 +159,28 @@ class AsyncPeer(BaseModel):
|
|||
stream: Whether to stream the response
|
||||
target: Optional target peer for local representation query. If provided,
|
||||
queries what this peer knows about the target peer rather than
|
||||
querying the peer's global representation
|
||||
session_id: Optional session ID to scope the query to a specific session.
|
||||
If provided, only information from that session is considered
|
||||
querying the peer's global representation. Can be a peer ID string
|
||||
or an AsyncPeer object.
|
||||
session: Optional session to scope the query to. If provided, only
|
||||
information from that session is considered. Can be a session
|
||||
ID string or an AsyncSession object.
|
||||
|
||||
Returns:
|
||||
For non-streaming: Response string containing the answer, or None if no relevant information
|
||||
For streaming: DialecticStreamResponse object that can be iterated over and provides final response
|
||||
"""
|
||||
# Extract IDs from objects if needed
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
if stream:
|
||||
|
||||
async def stream_response() -> AsyncGenerator[str]:
|
||||
|
|
@ -180,8 +192,8 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=True,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
) as response:
|
||||
response.http_response.raise_for_status()
|
||||
async for line in response.iter_lines():
|
||||
|
|
@ -205,8 +217,8 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=stream,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
# "If the context provided doesn't help address the query, write absolutely NOTHING but "None""
|
||||
if response.content in ("", None, "None"):
|
||||
|
|
@ -452,7 +464,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
async def card(
|
||||
self,
|
||||
target: str | AsyncPeer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the peer card for this peer.
|
||||
|
|
@ -472,10 +484,15 @@ class AsyncPeer(BaseModel):
|
|||
if isinstance(target, str) and len(target.strip()) == 0:
|
||||
raise ValueError("target string cannot be empty")
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
response: PeerCardResponse = await self._client.workspaces.peers.card(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
target=target_id,
|
||||
)
|
||||
|
||||
if response.peer_card is None:
|
||||
|
|
@ -486,8 +503,8 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
async def working_rep(
|
||||
self,
|
||||
session: str | AsyncSession | None = None,
|
||||
target: str | AsyncPeer | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
|
|
@ -537,12 +554,17 @@ class AsyncPeer(BaseModel):
|
|||
else session.id
|
||||
)
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.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,
|
||||
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
|
||||
|
|
@ -564,7 +586,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
async def get_context(
|
||||
self,
|
||||
target: str | AsyncPeer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
|
|
@ -609,7 +631,11 @@ class AsyncPeer(BaseModel):
|
|||
"""
|
||||
from ..types import PeerContext as _PeerContext
|
||||
|
||||
target_id = str(target.id) if isinstance(target, AsyncPeer) else target
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
response = await self._client.workspaces.peers.get_context(
|
||||
peer_id=self.id,
|
||||
|
|
@ -655,7 +681,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
return _AsyncObservationScope(self._client, self.workspace_id, self.id, self.id)
|
||||
|
||||
def observations_of(self, target: str | AsyncPeer) -> "AsyncObservationScope":
|
||||
def observations_of(self, target: str | PeerBase) -> "AsyncObservationScope":
|
||||
"""
|
||||
Access observations this peer has made about another peer.
|
||||
|
||||
|
|
@ -685,7 +711,7 @@ class AsyncPeer(BaseModel):
|
|||
"""
|
||||
from ..observations import AsyncObservationScope as _AsyncObservationScope
|
||||
|
||||
target_id = target.id if isinstance(target, AsyncPeer) else target
|
||||
target_id = target.id if isinstance(target, PeerBase) else target
|
||||
return _AsyncObservationScope(
|
||||
self._client, self.workspace_id, self.id, target_id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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 ..base import PeerBase, SessionBase
|
||||
from ..session_context import SessionContext, SessionSummaries, Summary
|
||||
from ..utils import prepare_file_for_upload
|
||||
from .pagination import AsyncPage
|
||||
|
|
@ -37,7 +38,7 @@ class SessionPeerConfig(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class AsyncSession(BaseModel):
|
||||
class AsyncSession(SessionBase):
|
||||
"""
|
||||
Represents a session in Honcho with async operations.
|
||||
|
||||
|
|
@ -54,10 +55,6 @@ class AsyncSession(BaseModel):
|
|||
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()
|
||||
|
|
@ -154,12 +151,12 @@ class AsyncSession(BaseModel):
|
|||
async def add_peers(
|
||||
self,
|
||||
peers: str
|
||||
| AsyncPeer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[AsyncPeer, SessionPeerConfig]
|
||||
| list[AsyncPeer | str]
|
||||
| list[tuple[AsyncPeer | str, SessionPeerConfig]]
|
||||
| list[AsyncPeer | str | tuple[AsyncPeer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -204,12 +201,12 @@ class AsyncSession(BaseModel):
|
|||
async def set_peers(
|
||||
self,
|
||||
peers: str
|
||||
| AsyncPeer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[AsyncPeer, SessionPeerConfig]
|
||||
| list[AsyncPeer | str]
|
||||
| list[tuple[AsyncPeer | str, SessionPeerConfig]]
|
||||
| list[AsyncPeer | str | tuple[AsyncPeer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to set for the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -252,7 +249,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
async def remove_peers(
|
||||
self,
|
||||
peers: str | AsyncPeer | list[AsyncPeer | str] = Field(
|
||||
peers: str | PeerBase | list[PeerBase | str] = Field(
|
||||
..., description="Peers to remove from the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -302,15 +299,14 @@ class AsyncSession(BaseModel):
|
|||
for peer in peers_page.items
|
||||
]
|
||||
|
||||
async def get_peer_config(self, peer: str | AsyncPeer) -> SessionPeerConfig:
|
||||
async def get_peer_config(self, peer: str | PeerBase) -> SessionPeerConfig:
|
||||
"""
|
||||
Get the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import AsyncPeer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
peer_get_config_response = (
|
||||
await self._client.workspaces.sessions.peers.get_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
|
@ -321,15 +317,14 @@ class AsyncSession(BaseModel):
|
|||
)
|
||||
|
||||
async def set_peer_config(
|
||||
self, peer: str | AsyncPeer, config: SessionPeerConfig
|
||||
self, peer: str | PeerBase, config: SessionPeerConfig
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import AsyncPeer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
await self._client.workspaces.sessions.peers.set_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
observe_others=omit
|
||||
|
|
@ -785,14 +780,16 @@ class AsyncSession(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def upload_file(
|
||||
self,
|
||||
file: tuple[str, bytes, str] | tuple[str, Any, str] | Any = Field(
|
||||
...,
|
||||
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"),
|
||||
peer: str | PeerBase = Field(
|
||||
..., description="The peer creating the messages (ID string or Peer object)"
|
||||
),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with the messages",
|
||||
|
|
@ -821,7 +818,8 @@ class AsyncSession(BaseModel):
|
|||
- a file object (must have .name and .read())
|
||||
- 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
|
||||
peer: The peer who will be attributed as the creator of the messages.
|
||||
Can be a peer ID string or an AsyncPeer object.
|
||||
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.
|
||||
|
|
@ -838,6 +836,9 @@ class AsyncSession(BaseModel):
|
|||
# Prepare file for upload using shared utility
|
||||
filename, content_bytes, content_type = prepare_file_for_upload(file)
|
||||
|
||||
# Extract peer ID from AsyncPeer object if needed
|
||||
resolved_peer_id = peer if isinstance(peer, str) else peer.id
|
||||
|
||||
# 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:
|
||||
|
|
@ -856,7 +857,7 @@ class AsyncSession(BaseModel):
|
|||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
file=(filename, content_bytes, content_type),
|
||||
peer_id=peer_id,
|
||||
peer_id=resolved_peer_id,
|
||||
extra_body=extra_body_data if extra_body_data else None,
|
||||
)
|
||||
|
||||
|
|
@ -864,9 +865,9 @@ class AsyncSession(BaseModel):
|
|||
|
||||
async def working_rep(
|
||||
self,
|
||||
peer: str | AsyncPeer,
|
||||
peer: str | PeerBase,
|
||||
*,
|
||||
target: str | AsyncPeer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
|
|
@ -907,13 +908,19 @@ class AsyncSession(BaseModel):
|
|||
```
|
||||
"""
|
||||
from ..types import Representation as _Representation
|
||||
from .peer import AsyncPeer as _AsyncPeer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
data = await self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, _AsyncPeer) else peer,
|
||||
peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=str(target.id) if isinstance(target, _AsyncPeer) else target,
|
||||
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
|
||||
|
|
@ -926,27 +933,42 @@ class AsyncSession(BaseModel):
|
|||
)
|
||||
return _Representation.from_dict(data) # type: ignore
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or AsyncPeer object) to scope the status check
|
||||
sender: Optional sender (ID string or AsyncPeer object) to scope the status check
|
||||
"""
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -961,8 +983,8 @@ class AsyncSession(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
observer: Optional observer (ID string or AsyncPeer object) to scope the status check
|
||||
sender: Optional sender (ID string or AsyncPeer object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -976,7 +998,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = await self.get_deriver_status(observer_id, sender_id)
|
||||
status = await self.get_deriver_status(observer, sender)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
"""Base classes for Honcho SDK entities.
|
||||
|
||||
This module provides base classes that contain only the essential data fields
|
||||
shared by both sync and async variants of Peer and Session. These base classes
|
||||
can be imported anywhere without causing circular import issues, enabling
|
||||
type-safe method signatures like `str | PeerBase`.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PeerBase(BaseModel):
|
||||
"""Base class for Peer objects (sync and async variants).
|
||||
|
||||
This class contains only the essential data fields shared by both
|
||||
Peer and AsyncPeer. Use this type in method signatures to accept
|
||||
either a peer ID string or any Peer object.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this peer
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
"""
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
class SessionBase(BaseModel):
|
||||
"""Base class for Session objects (sync and async variants).
|
||||
|
||||
This class contains only the essential data fields shared by both
|
||||
Session and AsyncSession. Use this type in method signatures to accept
|
||||
either a session ID string or any Session object.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this session
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
"""
|
||||
|
||||
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"
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@ from honcho_core.types.workspaces.session import Session as SessionCore
|
|||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .base import PeerBase, SessionBase
|
||||
from .pagination import SyncPage
|
||||
from .peer import Peer
|
||||
from .session import Session
|
||||
|
|
@ -445,29 +446,50 @@ class Honcho(BaseModel):
|
|||
self.workspace_id, query=query, filters=filters, limit=limit
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
session: Optional session (ID string or Session object) to scope the status check
|
||||
"""
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
session_id=session_id,
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
@validate_call
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -482,9 +504,9 @@ class Honcho(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
session_id: Optional session ID to scope the status check
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
session: Optional session (ID string or Session object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -498,7 +520,7 @@ class Honcho(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = self.get_deriver_status(observer_id, sender_id, session_id)
|
||||
status = self.get_deriver_status(observer, sender, session)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
@ -551,10 +573,9 @@ class Honcho(BaseModel):
|
|||
metadata: dict[str, object] = Field(
|
||||
..., description="The metadata to update for the message"
|
||||
),
|
||||
session_id: str | None = Field(
|
||||
session: str | SessionBase | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
description="The ID of the session (required if message is a string ID)",
|
||||
description="The session (ID string or Session object) - required if message is a string ID",
|
||||
),
|
||||
) -> Message:
|
||||
"""
|
||||
|
|
@ -565,7 +586,7 @@ class Honcho(BaseModel):
|
|||
Args:
|
||||
message: Either a Message object or a message ID string
|
||||
metadata: The metadata to update for the message
|
||||
session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
session: The session (ID string or Session object) - required if message is a string ID, ignored if message is a Message object
|
||||
|
||||
Returns:
|
||||
The updated Message object
|
||||
|
|
@ -578,9 +599,9 @@ class Honcho(BaseModel):
|
|||
resolved_session_id = message.session_id
|
||||
else:
|
||||
message_id = message
|
||||
if not session_id:
|
||||
raise ValueError("session_id is required when message is a string ID")
|
||||
resolved_session_id = session_id
|
||||
if not session:
|
||||
raise ValueError("session is required when message is a string ID")
|
||||
resolved_session_id = session if isinstance(session, str) else session.id
|
||||
|
||||
return self._client.workspaces.sessions.messages.update(
|
||||
message_id=message_id,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,29 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from .base import SessionBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import Representation
|
||||
from .types import ObservationCreateParam, Representation
|
||||
|
||||
|
||||
def _convert_observation(item: Any) -> dict[str, Any]:
|
||||
"""Convert a core SDK Observations model to a dict for our Observation class."""
|
||||
if hasattr(item, "model_dump"):
|
||||
# Pydantic model - use model_dump()
|
||||
return item.model_dump() # type: ignore[no-any-return]
|
||||
elif isinstance(item, dict):
|
||||
return cast(dict[str, Any], item)
|
||||
else:
|
||||
# Fallback: access as object attributes
|
||||
return {
|
||||
"id": getattr(item, "id", ""),
|
||||
"content": getattr(item, "content", ""),
|
||||
"observer_id": getattr(item, "observer_id", ""),
|
||||
"observed_id": getattr(item, "observed_id", ""),
|
||||
"session_id": getattr(item, "session_id", ""),
|
||||
"created_at": str(getattr(item, "created_at", "")),
|
||||
}
|
||||
|
||||
|
||||
class Observation:
|
||||
|
|
@ -56,7 +77,7 @@ class Observation:
|
|||
observer_id=data.get("observer_id", ""),
|
||||
observed_id=data.get("observed_id", ""),
|
||||
session_id=data.get("session_id", ""),
|
||||
created_at=data.get("created_at", ""),
|
||||
created_at=str(data.get("created_at", "")),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
@ -70,7 +91,7 @@ class ObservationScope:
|
|||
"""
|
||||
Scoped access to observations for a specific observer/observed relationship.
|
||||
|
||||
This class provides convenient methods to list, query, and delete observations
|
||||
This class provides convenient methods to list, query, create, and delete observations
|
||||
that are automatically scoped to a specific observer/observed pair.
|
||||
|
||||
Typically accessed via `peer.observations` (for self-observations) or
|
||||
|
|
@ -87,13 +108,6 @@ class ObservationScope:
|
|||
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
|
||||
|
|
@ -126,7 +140,7 @@ class ObservationScope:
|
|||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
session_id: str | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
List observations in this scope.
|
||||
|
|
@ -134,19 +148,23 @@ class ObservationScope:
|
|||
Args:
|
||||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session_id: Optional session ID to filter by
|
||||
session: Optional session (ID string or Session object) to filter by
|
||||
|
||||
Returns:
|
||||
List of Observation objects
|
||||
"""
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
if session_id:
|
||||
filters["session_id"] = session_id
|
||||
if resolved_session_id:
|
||||
filters["session_id"] = resolved_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,
|
||||
|
|
@ -154,7 +172,11 @@ class ObservationScope:
|
|||
size=size,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response.items]
|
||||
# response.items is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response.items
|
||||
]
|
||||
|
||||
def query(
|
||||
self,
|
||||
|
|
@ -178,7 +200,6 @@ class ObservationScope:
|
|||
"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,
|
||||
|
|
@ -187,7 +208,11 @@ class ObservationScope:
|
|||
filters=filters,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response]
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
def delete(self, observation_id: str) -> None:
|
||||
"""
|
||||
|
|
@ -196,12 +221,67 @@ class ObservationScope:
|
|||
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 create(
|
||||
self,
|
||||
observations: "ObservationCreateParam | list[ObservationCreateParam]",
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Create observations in this scope.
|
||||
|
||||
Args:
|
||||
observations: Single observation or list of observations to create.
|
||||
Each observation must have 'content' and 'session_id' keys.
|
||||
|
||||
Returns:
|
||||
List of created Observation objects
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Create a single observation
|
||||
observations = peer.observations.create(
|
||||
{"content": "User prefers dark mode", "session_id": "session1"}
|
||||
)
|
||||
|
||||
# Create multiple observations
|
||||
observations = peer.observations.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session1"},
|
||||
{"content": "User is interested in AI", "session_id": "session1"},
|
||||
])
|
||||
```
|
||||
"""
|
||||
# Normalize to list
|
||||
if not isinstance(observations, list):
|
||||
observations = [observations]
|
||||
|
||||
# Build the request body with observer/observed from scope
|
||||
request_observations = [
|
||||
{
|
||||
"content": obs["content"],
|
||||
"session_id": obs["session_id"]
|
||||
if isinstance(obs["session_id"], str)
|
||||
else obs["session_id"].id,
|
||||
"observer_id": self.observer,
|
||||
"observed_id": self.observed,
|
||||
}
|
||||
for obs in observations
|
||||
]
|
||||
|
||||
response = self._client.workspaces.observations.create( # type: ignore[attr-defined]
|
||||
workspace_id=self.workspace_id,
|
||||
observations=request_observations,
|
||||
)
|
||||
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
def get_representation(
|
||||
self,
|
||||
search_query: str | None = None,
|
||||
|
|
@ -262,7 +342,7 @@ 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
|
||||
This class provides convenient async methods to list, query, create, and delete observations
|
||||
that are automatically scoped to a specific observer/observed pair.
|
||||
|
||||
Typically accessed via `peer.observations` (for self-observations) or
|
||||
|
|
@ -279,13 +359,6 @@ class AsyncObservationScope:
|
|||
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
|
||||
|
|
@ -318,7 +391,7 @@ class AsyncObservationScope:
|
|||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
session_id: str | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
List observations in this scope.
|
||||
|
|
@ -326,19 +399,23 @@ class AsyncObservationScope:
|
|||
Args:
|
||||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session_id: Optional session ID to filter by
|
||||
session: Optional session (ID string or AsyncSession object) to filter by
|
||||
|
||||
Returns:
|
||||
List of Observation objects
|
||||
"""
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
if session_id:
|
||||
filters["session_id"] = session_id
|
||||
if resolved_session_id:
|
||||
filters["session_id"] = resolved_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,
|
||||
|
|
@ -346,7 +423,11 @@ class AsyncObservationScope:
|
|||
size=size,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response.items]
|
||||
# response.items is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response.items
|
||||
]
|
||||
|
||||
async def query(
|
||||
self,
|
||||
|
|
@ -370,7 +451,6 @@ class AsyncObservationScope:
|
|||
"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,
|
||||
|
|
@ -379,7 +459,11 @@ class AsyncObservationScope:
|
|||
filters=filters,
|
||||
)
|
||||
|
||||
return [Observation.from_api_response(item) for item in response]
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
async def delete(self, observation_id: str) -> None:
|
||||
"""
|
||||
|
|
@ -388,12 +472,67 @@ class AsyncObservationScope:
|
|||
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 create(
|
||||
self,
|
||||
observations: "ObservationCreateParam | list[ObservationCreateParam]",
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Create observations in this scope.
|
||||
|
||||
Args:
|
||||
observations: Single observation or list of observations to create.
|
||||
Each observation must have 'content' and 'session_id' keys.
|
||||
|
||||
Returns:
|
||||
List of created Observation objects
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Create a single observation
|
||||
observations = await peer.observations.create(
|
||||
{"content": "User prefers dark mode", "session_id": "session1"}
|
||||
)
|
||||
|
||||
# Create multiple observations
|
||||
observations = await peer.observations.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session1"},
|
||||
{"content": "User is interested in AI", "session_id": "session1"},
|
||||
])
|
||||
```
|
||||
"""
|
||||
# Normalize to list
|
||||
if not isinstance(observations, list):
|
||||
observations = [observations]
|
||||
|
||||
# Build the request body with observer/observed from scope
|
||||
request_observations = [
|
||||
{
|
||||
"content": obs["content"],
|
||||
"session_id": obs["session_id"]
|
||||
if isinstance(obs["session_id"], str)
|
||||
else obs["session_id"].id,
|
||||
"observer_id": self.observer,
|
||||
"observed_id": self.observed,
|
||||
}
|
||||
for obs in observations
|
||||
]
|
||||
|
||||
response = await self._client.workspaces.observations.create( # type: ignore[attr-defined]
|
||||
workspace_id=self.workspace_id,
|
||||
observations=request_observations,
|
||||
)
|
||||
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
async def get_representation(
|
||||
self,
|
||||
search_query: str | None = None,
|
||||
|
|
|
|||
|
|
@ -11,18 +11,20 @@ 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 pydantic import ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .base import PeerBase, SessionBase
|
||||
from .pagination import SyncPage
|
||||
from .types import DialecticStreamResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .observations import ObservationScope
|
||||
from .session import Session
|
||||
from .types import PeerContext, Representation
|
||||
|
||||
from .session import Session
|
||||
|
||||
class Peer(BaseModel):
|
||||
|
||||
class Peer(PeerBase):
|
||||
"""
|
||||
Represents a peer in the Honcho system.
|
||||
|
||||
|
|
@ -39,10 +41,6 @@ class Peer(BaseModel):
|
|||
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()
|
||||
|
|
@ -120,8 +118,8 @@ class Peer(BaseModel):
|
|||
query: str,
|
||||
*,
|
||||
stream: bool = False,
|
||||
target: str | Peer | None = None,
|
||||
session_id: str | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> str | DialecticStreamResponse | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
|
@ -135,14 +133,28 @@ class Peer(BaseModel):
|
|||
stream: Whether to stream the response
|
||||
target: Optional target peer for local representation query. If provided,
|
||||
queries what this peer knows about the target peer rather than
|
||||
querying the peer's global representation
|
||||
session_id: Optional session ID to scope the query to a specific session.
|
||||
If provided, only information from that session is considered
|
||||
querying the peer's global representation. Can be a peer ID string
|
||||
or a Peer object.
|
||||
session: Optional session to scope the query to. If provided, only
|
||||
information from that session is considered. Can be a session
|
||||
ID string or a Session object.
|
||||
|
||||
Returns:
|
||||
For non-streaming: Response string containing the answer, or None if no relevant information
|
||||
For streaming: DialecticStreamResponse object that can be iterated over and provides final response
|
||||
"""
|
||||
# Extract IDs from objects if needed
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
if stream:
|
||||
|
||||
def stream_response() -> Generator[str, None, None]:
|
||||
|
|
@ -154,8 +166,8 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=True,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
) as response:
|
||||
response.http_response.raise_for_status()
|
||||
for line in response.iter_lines():
|
||||
|
|
@ -179,8 +191,8 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=stream,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
if response.content in ("", None, "None"):
|
||||
return None
|
||||
|
|
@ -425,7 +437,7 @@ class Peer(BaseModel):
|
|||
|
||||
def card(
|
||||
self,
|
||||
target: str | Peer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the peer card for this peer.
|
||||
|
|
@ -445,10 +457,15 @@ class Peer(BaseModel):
|
|||
if isinstance(target, str) and len(target.strip()) == 0:
|
||||
raise ValueError("target string cannot be empty")
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
response: PeerCardResponse = self._client.workspaces.peers.card(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
target=target_id,
|
||||
)
|
||||
if response.peer_card is None:
|
||||
return ""
|
||||
|
|
@ -459,8 +476,8 @@ class Peer(BaseModel):
|
|||
|
||||
def working_rep(
|
||||
self,
|
||||
session: str | Session | None = None,
|
||||
target: str | Peer | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
|
|
@ -510,11 +527,16 @@ class Peer(BaseModel):
|
|||
else session.id
|
||||
)
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.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,
|
||||
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
|
||||
|
|
@ -533,7 +555,7 @@ class Peer(BaseModel):
|
|||
|
||||
def get_context(
|
||||
self,
|
||||
target: str | Peer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
|
|
@ -578,7 +600,11 @@ class Peer(BaseModel):
|
|||
"""
|
||||
from .types import PeerContext as _PeerContext
|
||||
|
||||
target_id = str(target.id) if isinstance(target, Peer) else target
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
response = self._client.workspaces.peers.get_context(
|
||||
peer_id=self.id,
|
||||
|
|
@ -624,7 +650,7 @@ class Peer(BaseModel):
|
|||
|
||||
return _ObservationScope(self._client, self.workspace_id, self.id, self.id)
|
||||
|
||||
def observations_of(self, target: str | Peer) -> "ObservationScope":
|
||||
def observations_of(self, target: str | PeerBase) -> "ObservationScope":
|
||||
"""
|
||||
Access observations this peer has made about another peer.
|
||||
|
||||
|
|
@ -654,7 +680,7 @@ class Peer(BaseModel):
|
|||
"""
|
||||
from .observations import ObservationScope as _ObservationScope
|
||||
|
||||
target_id = target.id if isinstance(target, Peer) else target
|
||||
target_id = target.id if isinstance(target, PeerBase) else target
|
||||
return _ObservationScope(self._client, self.workspace_id, self.id, target_id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ 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 .base import PeerBase, SessionBase
|
||||
from .pagination import SyncPage
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
from .utils import prepare_file_for_upload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .peer import Peer
|
||||
from .types import Representation
|
||||
from .peer import Peer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ class SessionPeerConfig(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class Session(BaseModel):
|
||||
class Session(SessionBase):
|
||||
"""
|
||||
Represents a session in Honcho.
|
||||
|
||||
|
|
@ -53,10 +54,6 @@ class Session(BaseModel):
|
|||
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()
|
||||
|
|
@ -130,12 +127,12 @@ class Session(BaseModel):
|
|||
def add_peers(
|
||||
self,
|
||||
peers: str
|
||||
| Peer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[Peer, SessionPeerConfig]
|
||||
| list[Peer | str]
|
||||
| list[tuple[Peer | str, SessionPeerConfig]]
|
||||
| list[Peer | str | tuple[Peer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -180,12 +177,12 @@ class Session(BaseModel):
|
|||
def set_peers(
|
||||
self,
|
||||
peers: str
|
||||
| Peer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[Peer, SessionPeerConfig]
|
||||
| list[Peer | str]
|
||||
| list[tuple[Peer | str, SessionPeerConfig]]
|
||||
| list[Peer | str | tuple[Peer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to set for the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -228,7 +225,7 @@ class Session(BaseModel):
|
|||
|
||||
def remove_peers(
|
||||
self,
|
||||
peers: str | Peer | list[Peer | str] = Field(
|
||||
peers: str | PeerBase | list[PeerBase | str] = Field(
|
||||
..., description="Peers to remove from the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -277,14 +274,13 @@ class Session(BaseModel):
|
|||
Peer(peer.id, self.workspace_id, self._client) for peer in peers_page.items
|
||||
]
|
||||
|
||||
def get_peer_config(self, peer: str | Peer) -> SessionPeerConfig:
|
||||
def get_peer_config(self, peer: str | PeerBase) -> SessionPeerConfig:
|
||||
"""
|
||||
Get the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import Peer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
peer_get_config_response = self._client.workspaces.sessions.peers.get_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
|
@ -293,14 +289,13 @@ class Session(BaseModel):
|
|||
observe_me=peer_get_config_response.observe_me,
|
||||
)
|
||||
|
||||
def set_peer_config(self, peer: str | Peer, config: SessionPeerConfig) -> None:
|
||||
def set_peer_config(self, peer: str | PeerBase, config: SessionPeerConfig) -> None:
|
||||
"""
|
||||
Set the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import Peer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
self._client.workspaces.sessions.peers.set_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
observe_others=omit
|
||||
|
|
@ -756,14 +751,16 @@ class Session(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def upload_file(
|
||||
self,
|
||||
file: tuple[str, bytes, str] | tuple[str, Any, str] | Any = Field(
|
||||
...,
|
||||
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"),
|
||||
peer: str | PeerBase = Field(
|
||||
..., description="The peer creating the messages (ID string or Peer object)"
|
||||
),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with the messages",
|
||||
|
|
@ -792,7 +789,8 @@ class Session(BaseModel):
|
|||
- a file object (must have .name and .read())
|
||||
- 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
|
||||
peer: The peer who will be attributed as the creator of the messages.
|
||||
Can be a peer ID string or a Peer object.
|
||||
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.
|
||||
|
|
@ -809,6 +807,9 @@ class Session(BaseModel):
|
|||
# Prepare file for upload using shared utility
|
||||
filename, content_bytes, content_type = prepare_file_for_upload(file)
|
||||
|
||||
# Extract peer ID from Peer object if needed
|
||||
resolved_peer_id = peer if isinstance(peer, str) else peer.id
|
||||
|
||||
# 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:
|
||||
|
|
@ -827,7 +828,7 @@ class Session(BaseModel):
|
|||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
file=(filename, content_bytes, content_type),
|
||||
peer_id=peer_id,
|
||||
peer_id=resolved_peer_id,
|
||||
extra_body=extra_body_data if extra_body_data else None,
|
||||
)
|
||||
|
||||
|
|
@ -835,9 +836,9 @@ class Session(BaseModel):
|
|||
|
||||
def working_rep(
|
||||
self,
|
||||
peer: str | Peer,
|
||||
peer: str | PeerBase,
|
||||
*,
|
||||
target: str | Peer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
|
|
@ -877,14 +878,20 @@ class Session(BaseModel):
|
|||
)
|
||||
```
|
||||
"""
|
||||
from .peer import Peer as _Peer
|
||||
from .types import Representation as _Representation
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
data = self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, _Peer) else peer,
|
||||
peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=str(target.id) if isinstance(target, _Peer) else target,
|
||||
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
|
||||
|
|
@ -897,27 +904,42 @@ class Session(BaseModel):
|
|||
)
|
||||
return _Representation.from_dict(data) # type: ignore
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
"""
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -932,8 +954,8 @@ class Session(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -947,7 +969,7 @@ class Session(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = self.get_deriver_status(observer_id, sender_id)
|
||||
status = self.get_deriver_status(observer, sender)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from datetime import datetime
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Re-export observation types from dedicated module
|
||||
from .observations import AsyncObservationScope, Observation, ObservationScope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import SessionBase
|
||||
|
||||
__all__ = [
|
||||
"AsyncObservationScope",
|
||||
"DeductiveObservation",
|
||||
|
|
@ -19,6 +23,7 @@ __all__ = [
|
|||
"ExplicitObservation",
|
||||
"ExplicitObservationBase",
|
||||
"Observation",
|
||||
"ObservationCreateParam",
|
||||
"ObservationMetadata",
|
||||
"ObservationScope",
|
||||
"PeerContext",
|
||||
|
|
@ -26,6 +31,18 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class ObservationCreateParam(TypedDict, total=False):
|
||||
"""Parameters for creating an observation.
|
||||
|
||||
Attributes:
|
||||
content: The observation content/text (required)
|
||||
session_id: The session this observation relates to (ID string or Session object) (required)
|
||||
"""
|
||||
|
||||
content: Required[str]
|
||||
session_id: "Required[str | SessionBase]"
|
||||
|
||||
|
||||
class ObservationMetadata(BaseModel):
|
||||
"""Metadata associated with an observation."""
|
||||
|
||||
|
|
|
|||
|
|
@ -441,9 +441,9 @@ describe('Honcho Client', () => {
|
|||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus);
|
||||
|
||||
const status = await honcho.getDeriverStatus({
|
||||
observerId: 'observer1',
|
||||
senderId: 'sender1',
|
||||
sessionId: 'session1',
|
||||
observer: 'observer1',
|
||||
sender: 'sender1',
|
||||
session: 'session1',
|
||||
});
|
||||
|
||||
expect(status).toEqual({
|
||||
|
|
@ -560,7 +560,7 @@ describe('Honcho Client', () => {
|
|||
const metadata = { updated: true };
|
||||
|
||||
await expect(honcho.updateMessage(messageId, metadata)).rejects.toThrow(
|
||||
'sessionId is required when message is a string ID'
|
||||
'session is required when message is a string ID'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ describe('Peer', () => {
|
|||
const mockResponse = { content: 'Session-specific response' };
|
||||
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
|
||||
await peer.chat('Hello', { sessionId: 'session-123' });
|
||||
await peer.chat('Hello', { session: 'session-123' });
|
||||
|
||||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
|
|
|
|||
|
|
@ -1160,8 +1160,8 @@ describe('Session', () => {
|
|||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus)
|
||||
|
||||
const status = await session.getDeriverStatus({
|
||||
observerId: 'observer1',
|
||||
senderId: 'sender1',
|
||||
observer: 'observer1',
|
||||
sender: 'sender1',
|
||||
})
|
||||
|
||||
expect(status).toEqual({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.6.0",
|
||||
"@honcho-ai/core": "^1.6.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="],
|
||||
|
||||
"@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=="],
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.6.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-sfKIqAIybP/yj6iXGQLFgrqlX1dA7OuAK86p8sy8XiT64ZpEYpEz6viifAsm65LRZMgb0HPTFeBGodseUSoqVQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.6.0",
|
||||
"@honcho-ai/core": "^1.6.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { Peer } from './peer'
|
|||
import { Session } from './session'
|
||||
import {
|
||||
type DeriverStatusOptions,
|
||||
DeriverStatusOptionsSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
type HonchoConfig,
|
||||
|
|
@ -486,28 +485,47 @@ export class Honcho {
|
|||
* The deriver is responsible for processing messages and updating peer representations.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.sessionId - Optional session ID to scope the status to
|
||||
* @param options.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @param options.session - Optional session (ID string or Session object) to scope the status to
|
||||
* @returns Promise resolving to the deriver status information including work unit counts
|
||||
*/
|
||||
async getDeriverStatus(options?: DeriverStatusOptions): Promise<{
|
||||
async getDeriverStatus(
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'observerId' | 'senderId' | 'sessionId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
session?: string | Session
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
const resolvedObserverId = options?.observer
|
||||
? typeof options.observer === 'string'
|
||||
? options.observer
|
||||
: options.observer.id
|
||||
: undefined
|
||||
const resolvedSenderId = options?.sender
|
||||
? typeof options.sender === 'string'
|
||||
? options.sender
|
||||
: options.sender.id
|
||||
: undefined
|
||||
const resolvedSessionId = options?.session
|
||||
? typeof options.session === 'string'
|
||||
? options.session
|
||||
: options.session.id
|
||||
: undefined
|
||||
|
||||
const queryParams: WorkspaceDeriverStatusParams = {}
|
||||
if (validatedOptions?.observerId)
|
||||
queryParams.observer_id = validatedOptions.observerId
|
||||
if (validatedOptions?.senderId)
|
||||
queryParams.sender_id = validatedOptions.senderId
|
||||
if (validatedOptions?.sessionId)
|
||||
queryParams.session_id = validatedOptions.sessionId
|
||||
if (resolvedObserverId) queryParams.observer_id = resolvedObserverId
|
||||
if (resolvedSenderId) queryParams.sender_id = resolvedSenderId
|
||||
if (resolvedSessionId) queryParams.session_id = resolvedSessionId
|
||||
|
||||
const status = await this._client.workspaces.deriverStatus(
|
||||
this.workspaceId,
|
||||
|
|
@ -531,28 +549,34 @@ export class Honcho {
|
|||
* The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.sessionId - Optional session ID to scope the status to
|
||||
* @param options.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @param options.session - Optional session (ID string or Session object) to scope the status to
|
||||
* @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
|
||||
* @returns Promise resolving to the final deriver status when processing is complete
|
||||
* @throws Error if timeout is exceeded before processing completes
|
||||
*/
|
||||
async pollDeriverStatus(options?: DeriverStatusOptions): Promise<{
|
||||
async pollDeriverStatus(
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'observerId' | 'senderId' | 'sessionId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
session?: string | Session
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
const status = await this.getDeriverStatus(validatedOptions)
|
||||
const status = await this.getDeriverStatus(options)
|
||||
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
|
||||
return status
|
||||
}
|
||||
|
|
@ -589,14 +613,14 @@ export class Honcho {
|
|||
*
|
||||
* @param message - Either a Message object or a message ID string
|
||||
* @param metadata - The metadata to update for the message
|
||||
* @param sessionId - The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
* @param session - The session (ID string or Session object) - required if message is a string ID, ignored if message is a Message object
|
||||
* @returns Promise resolving to the updated Message object
|
||||
* @throws Error if message is a string ID but sessionId is not provided
|
||||
* @throws Error if message is a string ID but session is not provided
|
||||
*/
|
||||
async updateMessage(
|
||||
message: Message | string,
|
||||
metadata: Record<string, unknown>,
|
||||
sessionId?: string
|
||||
session?: string | Session
|
||||
): Promise<Message> {
|
||||
const validatedMetadata = MessageMetadataSchema.parse(metadata)
|
||||
let messageId: string
|
||||
|
|
@ -604,10 +628,10 @@ export class Honcho {
|
|||
|
||||
if (typeof message === 'string') {
|
||||
messageId = message
|
||||
if (!sessionId) {
|
||||
throw new Error('sessionId is required when message is a string ID')
|
||||
if (!session) {
|
||||
throw new Error('session is required when message is a string ID')
|
||||
}
|
||||
resolvedSessionId = sessionId
|
||||
resolvedSessionId = typeof session === 'string' ? session : session.id
|
||||
} else {
|
||||
messageId = message.id
|
||||
resolvedSessionId = message.session_id
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import {
|
|||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
import type { Session } from './session'
|
||||
import type { ObservationCreateParam } from './types'
|
||||
|
||||
// Re-export for consumers who import from this module
|
||||
export type { RepresentationOptions }
|
||||
export type { RepresentationOptions, ObservationCreateParam }
|
||||
|
||||
/**
|
||||
* An observation from the theory-of-mind system.
|
||||
|
|
@ -161,20 +163,25 @@ export class ObservationScope {
|
|||
*
|
||||
* @param page - Page number (1-indexed)
|
||||
* @param size - Number of results per page
|
||||
* @param sessionId - Optional session ID to filter by
|
||||
* @param session - Optional session (ID string or Session object) to filter by
|
||||
* @returns Promise resolving to list of Observation objects
|
||||
*/
|
||||
async list(
|
||||
page: number = 1,
|
||||
size: number = 50,
|
||||
sessionId?: string
|
||||
session?: string | Session
|
||||
): Promise<Observation[]> {
|
||||
const resolvedSessionId = session
|
||||
? typeof session === 'string'
|
||||
? session
|
||||
: session.id
|
||||
: undefined
|
||||
const filters: Record<string, unknown> = {
|
||||
observer: this.observer,
|
||||
observed: this.observed,
|
||||
}
|
||||
if (sessionId) {
|
||||
filters.session_id = sessionId
|
||||
if (resolvedSessionId) {
|
||||
filters.session_id = resolvedSessionId
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
|
|
@ -239,6 +246,54 @@ export class ObservationScope {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create observations in this scope.
|
||||
*
|
||||
* @param observations - Single observation or array of observations with content and sessionId
|
||||
* @returns Promise resolving to list of created Observation objects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create a single observation
|
||||
* const observations = await peer.observations.create(
|
||||
* { content: 'User prefers dark mode', sessionId: 'session1' }
|
||||
* )
|
||||
*
|
||||
* // Create multiple observations
|
||||
* const observations = await peer.observations.create([
|
||||
* { content: 'User prefers dark mode', sessionId: 'session1' },
|
||||
* { content: 'User is interested in AI', sessionId: 'session1' },
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
async create(
|
||||
observations: ObservationCreateParam | ObservationCreateParam[]
|
||||
): Promise<Observation[]> {
|
||||
// Normalize to array
|
||||
const observationArray = Array.isArray(observations)
|
||||
? observations
|
||||
: [observations]
|
||||
|
||||
// Build the request body with observer/observed from scope
|
||||
const requestObservations = observationArray.map((obs) => ({
|
||||
content: obs.content,
|
||||
session_id:
|
||||
typeof obs.sessionId === 'string' ? obs.sessionId : obs.sessionId.id,
|
||||
observer_id: this.observer,
|
||||
observed_id: this.observed,
|
||||
}))
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
const response = await (this._client.workspaces as any).observations.create(
|
||||
this.workspaceId,
|
||||
{ observations: requestObservations }
|
||||
)
|
||||
|
||||
return (response ?? []).map((item: unknown) =>
|
||||
Observation.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computed representation for this scope.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -106,9 +106,11 @@ export class Peer {
|
|||
* @param stream - Whether to stream the response
|
||||
* @param target - Optional target peer for local representation query. If provided,
|
||||
* queries what this peer knows about the target peer rather than
|
||||
* querying the peer's global representation
|
||||
* @param sessionId - Optional session ID to scope the query to a specific session.
|
||||
* If provided, only information from that session is considered
|
||||
* querying the peer's global representation. Can be a peer ID string
|
||||
* or a Peer object.
|
||||
* @param session - Optional session to scope the query to. If provided, only
|
||||
* information from that session is considered. Can be a session
|
||||
* ID string or a Session object.
|
||||
* @returns Promise resolving to:
|
||||
* - For non-streaming: response string or null if no relevant information
|
||||
* - For streaming: DialecticStreamResponse that can be iterated over
|
||||
|
|
@ -118,26 +120,33 @@ export class Peer {
|
|||
options?: {
|
||||
stream?: boolean
|
||||
target?: string | Peer
|
||||
sessionId?: string
|
||||
session?: string | Session
|
||||
}
|
||||
): Promise<string | DialecticStreamResponse | null> {
|
||||
const targetId = options?.target
|
||||
? typeof options.target === 'string'
|
||||
? options.target
|
||||
: options.target.id
|
||||
: undefined
|
||||
const resolvedSessionId = options?.session
|
||||
? typeof options.session === 'string'
|
||||
? options.session
|
||||
: options.session.id
|
||||
: undefined
|
||||
|
||||
const chatParams = ChatQuerySchema.parse({
|
||||
query,
|
||||
stream: options?.stream,
|
||||
target: options?.target,
|
||||
sessionId: options?.sessionId,
|
||||
target: targetId,
|
||||
session: resolvedSessionId,
|
||||
})
|
||||
|
||||
if (chatParams.stream) {
|
||||
const body = {
|
||||
query: chatParams.query,
|
||||
stream: true,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
: chatParams.target.id
|
||||
: undefined,
|
||||
session_id: chatParams.sessionId,
|
||||
target: chatParams.target,
|
||||
session_id: chatParams.session,
|
||||
}
|
||||
|
||||
const url = `${this._client.baseURL}/v2/workspaces/${this.workspaceId}/peers/${this.id}/chat`
|
||||
|
|
@ -212,12 +221,8 @@ export class Peer {
|
|||
{
|
||||
query: chatParams.query,
|
||||
stream: false,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
: chatParams.target.id
|
||||
: undefined,
|
||||
session_id: chatParams.sessionId,
|
||||
target: chatParams.target,
|
||||
session_id: chatParams.session,
|
||||
}
|
||||
)
|
||||
if (!response.content || response.content === 'None') {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { SessionContext, SessionSummaries, Summary } from './session_context'
|
|||
import {
|
||||
ContextParamsSchema,
|
||||
type DeriverStatusOptions,
|
||||
DeriverStatusOptionsSchema,
|
||||
FileUploadSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
|
|
@ -896,12 +895,18 @@ export class Session {
|
|||
* This method automatically scopes the status to this session.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @returns Promise resolving to the deriver status information including work unit counts
|
||||
*/
|
||||
async getDeriverStatus(
|
||||
options?: Omit<DeriverStatusOptions, 'sessionId'>
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'sessionId' | 'observerId' | 'senderId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
|
|
@ -909,16 +914,22 @@ export class Session {
|
|||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
const resolvedObserverId = options?.observer
|
||||
? typeof options.observer === 'string'
|
||||
? options.observer
|
||||
: options.observer.id
|
||||
: undefined
|
||||
const resolvedSenderId = options?.sender
|
||||
? typeof options.sender === 'string'
|
||||
? options.sender
|
||||
: options.sender.id
|
||||
: undefined
|
||||
|
||||
const queryParams: WorkspaceDeriverStatusParams = {
|
||||
session_id: this.id, // Always use this session's ID
|
||||
}
|
||||
if (validatedOptions?.observerId)
|
||||
queryParams.observer_id = validatedOptions.observerId
|
||||
if (validatedOptions?.senderId)
|
||||
queryParams.sender_id = validatedOptions.senderId
|
||||
if (resolvedObserverId) queryParams.observer_id = resolvedObserverId
|
||||
if (resolvedSenderId) queryParams.sender_id = resolvedSenderId
|
||||
|
||||
const status = await this._client.workspaces.deriverStatus(
|
||||
this.workspaceId,
|
||||
|
|
@ -942,14 +953,20 @@ export class Session {
|
|||
* The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
|
||||
* @returns Promise resolving to the final deriver status when processing is complete
|
||||
* @throws Error if timeout is exceeded before processing completes
|
||||
*/
|
||||
async pollDeriverStatus(
|
||||
options?: Omit<DeriverStatusOptions, 'sessionId'>
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'sessionId' | 'observerId' | 'senderId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
|
|
@ -957,14 +974,11 @@ export class Session {
|
|||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
const status = await this.getDeriverStatus(validatedOptions)
|
||||
const status = await this.getDeriverStatus(options)
|
||||
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
|
||||
return status
|
||||
}
|
||||
|
|
@ -1006,7 +1020,7 @@ export class Session {
|
|||
* - File objects (browser File API)
|
||||
* - 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 peer - The peer (ID string or Peer object) 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
|
||||
|
|
@ -1032,7 +1046,7 @@ export class Session {
|
|||
*/
|
||||
async uploadFile(
|
||||
file: Uploadable,
|
||||
peerId: string,
|
||||
peer: string | Peer,
|
||||
options?: {
|
||||
metadata?: Record<string, unknown>
|
||||
configuration?: Record<string, unknown>
|
||||
|
|
@ -1044,9 +1058,11 @@ export class Session {
|
|||
? options.created_at.toISOString()
|
||||
: options?.created_at
|
||||
|
||||
const resolvedPeerId = typeof peer === 'string' ? peer : peer.id
|
||||
|
||||
const uploadParams = FileUploadSchema.parse({
|
||||
file,
|
||||
peerId,
|
||||
peer: resolvedPeerId,
|
||||
metadata: options?.metadata,
|
||||
configuration: options?.configuration,
|
||||
created_at: createdAt,
|
||||
|
|
@ -1055,7 +1071,7 @@ export class Session {
|
|||
// Build body with file and peer_id, plus optional fields as JSON strings
|
||||
const body = {
|
||||
file: uploadParams.file,
|
||||
peer_id: uploadParams.peerId,
|
||||
peer_id: resolvedPeerId,
|
||||
...(uploadParams.metadata !== undefined && uploadParams.metadata !== null
|
||||
? { metadata: JSON.stringify(uploadParams.metadata) }
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { Session } from './session'
|
||||
|
||||
/**
|
||||
* Shared types for the Honcho TypeScript SDK.
|
||||
*/
|
||||
|
|
@ -14,6 +16,16 @@ export interface Observation {
|
|||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for creating an observation.
|
||||
*/
|
||||
export interface ObservationCreateParam {
|
||||
/** The observation content/text */
|
||||
content: string
|
||||
/** The session this observation relates to (ID string or Session object) */
|
||||
sessionId: string | Session
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for semantic search of observations.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -130,8 +130,18 @@ export const FilterSchema = z.record(z.string(), z.unknown()).optional()
|
|||
export const ChatQuerySchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
stream: z.boolean().optional().default(false),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
sessionId: z.string().optional(),
|
||||
target: z
|
||||
.union([z.string(), z.object({ id: z.string() })])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
session: z
|
||||
.union([z.string(), z.object({ id: z.string() })])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -215,9 +225,9 @@ export const ContextParamsSchema = z
|
|||
* Schema for deriver status options.
|
||||
*/
|
||||
export const DeriverStatusOptionsSchema = z.object({
|
||||
observerId: z.string().optional(),
|
||||
senderId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
observer: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
sender: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
session: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
timeoutMs: z
|
||||
.number()
|
||||
.positive('Timeout must be a positive number')
|
||||
|
|
@ -252,7 +262,7 @@ export const FileUploadSchema = z.object({
|
|||
'File must not be null or undefined'
|
||||
),
|
||||
]),
|
||||
peerId: PeerIdSchema,
|
||||
peer: z.union([PeerIdSchema, z.object({ id: z.string() })]),
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: z.record(z.string(), z.unknown()).optional(),
|
||||
created_at: z.string().nullable().optional(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from .collection import get_collection, get_or_create_collection
|
|||
from .deriver import get_deriver_status
|
||||
from .document import (
|
||||
create_documents,
|
||||
create_observations,
|
||||
delete_document,
|
||||
delete_document_by_id,
|
||||
get_all_documents,
|
||||
|
|
@ -62,6 +63,7 @@ __all__ = [
|
|||
"get_deriver_status",
|
||||
# Document
|
||||
"create_documents",
|
||||
"create_observations",
|
||||
"get_all_documents",
|
||||
"get_documents_with_filters",
|
||||
"query_documents",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ from sqlalchemy.sql import Select
|
|||
|
||||
from src import models, schemas
|
||||
from src.config import settings
|
||||
from src.crud.collection import get_or_create_collection
|
||||
from src.crud.peer import get_peer
|
||||
from src.crud.session import get_session
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import ResourceNotFoundException, ValidationException
|
||||
from src.utils.filter import apply_filter
|
||||
|
|
@ -294,6 +297,101 @@ async def delete_document_by_id(
|
|||
)
|
||||
|
||||
|
||||
async def create_observations(
|
||||
db: AsyncSession,
|
||||
observations: list[schemas.ObservationCreate],
|
||||
workspace_name: str,
|
||||
) -> list[models.Document]:
|
||||
"""
|
||||
Create multiple observations (documents) from user input.
|
||||
|
||||
This function validates all referenced resources, generates embeddings
|
||||
in batch, and creates the documents.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
observations: List of observation creation schemas
|
||||
workspace_name: Name of the workspace
|
||||
|
||||
Returns:
|
||||
List of created Document objects
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If any session or peer is not found
|
||||
ValidationException: If embedding generation fails or integrity constraint is violated
|
||||
"""
|
||||
if not observations:
|
||||
return []
|
||||
|
||||
# Collect unique sessions and peer pairs to validate
|
||||
sessions_to_validate: set[str] = set()
|
||||
peers_to_validate: set[str] = set()
|
||||
collection_pairs: set[tuple[str, str]] = set()
|
||||
|
||||
for obs in observations:
|
||||
sessions_to_validate.add(obs.session_id)
|
||||
peers_to_validate.add(obs.observer_id)
|
||||
peers_to_validate.add(obs.observed_id)
|
||||
collection_pairs.add((obs.observer_id, obs.observed_id))
|
||||
|
||||
# Validate all sessions exist
|
||||
for session_name in sessions_to_validate:
|
||||
await get_session(db, session_name, workspace_name)
|
||||
|
||||
# Validate all peers exist
|
||||
for peer_name in peers_to_validate:
|
||||
await get_peer(db, workspace_name, schemas.PeerCreate(name=peer_name))
|
||||
|
||||
# Get or create all collections
|
||||
for observer, observed in collection_pairs:
|
||||
await get_or_create_collection(
|
||||
db, workspace_name, observer=observer, observed=observed
|
||||
)
|
||||
|
||||
# Generate embeddings in batch
|
||||
contents = [obs.content for obs in observations]
|
||||
try:
|
||||
embeddings = await embedding_client.simple_batch_embed(contents)
|
||||
except ValueError as e:
|
||||
raise ValidationException(str(e)) from e
|
||||
|
||||
# Create document objects
|
||||
honcho_documents: list[models.Document] = []
|
||||
for obs, embedding in zip(observations, embeddings, strict=True):
|
||||
honcho_documents.append(
|
||||
models.Document(
|
||||
workspace_name=workspace_name,
|
||||
observer=obs.observer_id,
|
||||
observed=obs.observed_id,
|
||||
content=obs.content,
|
||||
level="explicit", # Manually created observations are always explicit
|
||||
times_derived=1,
|
||||
internal_metadata={}, # No message_ids since not derived from messages
|
||||
embedding=embedding,
|
||||
session_name=obs.session_id,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
db.add_all(honcho_documents)
|
||||
await db.commit()
|
||||
# Refresh all documents to get generated IDs and timestamps
|
||||
for doc in honcho_documents:
|
||||
await db.refresh(doc)
|
||||
except IntegrityError as e:
|
||||
await db.rollback()
|
||||
raise ValidationException(
|
||||
"Failed to create observations due to integrity constraint violation"
|
||||
) from e
|
||||
|
||||
logger.debug(
|
||||
"Created %d observations in workspace %s",
|
||||
len(honcho_documents),
|
||||
workspace_name,
|
||||
)
|
||||
return honcho_documents
|
||||
|
||||
|
||||
async def is_rejected_duplicate(
|
||||
db: AsyncSession,
|
||||
doc: schemas.DocumentCreate,
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ async def get_peer(
|
|||
peer: Peer creation schema
|
||||
|
||||
Returns:
|
||||
The peer if found or created
|
||||
The peer if found
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the peer does not exist
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class _EmbeddingClient:
|
|||
self.max_embedding_tokens = settings.MAX_EMBEDDING_TOKENS
|
||||
self.max_batch_size = 2048 # OpenAI batch limit
|
||||
|
||||
self.encoding: tiktoken.Encoding = tiktoken.get_encoding("cl100k_base")
|
||||
self.encoding: tiktoken.Encoding = tiktoken.get_encoding("o200k_base")
|
||||
self.max_embedding_tokens_per_request: int = (
|
||||
settings.MAX_EMBEDDING_TOKENS_PER_REQUEST
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,40 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=list[schemas.Observation],
|
||||
)
|
||||
async def create_observations(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
body: schemas.ObservationBatchCreate = Body(
|
||||
..., description="Batch of observations to create"
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
) -> list[schemas.Observation]:
|
||||
"""
|
||||
Create one or more observations.
|
||||
|
||||
Creates observations (theory-of-mind facts) for the specified observer/observed peer pairs.
|
||||
Each observation must reference existing peers and a session within the workspace.
|
||||
Embeddings are automatically generated for semantic search.
|
||||
|
||||
Maximum of 100 observations per request.
|
||||
"""
|
||||
documents = await crud.create_observations(
|
||||
db,
|
||||
observations=body.observations,
|
||||
workspace_name=workspace_id,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Created %d observations in workspace %s",
|
||||
len(documents),
|
||||
workspace_id,
|
||||
)
|
||||
return [schemas.Observation.model_validate(doc) for doc in documents]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/list",
|
||||
response_model=Page[schemas.Observation],
|
||||
|
|
|
|||
|
|
@ -562,6 +562,37 @@ class ObservationQuery(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class ObservationCreate(BaseModel):
|
||||
"""Schema for creating a single observation"""
|
||||
|
||||
content: Annotated[str, Field(min_length=1, max_length=65535)]
|
||||
observer_id: str = Field(..., description="The peer making the observation")
|
||||
observed_id: str = Field(..., description="The peer being observed")
|
||||
session_id: str = Field(..., description="The session this observation relates to")
|
||||
|
||||
_token_count: int = PrivateAttr(default=0)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_token_count(self) -> Self:
|
||||
"""Validate that content doesn't exceed embedding token limit."""
|
||||
encoding = tiktoken.get_encoding("cl100k_base")
|
||||
tokens = encoding.encode(self.content)
|
||||
self._token_count = len(tokens)
|
||||
|
||||
if self._token_count > settings.MAX_EMBEDDING_TOKENS:
|
||||
raise ValueError(
|
||||
f"Content exceeds maximum embedding token limit of {settings.MAX_EMBEDDING_TOKENS} "
|
||||
+ f"(got {self._token_count} tokens)"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class ObservationBatchCreate(BaseModel):
|
||||
"""Schema for batch observation creation with a max of 100 observations"""
|
||||
|
||||
observations: list[ObservationCreate] = Field(..., min_length=1, max_length=100)
|
||||
|
||||
|
||||
class MessageSearchOptions(BaseModel):
|
||||
query: str = Field(..., description="Search query")
|
||||
filters: dict[str, Any] | None = Field(
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ class BEAMRunner:
|
|||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
status = await honcho_client.get_deriver_status(session_id=session_id)
|
||||
status = await honcho_client.get_deriver_status(session=session_id)
|
||||
except Exception:
|
||||
await asyncio.sleep(1)
|
||||
elapsed_time = time.time() - start_time
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ class LongMemEvalRunner:
|
|||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
status = await honcho_client.get_deriver_status(session_id=session_id)
|
||||
status = await honcho_client.get_deriver_status(session=session_id)
|
||||
except Exception as _e:
|
||||
await asyncio.sleep(1)
|
||||
elapsed_time = time.time() - start_time
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ class TestRunner:
|
|||
"""
|
||||
try:
|
||||
await honcho_client.poll_deriver_status(
|
||||
session_id=session_id,
|
||||
session=session_id,
|
||||
timeout=float(self.timeout_seconds)
|
||||
if self.timeout_seconds
|
||||
else 10000.0,
|
||||
|
|
|
|||
|
|
@ -753,3 +753,409 @@ class TestObservationRoutes:
|
|||
assert "embedding" not in observation
|
||||
assert "internal_metadata" not in observation
|
||||
assert "collection" not in observation
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_success(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating a single 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 observation via API
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "User prefers dark mode",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
|
||||
observation = data[0]
|
||||
assert observation["content"] == "User prefers dark mode"
|
||||
assert observation["observer_id"] == test_peer.name
|
||||
assert observation["observed_id"] == test_peer2.name
|
||||
assert observation["session_id"] == test_session.name
|
||||
assert "id" in observation
|
||||
assert "created_at" in observation
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observations_batch(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating multiple observations in batch"""
|
||||
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 multiple observations via API
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "User prefers dark mode",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
{
|
||||
"content": "User works late at night",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
{
|
||||
"content": "User enjoys programming",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 3
|
||||
|
||||
contents = [obs["content"] for obs in data]
|
||||
assert "User prefers dark mode" in contents
|
||||
assert "User works late at night" in contents
|
||||
assert "User enjoys programming" in contents
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_nonexistent_session(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating observation with non-existent session fails"""
|
||||
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.commit()
|
||||
|
||||
# Try to create observation with non-existent session
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "Test observation",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": "nonexistent_session",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_nonexistent_peer(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating observation with non-existent peer fails"""
|
||||
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 create observation with non-existent observer
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "Test observation",
|
||||
"observer_id": "nonexistent_peer",
|
||||
"observed_id": test_peer.name,
|
||||
"session_id": test_session.name,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_empty_content(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating observation with empty content fails validation"""
|
||||
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()
|
||||
|
||||
# Try to create observation with empty content
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_empty_list(
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating observations with empty list fails validation"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
# Try to create with empty observations list
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={"observations": []},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_creates_collection(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that creating observation auto-creates collection if needed"""
|
||||
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 observation via API (this should auto-create collection)
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "Test observation",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
|
||||
# The observation was created successfully, which means the collection
|
||||
# was created (since documents require a collection)
|
||||
observation = data[0]
|
||||
assert observation["observer_id"] == test_peer.name
|
||||
assert observation["observed_id"] == test_peer2.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_different_observer_observed_pairs(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test creating observations with different observer/observed pairs in single batch"""
|
||||
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 observations with different observer/observed pairs
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "Peer1 observes Peer2",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
{
|
||||
"content": "Peer2 observes Peer3",
|
||||
"observer_id": test_peer2.name,
|
||||
"observed_id": test_peer3.name,
|
||||
"session_id": test_session.name,
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 2
|
||||
|
||||
# Verify each observation has correct observer/observed
|
||||
obs1 = next(o for o in data if o["content"] == "Peer1 observes Peer2")
|
||||
assert obs1["observer_id"] == test_peer.name
|
||||
assert obs1["observed_id"] == test_peer2.name
|
||||
|
||||
obs2 = next(o for o in data if o["content"] == "Peer2 observes Peer3")
|
||||
assert obs2["observer_id"] == test_peer2.name
|
||||
assert obs2["observed_id"] == test_peer3.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_created_observations_are_searchable(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that created observations can be found via list endpoint"""
|
||||
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 observation via API
|
||||
create_response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations",
|
||||
json={
|
||||
"observations": [
|
||||
{
|
||||
"content": "Unique test content for searchability",
|
||||
"observer_id": test_peer.name,
|
||||
"observed_id": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert create_response.status_code == 200
|
||||
created_id = create_response.json()[0]["id"]
|
||||
|
||||
# List observations and verify the created one is there
|
||||
list_response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/observations/list",
|
||||
json={
|
||||
"filters": {
|
||||
"observer": test_peer.name,
|
||||
"observed": test_peer2.name,
|
||||
"session_id": test_session.name,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert list_response.status_code == 200
|
||||
data = list_response.json()
|
||||
ids = [obs["id"] for obs in data["items"]]
|
||||
assert created_id in ids
|
||||
|
|
|
|||
|
|
@ -210,24 +210,24 @@ async def test_get_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, st
|
|||
# Test with peer_id only
|
||||
peer = await honcho_client.peer(id="test-peer-deriver-status")
|
||||
await peer.get_metadata() # Create the peer
|
||||
status = await honcho_client.get_deriver_status(observer_id=peer.id)
|
||||
status = await honcho_client.get_deriver_status(observer=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with session_id only
|
||||
session = await honcho_client.session(id="test-session-deriver-status")
|
||||
await session.get_metadata() # Create the session
|
||||
status = await honcho_client.get_deriver_status(session_id=session.id)
|
||||
status = await honcho_client.get_deriver_status(session=session.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with both peer_id and session_id
|
||||
# Test with both peer and session
|
||||
status = await honcho_client.get_deriver_status(
|
||||
observer_id=peer.id, session_id=session.id
|
||||
observer=peer.id, session=session.id
|
||||
)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with include_sender=True
|
||||
# Test with sender
|
||||
status = await honcho_client.get_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
observer=peer.id, sender=peer.id
|
||||
)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
else:
|
||||
|
|
@ -243,25 +243,21 @@ async def test_get_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, st
|
|||
# Test with peer_id only
|
||||
peer = honcho_client.peer(id="test-peer-deriver-status")
|
||||
peer.get_metadata() # Create the peer
|
||||
status = honcho_client.get_deriver_status(observer_id=peer.id)
|
||||
status = honcho_client.get_deriver_status(observer=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with session_id only
|
||||
session = honcho_client.session(id="test-session-deriver-status")
|
||||
session.get_metadata() # Create the session
|
||||
status = honcho_client.get_deriver_status(session_id=session.id)
|
||||
status = honcho_client.get_deriver_status(session=session.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with both peer_id and session_id
|
||||
status = honcho_client.get_deriver_status(
|
||||
observer_id=peer.id, session_id=session.id
|
||||
)
|
||||
# Test with both peer and session
|
||||
status = honcho_client.get_deriver_status(observer=peer.id, session=session.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with include_sender=True
|
||||
status = honcho_client.get_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
# Test with sender
|
||||
status = honcho_client.get_deriver_status(observer=peer.id, sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
|
||||
|
|
@ -297,7 +293,7 @@ async def test_poll_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, s
|
|||
honcho_client, "get_deriver_status", return_value=completed_status
|
||||
):
|
||||
status = await honcho_client.poll_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
observer=peer.id, sender=peer.id
|
||||
)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
else:
|
||||
|
|
@ -315,9 +311,7 @@ async def test_poll_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, s
|
|||
with patch.object(
|
||||
honcho_client, "get_deriver_status", return_value=completed_status
|
||||
):
|
||||
status = honcho_client.poll_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
status = honcho_client.poll_deriver_status(observer=peer.id, sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
|
||||
|
|
@ -390,7 +384,7 @@ async def test_update_message_with_message_id(
|
|||
|
||||
# Update using message_id string
|
||||
updated = await honcho_client.update_message(
|
||||
message.id, {"updated": True}, session_id=session.id
|
||||
message.id, {"updated": True}, session=session.id
|
||||
)
|
||||
assert isinstance(updated, Message)
|
||||
assert updated.metadata == {"updated": True}
|
||||
|
|
@ -410,7 +404,7 @@ async def test_update_message_with_message_id(
|
|||
|
||||
# Update using message_id string
|
||||
updated = honcho_client.update_message(
|
||||
message.id, {"updated": True}, session_id=session.id
|
||||
message.id, {"updated": True}, session=session.id
|
||||
)
|
||||
assert isinstance(updated, Message)
|
||||
assert updated.metadata == {"updated": True}
|
||||
|
|
@ -422,15 +416,19 @@ async def test_update_message_validation(
|
|||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that update_message raises ValueError when message_id is provided without session_id.
|
||||
Tests that update_message raises ValueError when message ID is provided without session.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
with pytest.raises(ValueError, match="session_id is required"):
|
||||
with pytest.raises(
|
||||
ValueError, match="session is required when message is a string ID"
|
||||
):
|
||||
await honcho_client.update_message("msg_123", {"key": "value"})
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
with pytest.raises(ValueError, match="session_id is required"):
|
||||
with pytest.raises(
|
||||
ValueError, match="session is required when message is a string ID"
|
||||
):
|
||||
honcho_client.update_message("msg_123", {"key": "value"})
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ async def test_session_upload_file(
|
|||
user = honcho_client.peer(id="user-upload")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
else:
|
||||
# Async client
|
||||
|
|
@ -41,7 +41,7 @@ async def test_session_upload_file(
|
|||
user = await honcho_client.peer(id="user-upload")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
|
||||
# Verify messages were created
|
||||
|
|
@ -78,7 +78,7 @@ async def test_large_file_chunking(
|
|||
user = honcho_client.peer(id="user-chunking")
|
||||
messages = session.upload_file(
|
||||
file=large_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
else:
|
||||
# Async client
|
||||
|
|
@ -86,7 +86,7 @@ async def test_large_file_chunking(
|
|||
user = await honcho_client.peer(id="user-chunking")
|
||||
messages = await session.upload_file(
|
||||
file=large_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
|
||||
# Should be multiple messages due to chunking
|
||||
|
|
@ -129,16 +129,16 @@ async def test_multiple_files_upload(
|
|||
# Sync client
|
||||
session = honcho_client.session(id="test-session-multiple")
|
||||
user = honcho_client.peer(id="user-multiple")
|
||||
messages1 = session.upload_file(file=file1, peer_id=user.id)
|
||||
messages2 = session.upload_file(file=file2, peer_id=user.id)
|
||||
messages3 = session.upload_file(file=file3, peer_id=user.id)
|
||||
messages1 = session.upload_file(file=file1, peer=user.id)
|
||||
messages2 = session.upload_file(file=file2, peer=user.id)
|
||||
messages3 = session.upload_file(file=file3, peer=user.id)
|
||||
else:
|
||||
# Async client
|
||||
session = await honcho_client.session(id="test-session-multiple")
|
||||
user = await honcho_client.peer(id="user-multiple")
|
||||
messages1 = await session.upload_file(file=file1, peer_id=user.id)
|
||||
messages2 = await session.upload_file(file=file2, peer_id=user.id)
|
||||
messages3 = await session.upload_file(file=file3, peer_id=user.id)
|
||||
messages1 = await session.upload_file(file=file1, peer=user.id)
|
||||
messages2 = await session.upload_file(file=file2, peer=user.id)
|
||||
messages3 = await session.upload_file(file=file3, peer=user.id)
|
||||
|
||||
# Should be at least one message per file
|
||||
assert len(messages1) >= 1
|
||||
|
|
@ -187,7 +187,7 @@ async def test_json_file_upload(client_fixture: tuple[Honcho | AsyncHoncho, str]
|
|||
user = honcho_client.peer(id="user-json")
|
||||
messages = session.upload_file(
|
||||
file=json_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
else:
|
||||
# Async client
|
||||
|
|
@ -195,7 +195,7 @@ async def test_json_file_upload(client_fixture: tuple[Honcho | AsyncHoncho, str]
|
|||
user = await honcho_client.peer(id="user-json")
|
||||
messages = await session.upload_file(
|
||||
file=json_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
|
||||
# Should create at least one message
|
||||
|
|
@ -236,7 +236,7 @@ async def test_file_upload_with_tuple_input(
|
|||
user = honcho_client.peer(id="user-tuple")
|
||||
messages = session.upload_file(
|
||||
file=(filename, content.encode("utf-8"), content_type),
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
else:
|
||||
# Async client
|
||||
|
|
@ -244,7 +244,7 @@ async def test_file_upload_with_tuple_input(
|
|||
user = await honcho_client.peer(id="user-tuple")
|
||||
messages = await session.upload_file(
|
||||
file=(filename, content.encode("utf-8"), content_type),
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
)
|
||||
|
||||
# Should create at least one message
|
||||
|
|
@ -280,7 +280,7 @@ async def test_file_upload_with_metadata(
|
|||
user = honcho_client.peer(id="user-metadata")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
metadata=metadata,
|
||||
)
|
||||
else:
|
||||
|
|
@ -288,7 +288,7 @@ async def test_file_upload_with_metadata(
|
|||
user = await honcho_client.peer(id="user-metadata")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
|
@ -328,7 +328,7 @@ async def test_file_upload_with_configuration(
|
|||
user = honcho_client.peer(id="user-config")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
configuration=configuration,
|
||||
)
|
||||
else:
|
||||
|
|
@ -336,7 +336,7 @@ async def test_file_upload_with_configuration(
|
|||
user = await honcho_client.peer(id="user-config")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
configuration=configuration,
|
||||
)
|
||||
|
||||
|
|
@ -372,7 +372,7 @@ async def test_file_upload_with_created_at(
|
|||
user = honcho_client.peer(id="user-timestamp")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
created_at=test_timestamp.isoformat(),
|
||||
)
|
||||
else:
|
||||
|
|
@ -380,7 +380,7 @@ async def test_file_upload_with_created_at(
|
|||
user = await honcho_client.peer(id="user-timestamp")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
||||
|
|
@ -426,7 +426,7 @@ async def test_file_upload_with_all_parameters(
|
|||
user = honcho_client.peer(id="user-all")
|
||||
messages = session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
metadata=metadata,
|
||||
configuration=configuration,
|
||||
created_at=created_at_str,
|
||||
|
|
@ -436,7 +436,7 @@ async def test_file_upload_with_all_parameters(
|
|||
user = await honcho_client.peer(id="user-all")
|
||||
messages = await session.upload_file(
|
||||
file=text_file,
|
||||
peer_id=user.id,
|
||||
peer=user.id,
|
||||
metadata=metadata,
|
||||
configuration=configuration,
|
||||
created_at=created_at_str,
|
||||
|
|
@ -476,13 +476,13 @@ async def test_file_upload_with_datetime_object(
|
|||
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
|
||||
file=text_file, peer=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
|
||||
file=text_file, peer=user.id, created_at=test_timestamp
|
||||
)
|
||||
|
||||
assert len(messages) >= 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,621 @@
|
|||
"""Tests for observation SDK methods."""
|
||||
|
||||
import pytest
|
||||
|
||||
from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
||||
from sdks.python.src.honcho.client import Honcho
|
||||
from sdks.python.src.honcho.observations import (
|
||||
AsyncObservationScope,
|
||||
Observation,
|
||||
ObservationScope,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_create_single(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests creating a single observation via the SDK.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-create-single-observer")
|
||||
target = await honcho_client.peer(id="test-obs-create-single-target")
|
||||
session = await honcho_client.session(id="test-obs-create-single-session")
|
||||
|
||||
# Ensure session and both peers exist by adding messages from both
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope for observer -> target
|
||||
obs_scope = observer.observations_of(target)
|
||||
assert isinstance(obs_scope, AsyncObservationScope)
|
||||
|
||||
# Create a single observation
|
||||
created = await obs_scope.create(
|
||||
{"content": "User prefers dark mode", "session_id": session.id}
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert isinstance(created[0], Observation)
|
||||
assert created[0].content == "User prefers dark mode"
|
||||
assert created[0].observer_id == observer.id
|
||||
assert created[0].observed_id == target.id
|
||||
assert created[0].session_id == session.id
|
||||
assert created[0].id # Has an ID
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-create-single-observer")
|
||||
target = honcho_client.peer(id="test-obs-create-single-target")
|
||||
session = honcho_client.session(id="test-obs-create-single-session")
|
||||
|
||||
# Ensure session and both peers exist by adding messages from both
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope for observer -> target
|
||||
obs_scope = observer.observations_of(target)
|
||||
assert isinstance(obs_scope, ObservationScope)
|
||||
|
||||
# Create a single observation
|
||||
created = obs_scope.create(
|
||||
{"content": "User prefers dark mode", "session_id": session.id}
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert isinstance(created[0], Observation)
|
||||
assert created[0].content == "User prefers dark mode"
|
||||
assert created[0].observer_id == observer.id
|
||||
assert created[0].observed_id == target.id
|
||||
assert created[0].session_id == session.id
|
||||
assert created[0].id # Has an ID
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_create_batch(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests creating multiple observations in a batch via the SDK.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-create-batch-observer")
|
||||
target = await honcho_client.peer(id="test-obs-create-batch-target")
|
||||
session = await honcho_client.session(id="test-obs-create-batch-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create multiple observations
|
||||
created = await obs_scope.create(
|
||||
[
|
||||
{"content": "User prefers dark mode", "session_id": session.id},
|
||||
{"content": "User works late at night", "session_id": session.id},
|
||||
{"content": "User enjoys programming", "session_id": session.id},
|
||||
]
|
||||
)
|
||||
|
||||
assert len(created) == 3
|
||||
contents = {obs.content for obs in created}
|
||||
assert "User prefers dark mode" in contents
|
||||
assert "User works late at night" in contents
|
||||
assert "User enjoys programming" in contents
|
||||
|
||||
# All observations have correct observer/observed
|
||||
for obs in created:
|
||||
assert obs.observer_id == observer.id
|
||||
assert obs.observed_id == target.id
|
||||
assert obs.session_id == session.id
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-create-batch-observer")
|
||||
target = honcho_client.peer(id="test-obs-create-batch-target")
|
||||
session = honcho_client.session(id="test-obs-create-batch-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create multiple observations
|
||||
created = obs_scope.create(
|
||||
[
|
||||
{"content": "User prefers dark mode", "session_id": session.id},
|
||||
{"content": "User works late at night", "session_id": session.id},
|
||||
{"content": "User enjoys programming", "session_id": session.id},
|
||||
]
|
||||
)
|
||||
|
||||
assert len(created) == 3
|
||||
contents = {obs.content for obs in created}
|
||||
assert "User prefers dark mode" in contents
|
||||
assert "User works late at night" in contents
|
||||
assert "User enjoys programming" in contents
|
||||
|
||||
# All observations have correct observer/observed
|
||||
for obs in created:
|
||||
assert obs.observer_id == observer.id
|
||||
assert obs.observed_id == target.id
|
||||
assert obs.session_id == session.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_create_then_list(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that created observations can be listed.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-create-list-observer")
|
||||
target = await honcho_client.peer(id="test-obs-create-list-target")
|
||||
session = await honcho_client.session(id="test-obs-create-list-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observations
|
||||
created = await obs_scope.create(
|
||||
[
|
||||
{
|
||||
"content": "Unique observation for list test",
|
||||
"session_id": session.id,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# List observations
|
||||
listed = await obs_scope.list()
|
||||
|
||||
# The created observation should be in the list
|
||||
listed_ids = {obs.id for obs in listed}
|
||||
assert created[0].id in listed_ids
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-create-list-observer")
|
||||
target = honcho_client.peer(id="test-obs-create-list-target")
|
||||
session = honcho_client.session(id="test-obs-create-list-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observations
|
||||
created = obs_scope.create(
|
||||
[
|
||||
{
|
||||
"content": "Unique observation for list test",
|
||||
"session_id": session.id,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# List observations
|
||||
listed = obs_scope.list()
|
||||
|
||||
# The created observation should be in the list
|
||||
listed_ids = {obs.id for obs in listed}
|
||||
assert created[0].id in listed_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_create_then_query(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that created observations can be queried semantically.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-create-query-observer")
|
||||
target = await honcho_client.peer(id="test-obs-create-query-target")
|
||||
session = await honcho_client.session(id="test-obs-create-query-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observation with specific content
|
||||
await obs_scope.create(
|
||||
[
|
||||
{
|
||||
"content": "User loves Italian cuisine especially pasta and pizza",
|
||||
"session_id": session.id,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Query for food-related observations
|
||||
results = await obs_scope.query("food preferences")
|
||||
|
||||
assert len(results) >= 1
|
||||
# At least one result should mention Italian food
|
||||
contents = " ".join(obs.content for obs in results)
|
||||
assert "Italian" in contents or "pasta" in contents or "pizza" in contents
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-create-query-observer")
|
||||
target = honcho_client.peer(id="test-obs-create-query-target")
|
||||
session = honcho_client.session(id="test-obs-create-query-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observation with specific content
|
||||
obs_scope.create(
|
||||
[
|
||||
{
|
||||
"content": "User loves Italian cuisine especially pasta and pizza",
|
||||
"session_id": session.id,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Query for food-related observations
|
||||
results = obs_scope.query("food preferences")
|
||||
|
||||
assert len(results) >= 1
|
||||
# At least one result should mention Italian food
|
||||
contents = " ".join(obs.content for obs in results)
|
||||
assert "Italian" in contents or "pasta" in contents or "pizza" in contents
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_create_then_delete(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that created observations can be deleted.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-create-delete-observer")
|
||||
target = await honcho_client.peer(id="test-obs-create-delete-target")
|
||||
session = await honcho_client.session(id="test-obs-create-delete-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observations
|
||||
created = await obs_scope.create(
|
||||
[
|
||||
{"content": "Observation to be deleted", "session_id": session.id},
|
||||
]
|
||||
)
|
||||
|
||||
observation_id = created[0].id
|
||||
|
||||
# Delete the observation
|
||||
await obs_scope.delete(observation_id)
|
||||
|
||||
# List observations - should not contain deleted one
|
||||
listed = await obs_scope.list()
|
||||
listed_ids = {obs.id for obs in listed}
|
||||
assert observation_id not in listed_ids
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-create-delete-observer")
|
||||
target = honcho_client.peer(id="test-obs-create-delete-target")
|
||||
session = honcho_client.session(id="test-obs-create-delete-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observations
|
||||
created = obs_scope.create(
|
||||
[
|
||||
{"content": "Observation to be deleted", "session_id": session.id},
|
||||
]
|
||||
)
|
||||
|
||||
observation_id = created[0].id
|
||||
|
||||
# Delete the observation
|
||||
obs_scope.delete(observation_id)
|
||||
|
||||
# List observations - should not contain deleted one
|
||||
listed = obs_scope.list()
|
||||
listed_ids = {obs.id for obs in listed}
|
||||
assert observation_id not in listed_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_observation_create(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests creating self-observations (observer == observed).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-self-obs-create-peer")
|
||||
session = await honcho_client.session(id="test-self-obs-create-session")
|
||||
|
||||
# Ensure session exists
|
||||
await session.add_messages([peer.message("Hello")])
|
||||
|
||||
# Get self-observation scope
|
||||
obs_scope = peer.observations
|
||||
assert isinstance(obs_scope, AsyncObservationScope)
|
||||
assert obs_scope.observer == peer.id
|
||||
assert obs_scope.observed == peer.id
|
||||
|
||||
# Create a self-observation
|
||||
created = await obs_scope.create(
|
||||
{"content": "I prefer morning workouts", "session_id": session.id}
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert created[0].observer_id == peer.id
|
||||
assert created[0].observed_id == peer.id
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-self-obs-create-peer")
|
||||
session = honcho_client.session(id="test-self-obs-create-session")
|
||||
|
||||
# Ensure session exists
|
||||
session.add_messages([peer.message("Hello")])
|
||||
|
||||
# Get self-observation scope
|
||||
obs_scope = peer.observations
|
||||
assert isinstance(obs_scope, ObservationScope)
|
||||
assert obs_scope.observer == peer.id
|
||||
assert obs_scope.observed == peer.id
|
||||
|
||||
# Create a self-observation
|
||||
created = obs_scope.create(
|
||||
{"content": "I prefer morning workouts", "session_id": session.id}
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert created[0].observer_id == peer.id
|
||||
assert created[0].observed_id == peer.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_create_with_session_filter(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests creating observations and filtering list by session.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-session-filter-observer")
|
||||
target = await honcho_client.peer(id="test-obs-session-filter-target")
|
||||
session1 = await honcho_client.session(id="test-obs-session-filter-s1")
|
||||
session2 = await honcho_client.session(id="test-obs-session-filter-s2")
|
||||
|
||||
# Ensure sessions and both peers exist
|
||||
await session1.add_messages(
|
||||
[
|
||||
observer.message("Hello 1 from observer"),
|
||||
target.message("Hello 1 from target"),
|
||||
]
|
||||
)
|
||||
await session2.add_messages(
|
||||
[
|
||||
observer.message("Hello 2 from observer"),
|
||||
target.message("Hello 2 from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observations in different sessions
|
||||
await obs_scope.create(
|
||||
[
|
||||
{"content": "Session 1 observation", "session_id": session1.id},
|
||||
]
|
||||
)
|
||||
await obs_scope.create(
|
||||
[
|
||||
{"content": "Session 2 observation", "session_id": session2.id},
|
||||
]
|
||||
)
|
||||
|
||||
# List filtered by session1
|
||||
s1_obs = await obs_scope.list(session=session1)
|
||||
s1_contents = [obs.content for obs in s1_obs]
|
||||
assert "Session 1 observation" in s1_contents
|
||||
assert "Session 2 observation" not in s1_contents
|
||||
|
||||
# List filtered by session2
|
||||
s2_obs = await obs_scope.list(session=session2)
|
||||
s2_contents = [obs.content for obs in s2_obs]
|
||||
assert "Session 2 observation" in s2_contents
|
||||
assert "Session 1 observation" not in s2_contents
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-session-filter-observer")
|
||||
target = honcho_client.peer(id="test-obs-session-filter-target")
|
||||
session1 = honcho_client.session(id="test-obs-session-filter-s1")
|
||||
session2 = honcho_client.session(id="test-obs-session-filter-s2")
|
||||
|
||||
# Ensure sessions and both peers exist
|
||||
session1.add_messages(
|
||||
[
|
||||
observer.message("Hello 1 from observer"),
|
||||
target.message("Hello 1 from target"),
|
||||
]
|
||||
)
|
||||
session2.add_messages(
|
||||
[
|
||||
observer.message("Hello 2 from observer"),
|
||||
target.message("Hello 2 from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope
|
||||
obs_scope = observer.observations_of(target)
|
||||
|
||||
# Create observations in different sessions
|
||||
obs_scope.create(
|
||||
[
|
||||
{"content": "Session 1 observation", "session_id": session1.id},
|
||||
]
|
||||
)
|
||||
obs_scope.create(
|
||||
[
|
||||
{"content": "Session 2 observation", "session_id": session2.id},
|
||||
]
|
||||
)
|
||||
|
||||
# List filtered by session1
|
||||
s1_obs = obs_scope.list(session=session1)
|
||||
s1_contents = [obs.content for obs in s1_obs]
|
||||
assert "Session 1 observation" in s1_contents
|
||||
assert "Session 2 observation" not in s1_contents
|
||||
|
||||
# List filtered by session2
|
||||
s2_obs = obs_scope.list(session=session2)
|
||||
s2_contents = [obs.content for obs in s2_obs]
|
||||
assert "Session 2 observation" in s2_contents
|
||||
assert "Session 1 observation" not in s2_contents
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_observation_scope_via_peer_string(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests creating observations via observations_of(string).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-obs-string-target-observer")
|
||||
target = await honcho_client.peer(id="test-obs-string-target-target")
|
||||
session = await honcho_client.session(id="test-obs-string-target-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
await session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope using string ID
|
||||
obs_scope = observer.observations_of(target.id)
|
||||
assert obs_scope.observed == target.id
|
||||
|
||||
# Create observation
|
||||
created = await obs_scope.create(
|
||||
{"content": "Created via string target", "session_id": session.id}
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert created[0].observed_id == target.id
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-obs-string-target-observer")
|
||||
target = honcho_client.peer(id="test-obs-string-target-target")
|
||||
session = honcho_client.session(id="test-obs-string-target-session")
|
||||
|
||||
# Ensure session and both peers exist
|
||||
session.add_messages(
|
||||
[
|
||||
observer.message("Hello from observer"),
|
||||
target.message("Hello from target"),
|
||||
]
|
||||
)
|
||||
|
||||
# Get observation scope using string ID
|
||||
obs_scope = observer.observations_of(target.id)
|
||||
assert obs_scope.observed == target.id
|
||||
|
||||
# Create observation
|
||||
created = obs_scope.create(
|
||||
{"content": "Created via string target", "session_id": session.id}
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert created[0].observed_id == target.id
|
||||
|
|
@ -470,20 +470,18 @@ async def test_session_get_deriver_status(
|
|||
assert hasattr(status, "pending_work_units")
|
||||
assert status.sessions is None
|
||||
|
||||
# Test with observer_id only
|
||||
# Test with observer only
|
||||
peer = await honcho_client.peer(id="test-peer-session-deriver")
|
||||
await peer.get_metadata() # Create the peer
|
||||
status = await session.get_deriver_status(observer_id=peer.id)
|
||||
status = await session.get_deriver_status(observer=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with sender_id only
|
||||
status = await session.get_deriver_status(sender_id=peer.id)
|
||||
# Test with sender only
|
||||
status = await session.get_deriver_status(sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with both observer_id and sender_id
|
||||
status = await session.get_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
# Test with both observer and sender
|
||||
status = await session.get_deriver_status(observer=peer.id, sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
|
@ -499,18 +497,18 @@ async def test_session_get_deriver_status(
|
|||
assert hasattr(status, "pending_work_units")
|
||||
assert status.sessions is None
|
||||
|
||||
# Test with observer_id only
|
||||
# Test with observer only
|
||||
peer = honcho_client.peer(id="test-peer-session-deriver")
|
||||
peer.get_metadata() # Create the peer
|
||||
status = session.get_deriver_status(observer_id=peer.id)
|
||||
status = session.get_deriver_status(observer=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with sender_id only
|
||||
status = session.get_deriver_status(sender_id=peer.id)
|
||||
# Test with sender only
|
||||
status = session.get_deriver_status(sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with both observer_id and sender_id
|
||||
status = session.get_deriver_status(observer_id=peer.id, sender_id=peer.id)
|
||||
# Test with both observer and sender
|
||||
status = session.get_deriver_status(observer=peer.id, sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
|
||||
|
|
@ -554,9 +552,7 @@ async def test_session_poll_deriver_status(
|
|||
"get_deriver_status",
|
||||
new=AsyncMock(return_value=completed_status),
|
||||
):
|
||||
status = await session.poll_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
status = await session.poll_deriver_status(observer=peer.id, sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
|
@ -576,7 +572,7 @@ async def test_session_poll_deriver_status(
|
|||
with patch.object(
|
||||
session.__class__, "get_deriver_status", return_value=completed_status
|
||||
):
|
||||
status = session.poll_deriver_status(observer_id=peer.id, sender_id=peer.id)
|
||||
status = session.poll_deriver_status(observer=peer.id, sender=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ from pydantic import ValidationError
|
|||
sys.path.insert(0, str(Path(__file__).parents[2]))
|
||||
|
||||
from honcho import AsyncHoncho
|
||||
from honcho.async_client.peer import AsyncPeer
|
||||
from honcho.async_client.session import SessionPeerConfig as SDKSessionPeerConfig
|
||||
from honcho.base import PeerBase
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import (
|
||||
Configuration,
|
||||
MessageCreateParam,
|
||||
|
|
@ -110,7 +110,7 @@ class UnifiedTestExecutor:
|
|||
)
|
||||
|
||||
if step.peer_configs:
|
||||
peer_list: list[tuple[str | AsyncPeer, SDKSessionPeerConfig]] = []
|
||||
peer_list: list[tuple[str | PeerBase, SDKSessionPeerConfig]] = []
|
||||
for peer_id, config in step.peer_configs.items():
|
||||
sdk_config = SDKSessionPeerConfig(
|
||||
**config.model_dump(exclude_none=True)
|
||||
|
|
@ -203,7 +203,7 @@ class UnifiedTestExecutor:
|
|||
peer = await self.client.peer(id=step.observer_peer_id)
|
||||
|
||||
response = await peer.chat(
|
||||
step.input, session_id=step.session_id, target=step.observed_peer_id
|
||||
step.input, session=step.session_id, target=step.observed_peer_id
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
|
|||
8
uv.lock
8
uv.lock
|
|
@ -826,7 +826,7 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = ">=1.6.0" },
|
||||
{ name = "honcho-core", specifier = ">=1.6.1" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.12.0" },
|
||||
|
|
@ -837,7 +837,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
|||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.6.0"
|
||||
version = "1.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -847,9 +847,9 @@ dependencies = [
|
|||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/9a/19d48268e9cb3b7968141ac6f7ec9d6e3163d3402838ad3de58e0e77a769/honcho_core-1.6.0.tar.gz", hash = "sha256:1b394c7f9d611892685e815c918b5f0e8313126763c5936609c61e237d7c039b", size = 141235, upload-time = "2025-12-03T18:31:35.646Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/ca/5d0229382771d489b838805eb45829817d22b5c7c05d4838cd0a04f59081/honcho_core-1.6.1.tar.gz", hash = "sha256:e2baba3eaf2dfa59c2ecee164f1fb6cca121177167c194d4b861898cbfb5df2e", size = 142082, upload-time = "2025-12-04T16:37:32.725Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/25/7d9d48b6d8a1c9a1f578c76dd4354ef26c4f0704bfcc1d85ed0b2eae32b1/honcho_core-1.6.0-py3-none-any.whl", hash = "sha256:887c04dbff479a529fa4f4b5f774d5498e4dc2860c6438bea63b9964cb5bf6d8", size = 138075, upload-time = "2025-12-03T18:31:34.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/a6/8108dcedfcfa9c2eb1e9fdbcea4bd183e6f89b04b9acb8c9d1c71cf3981b/honcho_core-1.6.1-py3-none-any.whl", hash = "sha256:68ac553ea32c0f91ab47fce1be6637ccc0991d0a5a360155ea91a5ae9b7859b3", size = 139798, upload-time = "2025-12-04T16:37:31.692Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Reference in New Issue