Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
67 changes: 67 additions & 0 deletions sdks/python/src/honcho/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,73 @@ async def delete_workspace(self, workspace_id: str) -> None:
"""Delete a workspace asynchronously."""
await self._honcho._async_http_client.delete(routes.workspace(workspace_id))

@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
async def chat(
self,
query: str = Field(..., min_length=1, description="The natural language query"),
*,
session: str | SessionBase | None = None,
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[BaseModel] | dict[str, Any] | None = None,
) -> BaseModel | str | None:
"""Query the entire workspace asynchronously (see Honcho.chat)."""
await self._honcho._ensure_workspace_async()
resolved_session_id = resolve_id(session)
body: dict[str, Any] = {"query": query, "stream": False}
if resolved_session_id:
body["session_id"] = resolved_session_id
if reasoning_level:
body["reasoning_level"] = reasoning_level
response_format_schema = serialize_response_format(response_format)
if response_format_schema is not None:
body["response_format"] = response_format_schema

data = await self._honcho._async_http_client.post(
routes.workspace_chat(self._honcho.workspace_id),
body=body,
)
content = data.get("content")
if not content:
return None
if isinstance(response_format, type):
return response_format.model_validate_json(content)
return content

@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
async def chat_stream(
self,
query: str = Field(..., min_length=1, description="The natural language query"),
*,
session: str | SessionBase | None = None,
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[BaseModel] | dict[str, Any] | None = None,
) -> AsyncDialecticStreamResponse:
"""Streaming variant of :meth:`chat` (async)."""
await self._honcho._ensure_workspace_async()
resolved_session_id = resolve_id(session)
body: dict[str, Any] = {"query": query, "stream": True}
if resolved_session_id:
body["session_id"] = resolved_session_id
if reasoning_level:
body["reasoning_level"] = reasoning_level
response_format_schema = serialize_response_format(response_format)
if response_format_schema is not None:
body["response_format"] = response_format_schema

async def stream_response() -> AsyncGenerator[str, None]:
async for chunk in parse_sse_astream(
self._honcho._async_http_client.stream(
"POST",
routes.workspace_chat(self._honcho.workspace_id),
body=body,
)
):
yield chunk

return AsyncDialecticStreamResponse(stream_response())

@validate_call
async def search(
self,
Expand Down
91 changes: 88 additions & 3 deletions sdks/python/src/honcho/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging
import os
from collections.abc import Mapping
from collections.abc import Generator, Mapping
from typing import Any, Literal

import httpx
Expand All @@ -27,9 +27,10 @@
from .message import Message
from .mixins import MetadataConfigMixin
from .pagination import SyncPage
from .peer import Peer
from .peer import Peer, serialize_response_format
from .types import DialecticStreamResponse
from .session import Session
from .utils import normalize_peers_to_dict, resolve_id
from .utils import normalize_peers_to_dict, parse_sse_stream, resolve_id

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -582,6 +583,90 @@ def delete_workspace(
"""
self._http.delete(routes.workspace(workspace_id))

@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def chat(
self,
query: str = Field(..., min_length=1, description="The natural language query"),
*,
session: str | SessionBase | None = None,
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[BaseModel] | dict[str, Any] | None = None,
) -> BaseModel | str | None:
"""
Query the entire workspace with a natural language question.

Unlike peer.chat(), which queries a single peer's representation, this
searches across ALL peers and observations in the workspace — use it
for cross-peer analysis, common themes, or workspace-wide questions.

Args:
query: The natural language question to ask.
session: Optional session to scope message retrieval to.
reasoning_level: Optional reasoning level: "minimal", "low",
"medium", "high", or "max" (default "low").
response_format: Optional structure for the answer: a Pydantic
model class (returns a parsed instance) or a raw
JSON Schema dict (returns a JSON string).

Returns:
The synthesized answer, or None if no relevant information.
"""
self._ensure_workspace()
resolved_session_id = resolve_id(session)
body: dict[str, Any] = {"query": query, "stream": False}
if resolved_session_id:
body["session_id"] = resolved_session_id
if reasoning_level:
body["reasoning_level"] = reasoning_level
response_format_schema = serialize_response_format(response_format)
if response_format_schema is not None:
body["response_format"] = response_format_schema

data = self._http.post(
routes.workspace_chat(self.workspace_id),
body=body,
)
content = data.get("content")
if not content:
return None
if isinstance(response_format, type):
return response_format.model_validate_json(content)
return content

@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def chat_stream(
self,
query: str = Field(..., min_length=1, description="The natural language query"),
*,
session: str | SessionBase | None = None,
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[BaseModel] | dict[str, Any] | None = None,
) -> DialecticStreamResponse:
"""Streaming variant of :meth:`chat`. See chat() for argument docs."""
self._ensure_workspace()
resolved_session_id = resolve_id(session)
body: dict[str, Any] = {"query": query, "stream": True}
if resolved_session_id:
body["session_id"] = resolved_session_id
if reasoning_level:
body["reasoning_level"] = reasoning_level
response_format_schema = serialize_response_format(response_format)
if response_format_schema is not None:
body["response_format"] = response_format_schema

def stream_response() -> Generator[str, None, None]:
yield from parse_sse_stream(
self._http.stream(
"POST",
routes.workspace_chat(self.workspace_id),
body=body,
)
)

return DialecticStreamResponse(stream_response())

@validate_call
def search(
self,
Expand Down
4 changes: 4 additions & 0 deletions sdks/python/src/honcho/http/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def workspace(workspace_id: str) -> str:
return f"/{API_VERSION}/workspaces/{workspace_id}"


def workspace_chat(workspace_id: str) -> str:
return f"/{API_VERSION}/workspaces/{workspace_id}/chat"


def workspace_search(workspace_id: str) -> str:
return f"/{API_VERSION}/workspaces/{workspace_id}/search"

Expand Down
129 changes: 129 additions & 0 deletions sdks/typescript/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { API_VERSION } from './api-version'
import { HonchoHTTPClient } from './http/client'
import {
createDialecticStream,
type DialecticStreamResponse,
} from './http/streaming'
import { Message } from './message'
import { Page } from './pagination'
import { Peer } from './peer'
Expand All @@ -12,6 +16,7 @@ import type {
QueueStatusParams,
QueueStatusResponse,
SessionResponse,
WorkspaceChatResponse,
WorkspaceResponse,
} from './types/api'
import { resolveId, transformQueueStatus } from './utils'
Expand Down Expand Up @@ -49,6 +54,7 @@ import {
} from './validation'

const DEFAULT_BASE_URL = 'https://api.honcho.dev'
type ReasoningLevel = 'minimal' | 'low' | 'medium' | 'high' | 'max'

/**
* Main client for the Honcho TypeScript SDK.
Expand Down Expand Up @@ -361,6 +367,43 @@ export class Honcho {
)
}

private async _workspaceChat(
workspaceId: string,
params: {
query: string
stream?: boolean
session_id?: string
reasoning_level?: ReasoningLevel
}
): Promise<WorkspaceChatResponse> {
await this._ensureWorkspace()
return this._http.post<WorkspaceChatResponse>(
`/${API_VERSION}/workspaces/${workspaceId}/chat`,
{ body: params }
)
}

private async _workspaceChatStream(
workspaceId: string,
params: {
query: string
session_id?: string
reasoning_level?: ReasoningLevel
}
): Promise<Response> {
await this._ensureWorkspace()
return this._http.stream(
'POST',
`/${API_VERSION}/workspaces/${workspaceId}/chat`,
{
body: {
...params,
stream: true,
},
}
)
}

// ===========================================================================
// Public Methods
// ===========================================================================
Expand Down Expand Up @@ -803,6 +846,92 @@ export class Honcho {
return response.map(Message.fromApiResponse)
}

/**
* Query the workspace's collective knowledge using natural language.
*
* Performs agentic search and reasoning across ALL peers and observations
* in the workspace to synthesize a comprehensive answer. Useful for
* cross-peer analysis, discovering common themes, and workspace-wide queries.
*
* @param query - The natural language question to ask
* @param options.session - Optional session to scope message search to. Can be a session
* ID string or a Session object.
* @param options.reasoningLevel - Optional reasoning level for the query: "minimal", "low",
* "medium", "high", or "max". Defaults to "low" if not provided.
* @returns Promise resolving to the response string, or null if no relevant information
*
* @example
* ```typescript
* const response = await honcho.chat('What are common themes across all users?')
* ```
*/
async chat(
query: string,
options?: {
session?: string | Session
reasoningLevel?: ReasoningLevel
}
): Promise<string | null> {
const validatedQuery = SearchQuerySchema.parse(query)
const resolvedSessionId = options?.session
? resolveId(options.session)
: undefined

const response = await this._workspaceChat(this.workspaceId, {
query: validatedQuery,
stream: false,
session_id: resolvedSessionId,
reasoning_level: options?.reasoningLevel,
})
if (!response.content) {
return null
}
return response.content
}

/**
* Query the workspace's collective knowledge with streaming response.
*
* Performs agentic search and reasoning across ALL peers and observations
* in the workspace to synthesize a comprehensive answer, streaming the
* response as it is generated.
*
* @param query - The natural language question to ask
* @param options.session - Optional session to scope message search to. Can be a session
* ID string or a Session object.
* @param options.reasoningLevel - Optional reasoning level for the query: "minimal", "low",
* "medium", "high", or "max". Defaults to "low" if not provided.
* @returns Promise resolving to a DialecticStreamResponse that can be iterated over
*
* @example
* ```typescript
* const stream = await honcho.chatStream('What do all peers have in common?')
* for await (const chunk of stream) {
* process.stdout.write(chunk)
* }
* ```
*/
async chatStream(
query: string,
options?: {
session?: string | Session
reasoningLevel?: ReasoningLevel
}
): Promise<DialecticStreamResponse> {
const validatedQuery = SearchQuerySchema.parse(query)
const resolvedSessionId = options?.session
? resolveId(options.session)
: undefined

const response = await this._workspaceChatStream(this.workspaceId, {
query: validatedQuery,
session_id: resolvedSessionId,
reasoning_level: options?.reasoningLevel,
})

return createDialecticStream(response)
}

/**
* Get the queue processing status, optionally scoped to an observer, sender, and/or session.
*
Expand Down
1 change: 1 addition & 0 deletions sdks/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type {
SessionResponse,
SessionSummariesResponse,
SummaryResponse,
WorkspaceChatResponse,
WorkspaceResponse,
} from './types/api'

Expand Down
11 changes: 11 additions & 0 deletions sdks/typescript/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ export interface PeerChatResponse {
content: string | null
}

export interface WorkspaceChatParams {
query: string
stream?: boolean
session_id?: string
reasoning_level?: 'minimal' | 'low' | 'medium' | 'high' | 'max'
}

export interface WorkspaceChatResponse {
content: string | null
}

export interface PeerRepresentationParams {
session_id?: string
target?: string
Expand Down
Loading
Loading