Merge pull request #319 from plastic-labs/ben/renaming-and-api-cleanup-clean
feat: API renaming and cleanup
This commit is contained in:
commit
59b203e357
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -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) |
|
||||
|
||||
<CodeGroup>
|
||||
```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
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
|
@ -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.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
|
@ -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
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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\"",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
)
|
||||
|
|
@ -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})"
|
||||
)
|
||||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -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!');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<string, DeriverStatus.Sessions>
|
||||
sessions?: Record<string, QueueStatusResponse.Sessions>
|
||||
}> {
|
||||
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<string, DeriverStatus.Sessions>
|
||||
sessions?: Record<string, QueueStatusResponse.Sessions>
|
||||
}> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>): Observation {
|
||||
return new Observation(
|
||||
static fromApiResponse(data: Record<string, unknown>): 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<Observation[]> {
|
||||
): Promise<Conclusion[]> {
|
||||
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<string, unknown>)
|
||||
Conclusion.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Observation[]> {
|
||||
): Promise<Conclusion[]> {
|
||||
const filters: Record<string, unknown> = {
|
||||
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<string, unknown>)
|
||||
Conclusion.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
// 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<void> {
|
||||
// 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<Observation[]> {
|
||||
conclusions: ConclusionCreateParam | ConclusionCreateParam[]
|
||||
): Promise<Conclusion[]> {
|
||||
// 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<string, unknown>)
|
||||
Conclusion.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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<Representation> {
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
async getRepresentation(options?: RepresentationOptions): Promise<string> {
|
||||
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}')`
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<string | DialecticStreamResponse | null> {
|
||||
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<Representation> {
|
||||
const workingRepParams = PeerWorkingRepParamsSchema.parse({
|
||||
): Promise<string> {
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, ExplicitObservation>()
|
||||
const deductiveMap = new Map<string, DeductiveObservation>()
|
||||
const explicitMap = new Map<string, ExplicitConclusion>()
|
||||
const deductiveMap = new Map<string, DeductiveConclusion>()
|
||||
|
||||
// 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,
|
||||
|
|
|
|||
|
|
@ -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<SessionPeerConfig> {
|
||||
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<Page<Observation>> {
|
||||
// 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<Observation[]> {
|
||||
// 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<void> {
|
||||
// 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<string, DeriverStatus.Sessions>
|
||||
sessions?: Record<string, QueueStatusResponse.Sessions>
|
||||
}> {
|
||||
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<string, DeriverStatus.Sessions>
|
||||
sessions?: Record<string, QueueStatusResponse.Sessions>
|
||||
}> {
|
||||
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<Representation> {
|
||||
const workingRepParams = WorkingRepParamsSchema.parse({
|
||||
): Promise<string> {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<typeof MessageCreateSchema>
|
|||
export type Filters = z.infer<typeof FilterSchema>
|
||||
export type ChatQuery = z.infer<typeof ChatQuerySchema>
|
||||
export type ContextParams = z.infer<typeof ContextParamsSchema>
|
||||
export type DeriverStatusOptions = z.infer<typeof DeriverStatusOptionsSchema>
|
||||
export type QueueStatusOptions = z.infer<typeof QueueStatusOptionsSchema>
|
||||
export type FileUpload = z.infer<typeof FileUploadSchema>
|
||||
export type WorkingRepParams = z.infer<typeof WorkingRepParamsSchema>
|
||||
export type PeerWorkingRepParams = z.infer<typeof PeerWorkingRepParamsSchema>
|
||||
export type GetRepresentationParams = z.infer<
|
||||
typeof GetRepresentationParamsSchema
|
||||
>
|
||||
export type PeerGetRepresentationParams = z.infer<
|
||||
typeof PeerGetRepresentationParamsSchema
|
||||
>
|
||||
export type PeerAddition = z.infer<typeof PeerAdditionSchema>
|
||||
export type PeerRemoval = z.infer<typeof PeerRemovalSchema>
|
||||
export type MessageAddition = z.infer<typeof MessageAdditionSchema>
|
||||
export type WorkspaceMetadata = z.infer<typeof WorkspaceMetadataSchema>
|
||||
export type WorkspaceConfig = z.infer<typeof WorkspaceConfigSchema>
|
||||
export type Limit = z.infer<typeof LimitSchema>
|
||||
export type ObservationQueryParams = z.infer<
|
||||
typeof ObservationQueryParamsSchema
|
||||
>
|
||||
export type ConclusionQueryParams = z.infer<typeof ConclusionQueryParamsSchema>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
10
src/main.py
10
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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
105
src/schemas.py
105
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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
10
uv.lock
10
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]]
|
||||
|
|
|
|||
Loading…
Reference in New Issue