Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/v3/documentation/features/advanced/using-filters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,65 @@ messages = session.messages(filters={
```
</CodeGroup>

### Filtering Conclusions

Conclusions are scoped to an observer/observed peer pair (accessed via
`peer.conclusions` for self-conclusions or `peer.conclusions_of(target)` for
conclusions about another peer). The observer and observed are filled in
automatically by the scope, so the `filters` you pass add to them.

The most useful conclusion-specific field is `level`, the reasoning level:

- `explicit` — extracted directly from messages
- `deductive` / `inductive` / `contradiction` — derived later during dreaming

A common request is to surface only the directly-stated facts and exclude
anything inferred during dreaming — filter `level` to `explicit`:

<CodeGroup>
```python Python
# Only conclusions extracted directly from messages (exclude dream-derived)
explicit = peer.conclusions.list(filters={"level": "explicit"})

# Only dream-derived conclusions
derived = peer.conclusions.list(filters={"level": {"in": ["deductive", "inductive"]}})

# Same filtering on semantic search
results = peer.conclusions.query(
"food preferences",
filters={"level": "deductive"},
)

# Conclusions about another peer, explicit only
bob_explicit = peer.conclusions_of("bob").list(filters={"level": "explicit"})
```

```typescript TypeScript
(async () => {
// Only conclusions extracted directly from messages (exclude dream-derived)
const explicit = await peer.conclusions.list({ filters: { level: "explicit" } });

// Only dream-derived conclusions
const derived = await peer.conclusions.list({
filters: { level: { in: ["deductive", "inductive"] } }
});

// Same filtering on semantic search (query, topK, distance, filters)
const results = await peer.conclusions.query(
"food preferences",
10,
undefined,
{ level: "deductive" }
);

// Conclusions about another peer, explicit only
const bobExplicit = await peer.conclusionsOf("bob").list({
filters: { level: "explicit" }
});
})();
```
</CodeGroup>

## Error Handling

Handle filter errors gracefully:
Expand Down
38 changes: 30 additions & 8 deletions sdks/python/src/honcho/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1460,24 +1460,34 @@ async def list(
size: int = 50,
session: str | SessionBase | None = None,
*,
filters: dict[str, Any] | None = None,
reverse: bool = False,
) -> AsyncPage[ConclusionResponse, Conclusion]:
"""List conclusions in this scope asynchronously."""
"""List conclusions in this scope asynchronously.

Pass ``filters`` to add criteria merged with this scope's
observer/observed (and session, if given) — e.g.
``{"level": "explicit"}`` to get only conclusions extracted directly
from messages (i.e. not derived during dreaming). See
https://honcho.dev/docs/v3/documentation/features/advanced/using-filters
"""
await self._scope._honcho._ensure_workspace_async()
resolved_session_id = resolve_id(session)
filters: dict[str, Any] = {
merged_filters: dict[str, Any] = {
"observer_id": self._scope.observer,
"observed_id": self._scope.observed,
}
if resolved_session_id:
filters["session_id"] = resolved_session_id
merged_filters["session_id"] = resolved_session_id
if filters:
merged_filters.update(filters)

query: dict[str, Any] = {"page": page, "size": size}
if reverse:
query["reverse"] = "true"
data = await self._scope._honcho._async_http_client.post(
routes.conclusions_list(self._scope.workspace_id),
body={"filters": filters},
body={"filters": merged_filters},
query=query,
)

Expand All @@ -1492,7 +1502,7 @@ async def fetch_next(
next_query["reverse"] = "true"
next_data = await self._scope._honcho._async_http_client.post(
routes.conclusions_list(self._scope.workspace_id),
body={"filters": filters},
body={"filters": merged_filters},
query=next_query,
)
return AsyncPage(next_data, ConclusionResponse, transform, fetch_next)
Expand All @@ -1504,18 +1514,30 @@ async def query(
query: str,
top_k: int = 10,
distance: float | None = None,
*,
filters: dict[str, Any] | None = None,
) -> list[Conclusion]:
"""Semantic search for conclusions asynchronously."""
"""Semantic search for conclusions asynchronously.

Args:
query: The search query string
top_k: Maximum number of results to return
distance: Maximum cosine distance threshold (0.0-1.0)
filters: Optional dictionary of additional filter criteria, merged
with this scope's observer/observed (e.g. ``{"level": "deductive"}``).
"""
await self._scope._honcho._ensure_workspace_async()
filters: dict[str, Any] = {
merged_filters: dict[str, Any] = {
"observer_id": self._scope.observer,
"observed_id": self._scope.observed,
}
if filters:
merged_filters.update(filters)

body: dict[str, Any] = {
"query": query,
"top_k": top_k,
"filters": filters,
"filters": merged_filters,
}
if distance is not None:
body["distance"] = distance
Expand Down
5 changes: 5 additions & 0 deletions sdks/python/src/honcho/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

from pydantic import BaseModel, ConfigDict, Field

# Reasoning level of a conclusion. "explicit" conclusions are extracted directly
# from messages; the others are derived during dreaming.
ConclusionLevel = Literal["explicit", "deductive", "inductive", "contradiction"]

# ==============================================================================
# Configuration Types
# ==============================================================================
Expand Down Expand Up @@ -414,6 +418,7 @@ class ConclusionResponse(BaseModel):
observer_id: str
observed_id: str
session_id: str | None = None
level: ConclusionLevel = "explicit"
created_at: datetime.datetime


Expand Down
39 changes: 32 additions & 7 deletions sdks/python/src/honcho/conclusions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from pydantic import BaseModel

from .api_types import ConclusionResponse, RepresentationResponse
from .api_types import ConclusionLevel, ConclusionResponse, RepresentationResponse
from .base import SessionBase
from .http import routes
from .pagination import SyncPage
Expand Down Expand Up @@ -43,6 +43,9 @@ class Conclusion:
observer_id: The peer ID who made this conclusion
observed_id: The peer ID this conclusion is about
session_id: The session this conclusion relates to
level: Reasoning level ("explicit", "deductive", "inductive",
"contradiction"). "explicit" conclusions are extracted directly
from messages; the others are derived during dreaming.
created_at: Timestamp for when the conclusion was created
"""

Expand All @@ -51,6 +54,7 @@ class Conclusion:
observer_id: str
observed_id: str
session_id: str | None = None
level: ConclusionLevel = "explicit"
created_at: datetime.datetime

def __init__(
Expand All @@ -61,12 +65,14 @@ def __init__(
observed_id: str,
session_id: str | None,
created_at: datetime.datetime,
level: ConclusionLevel = "explicit",
) -> None:
self.id = id
self.content = content
self.observer_id = observer_id
self.observed_id = observed_id
self.session_id = session_id
self.level = level
self.created_at = created_at

@classmethod
Expand All @@ -78,6 +84,7 @@ def from_api_response(cls, data: ConclusionResponse) -> "Conclusion":
observer_id=data.observer_id,
observed_id=data.observed_id,
session_id=data.session_id,
level=data.level,
created_at=data.created_at,
)

Expand Down Expand Up @@ -169,6 +176,7 @@ def list(
size: int = 50,
session: str | SessionBase | None = None,
*,
filters: dict[str, Any] | None = None,
reverse: bool = False,
) -> SyncPage[ConclusionResponse, Conclusion]:
"""
Expand All @@ -178,26 +186,34 @@ def list(
page: Page number (1-indexed)
size: Number of results per page
session: Optional session (ID string or Session object) to filter by
filters: Optional dictionary of additional filter criteria, merged
with this scope's observer/observed (and session, if given).
Supports the same operators as other list endpoints — e.g.
``{"level": "explicit"}`` to get only conclusions extracted
directly from messages (i.e. not derived during dreaming). See
https://honcho.dev/docs/v3/documentation/features/advanced/using-filters
reverse: If True, reverses the default ordering. Default: False.

Returns:
Paginated response containing Conclusion objects
"""
self._honcho._ensure_workspace()
resolved_session_id = resolve_id(session)
filters: dict[str, Any] = {
merged_filters: dict[str, Any] = {
"observer_id": self.observer,
"observed_id": self.observed,
}
if resolved_session_id:
filters["session_id"] = resolved_session_id
merged_filters["session_id"] = resolved_session_id
if filters:
merged_filters.update(filters)

query: dict[str, Any] = {"page": page, "size": size}
if reverse:
query["reverse"] = "true"
data = self._honcho._http.post(
routes.conclusions_list(self.workspace_id),
body={"filters": filters},
body={"filters": merged_filters},
query=query,
)

Expand All @@ -212,7 +228,7 @@ def fetch_next(
next_query["reverse"] = "true"
next_data = self._honcho._http.post(
routes.conclusions_list(self.workspace_id),
body={"filters": filters},
body={"filters": merged_filters},
query=next_query,
)
return SyncPage(next_data, ConclusionResponse, transform, fetch_next)
Expand All @@ -224,6 +240,8 @@ def query(
query: str,
top_k: int = 10,
distance: float | None = None,
*,
filters: dict[str, Any] | None = None,
) -> list[Conclusion]:
"""
Semantic search for conclusions in this scope.
Expand All @@ -232,20 +250,27 @@ def query(
query: The search query string
top_k: Maximum number of results to return
distance: Maximum cosine distance threshold (0.0-1.0)
filters: Optional dictionary of additional filter criteria, merged
with this scope's observer/observed. Supports the same operators
as the list endpoint — e.g. ``{"level": "deductive"}`` to search
only conclusions derived during dreaming. See
https://honcho.dev/docs/v3/documentation/features/advanced/using-filters

Returns:
List of matching Conclusion objects
"""
self._honcho._ensure_workspace()
filters: dict[str, Any] = {
merged_filters: dict[str, Any] = {
"observer_id": self.observer,
"observed_id": self.observed,
}
if filters:
merged_filters.update(filters)

body: dict[str, Any] = {
"query": query,
"top_k": top_k,
"filters": filters,
"filters": merged_filters,
}
if distance is not None:
body["distance"] = distance
Expand Down
Loading
Loading