diff --git a/docs/docs.json b/docs/docs.json index 9fcc0353..3ad377a4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -15,21 +15,14 @@ }, "favicon": "/favicon.svg", "contextual": { - "options": [ - "copy", - "view", - "chatgpt", - "claude" - ] + "options": ["copy", "view", "chatgpt", "claude"] }, "navigation": { "versions": [ { "version": "v2.5.1", "api": { - "openapi": [ - "openapi.json" - ] + "openapi": ["openapi.json"] }, "tabs": [ { @@ -76,15 +69,11 @@ "groups": [ { "group": "Getting Started", - "pages": [ - "v2/guides/overview" - ] + "pages": ["v2/guides/overview"] }, { "group": "Migrations", - "pages": [ - "v2/migrations/from-mem0" - ] + "pages": ["v2/migrations/from-mem0"] }, { "group": "Integrations", @@ -97,10 +86,7 @@ }, { "group": "Application Interfaces", - "pages": [ - "v2/guides/discord", - "v2/guides/telegram" - ] + "pages": ["v2/guides/discord", "v2/guides/telegram"] } ] }, @@ -109,9 +95,7 @@ "groups": [ { "group": "API Documentation", - "pages": [ - "v2/api-reference/introduction" - ] + "pages": ["v2/api-reference/introduction"] }, { "group": "workspaces", @@ -227,9 +211,7 @@ { "version": "v2.6.0-alpha", "api": { - "openapi": [ - "openapi.json" - ] + "openapi": ["openapi.json"] }, "tabs": [ { @@ -301,9 +283,7 @@ }, { "group": "Migrations", - "pages": [ - "v2.6.0-alpha/guides/migrations/mem0" - ] + "pages": ["v2.6.0-alpha/guides/migrations/mem0"] }, { "group": "Chatbots", @@ -338,9 +318,7 @@ "groups": [ { "group": "API Documentation", - "pages": [ - "v2.6.0-alpha/api-reference/introduction" - ] + "pages": ["v2.6.0-alpha/api-reference/introduction"] }, { "group": "workspaces", @@ -442,9 +420,7 @@ { "version": "v1.1.0", "api": { - "openapi": [ - "openapi.json" - ] + "openapi": ["openapi.json"] }, "tabs": [ { @@ -474,23 +450,15 @@ "groups": [ { "group": "Getting Started", - "pages": [ - "v1/guides/overview", - "v1/guides/streaming-response" - ] + "pages": ["v1/guides/overview", "v1/guides/streaming-response"] }, { "group": "Application Interfaces", - "pages": [ - "v1/guides/discord", - "v1/guides/honcho-mcp" - ] + "pages": ["v1/guides/discord", "v1/guides/honcho-mcp"] }, { "group": "Personal Memory", - "pages": [ - "v1/guides/dialectic-endpoint" - ] + "pages": ["v1/guides/dialectic-endpoint"] } ] }, @@ -499,9 +467,7 @@ "groups": [ { "group": "API Documentation", - "pages": [ - "v1/api-reference/introduction" - ] + "pages": ["v1/api-reference/introduction"] }, { "group": "apps", @@ -549,9 +515,7 @@ }, { "group": "keys", - "pages": [ - "v1/api-reference/endpoint/keys/create-key" - ] + "pages": ["v1/api-reference/endpoint/keys/create-key"] }, { "group": "metamessages", diff --git a/docs/v2.6.0-alpha/documentation/features/advanced/representation-scopes.mdx b/docs/v2.6.0-alpha/documentation/features/advanced/representation-scopes.mdx index 7e399245..8512e87e 100644 --- a/docs/v2.6.0-alpha/documentation/features/advanced/representation-scopes.mdx +++ b/docs/v2.6.0-alpha/documentation/features/advanced/representation-scopes.mdx @@ -15,11 +15,11 @@ Assuming reasoning is enabled, you can control the perspectives representations When `observe_me=true` (the default), Honcho forms one representation per peer, reasoning over every message written to that peer across all sessions. -You can retrieve a subset of conclusions from a peer's representation using `working_rep()`: +You can retrieve a subset of conclusions from a peer's representation using `get_representation()`: ```python # Retrieve conclusions from Honcho's representation of Alice (across all sessions) -alice_rep = session.working_rep("alice") +alice_rep = session.get_representation("alice") # Or via chat response = alice.chat("What are Alice's main interests?", session_id=session.id) @@ -72,9 +72,9 @@ The `target` parameter controls which representation you retrieve: | Query | Returns | |-------|---------| -| `working_rep("alice")` | Conclusions from Honcho's representation of Alice (across all sessions) | -| `working_rep("alice", target="bob")` | Conclusions from Alice's representation of Bob (from sessions Alice participated in) | -| `working_rep("alice", target="charlie")` | Conclusions from Alice's representation of Charlie (from sessions Alice participated in) | +| `get_representation("alice")` | Conclusions from Honcho's representation of Alice (across all sessions) | +| `get_representation("alice", target="bob")` | Conclusions from Alice's representation of Bob (from sessions Alice participated in) | +| `get_representation("alice", target="charlie")` | Conclusions from Alice's representation of Charlie (from sessions Alice participated in) | ### Code Examples @@ -111,9 +111,9 @@ session2.add_messages([ ]) # Retrieve conclusions from different perspectives -honcho_view = session.working_rep("alice") # Across all sessions -bob_view = session.working_rep("alice", target="bob") # Alice's view of Bob -charlie_view = session2.working_rep("alice", target="charlie") # Alice's view of Charlie +honcho_view = session.get_representation("alice") # Across all sessions +bob_view = session.get_representation("alice", target="bob") # Alice's view of Bob +charlie_view = session2.get_representation("alice", target="charlie") # Alice's view of Charlie ``` ```typescript TypeScript @@ -144,9 +144,9 @@ await session2.addMessages([ ]); // Retrieve conclusions from different perspectives -const honchoView = await session.workingRep("alice"); // Across all sessions -const bobView = await session.workingRep("alice", { target: "bob" }); // Alice's view of Bob -const charlieView = await session2.workingRep("alice", { target: "charlie" }); // Alice's view of Charlie +const honchoView = await session.getRepresentation("alice"); // Across all sessions +const bobView = await session.getRepresentation("alice", { target: "bob" }); // Alice's view of Bob +const charlieView = await session2.getRepresentation("alice", { target: "charlie" }); // Alice's view of Charlie ``` @@ -224,34 +224,34 @@ This architecture enables: ## Semantic Search Parameters -Both `working_rep()` and `chat()` support semantic filtering to retrieve a subset of relevant conclusions. You can optionally filter by session to retrieve only conclusions from specific session context: +Both `get_representation()` and `chat()` support semantic filtering to retrieve a subset of relevant conclusions. You can optionally filter by session to retrieve only conclusions from specific session context: | Parameter | Type | Description | |-----------|------|-------------| | `search_query` | `str` | Semantic query to filter conclusions | | `search_top_k` | `int` | Number of results to include (1–100) | | `search_max_distance` | `float` | Maximum semantic distance (0.0–1.0) | -| `include_most_derived` | `bool` | Include most recently derived conclusions | -| `max_observations` | `int` | Cap on total conclusions returned (1–100) | +| `include_most_frequent` | `bool` | Include most frequent conclusions | +| `max_conclusions` | `int` | Cap on total conclusions returned (1–100) | ```python Python # Retrieve conclusions about billing from Alice's representation of Bob -alice_view_billing = session.working_rep( +alice_view_billing = session.get_representation( "alice", target="bob", search_query="billing issues", search_top_k=10, - include_most_derived=True + include_most_frequent=True ) ``` ```typescript TypeScript -const aliceViewBilling = await session.workingRep("alice", { +const aliceViewBilling = await session.getRepresentation("alice", { target: "bob", searchQuery: "billing issues", searchTopK: 10, - includeMostDerived: true + includeMostFrequent: true }); ``` @@ -267,5 +267,5 @@ Directional representations update automatically through the reasoning pipeline The pipeline respects scoping—Honcho's representations reason over messages across all sessions, while directional representations only reason over messages from sessions where the observer was an active participant. -Conclusions are cached for fast retrieval. Use `working_rep()` to retrieve stored conclusions for dashboards and analytics. Use `peer.chat()` when you need query-specific reasoning with natural language. +Conclusions are cached for fast retrieval. Use `get_representation()` to retrieve stored conclusions for dashboards and analytics. Use `peer.chat()` when you need query-specific reasoning with natural language. diff --git a/docs/v2.6.0-alpha/documentation/features/get-context.mdx b/docs/v2.6.0-alpha/documentation/features/get-context.mdx index 6b4ba186..67aa5428 100644 --- a/docs/v2.6.0-alpha/documentation/features/get-context.mdx +++ b/docs/v2.6.0-alpha/documentation/features/get-context.mdx @@ -153,10 +153,10 @@ context = session.get_context( tokens=2000, peer_target="user-123", last_user_message="What are my coding preferences?", - search_top_k=10, # Number of relevant observations + search_top_k=10, # Number of relevant conclusions to fetch search_max_distance=0.8, # Max semantic distance (0.0-1.0) - include_most_derived=True, # Include most recent observations - max_observations=25 # Cap total observations + include_most_frequent=True, # Include most frequent conclusions + max_conclusions=25 # Cap total conclusions ) ``` @@ -167,10 +167,10 @@ context = session.get_context( peerTarget: "user-123", lastUserMessage: "What are my coding preferences?", representationOptions: { - searchTopK: 10, // Number of relevant observations + searchTopK: 10, // Number of relevant conclusions to fetch searchMaxDistance: 0.8, // Max semantic distance (0.0-1.0) - includeMostDerived: true, // Include most recent observations - maxObservations: 25 // Cap total observations + includeMostFrequent: true, // Include most frequent conclusions + maxConclusions: 25 // Cap total conclusions } }); })(); @@ -212,11 +212,11 @@ context = session.get_context( | `peer_target` | `str` | Peer ID to include representation for | | `peer_perspective` | `str` | Peer ID for perspective (requires peer_target) | | `last_user_message` | `str` | Message for semantic search (requires peer_target) | -| `limit_to_session` | `bool` | Limit to session observations only | +| `limit_to_session` | `bool` | Limit to session conclusions only | | `search_top_k` | `int` | Semantic search results to include (1-100) | | `search_max_distance` | `float` | Max semantic distance (0.0-1.0) | -| `include_most_derived` | `bool` | Include most recently derived observations | -| `max_observations` | `int` | Maximum observations to include (1-100) | +| `include_most_frequent` | `bool` | Include most frequent conclusions | +| `max_conclusions` | `int` | Maximum conclusions to include (1-100) | ## Converting to LLM Formats diff --git a/docs/v2.6.0-alpha/documentation/reference/sdk.mdx b/docs/v2.6.0-alpha/documentation/reference/sdk.mdx index 7e4f7f80..07fb91a4 100644 --- a/docs/v2.6.0-alpha/documentation/reference/sdk.mdx +++ b/docs/v2.6.0-alpha/documentation/reference/sdk.mdx @@ -284,7 +284,7 @@ context = alice.get_context() context = alice.get_context(target="bob") # What alice knows about bob # Get working representation with semantic search -rep = alice.working_rep(search_query="preferences", search_top_k=10) +rep = alice.get_representation(search_query="preferences", search_top_k=10) # Access observations self_observations = alice.observations.list() # Self-observations @@ -335,7 +335,7 @@ const context = await alice.getContext(); const targetContext = await alice.getContext("bob"); // What alice knows about bob // Get working representation with semantic search -const rep = await alice.workingRep(undefined, undefined, { +const rep = await alice.getRepresentation(undefined, undefined, { searchQuery: "preferences", searchTopK: 10 }); @@ -366,8 +366,8 @@ context = alice.get_context( search_query="work preferences", search_top_k=10, search_max_distance=0.8, - include_most_derived=True, - max_observations=50 + include_most_frequent=True, + max_conclusions=50 ) ``` @@ -385,8 +385,8 @@ const searchedContext = await alice.getContext("bob", { searchQuery: "work preferences", searchTopK: 10, searchMaxDistance: 0.8, - includeMostDerived: true, - maxObservations: 50 + includeMostFrequent: true, + maxConclusions: 50 }); ``` @@ -455,8 +455,8 @@ context = alice.get_context( search_query="work preferences", search_top_k=10, search_max_distance=0.8, - include_most_derived=True, - max_observations=50 + include_most_frequent=True, + max_conclusions=50 ) ``` @@ -474,8 +474,8 @@ const searchedContext = await alice.getContext("bob", { searchQuery: "work preferences", searchTopK: 10, searchMaxDistance: 0.8, - includeMostDerived: true, - maxObservations: 50 + includeMostFrequent: true, + maxConclusions: 50 }); ``` @@ -626,7 +626,7 @@ context = session.get_context( limit_to_session=True, search_top_k=10, search_max_distance=0.8, - include_most_derived=True, + include_most_frequent=True, max_observations=25 ) @@ -634,13 +634,13 @@ context = session.get_context( results = session.search("help") # Working representation queries with semantic search -global_rep = session.working_rep("alice") -targeted_rep = session.working_rep(alice, target=bob) -searched_rep = session.working_rep( +global_rep = session.get_representation("alice") +targeted_rep = session.get_representation(alice, target=bob) +searched_rep = session.get_representation( "alice", search_query="preferences", search_top_k=10, - include_most_derived=True + include_most_frequent=True ) # Upload a file to create messages @@ -713,9 +713,9 @@ const richContext = await session.getContext({ const results = await session.search("help"); // Working representation queries with semantic search -const globalRep = await session.workingRep("alice"); -const targetedRep = await session.workingRep(alice, { target: bob }); -const searchedRep = await session.workingRep("alice", undefined, { +const globalRep = await session.getRepresentation("alice"); +const targetedRep = await session.getRepresentation(alice, { target: bob }); +const searchedRep = await session.getRepresentation("alice", undefined, { searchQuery: "preferences", searchTopK: 10, includeMostDerived: true @@ -845,8 +845,8 @@ The SessionContext object has the following structure: | `limit_to_session` | `bool` | Limit representation to session only | | `search_top_k` | `int` | Number of semantic search results (1-100) | | `search_max_distance` | `float` | Max semantic distance (0.0-1.0) | -| `include_most_derived` | `bool` | Include most derived observations | -| `max_observations` | `int` | Max observations to include (1-100) | +| `include_most_frequent` | `bool` | Include most frequent conclusions | +| `max_conclusions` | `int` | Max conclusions to include (1-100) | ## Advanced Usage diff --git a/docs/v2.6.0-alpha/guides/migrations/mem0.mdx b/docs/v2.6.0-alpha/guides/migrations/mem0.mdx index 76ab7514..8eb544ee 100644 --- a/docs/v2.6.0-alpha/guides/migrations/mem0.mdx +++ b/docs/v2.6.0-alpha/guides/migrations/mem0.mdx @@ -275,7 +275,7 @@ Additional features with **no Mem0 equivalent**: | Honcho Method | Description | Use Case | |---------------|-------------|----------| | `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization | -| `session.working_rep(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation | +| `session.get_representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation | | `session.get_summaries()` | Auto-generated short/long session summaries | Conversation continuity | | `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning | diff --git a/docs/v2.6.0-alpha/migrations/from-mem0.mdx b/docs/v2.6.0-alpha/migrations/from-mem0.mdx index 76ab7514..8eb544ee 100644 --- a/docs/v2.6.0-alpha/migrations/from-mem0.mdx +++ b/docs/v2.6.0-alpha/migrations/from-mem0.mdx @@ -275,7 +275,7 @@ Additional features with **no Mem0 equivalent**: | Honcho Method | Description | Use Case | |---------------|-------------|----------| | `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization | -| `session.working_rep(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation | +| `session.get_representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation | | `session.get_summaries()` | Auto-generated short/long session summaries | Conversation continuity | | `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning | diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 513c269a..2fe3b509 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -8,7 +8,7 @@ authors = [ { name = "Plastic Labs", email = "hello@plasticlabs.ai" }, ] dependencies = [ - "honcho-core>=1.8.0", + "honcho-core==1.10.0", "httpx>=0.28.0, <1", "pydantic>=2.0.0, <3", "typing-extensions>=4.12.0; python_version < \"3.12\"", diff --git a/sdks/python/src/honcho/__init__.py b/sdks/python/src/honcho/__init__.py index b378e5bd..02f15f8a 100644 --- a/sdks/python/src/honcho/__init__.py +++ b/sdks/python/src/honcho/__init__.py @@ -42,17 +42,13 @@ from .async_client import ( ) from .base import PeerBase, SessionBase from .client import Honcho -from .observations import AsyncObservationScope, Observation, ObservationScope +from .conclusions import AsyncConclusionScope, ConclusionScope from .pagination import SyncPage from .peer import Peer from .session import Session from .session_context import SessionContext, SessionSummaries, Summary from .types import ( - DeductiveObservation, DialecticStreamResponse, - ExplicitObservation, - PeerContext, - Representation, ) __version__ = "1.6.0" @@ -61,16 +57,14 @@ __email__ = "hello@plasticlabs.ai" __all__ = [ "AsyncHoncho", - "AsyncObservationScope", + "AsyncConclusionScope", "AsyncPeer", "AsyncSession", "AsyncPage", "Honcho", - "Observation", - "ObservationScope", + "ConclusionScope", "Peer", "PeerBase", - "PeerContext", "Session", "SessionBase", "SessionContext", @@ -78,7 +72,4 @@ __all__ = [ "Summary", "SyncPage", "DialecticStreamResponse", - "Representation", - "ExplicitObservation", - "DeductiveObservation", ] diff --git a/sdks/python/src/honcho/async_client/client.py b/sdks/python/src/honcho/async_client/client.py index cef8ef9d..814d89db 100644 --- a/sdks/python/src/honcho/async_client/client.py +++ b/sdks/python/src/honcho/async_client/client.py @@ -8,7 +8,7 @@ from typing import Any, Literal import httpx from honcho_core import AsyncHoncho as AsyncHonchoCore from honcho_core import Honcho as HonchoCore -from honcho_core.types import DeriverStatus, Workspace +from honcho_core.types.workspaces import QueueStatusResponse from honcho_core.types.workspaces.peer import Peer as PeerCore from honcho_core.types.workspaces.session import Session as SessionCore from honcho_core.types.workspaces.sessions.message import Message @@ -425,19 +425,16 @@ class AsyncHoncho(BaseModel): workspace_id: str = Field( ..., min_length=1, description="ID of the workspace to delete" ), - ) -> Workspace: + ) -> None: """ Delete a workspace. - Makes an async API call to delete the specified workspace. + Makes an async API call to delete the specified workspace. This action cannot be undone. Args: workspace_id: The ID of the workspace to delete - - Returns: - The deleted Workspace object """ - return await self._client.workspaces.delete(workspace_id) + await self._client.workspaces.delete(workspace_id) @validate_call async def search( @@ -472,14 +469,14 @@ class AsyncHoncho(BaseModel): ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - async def get_deriver_status( + async def get_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, session: str | SessionBase | None = None, - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Get the deriver processing status, optionally scoped to an observer, sender, and/or session. + Get the queue 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 @@ -502,7 +499,7 @@ class AsyncHoncho(BaseModel): else (session if isinstance(session, str) else session.id) ) - return await self._client.workspaces.deriver_status( + return await self._client.workspaces.queue.status( workspace_id=self.workspace_id, observer_id=resolved_observer_id, sender_id=resolved_sender_id, @@ -510,7 +507,7 @@ class AsyncHoncho(BaseModel): ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - async def poll_deriver_status( + async def poll_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, @@ -520,11 +517,11 @@ class AsyncHoncho(BaseModel): gt=0, description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).", ), - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0. - This allows you to guarantee that all messages have been processed by the deriver for - use with the dialectic endpoint. + Poll get_queue_status until pending_work_units and in_progress_work_units are both 0. + This allows you to guarantee that all messages have been processed by the queue for + use with the chat endpoint. The polling estimates sleep time by assuming each work unit takes 1 second. @@ -535,19 +532,19 @@ class AsyncHoncho(BaseModel): timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds). Returns: - DeriverStatus when all work units are complete + QueueStatusResponse when all work units are complete Raises: TimeoutError: If timeout is exceeded before work units complete - Exception: If get_deriver_status fails repeatedly + Exception: If get_queue_status fails repeatedly """ start_time = time.time() while True: try: - status = await self.get_deriver_status(observer, sender, session) + status = await self.get_queue_status(observer, sender, session) except Exception as e: - logger.warning(f"Failed to get deriver status: {e}") + logger.warning(f"Failed to get queue status: {e}") # Sleep briefly before retrying await asyncio.sleep(1) @@ -590,44 +587,44 @@ class AsyncHoncho(BaseModel): await asyncio.sleep(sleep_time) @validate_call - async def list_observations( + async def list_conclusions( self, filters: dict[str, object] | None = Field( - None, description="Filters to scope the observations" + None, description="Filters to scope the conclusions" ), reverse: bool = Field( False, description="Whether to reverse the order of results" ), ): """ - List all observations in the current workspace with optional filtering. + List all conclusions in the current workspace with optional filtering. - Makes an async API call to retrieve observations that match the specified filters. - Observations can be filtered by session_id, observer_id, and observed_id. + Makes an async API call to retrieve conclusions that match the specified filters. + Conclusions can be filtered by session_id, observer_id, and observed_id. Args: - filters: Optional filter criteria for observations. Supported filters include: - - session_id: Filter observations by session - - observer_id: Filter observations by observer peer - - observed_id: Filter observations by observed peer + filters: Optional filter criteria for conclusions. Supported filters include: + - session_id: Filter conclusions by session + - observer_id: Filter conclusions by observer peer + - observed_id: Filter conclusions by observed peer reverse: Whether to reverse the order of results (default: False) Returns: - A paginated list of Observation objects matching the specified criteria + A paginated list of Conclusion objects matching the specified criteria Example: - >>> observations = await client.list_observations( + >>> conclusions = await client.list_conclusions( ... filters={"observer_id": "user123", "observed_id": "assistant"} ... ) """ - return await self._client.workspaces.observations.list( + return await self._client.workspaces.conclusions.list( workspace_id=self.workspace_id, filters=filters, reverse=reverse, ) @validate_call - async def query_observations( + async def query_conclusions( self, query: str = Field(..., min_length=1, description="Semantic search query"), observer: str = Field( @@ -650,9 +647,9 @@ class AsyncHoncho(BaseModel): ), ): """ - Query observations using semantic search. + Query conclusions using semantic search. - Performs vector similarity search on observations to find semantically relevant results. + Performs vector similarity search on conclusions to find semantically relevant results. Observer and observed peer IDs are required for semantic search. Args: @@ -664,10 +661,10 @@ class AsyncHoncho(BaseModel): filters: Optional filters to scope the query Returns: - A list of Observation objects matching the query + A list of Conclusion objects matching the query Example: - >>> observations = await client.query_observations( + >>> conclusions = await client.query_conclusions( ... query="user preferences about music", ... observer="user123", ... observed="assistant", @@ -682,7 +679,7 @@ class AsyncHoncho(BaseModel): "observed": observed, } - return await self._client.workspaces.observations.query( + return await self._client.workspaces.conclusions.query( workspace_id=self.workspace_id, query=query, top_k=top_k, @@ -691,27 +688,27 @@ class AsyncHoncho(BaseModel): ) @validate_call - async def delete_observation( + async def delete_conclusion( self, - observation_id: str = Field( - ..., min_length=1, description="ID of the observation to delete" + conclusion_id: str = Field( + ..., min_length=1, description="ID of the conclusion to delete" ), ) -> None: """ - Delete a specific observation by ID. + Delete a specific conclusion by ID. - This permanently deletes the observation (document) from the theory-of-mind system. + This permanently deletes the conclusion (document) from the theory-of-mind system. This action cannot be undone. Args: - observation_id: The ID of the observation to delete + conclusion_id: The ID of the conclusion to delete Example: - >>> await client.delete_observation('obs_123abc') + >>> await client.delete_conclusion('con_123abc') """ - await self._client.workspaces.observations.delete( + await self._client.workspaces.conclusions.delete( workspace_id=self.workspace_id, - observation_id=observation_id, + conclusion_id=conclusion_id, ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) diff --git a/sdks/python/src/honcho/async_client/peer.py b/sdks/python/src/honcho/async_client/peer.py index 7b8ff60a..ce2893b9 100644 --- a/sdks/python/src/honcho/async_client/peer.py +++ b/sdks/python/src/honcho/async_client/peer.py @@ -2,13 +2,16 @@ from __future__ import annotations import datetime from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, cast +from typing import Literal from honcho_core import AsyncHoncho as AsyncHonchoCore from honcho_core._types import omit from honcho_core.types.workspaces import PeerCardResponse -from honcho_core.types.workspaces.peer_working_representation_response import ( - PeerWorkingRepresentationResponse, +from honcho_core.types.workspaces.peer_context_response import ( + PeerContextResponse, +) +from honcho_core.types.workspaces.peer_representation_response import ( + PeerRepresentationResponse, ) from honcho_core.types.workspaces.session import Session as SessionCore from honcho_core.types.workspaces.sessions import MessageCreateParam @@ -17,13 +20,9 @@ from honcho_core.types.workspaces.sessions.message_create_param import Configura from pydantic import ConfigDict, Field, PrivateAttr, validate_call from ..base import PeerBase, SessionBase +from ..conclusions import AsyncConclusionScope from ..types import DialecticStreamResponse from .pagination import AsyncPage - -if TYPE_CHECKING: - from ..observations import AsyncObservationScope - from ..types import PeerContext, Representation - from .session import AsyncSession @@ -146,6 +145,8 @@ class AsyncPeer(PeerBase): stream: bool = False, target: str | PeerBase | None = None, session: str | SessionBase | None = None, + reasoning_level: Literal["minimal", "low", "medium", "high", "extra-high"] + | None = None, ) -> str | DialecticStreamResponse | None: """ Query the peer's representation with a natural language question. @@ -164,6 +165,8 @@ class AsyncPeer(PeerBase): 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. + reasoning_level: Optional reasoning level for the query: "minimal", "low", "medium", + "high", or "extra-high". Defaults to "low" if not provided. Returns: For non-streaming: Response string containing the answer, or None if no relevant information @@ -194,6 +197,9 @@ class AsyncPeer(PeerBase): stream=True, target=target_id, session_id=resolved_session_id, + reasoning_level=reasoning_level + if reasoning_level is not None + else omit, ) as response: response.http_response.raise_for_status() async for line in response.iter_lines(): @@ -219,6 +225,7 @@ class AsyncPeer(PeerBase): stream=stream, target=target_id, session_id=resolved_session_id, + reasoning_level=reasoning_level if reasoning_level is not None else omit, ) # "If the context provided doesn't help address the query, write absolutely NOTHING but "None"" if response.content in ("", None, "None"): @@ -501,50 +508,49 @@ class AsyncPeer(PeerBase): items: list[str] = response.peer_card return "\n".join(items) - async def working_rep( + async def get_representation( self, 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, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "Representation": + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> str: """ - Get a working representation for this peer. + Get a subset of the representation of the peer. Args: session: Optional session to scope the representation to. target: Optional target peer to get the representation of. If provided, returns the representation of the target from the perspective of this peer. - search_query: Semantic search query to filter relevant observations + search_query: Semantic search query to filter relevant conclusions search_top_k: Number of semantically relevant facts to return search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include Returns: - A Representation object containing explicit and deductive observations + A Representation string Example: ```python # Get global representation - rep = await peer.working_rep() + rep = await peer.get_representation() print(rep) # Get representation scoped to a session - session_rep = await peer.working_rep(session='session-123') + session_rep = await peer.get_representation(session='session-123') # Get representation with semantic search - searched_rep = await peer.working_rep( + searched_rep = await peer.get_representation( search_query='preferences', search_top_k=10, - max_observations=50 + max_conclusions=50 ) ``` """ - from ..types import Representation as _Representation session_id = ( None @@ -559,8 +565,8 @@ class AsyncPeer(PeerBase): if target is None else (target if isinstance(target, str) else target.id) ) - data: PeerWorkingRepresentationResponse = ( - await self._client.workspaces.peers.working_representation( + data: PeerRepresentationResponse = ( + await self._client.workspaces.peers.representation( peer_id=self.id, workspace_id=self.workspace_id, session_id=session_id, @@ -570,19 +576,15 @@ class AsyncPeer(PeerBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations - if max_observations is not None + max_conclusions=max_conclusions + if max_conclusions is not None else omit, ) ) - representation = data.get("representation") - if representation is not None: - return _Representation.from_dict(cast(dict[str, object], representation)) - else: - return _Representation.from_dict(data) + return data.representation async def get_context( self, @@ -590,9 +592,9 @@ class AsyncPeer(PeerBase): search_query: str | None = None, search_top_k: int | None = None, search_max_distance: float | None = None, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "PeerContext": + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> PeerContextResponse: """ Get context for this peer, including representation and peer card. @@ -603,11 +605,11 @@ class AsyncPeer(PeerBase): target: Optional target peer to get context for. If provided, returns the context for the target from this peer's perspective. Can be an AsyncPeer object or peer ID string. - search_query: Semantic search query to filter relevant observations + search_query: Semantic search query to filter relevant conclusions search_top_k: Number of semantically relevant facts to return search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include Returns: A PeerContext object containing the representation and peer card @@ -629,15 +631,13 @@ class AsyncPeer(PeerBase): ) ``` """ - from ..types import PeerContext as _PeerContext - target_id = ( None if target is None else (target if isinstance(target, str) else target.id) ) - response = await self._client.workspaces.peers.get_context( + return await self._client.workspaces.peers.context( peer_id=self.id, workspace_id=self.workspace_id, target=target_id, @@ -646,73 +646,69 @@ class AsyncPeer(PeerBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations if max_observations is not None else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, ) - return _PeerContext.from_api_response(response) - @property - def observations(self) -> "AsyncObservationScope": + def conclusions(self) -> "AsyncConclusionScope": """ - Access this peer's self-observations (where observer == observed == self). + Access this peer's self-conclusions (where observer == observed == self). - This property provides a convenient way to access observations that this peer - has made about themselves. Use this for self-observation scenarios. + This property provides a convenient way to access conclusions that this peer + has made about themselves. Use this for self-conclusion scenarios. Returns: - An AsyncObservationScope scoped to this peer's self-observations + An AsyncConclusionScope scoped to this peer's self-conclusions Example: ```python - # List self-observations - obs_list = await peer.observations.list() + # List self-conclusions + obs_list = await peer.conclusions.list() - # Search self-observations - results = await peer.observations.query("preferences") + # Search self-conclusions + results = await peer.conclusions.query("preferences") - # Delete a self-observation - await peer.observations.delete("obs-123") + # Delete a self-conclusion + await peer.conclusions.delete("obs-123") ``` """ - from ..observations import AsyncObservationScope as _AsyncObservationScope + return AsyncConclusionScope(self._client, self.workspace_id, self.id, self.id) - return _AsyncObservationScope(self._client, self.workspace_id, self.id, self.id) - - def observations_of(self, target: str | PeerBase) -> "AsyncObservationScope": + def conclusions_of(self, target: str | PeerBase) -> "AsyncConclusionScope": """ - Access observations this peer has made about another peer. + Access conclusions this peer has made about another peer. - This method provides scoped access to observations where this peer is the + This method provides scoped access to conclusions where this peer is the observer and the target is the observed peer. Args: target: The target peer (either an AsyncPeer object or peer ID string) Returns: - An AsyncObservationScope scoped to this peer's observations of the target + An AsyncConclusionScope scoped to this peer's conclusions of the target Example: ```python - # Get observations about another peer - bob_observations = peer.observations_of("bob") + # Get conclusions about another peer + bob_conclusions = peer.conclusions_of("bob") - # List observations - obs_list = await bob_observations.list() + # List conclusions + obs_list = await bob_conclusions.list() - # Search observations - results = await bob_observations.query("work history") + # Search conclusions + results = await bob_conclusions.query("work history") - # Get the representation from these observations - rep = await bob_observations.get_representation() + # Get the representation from these conclusions + rep = await bob_conclusions.get_representation() ``` """ - from ..observations import AsyncObservationScope as _AsyncObservationScope + from ..conclusions import AsyncConclusionScope as _AsyncConclusionScope target_id = target.id if isinstance(target, PeerBase) else target - return _AsyncObservationScope( + return _AsyncConclusionScope( self._client, self.workspace_id, self.id, target_id ) diff --git a/sdks/python/src/honcho/async_client/session.py b/sdks/python/src/honcho/async_client/session.py index b9a66aaf..f68c38c0 100644 --- a/sdks/python/src/honcho/async_client/session.py +++ b/sdks/python/src/honcho/async_client/session.py @@ -9,7 +9,10 @@ from typing import TYPE_CHECKING, Any from honcho_core import AsyncHoncho as AsyncHonchoCore from honcho_core._types import omit -from honcho_core.types import DeriverStatus +from honcho_core.types.workspaces import QueueStatusResponse +from honcho_core.types.workspaces.peer_representation_response import ( + PeerRepresentationResponse, +) 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 @@ -21,7 +24,6 @@ from ..utils import prepare_file_for_upload from .pagination import AsyncPage if TYPE_CHECKING: - from ..types import Representation from .peer import AsyncPeer logger = logging.getLogger(__name__) @@ -304,16 +306,14 @@ class AsyncSession(SessionBase): Get the configuration for a peer in this session. """ peer_id = peer if isinstance(peer, str) else peer.id - peer_get_config_response = ( - await self._client.workspaces.sessions.peers.get_config( - peer_id=peer_id, - workspace_id=self.workspace_id, - session_id=self.id, - ) + peer_config_response = await self._client.workspaces.sessions.peers.config( + peer_id=peer_id, + workspace_id=self.workspace_id, + session_id=self.id, ) return SessionPeerConfig( - observe_others=peer_get_config_response.observe_others, - observe_me=peer_get_config_response.observe_me, + observe_others=peer_config_response.observe_others, + observe_me=peer_config_response.observe_me, ) async def set_peer_config( @@ -400,7 +400,7 @@ class AsyncSession(SessionBase): Makes an async API call to permanently delete this session and all related data including: - Messages - Message embeddings - - Observations + - Conclusions - Session-Peer associations - Background processing queue items @@ -573,7 +573,7 @@ class AsyncSession(SessionBase): ), last_user_message: str | Message | None = Field( None, - description="The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.", + description="The most recent message (string or Message object), used to fetch semantically relevant conclusions and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.", ), peer_perspective: str | None = Field( None, @@ -581,7 +581,7 @@ class AsyncSession(SessionBase): ), limit_to_session: bool = Field( False, - description="Whether to limit the representation to this session only. If True, only observations from this session will be included.", + description="Whether to limit the representation to this session only. If True, only conclusions from this session will be included.", ), search_top_k: int | None = Field( None, @@ -595,15 +595,15 @@ class AsyncSession(SessionBase): le=1.0, description="Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.", ), - include_most_derived: bool | None = Field( + include_most_frequent: bool | None = Field( None, - description="Whether to include the most derived observations in the representation.", + description="Whether to include the most frequent conclusions in the representation.", ), - max_observations: int | None = Field( + max_conclusions: int | None = Field( None, ge=1, le=100, - description="Maximum number of observations to include in the representation.", + description="Maximum number of conclusions to include in the representation.", ), ) -> SessionContext: """ @@ -619,13 +619,13 @@ class AsyncSession(SessionBase): tokens: Maximum number of tokens to include in the context. Will default to Honcho server configuration if not provided. peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*. - last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided. + last_user_message: The most recent message (string or Message object), used to fetch semantically relevant conclusions and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided. peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`. - limit_to_session: Whether to limit the representation to this session only. If True, only observations from this session will be included. + limit_to_session: Whether to limit the representation to this session only. If True, only conclusions from this session will be included. search_top_k: Number of semantically relevant facts to return when searching with `last_user_message`. search_max_distance: Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`. - include_most_derived: Whether to include the most derived observations in the representation. - max_observations: Maximum number of observations to include in the representation. + include_most_frequent: Whether to include the most frequent conclusions in the representation. + max_conclusions: Maximum number of conclusions to include in the representation. Returns: A SessionContext object containing the optimized message history and @@ -652,7 +652,7 @@ class AsyncSession(SessionBase): if isinstance(last_user_message, Message) else last_user_message ) - context = await self._client.workspaces.sessions.get_context( + context = await self._client.workspaces.sessions.context( session_id=self.id, workspace_id=self.workspace_id, tokens=tokens if tokens is not None else omit, @@ -667,10 +667,10 @@ class AsyncSession(SessionBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations if max_observations is not None else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, ) # Convert the honcho_core summary to our Summary if it exists @@ -863,7 +863,7 @@ class AsyncSession(SessionBase): return [Message.model_validate(msg) for msg in response] - async def working_rep( + async def get_representation( self, peer: str | PeerBase, *, @@ -871,43 +871,42 @@ class AsyncSession(SessionBase): search_query: str | None = None, search_top_k: int | None = None, search_max_distance: float | None = None, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "Representation": + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> str: """ - Get the current working representation of the peer in this session. + Get a subset of the representation of the peer in this session. Args: - peer: Peer to get the working representation of. + peer: Peer to get the representation of. target: Optional target peer to get the representation of. If provided, queries what `peer` knows about the `target`. - search_query: Semantic search query to filter relevant observations + search_query: Semantic search query to filter relevant conclusions search_top_k: Number of semantically relevant facts to return search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include Returns: - A Representation object containing explicit and deductive observations + A Representation string Example: ```python # Get peer's representation in this session - rep = await session.working_rep('user123') + rep = await session.get_representation('user123') print(rep) # Get what user123 knows about assistant in this session - local_rep = await session.working_rep('user123', target='assistant') + local_rep = await session.get_representation('user123', target='assistant') # Get representation with semantic search - searched_rep = await session.working_rep( + searched_rep = await session.get_representation( 'user123', search_query='preferences', search_top_k=10 ) ``` """ - from ..types import Representation as _Representation peer_id = peer if isinstance(peer, str) else peer.id target_id = ( @@ -915,31 +914,35 @@ class AsyncSession(SessionBase): if target is None else (target if isinstance(target, str) else target.id) ) - data = await self._client.workspaces.peers.working_representation( - peer_id, - workspace_id=self.workspace_id, - session_id=self.id, - 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 - if search_max_distance is not None - else omit, - include_most_derived=include_most_derived - if include_most_derived is not None - else omit, - max_observations=max_observations if max_observations is not None else omit, + data: PeerRepresentationResponse = ( + await self._client.workspaces.peers.representation( + peer_id, + workspace_id=self.workspace_id, + session_id=self.id, + 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 + if search_max_distance is not None + else omit, + include_most_frequent=include_most_frequent + if include_most_frequent is not None + else omit, + max_conclusions=max_conclusions + if max_conclusions is not None + else omit, + ) ) - return _Representation.from_dict(data) # type: ignore + return data.representation @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - async def get_deriver_status( + async def get_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Get the deriver processing status, optionally scoped to an observer, sender, and/or session. + Get the queue 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 @@ -956,7 +959,7 @@ class AsyncSession(SessionBase): else (sender if isinstance(sender, str) else sender.id) ) - return await self._client.workspaces.deriver_status( + return await self._client.workspaces.queue.status( workspace_id=self.workspace_id, observer_id=resolved_observer_id, sender_id=resolved_sender_id, @@ -964,7 +967,7 @@ class AsyncSession(SessionBase): ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - async def poll_deriver_status( + async def poll_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, @@ -973,10 +976,10 @@ class AsyncSession(SessionBase): gt=0, description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).", ), - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0. - This allows you to guarantee that all messages have been processed by the deriver for + Poll get_queue_status until pending_work_units and in_progress_work_units are both 0. + This allows you to guarantee that all messages have been processed by the queue for use with the dialectic endpoint. The polling estimates sleep time by assuming each work unit takes 1 second. @@ -987,19 +990,19 @@ class AsyncSession(SessionBase): timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds). Returns: - DeriverStatus when all work units are complete + QueueStatusResponse when all work units are complete Raises: TimeoutError: If timeout is exceeded before work units complete - Exception: If get_deriver_status fails repeatedly + Exception: If get_queue_status fails repeatedly """ start_time = time.time() while True: try: - status = await self.get_deriver_status(observer, sender) + status = await self.get_queue_status(observer, sender) except Exception as e: - logger.warning(f"Failed to get deriver status: {e}") + logger.warning(f"Failed to get queue status: {e}") # Sleep briefly before retrying await asyncio.sleep(1) diff --git a/sdks/python/src/honcho/client.py b/sdks/python/src/honcho/client.py index ae387836..75711906 100644 --- a/sdks/python/src/honcho/client.py +++ b/sdks/python/src/honcho/client.py @@ -6,7 +6,7 @@ from typing import Any, Literal import httpx from honcho_core import Honcho as HonchoCore -from honcho_core.types import DeriverStatus, Workspace +from honcho_core.types.workspaces import QueueStatusResponse from honcho_core.types.workspaces.peer import Peer as PeerCore from honcho_core.types.workspaces.session import Session as SessionCore from honcho_core.types.workspaces.sessions.message import Message @@ -403,19 +403,16 @@ class Honcho(BaseModel): workspace_id: str = Field( ..., min_length=1, description="ID of the workspace to delete" ), - ) -> Workspace: + ) -> None: """ Delete a workspace. - Makes an API call to delete the specified workspace. + Makes an API call to delete the specified workspace. This action cannot be undone. Args: workspace_id: The ID of the workspace to delete - - Returns: - The deleted Workspace object """ - return self._client.workspaces.delete(workspace_id) + self._client.workspaces.delete(workspace_id) @validate_call def search( @@ -447,14 +444,14 @@ class Honcho(BaseModel): ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - def get_deriver_status( + def get_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, session: str | SessionBase | None = None, - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Get the deriver processing status, optionally scoped to an observer, sender, and/or session. + Get the queue 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 @@ -477,7 +474,7 @@ class Honcho(BaseModel): else (session if isinstance(session, str) else session.id) ) - return self._client.workspaces.deriver_status( + return self._client.workspaces.queue.status( workspace_id=self.workspace_id, observer_id=resolved_observer_id, sender_id=resolved_sender_id, @@ -485,7 +482,7 @@ class Honcho(BaseModel): ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - def poll_deriver_status( + def poll_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, @@ -495,10 +492,10 @@ class Honcho(BaseModel): gt=0, description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).", ), - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0. - This allows you to guarantee that all messages have been processed by the deriver for + Poll get_queue_status until pending_work_units and in_progress_work_units are both 0. + This allows you to guarantee that all messages have been processed by the queue for use with the dialectic endpoint. The polling estimates sleep time by assuming each work unit takes 1 second. @@ -510,19 +507,19 @@ class Honcho(BaseModel): timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds). Returns: - DeriverStatus when all work units are complete + QueueStatusResponse when all work units are complete Raises: TimeoutError: If timeout is exceeded before work units complete - Exception: If get_deriver_status fails repeatedly + Exception: If get_queue_status fails repeatedly """ start_time = time.time() while True: try: - status = self.get_deriver_status(observer, sender, session) + status = self.get_queue_status(observer, sender, session) except Exception as e: - logger.warning(f"Failed to get deriver status: {e}") + logger.warning(f"Failed to get queue status: {e}") # Sleep briefly before retrying time.sleep(1) @@ -565,44 +562,44 @@ class Honcho(BaseModel): time.sleep(sleep_time) @validate_call - def list_observations( + def list_conclusions( self, filters: dict[str, object] | None = Field( - None, description="Filters to scope the observations" + None, description="Filters to scope the conclusions" ), reverse: bool = Field( False, description="Whether to reverse the order of results" ), ): """ - List all observations in the current workspace with optional filtering. + List all conclusions in the current workspace with optional filtering. - Makes an API call to retrieve observations that match the specified filters. - Observations can be filtered by session_id, observer_id, and observed_id. + Makes an API call to retrieve conclusions that match the specified filters. + conclusions can be filtered by session_id, observer_id, and observed_id. Args: - filters: Optional filter criteria for observations. Supported filters include: - - session_id: Filter observations by session - - observer_id: Filter observations by observer peer - - observed_id: Filter observations by observed peer + filters: Optional filter criteria for conclusions. Supported filters include: + - session_id: Filter conclusions by session + - observer_id: Filter conclusions by observer peer + - observed_id: Filter conclusions by observed peer reverse: Whether to reverse the order of results (default: False) Returns: - A paginated list of Observation objects matching the specified criteria + A paginated list of conclusion objects matching the specified criteria Example: - >>> observations = client.list_observations( + >>> conclusions = client.list_conclusions( ... filters={"observer_id": "user123", "observed_id": "assistant"} ... ) """ - return self._client.workspaces.observations.list( + return self._client.workspaces.conclusions.list( workspace_id=self.workspace_id, filters=filters, reverse=reverse, ) @validate_call - def query_observations( + def query_conclusions( self, query: str = Field(..., min_length=1, description="Semantic search query"), observer: str = Field( @@ -625,9 +622,9 @@ class Honcho(BaseModel): ), ): """ - Query observations using semantic search. + Query conclusions using semantic search. - Performs vector similarity search on observations to find semantically relevant results. + Performs vector similarity search on conclusions to find semantically relevant results. Observer and observed peer IDs are required for semantic search. Args: @@ -639,10 +636,10 @@ class Honcho(BaseModel): filters: Optional filters to scope the query Returns: - A list of Observation objects matching the query + A list of conclusion objects matching the query Example: - >>> observations = client.query_observations( + >>> conclusions = client.query_conclusions( ... query="user preferences about music", ... observer="user123", ... observed="assistant", @@ -657,7 +654,7 @@ class Honcho(BaseModel): "observed": observed, } - return self._client.workspaces.observations.query( + return self._client.workspaces.conclusions.query( workspace_id=self.workspace_id, query=query, top_k=top_k, @@ -666,27 +663,27 @@ class Honcho(BaseModel): ) @validate_call - def delete_observation( + def delete_conclusion( self, - observation_id: str = Field( - ..., min_length=1, description="ID of the observation to delete" + conclusion_id: str = Field( + ..., min_length=1, description="ID of the conclusion to delete" ), ) -> None: """ - Delete a specific observation by ID. + Delete a specific conclusion by ID. - This permanently deletes the observation (document) from the theory-of-mind system. + This permanently deletes the conclusion (document) from the theory-of-mind system. This action cannot be undone. Args: - observation_id: The ID of the observation to delete + conclusion_id: The ID of the conclusion to delete Example: - >>> client.delete_observation('obs_123abc') + >>> client.delete_conclusion('obs_123abc') """ - self._client.workspaces.observations.delete( + self._client.workspaces.conclusions.delete( workspace_id=self.workspace_id, - observation_id=observation_id, + conclusion_id=conclusion_id, ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) diff --git a/sdks/python/src/honcho/conclusions.py b/sdks/python/src/honcho/conclusions.py new file mode 100644 index 00000000..97aaf7c4 --- /dev/null +++ b/sdks/python/src/honcho/conclusions.py @@ -0,0 +1,463 @@ +"""Conclusion types and scoped access for the Honcho SDK.""" + +from __future__ import annotations + +from typing import Any + +from honcho_core import AsyncHoncho as AsyncHonchoCore +from honcho_core import Honcho as HonchoCore +from honcho_core.pagination import AsyncPage, SyncPage +from honcho_core.types.workspaces import conclusion_create_params +from honcho_core.types.workspaces.conclusion import Conclusion +from pydantic import BaseModel, PrivateAttr +from typing_extensions import TypeAlias + +from .base import SessionBase + +__all__ = [ + "Conclusion", + "ConclusionCreateResponse", + "ConclusionScope", + "ConclusionCreateParams", + "AsyncConclusionScope", +] + +ConclusionCreateResponse: TypeAlias = list[Conclusion] + + +class ConclusionCreateParams(BaseModel): + content: str + session_id: str + + +class ConclusionScope: + """ + Scoped access to conclusions for a specific observer/observed relationship. + + This class provides convenient methods to list, query, create, and delete conclusions + that are automatically scoped to a specific observer/observed pair. + + Typically accessed via `peer.conclusions` (for self-conclusions) or + `peer.conclusions_of(target)` (for conclusions about another peer). + + Example: + ```python + # Get self-conclusions + conclusions = peer.conclusions + obs_list = conclusions.list() + search_results = conclusions.query("preferences") + + # Get conclusions about another peer + bob_conclusions = peer.conclusions_of("bob") + bob_list = bob_conclusions.list() + ``` + """ + + _client: HonchoCore = PrivateAttr() + workspace_id: str + observer: str + observed: str + + def __init__( + self, + client: HonchoCore, + workspace_id: str, + observer: str, + observed: str, + ): + """ + Initialize a ConclusionScope. + + Args: + client: The Honcho client instance + workspace_id: The workspace ID + observer: The observer peer ID + observed: The observed peer ID + """ + self._client = client + self.workspace_id = workspace_id + self.observer = observer + self.observed = observed + + def list( + self, + page: int = 1, + size: int = 50, + session: str | SessionBase | None = None, + ) -> SyncPage[Conclusion]: + """ + List conclusions in this scope. + + Args: + page: Page number (1-indexed) + size: Number of results per page + session: Optional session (ID string or Session object) to filter by + + Returns: + Paginated response containing Conclusion 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 resolved_session_id: + filters["session_id"] = resolved_session_id + + return self._client.workspaces.conclusions.list( + workspace_id=self.workspace_id, + filters=filters, + page=page, + size=size, + ) + + def query( + self, + query: str, + top_k: int = 10, + distance: float | None = None, + ) -> list[Conclusion]: + """ + Semantic search for conclusions in this scope. + + Args: + query: The search query string + top_k: Maximum number of results to return + distance: Maximum cosine distance threshold (0.0-1.0) + + Returns: + List of matching Conclusion objects + """ + filters: dict[str, Any] = { + "observer": self.observer, + "observed": self.observed, + } + + return self._client.workspaces.conclusions.query( + workspace_id=self.workspace_id, + query=query, + top_k=top_k, + distance=distance, + filters=filters, + ) + + def delete(self, conclusion_id: str) -> None: + """ + Delete a conclusion by ID. + + Args: + conclusion_id: The ID of the conclusion to delete + """ + self._client.workspaces.conclusions.delete( + workspace_id=self.workspace_id, + conclusion_id=conclusion_id, + ) + + def create( + self, + conclusions: list[ConclusionCreateParams | dict[str, Any]], + ) -> list[Conclusion]: + """ + Create conclusions in this scope. + + Args: + conclusions: List of conclusions to create. + Each conclusion can be a ConclusionCreateParams object or a dictionary with 'content' and 'session_id' keys. + + Returns: + List of created Conclusion objects + + Example: + ```python + conclusions = peer.conclusions.create([ + {"content": "User prefers dark mode", "session_id": "session1"}, + {"content": "User is interested in AI", "session_id": "session1"}, + ]) + ``` + """ + + return self._client.workspaces.conclusions.create( + workspace_id=self.workspace_id, + conclusions=[ + conclusion_create_params.Conclusion( + content=conclusion.content + if isinstance(conclusion, ConclusionCreateParams) + else conclusion["content"], + session_id=conclusion.session_id + if isinstance(conclusion, ConclusionCreateParams) + else conclusion["session_id"], + observer_id=self.observer, + observed_id=self.observed, + ) + for conclusion in conclusions + ], + ) + + def get_representation( + self, + search_query: str | None = None, + search_top_k: int | None = None, + search_max_distance: float | None = None, + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> str: + """ + Get the computed representation for this scope. + + This returns the working representation (narrative) built from the + conclusions in this scope. + + Args: + search_query: Optional semantic search query to curate the representation + search_top_k: Number of semantically relevant facts to return + search_max_distance: Maximum semantic distance for search results (0.0-1.0) + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include + + Returns: + A Representation string + """ + from honcho_core._types import omit + + response = self._client.workspaces.peers.representation( + peer_id=self.observer, + workspace_id=self.workspace_id, + target=self.observed, + 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 + if search_max_distance is not None + else omit, + include_most_frequent=include_most_frequent + if include_most_frequent is not None + else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, + ) + + return response.representation + + def __repr__(self) -> str: + return ( + f"ConclusionScope(workspace_id={self.workspace_id!r}, " + f"observer={self.observer!r}, observed={self.observed!r})" + ) + + +class AsyncConclusionScope: + """ + Async scoped access to conclusions for a specific observer/observed relationship. + + This class provides convenient async methods to list, query, create, and delete conclusions + that are automatically scoped to a specific observer/observed pair. + + Typically accessed via `peer.conclusions` (for self-conclusions) or + `peer.conclusions_of(target)` (for conclusions about another peer). + + Example: + ```python + # Get self-conclusions + conclusions = peer.conclusions + obs_list = await conclusions.list() + search_results = await conclusions.query("preferences") + + # Get conclusions about another peer + bob_conclusions = peer.conclusions_of("bob") + bob_list = await bob_conclusions.list() + ``` + """ + + _client: AsyncHonchoCore = PrivateAttr() + workspace_id: str + observer: str + observed: str + + def __init__( + self, + client: AsyncHonchoCore, + workspace_id: str, + observer: str, + observed: str, + ): + """ + Initialize an AsyncConclusionScope. + + Args: + client: The AsyncHoncho client instance + workspace_id: The workspace ID + observer: The observer peer ID + observed: The observed peer ID + """ + self._client = client + self.workspace_id = workspace_id + self.observer = observer + self.observed = observed + + async def list( + self, + page: int = 1, + size: int = 50, + session: str | SessionBase | None = None, + ) -> AsyncPage[Conclusion]: + """ + List conclusions in this scope. + + Args: + page: Page number (1-indexed) + size: Number of results per page + session: Optional session (ID string or AsyncSession object) to filter by + + Returns: + Paginated response containing Conclusion 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 resolved_session_id: + filters["session_id"] = resolved_session_id + + return await self._client.workspaces.conclusions.list( + workspace_id=self.workspace_id, + filters=filters, + page=page, + size=size, + ) + + async def query( + self, + query: str, + top_k: int = 10, + distance: float | None = None, + ) -> list[Conclusion]: + """ + Semantic search for conclusions in this scope. + + Args: + query: The search query string + top_k: Maximum number of results to return + distance: Maximum cosine distance threshold (0.0-1.0) + + Returns: + List of matching Conclusion objects + """ + filters: dict[str, Any] = { + "observer": self.observer, + "observed": self.observed, + } + + return await self._client.workspaces.conclusions.query( + workspace_id=self.workspace_id, + query=query, + top_k=top_k, + distance=distance, + filters=filters, + ) + + async def delete(self, conclusion_id: str) -> None: + """ + Delete a conclusion by ID. + + Args: + conclusion_id: The ID of the conclusion to delete + """ + await self._client.workspaces.conclusions.delete( + workspace_id=self.workspace_id, + conclusion_id=conclusion_id, + ) + + async def create( + self, + conclusions: list[ConclusionCreateParams | dict[str, Any]], + ) -> list[Conclusion]: + """ + Create conclusions in this scope. + + Args: + conclusions: List of conclusions to create. + Each conclusion can be a ConclusionCreateParams object or a dictionary with 'content' and 'session_id' keys. + + Returns: + List of created Conclusion objects + + Example: + ```python + conclusions = await peer.conclusions.create([ + {"content": "User prefers dark mode", "session_id": "session1"}, + {"content": "User is interested in AI", "session_id": "session1"}, + ]) + ``` + """ + return await self._client.workspaces.conclusions.create( + workspace_id=self.workspace_id, + conclusions=[ + conclusion_create_params.Conclusion( + content=conclusion.content + if isinstance(conclusion, ConclusionCreateParams) + else conclusion["content"], + session_id=conclusion.session_id + if isinstance(conclusion, ConclusionCreateParams) + else conclusion["session_id"], + observer_id=self.observer, + observed_id=self.observed, + ) + for conclusion in conclusions + ], + ) + + async def get_representation( + self, + search_query: str | None = None, + search_top_k: int | None = None, + search_max_distance: float | None = None, + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> str: + """ + Get the computed representation for this scope. + + This returns the working representation (narrative) built from the + conclusions in this scope. + + Args: + search_query: Optional semantic search query to curate the representation + search_top_k: Number of semantically relevant facts to return + search_max_distance: Maximum semantic distance for search results (0.0-1.0) + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include + + Returns: + A Representation string + """ + from honcho_core._types import omit + + response = await self._client.workspaces.peers.representation( + peer_id=self.observer, + workspace_id=self.workspace_id, + target=self.observed, + 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 + if search_max_distance is not None + else omit, + include_most_frequent=include_most_frequent + if include_most_frequent is not None + else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, + ) + + return response.representation + + def __repr__(self) -> str: + return ( + f"AsyncConclusionScope(workspace_id={self.workspace_id!r}, " + f"observer={self.observer!r}, observed={self.observed!r})" + ) diff --git a/sdks/python/src/honcho/observations.py b/sdks/python/src/honcho/observations.py deleted file mode 100644 index 69f1fd8f..00000000 --- a/sdks/python/src/honcho/observations.py +++ /dev/null @@ -1,589 +0,0 @@ -"""Observation types and scoped access for the Honcho SDK.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, cast - -from .base import SessionBase - -if TYPE_CHECKING: - 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: - """ - An observation from the theory-of-mind system. - - Observations are facts derived from messages that help build a representation - of a peer. - - Attributes: - id: Unique identifier for this observation - content: The observation content/text - observer_id: The peer who made the observation - observed_id: The peer being observed - session_id: The session where this observation was made - created_at: When the observation was created - """ - - id: str - content: str - observer_id: str - observed_id: str - session_id: str - created_at: str - - def __init__( - self, - id: str, - content: str, - observer_id: str, - observed_id: str, - session_id: str, - created_at: str, - ): - self.id = id - self.content = content - self.observer_id = observer_id - self.observed_id = observed_id - self.session_id = session_id - self.created_at = created_at - - @classmethod - def from_api_response(cls, data: dict[str, Any]) -> "Observation": - """Create an Observation from an API response dict.""" - return cls( - id=data.get("id", ""), - content=data.get("content", ""), - observer_id=data.get("observer_id", ""), - observed_id=data.get("observed_id", ""), - session_id=data.get("session_id", ""), - created_at=str(data.get("created_at", "")), - ) - - def __repr__(self) -> str: - truncated = ( - f"{self.content[:50]}..." if len(self.content) > 50 else self.content - ) - return f"Observation(id={self.id!r}, content={truncated!r})" - - -class ObservationScope: - """ - Scoped access to observations for a specific observer/observed relationship. - - 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 - `peer.observations_of(target)` (for observations about another peer). - - Example: - ```python - # Get self-observations - observations = peer.observations - obs_list = observations.list() - search_results = observations.query("preferences") - - # Get observations about another peer - bob_observations = peer.observations_of("bob") - bob_list = bob_observations.list() - ``` - """ - - _client: Any - workspace_id: str - observer: str - observed: str - - def __init__( - self, - client: Any, - workspace_id: str, - observer: str, - observed: str, - ): - """ - Initialize an ObservationScope. - - Args: - client: The Honcho client instance - workspace_id: The workspace ID - observer: The observer peer ID - observed: The observed peer ID - """ - self._client = client - self.workspace_id = workspace_id - self.observer = observer - self.observed = observed - - def list( - self, - page: int = 1, - size: int = 50, - session: str | SessionBase | None = None, - ) -> list[Observation]: - """ - List observations in this scope. - - Args: - page: Page number (1-indexed) - size: Number of results per page - 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 resolved_session_id: - filters["session_id"] = resolved_session_id - - response = self._client.workspaces.observations.list( - workspace_id=self.workspace_id, - filters=filters, - page=page, - size=size, - ) - - # response.items is List[Observations] (Pydantic models) - return [ - Observation.from_api_response(_convert_observation(item)) - for item in response.items - ] - - def query( - self, - query: str, - top_k: int = 10, - distance: float | None = None, - ) -> list[Observation]: - """ - Semantic search for observations in this scope. - - Args: - query: The search query string - top_k: Maximum number of results to return - distance: Maximum cosine distance threshold (0.0-1.0) - - Returns: - List of matching Observation objects - """ - filters: dict[str, Any] = { - "observer": self.observer, - "observed": self.observed, - } - - response = self._client.workspaces.observations.query( - workspace_id=self.workspace_id, - query=query, - top_k=top_k, - distance=distance, - filters=filters, - ) - - # 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: - """ - Delete an observation by ID. - - Args: - observation_id: The ID of the observation to 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, - search_top_k: int | None = None, - search_max_distance: float | None = None, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "Representation": - """ - Get the computed representation for this scope. - - This returns the working representation (narrative) built from the - observations in this scope. - - Args: - search_query: Optional semantic search query to curate the representation - search_top_k: Number of semantically relevant facts to return - search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include - - Returns: - A Representation object containing explicit and deductive observations - """ - from honcho_core._types import omit - - from .types import Representation - - response = self._client.workspaces.peers.working_representation( - peer_id=self.observer, - workspace_id=self.workspace_id, - target=self.observed, - 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 - if search_max_distance is not None - else omit, - include_most_derived=include_most_derived - if include_most_derived is not None - else omit, - max_observations=max_observations if max_observations is not None else omit, - ) - - representation = response.get("representation") - if representation is not None: - return Representation.from_dict(cast(dict[str, Any], representation)) - else: - return Representation.from_dict(response) - - def __repr__(self) -> str: - return ( - f"ObservationScope(workspace_id={self.workspace_id!r}, " - f"observer={self.observer!r}, observed={self.observed!r})" - ) - - -class AsyncObservationScope: - """ - Async scoped access to observations for a specific observer/observed relationship. - - 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 - `peer.observations_of(target)` (for observations about another peer). - - Example: - ```python - # Get self-observations - observations = peer.observations - obs_list = await observations.list() - search_results = await observations.query("preferences") - - # Get observations about another peer - bob_observations = peer.observations_of("bob") - bob_list = await bob_observations.list() - ``` - """ - - _client: Any - workspace_id: str - observer: str - observed: str - - def __init__( - self, - client: Any, - workspace_id: str, - observer: str, - observed: str, - ): - """ - Initialize an AsyncObservationScope. - - Args: - client: The AsyncHoncho client instance - workspace_id: The workspace ID - observer: The observer peer ID - observed: The observed peer ID - """ - self._client = client - self.workspace_id = workspace_id - self.observer = observer - self.observed = observed - - async def list( - self, - page: int = 1, - size: int = 50, - session: str | SessionBase | None = None, - ) -> list[Observation]: - """ - List observations in this scope. - - Args: - page: Page number (1-indexed) - size: Number of results per page - 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 resolved_session_id: - filters["session_id"] = resolved_session_id - - response = await self._client.workspaces.observations.list( - workspace_id=self.workspace_id, - filters=filters, - page=page, - size=size, - ) - - # response.items is List[Observations] (Pydantic models) - return [ - Observation.from_api_response(_convert_observation(item)) - for item in response.items - ] - - async def query( - self, - query: str, - top_k: int = 10, - distance: float | None = None, - ) -> list[Observation]: - """ - Semantic search for observations in this scope. - - Args: - query: The search query string - top_k: Maximum number of results to return - distance: Maximum cosine distance threshold (0.0-1.0) - - Returns: - List of matching Observation objects - """ - filters: dict[str, Any] = { - "observer": self.observer, - "observed": self.observed, - } - - response = await self._client.workspaces.observations.query( - workspace_id=self.workspace_id, - query=query, - top_k=top_k, - distance=distance, - filters=filters, - ) - - # 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: - """ - Delete an observation by ID. - - Args: - observation_id: The ID of the observation to 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, - search_top_k: int | None = None, - search_max_distance: float | None = None, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "Representation": - """ - Get the computed representation for this scope. - - This returns the working representation (narrative) built from the - observations in this scope. - - Args: - search_query: Optional semantic search query to curate the representation - search_top_k: Number of semantically relevant facts to return - search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include - - Returns: - A Representation object containing explicit and deductive observations - """ - from honcho_core._types import omit - - from .types import Representation - - response = await self._client.workspaces.peers.working_representation( - peer_id=self.observer, - workspace_id=self.workspace_id, - target=self.observed, - 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 - if search_max_distance is not None - else omit, - include_most_derived=include_most_derived - if include_most_derived is not None - else omit, - max_observations=max_observations if max_observations is not None else omit, - ) - - representation = response.get("representation") - if representation is not None: - return Representation.from_dict(cast(dict[str, Any], representation)) - else: - return Representation.from_dict(response) - - def __repr__(self) -> str: - return ( - f"AsyncObservationScope(workspace_id={self.workspace_id!r}, " - f"observer={self.observer!r}, observed={self.observed!r})" - ) diff --git a/sdks/python/src/honcho/peer.py b/sdks/python/src/honcho/peer.py index ac7f49ee..da0d90f2 100644 --- a/sdks/python/src/honcho/peer.py +++ b/sdks/python/src/honcho/peer.py @@ -2,11 +2,17 @@ from __future__ import annotations import datetime from collections.abc import Generator -from typing import TYPE_CHECKING, cast +from typing import Literal from honcho_core import Honcho as HonchoCore from honcho_core._types import omit from honcho_core.types.workspaces import PeerCardResponse +from honcho_core.types.workspaces.peer_context_response import ( + PeerContextResponse, +) +from honcho_core.types.workspaces.peer_representation_response import ( + PeerRepresentationResponse, +) 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 @@ -14,14 +20,10 @@ from honcho_core.types.workspaces.sessions.message_create_param import Configura from pydantic import ConfigDict, Field, PrivateAttr, validate_call from .base import PeerBase, SessionBase +from .conclusions import ConclusionScope from .pagination import SyncPage -from .types import DialecticStreamResponse - -if TYPE_CHECKING: - from .observations import ObservationScope - from .types import PeerContext, Representation - from .session import Session +from .types import DialecticStreamResponse class Peer(PeerBase): @@ -120,6 +122,8 @@ class Peer(PeerBase): stream: bool = False, target: str | PeerBase | None = None, session: str | SessionBase | None = None, + reasoning_level: Literal["minimal", "low", "medium", "high", "extra-high"] + | None = None, ) -> str | DialecticStreamResponse | None: """ Query the peer's representation with a natural language question. @@ -138,6 +142,8 @@ class Peer(PeerBase): 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. + reasoning_level: Optional reasoning level for the query: "minimal", "low", "medium", + "high", or "extra-high". Defaults to "low" if not provided. Returns: For non-streaming: Response string containing the answer, or None if no relevant information @@ -168,6 +174,9 @@ class Peer(PeerBase): stream=True, target=target_id, session_id=resolved_session_id, + reasoning_level=reasoning_level + if reasoning_level is not None + else omit, ) as response: response.http_response.raise_for_status() for line in response.iter_lines(): @@ -193,6 +202,7 @@ class Peer(PeerBase): stream=stream, target=target_id, session_id=resolved_session_id, + reasoning_level=reasoning_level if reasoning_level is not None else omit, ) if response.content in ("", None, "None"): return None @@ -474,50 +484,49 @@ class Peer(PeerBase): return "\n".join(items) - def working_rep( + def get_representation( self, 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, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "Representation": + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> str: """ - Get a working representation for this peer. + Get a subset of the representation of the peer. Args: session: Optional session to scope the representation to. target: Optional target peer to get the representation of. If provided, returns the representation of the target from the perspective of this peer. - search_query: Semantic search query to filter relevant observations + search_query: Semantic search query to filter relevant conclusions search_top_k: Number of semantically relevant facts to return search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include Returns: - A Representation object containing explicit and deductive observations + A Representation string Example: ```python # Get global representation - rep = peer.working_rep() + rep = peer.get_representation() print(rep) # Get representation scoped to a session - session_rep = peer.working_rep(session='session-123') + session_rep = peer.get_representation(session='session-123') # Get representation with semantic search - searched_rep = peer.working_rep( + searched_rep = peer.get_representation( search_query='preferences', search_top_k=10, - max_observations=50 + max_conclusions=50 ) ``` """ - from .types import Representation as _Representation session_id = ( None @@ -532,7 +541,7 @@ class Peer(PeerBase): if target is None else (target if isinstance(target, str) else target.id) ) - data = self._client.workspaces.peers.working_representation( + data: PeerRepresentationResponse = self._client.workspaces.peers.representation( peer_id=self.id, workspace_id=self.workspace_id, session_id=session_id, @@ -542,16 +551,12 @@ class Peer(PeerBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations if max_observations is not None else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, ) - representation = data.get("representation") - if representation is not None: - return _Representation.from_dict(cast(dict[str, object], representation)) - else: - return _Representation.from_dict(data) + return data.representation def get_context( self, @@ -559,9 +564,9 @@ class Peer(PeerBase): search_query: str | None = None, search_top_k: int | None = None, search_max_distance: float | None = None, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "PeerContext": + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> PeerContextResponse: """ Get context for this peer, including representation and peer card. @@ -572,11 +577,11 @@ class Peer(PeerBase): target: Optional target peer to get context for. If provided, returns the context for the target from this peer's perspective. Can be a Peer object or peer ID string. - search_query: Semantic search query to filter relevant observations + search_query: Semantic search query to filter relevant conclusions search_top_k: Number of semantically relevant facts to return search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include Returns: A PeerContext object containing the representation and peer card @@ -598,7 +603,6 @@ class Peer(PeerBase): ) ``` """ - from .types import PeerContext as _PeerContext target_id = ( None @@ -606,7 +610,7 @@ class Peer(PeerBase): else (target if isinstance(target, str) else target.id) ) - response = self._client.workspaces.peers.get_context( + return self._client.workspaces.peers.context( peer_id=self.id, workspace_id=self.workspace_id, target=target_id, @@ -615,73 +619,71 @@ class Peer(PeerBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations if max_observations is not None else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, ) - return _PeerContext.from_api_response(response) - @property - def observations(self) -> "ObservationScope": + def conclusions(self) -> "ConclusionScope": """ - Access this peer's self-observations (where observer == observed == self). + Access this peer's self-conclusions (where observer == observed == self). - This property provides a convenient way to access observations that this peer - has made about themselves. Use this for self-observation scenarios. + This property provides a convenient way to access conclusions that this peer + has made about themselves. Use this for self-conclusion scenarios. Returns: - An ObservationScope scoped to this peer's self-observations + A ConclusionScope scoped to this peer's self-conclusions Example: ```python - # List self-observations - obs_list = peer.observations.list() + # List self-conclusions + obs_list = peer.conclusions.list() - # Search self-observations - results = peer.observations.query("preferences") + # Search self-conclusions + results = peer.conclusions.query("preferences") - # Delete a self-observation - peer.observations.delete("obs-123") + # Delete a self-conclusion + peer.conclusions.delete("obs-123") ``` """ - from .observations import ObservationScope as _ObservationScope + from .conclusions import ConclusionScope as _ConclusionScope - return _ObservationScope(self._client, self.workspace_id, self.id, self.id) + return _ConclusionScope(self._client, self.workspace_id, self.id, self.id) - def observations_of(self, target: str | PeerBase) -> "ObservationScope": + def conclusions_of(self, target: str | PeerBase) -> "ConclusionScope": """ - Access observations this peer has made about another peer. + Access conclusions this peer has made about another peer. - This method provides scoped access to observations where this peer is the + This method provides scoped access to conclusions where this peer is the observer and the target is the observed peer. Args: target: The target peer (either a Peer object or peer ID string) Returns: - An ObservationScope scoped to this peer's observations of the target + A ConclusionScope scoped to this peer's conclusions of the target Example: ```python - # Get observations about another peer - bob_observations = peer.observations_of("bob") + # Get conclusions about another peer + bob_conclusions = peer.conclusions_of("bob") - # List observations - obs_list = bob_observations.list() + # List conclusions + obs_list = bob_conclusions.list() - # Search observations - results = bob_observations.query("work history") + # Search conclusions + results = bob_conclusions.query("work history") - # Get the representation from these observations - rep = bob_observations.get_representation() + # Get the representation from these conclusions + rep = bob_conclusions.get_representation() ``` """ - from .observations import ObservationScope as _ObservationScope + from .conclusions import ConclusionScope as _ConclusionScope target_id = target.id if isinstance(target, PeerBase) else target - return _ObservationScope(self._client, self.workspace_id, self.id, target_id) + return _ConclusionScope(self._client, self.workspace_id, self.id, target_id) def __repr__(self) -> str: """ diff --git a/sdks/python/src/honcho/session.py b/sdks/python/src/honcho/session.py index 4e023698..4e48edd3 100644 --- a/sdks/python/src/honcho/session.py +++ b/sdks/python/src/honcho/session.py @@ -8,7 +8,10 @@ from typing import TYPE_CHECKING, Any from honcho_core import Honcho as HonchoCore from honcho_core._types import omit -from honcho_core.types import DeriverStatus +from honcho_core.types.workspaces import QueueStatusResponse +from honcho_core.types.workspaces.peer_representation_response import ( + PeerRepresentationResponse, +) 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 @@ -21,7 +24,6 @@ from .utils import prepare_file_for_upload if TYPE_CHECKING: from .peer import Peer - from .types import Representation logger = logging.getLogger(__name__) @@ -279,14 +281,14 @@ class Session(SessionBase): Get the configuration for a peer in this session. """ peer_id = peer if isinstance(peer, str) else peer.id - peer_get_config_response = self._client.workspaces.sessions.peers.get_config( + peer_config_response = self._client.workspaces.sessions.peers.config( peer_id=peer_id, workspace_id=self.workspace_id, session_id=self.id, ) return SessionPeerConfig( - observe_others=peer_get_config_response.observe_others, - observe_me=peer_get_config_response.observe_me, + observe_others=peer_config_response.observe_others, + observe_me=peer_config_response.observe_me, ) def set_peer_config(self, peer: str | PeerBase, config: SessionPeerConfig) -> None: @@ -390,7 +392,7 @@ class Session(SessionBase): Makes an API call to permanently delete this session and all related data including: - Messages - Message embeddings - - Observations + - Conclusions - Session-Peer associations - Background processing queue items @@ -544,7 +546,7 @@ class Session(SessionBase): ), last_user_message: str | Message | None = Field( None, - description="The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.", + description="The most recent message (string or Message object), used to fetch semantically relevant conclusions and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.", ), peer_perspective: str | None = Field( None, @@ -552,7 +554,7 @@ class Session(SessionBase): ), limit_to_session: bool = Field( False, - description="Whether to limit the representation to this session only. If True, only observations from this session will be included.", + description="Whether to limit the representation to this session only. If True, only conclusions from this session will be included.", ), search_top_k: int | None = Field( None, @@ -566,15 +568,15 @@ class Session(SessionBase): le=1.0, description="Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.", ), - include_most_derived: bool | None = Field( + include_most_frequent: bool | None = Field( None, - description="Whether to include the most derived observations in the representation.", + description="Whether to include the most frequent conclusions in the representation.", ), - max_observations: int | None = Field( + max_conclusions: int | None = Field( None, ge=1, le=100, - description="Maximum number of observations to include in the representation.", + description="Maximum number of conclusions to include in the representation.", ), ) -> SessionContext: """ @@ -590,13 +592,13 @@ class Session(SessionBase): tokens: Maximum number of tokens to include in the context. Will default to Honcho server configuration if not provided. peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*. - last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided. + last_user_message: The most recent message (string or Message object), used to fetch semantically relevant conclusions and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided. peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`. - limit_to_session: Whether to limit the representation to this session only. If True, only observations from this session will be included. + limit_to_session: Whether to limit the representation to this session only. If True, only conclusions from this session will be included. search_top_k: Number of semantically relevant facts to return when searching with `last_user_message`. search_max_distance: Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`. - include_most_derived: Whether to include the most derived observations in the representation. - max_observations: Maximum number of observations to include in the representation. + include_most_frequent: Whether to include the most frequent conclusions in the representation. + max_conclusions: Maximum number of conclusions to include in the representation. Returns: A SessionContext object containing the optimized message history and @@ -623,7 +625,7 @@ class Session(SessionBase): if isinstance(last_user_message, Message) else last_user_message ) - context = self._client.workspaces.sessions.get_context( + context = self._client.workspaces.sessions.context( session_id=self.id, workspace_id=self.workspace_id, tokens=tokens if tokens is not None else omit, @@ -638,10 +640,10 @@ class Session(SessionBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations if max_observations is not None else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, ) # Convert the honcho_core summary to our Summary if it exists @@ -834,7 +836,7 @@ class Session(SessionBase): return [Message.model_validate(msg) for msg in response] - def working_rep( + def get_representation( self, peer: str | PeerBase, *, @@ -842,43 +844,42 @@ class Session(SessionBase): search_query: str | None = None, search_top_k: int | None = None, search_max_distance: float | None = None, - include_most_derived: bool | None = None, - max_observations: int | None = None, - ) -> "Representation": + include_most_frequent: bool | None = None, + max_conclusions: int | None = None, + ) -> str: """ - Get the current working representation of the peer in this session. + Get a subset of the representation of the peer in this session. Args: - peer: Peer to get the working representation of. + peer: Peer to get the representation of. target: Optional target peer to get the representation of. If provided, queries what `peer` knows about the `target`. - search_query: Semantic search query to filter relevant observations + search_query: Semantic search query to filter relevant conclusions search_top_k: Number of semantically relevant facts to return search_max_distance: Maximum semantic distance for search results (0.0-1.0) - include_most_derived: Whether to include the most derived observations - max_observations: Maximum number of observations to include + include_most_frequent: Whether to include the most frequent conclusions + max_conclusions: Maximum number of conclusions to include Returns: - A Representation object containing explicit and deductive observations + A Representation string Example: ```python # Get peer's representation in this session - rep = session.working_rep('user123') + rep = session.get_representation('user123') print(rep) # Get what user123 knows about assistant in this session - local_rep = session.working_rep('user123', target='assistant') + local_rep = session.get_representation('user123', target='assistant') # Get representation with semantic search - searched_rep = session.working_rep( + searched_rep = session.get_representation( 'user123', search_query='preferences', search_top_k=10 ) ``` """ - from .types import Representation as _Representation peer_id = peer if isinstance(peer, str) else peer.id target_id = ( @@ -886,7 +887,7 @@ class Session(SessionBase): if target is None else (target if isinstance(target, str) else target.id) ) - data = self._client.workspaces.peers.working_representation( + data: PeerRepresentationResponse = self._client.workspaces.peers.representation( peer_id, workspace_id=self.workspace_id, session_id=self.id, @@ -896,21 +897,21 @@ class Session(SessionBase): search_max_distance=search_max_distance if search_max_distance is not None else omit, - include_most_derived=include_most_derived - if include_most_derived is not None + include_most_frequent=include_most_frequent + if include_most_frequent is not None else omit, - max_observations=max_observations if max_observations is not None else omit, + max_conclusions=max_conclusions if max_conclusions is not None else omit, ) - return _Representation.from_dict(data) # type: ignore + return data.representation @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - def get_deriver_status( + def get_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Get the deriver processing status, optionally scoped to an observer, sender, and/or session. + Get the queue 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 @@ -927,7 +928,7 @@ class Session(SessionBase): else (sender if isinstance(sender, str) else sender.id) ) - return self._client.workspaces.deriver_status( + return self._client.workspaces.queue.status( workspace_id=self.workspace_id, observer_id=resolved_observer_id, sender_id=resolved_sender_id, @@ -935,7 +936,7 @@ class Session(SessionBase): ) @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) - def poll_deriver_status( + def poll_queue_status( self, observer: str | PeerBase | None = None, sender: str | PeerBase | None = None, @@ -944,10 +945,10 @@ class Session(SessionBase): gt=0, description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).", ), - ) -> DeriverStatus: + ) -> QueueStatusResponse: """ - Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0. - This allows you to guarantee that all messages have been processed by the deriver for + Poll get_queue_status until pending_work_units and in_progress_work_units are both 0. + This allows you to guarantee that all messages have been processed by the queue for use with the dialectic endpoint. The polling estimates sleep time by assuming each work unit takes 1 second. @@ -958,19 +959,19 @@ class Session(SessionBase): timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds). Returns: - DeriverStatus when all work units are complete + QueueStatusResponse when all work units are complete Raises: TimeoutError: If timeout is exceeded before work units complete - Exception: If get_deriver_status fails repeatedly + Exception: If get_queue_status fails repeatedly """ start_time = time.time() while True: try: - status = self.get_deriver_status(observer, sender) + status = self.get_queue_status(observer, sender) except Exception as e: - logger.warning(f"Failed to get deriver status: {e}") + logger.warning(f"Failed to get queue status: {e}") # Sleep briefly before retrying time.sleep(1) diff --git a/sdks/python/src/honcho/types.py b/sdks/python/src/honcho/types.py index 8d4f8c2a..0db73ca2 100644 --- a/sdks/python/src/honcho/types.py +++ b/sdks/python/src/honcho/types.py @@ -3,340 +3,12 @@ from __future__ import annotations from collections.abc import AsyncIterator, Iterator -from datetime import datetime -from typing import TYPE_CHECKING, Any, cast - -from pydantic import BaseModel, Field -from typing_extensions import Required, TypedDict - -# Re-export observation types from dedicated module -from .observations import AsyncObservationScope, Observation, ObservationScope - -if TYPE_CHECKING: - from .base import SessionBase __all__ = [ - "AsyncObservationScope", - "DeductiveObservation", - "DeductiveObservationBase", "DialecticStreamResponse", - "ExplicitObservation", - "ExplicitObservationBase", - "Observation", - "ObservationCreateParam", - "ObservationMetadata", - "ObservationScope", - "PeerContext", - "Representation", ] -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.""" - - created_at: datetime - message_ids: list[int] - session_name: str - - -class ExplicitObservationBase(BaseModel): - """Base model for explicit observations - facts literally stated.""" - - content: str = Field(description="The explicit observation") - - -class DeductiveObservationBase(BaseModel): - """Base model for deductive observations - logical conclusions.""" - - premises: list[str] = Field( - description="Supporting premises or evidence for this conclusion", - default_factory=list, - ) - conclusion: str = Field(description="The deductive conclusion") - - -class ExplicitObservation(ExplicitObservationBase, ObservationMetadata): - """ - Explicit observation with content and metadata. - Represents facts LITERALLY stated - direct quotes or clear paraphrases only. - """ - - def __str__(self) -> str: - """Format observation with timestamp and content.""" - return f"[{self.created_at.replace(microsecond=0)}] {self.content}" - - def __hash__(self) -> int: - """ - Make ExplicitObservation hashable for use in sets. - """ - return hash((self.content, self.created_at, self.session_name)) - - def __eq__(self, other: object) -> bool: - """ - Define equality for ExplicitObservation objects. - Two observations are equal if content, created_at, and session_name match. - NOTE: message_ids are not included in the equality check. - """ - if not isinstance(other, ExplicitObservation): - return False - return ( - self.content == other.content - and self.created_at == other.created_at - and self.session_name == other.session_name - ) - - -class DeductiveObservation(DeductiveObservationBase, ObservationMetadata): - """ - Deductive observation with multiple premises and one conclusion, plus metadata. - Represents conclusions that MUST be true given explicit facts and premises. - """ - - def __str__(self) -> str: - """Format observation with timestamp, conclusion, and premises.""" - premises_text = "\n".join(f" - {premise}" for premise in self.premises) - return f"[{self.created_at.replace(microsecond=0)}] {self.conclusion}\n{premises_text}" - - def str_no_timestamps(self) -> str: - """Format observation without timestamps.""" - premises_text = "\n".join(f" - {premise}" for premise in self.premises) - return f"{self.conclusion}\n{premises_text}" - - def __hash__(self) -> int: - """ - Make DeductiveObservation hashable for use in sets. - NOTE: premises are not included in the hash. - """ - return hash((self.conclusion, self.created_at, self.session_name)) - - def __eq__(self, other: object) -> bool: - """ - Define equality for DeductiveObservation objects. - Two observations are equal if all their fields match. - NOTE: premises are not included in the equality check. - """ - if not isinstance(other, DeductiveObservation): - return False - return ( - self.conclusion == other.conclusion - and self.created_at == other.created_at - and self.session_name == other.session_name - ) - - -class Representation(BaseModel): - """ - A Representation is a traversable and diffable map of observations. - - At the base, we have a list of explicit observations, derived from a peer's messages. - From there, deductive observations can be made by establishing logical relationships - between explicit observations. - - All of a peer's observations are stored as documents in a collection. These documents - can be queried in various ways to produce this Representation object. - - A "working representation" is a version of this data structure representing the most - recent observations within a single session. - - A representation can have a maximum number of observations, which is applied - individually to each level of reasoning. If a maximum is set, observations are - added and removed in FIFO order. - """ - - explicit: list[ExplicitObservation] = Field( - description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference", - default_factory=list, - ) - deductive: list[DeductiveObservation] = Field( - description="Conclusions that MUST be true given explicit facts and premises - strict logical necessities", - default_factory=list, - ) - - def is_empty(self) -> bool: - """ - Check if the representation is empty. - """ - return len(self.explicit) == 0 and len(self.deductive) == 0 - - def diff_representation(self, other: "Representation") -> "Representation": - """ - Given this and another representation, return a new representation with only - observations that are unique to the other. - - Note: This only removes literal duplicates, not semantically equivalent ones. - - Args: - other: The representation to compare against - - Returns: - A new Representation containing only observations unique to other - """ - diff = Representation() - diff.explicit = [o for o in other.explicit if o not in self.explicit] - diff.deductive = [o for o in other.deductive if o not in self.deductive] - return diff - - def merge_representation( - self, other: "Representation", max_observations: int | None = None - ) -> None: - """ - Merge another representation object into this one. - - This will automatically deduplicate explicit and deductive observations. - This *preserves order* of observations so that they retain FIFO order. - - NOTE: observations with the *same* timestamp will not have order preserved. - That's fine though, because they are from the same timestamp... - - Args: - other: The representation to merge into this one - max_observations: Optional maximum number of observations to keep per type - """ - # removing duplicates by going list->set->list - self.explicit = list(set(self.explicit + other.explicit)) - self.deductive = list(set(self.deductive + other.deductive)) - # sort by created_at - self.explicit.sort(key=lambda x: x.created_at) - self.deductive.sort(key=lambda x: x.created_at) - - if max_observations: - self.explicit = self.explicit[-max_observations:] - self.deductive = self.deductive[-max_observations:] - - def __str__(self) -> str: - """ - Format representation into a clean, readable string for LLM prompts. - NOTE: we always strip subsecond precision from the timestamps. - - Returns: - Formatted string with clear sections and bullet points including temporal metadata - - Example: - EXPLICIT: - 1. [2025-01-01 12:00:00] The user has a dog named Rover - 2. [2025-01-01 12:01:00] The user's dog is 5 years old - - DEDUCTIVE: - 1. [2025-01-01 12:01:00] Rover is 5 years old - - The user has a dog named Rover - - The user's dog is 5 years old - """ - parts: list[str] = [] - - parts.append("EXPLICIT:\n") - for i, observation in enumerate(self.explicit, 1): - parts.append(f"{i}. {observation}") - parts.append("") - - parts.append("DEDUCTIVE:\n") - for i, observation in enumerate(self.deductive, 1): - parts.append(f"{i}. {observation}") - parts.append("") - - return "\n".join(parts) - - def str_no_timestamps(self) -> str: - """ - Format representation into a clean, readable string for LLM prompts... but without timestamps. - - Returns: - Formatted string with clear sections and bullet points without temporal metadata - - Example: - EXPLICIT: - 1. The user has a dog named Rover - 2. The user's dog is 5 years old - - DEDUCTIVE: - 1. Rover is 5 years old - - The user has a dog named Rover - - The user's dog is 5 years old - """ - parts: list[str] = [] - - parts.append("EXPLICIT:\n") - for i, observation in enumerate(self.explicit, 1): - parts.append(f"{i}. {observation.content}") - parts.append("") - - parts.append("DEDUCTIVE:\n") - for i, observation in enumerate(self.deductive, 1): - parts.append(f"{i}. {observation.str_no_timestamps()}") - parts.append("") - - return "\n".join(parts) - - def format_as_markdown(self) -> str: - """ - Format a Representation object as markdown. - NOTE: we always strip subsecond precision from the timestamps. - - Returns: - Formatted markdown string with headers and lists - """ - parts: list[str] = [] - - # Add explicit observations - parts.append("## Explicit Observations\n") - for i, obs in enumerate(self.explicit, 1): - parts.append(f"{i}. {obs}") - parts.append("") - - # Add deductive observations - parts.append("## Deductive Observations\n") - for i, obs in enumerate(self.deductive, 1): - parts.append(f"{i}. **Conclusion**: {obs.conclusion}") - if obs.premises: - parts.append(" **Premises**:") - for premise in obs.premises: - parts.append(f" - {premise}") - parts.append("") - parts.append("") - - return "\n".join(parts) - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "Representation": - """ - Create a Representation from a dictionary (typically from API response). - - Args: - data: Dictionary containing 'explicit' and 'deductive' observation lists - - Returns: - A new Representation instance - - Raises: - ValidationError: If observation data is missing required fields - """ - explicit_data: Any = data.get("explicit", []) - deductive_data: Any = data.get("deductive", []) - - explicit_list = cast( - list[Any], explicit_data if isinstance(explicit_data, list) else [] - ) - deductive_list = cast( - list[Any], deductive_data if isinstance(deductive_data, list) else [] - ) - - return cls( - explicit=[ExplicitObservation(**obs) for obs in explicit_list], - deductive=[DeductiveObservation(**obs) for obs in deductive_list], - ) - - class DialecticStreamResponse: """ Iterator for streaming dialectic responses with utilities for accessing the final response. @@ -436,81 +108,3 @@ class DialecticStreamResponse: def is_complete(self) -> bool: """Check if the stream has finished.""" return self._is_complete - - -class PeerContext: - """ - Context for a peer, including representation and peer card. - - This class holds both the working representation and peer card for a peer, - typically returned from the get_context API call. - - Attributes: - peer_id: The ID of the observer peer - target_id: The ID of the target peer being observed - representation: The working representation (may be None if no observations exist) - peer_card: List of peer card strings (may be None if no card exists) - """ - - peer_id: str - target_id: str - representation: Representation | None - peer_card: list[str] | None - - def __init__( - self, - peer_id: str, - target_id: str, - representation: Representation | None = None, - peer_card: list[str] | None = None, - ): - self.peer_id = peer_id - self.target_id = target_id - self.representation = representation - self.peer_card = peer_card - - @classmethod - def from_api_response(cls, response: Any) -> "PeerContext": - """ - Create a PeerContext from an API response. - - Args: - response: API response object with peer_id, target_id, representation, and peer_card - - Returns: - A new PeerContext instance - """ - peer_id = getattr(response, "peer_id", "") or "" - target_id = getattr(response, "target_id", "") or "" - - representation = None - rep_data = getattr(response, "representation", None) - if rep_data is not None: - if isinstance(rep_data, dict): - representation = Representation.from_dict( - cast(dict[str, Any], rep_data) - ) - elif hasattr(rep_data, "explicit") and hasattr(rep_data, "deductive"): - representation = Representation.from_dict( - { - "explicit": rep_data.explicit, - "deductive": rep_data.deductive, - } - ) - - peer_card = getattr(response, "peer_card", None) - - return cls( - peer_id=peer_id, - target_id=target_id, - representation=representation, - peer_card=peer_card, - ) - - def __repr__(self) -> str: - has_rep = self.representation is not None - has_card = self.peer_card is not None and len(self.peer_card) > 0 - return ( - f"PeerContext(peer_id={self.peer_id!r}, target_id={self.target_id!r}, " - f"has_representation={has_rep}, has_peer_card={has_card})" - ) diff --git a/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts b/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts index 1e8e369f..04216493 100644 --- a/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts +++ b/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts @@ -14,7 +14,7 @@ export default class MockHonchoCore { getOrCreate: jest.fn(), update: jest.fn(), search: jest.fn(), - workingRepresentation: jest.fn(), + getRepresentation: jest.fn(), }, sessions: { list: jest.fn(), diff --git a/sdks/typescript/__tests__/client.test.ts b/sdks/typescript/__tests__/client.test.ts index f826b08d..5c3c18fa 100644 --- a/sdks/typescript/__tests__/client.test.ts +++ b/sdks/typescript/__tests__/client.test.ts @@ -16,11 +16,13 @@ jest.mock('@honcho-ai/core', () => { list: jest.fn(), getOrCreate: jest.fn(), }, + queue: { + status: jest.fn(), + }, getOrCreate: jest.fn().mockResolvedValue({ id: 'test-workspace', metadata: {} }), update: jest.fn(), list: jest.fn(), search: jest.fn(), - deriverStatus: jest.fn(), }, })); }); @@ -408,8 +410,8 @@ describe('Honcho Client', () => { }); }); - describe('getDeriverStatus', () => { - it('should return deriver status without options', async () => { + describe('getQueueStatus', () => { + it('should return queue status without options', async () => { const mockStatus = { total_work_units: 10, completed_work_units: 5, @@ -417,9 +419,9 @@ describe('Honcho Client', () => { pending_work_units: 2, sessions: { 'session1': { status: 'active' } }, }; - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus); + mockClient.workspaces.queue.status.mockResolvedValue(mockStatus); - const status = await honcho.getDeriverStatus(); + const status = await honcho.getQueueStatus(); expect(status).toEqual({ totalWorkUnits: 10, @@ -428,19 +430,22 @@ describe('Honcho Client', () => { pendingWorkUnits: 2, sessions: { 'session1': { status: 'active' } }, }); - expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith('test-workspace', {}); + expect(mockClient.workspaces.queue.status).toHaveBeenCalledWith( + 'test-workspace', + {} + ); }); - it('should return deriver status with options', async () => { + it('should return queue status with options', async () => { const mockStatus = { total_work_units: 5, completed_work_units: 3, in_progress_work_units: 1, pending_work_units: 1, }; - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus); + mockClient.workspaces.queue.status.mockResolvedValue(mockStatus); - const status = await honcho.getDeriverStatus({ + const status = await honcho.getQueueStatus({ observer: 'observer1', sender: 'sender1', session: 'session1', @@ -453,15 +458,18 @@ describe('Honcho Client', () => { pendingWorkUnits: 1, sessions: undefined, }); - expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith('test-workspace', { - observer_id: 'observer1', - sender_id: 'sender1', - session_id: 'session1', - }); + expect(mockClient.workspaces.queue.status).toHaveBeenCalledWith( + 'test-workspace', + { + observer_id: 'observer1', + sender_id: 'sender1', + session_id: 'session1', + } + ); }); }); - describe('pollDeriverStatus', () => { + describe('pollQueueStatus', () => { it('should poll until processing is complete', async () => { const mockStatusComplete = { total_work_units: 5, @@ -469,9 +477,9 @@ describe('Honcho Client', () => { in_progress_work_units: 0, pending_work_units: 0, }; - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatusComplete); + mockClient.workspaces.queue.status.mockResolvedValue(mockStatusComplete); - const status = await honcho.pollDeriverStatus(); + const status = await honcho.pollQueueStatus(); expect(status).toEqual({ totalWorkUnits: 5, @@ -489,9 +497,9 @@ describe('Honcho Client', () => { in_progress_work_units: 2, pending_work_units: 1, }; - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatusPending); + mockClient.workspaces.queue.status.mockResolvedValue(mockStatusPending); - await expect(honcho.pollDeriverStatus({ timeoutMs: 100 })).rejects.toThrow(); + await expect(honcho.pollQueueStatus({ timeoutMs: 0 })).rejects.toThrow(); }); }); diff --git a/sdks/typescript/__tests__/integration.test.ts b/sdks/typescript/__tests__/integration.test.ts index f1578dd0..96fded3a 100644 --- a/sdks/typescript/__tests__/integration.test.ts +++ b/sdks/typescript/__tests__/integration.test.ts @@ -3,7 +3,6 @@ import { Peer } from '../src/peer' import { Session } from '../src/session' import { SessionContext } from '../src/session_context' import { Page } from '../src/pagination' -import { Representation } from '../src/representation' // Mock the @honcho-ai/core module let mockWorkspacesApi: any @@ -26,7 +25,7 @@ describe('Honcho SDK Integration Tests', () => { getOrCreate: jest.fn(), update: jest.fn(), search: jest.fn(), - workingRepresentation: jest.fn(), + representation: jest.fn(), }, sessions: { list: jest.fn(), @@ -39,7 +38,7 @@ describe('Honcho SDK Integration Tests', () => { messages: { create: jest.fn(), list: jest.fn() }, getOrCreate: jest.fn(), update: jest.fn(), - getContext: jest.fn(), + context: jest.fn(), search: jest.fn(), }, getOrCreate: jest.fn(), @@ -91,7 +90,7 @@ describe('Honcho SDK Integration Tests', () => { mockWorkspacesApi.workspaces.sessions.messages.create.mockResolvedValue( {} ) - mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue( + mockWorkspacesApi.workspaces.sessions.context.mockResolvedValue( mockContextData ) mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ @@ -285,7 +284,7 @@ describe('Honcho SDK Integration Tests', () => { mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue( new Error('Chat API failed') ) - mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue( + mockWorkspacesApi.workspaces.sessions.context.mockRejectedValue( new Error('Context API failed') ) @@ -370,34 +369,11 @@ describe('Honcho SDK Integration Tests', () => { }) it('should handle working representation queries', async () => { - const mockWorkingRepData = { - explicit: [ - { - content: 'Alice likes coffee', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'working-rep-session', - }, - { - content: 'Alice works as a developer', - created_at: '2024-01-01T00:01:00Z', - message_ids: [[3, 4]], - session_name: 'working-rep-session', - }, - ], - deductive: [ - { - conclusion: 'Alice is a coffee-drinking developer', - premises: ['Alice likes coffee', 'Alice works as a developer'], - created_at: '2024-01-01T00:02:00Z', - message_ids: [[5, 6]], - session_name: 'working-rep-session', - }, - ], - } + const mockRepresentation = + 'Alice likes coffee\nAlice works as a developer\nAlice is a coffee-drinking developer' - mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockWorkingRepData, + mockWorkspacesApi.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }) const session = await honcho.session('working-rep-session') @@ -405,28 +381,33 @@ describe('Honcho SDK Integration Tests', () => { const bob = await honcho.peer('bob') // Test working representation without target - const globalRep = await session.workingRep('alice') - expect(globalRep).toBeInstanceOf(Representation) - expect(globalRep.explicit).toHaveLength(2) - expect(globalRep.explicit[0].content).toBe('Alice likes coffee') - expect(globalRep.explicit[1].content).toBe('Alice works as a developer') - expect(globalRep.deductive).toHaveLength(1) - expect(globalRep.deductive[0].conclusion).toBe('Alice is a coffee-drinking developer') + const globalRep = await session.getRepresentation('alice') + expect(globalRep).toBe(mockRepresentation) expect( - mockWorkspacesApi.workspaces.peers.workingRepresentation + mockWorkspacesApi.workspaces.peers.representation ).toHaveBeenCalledWith('integration-test-workspace', 'alice', { session_id: 'working-rep-session', target: undefined, + search_query: undefined, + search_top_k: undefined, + search_max_distance: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }) // Test working representation with target - const targetRep = await session.workingRep(alice, bob) - expect(targetRep).toBeInstanceOf(Representation) + const targetRep = await session.getRepresentation(alice, bob) + expect(targetRep).toBe(mockRepresentation) expect( - mockWorkspacesApi.workspaces.peers.workingRepresentation + mockWorkspacesApi.workspaces.peers.representation ).toHaveBeenCalledWith('integration-test-workspace', 'alice', { session_id: 'working-rep-session', target: 'bob', + search_query: undefined, + search_top_k: undefined, + search_max_distance: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }) }) }) @@ -442,7 +423,7 @@ describe('Honcho SDK Integration Tests', () => { total: 0, hasNextPage: false, }) - mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({ + mockWorkspacesApi.workspaces.sessions.context.mockResolvedValue({ messages: [], }) @@ -480,7 +461,7 @@ describe('Honcho SDK Integration Tests', () => { expect(typeof message.metadata).toBe('object') // Mock successful operations - mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({ + mockWorkspacesApi.workspaces.sessions.context.mockResolvedValue({ messages: [{ id: 'msg1', content: 'Hello', peer_id: 'typed-peer' }], summary: { content: 'Test summary', diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts index 8e80125c..8a3d26e2 100644 --- a/sdks/typescript/__tests__/peer.test.ts +++ b/sdks/typescript/__tests__/peer.test.ts @@ -2,7 +2,6 @@ import { Peer } from '../src/peer'; import { Session } from '../src/session'; import { Page } from '../src/pagination'; import { Honcho } from '../src/client'; -import { Representation } from '../src/representation'; // Mock the @honcho-ai/core module jest.mock('@honcho-ai/core', () => { @@ -553,420 +552,268 @@ describe('Peer', () => { }); }); - describe('workingRep', () => { + describe('getRepresentation', () => { beforeEach(() => { - mockClient.workspaces.peers.workingRepresentation = jest.fn(); + mockClient.workspaces.peers.representation = jest.fn(); }); it('should get working representation with no parameters', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'Observation 1', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - { - content: 'Observation 2', - created_at: '2024-01-01T00:01:00Z', - message_ids: [[3, 4]], - session_name: 'test-session', - }, - ], - deductive: [ - { - conclusion: 'Conclusion 1', - premises: ['Observation 1', 'Observation 2'], - created_at: '2024-01-01T00:02:00Z', - message_ids: [[5, 6]], - session_name: 'test-session', - }, - ], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'Observation 1\nObservation 2\nConclusion 1'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep(); + const result = await peer.getRepresentation(); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(2); - expect(result.explicit[0].content).toBe('Observation 1'); - expect(result.explicit[1].content).toBe('Observation 2'); - expect(result.deductive).toHaveLength(1); - expect(result.deductive[0].conclusion).toBe('Conclusion 1'); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: undefined, search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }); }); it('should get working representation with session as string', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'Session-scoped observation', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'session-123', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'Session-scoped observation'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep('session-123'); + const result = await peer.getRepresentation('session-123'); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe('Session-scoped observation'); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: 'session-123', target: undefined, search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }); }); it('should get working representation with session as Session object', async () => { const session = new Session('session-123', 'test-workspace', mockClient); - const mockRepresentationData = { - explicit: [ - { - content: 'Session object observation', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'session-123', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'Session object observation'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep(session); + const result = await peer.getRepresentation(session); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe('Session object observation'); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: 'session-123', target: undefined, search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }); }); it('should get working representation with target as string', async () => { - const mockRepresentationData = { - explicit: [ - { - content: "Observer's view of target", - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = "Observer's view of target"; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep(undefined, 'target-peer'); + const result = await peer.getRepresentation(undefined, 'target-peer'); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe("Observer's view of target"); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: 'target-peer', search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }); }); it('should get working representation with target as Peer object', async () => { const targetPeer = new Peer('target-peer', 'test-workspace', mockClient); - const mockRepresentationData = { - explicit: [ - { - content: "Observer's view of target peer object", - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = "Observer's view of target peer object"; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep(undefined, targetPeer); + const result = await peer.getRepresentation(undefined, targetPeer); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe("Observer's view of target peer object"); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: 'target-peer', search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }); }); it('should get working representation with search query', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'Query-curated observation', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'Query-curated observation'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep( + const result = await peer.getRepresentation( undefined, undefined, { searchQuery: 'programming' } ); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe('Query-curated observation'); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: undefined, search_query: 'programming', search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }); }); it('should get working representation with custom size', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'Limited observations', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'Limited observations'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep(undefined, undefined, { maxObservations: 10 }); + const result = await peer.getRepresentation(undefined, undefined, { maxConclusions: 10 }); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe('Limited observations'); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: undefined, search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: 10, + include_most_frequent: undefined, + max_conclusions: 10, }); }); it('should get working representation with all parameters', async () => { const session = new Session('session-123', 'test-workspace', mockClient); const targetPeer = new Peer('target-peer', 'test-workspace', mockClient); - const mockRepresentationData = { - explicit: [ - { - content: 'Fully parameterized observation', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'session-123', - }, - ], - deductive: [ - { - conclusion: 'Conclusion with all params', - premises: ['Fully parameterized observation'], - created_at: '2024-01-01T00:01:00Z', - message_ids: [[3, 4]], - session_name: 'session-123', - }, - ], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'Fully parameterized observation\nConclusion with all params'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep( + const result = await peer.getRepresentation( session, targetPeer, - { searchQuery: 'Python programming', maxObservations: 25 } + { searchQuery: 'Python programming', maxConclusions: 25 } ); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe('Fully parameterized observation'); - expect(result.deductive).toHaveLength(1); - expect(result.deductive[0].conclusion).toBe('Conclusion with all params'); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: 'session-123', target: 'target-peer', search_query: 'Python programming', search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: 25, + include_most_frequent: undefined, + max_conclusions: 25, }); }); it('should get working representation with string session and string target', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'String params observation', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'session-456', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentation = 'String params observation'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentation, }); - const result = await peer.workingRep( + const result = await peer.getRepresentation( 'session-456', 'target-peer-123', - { searchQuery: 'machine learning', maxObservations: 50 } + { searchQuery: 'machine learning', maxConclusions: 50 } ); - expect(result).toBeInstanceOf(Representation); - expect(result.explicit).toHaveLength(1); - expect(result.explicit[0].content).toBe('String params observation'); - expect(result.deductive).toHaveLength(0); + expect(result).toBe(mockRepresentation); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'test-peer', { session_id: 'session-456', target: 'target-peer-123', search_query: 'machine learning', search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: 50, + include_most_frequent: undefined, + max_conclusions: 50, }); }); it('should handle boundary size values', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'Boundary test', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - }; - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentationString = 'Boundary test representation'; + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentationString, }); // Test size = 1 - const result1 = await peer.workingRep(undefined, undefined, { maxObservations: 1 }); - expect(result1).toBeInstanceOf(Representation); + const result1 = await peer.getRepresentation(undefined, undefined, { maxConclusions: 1 }); + expect(result1).toBe(mockRepresentationString); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenLastCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: undefined, search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: 1, + include_most_frequent: undefined, + max_conclusions: 1, }); // Test size = 100 - const result2 = await peer.workingRep(undefined, undefined, { maxObservations: 100 }); - expect(result2).toBeInstanceOf(Representation); + const result2 = await peer.getRepresentation(undefined, undefined, { maxConclusions: 100 }); + expect(result2).toBe(mockRepresentationString); expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenLastCalledWith('test-workspace', 'test-peer', { session_id: undefined, target: undefined, search_query: undefined, search_top_k: undefined, search_max_distance: undefined, - include_most_derived: undefined, - max_observations: 100, + include_most_frequent: undefined, + max_conclusions: 100, }); }); it('should handle API errors', async () => { - mockClient.workspaces.peers.workingRepresentation.mockRejectedValue( + mockClient.workspaces.peers.representation.mockRejectedValue( new Error('Working representation fetch failed') ); - await expect(peer.workingRep()).rejects.toThrow( + await expect(peer.getRepresentation()).rejects.toThrow( 'Working representation fetch failed' ); }); diff --git a/sdks/typescript/__tests__/session.test.ts b/sdks/typescript/__tests__/session.test.ts index 49647ca6..409cfbee 100644 --- a/sdks/typescript/__tests__/session.test.ts +++ b/sdks/typescript/__tests__/session.test.ts @@ -3,7 +3,6 @@ import { Peer } from '../src/peer' import { Page } from '../src/pagination' import { SessionContext } from '../src/session_context' import { Honcho } from '../src/client' -import { Representation } from '../src/representation' // Mock the @honcho-ai/core module jest.mock('@honcho-ai/core', () => { @@ -15,7 +14,7 @@ jest.mock('@honcho-ai/core', () => { set: jest.fn(), remove: jest.fn(), list: jest.fn(), - getConfig: jest.fn(), + config: jest.fn(), setConfig: jest.fn(), }, messages: { @@ -27,13 +26,15 @@ jest.mock('@honcho-ai/core', () => { update: jest.fn(), delete: jest.fn(), clone: jest.fn(), - getContext: jest.fn(), + context: jest.fn(), search: jest.fn(), }, peers: { - workingRepresentation: jest.fn(), + representation: jest.fn(), + }, + queue: { + status: jest.fn(), }, - deriverStatus: jest.fn(), getOrCreate: jest.fn(), update: jest.fn(), list: jest.fn(), @@ -332,7 +333,7 @@ describe('Session', () => { describe('getPeerConfig', () => { it('should return peer configuration', async () => { const mockConfig = { observe_me: true, observe_others: false } - mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue( + mockClient.workspaces.sessions.peers.config.mockResolvedValue( mockConfig ) @@ -340,14 +341,14 @@ describe('Session', () => { expect(config).toEqual(mockConfig) expect( - mockClient.workspaces.sessions.peers.getConfig + mockClient.workspaces.sessions.peers.config ).toHaveBeenCalledWith('test-workspace', 'test-session', 'peer1') }) it('should handle Peer object input', async () => { const peer = new Peer('peer1', 'test-workspace', mockClient) const mockConfig = { observe_me: false, observe_others: true } - mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue( + mockClient.workspaces.sessions.peers.config.mockResolvedValue( mockConfig ) @@ -355,7 +356,7 @@ describe('Session', () => { expect(config).toEqual(mockConfig) expect( - mockClient.workspaces.sessions.peers.getConfig + mockClient.workspaces.sessions.peers.config ).toHaveBeenCalledWith('test-workspace', 'test-session', 'peer1') }) }) @@ -774,7 +775,7 @@ describe('Session', () => { token_count: 100, }, } - mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext) + mockClient.workspaces.sessions.context.mockResolvedValue(mockContext) const context = await session.getContext() @@ -782,7 +783,7 @@ describe('Session', () => { expect(context.sessionId).toBe('test-session') expect(context.messages).toEqual(mockContext.messages) expect(context.summary?.content).toBe('Conversation summary') - expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith( + expect(mockClient.workspaces.sessions.context).toHaveBeenCalledWith( 'test-workspace', 'test-session', { tokens: undefined, summary: undefined } @@ -800,12 +801,12 @@ describe('Session', () => { token_count: 50, }, } - mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext) + mockClient.workspaces.sessions.context.mockResolvedValue(mockContext) const context = await session.getContext({ summary: true, tokens: 1000 }) expect(context).toBeInstanceOf(SessionContext) - expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith( + expect(mockClient.workspaces.sessions.context).toHaveBeenCalledWith( 'test-workspace', 'test-session', { tokens: 1000, summary: true } @@ -816,7 +817,7 @@ describe('Session', () => { const mockContext = { messages: [{ id: 'msg1', content: 'Hello', peer_id: 'peer1' }], } - mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext) + mockClient.workspaces.sessions.context.mockResolvedValue(mockContext) const context = await session.getContext() @@ -824,7 +825,7 @@ describe('Session', () => { }) it('should handle API errors', async () => { - mockClient.workspaces.sessions.getContext.mockRejectedValue( + mockClient.workspaces.sessions.context.mockRejectedValue( new Error('Failed to get context') ) @@ -903,136 +904,108 @@ describe('Session', () => { }) }) - describe('workingRep', () => { + describe('getRepresentation', () => { it('should get working representation with peer string', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'Some knowledge about the peer', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - } - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentationString = 'Some knowledge about the peer' + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentationString, }) - const result = await session.workingRep('peer1') + const result = await session.getRepresentation('peer1') - expect(result).toBeInstanceOf(Representation) - expect(result.explicit).toHaveLength(1) - expect(result.explicit[0].content).toBe('Some knowledge about the peer') - expect(result.deductive).toHaveLength(0) + expect(typeof result).toBe('string') + expect(result).toBe(mockRepresentationString) expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'peer1', { session_id: 'test-session', target: undefined, + search_query: undefined, + search_top_k: undefined, + search_max_distance: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }) }) it('should get working representation with Peer object', async () => { const peer = new Peer('peer1', 'test-workspace', mockClient) - const mockRepresentationData = { - explicit: [ - { - content: 'Some knowledge', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - } - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentationString = 'Some knowledge' + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentationString, }) - const result = await session.workingRep(peer) + const result = await session.getRepresentation(peer) - expect(result).toBeInstanceOf(Representation) - expect(result.explicit).toHaveLength(1) - expect(result.explicit[0].content).toBe('Some knowledge') - expect(result.deductive).toHaveLength(0) + expect(typeof result).toBe('string') + expect(result).toBe(mockRepresentationString) expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'peer1', { session_id: 'test-session', target: undefined, + search_query: undefined, + search_top_k: undefined, + search_max_distance: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }) }) it('should get working representation with target peer string', async () => { - const mockRepresentationData = { - explicit: [ - { - content: 'What peer1 knows about target', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - } - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentationString = 'What peer1 knows about target' + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentationString, }) - const result = await session.workingRep('peer1', 'target-peer') + const result = await session.getRepresentation('peer1', 'target-peer') - expect(result).toBeInstanceOf(Representation) - expect(result.explicit).toHaveLength(1) - expect(result.explicit[0].content).toBe('What peer1 knows about target') - expect(result.deductive).toHaveLength(0) + expect(typeof result).toBe('string') + expect(result).toBe(mockRepresentationString) expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'peer1', { session_id: 'test-session', target: 'target-peer', + search_query: undefined, + search_top_k: undefined, + search_max_distance: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }) }) it('should get working representation with target Peer object', async () => { const peer = new Peer('peer1', 'test-workspace', mockClient) const target = new Peer('target-peer', 'test-workspace', mockClient) - const mockRepresentationData = { - explicit: [ - { - content: 'What peer1 knows about target', - created_at: '2024-01-01T00:00:00Z', - message_ids: [[1, 2]], - session_name: 'test-session', - }, - ], - deductive: [], - } - mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({ - representation: mockRepresentationData, + const mockRepresentationString = 'What peer1 knows about target' + mockClient.workspaces.peers.representation.mockResolvedValue({ + representation: mockRepresentationString, }) - const result = await session.workingRep(peer, target) + const result = await session.getRepresentation(peer, target) - expect(result).toBeInstanceOf(Representation) - expect(result.explicit).toHaveLength(1) - expect(result.explicit[0].content).toBe('What peer1 knows about target') - expect(result.deductive).toHaveLength(0) + expect(typeof result).toBe('string') + expect(result).toBe(mockRepresentationString) expect( - mockClient.workspaces.peers.workingRepresentation + mockClient.workspaces.peers.representation ).toHaveBeenCalledWith('test-workspace', 'peer1', { session_id: 'test-session', target: 'target-peer', + search_query: undefined, + search_top_k: undefined, + search_max_distance: undefined, + include_most_frequent: undefined, + max_conclusions: undefined, }) }) it('should handle API errors', async () => { - mockClient.workspaces.peers.workingRepresentation.mockRejectedValue( + mockClient.workspaces.peers.representation.mockRejectedValue( new Error('Failed to get working representation') ) - await expect(session.workingRep('peer1')).rejects.toThrow() + await expect(session.getRepresentation('peer1')).rejects.toThrow() }) }) describe('delete', () => { @@ -1124,8 +1097,8 @@ describe('Session', () => { }) }) - describe('getDeriverStatus', () => { - it('should return deriver status without options', async () => { + describe('getQueueStatus', () => { + it('should return queue status without options', async () => { const mockStatus = { total_work_units: 10, completed_work_units: 5, @@ -1133,9 +1106,9 @@ describe('Session', () => { pending_work_units: 2, sessions: { session1: { status: 'active' } }, } - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus) + mockClient.workspaces.queue.status.mockResolvedValue(mockStatus) - const status = await session.getDeriverStatus() + const status = await session.getQueueStatus() expect(status).toEqual({ totalWorkUnits: 10, @@ -1144,22 +1117,22 @@ describe('Session', () => { pendingWorkUnits: 2, sessions: { session1: { status: 'active' } }, }) - expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith( + expect(mockClient.workspaces.queue.status).toHaveBeenCalledWith( 'test-workspace', { session_id: 'test-session' } ) }) - it('should return deriver status with options', async () => { + it('should return queue status with options', async () => { const mockStatus = { total_work_units: 5, completed_work_units: 3, in_progress_work_units: 1, pending_work_units: 1, } - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus) + mockClient.workspaces.queue.status.mockResolvedValue(mockStatus) - const status = await session.getDeriverStatus({ + const status = await session.getQueueStatus({ observer: 'observer1', sender: 'sender1', }) @@ -1171,7 +1144,7 @@ describe('Session', () => { pendingWorkUnits: 1, sessions: undefined, }) - expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith( + expect(mockClient.workspaces.queue.status).toHaveBeenCalledWith( 'test-workspace', { observer_id: 'observer1', @@ -1181,7 +1154,7 @@ describe('Session', () => { ) }) - describe('pollDeriverStatus', () => { + describe('pollQueueStatus', () => { it('should poll until processing is complete', async () => { const mockStatusComplete = { total_work_units: 5, @@ -1189,11 +1162,11 @@ describe('Session', () => { in_progress_work_units: 0, pending_work_units: 0, } - mockClient.workspaces.deriverStatus.mockResolvedValue( + mockClient.workspaces.queue.status.mockResolvedValue( mockStatusComplete ) - const status = await session.pollDeriverStatus() + const status = await session.pollQueueStatus() expect(status).toEqual({ totalWorkUnits: 5, @@ -1203,7 +1176,7 @@ describe('Session', () => { sessions: undefined, }) - expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith( + expect(mockClient.workspaces.queue.status).toHaveBeenCalledWith( 'test-workspace', { session_id: 'test-session' } ) @@ -1216,10 +1189,10 @@ describe('Session', () => { in_progress_work_units: 2, pending_work_units: 1, } - mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatusPending) + mockClient.workspaces.queue.status.mockResolvedValue(mockStatusPending) await expect( - session.pollDeriverStatus({ timeoutMs: 100 }) + session.pollQueueStatus({ timeoutMs: 100 }) ).rejects.toThrow() }) }) diff --git a/sdks/typescript/bun.lock b/sdks/typescript/bun.lock index 78a4a5dd..86fba967 100644 --- a/sdks/typescript/bun.lock +++ b/sdks/typescript/bun.lock @@ -4,7 +4,7 @@ "": { "name": "@honcho-ai/sdk", "dependencies": { - "@honcho-ai/core": "^1.8.0", + "@honcho-ai/core": "2.1.0", "@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.8.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-qxBNoXLezH8yx4iBoz4Bsxkm9zp4Gm1fNwuP8gHRdSelxhR0dXpvLffx8B5V0XFWsx+SfPaJFyaKw0X2sYMwLA=="], + "@honcho-ai/core": ["@honcho-ai/core@2.1.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-BssURYjiBmFF/guQQkr6Dpiz1ZOzhJsf/rdp6Ek/A3T2vNz3pop0fNgHK6SjCFcqjSKF8SHWJMF2VqqH0y4J6Q=="], "@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=="], diff --git a/sdks/typescript/examples/get_representation.ts b/sdks/typescript/examples/get_representation.ts index 3f10fb57..7b036da7 100644 --- a/sdks/typescript/examples/get_representation.ts +++ b/sdks/typescript/examples/get_representation.ts @@ -1,4 +1,4 @@ -import { Honcho, Message } from '../src'; +import { Honcho, type MessageCreate } from '../src'; /** * Example demonstrating how to get peer representations. @@ -27,7 +27,7 @@ async function main() { console.log('Generating random messages...'); // Generate some random messages from alice, bob, and charlie and add them to the session - const messages: Message[] = []; + const messages: MessageCreate[] = []; for (let i = 0; i < 10; i++) { const randomPeer = peers[Math.floor(Math.random() * peers.length)]; messages.push( @@ -43,13 +43,13 @@ async function main() { console.log('Getting alice\'s working representation in session...'); // Get alice's working representation in the session - const workingRepresentation = await session.workingRep(alice); - console.log('Working representation returned:', workingRepresentation); + const representation = await session.getRepresentation(alice); + console.log('Representation returned:', representation); console.log('Getting alice\'s working representation *of bob* in session...'); // Get alice's working representation *of bob* in the session - const workingRepresentationOfBob = await session.workingRep(alice, bob); - console.log('Working representation returned:', workingRepresentationOfBob); + const representationOfBob = await session.getRepresentation(alice, bob); + console.log('Representation returned:', representationOfBob); console.log('Example completed successfully!'); } diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index a780bc86..9613a7ff 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -20,7 +20,7 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "@honcho-ai/core": "^1.8.0", + "@honcho-ai/core": "2.1.0", "@types/node": "^24.0.1", "zod": "4.0.0" }, diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index b185feee..9485e202 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -1,15 +1,14 @@ import HonchoCore from '@honcho-ai/core' import type { DefaultQuery } from '@honcho-ai/core/core' -import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages' import type { - DeriverStatus, - WorkspaceDeriverStatusParams, -} from '@honcho-ai/core/resources/workspaces/workspaces' + QueueStatusParams, + QueueStatusResponse, +} from '@honcho-ai/core/resources/workspaces/queue' +import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages' import { Page } from './pagination' import { Peer } from './peer' import { Session } from './session' import { - type DeriverStatusOptions, FilterSchema, type Filters, type HonchoConfig, @@ -21,6 +20,7 @@ import { PeerIdSchema, type PeerMetadata, PeerMetadataSchema, + type QueueStatusOptions, SearchQuerySchema, type SessionConfig, SessionConfigSchema, @@ -479,20 +479,20 @@ export class Honcho { } /** - * Get the deriver processing status, optionally scoped to an observer, sender, and/or session. + * Get the queue processing status, optionally scoped to an observer, sender, and/or session. * - * Makes an API call to retrieve the current status of the deriver processing queue. - * The deriver is responsible for processing messages and updating peer representations. + * Makes an API call to retrieve the current status of the queue processing queue. + * The queue is responsible for processing messages and updating peer representations. * * @param options - Configuration options for the status request * @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 + * @returns Promise resolving to the queue status information including work unit counts */ - async getDeriverStatus( + async getQueueStatus( options?: Omit< - DeriverStatusOptions, + QueueStatusOptions, 'observerId' | 'senderId' | 'sessionId' > & { observer?: string | Peer @@ -504,7 +504,7 @@ export class Honcho { completedWorkUnits: number inProgressWorkUnits: number pendingWorkUnits: number - sessions?: Record + sessions?: Record }> { const resolvedObserverId = options?.observer ? typeof options.observer === 'string' @@ -522,12 +522,12 @@ export class Honcho { : options.session.id : undefined - const queryParams: WorkspaceDeriverStatusParams = {} + const queryParams: QueueStatusParams = {} 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( + const status = await this._client.workspaces.queue.status( this.workspaceId, queryParams ) @@ -542,8 +542,8 @@ export class Honcho { } /** - * Poll getDeriverStatus until pendingWorkUnits and inProgressWorkUnits are both 0. - * This allows you to guarantee that all messages have been processed by the deriver for + * Poll getQueueStatus until pendingWorkUnits and inProgressWorkUnits are both 0. + * This allows you to guarantee that all messages have been processed by the queue for * use with the dialectic endpoint. * * The polling estimates sleep time by assuming each work unit takes 1 second. @@ -553,12 +553,12 @@ export class Honcho { * @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 + * @returns Promise resolving to the final queue status when processing is complete * @throws Error if timeout is exceeded before processing completes */ - async pollDeriverStatus( + async pollQueueStatus( options?: Omit< - DeriverStatusOptions, + QueueStatusOptions, 'observerId' | 'senderId' | 'sessionId' > & { observer?: string | Peer @@ -570,13 +570,13 @@ export class Honcho { completedWorkUnits: number inProgressWorkUnits: number pendingWorkUnits: number - sessions?: Record + sessions?: Record }> { const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes const startTime = Date.now() while (true) { - const status = await this.getDeriverStatus(options) + const status = await this.getQueueStatus(options) if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) { return status } diff --git a/sdks/typescript/src/observations.ts b/sdks/typescript/src/conclusions.ts similarity index 52% rename from sdks/typescript/src/observations.ts rename to sdks/typescript/src/conclusions.ts index 50e830a4..cfb26dc2 100644 --- a/sdks/typescript/src/observations.ts +++ b/sdks/typescript/src/conclusions.ts @@ -1,34 +1,30 @@ import type HonchoCore from '@honcho-ai/core' -import { - Representation, - type RepresentationData, - type RepresentationOptions, -} from './representation' +import type { RepresentationOptions } from './representation' import type { Session } from './session' -import type { ObservationCreateParam } from './types' +import type { ConclusionCreateParam } from './types' // Re-export for consumers who import from this module -export type { RepresentationOptions, ObservationCreateParam } +export type { RepresentationOptions, ConclusionCreateParam } /** - * An observation from the theory-of-mind system. + * A conclusion from Honcho's reasoning system. * - * Observations are facts derived from messages that help build a representation + * Conclusions are facts derived from messages that help build a representation * of a peer. */ -export class Observation { +export class Conclusion { /** - * Unique identifier for this observation. + * Unique identifier for this conclusion. */ readonly id: string /** - * The observation content/text. + * The conclusion content/text. */ readonly content: string /** - * The peer who made the observation. + * The peer who made the conclusion. */ readonly observerId: string @@ -38,12 +34,12 @@ export class Observation { readonly observedId: string /** - * The session where this observation was made. + * The session where this conclusion was made. */ readonly sessionId: string /** - * When the observation was created. + * When the conclusion was created. */ readonly createdAt: string @@ -64,13 +60,13 @@ export class Observation { } /** - * Create an Observation from an API response object. + * Create a Conclusion from an API response object. * * @param data - API response data - * @returns A new Observation instance + * @returns A new Conclusion instance */ - static fromApiResponse(data: Record): Observation { - return new Observation( + static fromApiResponse(data: Record): Conclusion { + return new Conclusion( (data.id as string) ?? '', (data.content as string) ?? '', (data.observer_id as string) ?? '', @@ -81,46 +77,46 @@ export class Observation { } /** - * Return a string representation of the Observation. + * Return a string representation of the Conclusion. */ toString(): string { const truncatedContent = this.content.length > 50 ? `${this.content.slice(0, 50)}...` : this.content - return `Observation(id='${this.id}', content='${truncatedContent}')` + return `Conclusion(id='${this.id}', content='${truncatedContent}')` } } /** - * Scoped access to observations for a specific observer/observed relationship. + * Scoped access to conclusions 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, and delete conclusions * that are automatically scoped to a specific observer/observed pair. * - * Typically accessed via `peer.observations` (for self-observations) or - * `peer.observationsOf(target)` (for observations about another peer). + * Typically accessed via `peer.conclusions` (for self-conclusions) or + * `peer.conclusionsOf(target)` (for conclusions about another peer). * * @example * ```typescript - * // Get self-observations - * const observations = peer.observations - * const obsList = await observations.list() - * const searchResults = await observations.query('preferences') + * // Get self-conclusions + * const conclusions = peer.conclusions + * const obsList = await conclusions.list() + * const searchResults = await conclusions.query('preferences') * - * // Get observations about another peer - * const bobObservations = peer.observationsOf('bob') - * const bobList = await bobObservations.list() + * // Get conclusions about another peer + * const bobConclusions = peer.conclusionsOf('bob') + * const bobList = await bobConclusions.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} + * This class requires the core Honcho SDK to support conclusion endpoints. + * The conclusion endpoints are: + * - POST /workspaces/{workspace_id}/conclusions/list + * - POST /workspaces/{workspace_id}/conclusions/query + * - DELETE /workspaces/{workspace_id}/conclusions/{conclusion_id} */ -export class ObservationScope { +export class ConclusionScope { private _client: HonchoCore /** @@ -139,7 +135,7 @@ export class ObservationScope { readonly observed: string /** - * Initialize an ObservationScope. + * Initialize a ConclusionScope. * * @param client - The Honcho client instance * @param workspaceId - The workspace ID @@ -159,18 +155,18 @@ export class ObservationScope { } /** - * List observations in this scope. + * List conclusions in this scope. * * @param page - Page number (1-indexed) * @param size - Number of results per page * @param session - Optional session (ID string or Session object) to filter by - * @returns Promise resolving to list of Observation objects + * @returns Promise resolving to list of Conclusion objects */ async list( page: number = 1, size: number = 50, session?: string | Session - ): Promise { + ): Promise { const resolvedSessionId = session ? typeof session === 'string' ? session @@ -184,8 +180,8 @@ export class ObservationScope { filters.session_id = resolvedSessionId } - // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations - const response = await (this._client.workspaces as any).observations.list( + // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include conclusions + const response = await (this._client.workspaces as any).conclusions.list( this.workspaceId, { filters, @@ -195,30 +191,30 @@ export class ObservationScope { ) return (response.items ?? []).map((item: unknown) => - Observation.fromApiResponse(item as Record) + Conclusion.fromApiResponse(item as Record) ) } /** - * Semantic search for observations in this scope. + * Semantic search for conclusions in this scope. * * @param query - The search query string * @param topK - Maximum number of results to return * @param distance - Maximum cosine distance threshold (0.0-1.0) - * @returns Promise resolving to list of matching Observation objects + * @returns Promise resolving to list of matching Conclusion objects */ async query( query: string, topK: number = 10, distance?: number - ): Promise { + ): Promise { const filters: Record = { observer: this.observer, observed: this.observed, } - // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations - const response = await (this._client.workspaces as any).observations.query( + // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include conclusions + const response = await (this._client.workspaces as any).conclusions.query( this.workspaceId, { query, @@ -229,53 +225,53 @@ export class ObservationScope { ) return (response ?? []).map((item: unknown) => - Observation.fromApiResponse(item as Record) + Conclusion.fromApiResponse(item as Record) ) } /** - * Delete an observation by ID. + * Delete a conclusion by ID. * - * @param observationId - The ID of the observation to delete + * @param conclusionId - The ID of the conclusion to delete */ - async delete(observationId: string): Promise { - // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations - await (this._client.workspaces as any).observations.delete( + async delete(conclusionId: string): Promise { + // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include conclusions + await (this._client.workspaces as any).conclusions.delete( this.workspaceId, - observationId + conclusionId ) } /** - * Create observations in this scope. + * Create conclusions in this scope. * - * @param observations - Single observation or array of observations with content and sessionId - * @returns Promise resolving to list of created Observation objects + * @param conclusions - Single conclusion or array of conclusions with content and sessionId + * @returns Promise resolving to list of created Conclusion objects * * @example * ```typescript - * // Create a single observation - * const observations = await peer.observations.create( + * // Create a single conclusion + * const conclusions = await peer.conclusions.create( * { content: 'User prefers dark mode', sessionId: 'session1' } * ) * - * // Create multiple observations - * const observations = await peer.observations.create([ + * // Create multiple conclusions + * const conclusions = await peer.conclusions.create([ * { content: 'User prefers dark mode', sessionId: 'session1' }, * { content: 'User is interested in AI', sessionId: 'session1' }, * ]) * ``` */ async create( - observations: ObservationCreateParam | ObservationCreateParam[] - ): Promise { + conclusions: ConclusionCreateParam | ConclusionCreateParam[] + ): Promise { // Normalize to array - const observationArray = Array.isArray(observations) - ? observations - : [observations] + const conclusionArray = Array.isArray(conclusions) + ? conclusions + : [conclusions] // Build the request body with observer/observed from scope - const requestObservations = observationArray.map((obs) => ({ + const requestConclusions = conclusionArray.map((obs) => ({ content: obs.content, session_id: typeof obs.sessionId === 'string' ? obs.sessionId : obs.sessionId.id, @@ -283,14 +279,14 @@ export class ObservationScope { 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( + // biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include conclusions + const response = await (this._client.workspaces as any).conclusions.create( this.workspaceId, - { observations: requestObservations } + { conclusions: requestConclusions } ) return (response ?? []).map((item: unknown) => - Observation.fromApiResponse(item as Record) + Conclusion.fromApiResponse(item as Record) ) } @@ -298,15 +294,13 @@ export class ObservationScope { * Get the computed representation for this scope. * * This returns the working representation (narrative) built from the - * observations in this scope. + * conclusions in this scope. * * @param options - Optional options to configure the representation - * @returns Promise resolving to a Representation object + * @returns Promise resolving to a string of the representation */ - async getRepresentation( - options?: RepresentationOptions - ): Promise { - const response = await this._client.workspaces.peers.workingRepresentation( + async getRepresentation(options?: RepresentationOptions): Promise { + const response = await this._client.workspaces.peers.representation( this.workspaceId, this.observer, { @@ -314,25 +308,17 @@ export class ObservationScope { search_query: options?.searchQuery, search_top_k: options?.searchTopK, search_max_distance: options?.searchMaxDistance, - include_most_derived: options?.includeMostDerived, - max_observations: options?.maxObservations, + include_most_frequent: options?.includeMostFrequent, + max_conclusions: options?.maxConclusions, } ) - - const maybe = response as - | RepresentationData - | { representation?: RepresentationData | null } - | null - const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe - ? (maybe as { representation?: RepresentationData | null }).representation - : maybe) ?? { explicit: [], deductive: [] } - return Representation.fromData(rep as RepresentationData) + return response.representation } /** - * Return a string representation of the ObservationScope. + * Return a string representation of the ConclusionScope. */ toString(): string { - return `ObservationScope(workspaceId='${this.workspaceId}', observer='${this.observer}', observed='${this.observed}')` + return `ConclusionScope(workspaceId='${this.workspaceId}', observer='${this.observer}', observed='${this.observed}')` } } diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts index 22597052..54a0503a 100644 --- a/sdks/typescript/src/index.ts +++ b/sdks/typescript/src/index.ts @@ -3,7 +3,7 @@ export type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages' export { Honcho } from './client' -export { Observation, ObservationScope } from './observations' +export { Conclusion, ConclusionScope } from './conclusions' export { Page } from './pagination' export { Peer, PeerContext } from './peer' export { Session, SessionPeerConfig } from './session' @@ -14,30 +14,31 @@ export { type SummaryData, } from './session_context' export { + type Conclusion as ConclusionData, + type ConclusionQueryParams, type DialecticStreamChunk, type DialecticStreamDelta, DialecticStreamResponse, - type Observation as ObservationData, - type ObservationQueryParams, } from './types' // Export validation types for advanced usage export type { ChatQuery, ContextParams, - DeriverStatusOptions, FileUpload, Filters, + GetRepresentationParams, HonchoConfig, MessageAddition, MessageCreate, PeerAddition, PeerConfig, + PeerGetRepresentationParams, PeerMetadata, PeerRemoval, + QueueStatusOptions, SessionConfig, SessionMetadata, - WorkingRepParams, WorkspaceConfig, WorkspaceMetadata, } from './validation' diff --git a/sdks/typescript/src/peer.ts b/sdks/typescript/src/peer.ts index a8701a3e..8623f749 100644 --- a/sdks/typescript/src/peer.ts +++ b/sdks/typescript/src/peer.ts @@ -1,6 +1,6 @@ import type HonchoCore from '@honcho-ai/core' import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages' -import { ObservationScope } from './observations' +import { ConclusionScope } from './conclusions' import { Page } from './pagination' import { Representation, @@ -16,7 +16,7 @@ import { LimitSchema, MessageContentSchema, MessageMetadataSchema, - PeerWorkingRepParamsSchema, + PeerGetRepresentationParamsSchema, SearchQuerySchema, type MessageCreate as ValidatedMessageCreate, } from './validation' @@ -111,6 +111,8 @@ export class Peer { * @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. + * @param reasoningLevel - Optional reasoning level for the query: "minimal", "low", "medium", + * "high", or "extra-high". Defaults to "low" if not provided. * @returns Promise resolving to: * - For non-streaming: response string or null if no relevant information * - For streaming: DialecticStreamResponse that can be iterated over @@ -121,6 +123,7 @@ export class Peer { stream?: boolean target?: string | Peer session?: string | Session + reasoningLevel?: string } ): Promise { const targetId = options?.target @@ -139,6 +142,7 @@ export class Peer { stream: options?.stream, target: targetId, session: resolvedSessionId, + reasoningLevel: options?.reasoningLevel, }) if (chatParams.stream) { @@ -147,6 +151,7 @@ export class Peer { stream: true, target: chatParams.target, session_id: chatParams.session, + reasoning_level: chatParams.reasoningLevel, } const url = `${this._client.baseURL}/v2/workspaces/${this.workspaceId}/peers/${this.id}/chat` @@ -223,6 +228,7 @@ export class Peer { stream: false, target: chatParams.target, session_id: chatParams.session, + reasoning_level: chatParams.reasoningLevel, } ) if (!response.content || response.content === 'None') { @@ -264,7 +270,7 @@ export class Peer { * * @param content - The text content for the message * @param options.metadata - Optional metadata to associate with the message - * @param options.configuration - Optional message-level configuration (e.g., deriver settings) + * @param options.configuration - Optional message-level configuration (e.g., reasoning settings) * @param options.created_at - Optional ISO 8601 timestamp for the message * @returns A new message object with this peer's ID and the provided content */ @@ -481,75 +487,69 @@ export class Peer { } /** - * Get a working representation for this peer. + * Get a subset of Honcho's Representation of a peer. * - * Makes an API call to retrieve the working representation for this peer. + * Makes an API call to retrieve the representation for this peer. * * @param session - Optional session to scope the representation to. * @param target - Optional target peer to get the representation of. If provided, * returns the representation of the target from the perspective of this peer. * @param options - Optional representation options to filter and configure the results - * @returns Promise resolving to a Representation object containing explicit and deductive observations + * @returns Promise resolving to a Representation object containing explicit and deductive conclusions * * @example * ```typescript * // Get global representation - * const globalRep = await peer.workingRep() + * const globalRep = await peer.getRepresentation() * console.log(globalRep.toString()) * * // Get representation scoped to a session - * const sessionRep = await peer.workingRep('session-123') + * const sessionRep = await peer.getRepresentation('session-123') * * // Get representation with semantic search - * const searchedRep = await peer.workingRep(undefined, undefined, { + * const searchedRep = await peer.getRepresentation(undefined, undefined, { * searchQuery: 'preferences', * searchTopK: 10, - * maxObservations: 50 + * maxConclusions: 50 * }) * ``` */ - async workingRep( + async getRepresentation( session?: string | Session, target?: string | Peer, options?: RepresentationOptions - ): Promise { - const workingRepParams = PeerWorkingRepParamsSchema.parse({ + ): Promise { + const getRepresentationParams = PeerGetRepresentationParamsSchema.parse({ session, target, options, }) - const sessionId = workingRepParams.session - ? typeof workingRepParams.session === 'string' - ? workingRepParams.session - : workingRepParams.session.id + const sessionId = getRepresentationParams.session + ? typeof getRepresentationParams.session === 'string' + ? getRepresentationParams.session + : getRepresentationParams.session.id : undefined - const targetId = workingRepParams.target - ? typeof workingRepParams.target === 'string' - ? workingRepParams.target - : workingRepParams.target.id + const targetId = getRepresentationParams.target + ? typeof getRepresentationParams.target === 'string' + ? getRepresentationParams.target + : getRepresentationParams.target.id : undefined - const response = await this._client.workspaces.peers.workingRepresentation( + const response = await this._client.workspaces.peers.representation( this.workspaceId, this.id, { session_id: sessionId, target: targetId, - search_query: workingRepParams.options?.searchQuery, - search_top_k: workingRepParams.options?.searchTopK, - search_max_distance: workingRepParams.options?.searchMaxDistance, - include_most_derived: workingRepParams.options?.includeMostDerived, - max_observations: workingRepParams.options?.maxObservations, + search_query: getRepresentationParams.options?.searchQuery, + search_top_k: getRepresentationParams.options?.searchTopK, + search_max_distance: getRepresentationParams.options?.searchMaxDistance, + include_most_frequent: + getRepresentationParams.options?.includeMostFrequent, + max_conclusions: getRepresentationParams.options?.maxConclusions, } ) - const maybe = response as - | RepresentationData - | { representation?: RepresentationData | null } - | null - const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe - ? (maybe as { representation?: RepresentationData | null }).representation - : maybe) ?? { explicit: [], deductive: [] } - return Representation.fromData(rep as RepresentationData) + return response.representation } /** @@ -590,7 +590,7 @@ export class Peer { : target.id : undefined - const response = await this._client.workspaces.peers.getContext( + const response = await this._client.workspaces.peers.context( this.workspaceId, this.id, { @@ -598,8 +598,8 @@ export class Peer { search_query: options?.searchQuery, search_top_k: options?.searchTopK, search_max_distance: options?.searchMaxDistance, - include_most_derived: options?.includeMostDerived, - max_observations: options?.maxObservations, + include_most_frequent: options?.includeMostFrequent, + max_conclusions: options?.maxConclusions, } ) @@ -609,61 +609,56 @@ export class Peer { } /** - * Access this peer's self-observations (where observer == observed == self). + * Access this peer's self-conclusions (where observer == observed == self). * - * This property provides a convenient way to access observations that this peer - * has made about themselves. Use this for self-observation scenarios. + * This property provides a convenient way to access conclusions that this peer + * has made about themselves. Use this for self-conclusion scenarios. * - * @returns An ObservationScope scoped to this peer's self-observations + * @returns A ConclusionScope scoped to this peer's self-conclusions * * @example * ```typescript - * // List self-observations - * const obsList = await peer.observations.list() + * // List self-conclusions + * const obsList = await peer.conclusions.list() * - * // Search self-observations - * const results = await peer.observations.query('preferences') + * // Search self-conclusions + * const results = await peer.conclusions.query('preferences') * - * // Delete a self-observation - * await peer.observations.delete('obs-123') + * // Delete a self-conclusion + * await peer.conclusions.delete('obs-123') * ``` */ - get observations(): ObservationScope { - return new ObservationScope( - this._client, - this.workspaceId, - this.id, - this.id - ) + get conclusions(): ConclusionScope { + return new ConclusionScope(this._client, this.workspaceId, this.id, this.id) } /** - * Access observations this peer has made about another peer. + * Access conclusions this peer has made about another peer. * - * This method provides scoped access to observations where this peer is the + * This method provides scoped access to conclusions where this peer is the * observer and the target is the observed peer. * * @param target - The target peer (either a Peer object or peer ID string) - * @returns An ObservationScope scoped to this peer's observations of the target + * @returns A ConclusionScope scoped to this peer's conclusions of the target * * @example * ```typescript - * // Get observations about another peer - * const bobObservations = peer.observationsOf('bob') + * // Get conclusions about another peer + * const bobConclusions = peer.conclusionsOf('bob') * - * // List observations - * const obsList = await bobObservations.list() + * // List conclusions + * const obsList = await bobConclusions.list() * - * // Search observations - * const results = await bobObservations.query('work history') + * // Search conclusions + * const results = await bobConclusions.query('work history') * - * // Get the representation from these observations - * const rep = await bobObservations.getRepresentation() + * // Get the representation from these conclusions + * const rep = await bobConclusions.getRepresentation() * ``` */ - observationsOf(target: string | Peer): ObservationScope { + conclusionsOf(target: string | Peer): ConclusionScope { const targetId = typeof target === 'string' ? target : target.id - return new ObservationScope( + return new ConclusionScope( this._client, this.workspaceId, this.id, @@ -699,7 +694,7 @@ export class PeerContext { readonly targetId: string /** - * The working representation (may be null if no observations exist). + * The working representation (may be null if no conclusions exist). */ readonly representation: Representation | null diff --git a/sdks/typescript/src/representation.ts b/sdks/typescript/src/representation.ts index 494b4cc9..f80b3f0f 100644 --- a/sdks/typescript/src/representation.ts +++ b/sdks/typescript/src/representation.ts @@ -3,12 +3,12 @@ */ export interface RepresentationOptions { /** - * Semantic search query to filter relevant observations. + * Semantic search query to filter relevant conclusions. */ searchQuery?: string /** - * Number of semantically relevant facts to return. + * Number of semantically relevant conclusions to return. */ searchTopK?: number @@ -18,94 +18,94 @@ export interface RepresentationOptions { searchMaxDistance?: number /** - * Whether to include the most derived observations. + * Whether to include the most frequent conclusions. */ - includeMostDerived?: boolean + includeMostFrequent?: boolean /** - * Maximum number of observations to include. + * Maximum number of conclusions to include. */ - maxObservations?: number + maxConclusions?: number } /** - * Metadata associated with an observation. + * Metadata associated with a conclusion. */ -export interface ObservationMetadata { +export interface ConclusionMetadata { created_at: string message_ids: Array<[number, number]> session_name: string } /** - * An explicit observation with full metadata. + * An explicit conclusion with full metadata. * Represents facts LITERALLY stated - direct quotes or clear paraphrases only. */ -export interface ExplicitObservationBase { +export interface ExplicitConclusionBase { content: string } /** - * Base interface for deductive observations - logical conclusions. + * Base interface for deductive conclusions - logical conclusions. */ -export interface DeductiveObservationBase { +export interface DeductiveConclusionBase { premises: string[] conclusion: string } -export interface ExplicitObservation - extends ExplicitObservationBase, - ObservationMetadata {} +export interface ExplicitConclusion + extends ExplicitConclusionBase, + ConclusionMetadata {} /** - * A deductive observation with full metadata. + * A deductive conclusion with full metadata. * Represents conclusions that MUST be true given explicit facts and premises. */ -export interface DeductiveObservation - extends DeductiveObservationBase, - ObservationMetadata {} +export interface DeductiveConclusion + extends DeductiveConclusionBase, + ConclusionMetadata {} /** * Raw representation data structure returned from the API. */ export interface RepresentationData { - explicit: ExplicitObservation[] - deductive: DeductiveObservation[] + explicit: ExplicitConclusion[] + deductive: DeductiveConclusion[] } /** - * A Representation is a traversable and diffable map of observations. + * A Representation is a traversable and diffable map of conclusions. * - * At the base, we have a list of explicit observations, derived from a peer's messages. - * From there, deductive observations can be made by establishing logical relationships - * between explicit observations. + * At the base, we have a list of explicit conclusions, derived from a peer's messages. + * From there, deductive conclusions can be made by establishing logical relationships + * between explicit conclusions. * - * All of a peer's observations are stored as documents in a collection. These documents + * All of a peer's conclusions are stored as documents in a collection. These documents * can be queried in various ways to produce this Representation object. * * A "working representation" is a version of this data structure representing the most - * recent observations within a single session. + * recent conclusions within a single session. */ export class Representation { /** * Facts LITERALLY stated - direct quotes or clear paraphrases only, no interpretation or inference. */ - explicit: ExplicitObservation[] + explicit: ExplicitConclusion[] /** * Conclusions that MUST be true given explicit facts and premises - strict logical necessities. */ - deductive: DeductiveObservation[] + deductive: DeductiveConclusion[] /** - * Create a new Representation from observation lists. + * Create a new Representation from conclusion lists. * - * @param explicit - List of explicit observations - * @param deductive - List of deductive observations + * @param explicit - List of explicit conclusions + * @param deductive - List of deductive conclusions */ constructor( - explicit: ExplicitObservation[] = [], - deductive: DeductiveObservation[] = [] + explicit: ExplicitConclusion[] = [], + deductive: DeductiveConclusion[] = [] ) { this.explicit = explicit this.deductive = deductive @@ -114,7 +114,7 @@ export class Representation { /** * Check if the representation is empty. * - * @returns True if both explicit and deductive observation lists are empty + * @returns True if both explicit and deductive conclusion lists are empty */ isEmpty(): boolean { return this.explicit.length === 0 && this.deductive.length === 0 @@ -122,13 +122,13 @@ export class Representation { /** * Given this and another representation, return a new representation with only - * observations that are unique to the other. + * conclusions that are unique to the other. * * Note: This only removes literal duplicates based on stringified comparison, * not semantically equivalent ones. * * @param other - The representation to compare against - * @returns A new Representation containing only observations unique to other + * @returns A new Representation containing only conclusions unique to other */ diff(other: Representation): Representation { const thisExplicitSet = new Set( @@ -151,21 +151,21 @@ export class Representation { /** * Merge another representation into this one. * - * This automatically deduplicates explicit and deductive observations. - * Preserves order of observations to retain FIFO order. + * This automatically deduplicates explicit and deductive conclusions. + * Preserves order of conclusions to retain FIFO order. * - * Note: Observations with the same timestamp may not have order preserved, + * Note: Conclusions with the same timestamp may not have order preserved, * but that's acceptable since they're from the same timestamp. * * @param other - The representation to merge into this one - * @param maxObservations - Optional maximum number of observations to keep per type + * @param maxConclusions - Optional maximum number of conclusions to keep per type */ - merge(other: Representation, maxObservations?: number): void { + merge(other: Representation, maxConclusions?: number): void { // Deduplicate by converting to Set using hash, then back to array - const explicitMap = new Map() - const deductiveMap = new Map() + const explicitMap = new Map() + const deductiveMap = new Map() - // Add existing observations + // Add existing conclusions for (const obs of this.explicit) { explicitMap.set(this._hashExplicit(obs), obs) } @@ -173,7 +173,7 @@ export class Representation { deductiveMap.set(this._hashDeductive(obs), obs) } - // Add new observations (overwrites duplicates) + // Add new conclusions (overwrites duplicates) for (const obs of other.explicit) { explicitMap.set(this._hashExplicit(obs), obs) } @@ -193,10 +193,10 @@ export class Representation { this._parseTimestampForSort(b.created_at) ) - // Apply max observations limit if specified - if (maxObservations !== undefined) { - this.explicit = this.explicit.slice(-maxObservations) - this.deductive = this.deductive.slice(-maxObservations) + // Apply max conclusions limit if specified + if (maxConclusions !== undefined) { + this.explicit = this.explicit.slice(-maxConclusions) + this.deductive = this.deductive.slice(-maxConclusions) } } @@ -293,7 +293,7 @@ export class Representation { toMarkdown(): string { const parts: string[] = [] - parts.push('## Explicit Observations\n') + parts.push('## Explicit Conclusions\n') for (let i = 0; i < this.explicit.length; i++) { const obs = this.explicit[i] const timestamp = this._stripMicroseconds(obs.created_at) @@ -301,7 +301,7 @@ export class Representation { } parts.push('') - parts.push('## Deductive Observations\n') + parts.push('## Deductive Conclusions\n') for (let i = 0; i < this.deductive.length; i++) { const obs = this.deductive[i] const timestamp = this._stripMicroseconds(obs.created_at) @@ -330,10 +330,10 @@ export class Representation { } /** - * Create a hash string for an explicit observation for deduplication. + * Create a hash string for an explicit conclusion for deduplication. * Based on content, created_at, and session_name. */ - private _hashExplicit(obs: ExplicitObservation): string { + private _hashExplicit(obs: ExplicitConclusion): string { return JSON.stringify({ content: obs.content, created_at: obs.created_at, @@ -342,10 +342,10 @@ export class Representation { } /** - * Create a hash string for a deductive observation for deduplication. + * Create a hash string for a deductive conclusion for deduplication. * Based on conclusion, created_at, and session_name (premises not included). */ - private _hashDeductive(obs: DeductiveObservation): string { + private _hashDeductive(obs: DeductiveConclusion): string { return JSON.stringify({ conclusion: obs.conclusion, created_at: obs.created_at, diff --git a/sdks/typescript/src/session.ts b/sdks/typescript/src/session.ts index 6e38fe0c..4105ee41 100644 --- a/sdks/typescript/src/session.ts +++ b/sdks/typescript/src/session.ts @@ -1,37 +1,30 @@ import type HonchoCore from '@honcho-ai/core' import type { - DeriverStatus, - WorkspaceDeriverStatusParams, -} from '@honcho-ai/core/resources/index' + QueueStatusParams, + QueueStatusResponse, +} from '@honcho-ai/core/resources/workspaces/queue' import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages' import type { Uploadable } from '@honcho-ai/core/uploads' import { Page } from './pagination' import { Peer } from './peer' -import { - Representation, - type RepresentationData, - type RepresentationOptions, -} from './representation' +import type { RepresentationOptions } from './representation' import { SessionContext, SessionSummaries, Summary } from './session_context' -// Disabled: observations not ready for release -// import type { Observation, ObservationQueryParams } from './types' import { ContextParamsSchema, - type DeriverStatusOptions, FileUploadSchema, FilterSchema, type Filters, + GetRepresentationParamsSchema, LimitSchema, type MessageAddition, MessageAdditionSchema, - // ObservationQueryParamsSchema, // Disabled: observations not ready for release type PeerAddition, PeerAdditionSchema, type PeerRemoval, PeerRemovalSchema, + type QueueStatusOptions, SearchQuerySchema, SessionPeerConfigSchema, - WorkingRepParamsSchema, } from './validation' /** @@ -57,7 +50,7 @@ export class SessionPeerConfig { observe_others?: boolean | null /** - * Initialize SessionPeerConfig with observation settings. + * Initialize SessionPeerConfig with conclusion settings. * * @param observe_me - Whether other peers should observe this peer in the session * @param observe_others - Whether this peer should observe others in the session @@ -82,7 +75,7 @@ export class SessionPeerConfig { * representations of each other. * * Key features: - * - Multi-peer conversations with configurable observation settings + * - Multi-peer conversations with configurable conclusion settings * - Message storage and retrieval with filtering capabilities * - Context optimization for token-limited scenarios * - File upload support with automatic message creation @@ -179,7 +172,7 @@ export class Session { * Makes an API call to add one or more peers to this session. Adding peers * creates bidirectional relationships and allows them to participate in * the session's conversations. Peers can be added with optional session-specific - * configuration to control observation behaviors. + * configuration to control conclusion behaviors. * * @param peers - Peers to add to the session. Can be: * - string: Single peer ID @@ -336,7 +329,7 @@ export class Session { * Get the configuration for a peer in this session. * * Makes an API call to retrieve the session-specific configuration for a peer. - * This includes observation settings that control how this peer interacts + * This includes conclusion settings that control how this peer interacts * with other peers within this session context. * * @param peer - The peer to get configuration for. Can be peer ID string or Peer object @@ -344,7 +337,7 @@ export class Session { */ async getPeerConfig(peer: string | Peer): Promise { const peerId = typeof peer === 'string' ? peer : peer.id - return await this._client.workspaces.sessions.peers.getConfig( + return await this._client.workspaces.sessions.peers.config( this.workspaceId, this.id, peerId @@ -355,11 +348,11 @@ export class Session { * Set the configuration for a peer in this session. * * Makes an API call to update the session-specific configuration for a peer. - * This controls observation behaviors and theory-of-mind formation within + * This controls conclusion behaviors and theory-of-mind formation within * this session context. * * @param peer - The peer to configure. Can be peer ID string or Peer object - * @param config - SessionPeerConfig object specifying the observation settings + * @param config - SessionPeerConfig object specifying the conclusion settings */ async setPeerConfig( peer: string | Peer, @@ -541,7 +534,7 @@ export class Session { * Makes an API call to permanently delete this session and all related data including: * - Messages * - Message embeddings - * - Observations + * - Conclusions * - Session-Peer associations * - Background processing queue items * @@ -608,7 +601,7 @@ export class Session { * If given with `peerPerspective`, will get the representation and card * for this peer from the perspective of that peer. * @param options.lastUserMessage - The most recent message, used to fetch semantically relevant - * observations and returned as part of the context object. + * conclusions and returned as part of the context object. * Can be either a message ID string or a Message object. * @param options.peerPerspective - A peer to get context for. If given, response will attempt to * include representation and card from the perspective of that peer. @@ -707,7 +700,7 @@ export class Session { ? contextParams.lastUserMessage : contextParams.lastUserMessage?.id - const context = await this._client.workspaces.sessions.getContext( + const context = await this._client.workspaces.sessions.context( this.workspaceId, this.id, { @@ -720,9 +713,9 @@ export class Session { search_top_k: contextParams.representationOptions?.searchTopK, search_max_distance: contextParams.representationOptions?.searchMaxDistance, - include_most_derived: - contextParams.representationOptions?.includeMostDerived, - max_observations: contextParams.representationOptions?.maxObservations, + include_most_frequent: + contextParams.representationOptions?.includeMostFrequent, + max_conclusions: contextParams.representationOptions?.maxConclusions, } ) // Convert the summary response to Summary object if present @@ -803,105 +796,20 @@ export class Session { } /** - * List all observations for this session. + * Get the queue processing status for this session, optionally scoped to an observer or sender. * - * Observations are theory-of-mind data (documents) that peers have formed about each other. - * Returns paginated results that can be filtered by observer_id and observed_id. - * - * @param filters - Optional filters to scope the observations: see [filters documentation](https://docs.honcho.dev/v2/guides/using-filters). - * @returns A paginated list of Observation objects. - * - * @example - * ```typescript - * const observations = await session.listObservations() - * for await (const observation of observations) { - * console.log(`${observation.observer_id} observed: ${observation.content}`) - * } - * ``` - */ - // Disabled: observations not ready for release - // async listObservations(filters?: Filters): Promise> { - // const validatedFilters = filters ? FilterSchema.parse(filters) : undefined - // const response = await this._client.workspaces.sessions.observations.list( - // this.workspaceId, - // this.id, - // { filters: validatedFilters } - // ) - // return new Page(response) - // } - - /** - * Query observations using semantic search. - * - * Performs vector similarity search on observations to find semantically relevant results. - * Use this to find observations related to a specific topic or concept. - * - * @param params - Query parameters - * @param params.query - The semantic search query - * @param params.top_k - Number of results to return (1-100, default: 10) - * @param params.distance - Maximum cosine distance threshold for results (0.0-1.0) - * @param params.filters - Optional filters to scope the query - * @returns A list of Observation objects matching the query - * - * @example - * ```typescript - * const observations = await session.queryObservations({ - * query: "user preferences about music", - * top_k: 5, - * distance: 0.8 - * }) - * ``` - */ - // Disabled: observations not ready for release - // async queryObservations( - // params: ObservationQueryParams - // ): Promise { - // const validated = ObservationQueryParamsSchema.parse(params) - // return await this._client.workspaces.sessions.observations.query( - // this.workspaceId, - // this.id, - // validated - // ) - // } - - /** - * Delete a specific observation by ID. - * - * This permanently deletes the observation (document) from the theory-of-mind system. - * This action cannot be undone. - * - * @param observationId - The ID of the observation to delete - * @returns A promise that resolves when the observation is deleted - * - * @example - * ```typescript - * await session.deleteObservation('obs_123abc') - * ``` - */ - // Disabled: observations not ready for release - // async deleteObservation(observationId: string): Promise { - // await this._client.workspaces.sessions.observations.delete( - // this.workspaceId, - // this.id, - // observationId - // ) - // } - - /** - * Get the deriver processing status for this session, optionally scoped to an observer or sender. - * - * Makes an API call to retrieve the current status of the deriver processing queue. - * The deriver is responsible for processing messages and updating peer representations. + * Makes an API call to retrieve the current status of the queue processing queue. + * The queue is responsible for processing messages and updating peer representations. * This method automatically scopes the status to this session. * * @param options - Configuration options for the status request * @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 + * @returns Promise resolving to the queue status information including work unit counts */ - async getDeriverStatus( + async getQueueStatus( options?: Omit< - DeriverStatusOptions, + QueueStatusOptions, 'sessionId' | 'observerId' | 'senderId' > & { observer?: string | Peer @@ -912,7 +820,7 @@ export class Session { completedWorkUnits: number inProgressWorkUnits: number pendingWorkUnits: number - sessions?: Record + sessions?: Record }> { const resolvedObserverId = options?.observer ? typeof options.observer === 'string' @@ -925,13 +833,13 @@ export class Session { : options.sender.id : undefined - const queryParams: WorkspaceDeriverStatusParams = { + const queryParams: QueueStatusParams = { session_id: this.id, // Always use this session's ID } if (resolvedObserverId) queryParams.observer_id = resolvedObserverId if (resolvedSenderId) queryParams.sender_id = resolvedSenderId - const status = await this._client.workspaces.deriverStatus( + const status = await this._client.workspaces.queue.status( this.workspaceId, queryParams ) @@ -946,8 +854,8 @@ export class Session { } /** - * Poll getDeriverStatus until pending_work_units and in_progress_work_units are both 0. - * This allows you to guarantee that all messages have been processed by the deriver for + * Poll getQueueStatus until pending_work_units and in_progress_work_units are both 0. + * This allows you to guarantee that all messages have been processed by the queue for * use with the dialectic endpoint. * * The polling estimates sleep time by assuming each work unit takes 1 second. @@ -956,12 +864,12 @@ export class Session { * @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 + * @returns Promise resolving to the final queue status when processing is complete * @throws Error if timeout is exceeded before processing completes */ - async pollDeriverStatus( + async pollQueueStatus( options?: Omit< - DeriverStatusOptions, + QueueStatusOptions, 'sessionId' | 'observerId' | 'senderId' > & { observer?: string | Peer @@ -972,13 +880,13 @@ export class Session { completedWorkUnits: number inProgressWorkUnits: number pendingWorkUnits: number - sessions?: Record + sessions?: Record }> { const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes const startTime = Date.now() while (true) { - const status = await this.getDeriverStatus(options) + const status = await this.getQueueStatus(options) if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) { return status } @@ -1095,7 +1003,7 @@ export class Session { } /** - * Get the current working representation of a peer in this session. + * Get a subset of Honcho's Representation of a peer in this session. * * Makes an API call to retrieve the session-scoped representation that has been * built for a peer. This can be either the peer's global representation or @@ -1105,71 +1013,65 @@ export class Session { * @param target - Optional target peer. If provided, returns what `peer` knows about * `target` within this session context rather than `peer`'s global representation * @param options - Optional representation options to filter and configure the results - * @returns Promise resolving to a Representation object containing explicit and deductive observations + * @returns Promise resolving to a Representation string * * @example * ```typescript * // Get peer's global representation in this session - * const globalRep = await session.workingRep('user123') - * console.log(globalRep.toString()) + * const globalRep = await session.getRepresentation('user123') + * console.log(globalRep) * * // Get what user123 knows about assistant in this session - * const localRep = await session.workingRep('user123', 'assistant') + * const localRep = await session.getRepresentation('user123', 'assistant') * * // Get representation with semantic search - * const searchedRep = await session.workingRep('user123', undefined, { + * const searchedRep = await session.getRepresentation('user123', undefined, { * searchQuery: 'preferences', * searchTopK: 10 * }) * ``` */ - async workingRep( + async getRepresentation( peer: string | Peer, target?: string | Peer, options?: { searchQuery?: string searchTopK?: number searchMaxDistance?: number - includeMostDerived?: boolean - maxObservations?: number + includeMostFrequent?: boolean + maxConclusions?: number } - ): Promise { - const workingRepParams = WorkingRepParamsSchema.parse({ + ): Promise { + const getRepresentationParams = GetRepresentationParamsSchema.parse({ peer, target, options, }) const peerId = - typeof workingRepParams.peer === 'string' - ? workingRepParams.peer - : workingRepParams.peer.id - const targetId = workingRepParams.target - ? typeof workingRepParams.target === 'string' - ? workingRepParams.target - : workingRepParams.target.id + typeof getRepresentationParams.peer === 'string' + ? getRepresentationParams.peer + : getRepresentationParams.peer.id + const targetId = getRepresentationParams.target + ? typeof getRepresentationParams.target === 'string' + ? getRepresentationParams.target + : getRepresentationParams.target.id : undefined - const response = await this._client.workspaces.peers.workingRepresentation( + const response = await this._client.workspaces.peers.representation( this.workspaceId, peerId, { session_id: this.id, target: targetId, - search_query: workingRepParams.options?.searchQuery, - search_top_k: workingRepParams.options?.searchTopK, - search_max_distance: workingRepParams.options?.searchMaxDistance, - include_most_derived: workingRepParams.options?.includeMostDerived, - max_observations: workingRepParams.options?.maxObservations, + search_query: getRepresentationParams.options?.searchQuery, + search_top_k: getRepresentationParams.options?.searchTopK, + search_max_distance: getRepresentationParams.options?.searchMaxDistance, + include_most_frequent: + getRepresentationParams.options?.includeMostFrequent, + max_conclusions: getRepresentationParams.options?.maxConclusions, } ) - const maybe = response as - | RepresentationData - | { representation?: RepresentationData | null } - | null - const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe - ? (maybe as { representation?: RepresentationData | null }).representation - : maybe) ?? { explicit: [], deductive: [] } - return Representation.fromData(rep as RepresentationData) + return response.representation } /** diff --git a/sdks/typescript/src/types.ts b/sdks/typescript/src/types.ts index 6ba97e75..bbb2bcf3 100644 --- a/sdks/typescript/src/types.ts +++ b/sdks/typescript/src/types.ts @@ -5,9 +5,9 @@ import type { Session } from './session' */ /** - * Observation - external view of a document (theory-of-mind data). + * Conclusion - external view of a document (theory-of-mind data). */ -export interface Observation { +export interface Conclusion { id: string content: string observer_id: string @@ -17,19 +17,19 @@ export interface Observation { } /** - * Parameters for creating an observation. + * Parameters for creating a conclusion. */ -export interface ObservationCreateParam { - /** The observation content/text */ +export interface ConclusionCreateParam { + /** The conclusion content/text */ content: string - /** The session this observation relates to (ID string or Session object) */ + /** The session this conclusion relates to (ID string or Session object) */ sessionId: string | Session } /** - * Parameters for semantic search of observations. + * Parameters for semantic search of conclusions. */ -export interface ObservationQueryParams { +export interface ConclusionQueryParams { query: string top_k?: number distance?: number @@ -41,10 +41,6 @@ export interface ObservationQueryParams { */ export interface DialecticStreamDelta { content?: string - // Future fields can be added here: - // premises?: string[] - // tokens?: number - // analytics?: Record } /** diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts index 136fa5d2..f8e84510 100644 --- a/sdks/typescript/src/validation.ts +++ b/sdks/typescript/src/validation.ts @@ -14,7 +14,7 @@ import { z } from 'zod' export const HonchoConfigSchema = z.object({ apiKey: z.string().optional(), environment: z.enum(['local', 'production']).optional(), - baseURL: z.string().url('Base URL must be a valid URL').optional(), + baseURL: z.url('Base URL must be a valid URL').optional(), workspaceId: z .string() .min(1, 'Workspace ID must be a non-empty string') @@ -142,6 +142,9 @@ export const ChatQuerySchema = z.object({ .transform((val) => val ? (typeof val === 'string' ? val : val.id) : undefined ), + reasoningLevel: z + .enum(['minimal', 'low', 'medium', 'high', 'extra-high']) + .optional(), }) /** @@ -173,12 +176,12 @@ export const RepresentationOptionsSchema = z.object({ .min(0.0, 'searchMaxDistance must be at least 0.0') .max(1.0, 'searchMaxDistance must be at most 1.0') .optional(), - includeMostDerived: z.boolean().optional(), - maxObservations: z + includeMostFrequent: z.boolean().optional(), + maxConclusions: z .number() .int() - .min(1, 'maxObservations must be at least 1') - .max(100, 'maxObservations must be at most 100') + .min(1, 'maxConclusions must be at least 1') + .max(100, 'maxConclusions must be at most 100') .optional(), }) @@ -224,7 +227,7 @@ export const ContextParamsSchema = z /** * Schema for deriver status options. */ -export const DeriverStatusOptionsSchema = z.object({ +export const QueueStatusOptionsSchema = z.object({ 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(), @@ -269,9 +272,9 @@ export const FileUploadSchema = z.object({ }) /** - * Schema for working representation parameters. + * Schema for get representation parameters. */ -export const WorkingRepParamsSchema = z.object({ +export const GetRepresentationParamsSchema = z.object({ peer: z.union([z.string(), z.object({ id: z.string() })]), target: z.union([z.string(), z.object({ id: z.string() })]).optional(), options: RepresentationOptionsSchema.extend({ @@ -280,9 +283,9 @@ export const WorkingRepParamsSchema = z.object({ }) /** - * Schema for peer working representation parameters. + * Schema for peer get representation parameters. */ -export const PeerWorkingRepParamsSchema = z.object({ +export const PeerGetRepresentationParamsSchema = z.object({ session: z.union([z.string(), z.object({ id: z.string() })]).optional(), target: z.union([z.string(), z.object({ id: z.string() })]).optional(), options: RepresentationOptionsSchema.extend({ @@ -349,9 +352,9 @@ export const LimitSchema = z .max(100, 'Limit must be less than or equal to 100') /** - * Schema for observation query parameters. + * Schema for conclusion query parameters. */ -export const ObservationQueryParamsSchema = z.object({ +export const ConclusionQueryParamsSchema = z.object({ query: SearchQuerySchema, top_k: z .number() @@ -380,16 +383,18 @@ export type MessageCreate = z.infer export type Filters = z.infer export type ChatQuery = z.infer export type ContextParams = z.infer -export type DeriverStatusOptions = z.infer +export type QueueStatusOptions = z.infer export type FileUpload = z.infer -export type WorkingRepParams = z.infer -export type PeerWorkingRepParams = z.infer +export type GetRepresentationParams = z.infer< + typeof GetRepresentationParamsSchema +> +export type PeerGetRepresentationParams = z.infer< + typeof PeerGetRepresentationParamsSchema +> export type PeerAddition = z.infer export type PeerRemoval = z.infer export type MessageAddition = z.infer export type WorkspaceMetadata = z.infer export type WorkspaceConfig = z.infer export type Limit = z.infer -export type ObservationQueryParams = z.infer< - typeof ObservationQueryParamsSchema -> +export type ConclusionQueryParams = z.infer diff --git a/src/config.py b/src/config.py index 75b05e9b..91ff1636 100644 --- a/src/config.py +++ b/src/config.py @@ -323,6 +323,19 @@ class DialecticLevelSettings(BaseModel): ) return self + @model_validator(mode="after") + def _validate_anthropic_thinking_budget(self) -> "DialecticLevelSettings": + """Ensure Anthropic thinking budget is >= 1024 when enabled.""" + if ( + self.PROVIDER == "anthropic" + and self.THINKING_BUDGET_TOKENS > 0 + and self.THINKING_BUDGET_TOKENS < 1024 + ): + raise ValueError( + f"THINKING_BUDGET_TOKENS must be >= 1024 for Anthropic provider when enabled (got {self.THINKING_BUDGET_TOKENS})" + ) + return self + class DialecticSettings(HonchoSettings): model_config = SettingsConfigDict( # pyright: ignore @@ -341,14 +354,14 @@ class DialecticSettings(HonchoSettings): ), "low": DialecticLevelSettings( PROVIDER="google", - MODEL="gemini-3-flash", + MODEL="gemini-3-flash-preview", THINKING_BUDGET_TOKENS=0, MAX_TOOL_ITERATIONS=5, ), "medium": DialecticLevelSettings( PROVIDER="anthropic", MODEL="claude-haiku-4-5", - THINKING_BUDGET_TOKENS=512, + THINKING_BUDGET_TOKENS=1024, MAX_TOOL_ITERATIONS=4, ), "high": DialecticLevelSettings( @@ -360,7 +373,7 @@ class DialecticSettings(HonchoSettings): "extra-high": DialecticLevelSettings( PROVIDER="anthropic", MODEL="claude-opus-4-5", - THINKING_BUDGET_TOKENS=512, + THINKING_BUDGET_TOKENS=2048, MAX_TOOL_ITERATIONS=10, ), } diff --git a/src/crud/peer.py b/src/crud/peer.py index 9006f272..8d3a761e 100644 --- a/src/crud/peer.py +++ b/src/crud/peer.py @@ -13,6 +13,7 @@ from src.crud.workspace import get_or_create_workspace from src.exceptions import ConflictException, ResourceNotFoundException from src.models import Peer from src.utils.filter import apply_filter +from src.utils.types import GetOrCreateResult logger = getLogger(__name__) @@ -38,7 +39,7 @@ async def get_or_create_peers( peers: list[schemas.PeerCreate], *, _retry: bool = False, -) -> list[models.Peer]: +) -> GetOrCreateResult[list[models.Peer]]: """ Get an existing list of peers or create new peers if they don't exist. Updates existing peers with metadata and configuration if provided. @@ -50,7 +51,7 @@ async def get_or_create_peers( _retry: Whether to retry the operation Returns: - List of peers if found or created + GetOrCreateResult containing the list of peers and whether any were created Raises: ConflictException: If we fail to get or create the peers @@ -132,7 +133,8 @@ async def get_or_create_peers( ) # Return combined list of existing and new peers - return existing_peers + new_peers + # created=True if any new peers were created + return GetOrCreateResult(existing_peers + new_peers, created=len(new_peers) > 0) @cache( @@ -224,7 +226,7 @@ async def update_peer( await get_or_create_peers( db, workspace_name, [schemas.PeerCreate(name=peer_name)] ) - )[0] + ).resource[0] needs_update = False diff --git a/src/crud/session.py b/src/crud/session.py index e447ba62..8a3146d2 100644 --- a/src/crud/session.py +++ b/src/crud/session.py @@ -18,6 +18,7 @@ from src.exceptions import ( ResourceNotFoundException, ) from src.utils.filter import apply_filter +from src.utils.types import GetOrCreateResult from .peer import get_or_create_peers, get_peer from .workspace import get_or_create_workspace @@ -102,7 +103,7 @@ async def get_or_create_session( workspace_name: str, *, _retry: bool = False, -) -> models.Session: +) -> GetOrCreateResult[models.Session]: """ Get or create a session in a workspace with specified peers. If the session already exists, the peers are added to the session. @@ -114,7 +115,7 @@ async def get_or_create_session( peer_names: List of peer names to add to the session _retry: Whether to retry the operation Returns: - The created session + GetOrCreateResult containing the session and whether it was created Raises: ResourceNotFoundException: If the session does not exist and create is false @@ -136,8 +137,9 @@ async def get_or_create_session( f"Session {session.name} not found in workspace {workspace_name}" ) - # Track if we need to update cache + # Track if we need to update cache and if session was created needs_cache_update = False + created = False # Check if session already exists if honcho_session is None: @@ -167,6 +169,7 @@ async def get_or_create_session( # Flush to ensure session exists in DB before adding peers and set flag to warm cache await db.flush() needs_cache_update = True + created = True except IntegrityError: await db.rollback() @@ -224,7 +227,7 @@ async def get_or_create_session( "Session %s cache updated in workspace %s", session.name, workspace_name ) - return honcho_session + return GetOrCreateResult(honcho_session, created=created) async def get_session( @@ -290,9 +293,11 @@ async def update_session( Raises: ResourceNotFoundException: If the session does not exist or peer is not in session """ - honcho_session = await get_or_create_session( - db, schemas.SessionCreate(name=session_name), workspace_name=workspace_name - ) + honcho_session: models.Session = ( + await get_or_create_session( + db, schemas.SessionCreate(name=session_name), workspace_name=workspace_name + ) + ).resource # Track if anything changed needs_update = False diff --git a/src/crud/webhook.py b/src/crud/webhook.py index 65b05569..7dc567eb 100644 --- a/src/crud/webhook.py +++ b/src/crud/webhook.py @@ -7,6 +7,7 @@ from src import models, schemas from src.config import settings from src.crud.workspace import get_workspace from src.exceptions import ResourceNotFoundException +from src.utils.types import GetOrCreateResult logger = getLogger(__name__) @@ -15,7 +16,7 @@ async def get_or_create_webhook_endpoint( db: AsyncSession, workspace_name: str, webhook: schemas.WebhookEndpointCreate, -) -> schemas.WebhookEndpoint: +) -> GetOrCreateResult[schemas.WebhookEndpoint]: """ Get or create a webhook endpoint, optionally for a workspace. @@ -24,7 +25,7 @@ async def get_or_create_webhook_endpoint( webhook: Webhook endpoint creation schema Returns: - The webhook endpoint + GetOrCreateResult containing the webhook endpoint and whether it was created Raises: ResourceNotFoundException: If the workspace is specified and does not exist @@ -47,7 +48,9 @@ async def get_or_create_webhook_endpoint( # Check if webhook already exists for this workspace for endpoint in endpoints: if endpoint.url == webhook.url: - return schemas.WebhookEndpoint.model_validate(endpoint) + return GetOrCreateResult( + schemas.WebhookEndpoint.model_validate(endpoint), created=False + ) # Create new webhook endpoint webhook_endpoint = models.WebhookEndpoint( @@ -59,7 +62,9 @@ async def get_or_create_webhook_endpoint( await db.refresh(webhook_endpoint) logger.debug("Webhook endpoint created: %s", webhook.url) - return schemas.WebhookEndpoint.model_validate(webhook_endpoint) + return GetOrCreateResult( + schemas.WebhookEndpoint.model_validate(webhook_endpoint), created=True + ) async def list_webhook_endpoints( diff --git a/src/crud/workspace.py b/src/crud/workspace.py index 96942fa6..cfc33a41 100644 --- a/src/crud/workspace.py +++ b/src/crud/workspace.py @@ -11,6 +11,7 @@ from src.cache.client import cache, get_cache_namespace from src.config import settings from src.exceptions import ConflictException, ResourceNotFoundException from src.utils.filter import apply_filter +from src.utils.types import GetOrCreateResult logger = getLogger(__name__) @@ -52,7 +53,7 @@ async def get_or_create_workspace( workspace: schemas.WorkspaceCreate, *, _retry: bool = False, -) -> models.Workspace: +) -> GetOrCreateResult[models.Workspace]: """ Get an existing workspace or create a new one if it doesn't exist. @@ -61,7 +62,7 @@ async def get_or_create_workspace( workspace: Workspace creation schema Returns: - The workspace if found or created + GetOrCreateResult containing the workspace and whether it was created Raises: ConflictException: If we fail to get or create the workspace @@ -77,7 +78,7 @@ async def get_or_create_workspace( logger.debug("Found existing workspace: %s", workspace.name) # Merge cached object into session (cached objects are detached) existing_workspace = await db.merge(existing_workspace, load=False) - return existing_workspace + return GetOrCreateResult(existing_workspace, created=False) # Workspace doesn't exist, create a new one honcho_workspace = models.Workspace( @@ -96,7 +97,7 @@ async def get_or_create_workspace( await cache.set( cache_key, honcho_workspace, expire=settings.CACHE.DEFAULT_TTL_SECONDS ) - return honcho_workspace + return GetOrCreateResult(honcho_workspace, created=True) except IntegrityError: await db.rollback() if _retry: @@ -164,13 +165,16 @@ async def update_workspace( Returns: The updated workspace """ - honcho_workspace = await get_or_create_workspace( - db, - schemas.WorkspaceCreate( - name=workspace_name, - metadata=workspace.metadata or {}, # Provide empty dict if metadata is None - ), - ) + honcho_workspace: models.Workspace = ( + await get_or_create_workspace( + db, + schemas.WorkspaceCreate( + name=workspace_name, + metadata=workspace.metadata + or {}, # Provide empty dict if metadata is None + ), + ) + ).resource # Track if anything changed needs_update = False diff --git a/src/deriver/deriver.py b/src/deriver/deriver.py index e129b47d..b765e987 100644 --- a/src/deriver/deriver.py +++ b/src/deriver/deriver.py @@ -61,8 +61,8 @@ async def process_representation_tasks_batch( ), ) - # Skip if deriver disabled - if message_level_configuration.deriver.enabled is False: + # Skip if disabled + if message_level_configuration.reasoning.enabled is False: return accumulate_metric( diff --git a/src/deriver/enqueue.py b/src/deriver/enqueue.py index 8981923c..e0beaa2c 100644 --- a/src/deriver/enqueue.py +++ b/src/deriver/enqueue.py @@ -2,8 +2,7 @@ import logging from datetime import datetime, timezone from typing import Any, Literal -from sqlalchemy import exists, insert, select, text, update -from sqlalchemy.dialects.postgresql import insert as pg_insert +from sqlalchemy import exists, insert, select, update from sqlalchemy.ext.asyncio import AsyncSession from src import crud, models, schemas @@ -94,11 +93,13 @@ async def handle_session( Returns: List of queue records to insert """ - session = await crud.get_or_create_session( - db_session, - session=schemas.SessionCreate(name=session_name), - workspace_name=workspace_name, - ) + session = ( + await crud.get_or_create_session( + db_session, + session=schemas.SessionCreate(name=session_name), + workspace_name=workspace_name, + ) + ).resource # Fetch workspace for configuration resolution workspace = await crud.get_workspace(db_session, workspace_name=workspace_name) @@ -337,7 +338,7 @@ async def generate_queue_records( # Check if the sender should be observed based on peer configuration should_observe = get_effective_observe_me(observed, peers_with_configuration) - if not conf.deriver.enabled: + if not conf.reasoning.enabled: return records if should_observe: @@ -486,18 +487,18 @@ async def enqueue_dream( ) return - stmt = ( - pg_insert(QueueItem) - .values(dream_record) - .on_conflict_do_nothing( - index_elements=[QueueItem.work_unit_key], - index_where=text("task_type = 'dream' AND processed = false"), + # Check if there's already a pending dream with the same work_unit_key + pending_check = select( + exists( + select(QueueItem.id).where( + QueueItem.work_unit_key == work_unit_key, + QueueItem.processed == False, # noqa: E712 + ) ) - .returning(QueueItem.id) ) - result = await db_session.execute(stmt) - inserted_id = result.scalar_one_or_none() - if inserted_id is None: + is_pending = await db_session.scalar(pending_check) + + if is_pending: logger.info( "Dream already pending in queue: %s/%s/%s (type: %s)", workspace_name, @@ -507,6 +508,10 @@ async def enqueue_dream( ) return + # Insert into queue + stmt = insert(QueueItem).returning(QueueItem) + await db_session.execute(stmt, [dream_record]) + # Update collection metadata now_iso = datetime.now(timezone.utc).isoformat() update_stmt = ( diff --git a/src/dialectic/chat.py b/src/dialectic/chat.py index fe4e2824..e2783db8 100644 --- a/src/dialectic/chat.py +++ b/src/dialectic/chat.py @@ -12,6 +12,7 @@ from src import crud from src.config import ReasoningLevel from src.dependencies import tracked_db from src.dialectic.core import DialecticAgent +from src.utils.config_helpers import get_configuration logger = logging.getLogger(__name__) @@ -39,15 +40,26 @@ async def agentic_chat( The synthesized answer string """ async with tracked_db("dialectic.agentic_chat") as db: - # Get peer cards for context - observer_peer_card = await crud.get_peer_card( - db, workspace_name, observer=observer, observed=observer - ) - observed_peer_card = None - if observer != observed: - observed_peer_card = await crud.get_peer_card( - db, workspace_name, observer=observer, observed=observed + # Resolve configuration to check if peer cards should be used + session = None + if session_name: + session = await crud.get_session( + db, workspace_name=workspace_name, session_name=session_name ) + workspace = await crud.get_workspace(db, workspace_name=workspace_name) + configuration = get_configuration(None, session, workspace) + + # Get peer cards for context (if enabled) + observer_peer_card = None + observed_peer_card = None + if configuration.peer_card.use: + observer_peer_card = await crud.get_peer_card( + db, workspace_name, observer=observer, observed=observer + ) + if observer != observed: + observed_peer_card = await crud.get_peer_card( + db, workspace_name, observer=observer, observed=observed + ) # Create and run the dialectic agent agent = DialecticAgent( @@ -89,15 +101,26 @@ async def agentic_chat_stream( Chunks of the response text as they are generated """ async with tracked_db("dialectic.agentic_chat_stream") as db: - # Get peer cards for context - observer_peer_card = await crud.get_peer_card( - db, workspace_name, observer=observer, observed=observer - ) - observed_peer_card = None - if observer != observed: - observed_peer_card = await crud.get_peer_card( - db, workspace_name, observer=observer, observed=observed + # Resolve configuration to check if peer cards should be used + session = None + if session_name: + session = await crud.get_session( + db, workspace_name=workspace_name, session_name=session_name ) + workspace = await crud.get_workspace(db, workspace_name=workspace_name) + configuration = get_configuration(None, session, workspace) + + # Get peer cards for context (if enabled) + observer_peer_card = None + observed_peer_card = None + if configuration.peer_card.use: + observer_peer_card = await crud.get_peer_card( + db, workspace_name, observer=observer, observed=observer + ) + if observer != observed: + observed_peer_card = await crud.get_peer_card( + db, workspace_name, observer=observer, observed=observed + ) # Create and run the dialectic agent agent = DialecticAgent( diff --git a/src/dialectic/prompts.py b/src/dialectic/prompts.py index 5430df6b..51ae0ccf 100644 --- a/src/dialectic/prompts.py +++ b/src/dialectic/prompts.py @@ -21,6 +21,10 @@ def agent_system_prompt( Returns: Formatted system prompt string for the agent """ + # Determine if we have any peer card data + peer_cards_enabled = ( + observer_peer_card is not None or observed_peer_card is not None + ) # Build peer card sections if observer != observed: # Directional query: observer asking about observed @@ -64,6 +68,15 @@ Known biographical information about {observed}: You are answering queries about '{observed}'. {peer_card_section} +""" + + # Build peer card explanation section (only if peer cards are being used) + peer_card_explanation = "" + if peer_cards_enabled: + peer_card_explanation = """ +Peer cards are **constructed summaries** - they are synthesized from the same observations stored in memory. This means: +- Information in a peer card originates from observations you can also find via `search_memory` +- The peer card is a convenience summary, not a separate source of truth """ return f""" @@ -72,11 +85,7 @@ You are a helpful and concise context synthesis agent that answers questions abo Always give users the answer *they expect* based on the message history -- the goal is to help recall and *reason through* insights that the memory system has already gathered. You have many tools for gathering context. Search wisely. {perspective_section} - -Peer cards are **constructed summaries** - they are synthesized from the same observations stored in memory. This means: -- Information in a peer card originates from observations you can also find via `search_memory` -- The peer card is a convenience summary, not a separate source of truth - +{peer_card_explanation} ## AVAILABLE TOOLS **Observation Tools (read):** diff --git a/src/dreamer/dream_scheduler.py b/src/dreamer/dream_scheduler.py index 035483a4..39f7057b 100644 --- a/src/dreamer/dream_scheduler.py +++ b/src/dreamer/dream_scheduler.py @@ -200,7 +200,9 @@ class DreamScheduler: ) -> None: """Execute the dream by enqueueing it and updating collection metadata.""" # Import here to avoid circular dependency + from src import crud from src.deriver.enqueue import enqueue_dream + from src.utils.config_helpers import get_configuration # Find the most recent session for this observer/observed pair async with tracked_db("dream_session_lookup") as db: @@ -216,11 +218,24 @@ class DreamScheduler: ) session_name = await db.scalar(stmt) - if not session_name: - logger.warning( - f"No documents found for {workspace_name}/{observer}/{observed}, skipping dream" + if not session_name: + logger.warning( + f"No documents found for {workspace_name}/{observer}/{observed}, skipping dream" + ) + return + + session = await crud.get_session( + db, workspace_name=workspace_name, session_name=session_name ) - return + workspace = await crud.get_workspace(db, workspace_name=workspace_name) + + configuration = get_configuration(None, session, workspace) + + if not configuration.dream.enabled: + logger.info( + f"Dreams disabled for {workspace_name}/{session_name}, skipping dream" + ) + return await enqueue_dream( workspace_name, diff --git a/src/dreamer/orchestrator.py b/src/dreamer/orchestrator.py index f2a10c5d..d85d346d 100644 --- a/src/dreamer/orchestrator.py +++ b/src/dreamer/orchestrator.py @@ -17,10 +17,12 @@ from typing import Any from sqlalchemy.ext.asyncio import AsyncSession +from src import crud from src.config import settings from src.dreamer.specialists import SPECIALISTS from src.dreamer.surprisal import SurprisalScore # type: ignore from src.exceptions import SpecialistExecutionError, SurprisalError +from src.utils.config_helpers import get_configuration from src.utils.logging import ( accumulate_metric, log_performance_metrics, @@ -71,6 +73,8 @@ async def run_dream( observed: Observed peer name session_name: Session identifier """ + if not settings.DREAM.ENABLED: + return run_id = str(uuid.uuid4())[:8] task_name = f"dream_orchestrator_{run_id}" @@ -80,6 +84,18 @@ async def run_dream( f"[{run_id}] Starting dream cycle for {workspace_name}/{observer}/{observed}" ) + session = await crud.get_session( + db, workspace_name=workspace_name, session_name=session_name + ) + workspace = await crud.get_workspace(db, workspace_name=workspace_name) + + configuration = get_configuration(None, session, workspace) + if not configuration.dream.enabled: + logger.info( + f"[{run_id}] Dreams disabled for {workspace_name}/{session_name}, skipping dream" + ) + return + # Phase 0: Surprisal-based sampling (if enabled) probing_questions = PROBING_QUESTIONS # Default @@ -149,6 +165,7 @@ async def run_dream( observed=observed, session_name=session_name, probing_questions=probing_questions, + configuration=configuration, ) logger.info(f"[{run_id}] Deduction completed: {deduction_result[:200]}...") accumulate_metric(task_name, "deduction_result", deduction_result, "blob") @@ -167,6 +184,7 @@ async def run_dream( observed=observed, session_name=session_name, probing_questions=probing_questions, + configuration=configuration, ) logger.info(f"[{run_id}] Induction completed: {induction_result[:200]}...") accumulate_metric(task_name, "induction_result", induction_result, "blob") diff --git a/src/dreamer/specialists.py b/src/dreamer/specialists.py index de0966cc..946dae89 100644 --- a/src/dreamer/specialists.py +++ b/src/dreamer/specialists.py @@ -21,6 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from src import prometheus from src.config import settings +from src.schemas import ResolvedConfiguration from src.utils.agent_tools import ( DEDUCTION_SPECIALIST_TOOLS, INDUCTION_SPECIALIST_TOOLS, @@ -32,13 +33,17 @@ from src.utils.logging import accumulate_metric, log_performance_metrics logger = logging.getLogger(__name__) +# Tool names to exclude when peer card creation is disabled +PEER_CARD_TOOL_NAMES = {"update_peer_card", "get_peer_card"} + + class BaseSpecialist(ABC): """Base class for agentic specialists.""" name: str = "base" @abstractmethod - def get_tools(self) -> list[dict[str, Any]]: + def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]: """Get the tools available to this specialist.""" ... @@ -56,7 +61,9 @@ class BaseSpecialist(ABC): return 15 @abstractmethod - def build_system_prompt(self, observed: str) -> str: + def build_system_prompt( + self, observed: str, *, peer_card_enabled: bool = True + ) -> str: """Build the system prompt for this specialist.""" ... @@ -73,6 +80,7 @@ class BaseSpecialist(ABC): observed: str, session_name: str, probing_questions: list[str], + configuration: ResolvedConfiguration | None = None, ) -> str: """ Run the specialist agent. @@ -84,6 +92,7 @@ class BaseSpecialist(ABC): observed: The peer being observed session_name: Session identifier probing_questions: Entry point questions to guide exploration + configuration: Resolved configuration for checking feature flags (optional) Returns: Summary of work done @@ -92,9 +101,17 @@ class BaseSpecialist(ABC): task_name = f"dreamer_{self.name}_{run_id}" start_time = time.perf_counter() + # Determine if peer card tools should be included + peer_card_enabled = configuration is None or configuration.peer_card.create + # Build messages messages: list[dict[str, str]] = [ - {"role": "system", "content": self.build_system_prompt(observed)}, + { + "role": "system", + "content": self.build_system_prompt( + observed, peer_card_enabled=peer_card_enabled + ), + }, {"role": "user", "content": self.build_user_prompt(probing_questions)}, ] @@ -109,6 +126,7 @@ class BaseSpecialist(ABC): session_name=session_name, include_observation_ids=True, history_token_limit=settings.DREAM.HISTORY_TOKEN_LIMIT, + configuration=configuration, ) # Get model with potential override @@ -120,7 +138,7 @@ class BaseSpecialist(ABC): llm_settings=llm_settings, prompt="", # Ignored since we pass messages max_tokens=self.get_max_tokens(), - tools=self.get_tools(), + tools=self.get_tools(peer_card_enabled=peer_card_enabled), tool_choice=None, tool_executor=tool_executor, max_tool_iterations=self.get_max_iterations(), @@ -172,8 +190,14 @@ class DeductionSpecialist(BaseSpecialist): name: str = "deduction" - def get_tools(self) -> list[dict[str, Any]]: - return DEDUCTION_SPECIALIST_TOOLS + def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]: + if peer_card_enabled: + return DEDUCTION_SPECIALIST_TOOLS + return [ + t + for t in DEDUCTION_SPECIALIST_TOOLS + if t["name"] not in PEER_CARD_TOOL_NAMES + ] def get_model(self) -> str: return settings.DREAM.DEDUCTION_MODEL @@ -184,7 +208,50 @@ class DeductionSpecialist(BaseSpecialist): def get_max_iterations(self) -> int: return 12 - def build_system_prompt(self, observed: str) -> str: + def build_system_prompt( + self, observed: str, *, peer_card_enabled: bool = True + ) -> str: + # Base tools list + tools_section = """## TOOLS + +- `search_memory`: Find observations by semantic query +- `create_observations`: Create new deductive OR contradiction observations (USE THIS!) +- `delete_observations`: Remove outdated observations (USE AFTER KNOWLEDGE UPDATES!) +- `get_recent_observations`: See recent activity""" + + if peer_card_enabled: + tools_section += """ +- `get_peer_card`: Retrieve current peer card contents +- `update_peer_card`: Update the peer card with key facts""" + + # Peer card section (only if enabled) + peer_card_section = "" + if peer_card_enabled: + peer_card_section = """ + +## PEER CARD UPDATES + +The peer card is a concise summary of permanent, stable information about the peer. Update it when you discover important facts that should be easily accessible. + +**Peer card format** - Use these prefixes to organize entries: +- Plain facts for biographical info: "Name: Alice", "Works at Google", "Lives in NYC" +- `INSTRUCTION: ...` for standing instructions: "INSTRUCTION: Always call me Al", "INSTRUCTION: Send meeting agendas 24h in advance" +- `PREFERENCE: ...` for preferences: "PREFERENCE: Prefers morning meetings", "PREFERENCE: Likes detailed explanations" +- `TRAIT: ...` for personality traits: "TRAIT: Analytical thinker", "TRAIT: Detail-oriented" + +Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list.""" + + # Remember section + remember_section = """ + +REMEMBER: +1. Knowledge updates are your #1 priority. When the same fact has different values at different times, CREATE an update observation AND DELETE the outdated observation. +2. Flag contradictions when statements are logically incompatible (can't both be true).""" + + if peer_card_enabled: + remember_section += """ +3. Update the peer card with permanent biographical facts and key insights.""" + return f"""You are a deductive reasoning specialist for {observed}. Your ONLY job is to create deductive observations by calling tools. Do NOT explain your reasoning - just make tool calls. ## MANDATORY WORKFLOW - YOU MUST FOLLOW THIS PATTERN @@ -290,31 +357,7 @@ Create deductions that make implicit information explicit: }} ``` -## TOOLS - -- `search_memory`: Find observations by semantic query -- `create_observations`: Create new deductive OR contradiction observations (USE THIS!) -- `delete_observations`: Remove outdated observations (USE AFTER KNOWLEDGE UPDATES!) -- `get_recent_observations`: See recent activity -- `get_peer_card`: Retrieve current peer card contents -- `update_peer_card`: Update the peer card with key facts - -## PEER CARD UPDATES - -The peer card is a concise summary of permanent, stable information about the peer. Update it when you discover important facts that should be easily accessible. - -**Peer card format** - Use these prefixes to organize entries: -- Plain facts for biographical info: "Name: Alice", "Works at Google", "Lives in NYC" -- `INSTRUCTION: ...` for standing instructions: "INSTRUCTION: Always call me Al", "INSTRUCTION: Send meeting agendas 24h in advance" -- `PREFERENCE: ...` for preferences: "PREFERENCE: Prefers morning meetings", "PREFERENCE: Likes detailed explanations" -- `TRAIT: ...` for personality traits: "TRAIT: Analytical thinker", "TRAIT: Detail-oriented" - -Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list. - -REMEMBER: -1. Knowledge updates are your #1 priority. When the same fact has different values at different times, CREATE an update observation AND DELETE the outdated observation. -2. Flag contradictions when statements are logically incompatible (can't both be true). -3. Update the peer card with permanent biographical facts and key insights.""" +{tools_section}{peer_card_section}{remember_section}""" def build_user_prompt(self, probing_questions: list[str]) -> str: questions_text = "\n".join(f"- {q}" for q in probing_questions) @@ -342,8 +385,14 @@ class InductionSpecialist(BaseSpecialist): name: str = "induction" - def get_tools(self) -> list[dict[str, Any]]: - return INDUCTION_SPECIALIST_TOOLS + def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]: + if peer_card_enabled: + return INDUCTION_SPECIALIST_TOOLS + return [ + t + for t in INDUCTION_SPECIALIST_TOOLS + if t["name"] not in PEER_CARD_TOOL_NAMES + ] def get_model(self) -> str: return settings.DREAM.INDUCTION_MODEL @@ -354,7 +403,48 @@ class InductionSpecialist(BaseSpecialist): def get_max_iterations(self) -> int: return 10 - def build_system_prompt(self, observed: str) -> str: + def build_system_prompt( + self, observed: str, *, peer_card_enabled: bool = True + ) -> str: + # Base tools list + tools_section = """## TOOLS + +- `search_memory`: Find observations by semantic query +- `create_observations`: Create new inductive observations (USE THIS!) +- `get_recent_observations`: See recent activity""" + + if peer_card_enabled: + tools_section += """ +- `get_peer_card`: Retrieve current peer card contents +- `update_peer_card`: Update the peer card with key facts""" + + # Peer card section (only if enabled) + peer_card_section = "" + if peer_card_enabled: + peer_card_section = """ + +## PEER CARD UPDATES + +The peer card is a concise summary of permanent, stable information about the peer. After identifying high-confidence patterns, update the peer card. + +**Peer card format** - Use these prefixes to organize entries: +- Plain facts for biographical info: "Name: Alice", "Works at Google", "Lives in NYC" +- `INSTRUCTION: ...` for standing instructions: "INSTRUCTION: Always call me Al" +- `PREFERENCE: ...` for preferences: "PREFERENCE: Prefers morning meetings" +- `TRAIT: ...` for personality/behavioral traits: "TRAIT: Analytical thinker", "TRAIT: Tends to reschedule when stressed" + +Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list.""" + + # Remember section + remember_section = """ + +REMEMBER: Focus on temporal patterns and how things change. Create observations, don't just search.""" + + if peer_card_enabled: + remember_section += ( + " Update the peer card with high-confidence patterns and traits." + ) + return f"""You are an inductive reasoning specialist for {observed}. Your ONLY job is to create inductive observations by calling tools. Do NOT explain your reasoning - just make tool calls. ## MANDATORY WORKFLOW - YOU MUST FOLLOW THIS PATTERN @@ -432,27 +522,7 @@ REQUIREMENTS: - Confidence based on source count: low=2, medium=3-4, high=5+ - Pattern must generalize, not just restate one fact -## TOOLS - -- `search_memory`: Find observations by semantic query -- `create_observations`: Create new inductive observations (USE THIS!) -- `get_recent_observations`: See recent activity -- `get_peer_card`: Retrieve current peer card contents -- `update_peer_card`: Update the peer card with key facts - -## PEER CARD UPDATES - -The peer card is a concise summary of permanent, stable information about the peer. After identifying high-confidence patterns, update the peer card. - -**Peer card format** - Use these prefixes to organize entries: -- Plain facts for biographical info: "Name: Alice", "Works at Google", "Lives in NYC" -- `INSTRUCTION: ...` for standing instructions: "INSTRUCTION: Always call me Al" -- `PREFERENCE: ...` for preferences: "PREFERENCE: Prefers morning meetings" -- `TRAIT: ...` for personality/behavioral traits: "TRAIT: Analytical thinker", "TRAIT: Tends to reschedule when stressed" - -Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list. - -REMEMBER: Focus on temporal patterns and how things change. Create observations, don't just search. Update the peer card with high-confidence patterns and traits.""" +{tools_section}{peer_card_section}{remember_section}""" def build_user_prompt(self, probing_questions: list[str]) -> str: questions_text = "\n".join(f"- {q}" for q in probing_questions) diff --git a/src/main.py b/src/main.py index 300b99bf..60bfa4c2 100644 --- a/src/main.py +++ b/src/main.py @@ -24,7 +24,6 @@ from src.routers import ( conclusions, keys, messages, - observations, peers, sessions, webhooks, @@ -133,14 +132,13 @@ async def lifespan(_: FastAPI): app = FastAPI( lifespan=lifespan, servers=[ - {"url": "http://localhost:8000", "description": "Local Development Server"}, - {"url": "https://demo.honcho.dev", "description": "Demo Server"}, {"url": "https://api.honcho.dev", "description": "Production SaaS Platform"}, + {"url": "http://localhost:8000", "description": "Local Development Server"}, ], title="Honcho API", summary="The Identity Layer for the Agentic World", - description="""Honcho is a platform for giving agents user-centric memory and social cognition""", - version="2.5.1", + description="""Honcho is a platform for giving agents user-centric memory and social cognition.""", + version="2.6.0", contact={ "name": "Plastic Labs", "url": "https://honcho.dev", @@ -156,7 +154,6 @@ app = FastAPI( origins = [ "http://localhost", "http://127.0.0.1:8000", - "https://demo.honcho.dev", "https://api.honcho.dev", ] @@ -176,7 +173,6 @@ app.include_router(peers.router, prefix="/v2") app.include_router(sessions.router, prefix="/v2") app.include_router(messages.router, prefix="/v2") app.include_router(conclusions.router, prefix="/v2") -app.include_router(observations.router, prefix="/v2") app.include_router(keys.router, prefix="/v2") app.include_router(webhooks.router, prefix="/v2") diff --git a/src/models.py b/src/models.py index 0fa08120..d5293db3 100644 --- a/src/models.py +++ b/src/models.py @@ -467,12 +467,6 @@ class QueueItem(Base): "message_id", postgresql_where=text("message_id IS NOT NULL"), ), - Index( - "ux_queue_dream_pending_work_unit_key", - "work_unit_key", - unique=True, - postgresql_where=text("task_type = 'dream' AND processed = false"), - ), Index( "ix_queue_work_unit_key_processed_id", "work_unit_key", diff --git a/src/routers/conclusions.py b/src/routers/conclusions.py index a2000947..6ba2ed67 100644 --- a/src/routers/conclusions.py +++ b/src/routers/conclusions.py @@ -22,19 +22,20 @@ router = APIRouter( @router.post( "", response_model=list[schemas.Conclusion], + status_code=201, ) async def create_conclusions( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), body: schemas.ConclusionBatchCreate = Body( ..., - description="Batch of conclusions to create", + description="Batch of Conclusions to create", ), db: AsyncSession = db, ) -> list[schemas.Conclusion]: """ - Create one or more conclusions. + Create one or more Conclusions. - Conclusions are theory-of-mind facts derived from interactions between peers. + Conclusions are logical certainties derived from interactions between Peers. They form the basis of a Peer's Representation. """ documents = await crud.create_observations( db, @@ -55,10 +56,10 @@ async def create_conclusions( response_model=Page[schemas.Conclusion], ) async def list_conclusions( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), options: schemas.ConclusionGet | None = Body( None, - description="Filtering options for the conclusions list", + description="Filtering options for the Conclusions list", ), reverse: bool | None = Query( False, @@ -67,7 +68,7 @@ async def list_conclusions( db: AsyncSession = db, ): """ - List conclusions using custom filters, ordered by recency unless `reverse` is true. + List Conclusions using optional filters, ordered by recency unless `reverse` is true. Results are paginated. """ filters = None if options and hasattr(options, "filters"): @@ -89,15 +90,15 @@ async def list_conclusions( response_model=list[schemas.Conclusion], ) async def query_conclusions( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), body: schemas.ConclusionQuery = Body( ..., - description="Semantic search parameters for conclusions", + description="Semantic search parameters for Conclusions", ), db: AsyncSession = db, ) -> list[schemas.Conclusion]: """ - Query conclusions using semantic search. + Query Conclusions using semantic search. Use `top_k` to control the number of results returned. """ observer = None observed = None @@ -125,14 +126,18 @@ async def query_conclusions( @router.delete( "/{conclusion_id}", + status_code=204, + response_model=None, ) async def delete_conclusion( - workspace_id: str = Path(..., description="ID of the workspace"), - conclusion_id: str = Path(..., description="ID of the conclusion to delete"), + workspace_id: str = Path(...), + conclusion_id: str = Path(...), db: AsyncSession = db, ): """ - Delete a specific conclusion (document). + Delete a single Conclusion by ID. + + This action cannot be undone. """ try: await crud.delete_document_by_id( @@ -142,7 +147,6 @@ async def delete_conclusion( ) logger.debug("Conclusion %s deleted successfully", conclusion_id) - return {"message": "Conclusion deleted successfully"} except ResourceNotFoundException: raise except ValueError as e: diff --git a/src/routers/messages.py b/src/routers/messages.py index 43e8c968..0e2d820f 100644 --- a/src/routers/messages.py +++ b/src/routers/messages.py @@ -79,7 +79,7 @@ async def parse_upload_form( ) -@router.post("/", response_model=list[schemas.Message]) +@router.post("", response_model=list[schemas.Message], status_code=201) async def create_messages_for_session( background_tasks: BackgroundTasks, messages: schemas.MessageBatchCreate, @@ -127,7 +127,7 @@ async def create_messages_for_session( raise -@router.post("/upload", response_model=list[schemas.Message]) +@router.post("/upload", response_model=list[schemas.Message], status_code=201) async def create_messages_with_file( background_tasks: BackgroundTasks, workspace_id: str = Path(...), @@ -200,17 +200,17 @@ async def create_messages_with_file( @router.post("/list", response_model=Page[schemas.Message]) async def get_messages( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), options: schemas.MessageGet | None = Body( - None, description="Filtering options for the messages list" + None, description="Filtering options for the message list" ), reverse: bool | None = Query( False, description="Whether to reverse the order of results" ), db: AsyncSession = db, ): - """Get all messages for a session""" + """Get all messages for a Session with optional filters. Results are paginated.""" try: filters = None if options and hasattr(options, "filters"): @@ -233,12 +233,12 @@ async def get_messages( @router.get("/{message_id}", response_model=schemas.Message) async def get_message( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), - message_id: str = Path(..., description="ID of the message to retrieve"), + workspace_id: str = Path(...), + session_id: str = Path(...), + message_id: str = Path(...), db: AsyncSession = db, ): - """Get a Message by ID""" + """Get a single message by ID from a Session.""" honcho_message = await crud.get_message( db, workspace_name=workspace_id, session_name=session_id, message_id=message_id ) @@ -250,15 +250,19 @@ async def get_message( @router.put("/{message_id}", response_model=schemas.Message) async def update_message( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), - message_id: str = Path(..., description="ID of the message to update"), + workspace_id: str = Path(...), + session_id: str = Path(...), + message_id: str = Path(...), message: schemas.MessageUpdate = Body( ..., description="Updated message parameters" ), db: AsyncSession = db, ): - """Update the metadata of a Message""" + """ + Update the metadata of a message. + + This will overwrite any existing metadata for the message. + """ try: updated_message = await crud.update_message( db, diff --git a/src/routers/observations.py b/src/routers/observations.py deleted file mode 100644 index e0a8bfd3..00000000 --- a/src/routers/observations.py +++ /dev/null @@ -1,170 +0,0 @@ -import logging - -from fastapi import APIRouter, Body, Depends, Path, Query -from fastapi_pagination import Page -from fastapi_pagination.ext.sqlalchemy import apaginate -from sqlalchemy.ext.asyncio import AsyncSession - -from src import crud, schemas -from src.dependencies import db -from src.exceptions import ResourceNotFoundException, ValidationException -from src.security import require_auth - -logger = logging.getLogger(__name__) - -router = APIRouter( - prefix="/workspaces/{workspace_id}/observations", - tags=["observations"], - dependencies=[Depends(require_auth(workspace_name="workspace_id"))], -) - - -@router.post( - "", - response_model=list[schemas.Observation], - deprecated=True, -) -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.conclusions, - 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], - deprecated=True, -) -async def list_observations( - workspace_id: str = Path(..., description="ID of the workspace"), - options: schemas.ObservationGet | None = Body( - None, description="Filtering options for the observations list" - ), - reverse: bool | None = Query( - False, description="Whether to reverse the order of results" - ), - db: AsyncSession = db, -): - """ - List all observations using custom filters. Observations are listed by recency unless `reverse` is set to `true`. - - Observations can be filtered by session_id, observer_id and observed_id using the filters parameter. - """ - try: - filters = None - if options and hasattr(options, "filters"): - filters = options.filters - if filters == {}: - filters = None - - stmt = crud.get_documents_with_filters( - workspace_name=workspace_id, - filters=filters, - reverse=reverse or False, - ) - - return await apaginate(db, stmt) - except ValueError as e: - logger.warning(f"Failed to list observations: {str(e)}") - raise ResourceNotFoundException("Session not found") from e - - -@router.post( - "/query", - response_model=list[schemas.Observation], - deprecated=True, -) -async def query_observations( - workspace_id: str = Path(..., description="ID of the workspace"), - body: schemas.ObservationQuery = Body( - ..., description="Semantic search parameters for observations" - ), - db: AsyncSession = db, -) -> list[schemas.Observation]: - """ - Query observations using semantic search. - - Performs vector similarity search on observations to find semantically relevant results. - Observer and observed are required for semantic search and must be provided in filters. - """ - # Extract observer and observed from filters if provided - observer = None - observed = None - if body.filters: - observer = body.filters.get("observer") or body.filters.get("observer_id") - observed = body.filters.get("observed") or body.filters.get("observed_id") - - # If no observer/observed specified, we need to query across all session documents - # For now, we'll require these to be specified for semantic search - if not observer or not observed: - raise ValidationException( - "observer and observed must be specified for semantic search" - ) - else: - # Query specific observer/observed pair - documents = await crud.query_documents( - db, - workspace_name=workspace_id, - query=body.query, - observer=observer, - observed=observed, - filters=body.filters, - max_distance=body.distance, - top_k=body.top_k, - ) - return [schemas.Observation.model_validate(doc) for doc in documents] - - -@router.delete( - "/{observation_id}", - deprecated=True, -) -async def delete_observation( - workspace_id: str = Path(..., description="ID of the workspace"), - observation_id: str = Path(..., description="ID of the observation to delete"), - db: AsyncSession = db, -): - """ - Delete a specific observation. - - This permanently deletes the observation (document) from the theory-of-mind system. - This action cannot be undone. - """ - try: - await crud.delete_document_by_id( - db, - workspace_name=workspace_id, - document_id=observation_id, - ) - - logger.debug("Observation %s deleted successfully", observation_id) - return {"message": "Observation deleted successfully"} - except ResourceNotFoundException: - raise - except ValueError as e: - logger.warning(f"Failed to delete observation {observation_id}: {str(e)}") - raise ResourceNotFoundException("Observation not found") from e diff --git a/src/routers/peers.py b/src/routers/peers.py index 02fd7920..4355a087 100644 --- a/src/routers/peers.py +++ b/src/routers/peers.py @@ -2,7 +2,7 @@ import json import logging from collections.abc import AsyncIterator -from fastapi import APIRouter, Body, Depends, Path, Query +from fastapi import APIRouter, Body, Depends, Path, Query, Response from fastapi.responses import StreamingResponse from fastapi_pagination import Page from fastapi_pagination.ext.sqlalchemy import apaginate @@ -30,13 +30,13 @@ router = APIRouter( dependencies=[Depends(require_auth(workspace_name="workspace_id"))], ) async def get_peers( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), options: schemas.PeerGet | None = Body( None, description="Filtering options for the peers list" ), db: AsyncSession = db, ): - """Get All Peers for a Workspace""" + """Get all Peers for a Workspace, paginated with optional filters.""" filter_param = None if options and hasattr(options, "filters"): filter_param = options.filters @@ -54,13 +54,14 @@ async def get_peers( response_model=schemas.Peer, ) async def get_or_create_peer( - workspace_id: str = Path(..., description="ID of the workspace"), + response: Response, + workspace_id: str = Path(...), peer: schemas.PeerCreate = Body(..., description="Peer creation parameters"), jwt_params: JWTParams = Depends(require_auth()), db: AsyncSession = db, ): """ - Get a Peer by ID + Get a Peer by ID or create a new Peer with the given ID. If peer_id is provided as a query parameter, it uses that (must match JWT workspace_id). Otherwise, it uses the peer_id from the JWT. @@ -77,10 +78,11 @@ async def get_or_create_peer( if not jwt_params.p: raise AuthenticationException("Peer ID not found in query parameter or JWT") peer.name = jwt_params.p - peer = ( - await crud.get_or_create_peers(db, workspace_name=workspace_id, peers=[peer]) - )[0] - return peer + result = await crud.get_or_create_peers( + db, workspace_name=workspace_id, peers=[peer] + ) + response.status_code = 201 if result.created else 200 + return result.resource[0] @router.put( @@ -91,12 +93,12 @@ async def get_or_create_peer( ], ) async def update_peer( - workspace_id: str = Path(..., description="ID of the workspace"), - peer_id: str = Path(..., description="ID of the peer to update"), + workspace_id: str = Path(...), + peer_id: str = Path(...), peer: schemas.PeerUpdate = Body(..., description="Updated peer parameters"), db: AsyncSession = db, ): - """Update a Peer's name and/or metadata""" + """Update a Peer's metadata and/or configuration.""" updated_peer = await crud.update_peer( db, workspace_name=workspace_id, peer_name=peer_id, peer=peer ) @@ -111,14 +113,14 @@ async def update_peer( ], ) async def get_sessions_for_peer( - workspace_id: str = Path(..., description="ID of the workspace"), - peer_id: str = Path(..., description="ID of the peer"), + workspace_id: str = Path(...), + peer_id: str = Path(...), options: schemas.SessionGet | None = Body( None, description="Filtering options for the sessions list" ), db: AsyncSession = db, ): - """Get All Sessions for a Peer""" + """Get all Sessions for a Peer, paginated with optional filters.""" filter_param = None if options and hasattr(options, "filters"): @@ -138,9 +140,9 @@ async def get_sessions_for_peer( @router.post( "/{peer_id}/chat", + summary="Query a Peer's representation using natural language", responses={ 200: { - "description": "Response to a question informed by Honcho's User Representation", "content": { "application/json": { "schema": schemas.DialecticResponse.model_json_schema() @@ -154,12 +156,14 @@ async def get_sessions_for_peer( ], ) async def chat( - workspace_id: str = Path(..., description="ID of the workspace"), - peer_id: str = Path(..., description="ID of the peer"), - options: schemas.DialecticOptions = Body( - ..., description="Dialectic Endpoint Parameters" - ), + workspace_id: str = Path(...), + peer_id: str = Path(...), + options: schemas.DialecticOptions = Body(...), ): + """ + Query a Peer's representation using natural language. Performs agentic search and reasoning to comprehensively + answer the query based on all latent knowledge gathered about the peer from their messages and conclusions. + """ # Get or create the peer to ensure it exists async with tracked_db("peers.chat.get_or_create_peer") as peer_db: await crud.get_or_create_peers( @@ -221,23 +225,25 @@ async def chat( @router.post( "/{peer_id}/representation", - response_model=dict[str, object], + response_model=schemas.RepresentationResponse, dependencies=[ Depends(require_auth(workspace_name="workspace_id", peer_name="peer_id")) ], ) -async def get_working_representation( - workspace_id: str = Path(..., description="ID of the workspace"), - peer_id: str = Path(..., description="ID of the peer"), +async def get_representation( + workspace_id: str = Path(...), + peer_id: str = Path(...), options: schemas.PeerRepresentationGet = Body( ..., description="Options for getting the peer representation" ), ): - """Get a peer's working representation for a session. + """Get a curated subset of a Peer's Representation. A Representation is always a subset of the total + knowledge about the Peer. The subset can be scoped and filtered in various ways. - If a session_id is provided in the body, we get the working representation of the peer in that session. - If a target is provided, we get the representation of the target from the perspective of the peer. - If no target is provided, we get the omniscient Honcho representation of the peer. + + If a session_id is provided in the body, we get the Representation of the Peer scoped to that Session. + If a target is provided, we get the Representation of the target from the perspective of the Peer. + If no target is provided, we get the omniscient Honcho Representation of the Peer. """ try: # If no target specified, get global representation (omniscient Honcho perspective) @@ -249,18 +255,18 @@ async def get_working_representation( include_semantic_query=options.search_query, semantic_search_top_k=options.search_top_k, semantic_search_max_distance=options.search_max_distance, - include_most_derived=options.include_most_derived - if options.include_most_derived is not None + include_most_derived=options.include_most_frequent + if options.include_most_frequent is not None else False, - max_observations=options.max_observations - if options.max_observations is not None + max_observations=options.max_conclusions + if options.max_conclusions is not None else settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS, ) - return {"representation": representation} - except ValueError as e: - logger.warning( - f"Failed to get working representation for peer {peer_id}: {str(e)}" + return schemas.RepresentationResponse( + representation=representation.format_as_markdown() ) + except ValueError as e: + logger.warning(f"Failed to get representation for peer {peer_id}: {str(e)}") raise ResourceNotFoundException("Peer or session not found") from e @@ -272,11 +278,11 @@ async def get_working_representation( ], ) async def get_peer_card( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), peer_id: str = Path(..., description="ID of the observer peer"), target: str | None = Query( None, - description="The peer whose card to retrieve. If not provided, returns the observer's own card", + description="Optional target peer to retrieve a card for, from the observer's perspective. If not provided, returns the observer's own card", ), db: AsyncSession = db, ): @@ -302,14 +308,14 @@ async def get_peer_card( ], ) async def set_peer_card( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), peer_id: str = Path(..., description="ID of the observer peer"), peer_card_data: schemas.PeerCardSet = Body( ..., description="Peer card data to set" ), target: str | None = Query( None, - description="The peer whose card to set. If not provided, sets the observer's own card", + description="Optional target peer to set a card for, from the observer's perspective. If not provided, sets the observer's own card", ), db: AsyncSession = db, ): @@ -344,11 +350,11 @@ async def set_peer_card( ], ) async def get_peer_context( - workspace_id: str = Path(..., description="ID of the workspace"), - peer_id: str = Path(..., description="ID of the peer (observer)"), + workspace_id: str = Path(...), + peer_id: str = Path(..., description="ID of the observer peer"), target: str | None = Query( None, - description="The target peer to get context for. If not provided, returns the peer's own context (self-observation)", + description="Optional target peer to get context for, from the observer's perspective. If not provided, returns the observer's own context (self-observation)", ), search_query: str | None = Query( None, @@ -358,30 +364,30 @@ async def get_peer_context( None, ge=1, le=100, - description="Only used if `search_query` is provided. Number of semantic-search-retrieved observations to include", + description="Only used if `search_query` is provided. Number of semantic-search-retrieved conclusions to include", ), search_max_distance: float | None = Query( None, ge=0.0, le=1.0, - description="Only used if `search_query` is provided. Maximum distance for semantically relevant observations", + description="Only used if `search_query` is provided. Maximum distance for semantically relevant conclusions", ), - include_most_derived: bool = Query( + include_most_frequent: bool = Query( default=True, - description="Whether to include the most derived observations in the representation", + description="Whether to include the most frequent conclusions in the representation", ), - max_observations: int | None = Query( + max_conclusions: int | None = Query( None, ge=1, le=100, - description="Maximum number of observations to include in the representation", + description="Maximum number of conclusions to include in the representation", ), db: AsyncSession = db, ): """ Get context for a peer, including their representation and peer card. - This endpoint returns the working representation and peer card for a peer. + This endpoint returns a curated subset of the representation and peer card for a peer. If a target is specified, returns the context for the target from the observer peer's perspective. If no target is specified, returns the peer's own context (self-observation). @@ -402,9 +408,9 @@ async def get_peer_context( include_semantic_query=search_query, semantic_search_top_k=search_top_k, semantic_search_max_distance=search_max_distance, - include_most_derived=include_most_derived, - max_observations=max_observations - if max_observations is not None + include_most_derived=include_most_frequent, + max_observations=max_conclusions + if max_conclusions is not None else settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS, ) @@ -416,7 +422,7 @@ async def get_peer_context( return schemas.PeerContext( peer_id=peer_id, target_id=observed, - representation=representation, + representation=representation.format_as_markdown(), peer_card=peer_card, ) except ValueError as e: @@ -432,14 +438,15 @@ async def get_peer_context( ], ) async def search_peer( - workspace_id: str = Path(..., description="ID of the workspace"), - peer_id: str = Path(..., description="ID of the peer"), + workspace_id: str = Path(...), + peer_id: str = Path(...), body: schemas.MessageSearchOptions = Body( - ..., description="Message search parameters " + ..., + description="Message search parameters. Use `limit` to control the number of results returned.", ), db: AsyncSession = db, ): - """Search a Peer""" + """Search a Peer's messages, optionally filtered by various criteria.""" # take user-provided filter and add workspace_id and peer_id to it filters = body.filters or {} filters["workspace_id"] = workspace_id diff --git a/src/routers/sessions.py b/src/routers/sessions.py index 24023642..4e141837 100644 --- a/src/routers/sessions.py +++ b/src/routers/sessions.py @@ -128,12 +128,38 @@ async def _get_session_context_task( return summary, message_schemas +@router.post( + "/list", + response_model=Page[schemas.Session], + dependencies=[Depends(require_auth(workspace_name="workspace_id"))], +) +async def get_sessions( + workspace_id: str = Path(...), + options: schemas.SessionGet | None = Body( + None, description="Filtering and pagination options for the sessions list" + ), + db: AsyncSession = db, +): + """Get all Sessions for a Workspace, paginated with optional filters.""" + filter_param = None + + if options and hasattr(options, "filters") and options.filters: + filter_param = options.filters + if filter_param == {}: # Explicitly check for empty dict + filter_param = None + + return await apaginate( + db, await crud.get_sessions(workspace_name=workspace_id, filters=filter_param) + ) + + @router.post( "", response_model=schemas.Session, ) async def get_or_create_session( - workspace_id: str = Path(..., description="ID of the workspace"), + response: Response, + workspace_id: str = Path(...), session: schemas.SessionCreate = Body( ..., description="Session creation parameters" ), @@ -141,9 +167,9 @@ async def get_or_create_session( db: AsyncSession = db, ): """ - Get a specific session in a workspace. + Get a Session by ID or create a new Session with the given ID. - If session_id is provided as a query parameter, it verifies the session is in the workspace. + If Session ID is provided as a parameter, it verifies the Session is in the Workspace. Otherwise, it uses the session_id from the JWT for verification. """ # Verify JWT has access to the requested resource @@ -167,39 +193,16 @@ async def get_or_create_session( # Handle session creation with proper error handling try: - return await crud.get_or_create_session( + result = await crud.get_or_create_session( db, workspace_name=workspace_id, session=session ) + response.status_code = 201 if result.created else 200 + return result.resource except ValueError as e: logger.warning(f"Failed to get or create session {session.name}: {str(e)}") raise ValidationException(str(e)) from e -@router.post( - "/list", - response_model=Page[schemas.Session], - dependencies=[Depends(require_auth(workspace_name="workspace_id"))], -) -async def get_sessions( - workspace_id: str = Path(..., description="ID of the workspace"), - options: schemas.SessionGet | None = Body( - None, description="Filtering and pagination options for the sessions list" - ), - db: AsyncSession = db, -): - """Get All Sessions in a Workspace""" - filter_param = None - - if options and hasattr(options, "filters") and options.filters: - filter_param = options.filters - if filter_param == {}: # Explicitly check for empty dict - filter_param = None - - return await apaginate( - db, await crud.get_sessions(workspace_name=workspace_id, filters=filter_param) - ) - - @router.put( "/{session_id}", response_model=schemas.Session, @@ -208,19 +211,18 @@ async def get_sessions( ], ) async def update_session( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session to update"), + workspace_id: str = Path(...), + session_id: str = Path(...), session: schemas.SessionUpdate = Body( ..., description="Updated session parameters" ), db: AsyncSession = db, ): - """Update the metadata of a Session""" + """Update a Session's metadata and/or configuration.""" try: updated_session = await crud.update_session( db, workspace_name=workspace_id, session_name=session_id, session=session ) - logger.debug("Session %s updated successfully", session_id) return updated_session except ValueError as e: logger.warning(f"Failed to update session {session_id}: {str(e)}") @@ -235,16 +237,15 @@ async def update_session( ], ) async def delete_session( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session to delete"), + workspace_id: str = Path(...), + session_id: str = Path(...), db: AsyncSession = db, ): """ - Delete a session and all associated data. + Delete a Session and all associated messages. - The session is marked as inactive immediately and returns 202 Accepted. The actual - deletion of all related data (messages, embeddings, documents, etc.) happens - asynchronously via the queue with retry support. + The Session is marked as inactive immediately and returns 202 Accepted. The actual + deletion of all related data happens asynchronously via the queue with retry support. This action cannot be undone. """ @@ -271,22 +272,23 @@ async def delete_session( raise ResourceNotFoundException("Session not found") from e -@router.get( +@router.post( "/{session_id}/clone", response_model=schemas.Session, + status_code=201, dependencies=[ Depends(require_auth(workspace_name="workspace_id", session_name="session_id")) ], ) async def clone_session( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session to clone"), - db: AsyncSession = db, + workspace_id: str = Path(...), + session_id: str = Path(...), message_id: str | None = Query( None, description="Message ID to cut off the clone at" ), + db: AsyncSession = db, ): - """Clone a session, optionally up to a specific message""" + """Clone a Session, optionally up to a specific message ID.""" try: # TODO: Update crud.clone_session to work with new paradigm cloned_session = await crud.clone_session( @@ -310,16 +312,17 @@ async def clone_session( ], ) async def add_peers_to_session( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), peers: dict[str, schemas.SessionPeerConfig] = Body( - ..., description="List of peer IDs to add to the session" + ..., + description="List of peer IDs (with session-level configuration) to add to the session", ), db: AsyncSession = db, ): - """Add peers to a session""" + """Add Peers to a Session. If a Peer does not yet exist, it will be created automatically.""" try: - session = await crud.get_or_create_session( + result = await crud.get_or_create_session( db, session=schemas.SessionCreate( name=session_id, @@ -327,8 +330,7 @@ async def add_peers_to_session( ), workspace_name=workspace_id, ) - logger.debug("Added peers to session %s successfully", session_id) - return session + return result.resource except ValueError as e: logger.warning(f"Failed to add peers to session {session_id}: {str(e)}") raise ResourceNotFoundException("Session not found") from e @@ -342,14 +344,19 @@ async def add_peers_to_session( ], ) async def set_session_peers( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), peers: dict[str, schemas.SessionPeerConfig] = Body( - ..., description="List of peer IDs to set for the session" + ..., + description="List of peer IDs (with session-level configuration) to set for the session", ), db: AsyncSession = db, ): - """Set the peers in a session""" + """ + Set the Peers in a Session. If a Peer does not yet exist, it will be created automatically. + + This will fully replace the current set of Peers in the Session. + """ try: await crud.set_peers_for_session( db, @@ -358,13 +365,13 @@ async def set_session_peers( peer_names=peers, ) # Get the session to return - session = await crud.get_or_create_session( + result = await crud.get_or_create_session( db, session=schemas.SessionCreate(name=session_id), workspace_name=workspace_id, ) logger.debug("Set peers for session %s successfully", session_id) - return session + return result.resource except ValueError as e: logger.warning(f"Failed to set peers for session {session_id}: {str(e)}") raise ResourceNotFoundException("Failed to set peers for session") from e @@ -378,14 +385,14 @@ async def set_session_peers( ], ) async def remove_peers_from_session( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), peers: list[str] = Body( ..., description="List of peer IDs to remove from the session" ), db: AsyncSession = db, ): - """Remove peers from a session""" + """Remove Peers by ID from a Session.""" try: await crud.remove_peers_from_session( db, @@ -394,13 +401,13 @@ async def remove_peers_from_session( peer_names=set(peers), ) # Get the session to return - session = await crud.get_or_create_session( + result = await crud.get_or_create_session( db, session=schemas.SessionCreate(name=session_id), workspace_name=workspace_id, ) logger.debug("Removed peers from session %s successfully", session_id) - return session + return result.resource except ValueError as e: logger.warning(f"Failed to remove peers from session {session_id}: {str(e)}") raise ResourceNotFoundException("Session not found") from e @@ -414,12 +421,12 @@ async def remove_peers_from_session( ], ) async def get_peer_config( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), - peer_id: str = Path(..., description="ID of the peer"), + workspace_id: str = Path(...), + session_id: str = Path(...), + peer_id: str = Path(...), db: AsyncSession = db, ): - """Get the configuration for a peer in a session""" + """Get the configuration for a Peer in a Session.""" return await crud.get_peer_config( db, workspace_name=workspace_id, @@ -428,20 +435,22 @@ async def get_peer_config( ) -@router.post( +@router.put( "/{session_id}/peers/{peer_id}/config", + status_code=204, + response_model=None, dependencies=[ Depends(require_auth(workspace_name="workspace_id", session_name="session_id")) ], ) async def set_peer_config( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), - peer_id: str = Path(..., description="ID of the peer"), - config: schemas.SessionPeerConfig = Body(..., description="Peer configuration"), + workspace_id: str = Path(...), + session_id: str = Path(...), + peer_id: str = Path(...), + config: schemas.SessionPeerConfig = Body(..., description="New peer configuration"), db: AsyncSession = db, ): - """Set the configuration for a peer in a session""" + """Set the configuration for a Peer in a Session.""" try: await crud.set_peer_config( db, @@ -453,7 +462,6 @@ async def set_peer_config( logger.debug( "Set peer config for %s in session %s successfully", peer_id, session_id ) - return Response(status_code=200) except ValueError as e: logger.warning( f"Failed to set peer config for {peer_id} in session {session_id}: {str(e)}" @@ -469,11 +477,11 @@ async def set_peer_config( ], ) async def get_session_peers( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), db: AsyncSession = db, ): - """Get peers from a session""" + """Get all Peers in a Session. Results are paginated.""" try: peers_query = await crud.get_peers_from_session( workspace_name=workspace_id, session_name=session_id @@ -492,8 +500,8 @@ async def get_session_peers( ], ) async def get_session_context( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), tokens: int | None = Query( None, le=config.settings.GET_CONTEXT_MAX_TOKENS, @@ -502,7 +510,7 @@ async def get_session_context( *, last_message: str | None = Query( None, - description="The most recent message, used to fetch semantically relevant observations", + description="The most recent message, used to fetch semantically relevant conclusions", ), include_summary: bool = Query( default=True, @@ -525,27 +533,27 @@ async def get_session_context( None, ge=1, le=100, - description="Only used if `last_message` is provided. The number of semantic-search-retrieved observations to include in the representation", + description="Only used if `last_message` is provided. The number of semantic-search-retrieved conclusions to include in the representation", ), search_max_distance: float | None = Query( None, ge=0.0, le=1.0, - description="Only used if `last_message` is provided. The maximum distance to search for semantically relevant observations", + description="Only used if `last_message` is provided. The maximum distance to search for semantically relevant conclusions", ), - include_most_derived: bool = Query( + include_most_frequent: bool = Query( default=False, - description="Only used if `last_message` is provided. Whether to include the most derived observations in the representation", + description="Only used if `last_message` is provided. Whether to include the most frequent conclusions in the representation", ), - max_observations: int | None = Query( + max_conclusions: int | None = Query( None, ge=1, le=100, - description="Only used if `last_message` is provided. The maximum number of observations to include in the representation", + description="Only used if `last_message` is provided. The maximum number of conclusions to include in the representation", ), ): """ - Produce a context object from the session. The caller provides an optional token limit which the entire context must fit into. + Produce a context object from the Session. The caller provides an optional token limit which the entire context must fit into. If not provided, the context will be exhaustive (within configured max tokens). To do this, we allocate 40% of the token limit to the summary, and 60% to recent messages -- as many as can fit. Note that the summary will usually take up less space than this. If the caller does not want a summary, we allocate all the tokens to recent messages. @@ -583,8 +591,8 @@ async def get_session_context( session_name=session_id if limit_to_session else None, search_top_k=search_top_k, search_max_distance=search_max_distance, - include_most_derived=include_most_derived, - max_observations=max_observations, + include_most_derived=include_most_frequent, + max_observations=max_conclusions, ) card = await _get_peer_card_task(workspace_id, observer=observer, observed=observed) @@ -603,7 +611,7 @@ async def get_session_context( name=session_id, messages=messages, summary=summary, - peer_representation=representation, + peer_representation=representation.format_as_markdown(), peer_card=card, ) @@ -616,12 +624,12 @@ async def get_session_context( ], ) async def get_session_summaries( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), db: AsyncSession = db, ) -> schemas.SessionSummaries: """ - Get available summaries for a session. + Get available summaries for a Session. Returns both short and long summaries if available, including metadata like the message ID they cover up to, creation timestamp, and token count. @@ -657,14 +665,16 @@ async def get_session_summaries( ], ) async def search_session( - workspace_id: str = Path(..., description="ID of the workspace"), - session_id: str = Path(..., description="ID of the session"), + workspace_id: str = Path(...), + session_id: str = Path(...), body: schemas.MessageSearchOptions = Body( ..., description="Message search parameters" ), db: AsyncSession = db, ): - """Search a Session""" + """ + Search a Session with optional filters. Use `limit` to control the number of results returned. + """ # take user-provided filter and add workspace_id and session_id to it filters = body.filters or {} filters["workspace_id"] = workspace_id diff --git a/src/routers/webhooks.py b/src/routers/webhooks.py index aa3256c1..f66164e9 100644 --- a/src/routers/webhooks.py +++ b/src/routers/webhooks.py @@ -1,6 +1,6 @@ import logging -from fastapi import APIRouter, Body, Depends, Path +from fastapi import APIRouter, Body, Depends, Path, Response from fastapi_pagination import Page from fastapi_pagination.ext.sqlalchemy import apaginate from sqlalchemy.ext.asyncio import AsyncSession @@ -26,6 +26,7 @@ router = APIRouter( @router.post("", response_model=schemas.WebhookEndpoint) async def get_or_create_webhook_endpoint( + response: Response, workspace_id: str = Path(..., description="Workspace ID"), webhook: schemas.WebhookEndpointCreate = Body( ..., description="Webhook endpoint parameters" @@ -40,9 +41,11 @@ async def get_or_create_webhook_endpoint( raise AuthenticationException("Unauthorized access to resource") try: - return await crud.get_or_create_webhook_endpoint( + result = await crud.get_or_create_webhook_endpoint( db, workspace_id, webhook=webhook ) + response.status_code = 201 if result.created else 200 + return result.resource except ValueError as e: raise ConflictException( f"Maximum number of webhook endpoints ({settings.WEBHOOK.MAX_WORKSPACE_LIMIT}) reached for this workspace." @@ -65,7 +68,7 @@ async def list_webhook_endpoints( return await apaginate(db, stmt) -@router.delete("/{endpoint_id}", response_model=None) +@router.delete("/{endpoint_id}", response_model=None, status_code=204) async def delete_webhook_endpoint( workspace_id: str = Path(..., description="Workspace ID"), endpoint_id: str = Path(..., description="Webhook endpoint ID"), diff --git a/src/routers/workspaces.py b/src/routers/workspaces.py index bbea0216..535ab886 100644 --- a/src/routers/workspaces.py +++ b/src/routers/workspaces.py @@ -1,6 +1,6 @@ import logging -from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Response from fastapi_pagination import Page from fastapi_pagination.ext.sqlalchemy import apaginate from sqlalchemy import func, select @@ -24,6 +24,7 @@ router = APIRouter( @router.post("", response_model=schemas.Workspace) async def get_or_create_workspace( + response: Response, workspace: schemas.WorkspaceCreate = Body( ..., description="Workspace creation parameters" ), @@ -48,7 +49,9 @@ async def get_or_create_workspace( ) workspace.name = jwt_params.w - return await crud.get_or_create_workspace(db, workspace=workspace) + result = await crud.get_or_create_workspace(db, workspace=workspace) + response.status_code = 201 if result.created else 200 + return result.resource @router.post( @@ -62,7 +65,7 @@ async def get_all_workspaces( ), db: AsyncSession = db, ): - """Get all Workspaces""" + """Get all Workspaces, paginated with optional filters.""" filter_param = None if options and hasattr(options, "filters"): filter_param = options.filters @@ -81,13 +84,13 @@ async def get_all_workspaces( dependencies=[Depends(require_auth(workspace_name="workspace_id"))], ) async def update_workspace( - workspace_id: str = Path(..., description="ID of the workspace to update"), + workspace_id: str = Path(...), workspace: schemas.WorkspaceUpdate = Body( ..., description="Updated workspace parameters" ), db: AsyncSession = db, ): - """Update a Workspace""" + """Update Workspace metadata and/or configuration.""" # ResourceNotFoundException will be caught by global handler if workspace not found honcho_workspace = await crud.update_workspace( db, workspace_name=workspace_id, workspace=workspace @@ -97,15 +100,21 @@ async def update_workspace( @router.delete( "/{workspace_id}", - response_model=schemas.Workspace, + status_code=204, + response_model=None, dependencies=[Depends(require_auth(workspace_name="workspace_id"))], ) async def delete_workspace( - workspace_id: str = Path(..., description="ID of the workspace to delete"), + workspace_id: str = Path(...), db: AsyncSession = db, ): - """Delete a Workspace""" - return await crud.delete_workspace(db, workspace_name=workspace_id) + """ + Delete a Workspace. This will permanently delete all sessions, peers, messages, and conclusions + associated with the workspace. + + This action cannot be undone. + """ + await crud.delete_workspace(db, workspace_name=workspace_id) @router.post( @@ -114,13 +123,16 @@ async def delete_workspace( dependencies=[Depends(require_auth(workspace_name="workspace_id"))], ) async def search_workspace( - workspace_id: str = Path(..., description="ID of the workspace to search"), + workspace_id: str = Path(...), body: schemas.MessageSearchOptions = Body( - ..., description="Message search parameters " + ..., description="Message search parameters" ), db: AsyncSession = db, ): - """Search a Workspace""" + """ + Search messages in a Workspace using optional filters. Use `limit` to control the number of + results returned. + """ # take user-provided filter and add workspace_id to it filters = body.filters or {} filters["workspace_id"] = workspace_id @@ -133,7 +145,7 @@ async def search_workspace( dependencies=[Depends(require_auth(workspace_name="workspace_id"))], ) async def get_queue_status( - workspace_id: str = Path(..., description="ID of the workspace"), + workspace_id: str = Path(...), observer_id: str | None = Query( None, description="Optional observer ID to filter by" ), @@ -143,38 +155,10 @@ async def get_queue_status( ), db: AsyncSession = db, ): - """Get the processing queue status, optionally scoped to an observer, sender, and/or session.""" - try: - return await crud.get_queue_status( - db, - workspace_name=workspace_id, - session_name=session_id, - observer=observer_id, - observed=sender_id, - ) - except ValueError as e: - logger.warning(f"Invalid request parameters: {str(e)}") - raise HTTPException(status_code=400, detail=str(e)) from e - - -@router.get( - "/{workspace_id}/deriver/status", - response_model=schemas.QueueStatus, - dependencies=[Depends(require_auth(workspace_name="workspace_id"))], - deprecated=True, -) -async def get_deriver_status( - workspace_id: str = Path(..., description="ID of the workspace"), - observer_id: str | None = Query( - None, description="Optional observer ID to filter by" - ), - sender_id: str | None = Query(None, description="Optional sender ID to filter by"), - session_id: str | None = Query( - None, description="Optional session ID to filter by" - ), - db: AsyncSession = db, -): - """Deprecated: use /queue/status. Provides identical response payload.""" + """ + Get the processing queue status for a Workspace, optionally scoped to an observer, sender, + and/or session. + """ try: return await crud.get_queue_status( db, @@ -189,22 +173,25 @@ async def get_deriver_status( @router.post( - "/{workspace_id}/trigger_dream", + "/{workspace_id}/schedule_dream", status_code=204, dependencies=[Depends(require_auth(workspace_name="workspace_id"))], ) -async def trigger_dream( - workspace_id: str = Path(..., description="ID of the workspace"), - request: schemas.TriggerDreamRequest = Body( - ..., description="Dream trigger parameters" +async def schedule_dream( + workspace_id: str = Path(...), + request: schemas.ScheduleDreamRequest = Body( + ..., description="Dream scheduling parameters" ), db: AsyncSession = db, ): """ - Manually trigger a dream task immediately for a specific collection. + Manually schedule a dream task for a specific collection. This endpoint bypasses all automatic dream conditions (document threshold, - minimum hours between dreams) and executes the dream task immediately without delay. + minimum hours between dreams) and schedules the dream task for a future execution. + + Currently this endpoint only supports scheduling immediate dreams. In the future, + users may pass a cron-style expression to schedule dreams at specific times. """ # Check if dreams are enabled if not settings.DREAM.ENABLED: @@ -237,7 +224,7 @@ async def trigger_dream( ) logger.info( - "Manually triggered dream: %s for %s/%s/%s (session: %s)", + "Manually scheduled dream: %s for %s/%s/%s (session: %s)", dream_type.value, workspace_id, observer, diff --git a/src/schemas.py b/src/schemas.py index d9d0d3bc..9b482c7f 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -16,7 +16,6 @@ from pydantic import ( ) from src.config import ReasoningLevel, settings -from src.utils.representation import Representation from src.utils.types import DocumentLevel RESOURCE_NAME_PATTERN = r"^[a-zA-Z0-9_-]+$" @@ -28,21 +27,21 @@ class DreamType(str, Enum): OMNI = "omni" -class DeriverConfiguration(BaseModel): +class ReasoningConfiguration(BaseModel): enabled: bool | None = Field( default=None, - description="Whether to enable deriver functionality.", + description="Whether to enable reasoning functionality.", ) custom_instructions: str | None = Field( default=None, - description="TODO: currently unused. Custom instructions to use for the deriver on this workspace/session/message.", + description="TODO: currently unused. Custom instructions to use for the reasoning system on this workspace/session/message.", ) class PeerCardConfiguration(BaseModel): use: bool | None = Field( default=None, - description="Whether to use peer card related to this peer during deriver process.", + description="Whether to use peer card related to this peer during reasoning process.", ) create: bool | None = Field( default=None, @@ -83,7 +82,7 @@ class SummaryConfiguration(BaseModel): class DreamConfiguration(BaseModel): enabled: bool | None = Field( default=None, - description="Whether to enable dream functionality. If deriver is disabled, dreams will also be disabled and this setting will be ignored.", + description="Whether to enable dream functionality. If reasoning is disabled, dreams will also be disabled and this setting will be ignored.", ) @@ -96,13 +95,13 @@ class WorkspaceConfiguration(BaseModel): model_config = ConfigDict(extra="allow") # pyright: ignore - deriver: DeriverConfiguration | None = Field( + reasoning: ReasoningConfiguration | None = Field( default=None, - description="Configuration for deriver functionality.", + description="Configuration for reasoning functionality.", ) peer_card: PeerCardConfiguration | None = Field( default=None, - description="Configuration for peer card functionality. If deriver is disabled, peer cards will also be disabled and these settings will be ignored.", + description="Configuration for peer card functionality. If reasoning is disabled, peer cards will also be disabled and these settings will be ignored.", ) summary: SummaryConfiguration | None = Field( default=None, @@ -110,7 +109,7 @@ class WorkspaceConfiguration(BaseModel): ) dream: DreamConfiguration | None = Field( default=None, - description="Configuration for dream functionality. If deriver is disabled, dreams will also be disabled and these settings will be ignored.", + description="Configuration for dream functionality. If reasoning is disabled, dreams will also be disabled and these settings will be ignored.", ) @@ -131,17 +130,13 @@ class MessageConfiguration(BaseModel): All fields are optional. Message-level configuration overrides all other configurations. """ - deriver: DeriverConfiguration | None = Field( + reasoning: ReasoningConfiguration | None = Field( default=None, - description="Configuration for deriver functionality.", - ) - peer_card: PeerCardConfiguration | None = Field( - default=None, - description="Configuration for peer card functionality. If deriver is disabled, peer cards will also be disabled and these settings will be ignored.", + description="Configuration for reasoning functionality.", ) -class ResolvedDeriverConfiguration(BaseModel): +class ResolvedReasoningConfiguration(BaseModel): enabled: bool @@ -166,7 +161,7 @@ class ResolvedConfiguration(BaseModel): Hierarchy: message > session > workspace > global configuration """ - deriver: ResolvedDeriverConfiguration + reasoning: ResolvedReasoningConfiguration peer_card: ResolvedPeerCardConfiguration summary: ResolvedSummaryConfiguration dream: ResolvedDreamConfiguration @@ -176,7 +171,7 @@ class PeerConfig(BaseModel): # TODO: Update description - should say "Whether honcho forms a representation of the peer itself" observe_me: bool | None = Field( default=None, - description="Whether honcho should form a global theory-of-mind representation of this peer", + description="Whether Honcho will use reasoning to form a representation of this peer", ) @@ -267,7 +262,7 @@ class Peer(PeerBase): class PeerRepresentationGet(BaseModel): session_id: str | None = Field( - None, description="Get the working representation within this session" + None, description="Optional session ID within which to scope the representation" ) target: str | None = Field( None, @@ -281,26 +276,30 @@ class PeerRepresentationGet(BaseModel): None, ge=1, le=100, - description="Only used if `search_query` is provided. Number of semantic-search-retrieved observations to include in the representation", + description="Only used if `search_query` is provided. Number of semantic-search-retrieved conclusions to include in the representation", ) search_max_distance: float | None = Field( None, ge=0.0, le=1.0, - description="Only used if `search_query` is provided. Maximum distance to search for semantically relevant observations", + description="Only used if `search_query` is provided. Maximum distance to search for semantically relevant conclusions", ) - include_most_derived: bool | None = Field( + include_most_frequent: bool | None = Field( default=None, - description="Only used if `search_query` is provided. Whether to include the most derived observations in the representation", + description="Only used if `search_query` is provided. Whether to include the most frequent conclusions in the representation", ) - max_observations: int | None = Field( + max_conclusions: int | None = Field( default=25, ge=1, le=100, - description="Only used if `search_query` is provided. Maximum number of observations to include in the representation", + description="Only used if `search_query` is provided. Maximum number of conclusions to include in the representation", ) +class RepresentationResponse(BaseModel): + representation: str + + class PeerCardResponse(BaseModel): peer_card: list[str] | None = Field( None, description="The peer card content, or None if not found" @@ -442,9 +441,9 @@ class SessionContext(SessionBase): summary: Summary | None = Field( default=None, description="The summary if available" ) - peer_representation: Representation | None = Field( + peer_representation: str | None = Field( default=None, - description="The peer representation, if context is requested from a specific perspective", + description="A curated subset of a peer representation, if context is requested from a specific perspective", ) peer_card: list[str] | None = Field( default=None, @@ -461,9 +460,9 @@ class PeerContext(BaseModel): peer_id: str = Field(description="The ID of the peer") target_id: str = Field(description="The ID of the target peer being observed") - representation: Representation | None = Field( + representation: str | None = Field( default=None, - description="The working representation of the target peer from the observer's perspective", + description="A curated subset of the representation of the target peer from the observer's perspective", ) peer_card: list[str] | None = Field( default=None, @@ -498,24 +497,23 @@ class DocumentMetadata(BaseModel): ) source_ids: list[str] | None = Field( default=None, - description="Document IDs of source observations for tree traversal -- required for deductive and inductive observations", + description="Document IDs of source documents for tree traversal -- required for deductive and inductive documents", ) - # Deductive observation fields premises: list[str] | None = Field( default=None, - description="Human-readable premise text for display -- only applicable for deductive observations", + description="Human-readable premise text for display -- only applicable for deductive documents", ) sources: list[str] | None = Field( default=None, - description="Human-readable source text for display -- only applicable for inductive observations", + description="Human-readable source text for display -- only applicable for inductive documents", ) pattern_type: str | None = Field( default=None, - description="Type of pattern identified (preference, behavior, personality, tendency, correlation) -- only applicable for inductive observations", + description="Type of pattern identified (preference, behavior, personality, tendency, correlation) -- only applicable for inductive documents", ) confidence: str | None = Field( default=None, - description="Confidence level (high, medium, low) -- only applicable for inductive observations", + description="Confidence level (high, medium, low) -- only applicable for inductive documents", ) @@ -538,7 +536,7 @@ class DocumentCreate(DocumentBase): # Tree linkage field source_ids: list[str] | None = Field( default=None, - description="Document IDs of source/premise observations -- for deductive and inductive observations", + description="Document IDs of source/premise documents -- for deductive and inductive documents", ) @@ -628,26 +626,6 @@ class ConclusionBatchCreate(BaseModel): ) -class ObservationGet(ConclusionGet): - """Deprecated: use ConclusionGet.""" - - -class Observation(Conclusion): - """Deprecated: use Conclusion.""" - - -class ObservationQuery(ConclusionQuery): - """Deprecated: use ConclusionQuery.""" - - -class ObservationCreate(ConclusionCreate): - """Deprecated: use ConclusionCreate.""" - - -class ObservationBatchCreate(ConclusionBatchCreate): - """Deprecated: use ConclusionBatchCreate.""" - - class MessageSearchOptions(BaseModel): query: str = Field(..., description="Search query") filters: dict[str, Any] | None = Field( @@ -776,21 +754,12 @@ class QueueStatus(BaseModel): ) -class SessionDeriverStatus(SessionQueueStatus): - """Deprecated: use SessionQueueStatus.""" - - -class DeriverStatus(QueueStatus): - """Deprecated: use QueueStatus.""" - - -# Dream trigger schema -class TriggerDreamRequest(BaseModel): +class ScheduleDreamRequest(BaseModel): observer: str = Field(..., description="Observer peer name") observed: str | None = Field( None, description="Observed peer name (defaults to observer if not specified)" ) - dream_type: DreamType = Field(..., description="Type of dream to trigger") + dream_type: DreamType = Field(..., description="Type of dream to schedule") session_id: str = Field(..., description="Session ID to scope the dream to") diff --git a/src/utils/agent_tools.py b/src/utils/agent_tools.py index c07eced9..62faea4f 100644 --- a/src/utils/agent_tools.py +++ b/src/utils/agent_tools.py @@ -12,6 +12,7 @@ from src import crud, models, schemas from src.config import settings from src.embedding_client import embedding_client from src.models import Document +from src.schemas import ResolvedConfiguration from src.utils import summarizer from src.utils.formatting import format_new_turn_with_timestamp, utc_now_iso from src.utils.representation import Representation @@ -884,6 +885,8 @@ class ToolContext: # This lock is obtained from the module-level registry to ensure all concurrent # tool executors for the same data share the same lock. db_lock: asyncio.Lock + # Optional resolved configuration for checking feature flags + configuration: ResolvedConfiguration | None = None async def _handle_create_observations( @@ -998,6 +1001,15 @@ async def _handle_create_observations( async def _handle_update_peer_card(ctx: ToolContext, tool_input: dict[str, Any]) -> str: """Handle update_peer_card tool.""" + # Check if peer card creation is disabled via configuration + if ctx.configuration is not None and not ctx.configuration.peer_card.create: + logger.info( + f"Peer card creation disabled for {ctx.workspace_name}, skipping update" + ) + return ( + "Peer card creation is disabled for this workspace/session configuration." + ) + async with ctx.db_lock: await crud.set_peer_card( ctx.db, @@ -1544,6 +1556,7 @@ async def create_tool_executor( current_messages: list[models.Message] | None = None, include_observation_ids: bool = False, history_token_limit: int = 8192, + configuration: ResolvedConfiguration | None = None, ) -> Callable[[str, dict[str, Any]], Any]: """ Create a unified tool executor function for all agent operations. @@ -1560,6 +1573,7 @@ async def create_tool_executor( current_messages: List of current messages being processed (optional, for deriver) include_observation_ids: If True, include observation IDs in output (for dreamer agent) history_token_limit: Maximum tokens for get_recent_history (default: 8192) + configuration: Resolved configuration for checking feature flags (optional) Returns: An async callable that executes tools with the captured context @@ -1578,6 +1592,7 @@ async def create_tool_executor( include_observation_ids=include_observation_ids, history_token_limit=history_token_limit, db_lock=shared_lock, + configuration=configuration, ) async def execute_tool(tool_name: str, tool_input: dict[str, Any]) -> str: diff --git a/src/utils/config_helpers.py b/src/utils/config_helpers.py index 13c60307..74099a62 100644 --- a/src/utils/config_helpers.py +++ b/src/utils/config_helpers.py @@ -28,9 +28,58 @@ def deep_update(base: dict[str, Any], update: dict[str, Any]) -> None: base[key] = value +def normalize_configuration_dict(raw: dict[str, Any]) -> dict[str, Any]: + """ + Normalize a workspace/session/message configuration dict to match the current + `ResolvedConfiguration` schema. + + This function exists to preserve backwards compatibility with older clients/tests + that used legacy configuration keys (e.g. `deriver.enabled` or `skip_deriver`). + + Behavior: + - If `reasoning.enabled` is not explicitly set, but `deriver.enabled` is present, + `reasoning.enabled` is derived from `deriver.enabled`. + - If `skip_deriver` is explicitly `True`, `reasoning.enabled` is forced to `False` + unless `reasoning.enabled` was already explicitly set. + - Legacy keys are removed from the returned dict to avoid polluting the resolved + configuration with unused fields. + """ + normalized: dict[str, Any] = dict(raw) + + reasoning_raw = normalized.get("reasoning") + reasoning_present = "reasoning" in normalized + reasoning: dict[str, Any] + if isinstance(reasoning_raw, dict): + reasoning = dict(cast(dict[str, Any], reasoning_raw)) + else: + reasoning = {} + reasoning_enabled_explicit = reasoning.get("enabled") is not None + + if not reasoning_enabled_explicit: + deriver_raw = normalized.get("deriver") + deriver: dict[str, Any] + if isinstance(deriver_raw, dict): + deriver = dict(cast(dict[str, Any], deriver_raw)) + else: + deriver = {} + if deriver.get("enabled") is not None: + reasoning["enabled"] = bool(deriver["enabled"]) + + if not reasoning_enabled_explicit and normalized.get("skip_deriver") is True: + reasoning["enabled"] = False + + if reasoning_present or reasoning: + normalized["reasoning"] = reasoning + + normalized.pop("deriver", None) + normalized.pop("skip_deriver", None) + + return normalized + + def get_configuration( message_configuration: MessageConfiguration | None, - session: models.Session, + session: models.Session | None, workspace: models.Workspace | None = None, ) -> ResolvedConfiguration: """ @@ -43,15 +92,16 @@ def get_configuration( 4. Global defaults from settings Args: - session: The session model - workspace: Optional workspace model (if not provided, only session and global config are used) + message_configuration: Optional message configuration + session: Optional session model + workspace: Optional workspace model Returns: ResolvedConfiguration """ # Start with defaults config_dict: dict[str, Any] = { - "deriver": {"enabled": settings.DERIVER.ENABLED}, + "reasoning": {"enabled": settings.DERIVER.ENABLED}, "peer_card": { "use": settings.PEER_CARD.ENABLED, "create": settings.PEER_CARD.ENABLED, @@ -68,11 +118,17 @@ def get_configuration( # Note: deep_update modifies config_dict in place if workspace is not None: - deep_update(config_dict, workspace.configuration) + deep_update(config_dict, normalize_configuration_dict(workspace.configuration)) - deep_update(config_dict, session.configuration) + if session is not None: + deep_update(config_dict, normalize_configuration_dict(session.configuration)) if message_configuration is not None: - deep_update(config_dict, message_configuration.model_dump(exclude_none=True)) + deep_update( + config_dict, + normalize_configuration_dict( + message_configuration.model_dump(exclude_none=True) + ), + ) return ResolvedConfiguration(**config_dict) diff --git a/src/utils/types.py b/src/utils/types.py index 6d728272..75efd737 100644 --- a/src/utils/types.py +++ b/src/utils/types.py @@ -1,4 +1,14 @@ -from typing import Literal +from typing import Generic, Literal, NamedTuple, TypeVar + +T = TypeVar("T") + + +class GetOrCreateResult(NamedTuple, Generic[T]): + """Result of a get_or_create operation indicating whether the resource was created.""" + + resource: T + created: bool + SupportedProviders = Literal["anthropic", "openai", "google", "groq", "custom", "vllm"] TaskType = Literal["webhook", "summary", "representation", "dream", "deletion"] diff --git a/tests/bench/beam.py b/tests/bench/beam.py index 5419337c..773d555a 100644 --- a/tests/bench/beam.py +++ b/tests/bench/beam.py @@ -212,7 +212,7 @@ class BEAMRunner: start_time = time.time() while True: try: - status = await honcho_client.get_deriver_status(session=session_id) + status = await honcho_client.get_queue_status(session=session_id) except Exception: await asyncio.sleep(1) elapsed_time = time.time() - start_time @@ -254,7 +254,7 @@ class BEAMRunner: observed = observed or observer honcho_url = self.get_honcho_url_for_index(0) - url = f"{honcho_url}/v2/workspaces/{workspace_id}/trigger_dream" + url = f"{honcho_url}/v2/workspaces/{workspace_id}/schedule_dream" payload: dict[str, Any] = { "observer": observer, "observed": observed, diff --git a/tests/bench/locomo.py b/tests/bench/locomo.py index 7ea1e2c4..3f6b8284 100644 --- a/tests/bench/locomo.py +++ b/tests/bench/locomo.py @@ -256,7 +256,7 @@ class LoCoMoRunner: start_time = time.time() while True: try: - status = await honcho_client.get_deriver_status(session=session_id) + status = await honcho_client.get_queue_status(session=session_id) except Exception: await asyncio.sleep(1) elapsed_time = time.time() - start_time @@ -296,7 +296,7 @@ class LoCoMoRunner: observed = observed or observer honcho_url = self.get_honcho_url_for_index(0) - url = f"{honcho_url}/v2/workspaces/{workspace_id}/trigger_dream" + url = f"{honcho_url}/v2/workspaces/{workspace_id}/schedule_dream" payload = { "observer": observer, "observed": observed, diff --git a/tests/bench/longmem.py b/tests/bench/longmem.py index c4b19ab4..18eb5970 100644 --- a/tests/bench/longmem.py +++ b/tests/bench/longmem.py @@ -270,7 +270,7 @@ class LongMemEvalRunner: start_time = time.time() while True: try: - status = await honcho_client.get_deriver_status(session=session_id) + status = await honcho_client.get_queue_status(session=session_id) except Exception as _e: await asyncio.sleep(1) elapsed_time = time.time() - start_time @@ -310,7 +310,7 @@ class LongMemEvalRunner: observed = observed or observer honcho_url = self.get_honcho_url_for_index(0) - url = f"{honcho_url}/v2/workspaces/{workspace_id}/trigger_dream" + url = f"{honcho_url}/v2/workspaces/{workspace_id}/schedule_dream" payload: dict[str, Any] = { "observer": observer, "observed": observed, diff --git a/tests/bench/run_tests.py b/tests/bench/run_tests.py index 29707276..9054aa3f 100644 --- a/tests/bench/run_tests.py +++ b/tests/bench/run_tests.py @@ -171,7 +171,7 @@ class TestRunner: True if queue is empty, False if timeout exceeded """ try: - await honcho_client.poll_deriver_status( + await honcho_client.poll_queue_status( session=session_id, timeout=float(self.timeout_seconds) if self.timeout_seconds diff --git a/tests/deriver/conftest.py b/tests/deriver/conftest.py index 2627b567..a11d5cc6 100644 --- a/tests/deriver/conftest.py +++ b/tests/deriver/conftest.py @@ -32,18 +32,20 @@ async def sample_session_with_peers( await db_session.flush() # Create session with peer configurations - session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - peer1.name: schemas.SessionPeerConfig(observe_me=True), - peer2.name: schemas.SessionPeerConfig(observe_others=True), - peer3.name: schemas.SessionPeerConfig(), # No special observation settings - }, - ), - workspace.name, - ) + session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + peer1.name: schemas.SessionPeerConfig(observe_me=True), + peer2.name: schemas.SessionPeerConfig(observe_others=True), + peer3.name: schemas.SessionPeerConfig(), # No special observation settings + }, + ), + workspace.name, + ) + ).resource await db_session.commit() return session, [peer1, peer2, peer3] @@ -124,7 +126,7 @@ def create_queue_payload() -> Callable[..., Any]: } configuration = schemas.ResolvedConfiguration( - deriver=schemas.ResolvedDeriverConfiguration(enabled=True), + reasoning=schemas.ResolvedReasoningConfiguration(enabled=True), peer_card=schemas.ResolvedPeerCardConfiguration(use=True, create=True), summary=schemas.ResolvedSummaryConfiguration( enabled=True, diff --git a/tests/integration/test_enqueue.py b/tests/integration/test_enqueue.py index 851b8430..bdfd3f5e 100644 --- a/tests/integration/test_enqueue.py +++ b/tests/integration/test_enqueue.py @@ -132,14 +132,16 @@ class TestEnqueueFunction: ): test_workspace, test_peer = sample_data - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={test_peer.name: schemas.SessionPeerConfig(observe_me=True)}, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={test_peer.name: schemas.SessionPeerConfig(observe_me=True)}, + ), + test_workspace.name, + ) + ).resource await db_session.commit() payload = await self.create_sample_payload( @@ -180,17 +182,19 @@ class TestEnqueueFunction: ) db_session.add(test_peer2) - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - test_peer1.name: schemas.SessionPeerConfig(), - test_peer2.name: schemas.SessionPeerConfig(), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + test_peer1.name: schemas.SessionPeerConfig(), + test_peer2.name: schemas.SessionPeerConfig(), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() @@ -256,17 +260,19 @@ class TestEnqueueFunction: ) db_session.add(test_peer2) - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - test_peer1.name: schemas.SessionPeerConfig(), - test_peer2.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + test_peer1.name: schemas.SessionPeerConfig(), + test_peer2.name: schemas.SessionPeerConfig(observe_others=True), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() NUM_MESSAGES = 3 @@ -345,18 +351,22 @@ class TestEnqueueFunction: db_session.add(unobserving_peer) # Create session - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - test_peer1.name: schemas.SessionPeerConfig(observe_me=True), - observing_peer.name: schemas.SessionPeerConfig(observe_others=True), - unobserving_peer.name: schemas.SessionPeerConfig(), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + test_peer1.name: schemas.SessionPeerConfig(observe_me=True), + observing_peer.name: schemas.SessionPeerConfig( + observe_others=True + ), + unobserving_peer.name: schemas.SessionPeerConfig(), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() NUM_MESSAGES = 3 @@ -430,14 +440,16 @@ class TestEnqueueFunction: test_peer.configuration = {"observe_me": True} # Create session - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={test_peer.name: schemas.SessionPeerConfig(observe_me=False)}, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={test_peer.name: schemas.SessionPeerConfig(observe_me=False)}, + ), + test_workspace.name, + ) + ).resource await db_session.commit() @@ -475,18 +487,22 @@ class TestEnqueueFunction: db_session.add_all([additional_sender_peer, observer_peer]) - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - test_peer1.name: schemas.SessionPeerConfig(), - additional_sender_peer.name: schemas.SessionPeerConfig(), - observer_peer.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + test_peer1.name: schemas.SessionPeerConfig(), + additional_sender_peer.name: schemas.SessionPeerConfig(), + observer_peer.name: schemas.SessionPeerConfig( + observe_others=True + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() payload1 = await self.create_sample_payload( @@ -574,17 +590,21 @@ class TestEnqueueFunction: db_session.add(observer_peer) # Create session with both peers - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - sender_peer.name: schemas.SessionPeerConfig(observe_me=True), - observer_peer.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + sender_peer.name: schemas.SessionPeerConfig(observe_me=True), + observer_peer.name: schemas.SessionPeerConfig( + observe_others=True + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Simulate sender leaving the session by setting left_at @@ -666,22 +686,24 @@ class TestEnqueueFunction: db_session.add_all([observer_who_left, observer_who_stayed]) # Create session with all peers - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - sender_peer.name: schemas.SessionPeerConfig(observe_me=True), - observer_who_left.name: schemas.SessionPeerConfig( - observe_others=True - ), - observer_who_stayed.name: schemas.SessionPeerConfig( - observe_others=True - ), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + sender_peer.name: schemas.SessionPeerConfig(observe_me=True), + observer_who_left.name: schemas.SessionPeerConfig( + observe_others=True + ), + observer_who_stayed.name: schemas.SessionPeerConfig( + observe_others=True + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Simulate one observer leaving the session @@ -741,16 +763,20 @@ class TestEnqueueFunction: db_session.add(observer_peer) # Create session with only observer (sender not in peers_with_configuration) - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - observer_peer.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + observer_peer.name: schemas.SessionPeerConfig( + observe_others=True + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Create message from peer NOT in the session configuration @@ -834,28 +860,30 @@ class TestEnqueueFunction: ) # Create session with all peers having different configurations - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - sender_peer.name: schemas.SessionPeerConfig(observe_me=True), - active_observer.name: schemas.SessionPeerConfig( - observe_others=True - ), - inactive_observer.name: schemas.SessionPeerConfig( - observe_others=True - ), - active_non_observer.name: schemas.SessionPeerConfig( - observe_others=False - ), - inactive_non_observer.name: schemas.SessionPeerConfig( - observe_others=False - ), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + sender_peer.name: schemas.SessionPeerConfig(observe_me=True), + active_observer.name: schemas.SessionPeerConfig( + observe_others=True + ), + inactive_observer.name: schemas.SessionPeerConfig( + observe_others=True + ), + active_non_observer.name: schemas.SessionPeerConfig( + observe_others=False + ), + inactive_non_observer.name: schemas.SessionPeerConfig( + observe_others=False + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Mark some peers as having left the session @@ -1116,18 +1144,20 @@ class TestAdvancedEnqueueEdgeCases: db_session.add_all([observer1, observer2]) # Create session with all peers - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - sender_peer.name: schemas.SessionPeerConfig(observe_me=True), - observer1.name: schemas.SessionPeerConfig(observe_others=True), - observer2.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + sender_peer.name: schemas.SessionPeerConfig(observe_me=True), + observer1.name: schemas.SessionPeerConfig(observe_others=True), + observer2.name: schemas.SessionPeerConfig(observe_others=True), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Mark all observers as having left @@ -1184,17 +1214,21 @@ class TestAdvancedEnqueueEdgeCases: db_session.add(observer_peer) # Create session - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - sender_peer.name: schemas.SessionPeerConfig(observe_me=True), - observer_peer.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + sender_peer.name: schemas.SessionPeerConfig(observe_me=True), + observer_peer.name: schemas.SessionPeerConfig( + observe_others=True + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Mark both as having left (observer left first, then sender) @@ -1266,16 +1300,20 @@ class TestAdvancedEnqueueEdgeCases: db_session.add(observer_peer) # Create session with only observer peer - test_session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={ - observer_peer.name: schemas.SessionPeerConfig(observe_others=True), - }, - ), - test_workspace.name, - ) + test_session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={ + observer_peer.name: schemas.SessionPeerConfig( + observe_others=True + ), + }, + ), + test_workspace.name, + ) + ).resource await db_session.commit() # Create message from peer who was NEVER in the session @@ -1376,7 +1414,7 @@ class TestGenerateQueueRecordsSeqInSession: ] } resolved_configuration = schemas.ResolvedConfiguration( - deriver=schemas.ResolvedDeriverConfiguration(enabled=True), + reasoning=schemas.ResolvedReasoningConfiguration(enabled=True), summary=schemas.ResolvedSummaryConfiguration( enabled=True, messages_per_short_summary=20, @@ -1456,7 +1494,7 @@ class TestGenerateQueueRecordsSeqInSession: ] } resolved_configuration = schemas.ResolvedConfiguration( - deriver=schemas.ResolvedDeriverConfiguration(enabled=True), + reasoning=schemas.ResolvedReasoningConfiguration(enabled=True), summary=schemas.ResolvedSummaryConfiguration( enabled=True, messages_per_short_summary=20, diff --git a/tests/integration/test_representation.py b/tests/integration/test_representation.py index 39e45597..e58fecdd 100644 --- a/tests/integration/test_representation.py +++ b/tests/integration/test_representation.py @@ -247,10 +247,10 @@ class TestDocumentCreationWorkflow: assert len(representation.explicit) == 1 assert representation.explicit[0].content == "User likes dogs" - async def test_get_working_representation_with_most_derived( + async def test_get_working_representation_with_include_most_frequent( self, db_session: AsyncSession ): - """Test working representation retrieval prioritizing most derived observations""" + """Test working representation retrieval prioritizing most frequent observations""" workspace, observer_peer = await self.create_test_workspace_and_peer(db_session) _, observed_peer = await self.create_test_workspace_and_peer( db_session, workspace.name @@ -297,7 +297,7 @@ class TestDocumentCreationWorkflow: await db_session.commit() - # Retrieve with most_derived=True + # Retrieve with include_most_frequent=True representation = await crud.get_working_representation( workspace.name, include_most_derived=True, diff --git a/tests/integration/test_token_metrics.py b/tests/integration/test_token_metrics.py index cb124aa1..a1908c65 100644 --- a/tests/integration/test_token_metrics.py +++ b/tests/integration/test_token_metrics.py @@ -21,9 +21,9 @@ from src import crud, models, schemas from src.models import Peer, Workspace from src.schemas import ( ResolvedConfiguration, - ResolvedDeriverConfiguration, ResolvedDreamConfiguration, ResolvedPeerCardConfiguration, + ResolvedReasoningConfiguration, ResolvedSummaryConfiguration, ) from src.utils.clients import HonchoLLMCallResponse @@ -97,14 +97,16 @@ async def create_test_session_with_peer( peer: Peer, ) -> models.Session: """Create a session with a peer configured for observation.""" - session = await crud.get_or_create_session( - db_session, - schemas.SessionCreate( - name=str(generate_nanoid()), - peers={peer.name: schemas.SessionPeerConfig(observe_me=True)}, - ), - workspace.name, - ) + session = ( + await crud.get_or_create_session( + db_session, + schemas.SessionCreate( + name=str(generate_nanoid()), + peers={peer.name: schemas.SessionPeerConfig(observe_me=True)}, + ), + workspace.name, + ) + ).resource await db_session.commit() return session @@ -142,7 +144,7 @@ async def create_test_messages( def create_test_configuration() -> ResolvedConfiguration: """Create a test configuration to avoid DB lookups in tests.""" return ResolvedConfiguration( - deriver=ResolvedDeriverConfiguration(enabled=True), + reasoning=ResolvedReasoningConfiguration(enabled=True), peer_card=ResolvedPeerCardConfiguration(use=False, create=False), summary=ResolvedSummaryConfiguration( enabled=True, messages_per_short_summary=20, messages_per_long_summary=60 diff --git a/tests/routes/test_observations.py b/tests/routes/test_conclusions.py similarity index 76% rename from tests/routes/test_observations.py rename to tests/routes/test_conclusions.py index 9f1bba94..87377327 100644 --- a/tests/routes/test_observations.py +++ b/tests/routes/test_conclusions.py @@ -7,8 +7,8 @@ from src import models from src.models import Peer, Workspace -class TestObservationRoutes: - """Test suite for observation API endpoints""" +class TestConclusionRoutes: + """Test suite for conclusion API endpoints""" async def _create_collection( self, @@ -28,13 +28,13 @@ class TestObservationRoutes: return collection @pytest.mark.asyncio - async def test_list_observations_success( + async def test_list_conclusions_success( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test listing observations for a session""" + """Test listing conclusions for a session""" test_workspace, test_peer = sample_data # Create another peer @@ -56,7 +56,7 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create test observations (documents) + # Create test conclusions (documents) doc1 = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, @@ -76,9 +76,9 @@ class TestObservationRoutes: db_session.add_all([doc1, doc2]) await db_session.commit() - # List observations + # List conclusions response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list", + f"/v2/workspaces/{test_workspace.name}/conclusions/list", json={"filters": {"session_id": test_session.name}}, ) @@ -87,14 +87,14 @@ class TestObservationRoutes: assert "items" in data assert len(data["items"]) == 2 - # Check observation structure - observation = data["items"][0] - assert "id" in observation - assert "content" in observation - assert "observer_id" in observation - assert "observed_id" in observation - assert "session_id" in observation - assert "created_at" in observation + # Check conclusion structure + conclusion = data["items"][0] + assert "id" in conclusion + assert "content" in conclusion + assert "observer_id" in conclusion + assert "observed_id" in conclusion + assert "session_id" in conclusion + assert "created_at" in conclusion # Verify content contents = [item["content"] for item in data["items"]] @@ -102,25 +102,25 @@ class TestObservationRoutes: assert "User works late at night" in contents @pytest.mark.asyncio - async def test_list_observations_empty_session( + async def test_list_conclusions_empty_session( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test listing observations for a session with no observations""" + """Test listing conclusions for a session with no conclusions""" test_workspace, _test_peer = sample_data - # Create a session without any observations + # Create a session without any conclusions test_session = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(test_session) await db_session.commit() - # List observations + # List conclusions response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list", + f"/v2/workspaces/{test_workspace.name}/conclusions/list", json={"filters": {"session_id": test_session.name}}, ) @@ -130,13 +130,13 @@ class TestObservationRoutes: assert len(data["items"]) == 0 @pytest.mark.asyncio - async def test_list_observations_with_filters( + async def test_list_conclusions_with_filters( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test listing observations with observer/observed filters""" + """Test listing conclusions with observer/observed filters""" test_workspace, test_peer = sample_data # Create two more peers @@ -164,7 +164,7 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer2.name, test_peer3.name ) - # Create observations with different observer/observed pairs + # Create conclusions with different observer/observed pairs doc1 = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, @@ -184,9 +184,9 @@ class TestObservationRoutes: db_session.add_all([doc1, doc2]) await db_session.commit() - # List observations filtered by observer + # List conclusions filtered by observer response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list", + f"/v2/workspaces/{test_workspace.name}/conclusions/list", json={ "filters": {"observer": test_peer.name, "session_id": test_session.name} }, @@ -199,13 +199,13 @@ class TestObservationRoutes: assert data["items"][0]["observer_id"] == test_peer.name @pytest.mark.asyncio - async def test_list_observations_reverse_order( + async def test_list_conclusions_reverse_order( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test listing observations in reverse chronological order""" + """Test listing conclusions in reverse chronological order""" test_workspace, test_peer = sample_data # Create another peer @@ -227,12 +227,12 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create observations + # Create conclusions doc1 = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content="First observation", + content="First conclusion", embedding=[0.1] * 1536, session_name=test_session.name, ) @@ -243,33 +243,33 @@ class TestObservationRoutes: workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content="Second observation", + content="Second conclusion", embedding=[0.2] * 1536, session_name=test_session.name, ) db_session.add(doc2) await db_session.commit() - # List observations in reverse (oldest first) + # List conclusions in reverse (oldest first) response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list?reverse=true", + f"/v2/workspaces/{test_workspace.name}/conclusions/list?reverse=true", json={"filters": {"session_id": test_session.name}}, ) assert response.status_code == 200 data = response.json() assert len(data["items"]) == 2 - assert data["items"][0]["content"] == "First observation" - assert data["items"][1]["content"] == "Second observation" + assert data["items"][0]["content"] == "First conclusion" + assert data["items"][1]["content"] == "Second conclusion" @pytest.mark.asyncio - async def test_list_observations_pagination( + async def test_list_conclusions_pagination( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test pagination of observations list""" + """Test pagination of conclusions list""" test_workspace, test_peer = sample_data # Create another peer @@ -291,13 +291,13 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create multiple observations + # Create multiple conclusions for i in range(15): doc = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content=f"Observation {i}", + content=f"Conclusion {i}", embedding=[0.1 * i] * 1536, session_name=test_session.name, ) @@ -306,7 +306,7 @@ class TestObservationRoutes: # Get first page (default size) response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list?page=1&size=10", + f"/v2/workspaces/{test_workspace.name}/conclusions/list?page=1&size=10", json={"filters": {"session_id": test_session.name}}, ) @@ -317,7 +317,7 @@ class TestObservationRoutes: # Get second page response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list?page=2&size=10", + f"/v2/workspaces/{test_workspace.name}/conclusions/list?page=2&size=10", json={"filters": {"session_id": test_session.name}}, ) @@ -327,13 +327,13 @@ class TestObservationRoutes: assert data["total"] == 15 @pytest.mark.asyncio - async def test_query_observations_success( + async def test_query_conclusions_success( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test querying observations with semantic search""" + """Test querying conclusions with semantic search""" test_workspace, test_peer = sample_data # Create another peer @@ -355,7 +355,7 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create test observations + # Create test conclusions doc1 = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, @@ -375,9 +375,9 @@ class TestObservationRoutes: db_session.add_all([doc1, doc2]) await db_session.commit() - # Query observations + # Query conclusions response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/query", + f"/v2/workspaces/{test_workspace.name}/conclusions/query", json={ "query": "food preferences", "filters": { @@ -393,21 +393,21 @@ class TestObservationRoutes: assert isinstance(data, list) assert len(data) >= 1 # pyright: ignore - # Check observation structure - observation = data[0] # pyright: ignore - assert "id" in observation - assert "content" in observation - assert "observer_id" in observation - assert "observed_id" in observation + # Check conclusion structure + conclusion = data[0] # pyright: ignore + assert "id" in conclusion + assert "content" in conclusion + assert "observer_id" in conclusion + assert "observed_id" in conclusion @pytest.mark.asyncio - async def test_query_observations_with_top_k( + async def test_query_conclusions_with_top_k( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test querying observations with top_k limit""" + """Test querying conclusions with top_k limit""" test_workspace, test_peer = sample_data # Create another peer @@ -429,13 +429,13 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create multiple observations + # Create multiple conclusions for i in range(5): doc = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content=f"Observation about topic {i}", + content=f"Conclusion about topic {i}", embedding=[0.1 * i] * 1536, session_name=test_session.name, ) @@ -444,7 +444,7 @@ class TestObservationRoutes: # Query with top_k=2 response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/query", + f"/v2/workspaces/{test_workspace.name}/conclusions/query", json={ "query": "relevant topic", "top_k": 2, @@ -462,13 +462,13 @@ class TestObservationRoutes: assert len(data) <= 2 # pyright: ignore @pytest.mark.asyncio - async def test_query_observations_with_distance_threshold( + async def test_query_conclusions_with_distance_threshold( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test querying observations with distance threshold""" + """Test querying conclusions with distance threshold""" test_workspace, test_peer = sample_data # Create another peer @@ -490,12 +490,12 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create test observation + # Create test conclusion doc = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content="Test observation", + content="Test conclusion", embedding=[0.5] * 1536, session_name=test_session.name, ) @@ -504,7 +504,7 @@ class TestObservationRoutes: # Query with distance threshold response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/query", + f"/v2/workspaces/{test_workspace.name}/conclusions/query", json={ "query": "test", "distance": 0.8, @@ -521,13 +521,13 @@ class TestObservationRoutes: assert isinstance(data, list) @pytest.mark.asyncio - async def test_query_observations_requires_observer_observed( + async def test_query_conclusions_requires_observer_observed( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test query observations requires observer and observed in filters""" + """Test query conclusions requires observer and observed in filters""" test_workspace, _test_peer = sample_data # Create a session @@ -539,20 +539,20 @@ class TestObservationRoutes: # Query without observer/observed filters should fail response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/query", + f"/v2/workspaces/{test_workspace.name}/conclusions/query", json={"query": "test"}, ) assert response.status_code == 422 @pytest.mark.asyncio - async def test_query_observations_invalid_top_k( + async def test_query_conclusions_invalid_top_k( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test query observations validates top_k range""" + """Test query conclusions validates top_k range""" test_workspace, test_peer = sample_data # Create another peer @@ -571,7 +571,7 @@ class TestObservationRoutes: # Query with invalid top_k (too high) response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/query", + f"/v2/workspaces/{test_workspace.name}/conclusions/query", json={ "query": "test", "top_k": 101, # Max is 100 @@ -586,13 +586,13 @@ class TestObservationRoutes: assert response.status_code == 422 @pytest.mark.asyncio - async def test_delete_observation_success( + async def test_delete_conclusion_success( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test deleting an observation""" + """Test deleting a conclusion""" test_workspace, test_peer = sample_data # Create another peer @@ -614,44 +614,42 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create a test observation + # Create a test conclusion doc = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content="Test observation to delete", + content="Test conclusion to delete", embedding=[0.1] * 1536, session_name=test_session.name, ) db_session.add(doc) await db_session.commit() - observation_id = doc.id + conclusion_id = doc.id - # Delete observation + # Delete conclusion response = client.delete( - f"/v2/workspaces/{test_workspace.name}/observations/{observation_id}" + f"/v2/workspaces/{test_workspace.name}/conclusions/{conclusion_id}" ) - assert response.status_code == 200 - data = response.json() - assert data["message"] == "Observation deleted successfully" + assert response.status_code == 204 - # Verify observation is deleted + # Verify conclusion is deleted from sqlalchemy import select - stmt = select(models.Document).where(models.Document.id == observation_id) + stmt = select(models.Document).where(models.Document.id == conclusion_id) result = await db_session.execute(stmt) assert result.scalar_one_or_none() is None @pytest.mark.asyncio - async def test_delete_observation_not_found( + async def test_delete_conclusion_not_found( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test deleting a non-existent observation""" + """Test deleting a non-existent conclusion""" test_workspace, _test_peer = sample_data # Create a session @@ -661,9 +659,9 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Try to delete non-existent observation + # Try to delete non-existent conclusion response = client.delete( - f"/v2/workspaces/{test_workspace.name}/observations/nonexistent_id" + f"/v2/workspaces/{test_workspace.name}/conclusions/nonexistent_id" ) assert response.status_code == 404 @@ -671,33 +669,33 @@ class TestObservationRoutes: assert "not found" in data["detail"].lower() @pytest.mark.asyncio - async def test_list_observations_nonexistent_session( + async def test_list_conclusions_nonexistent_session( self, client: TestClient, sample_data: tuple[Workspace, Peer], ): - """Test listing observations for non-existent session""" + """Test listing conclusions for non-existent session""" test_workspace, _test_peer = sample_data - # Try to list observations for non-existent session + # Try to list conclusions for non-existent session response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list", + f"/v2/workspaces/{test_workspace.name}/conclusions/list", json={"filters": {"session_id": "nonexistent_session"}}, ) - # Should return empty result, not error (session might exist but no observations) + # Should return empty result, not error (session might exist but no conclusions) assert response.status_code == 200 data = response.json() assert len(data["items"]) == 0 @pytest.mark.asyncio - async def test_observations_field_mapping( + async def test_conclusions_field_mapping( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test that observation fields are properly mapped from document model""" + """Test that conclusion fields are properly mapped from document model""" test_workspace, test_peer = sample_data # Create another peer @@ -719,49 +717,49 @@ class TestObservationRoutes: db_session, test_workspace.name, test_peer.name, test_peer2.name ) - # Create test observation + # Create test conclusion doc = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer2.name, - content="Test observation content", + content="Test conclusion content", embedding=[0.1] * 1536, session_name=test_session.name, ) db_session.add(doc) await db_session.commit() - # List observations + # List conclusions response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list", + f"/v2/workspaces/{test_workspace.name}/conclusions/list", json={"filters": {"session_id": test_session.name}}, ) assert response.status_code == 200 data = response.json() - observation = data["items"][0] + conclusion = data["items"][0] # Verify field mappings - assert observation["id"] == doc.id - assert observation["content"] == doc.content - assert observation["observer_id"] == doc.observer - assert observation["observed_id"] == doc.observed - assert observation["session_id"] == doc.session_name - assert "created_at" in observation + assert conclusion["id"] == doc.id + assert conclusion["content"] == doc.content + assert conclusion["observer_id"] == doc.observer + assert conclusion["observed_id"] == doc.observed + assert conclusion["session_id"] == doc.session_name + assert "created_at" in conclusion # Verify internal fields are NOT exposed - assert "embedding" not in observation - assert "internal_metadata" not in observation - assert "collection" not in observation + assert "embedding" not in conclusion + assert "internal_metadata" not in conclusion + assert "collection" not in conclusion @pytest.mark.asyncio - async def test_create_observation_success( + async def test_create_conclusion_success( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test creating a single observation""" + """Test creating a single conclusion""" test_workspace, test_peer = sample_data # Create another peer @@ -778,11 +776,11 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Create observation via API + # Create conclusion via API response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { "content": "User prefers dark mode", "observer_id": test_peer.name, @@ -793,26 +791,26 @@ class TestObservationRoutes: }, ) - assert response.status_code == 200 + assert response.status_code == 201 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 + conclusion = data[0] + assert conclusion["content"] == "User prefers dark mode" + assert conclusion["observer_id"] == test_peer.name + assert conclusion["observed_id"] == test_peer2.name + assert conclusion["session_id"] == test_session.name + assert "id" in conclusion + assert "created_at" in conclusion @pytest.mark.asyncio - async def test_create_observations_batch( + async def test_create_conclusions_batch( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test creating multiple observations in batch""" + """Test creating multiple conclusions in batch""" test_workspace, test_peer = sample_data # Create another peer @@ -829,11 +827,11 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Create multiple observations via API + # Create multiple conclusions via API response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { "content": "User prefers dark mode", "observer_id": test_peer.name, @@ -856,7 +854,7 @@ class TestObservationRoutes: }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 3 @@ -866,13 +864,13 @@ class TestObservationRoutes: assert "User enjoys programming" in contents @pytest.mark.asyncio - async def test_create_observation_nonexistent_session( + async def test_create_conclusion_nonexistent_session( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test creating observation with non-existent session fails""" + """Test creating conclusion with non-existent session fails""" test_workspace, test_peer = sample_data # Create another peer @@ -882,13 +880,13 @@ class TestObservationRoutes: db_session.add(test_peer2) await db_session.commit() - # Try to create observation with non-existent session + # Try to create conclusion with non-existent session response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { - "content": "Test observation", + "content": "Test conclusion", "observer_id": test_peer.name, "observed_id": test_peer2.name, "session_id": "nonexistent_session", @@ -900,13 +898,13 @@ class TestObservationRoutes: assert response.status_code == 404 @pytest.mark.asyncio - async def test_create_observation_nonexistent_peer( + async def test_create_conclusion_nonexistent_peer( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test creating observation with non-existent peer fails""" + """Test creating conclusion with non-existent peer fails""" test_workspace, test_peer = sample_data # Create a session @@ -916,13 +914,13 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Try to create observation with non-existent observer + # Try to create conclusion with non-existent observer response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { - "content": "Test observation", + "content": "Test conclusion", "observer_id": "nonexistent_peer", "observed_id": test_peer.name, "session_id": test_session.name, @@ -934,13 +932,13 @@ class TestObservationRoutes: assert response.status_code == 404 @pytest.mark.asyncio - async def test_create_observation_empty_content( + async def test_create_conclusion_empty_content( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test creating observation with empty content fails validation""" + """Test creating conclusion with empty content fails validation""" test_workspace, test_peer = sample_data # Create another peer @@ -957,11 +955,11 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Try to create observation with empty content + # Try to create conclusion with empty content response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { "content": "", "observer_id": test_peer.name, @@ -975,30 +973,30 @@ class TestObservationRoutes: assert response.status_code == 422 @pytest.mark.asyncio - async def test_create_observation_empty_list( + async def test_create_conclusion_empty_list( self, client: TestClient, sample_data: tuple[Workspace, Peer], ): - """Test creating observations with empty list fails validation""" + """Test creating conclusions with empty list fails validation""" test_workspace, _test_peer = sample_data - # Try to create with empty observations list + # Try to create with empty conclusions list response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", - json={"observations": []}, + f"/v2/workspaces/{test_workspace.name}/conclusions", + json={"conclusions": []}, ) assert response.status_code == 422 @pytest.mark.asyncio - async def test_create_observation_creates_collection( + async def test_create_conclusion_creates_collection( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test that creating observation auto-creates collection if needed""" + """Test that creating conclusion auto-creates collection if needed""" test_workspace, test_peer = sample_data # Create another peer @@ -1015,13 +1013,13 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Create observation via API (this should auto-create collection) + # Create conclusion via API (this should auto-create collection) response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { - "content": "Test observation", + "content": "Test conclusion", "observer_id": test_peer.name, "observed_id": test_peer2.name, "session_id": test_session.name, @@ -1030,24 +1028,24 @@ class TestObservationRoutes: }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 - # The observation was created successfully, which means the collection + # The conclusion 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 + conclusion = data[0] + assert conclusion["observer_id"] == test_peer.name + assert conclusion["observed_id"] == test_peer2.name @pytest.mark.asyncio - async def test_create_observation_different_observer_observed_pairs( + async def test_create_conclusion_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 creating conclusions with different observer/observed pairs in single batch""" test_workspace, test_peer = sample_data # Create two more peers @@ -1067,11 +1065,11 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Create observations with different observer/observed pairs + # Create conclusions with different observer/observed pairs response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { "content": "Peer1 observes Peer2", "observer_id": test_peer.name, @@ -1088,11 +1086,11 @@ class TestObservationRoutes: }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 2 - # Verify each observation has correct observer/observed + # Verify each conclusion 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 @@ -1102,13 +1100,13 @@ class TestObservationRoutes: assert obs2["observed_id"] == test_peer3.name @pytest.mark.asyncio - async def test_created_observations_are_searchable( + async def test_created_conclusions_are_searchable( self, client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): - """Test that created observations can be found via list endpoint""" + """Test that created conclusions can be found via list endpoint""" test_workspace, test_peer = sample_data # Create another peer @@ -1125,11 +1123,11 @@ class TestObservationRoutes: db_session.add(test_session) await db_session.commit() - # Create observation via API + # Create conclusion via API create_response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations", + f"/v2/workspaces/{test_workspace.name}/conclusions", json={ - "observations": [ + "conclusions": [ { "content": "Unique test content for searchability", "observer_id": test_peer.name, @@ -1140,12 +1138,12 @@ class TestObservationRoutes: }, ) - assert create_response.status_code == 200 + assert create_response.status_code == 201 created_id = create_response.json()[0]["id"] - # List observations and verify the created one is there + # List conclusions and verify the created one is there list_response = client.post( - f"/v2/workspaces/{test_workspace.name}/observations/list", + f"/v2/workspaces/{test_workspace.name}/conclusions/list", json={ "filters": { "observer": test_peer.name, diff --git a/tests/routes/test_files.py b/tests/routes/test_files.py index ccb8f20a..78d919d3 100644 --- a/tests/routes/test_files.py +++ b/tests/routes/test_files.py @@ -57,7 +57,7 @@ async def test_create_messages_with_text_file( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 # Should be 1 message since text is short @@ -90,7 +90,7 @@ async def test_create_messages_with_large_file_chunking( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) > 1 # Should be multiple messages due to chunking @@ -124,7 +124,7 @@ async def test_create_messages_with_json_file( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 @@ -206,7 +206,7 @@ async def test_create_messages_with_empty_file( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() # Should create one message with empty content assert len(data) == 1 @@ -233,7 +233,7 @@ async def test_file_metadata_stored_in_internal_metadata( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() message_id = data[0]["id"] @@ -298,7 +298,7 @@ async def test_pdf_file_processing( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) >= 1 # PDF should create at least one message @@ -364,7 +364,7 @@ async def test_file_upload_with_metadata( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 @@ -405,7 +405,7 @@ async def test_file_upload_with_configuration( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 @@ -449,7 +449,7 @@ async def test_file_upload_with_created_at( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 @@ -500,7 +500,7 @@ async def test_file_upload_with_all_parameters( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 @@ -543,7 +543,7 @@ async def test_file_upload_with_invalid_metadata_json( response = client.post(url, files=files, data=form_data) # Should still succeed but metadata will be None (backend handles gracefully) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() # Metadata parsing failure is logged but doesn't fail the request assert len(data) == 1 @@ -577,7 +577,7 @@ async def test_large_file_upload_with_metadata( url = _get_upload_url(test_workspace.name, session_name) response = client.post(url, files=files, data=form_data) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) > 1 # Should be multiple messages due to chunking diff --git a/tests/routes/test_messages.py b/tests/routes/test_messages.py index 5bc3f1d1..1ee9d734 100644 --- a/tests/routes/test_messages.py +++ b/tests/routes/test_messages.py @@ -35,7 +35,7 @@ async def test_create_message( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 message = data[0] @@ -77,7 +77,7 @@ async def test_create_batch_messages_with_metadata( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 2 @@ -115,7 +115,7 @@ async def test_create_batch_messages_without_metadata( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 assert data[0]["content"] == "Message without metadata" @@ -148,7 +148,7 @@ async def test_create_batch_messages_with_null_metadata( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 assert data[0]["content"] == "Message with null metadata" @@ -732,8 +732,8 @@ async def test_create_messages_for_nonexistent_session( ] }, ) - # Should create the session and return 200 with the created message - assert response.status_code == 200 + # Should create the session and return 201 with the created message + assert response.status_code == 201 data = response.json() assert len(data) == 1 assert data[0]["content"] == "Test message" @@ -807,7 +807,7 @@ async def test_create_batch_messages_max_limit( f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages", json={"messages": messages}, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 100 assert data[0]["content"] == "Message 0" @@ -973,7 +973,7 @@ async def test_create_message_with_timestamp( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 message = data[0] @@ -1020,7 +1020,7 @@ async def test_create_message_without_timestamp_uses_default( # Record time after request after_request = datetime.datetime.now(datetime.timezone.utc) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 message = data[0] @@ -1083,7 +1083,7 @@ async def test_create_batch_messages_with_mixed_timestamps( after_request = datetime.datetime.now(datetime.timezone.utc) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 3 @@ -1143,7 +1143,7 @@ async def test_create_message_with_null_timestamp( after_request = datetime.datetime.now(datetime.timezone.utc) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert len(data) == 1 message = data[0] diff --git a/tests/routes/test_peers.py b/tests/routes/test_peers.py index fa2c348d..87ac4546 100644 --- a/tests/routes/test_peers.py +++ b/tests/routes/test_peers.py @@ -16,7 +16,7 @@ def test_get_or_create_peer(client: TestClient, sample_data: tuple[Workspace, Pe f"/v2/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": {"peer_key": "peer_value"}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["metadata"] == {"peer_key": "peer_value"} @@ -35,7 +35,7 @@ def test_get_or_create_peer_with_configuration( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": name, "configuration": configuration}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["configuration"] == configuration @@ -54,7 +54,7 @@ def test_get_or_create_peer_with_all_optional_params( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": metadata, "configuration": configuration}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["metadata"] == metadata @@ -72,7 +72,7 @@ def test_get_or_create_existing_peer( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": {"peer_key": "peer_value"}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] peer1 = response.json() # Try to create the same peer again - should return existing peer @@ -80,7 +80,7 @@ def test_get_or_create_existing_peer( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": {"peer_key": "peer_value"}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] peer2 = response.json() # Both should be the same peer @@ -275,7 +275,7 @@ def test_get_sessions_for_peer(client: TestClient, sample_data: tuple[Workspace, f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_name, "peer_names": {test_peer.name: {}}}, ) - assert create_response.status_code == 200 + assert create_response.status_code in [200, 201] created_session = create_response.json() assert created_session["id"] == session_name @@ -381,7 +381,7 @@ def test_get_peer_representation_with_session( assert response.status_code == 200 data = response.json() assert "representation" in data - assert isinstance(data["representation"], dict) + assert isinstance(data["representation"], str) def test_get_peer_representation_global( @@ -398,7 +398,7 @@ def test_get_peer_representation_global( assert response.status_code == 200 data = response.json() assert "representation" in data - assert isinstance(data["representation"], dict) + assert isinstance(data["representation"], str) def test_get_peer_representation_with_target( @@ -424,7 +424,7 @@ def test_get_peer_representation_with_target( assert response.status_code == 200 data = response.json() assert "representation" in data - assert isinstance(data["representation"], dict) + assert isinstance(data["representation"], str) def test_get_peer_representation_with_search_query( @@ -485,30 +485,30 @@ def test_get_peer_representation_with_search_max_distance( assert "representation" in data -def test_get_peer_representation_with_include_most_derived( +def test_get_peer_representation_with_include_most_frequent( client: TestClient, sample_data: tuple[Workspace, Peer] ): - """Test peer representation with include_most_derived parameter""" + """Test peer representation with include_most_frequent parameter""" test_workspace, test_peer = sample_data - # Test with include_most_derived=True + # Test with include_most_frequent=True response = client.post( f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", - "include_most_derived": True, + "include_most_frequent": True, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data - # Test with include_most_derived=False + # Test with include_most_frequent=False response = client.post( f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", - "include_most_derived": False, + "include_most_frequent": False, }, ) assert response.status_code == 200 @@ -566,14 +566,14 @@ def test_get_peer_representation_with_all_parameters( "search_query": "What do I know about this peer?", "search_top_k": 15, "search_max_distance": 0.75, - "include_most_derived": True, + "include_most_frequent": True, "max_observations": 30, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data - assert isinstance(data["representation"], dict) + assert isinstance(data["representation"], str) def test_get_peer_representation_structure( @@ -592,13 +592,7 @@ def test_get_peer_representation_structure( # Validate response structure assert "representation" in data - assert isinstance(data["representation"], dict) - - # Representation should have expected keys based on Representation type - representation = data["representation"] - # The exact keys depend on the Representation implementation, - # but we can verify it's a dict - assert isinstance(representation, dict) + assert isinstance(data["representation"], str) def test_get_peer_representation_boundary_values( @@ -841,7 +835,7 @@ def test_get_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]): f"/v2/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Test getting observer's own card (should return null initially) response = client.get( @@ -876,7 +870,7 @@ async def test_get_peer_card_with_data( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Set up peer cards using the database directly # Set a self-card for the observer peer diff --git a/tests/routes/test_queue_status.py b/tests/routes/test_queue_status.py index bc50dcc7..4ae82197 100644 --- a/tests/routes/test_queue_status.py +++ b/tests/routes/test_queue_status.py @@ -8,7 +8,7 @@ from src.utils.work_unit import construct_work_unit_key @pytest.mark.asyncio class TestDeriverStatusEndpoint: - """Test suite for the /deriver/status endpoint""" + """Test suite for the /queue/status endpoint""" async def test_get_deriver_status_peer_only( self, @@ -18,7 +18,7 @@ class TestDeriverStatusEndpoint: """Test getting deriver status filtered by peer only""" workspace, peer = sample_data response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name}, ) assert response.status_code == 200 @@ -36,7 +36,7 @@ class TestDeriverStatusEndpoint: db_session.add(session) await db_session.commit() response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"session_id": session.name}, ) assert response.status_code == 200 @@ -54,7 +54,7 @@ class TestDeriverStatusEndpoint: db_session.add(session) await db_session.commit() response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name, "session_id": session.name}, ) assert response.status_code == 200 @@ -68,7 +68,7 @@ class TestDeriverStatusEndpoint: """Test getting deriver status with include_sender=True""" workspace, peer = sample_data response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name, "sender_id": peer.name}, ) assert response.status_code == 200 @@ -82,7 +82,7 @@ class TestDeriverStatusEndpoint: """Test getting deriver status with include_sender=False (default)""" workspace, peer = sample_data response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name}, ) assert response.status_code == 200 @@ -93,7 +93,7 @@ class TestDeriverStatusEndpoint: ): """Test getting deriver status without required parameters returns 200""" workspace, _ = sample_data - response = client.get(f"/v2/workspaces/{workspace.name}/deriver/status") + response = client.get(f"/v2/workspaces/{workspace.name}/queue/status") assert response.status_code == 200 async def test_get_deriver_status_nonexistent_peer( @@ -102,7 +102,7 @@ class TestDeriverStatusEndpoint: """Test getting deriver status for nonexistent peer returns empty result""" workspace, _ = sample_data response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": "nonexistent"}, ) assert response.status_code == 200 @@ -117,7 +117,7 @@ class TestDeriverStatusEndpoint: """Test getting deriver status for nonexistent session returns empty result""" workspace, _ = sample_data response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"session_id": "nonexistent"}, ) assert response.status_code == 200 @@ -128,7 +128,7 @@ class TestDeriverStatusEndpoint: async def test_get_deriver_status_nonexistent_workspace(self, client: TestClient): """Test getting deriver status for nonexistent workspace returns empty result""" - response = client.get("/v2/workspaces/nonexistent/deriver/status") + response = client.get("/v2/workspaces/nonexistent/queue/status") assert response.status_code == 200 assert response.json()["total_work_units"] == 0 @@ -166,13 +166,13 @@ class TestDeriverStatusEndpoint: db_session.add_all(queue_items) await db_session.commit() # Test without parameters - response = client.get(f"/v2/workspaces/{workspace.name}/deriver/status") + response = client.get(f"/v2/workspaces/{workspace.name}/queue/status") assert response.status_code == 200 assert response.json()["total_work_units"] == 5 assert response.json()["pending_work_units"] == 5 # Test with observer_id response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name}, ) assert response.status_code == 200 @@ -180,7 +180,7 @@ class TestDeriverStatusEndpoint: assert response.json()["pending_work_units"] == 5 # Test with sender_id (new capability) response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"sender_id": peer.name}, ) assert response.status_code == 200 @@ -188,7 +188,7 @@ class TestDeriverStatusEndpoint: assert response.json()["pending_work_units"] == 5 # Test with both (OR filter) response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name, "sender_id": peer.name}, ) assert response.status_code == 200 @@ -196,7 +196,7 @@ class TestDeriverStatusEndpoint: assert response.json()["pending_work_units"] == 5 # Test with different observer and sender (should be ok) response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name, "sender_id": "different"}, ) assert response.status_code == 200 @@ -241,7 +241,7 @@ class TestDeriverStatusEndpoint: db_session.add_all(queue_items) await db_session.commit() response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name}, ) assert response.status_code == 200 @@ -262,7 +262,7 @@ class TestDeriverStatusEndpoint: """Test various edge cases with empty or invalid parameters""" workspace, _ = sample_data response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={ "observer_id": "", "session_id": "", @@ -305,7 +305,7 @@ class TestDeriverStatusEndpoint: responses = [] for _ in range(3): response = client.get( - f"/v2/workspaces/{workspace.name}/deriver/status", + f"/v2/workspaces/{workspace.name}/queue/status", params={"observer_id": peer.name}, ) assert response.status_code == 200 diff --git a/tests/routes/test_scoped_api.py b/tests/routes/test_scoped_api.py index 507147ac..d07c9d58 100644 --- a/tests/routes/test_scoped_api.py +++ b/tests/routes/test_scoped_api.py @@ -17,7 +17,7 @@ def test_create_workspace_with_auth(auth_client: AuthClient): assert response.status_code == 401 return - assert response.status_code == 200 + assert response.status_code in [200, 201] def test_auth_response_time(auth_client: AuthClient): @@ -42,7 +42,7 @@ def test_auth_response_time(auth_client: AuthClient): assert response.status_code == 401 return - assert response.status_code == 200 + assert response.status_code in [200, 201] def test_get_or_create_workspace_with_auth(auth_client: AuthClient): @@ -56,7 +56,7 @@ def test_get_or_create_workspace_with_auth(auth_client: AuthClient): assert response.status_code == 401 return - assert response.status_code == 200 + assert response.status_code in [200, 201] def test_get_workspace_with_auth( @@ -74,7 +74,7 @@ def test_get_workspace_with_auth( # Admin JWT or JWT with matching workspace should be allowed if auth_client.auth_type in ["admin", "empty"]: - assert response.status_code == 200 + assert response.status_code in [200, 201] else: assert response.status_code == 401 @@ -149,7 +149,7 @@ def test_create_peer_with_auth( # Only admin JWT or JWT with matching workspace should be allowed if auth_client.auth_type in ["admin", "empty"]: - assert response.status_code == 200 + assert response.status_code in [200, 201] else: assert response.status_code == 401 @@ -188,7 +188,7 @@ def test_get_peer_by_name_with_auth( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": test_peer.name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] def test_update_peer_with_auth( @@ -250,7 +250,7 @@ def test_create_session_with_auth( # Only admin JWT or JWT with matching workspace should be allowed if auth_client.auth_type in ["admin", "empty"]: - assert response.status_code == 200 + assert response.status_code in [200, 201] else: assert response.status_code == 401 @@ -266,7 +266,7 @@ def test_create_session_with_auth( json={"name": session_name2, "peer_names": {test_peer.name: {}}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] def test_get_session_by_name_with_auth( @@ -290,13 +290,13 @@ def test_get_session_by_name_with_auth( assert create_response.status_code == 401 return - assert create_response.status_code == 200 + assert create_response.status_code in [200, 201] # Test with workspace scoped JWT - get the same session response = auth_client.post( f"/v2/workspaces/{test_workspace.name}/sessions", json={"name": session_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] if auth_client.auth_type == "empty": # Test with session-scoped JWT @@ -308,7 +308,7 @@ def test_get_session_by_name_with_auth( f"/v2/workspaces/{test_workspace.name}/sessions", json={"name": session_name}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Test with wrong session_name (should be 401 since we have a session-scoped JWT) assert ( @@ -324,26 +324,20 @@ def test_get_session_by_name_with_auth( f"Bearer {create_jwt(JWTParams(p=test_peer.name))}" ) - assert ( - auth_client.post( - f"/v2/workspaces/{test_workspace.name}/sessions", - json={"name": session_name}, - ).status_code - == 200 - ) + assert auth_client.post( + f"/v2/workspaces/{test_workspace.name}/sessions", + json={"name": session_name}, + ).status_code in [200, 201] # Test with workspace-scoped JWT auth_client.headers["Authorization"] = ( f"Bearer {create_jwt(JWTParams(w=test_workspace.name))}" ) - assert ( - auth_client.post( - f"/v2/workspaces/{test_workspace.name}/sessions", - json={"name": session_name}, - ).status_code - == 200 - ) + assert auth_client.post( + f"/v2/workspaces/{test_workspace.name}/sessions", + json={"name": session_name}, + ).status_code in [200, 201] # Test with wrong session_name using DELETE endpoint (should be 404 since session doesn't exist) wrong_session_name = generate_nanoid() diff --git a/tests/routes/test_sessions.py b/tests/routes/test_sessions.py index 263e7467..9804c0f9 100644 --- a/tests/routes/test_sessions.py +++ b/tests/routes/test_sessions.py @@ -15,7 +15,7 @@ def test_get_or_create_session(client: TestClient, sample_data: tuple[Workspace, f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": str(generate_nanoid())}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert isinstance(data["id"], str) assert data["metadata"] == {} @@ -27,7 +27,7 @@ def test_get_or_create_session(client: TestClient, sample_data: tuple[Workspace, f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert response2.status_code == 200 + assert response2.status_code in [200, 201] data2 = response2.json() assert data2["id"] == session_id assert data2["metadata"] == {} @@ -38,7 +38,7 @@ def test_get_or_create_session(client: TestClient, sample_data: tuple[Workspace, f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert response3.status_code == 200 + assert response3.status_code in [200, 201] data3 = response3.json() assert data3["id"] == session_id assert data3["metadata"] == {} @@ -58,7 +58,7 @@ def test_create_session_with_metadata( "metadata": {"session_key": "session_value"}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == {"session_key": "session_value"} assert "id" in data @@ -82,7 +82,7 @@ def test_create_session_with_configuration( "configuration": configuration, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["configuration"] == configuration assert data["id"] == session_id @@ -107,7 +107,7 @@ def test_create_session_with_all_optional_params( "configuration": configuration, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == metadata assert data["configuration"] == configuration @@ -129,7 +129,7 @@ def test_create_session_with_too_many_peers( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] peer_names.append(peer_name) # Test 1: Create session with 11 non-observers should succeed @@ -140,7 +140,7 @@ def test_create_session_with_too_many_peers( "peer_names": {peer_name: {} for peer_name in peer_names}, }, ) - assert response.status_code == 200 # Should succeed since no observers + assert response.status_code in [200, 201] # Should succeed since no observers # Test 2: Try to create session with 11 observers (exceeds limit) response = client.post( @@ -173,7 +173,7 @@ def test_create_session_with_too_many_peers( "peer_names": {peer_name: {} for peer_name in peer_names}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == "test_session" assert data["workspace_id"] == test_workspace.name @@ -191,7 +191,7 @@ def test_get_sessions(client: TestClient, sample_data: tuple[Workspace, Peer]): "metadata": {"test_key": "test_value"}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == {"test_key": "test_value"} assert "id" in data @@ -237,7 +237,7 @@ def test_update_delete_metadata( "metadata": {"default": "value"}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] response = client.put( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}", @@ -259,7 +259,7 @@ def test_update_session(client: TestClient, sample_data: tuple[Workspace, Peer]) "peer_names": {test_peer.name: {}}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] response = client.put( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}", @@ -283,7 +283,7 @@ def test_delete_session(client: TestClient, sample_data: tuple[Workspace, Peer]) "metadata": {"test_key": "test_value"}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Delete the session response = client.delete( @@ -392,7 +392,7 @@ def test_clone_session(client: TestClient, sample_data: tuple[Workspace, Peer]): "metadata": {"test": "key"}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create some messages in the session response = client.post( @@ -412,12 +412,12 @@ def test_clone_session(client: TestClient, sample_data: tuple[Workspace, Peer]): ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 - response = client.get( + response = client.post( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/clone", ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert data["metadata"] == {"test": "key"} @@ -462,16 +462,16 @@ def test_clone_session_with_cutoff( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 messages_data = response.json() # The response is a list of messages, not a paginated response first_message_id = messages_data[0]["id"] # Clone with cutoff at first message - response = client.get( + response = client.post( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/clone?message_id={first_message_id}", ) - assert response.status_code == 200 + assert response.status_code == 201 data = response.json() assert "id" in data @@ -484,7 +484,7 @@ def test_add_peers_to_session(client: TestClient, sample_data: tuple[Workspace, f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create a test session session_id = str(generate_nanoid()) @@ -495,7 +495,7 @@ def test_add_peers_to_session(client: TestClient, sample_data: tuple[Workspace, "peer_names": {test_peer.name: {}}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Add another peer to the session response = client.post( @@ -513,7 +513,7 @@ def test_get_session_peers(client: TestClient, sample_data: tuple[Workspace, Pee f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create a test session with multiple peers session_id = str(generate_nanoid()) @@ -524,7 +524,7 @@ def test_get_session_peers(client: TestClient, sample_data: tuple[Workspace, Pee "peer_names": {test_peer.name: {}, peer2_name: {}}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Get peers from the session response = client.get( @@ -547,7 +547,7 @@ def test_set_session_peers(client: TestClient, sample_data: tuple[Workspace, Pee f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create a test session session_id = str(generate_nanoid()) @@ -558,7 +558,7 @@ def test_set_session_peers(client: TestClient, sample_data: tuple[Workspace, Pee "peer_names": {test_peer.name: {}}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Set peers for the session (should replace existing peers) response = client.put( @@ -593,7 +593,7 @@ def test_set_session_peers_with_observer_limit( "id": session_id, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create 15 peers (more than the limit of 10 observers) peer_names = [test_peer.name] @@ -603,7 +603,7 @@ def test_set_session_peers_with_observer_limit( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] peer_names.append(peer_name) # Test 1: Adding 15 peers with observe_others=False should succeed @@ -654,7 +654,7 @@ def test_update_peer_config_observer_limit( "id": session_id, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create exactly 10 peers and add them as observers peer_names: list[str] = [] @@ -664,7 +664,7 @@ def test_update_peer_config_observer_limit( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] peer_names.append(peer_name) # Add all 10 peers as observers @@ -681,7 +681,7 @@ def test_update_peer_config_observer_limit( assert response.status_code == 200 # Should succeed with exactly 10 observers # Now try to update test_peer to become an observer (would exceed limit) - response = client.post( + response = client.put( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{test_peer.name}/config", json={"observe_others": True}, ) @@ -690,25 +690,25 @@ def test_update_peer_config_observer_limit( assert "Maximum allowed is 10 observers" in response.json()["detail"] # Verify that updating a peer that's already an observer still works - response = client.post( + response = client.put( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{peer_names[0]}/config", json={"observe_others": True, "observe_me": False}, # Still an observer ) - assert response.status_code == 200 # Should succeed since count doesn't change + assert response.status_code == 204 # Should succeed since count doesn't change # Change one observer to non-observer - response = client.post( + response = client.put( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{peer_names[0]}/config", json={"observe_others": False}, ) - assert response.status_code == 200 + assert response.status_code == 204 # Now test_peer can become an observer (9 + 1 = 10) - response = client.post( + response = client.put( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{test_peer.name}/config", json={"observe_others": True}, ) - assert response.status_code == 200 # Should succeed now + assert response.status_code == 204 # Should succeed now def test_remove_peers_from_session( @@ -721,7 +721,7 @@ def test_remove_peers_from_session( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create a test session with multiple peers session_id = str(generate_nanoid()) @@ -732,7 +732,7 @@ def test_remove_peers_from_session( "peer_names": {test_peer.name: {}, peer2_name: {}}, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Remove one peer from the session response = client.request( @@ -1092,7 +1092,7 @@ def test_get_session_context_with_peer_target( assert "peer_card" in data # Representation should be present assert data["peer_representation"] is not None - assert isinstance(data["peer_representation"], dict) + assert isinstance(data["peer_representation"], str) def test_get_session_context_with_peer_perspective( @@ -1104,10 +1104,11 @@ def test_get_session_context_with_peer_perspective( # Create another peer peer2_name = str(generate_nanoid()) - client.post( + response = client.post( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) + assert response.status_code in [200, 201] # Create session with both peers client.post( @@ -1229,10 +1230,10 @@ def test_get_session_context_with_search_parameters( assert "peer_representation" in data -def test_get_session_context_with_include_most_derived( +def test_get_session_context_with_include_most_frequent( client: TestClient, sample_data: tuple[Workspace, Peer] ): - """Test session context with include_most_derived parameter""" + """Test session context with include_most_frequent parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) @@ -1242,13 +1243,13 @@ def test_get_session_context_with_include_most_derived( json={"id": session_id, "peers": {test_peer.name: {}}}, ) - # Get context with include_most_derived + # Get context with include_most_frequent response = client.get( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "peer_target": test_peer.name, "last_message": "Test query", - "include_most_derived": True, + "include_most_frequent": True, }, ) assert response.status_code == 200 @@ -1292,10 +1293,11 @@ def test_get_session_context_with_all_representation_params( # Create another peer peer2_name = str(generate_nanoid()) - client.post( + response = client.post( f"/v2/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) + assert response.status_code in [200, 201] # Create session client.post( @@ -1314,7 +1316,7 @@ def test_get_session_context_with_all_representation_params( "limit_to_session": True, "search_top_k": 10, "search_max_distance": 0.9, # float value (semantic distance 0.0-1.0) - "include_most_derived": True, + "include_most_frequent": True, "max_observations": 15, "summary": True, }, @@ -1329,7 +1331,7 @@ def test_get_session_context_with_all_representation_params( assert "peer_representation" in data assert "peer_card" in data # Validate representation structure - assert isinstance(data["peer_representation"], dict) + assert isinstance(data["peer_representation"], str) def test_get_session_context_response_structure( @@ -1355,7 +1357,7 @@ def test_get_session_context_response_structure( ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 # Get context and validate response structure response = client.get( diff --git a/tests/routes/test_validation_api.py b/tests/routes/test_validation_api.py index 23de5641..8de2bdee 100644 --- a/tests/routes/test_validation_api.py +++ b/tests/routes/test_validation_api.py @@ -70,7 +70,7 @@ def test_message_validations_api( f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Test content too long response = client.post( @@ -110,7 +110,7 @@ def test_session_validations_api( "configuration": {"test_flag": "test_value"}, }, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Test invalid metadata type response = client.put( @@ -137,7 +137,7 @@ def test_session_validations_api( "id": session_id, }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == {"test_key": "test_value"} assert data["configuration"] == {"test_flag": "test_value"} @@ -159,7 +159,7 @@ def test_agent_query_validations_api( f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Test valid string query (under 10000 chars) response = client.post( @@ -199,7 +199,7 @@ def test_required_field_validations_api( f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Test missing required content in message response = client.post( @@ -232,7 +232,7 @@ def test_filter_validations_api( f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Test invalid filter type in message list (at session level) response = client.post( diff --git a/tests/routes/test_webhooks.py b/tests/routes/test_webhooks.py index 8272dc9f..49e4e1cd 100644 --- a/tests/routes/test_webhooks.py +++ b/tests/routes/test_webhooks.py @@ -18,7 +18,7 @@ async def test_create_webhook_endpoint( "url": "http://example.com/webhook", }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] response_json = response.json() assert response_json["url"] == "http://example.com/webhook" assert "id" in response_json @@ -71,7 +71,7 @@ async def test_list_webhook_endpoints_with_data( "url": "http://example1.com/webhook", }, ) - assert response1.status_code == 200 + assert response1.status_code in [200, 201] # Create second endpoint response2 = client.post( @@ -80,7 +80,7 @@ async def test_list_webhook_endpoints_with_data( "url": "http://example2.com/webhook", }, ) - assert response2.status_code == 200 + assert response2.status_code in [200, 201] # List endpoints list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks") @@ -108,7 +108,7 @@ async def test_delete_webhook_endpoint( "url": "http://example.com/webhook", }, ) - assert create_response.status_code == 200 + assert create_response.status_code in [200, 201] endpoint = create_response.json() endpoint_id = endpoint["id"] @@ -116,7 +116,7 @@ async def test_delete_webhook_endpoint( delete_response = client.delete( f"/v2/workspaces/{test_workspace.name}/webhooks/{endpoint_id}" ) - assert delete_response.status_code == 200 + assert delete_response.status_code == 204 # Verify endpoint is deleted list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks") @@ -160,7 +160,7 @@ async def test_multiple_endpoints_per_workspace( f"/v2/workspaces/{test_workspace.name}/webhooks", json={"url": url}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] created_endpoints.append(response.json()) # List all endpoints @@ -179,7 +179,7 @@ async def test_multiple_endpoints_per_workspace( delete_response = client.delete( f"/v2/workspaces/{test_workspace.name}/webhooks/{created_endpoints[0]['id']}" ) - assert delete_response.status_code == 200 + assert delete_response.status_code == 204 # Verify only 2 endpoints remain list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks") @@ -201,14 +201,14 @@ async def test_create_duplicate_webhook_endpoint( f"/v2/workspaces/{test_workspace.name}/webhooks", json={"url": url}, ) - assert response1.status_code == 200 + assert response1.status_code in [200, 201] # Try to create it again response2 = client.post( f"/v2/workspaces/{test_workspace.name}/webhooks", json={"url": url}, ) - assert response2.status_code == 200 + assert response2.status_code in [200, 201] assert response1.json() == response2.json() # Verify only one endpoint exists @@ -235,7 +235,7 @@ async def test_max_webhook_endpoints_per_workspace( "url": f"http://example{i}.com/webhook", }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Try to create one more response = client.post( @@ -253,7 +253,7 @@ async def test_same_endpoint_in_different_workspaces( ): ws1, _ = sample_data ws2_response = client.post("/v2/workspaces", json={"name": "workspace-2"}) - assert ws2_response.status_code == 200 + assert ws2_response.status_code in [200, 201] ws2 = ws2_response.json() url = "http://example.com/shared" @@ -263,14 +263,14 @@ async def test_same_endpoint_in_different_workspaces( f"/v2/workspaces/{ws1.name}/webhooks", json={"url": url}, ) - assert response1.status_code == 200 + assert response1.status_code in [200, 201] # Create same endpoint in workspace 2 response2 = client.post( f"/v2/workspaces/{ws2['id']}/webhooks", json={"url": url}, ) - assert response2.status_code == 200 + assert response2.status_code in [200, 201] # Verify they are different resources assert response1.json()["id"] != response2.json()["id"] diff --git a/tests/routes/test_workspaces.py b/tests/routes/test_workspaces.py index f43e4dbe..d1ac27df 100644 --- a/tests/routes/test_workspaces.py +++ b/tests/routes/test_workspaces.py @@ -12,7 +12,7 @@ def test_get_or_create_workspace(client: TestClient): # This should create the workspace using POST /v2/workspaces response = client.post("/v2/workspaces", json={"name": name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert "id" in data @@ -26,7 +26,7 @@ def test_get_or_create_workspace_with_configuration(client: TestClient): response = client.post( "/v2/workspaces", json={"name": name, "configuration": configuration} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["configuration"] == configuration @@ -42,7 +42,7 @@ def test_get_or_create_workspace_with_all_optional_params(client: TestClient): "/v2/workspaces", json={"name": name, "metadata": metadata, "configuration": configuration}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["metadata"] == metadata @@ -56,14 +56,14 @@ def test_get_or_create_existing_workspace(client: TestClient): response = client.post( "/v2/workspaces", json={"name": name, "metadata": {"key": "value"}} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] workspace1 = response.json() # Try to create the same workspace again - should return existing workspace response = client.post( "/v2/workspaces", json={"name": name, "metadata": {"key": "value"}} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] workspace2 = response.json() # Both should be the same workspace @@ -207,13 +207,13 @@ def test_create_duplicate_workspace_name(client: TestClient): # Create an workspace name = str(generate_nanoid()) response = client.post("/v2/workspaces", json={"name": name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Try to create another workspace with the same name - should return existing workspace response = client.post("/v2/workspaces", json={"name": name}) # Should return the existing workspace with 200 status (get_or_create behavior) - assert response.status_code == 200 + assert response.status_code in [200, 201] data = response.json() assert data["id"] == name @@ -272,15 +272,13 @@ def test_delete_workspace(client: TestClient): # Create a workspace response = client.post("/v2/workspaces", json={"name": name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] workspace = response.json() assert workspace["id"] == name # Delete the workspace response = client.delete(f"/v2/workspaces/{name}") - assert response.status_code == 200 - deleted_workspace = response.json() - assert deleted_workspace["id"] == name + assert response.status_code == 204 # Verify the workspace no longer exists by trying to update it response = client.put( @@ -307,7 +305,7 @@ def test_delete_workspace_with_peers(client: TestClient): # Create workspace response = client.post("/v2/workspaces", json={"name": workspace_name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create peers peer1_name = str(generate_nanoid()) @@ -315,15 +313,15 @@ def test_delete_workspace_with_peers(client: TestClient): response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer1_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer2_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Delete workspace response = client.delete(f"/v2/workspaces/{workspace_name}") - assert response.status_code == 200 + assert response.status_code == 204 def test_delete_workspace_with_sessions(client: TestClient): @@ -332,14 +330,14 @@ def test_delete_workspace_with_sessions(client: TestClient): # Create workspace response = client.post("/v2/workspaces", json={"name": workspace_name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create peer peer_name = str(generate_nanoid()) response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create sessions session1_name = str(generate_nanoid()) @@ -347,15 +345,15 @@ def test_delete_workspace_with_sessions(client: TestClient): response = client.post( f"/v2/workspaces/{workspace_name}/sessions", json={"name": session1_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] response = client.post( f"/v2/workspaces/{workspace_name}/sessions", json={"name": session2_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Delete workspace response = client.delete(f"/v2/workspaces/{workspace_name}") - assert response.status_code == 200 + assert response.status_code == 204 def test_delete_workspace_with_messages(client: TestClient): @@ -364,21 +362,21 @@ def test_delete_workspace_with_messages(client: TestClient): # Create workspace response = client.post("/v2/workspaces", json={"name": workspace_name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create peer peer_name = str(generate_nanoid()) response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create session session_name = str(generate_nanoid()) response = client.post( f"/v2/workspaces/{workspace_name}/sessions", json={"name": session_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Add peer to session response = client.post( @@ -397,11 +395,11 @@ def test_delete_workspace_with_messages(client: TestClient): ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 # Delete workspace response = client.delete(f"/v2/workspaces/{workspace_name}") - assert response.status_code == 200 + assert response.status_code == 204 def test_delete_workspace_with_webhooks(client: TestClient): @@ -410,7 +408,7 @@ def test_delete_workspace_with_webhooks(client: TestClient): # Create workspace response = client.post("/v2/workspaces", json={"name": workspace_name}) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create webhook response = client.post( @@ -419,11 +417,11 @@ def test_delete_workspace_with_webhooks(client: TestClient): "url": "https://example.com/webhook", }, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Delete workspace response = client.delete(f"/v2/workspaces/{workspace_name}") - assert response.status_code == 200 + assert response.status_code == 204 # Verify webhook is deleted by checking workspace doesn't exist response = client.get(f"/v2/workspaces/{workspace_name}/webhooks") @@ -440,7 +438,7 @@ def test_delete_workspace_cascade(client: TestClient): "/v2/workspaces", json={"name": workspace_name, "metadata": {"test": "cascade"}}, ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create multiple peers peer_names = [str(generate_nanoid()) for _ in range(3)] @@ -448,7 +446,7 @@ def test_delete_workspace_cascade(client: TestClient): response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Create multiple sessions session_names = [str(generate_nanoid()) for _ in range(2)] @@ -456,7 +454,7 @@ def test_delete_workspace_cascade(client: TestClient): response = client.post( f"/v2/workspaces/{workspace_name}/sessions", json={"name": session_name} ) - assert response.status_code == 200 + assert response.status_code in [200, 201] # Add peers to sessions and create messages for session_name in session_names: @@ -479,18 +477,15 @@ def test_delete_workspace_cascade(client: TestClient): ] }, ) - assert response.status_code == 200 + assert response.status_code == 201 # Delete the workspace response = client.delete(f"/v2/workspaces/{workspace_name}") - assert response.status_code == 200 - deleted_workspace = response.json() - assert deleted_workspace["id"] == workspace_name - assert deleted_workspace["metadata"]["test"] == "cascade" + assert response.status_code == 204 -def test_delete_workspace_returns_workspace_data(client: TestClient): - """Test that delete workspace returns the deleted workspace data""" +def test_delete_workspace_returns_no_content(client: TestClient): + """Test that delete workspace returns 204 No Content""" name = str(generate_nanoid()) metadata = {"key": "value", "number": 42} configuration = {"feature": True} @@ -500,16 +495,8 @@ def test_delete_workspace_returns_workspace_data(client: TestClient): "/v2/workspaces", json={"name": name, "metadata": metadata, "configuration": configuration}, ) - assert response.status_code == 200 - created_workspace = response.json() + assert response.status_code in [200, 201] # Delete workspace response = client.delete(f"/v2/workspaces/{name}") - assert response.status_code == 200 - deleted_workspace = response.json() - - # Verify returned data matches original workspace - assert deleted_workspace["id"] == created_workspace["id"] - assert deleted_workspace["metadata"] == metadata - assert deleted_workspace["configuration"] == configuration - assert "created_at" in deleted_workspace + assert response.status_code == 204 diff --git a/tests/sdk/test_client.py b/tests/sdk/test_client.py index 6c5d8670..1b247d9f 100644 --- a/tests/sdk/test_client.py +++ b/tests/sdk/test_client.py @@ -2,7 +2,7 @@ from unittest.mock import patch import pytest from fastapi.testclient import TestClient -from honcho_core.types import DeriverStatus +from honcho_core.types.workspaces import QueueStatusResponse from honcho_core.types.workspaces.sessions.message import Message from sdks.python.src.honcho.async_client.client import AsyncHoncho @@ -200,8 +200,8 @@ async def test_get_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, st if client_type == "async": assert isinstance(honcho_client, AsyncHoncho) # Test with no parameters - this should work in the SDK even though API requires at least one - status = await honcho_client.get_deriver_status() - assert isinstance(status, DeriverStatus) + status = await honcho_client.get_queue_status() + assert isinstance(status, QueueStatusResponse) assert hasattr(status, "total_work_units") assert hasattr(status, "completed_work_units") assert hasattr(status, "in_progress_work_units") @@ -210,67 +210,65 @@ 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=peer.id) - assert isinstance(status, DeriverStatus) + status = await honcho_client.get_queue_status(observer=peer.id) + assert isinstance(status, QueueStatusResponse) # 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=session.id) - assert isinstance(status, DeriverStatus) + status = await honcho_client.get_queue_status(session=session.id) + assert isinstance(status, QueueStatusResponse) # Test with both peer and session - status = await honcho_client.get_deriver_status( + status = await honcho_client.get_queue_status( observer=peer.id, session=session.id ) - assert isinstance(status, DeriverStatus) + assert isinstance(status, QueueStatusResponse) # Test with sender - status = await honcho_client.get_deriver_status( - observer=peer.id, sender=peer.id - ) - assert isinstance(status, DeriverStatus) + status = await honcho_client.get_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) else: assert isinstance(honcho_client, Honcho) # Test with no parameters - status = honcho_client.get_deriver_status() - assert isinstance(status, DeriverStatus) + status = honcho_client.get_queue_status() + assert isinstance(status, QueueStatusResponse) assert hasattr(status, "total_work_units") assert hasattr(status, "completed_work_units") assert hasattr(status, "in_progress_work_units") assert hasattr(status, "pending_work_units") # Test with peer_id only - peer = honcho_client.peer(id="test-peer-deriver-status") + peer = honcho_client.peer(id="test-peer-queue-status") peer.get_metadata() # Create the peer - status = honcho_client.get_deriver_status(observer=peer.id) - assert isinstance(status, DeriverStatus) + status = honcho_client.get_queue_status(observer=peer.id) + assert isinstance(status, QueueStatusResponse) # Test with session_id only - session = honcho_client.session(id="test-session-deriver-status") + session = honcho_client.session(id="test-session-queue-status") session.get_metadata() # Create the session - status = honcho_client.get_deriver_status(session=session.id) - assert isinstance(status, DeriverStatus) + status = honcho_client.get_queue_status(session=session.id) + assert isinstance(status, QueueStatusResponse) # Test with both peer and session - status = honcho_client.get_deriver_status(observer=peer.id, session=session.id) - assert isinstance(status, DeriverStatus) + status = honcho_client.get_queue_status(observer=peer.id, session=session.id) + assert isinstance(status, QueueStatusResponse) # Test with sender - status = honcho_client.get_deriver_status(observer=peer.id, sender=peer.id) - assert isinstance(status, DeriverStatus) + status = honcho_client.get_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) @pytest.mark.asyncio -async def test_poll_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, str]): +async def test_poll_queue_status(client_fixture: tuple[Honcho | AsyncHoncho, str]): """ - Tests polling deriver status until completion. + Tests polling queue status until completion. """ honcho_client, client_type = client_fixture - # Mock the get_deriver_status method to return a "completed" status + # Mock the get_queue_status method to return a "completed" status # to avoid infinite polling in tests - completed_status = DeriverStatus( + completed_status = QueueStatusResponse( total_work_units=0, completed_work_units=0, in_progress_work_units=0, @@ -280,39 +278,39 @@ async def test_poll_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, s if client_type == "async": assert isinstance(honcho_client, AsyncHoncho) with patch.object( - honcho_client, "get_deriver_status", return_value=completed_status + honcho_client, "get_queue_status", return_value=completed_status ): - status = await honcho_client.poll_deriver_status() - assert isinstance(status, DeriverStatus) + status = await honcho_client.poll_queue_status() + assert isinstance(status, QueueStatusResponse) assert status.pending_work_units == 0 assert status.in_progress_work_units == 0 # Test with parameters peer = await honcho_client.peer(id="test-peer-poll-status") with patch.object( - honcho_client, "get_deriver_status", return_value=completed_status + honcho_client, "get_queue_status", return_value=completed_status ): - status = await honcho_client.poll_deriver_status( + status = await honcho_client.poll_queue_status( observer=peer.id, sender=peer.id ) - assert isinstance(status, DeriverStatus) + assert isinstance(status, QueueStatusResponse) else: assert isinstance(honcho_client, Honcho) with patch.object( - honcho_client, "get_deriver_status", return_value=completed_status + honcho_client, "get_queue_status", return_value=completed_status ): - status = honcho_client.poll_deriver_status() - assert isinstance(status, DeriverStatus) + status = honcho_client.poll_queue_status() + assert isinstance(status, QueueStatusResponse) assert status.pending_work_units == 0 assert status.in_progress_work_units == 0 # Test with parameters peer = honcho_client.peer(id="test-peer-poll-status") with patch.object( - honcho_client, "get_deriver_status", return_value=completed_status + honcho_client, "get_queue_status", return_value=completed_status ): - status = honcho_client.poll_deriver_status(observer=peer.id, sender=peer.id) - assert isinstance(status, DeriverStatus) + status = honcho_client.poll_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) @pytest.mark.asyncio diff --git a/tests/sdk/test_observations.py b/tests/sdk/test_conclusions.py similarity index 85% rename from tests/sdk/test_observations.py rename to tests/sdk/test_conclusions.py index 3e6faa33..e2476ef7 100644 --- a/tests/sdk/test_observations.py +++ b/tests/sdk/test_conclusions.py @@ -1,13 +1,14 @@ """Tests for observation SDK methods.""" import pytest +from honcho_core.types.workspaces.conclusion import Conclusion 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, +from sdks.python.src.honcho.conclusions import ( + AsyncConclusionScope, + ConclusionCreateParams, + ConclusionScope, ) @@ -35,16 +36,21 @@ async def test_observation_create_single( ) # Get observation scope for observer -> target - obs_scope = observer.observations_of(target) - assert isinstance(obs_scope, AsyncObservationScope) + obs_scope = observer.conclusions_of(target) + assert isinstance(obs_scope, AsyncConclusionScope) # Create a single observation created = await obs_scope.create( - {"content": "User prefers dark mode", "session_id": session.id} + [ + ConclusionCreateParams( + content="User prefers dark mode", + session_id=session.id, + ) + ] ) assert len(created) == 1 - assert isinstance(created[0], Observation) + assert isinstance(created[0], Conclusion) assert created[0].content == "User prefers dark mode" assert created[0].observer_id == observer.id assert created[0].observed_id == target.id @@ -65,16 +71,21 @@ async def test_observation_create_single( ) # Get observation scope for observer -> target - obs_scope = observer.observations_of(target) - assert isinstance(obs_scope, ObservationScope) + obs_scope = observer.conclusions_of(target) + assert isinstance(obs_scope, ConclusionScope) # Create a single observation created = obs_scope.create( - {"content": "User prefers dark mode", "session_id": session.id} + [ + ConclusionCreateParams( + content="User prefers dark mode", + session_id=session.id, + ) + ] ) assert len(created) == 1 - assert isinstance(created[0], Observation) + assert isinstance(created[0], Conclusion) assert created[0].content == "User prefers dark mode" assert created[0].observer_id == observer.id assert created[0].observed_id == target.id @@ -106,14 +117,23 @@ async def test_observation_create_batch( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_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}, + ConclusionCreateParams( + content="User prefers dark mode", + session_id=session.id, + ), + ConclusionCreateParams( + content="User works late at night", + session_id=session.id, + ), + ConclusionCreateParams( + content="User enjoys programming", + session_id=session.id, + ), ] ) @@ -143,7 +163,7 @@ async def test_observation_create_batch( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create multiple observations created = obs_scope.create( @@ -191,7 +211,7 @@ async def test_observation_create_then_list( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observations created = await obs_scope.create( @@ -206,8 +226,12 @@ async def test_observation_create_then_list( # List observations listed = await obs_scope.list() + listed_all: list[Conclusion] = [ + Conclusion.model_validate(item) for item in listed.items + ] + # The created observation should be in the list - listed_ids = {obs.id for obs in listed} + listed_ids = {obs.id for obs in listed_all} assert created[0].id in listed_ids else: assert isinstance(honcho_client, Honcho) @@ -224,7 +248,7 @@ async def test_observation_create_then_list( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observations created = obs_scope.create( @@ -268,7 +292,7 @@ async def test_observation_create_then_query( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observation with specific content await obs_scope.create( @@ -302,7 +326,7 @@ async def test_observation_create_then_query( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observation with specific content obs_scope.create( @@ -347,7 +371,7 @@ async def test_observation_create_then_delete( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observations created = await obs_scope.create( @@ -363,7 +387,10 @@ async def test_observation_create_then_delete( # List observations - should not contain deleted one listed = await obs_scope.list() - listed_ids = {obs.id for obs in listed} + listed_all: list[Conclusion] = [ + Conclusion.model_validate(item) for item in listed.items + ] + listed_ids = {obs.id for obs in listed_all} assert observation_id not in listed_ids else: assert isinstance(honcho_client, Honcho) @@ -380,7 +407,7 @@ async def test_observation_create_then_delete( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observations created = obs_scope.create( @@ -418,14 +445,14 @@ async def test_self_observation_create( await session.add_messages([peer.message("Hello")]) # Get self-observation scope - obs_scope = peer.observations - assert isinstance(obs_scope, AsyncObservationScope) + obs_scope = peer.conclusions + assert isinstance(obs_scope, AsyncConclusionScope) 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} + [{"content": "I prefer morning workouts", "session_id": session.id}] ) assert len(created) == 1 @@ -440,14 +467,14 @@ async def test_self_observation_create( session.add_messages([peer.message("Hello")]) # Get self-observation scope - obs_scope = peer.observations - assert isinstance(obs_scope, ObservationScope) + obs_scope = peer.conclusions + assert isinstance(obs_scope, ConclusionScope) 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} + [{"content": "I prefer morning workouts", "session_id": session.id}] ) assert len(created) == 1 @@ -486,7 +513,7 @@ async def test_observation_create_with_session_filter( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observations in different sessions await obs_scope.create( @@ -502,13 +529,19 @@ async def test_observation_create_with_session_filter( # List filtered by session1 s1_obs = await obs_scope.list(session=session1) - s1_contents = [obs.content for obs in s1_obs] + s1_obs_all: list[Conclusion] = [ + Conclusion.model_validate(item) for item in s1_obs.items + ] + s1_contents = [obs.content for obs in s1_obs_all] 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] + s2_obs_all: list[Conclusion] = [ + Conclusion.model_validate(item) for item in s2_obs.items + ] + s2_contents = [obs.content for obs in s2_obs_all] assert "Session 2 observation" in s2_contents assert "Session 1 observation" not in s2_contents else: @@ -533,7 +566,7 @@ async def test_observation_create_with_session_filter( ) # Get observation scope - obs_scope = observer.observations_of(target) + obs_scope = observer.conclusions_of(target) # Create observations in different sessions obs_scope.create( @@ -565,7 +598,7 @@ async def test_observation_scope_via_peer_string( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests creating observations via observations_of(string). + Tests creating observations via conclusions_of(string). """ honcho_client, client_type = client_fixture @@ -584,12 +617,12 @@ async def test_observation_scope_via_peer_string( ) # Get observation scope using string ID - obs_scope = observer.observations_of(target.id) + obs_scope = observer.conclusions_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} + [{"content": "Created via string target", "session_id": session.id}] ) assert len(created) == 1 @@ -609,12 +642,12 @@ async def test_observation_scope_via_peer_string( ) # Get observation scope using string ID - obs_scope = observer.observations_of(target.id) + obs_scope = observer.conclusions_of(target.id) assert obs_scope.observed == target.id # Create observation created = obs_scope.create( - {"content": "Created via string target", "session_id": session.id} + [{"content": "Created via string target", "session_id": session.id}] ) assert len(created) == 1 diff --git a/tests/sdk/test_peer.py b/tests/sdk/test_peer.py index 01373ab0..7e772846 100644 --- a/tests/sdk/test_peer.py +++ b/tests/sdk/test_peer.py @@ -6,7 +6,7 @@ from sdks.python.src.honcho.async_client.client import AsyncHoncho from sdks.python.src.honcho.async_client.peer import AsyncPeer from sdks.python.src.honcho.client import Honcho from sdks.python.src.honcho.peer import Peer -from sdks.python.src.honcho.types import DialecticStreamResponse, Representation +from sdks.python.src.honcho.types import DialecticStreamResponse @pytest.mark.asyncio @@ -292,11 +292,11 @@ async def test_peer_chat_non_streaming( @pytest.mark.asyncio -async def test_peer_working_rep_no_params( +async def test_peer_get_representation_no_params( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with no parameters (default behavior). + Tests peer.get_representation() with no parameters (default behavior). """ honcho_client, client_type = client_fixture @@ -309,10 +309,8 @@ async def test_peer_working_rep_no_params( await session.add_messages([peer.message("I enjoy hiking and nature")]) # Get working representation with no parameters - result = await peer.working_rep() - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await peer.get_representation() + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) peer = honcho_client.peer(id="test-working-rep-no-params") @@ -322,18 +320,16 @@ async def test_peer_working_rep_no_params( session.add_messages([peer.message("I enjoy hiking and nature")]) # Get working representation with no parameters - result = peer.working_rep() - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = peer.get_representation() + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_session_string( +async def test_peer_get_representation_with_session_string( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with session parameter as string. + Tests peer.get_representation() with session parameter as string. """ honcho_client, client_type = client_fixture @@ -346,10 +342,8 @@ async def test_peer_working_rep_with_session_string( await session.add_messages([peer.message("I like reading books")]) # Get working representation scoped to session (as string) - result = await peer.working_rep(session=session.id) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await peer.get_representation(session=session.id) + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) peer = honcho_client.peer(id="test-working-rep-session-str") @@ -359,18 +353,16 @@ async def test_peer_working_rep_with_session_string( session.add_messages([peer.message("I like reading books")]) # Get working representation scoped to session (as string) - result = peer.working_rep(session=session.id) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = peer.get_representation(session=session.id) + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_session_object( +async def test_peer_get_representation_with_session_object( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with session parameter as Session object. + Tests peer.get_representation() with session parameter as Session object. """ honcho_client, client_type = client_fixture @@ -386,10 +378,8 @@ async def test_peer_working_rep_with_session_object( await session.add_messages([peer.message("I prefer tea over coffee")]) # Get working representation scoped to session (as Session object) - result = await peer.working_rep(session=session) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await peer.get_representation(session=session) + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) peer = honcho_client.peer(id="test-working-rep-session-obj") @@ -402,18 +392,16 @@ async def test_peer_working_rep_with_session_object( session.add_messages([peer.message("I prefer tea over coffee")]) # Get working representation scoped to session (as Session object) - result = peer.working_rep(session=session) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = peer.get_representation(session=session) + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_target_string( +async def test_peer_get_representation_with_target_string( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with target parameter as string. + Tests peer.get_representation() with target parameter as string. """ honcho_client, client_type = client_fixture @@ -432,10 +420,8 @@ async def test_peer_working_rep_with_target_string( ) # Get working representation of target from observer's perspective (as string) - result = await observer.working_rep(target=target.id) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await observer.get_representation(target=target.id) + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) observer = honcho_client.peer(id="test-working-rep-target-str-observer") @@ -451,18 +437,16 @@ async def test_peer_working_rep_with_target_string( ) # Get working representation of target from observer's perspective (as string) - result = observer.working_rep(target=target.id) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = observer.get_representation(target=target.id) + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_target_object( +async def test_peer_get_representation_with_target_object( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with target parameter as Peer object. + Tests peer.get_representation() with target parameter as Peer object. """ honcho_client, client_type = client_fixture @@ -484,10 +468,8 @@ async def test_peer_working_rep_with_target_object( ) # Get working representation of target from observer's perspective (as Peer object) - result = await observer.working_rep(target=target) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await observer.get_representation(target=target) + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) observer = honcho_client.peer(id="test-working-rep-target-obj-observer") @@ -506,18 +488,16 @@ async def test_peer_working_rep_with_target_object( ) # Get working representation of target from observer's perspective (as Peer object) - result = observer.working_rep(target=target) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = observer.get_representation(target=target) + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_search_query( +async def test_peer_get_representation_with_search_query( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with search_query parameter. + Tests peer.get_representation() with search_query parameter. """ honcho_client, client_type = client_fixture @@ -535,10 +515,8 @@ async def test_peer_working_rep_with_search_query( ) # Get working representation with search query - result = await peer.working_rep(search_query="programming") - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await peer.get_representation(search_query="programming") + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) peer = honcho_client.peer(id="test-working-rep-search-query") @@ -553,18 +531,16 @@ async def test_peer_working_rep_with_search_query( ) # Get working representation with search query - result = peer.working_rep(search_query="programming") - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = peer.get_representation(search_query="programming") + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_size( +async def test_peer_get_representation_with_size( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with size parameter. + Tests peer.get_representation() with size parameter. """ honcho_client, client_type = client_fixture @@ -578,22 +554,16 @@ async def test_peer_working_rep_with_size( [peer.message(f"Message number {i}") for i in range(10)] ) - # Get working representation with custom max_observations - result = await peer.working_rep(max_observations=5) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + # Get working representation with custom max_conclusions + result = await peer.get_representation(max_conclusions=5) + assert isinstance(result, str) - # Test with different max_observations values - result = await peer.working_rep(max_observations=1) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + # Test with different max_conclusions values + result = await peer.get_representation(max_conclusions=1) + assert isinstance(result, str) - result = await peer.working_rep(max_observations=100) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = await peer.get_representation(max_conclusions=100) + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) peer = honcho_client.peer(id="test-working-rep-size") @@ -603,29 +573,23 @@ async def test_peer_working_rep_with_size( session.add_messages([peer.message(f"Message number {i}") for i in range(10)]) # Get working representation with custom size - result = peer.working_rep(max_observations=5) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = peer.get_representation(max_conclusions=5) + assert isinstance(result, str) - # Test with different max_observations values - result = peer.working_rep(max_observations=1) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + # Test with different max_conclusions values + result = peer.get_representation(max_conclusions=1) + assert isinstance(result, str) - result = peer.working_rep(max_observations=100) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + result = peer.get_representation(max_conclusions=100) + assert isinstance(result, str) @pytest.mark.asyncio -async def test_peer_working_rep_with_all_params( +async def test_peer_get_representation_with_all_params( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ - Tests peer.working_rep() with all parameters combined. + Tests peer.get_representation() with all parameters combined. """ honcho_client, client_type = client_fixture @@ -646,23 +610,19 @@ async def test_peer_working_rep_with_all_params( ) # Get working representation with all parameters - result = await observer.working_rep( - session=session, target=target, search_query="Python", max_observations=10 + result = await observer.get_representation( + session=session, target=target, search_query="Python", max_conclusions=10 ) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + assert isinstance(result, str) # Test with session as string and target as string - result = await observer.working_rep( + result = await observer.get_representation( session=session.id, target=target.id, search_query="machine learning", - max_observations=5, + max_conclusions=5, ) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + assert isinstance(result, str) else: assert isinstance(honcho_client, Honcho) observer = honcho_client.peer(id="test-working-rep-all-observer") @@ -680,20 +640,16 @@ async def test_peer_working_rep_with_all_params( ) # Get working representation with all parameters - result = observer.working_rep( - session=session, target=target, search_query="Python", max_observations=10 + result = observer.get_representation( + session=session, target=target, search_query="Python", max_conclusions=10 ) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + assert isinstance(result, str) # Test with session as string and target as string - result = observer.working_rep( + result = observer.get_representation( session=session.id, target=target.id, search_query="machine learning", - max_observations=5, + max_conclusions=5, ) - assert isinstance(result, Representation) - assert hasattr(result, "explicit") - assert hasattr(result, "deductive") + assert isinstance(result, str) diff --git a/tests/sdk/test_session.py b/tests/sdk/test_session.py index ebf04615..beed854c 100644 --- a/tests/sdk/test_session.py +++ b/tests/sdk/test_session.py @@ -1,7 +1,7 @@ from unittest.mock import AsyncMock, patch import pytest -from honcho_core.types import DeriverStatus +from honcho_core.types.workspaces import QueueStatusResponse from sdks.python.src.honcho.async_client.client import AsyncHoncho from sdks.python.src.honcho.async_client.peer import AsyncPeer @@ -355,7 +355,9 @@ async def test_session_add_messages_return_value( @pytest.mark.asyncio -async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, str]): +async def test_session_get_representation( + client_fixture: tuple[Honcho | AsyncHoncho, str], +): """ Tests getting the working representation of a peer in a session. """ @@ -368,7 +370,7 @@ async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, s peer = await honcho_client.peer(id="peer-wr") assert isinstance(peer, AsyncPeer) await session.add_messages([peer.message("test message for working rep")]) - _working_rep = await session.working_rep(peer) + await session.get_representation(peer) else: assert isinstance(honcho_client, Honcho) session = honcho_client.session(id="test-session-wr") @@ -376,7 +378,7 @@ async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, s peer = honcho_client.peer(id="peer-wr") assert isinstance(peer, Peer) session.add_messages([peer.message("test message for working rep")]) - _working_rep = session.working_rep(peer) + session.get_representation(peer) @pytest.mark.asyncio @@ -449,7 +451,7 @@ async def test_session_delete(client_fixture: tuple[Honcho | AsyncHoncho, str]) @pytest.mark.asyncio -async def test_session_get_deriver_status( +async def test_session_get_queue_status( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ @@ -462,8 +464,8 @@ async def test_session_get_deriver_status( session = await honcho_client.session(id="test-session-deriver-status") assert isinstance(session, AsyncSession) - status = await session.get_deriver_status() - assert isinstance(status, DeriverStatus) + status = await session.get_queue_status() + assert isinstance(status, QueueStatusResponse) assert hasattr(status, "total_work_units") assert hasattr(status, "completed_work_units") assert hasattr(status, "in_progress_work_units") @@ -473,24 +475,24 @@ async def test_session_get_deriver_status( # 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=peer.id) - assert isinstance(status, DeriverStatus) + status = await session.get_queue_status(observer=peer.id) + assert isinstance(status, QueueStatusResponse) # Test with sender only - status = await session.get_deriver_status(sender=peer.id) - assert isinstance(status, DeriverStatus) + status = await session.get_queue_status(sender=peer.id) + assert isinstance(status, QueueStatusResponse) # Test with both observer and sender - status = await session.get_deriver_status(observer=peer.id, sender=peer.id) - assert isinstance(status, DeriverStatus) + status = await session.get_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) else: assert isinstance(honcho_client, Honcho) session = honcho_client.session(id="test-session-deriver-status") assert isinstance(session, Session) # Test with no parameters - status = session.get_deriver_status() - assert isinstance(status, DeriverStatus) + status = session.get_queue_status() + assert isinstance(status, QueueStatusResponse) assert hasattr(status, "total_work_units") assert hasattr(status, "completed_work_units") assert hasattr(status, "in_progress_work_units") @@ -500,20 +502,20 @@ async def test_session_get_deriver_status( # 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=peer.id) - assert isinstance(status, DeriverStatus) + status = session.get_queue_status(observer=peer.id) + assert isinstance(status, QueueStatusResponse) # Test with sender only - status = session.get_deriver_status(sender=peer.id) - assert isinstance(status, DeriverStatus) + status = session.get_queue_status(sender=peer.id) + assert isinstance(status, QueueStatusResponse) # Test with both observer and sender - status = session.get_deriver_status(observer=peer.id, sender=peer.id) - assert isinstance(status, DeriverStatus) + status = session.get_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) @pytest.mark.asyncio -async def test_session_poll_deriver_status( +async def test_session_poll_queue_status( client_fixture: tuple[Honcho | AsyncHoncho, str], ): """ @@ -521,9 +523,9 @@ async def test_session_poll_deriver_status( """ honcho_client, client_type = client_fixture - # Mock the get_deriver_status method to return a "completed" status + # Mock the get_queue_status method to return a "completed" status # to avoid infinite polling in tests - completed_status = DeriverStatus( + completed_status = QueueStatusResponse( total_work_units=0, completed_work_units=0, in_progress_work_units=0, @@ -532,16 +534,16 @@ async def test_session_poll_deriver_status( if client_type == "async": assert isinstance(honcho_client, AsyncHoncho) - session = await honcho_client.session(id="test-session-poll-deriver") + session = await honcho_client.session(id="test-session-poll-queue") assert isinstance(session, AsyncSession) with patch.object( session.__class__, - "get_deriver_status", + "get_queue_status", new=AsyncMock(return_value=completed_status), ): - status = await session.poll_deriver_status() - assert isinstance(status, DeriverStatus) + status = await session.poll_queue_status() + assert isinstance(status, QueueStatusResponse) assert status.pending_work_units == 0 assert status.in_progress_work_units == 0 @@ -549,31 +551,31 @@ async def test_session_poll_deriver_status( peer = await honcho_client.peer(id="test-peer-session-poll") with patch.object( session.__class__, - "get_deriver_status", + "get_queue_status", new=AsyncMock(return_value=completed_status), ): - status = await session.poll_deriver_status(observer=peer.id, sender=peer.id) - assert isinstance(status, DeriverStatus) + status = await session.poll_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) else: assert isinstance(honcho_client, Honcho) - session = honcho_client.session(id="test-session-poll-deriver") + session = honcho_client.session(id="test-session-poll-queue") assert isinstance(session, Session) with patch.object( - session.__class__, "get_deriver_status", return_value=completed_status + session.__class__, "get_queue_status", return_value=completed_status ): - status = session.poll_deriver_status() - assert isinstance(status, DeriverStatus) + status = session.poll_queue_status() + assert isinstance(status, QueueStatusResponse) assert status.pending_work_units == 0 assert status.in_progress_work_units == 0 # Test with parameters peer = honcho_client.peer(id="test-peer-session-poll") with patch.object( - session.__class__, "get_deriver_status", return_value=completed_status + session.__class__, "get_queue_status", return_value=completed_status ): - status = session.poll_deriver_status(observer=peer.id, sender=peer.id) - assert isinstance(status, DeriverStatus) + status = session.poll_queue_status(observer=peer.id, sender=peer.id) + assert isinstance(status, QueueStatusResponse) @pytest.mark.asyncio diff --git a/tests/test_advanced_filters.py b/tests/test_advanced_filters.py index 8afab352..64d4633f 100644 --- a/tests/test_advanced_filters.py +++ b/tests/test_advanced_filters.py @@ -174,7 +174,7 @@ async def test_comparison_operators_filters( f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Create messages with numeric metadata for comparison tests message_configs = [ @@ -203,7 +203,7 @@ async def test_comparison_operators_filters( f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={"messages": message_configs}, ) - assert messages_response.status_code == 200 + assert messages_response.status_code == 201 # Test the filter configuration response = client.post( @@ -870,7 +870,7 @@ async def test_all_message_columns_filtering( f"/v2/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) - assert session_response.status_code == 200 + assert session_response.status_code == 201 # Create messages with various data messages_response = client.post( @@ -895,7 +895,7 @@ async def test_all_message_columns_filtering( ] }, ) - assert messages_response.status_code == 200 + assert messages_response.status_code == 201 # Test filtering by session_id (maps to session_name internally) response = client.post( @@ -2090,13 +2090,13 @@ async def test_float_precision_edge_cases(client: TestClient): # Create workspace response = client.post("/v2/workspaces", json={"name": workspace_name}) - assert response.status_code == 200 + assert response.status_code == 201 # Create peer response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer_name} ) - assert response.status_code == 200 + assert response.status_code == 201 # Create messages with problematic floating point values using correct endpoint messages_response = client.post( @@ -2160,9 +2160,9 @@ async def test_float_precision_edge_cases(client: TestClient): ] }, ) - assert messages_response.status_code == 200 + assert messages_response.status_code == 201 - # Test exact equality - this may or may not work due to floating point precision + # Test exact equality response = client.post( f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list", json={"filters": {"metadata": {"value": 0.3}}}, @@ -2255,13 +2255,13 @@ async def test_mixed_type_comparisons(client: TestClient): # Create workspace response = client.post("/v2/workspaces", json={"name": workspace_name}) - assert response.status_code == 200 + assert response.status_code == 201 # Create peer response = client.post( f"/v2/workspaces/{workspace_name}/peers", json={"name": peer_name} ) - assert response.status_code == 200 + assert response.status_code == 201 # Create messages with mixed data types for the same logical field messages_response = client.post( @@ -2331,7 +2331,7 @@ async def test_mixed_type_comparisons(client: TestClient): ] }, ) - assert messages_response.status_code == 200 + assert messages_response.status_code == 201 # Test string vs numeric equality response = client.post( diff --git a/tests/unified/runner.py b/tests/unified/runner.py index bab13596..c146d29b 100644 --- a/tests/unified/runner.py +++ b/tests/unified/runner.py @@ -13,7 +13,7 @@ import httpx from anthropic import AsyncAnthropic from honcho.async_client.session import AsyncSession from honcho.session_context import SessionContext -from honcho_core.types.deriver_status import DeriverStatus +from honcho_core.types.workspaces import QueueStatusResponse from pydantic import ValidationError # Adjust path to allow imports from tests.bench @@ -38,10 +38,10 @@ from tests.unified.schema import ( LLMJudgeAssertion, NotContainsAssertion, QueryAction, + ScheduleDreamAction, SetSessionConfigAction, SetWorkspaceConfigAction, TestDefinition, - TriggerDreamAction, WaitAction, ) @@ -271,10 +271,11 @@ class UnifiedTestExecutor: if step.target == "queue_empty": await self.wait_for_queue(step.timeout) - elif isinstance(step, TriggerDreamAction): + elif isinstance(step, ScheduleDreamAction): # Use the core SDK to trigger a dream - await self.client.core.workspaces.trigger_dream( + await self.client.core.workspaces.schedule_dream( workspace_id=self.client.workspace_id, + session_id=step.session_id, observer=step.observer, observed=step.observed, dream_type=step.dream_type.value, @@ -291,7 +292,7 @@ class UnifiedTestExecutor: await asyncio.sleep(1) start = time.time() while time.time() - start < timeout: - status: DeriverStatus = await self.client.get_deriver_status() + status: QueueStatusResponse = await self.client.get_queue_status() # status structure from schema: DeriverStatus with pending_work_units, in_progress_work_units if status.pending_work_units == 0 and status.in_progress_work_units == 0: return @@ -339,7 +340,7 @@ class UnifiedTestExecutor: raise ValueError("observer_peer_id required for get_representation") peer = await self.client.peer(id=step.observer_peer_id) - representation = await peer.working_rep( + representation = await peer.get_representation( step.session_id, target=step.observed_peer_id, search_query=step.input ) return representation diff --git a/tests/unified/schema.py b/tests/unified/schema.py index 0fa840b0..0e59b723 100644 --- a/tests/unified/schema.py +++ b/tests/unified/schema.py @@ -77,13 +77,14 @@ class WaitAction(TestStep): # --- Dream Actions --- -class TriggerDreamAction(TestStep): - step_type: Literal["trigger_dream"] = "trigger_dream" +class ScheduleDreamAction(TestStep): + step_type: Literal["schedule_dream"] = "schedule_dream" observer: str = Field(..., description="Observer peer name") observed: str | None = Field( None, description="Observed peer name (defaults to observer if not specified)" ) - dream_type: DreamType = Field(..., description="Type of dream to trigger") + session_id: str = Field(..., description="Session ID to scope the dream to") + dream_type: DreamType = Field(..., description="Type of dream to schedule") # --- Assertions --- @@ -163,7 +164,7 @@ class TestDefinition(BaseModel): | AddMessageAction | AddMessagesAction | WaitAction - | TriggerDreamAction + | ScheduleDreamAction | QueryAction, Field(discriminator="step_type"), ] diff --git a/tests/unified/test_cases/dream_consolidate_reduces_documents.json b/tests/unified/test_cases/dream_consolidate_reduces_documents.json index f8b1b159..e77b3b73 100644 --- a/tests/unified/test_cases/dream_consolidate_reduces_documents.json +++ b/tests/unified/test_cases/dream_consolidate_reduces_documents.json @@ -108,9 +108,10 @@ ] }, { - "step_type": "trigger_dream", + "step_type": "schedule_dream", "observer": "user", - "dream_type": "omni" + "dream_type": "omni", + "session_id": "session_dream_test" }, { "step_type": "wait", diff --git a/uv.lock b/uv.lock index 741decae..d9df1526 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13'", @@ -859,7 +859,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "honcho-core", specifier = ">=1.8.0" }, + { name = "honcho-core", specifier = "==1.10.0" }, { 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" }, @@ -870,7 +870,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }] [[package]] name = "honcho-core" -version = "1.8.0" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -880,9 +880,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/ea/c0949bbac5a9f20625bdb152b7da2350e89ffc15b5862cd6094b464cde14/honcho_core-1.8.0.tar.gz", hash = "sha256:ffe0840639651640722ad0ed38d193cc9402b077dac3e6726ac7be551398d952", size = 142469, upload-time = "2025-12-15T19:27:59.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/5f/912716e911966d8cefb34a23f9b3a452348f147e040c1025113a202363a2/honcho_core-1.10.0.tar.gz", hash = "sha256:1dc139f6199a7589781636b4535cc97da6016477fbaeeaee53376e7a227a5c51", size = 144284, upload-time = "2026-01-12T23:30:30.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/9a/5aba73353c7e70d331a21e01a931c951c5bd8688fb25e5fc318517f2adf9/honcho_core-1.8.0-py3-none-any.whl", hash = "sha256:30a44b7d421328dfac015e8a6ecbe09c89b6cac9f3a913262244e7d15698a8a8", size = 140580, upload-time = "2025-12-15T19:27:58.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/7cc9c5d0a91921339b843bed3d520225731199156c3410604375e5faf487/honcho_core-1.10.0-py3-none-any.whl", hash = "sha256:d0c7a1285b8e75c106b540773c0077e563c9bf6abda040da583bb7566828877d", size = 138588, upload-time = "2026-01-12T23:30:29.351Z" }, ] [[package]]