First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "honcho-client"
version = "0.1.0"
edition = "2024"
[dependencies]
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "charset", "http2", "system-proxy", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
+173
View File
@@ -0,0 +1,173 @@
use reqwest::{Client, RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::Serialize;
use tracing::debug;
use crate::error::{HonchoError, Result};
const DEFAULT_BASE_URL: &str = "https://api.honcho.dev";
/// Honcho REST API client.
///
/// Cheaply clone-able — the inner `reqwest::Client` holds an `Arc`.
#[derive(Debug, Clone)]
pub struct HonchoClient {
pub(crate) http: Client,
pub(crate) base_url: String,
pub(crate) token: String,
}
impl HonchoClient {
/// Create a client pointing at the production SaaS endpoint.
pub fn new(token: impl Into<String>) -> Self {
Self::with_base_url(DEFAULT_BASE_URL, token)
}
/// Create a client pointing at a custom base URL (e.g. `http://localhost:8000`).
pub fn with_base_url(base_url: impl Into<String>, token: impl Into<String>) -> Self {
Self {
http: Client::new(),
base_url: base_url.into().trim_end_matches('/').to_owned(),
token: token.into(),
}
}
// ── internal helpers ──────────────────────────────────────────────────
pub(crate) fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
/// Build a URL with optional query params (key-value pairs).
pub(crate) fn url_with_query(&self, path: &str, params: &[(&str, String)]) -> String {
if params.is_empty() {
return self.url(path);
}
let qs: String = params
.iter()
.map(|(k, v)| format!("{}={}", urlencoding(k), urlencoding(v)))
.collect::<Vec<_>>()
.join("&");
format!("{}{}?{}", self.base_url, path, qs)
}
fn auth(&self, rb: RequestBuilder) -> RequestBuilder {
rb.bearer_auth(&self.token)
}
pub(crate) async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = self.url(path);
debug!("GET {url}");
let resp = self.auth(self.http.get(&url)).send().await?;
parse_response(resp).await
}
pub(crate) async fn get_with_query<T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
) -> Result<T> {
let url = self.url_with_query(path, query);
debug!("GET {url}");
let resp = self.auth(self.http.get(&url)).send().await?;
parse_response(resp).await
}
pub(crate) async fn post<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = self.url(path);
debug!("POST {url}");
let resp = self.auth(self.http.post(&url)).json(body).send().await?;
parse_response(resp).await
}
pub(crate) async fn post_with_query<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
body: &B,
) -> Result<T> {
let url = self.url_with_query(path, query);
debug!("POST {url}");
let resp = self.auth(self.http.post(&url)).json(body).send().await?;
parse_response(resp).await
}
pub(crate) async fn put<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = self.url(path);
debug!("PUT {url}");
let resp = self.auth(self.http.put(&url)).json(body).send().await?;
parse_response(resp).await
}
pub(crate) async fn put_with_query<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
body: &B,
) -> Result<T> {
let url = self.url_with_query(path, query);
debug!("PUT {url}");
let resp = self.auth(self.http.put(&url)).json(body).send().await?;
parse_response(resp).await
}
pub(crate) async fn delete_ok(&self, path: &str) -> Result<()> {
let url = self.url(path);
debug!("DELETE {url}");
let resp = self.auth(self.http.delete(&url)).send().await?;
parse_no_content(resp).await
}
pub(crate) async fn delete_json<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
let url = self.url(path);
debug!("DELETE {url} (with body)");
let resp = self
.auth(self.http.delete(&url))
.json(body)
.send()
.await?;
parse_no_content(resp).await
}
}
async fn parse_response<T: DeserializeOwned>(resp: Response) -> Result<T> {
let status = resp.status();
if status.is_success() {
Ok(resp.json::<T>().await?)
} else {
let code = status.as_u16();
let body = resp.text().await.unwrap_or_default();
Err(HonchoError::Http { status: code, body })
}
}
pub(crate) async fn parse_no_content(resp: Response) -> Result<()> {
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Err(HonchoError::Http { status, body })
}
}
/// Minimal percent-encoding for query param keys and values.
fn urlencoding(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
| b'-' | b'_' | b'.' | b'~' => out.push(b as char),
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
+80
View File
@@ -0,0 +1,80 @@
use crate::{
client::{parse_no_content, HonchoClient},
error::Result,
models::*,
workspaces::page_query,
};
impl HonchoClient {
/// Create up to 100 conclusions in a single batch.
pub async fn add_conclusions(
&self,
workspace_id: &str,
batch: &ConclusionBatchCreate,
) -> Result<Vec<Conclusion>> {
self.post(
&format!("/v3/workspaces/{workspace_id}/conclusions"),
batch,
)
.await
}
/// Convenience: add a single conclusion.
pub async fn add_conclusion(
&self,
workspace_id: &str,
c: ConclusionCreate,
) -> Result<Vec<Conclusion>> {
self.add_conclusions(
workspace_id,
&ConclusionBatchCreate {
conclusions: vec![c],
},
)
.await
}
pub async fn list_conclusions(
&self,
workspace_id: &str,
params: &PageParams,
filter: &ConclusionGet,
) -> Result<Page<Conclusion>> {
self.post_with_query(
&format!("/v3/workspaces/{workspace_id}/conclusions/list"),
&page_query(params),
filter,
)
.await
}
/// Semantic search over conclusions.
pub async fn query_conclusions(
&self,
workspace_id: &str,
query: &ConclusionQuery,
) -> Result<Vec<Conclusion>> {
self.post(
&format!("/v3/workspaces/{workspace_id}/conclusions/query"),
query,
)
.await
}
pub async fn delete_conclusion(
&self,
workspace_id: &str,
conclusion_id: &str,
) -> Result<()> {
let url = self.url(&format!(
"/v3/workspaces/{workspace_id}/conclusions/{conclusion_id}"
));
let resp = self
.http
.delete(&url)
.bearer_auth(&self.token)
.send()
.await?;
parse_no_content(resp).await
}
}
+56
View File
@@ -0,0 +1,56 @@
use std::error::Error as StdError;
use std::fmt;
#[derive(Debug)]
pub enum HonchoError {
Http { status: u16, body: String },
Request(reqwest::Error),
Json(serde_json::Error),
}
impl fmt::Display for HonchoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Http { status, body } => write!(f, "HTTP error {status}: {body}"),
Self::Request(e) => {
// reqwest's own Display stops at "error sending request for url
// (...)" and hides the transport cause. Walk the source chain so
// the real reason (e.g. "Connection reset by peer", "operation
// timed out") shows up in logs instead of a useless top line.
write!(f, "Request failed: {e}")?;
let mut src = e.source();
while let Some(cause) = src {
write!(f, ": {cause}")?;
src = cause.source();
}
Ok(())
}
Self::Json(e) => write!(f, "JSON error: {e}"),
}
}
}
impl std::error::Error for HonchoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Request(e) => Some(e),
Self::Json(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for HonchoError {
fn from(e: reqwest::Error) -> Self {
Self::Request(e)
}
}
impl From<serde_json::Error> for HonchoError {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
pub type Result<T> = std::result::Result<T, HonchoError>;
+63
View File
@@ -0,0 +1,63 @@
//! Honcho v3 REST API client.
//!
//! # Quick start
//!
//! ```rust,no_run
//! use honcho_client::{HonchoClient, models::*};
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! // Point at your local Docker instance
//! let client = HonchoClient::with_base_url("http://localhost:8000", "your-api-key");
//!
//! // Create or retrieve a workspace
//! let ws = client.create_workspace(&WorkspaceCreate {
//! id: "my-agent".into(),
//! ..Default::default()
//! }).await?;
//!
//! // Create a peer (= one user / agent identity)
//! let peer = client.create_peer(&ws.id, &PeerCreate {
//! id: "daniele".into(),
//! metadata: None,
//! configuration: None,
//! }).await?;
//!
//! // Open a session
//! let session = client.create_session(&ws.id, &SessionCreate::default()).await?;
//!
//! // Add messages
//! client.add_message(&ws.id, &session.id, MessageCreate {
//! content: "Hello!".into(),
//! peer_id: peer.id.clone(),
//! metadata: None,
//! configuration: None,
//! created_at: None,
//! }).await?;
//!
//! // Query memory (Dialectic API)
//! let answer = client.peer_chat(&ws.id, &peer.id, &DialecticOptions {
//! query: "What did the user say?".into(),
//! session_id: Some(session.id.clone()),
//! target: None,
//! stream: Some(false),
//! reasoning_level: None,
//! }).await?;
//!
//! println!("{answer:#?}");
//! Ok(())
//! }
//! ```
pub mod client;
pub mod conclusions;
pub mod error;
pub mod messages;
pub mod models;
pub mod peers;
pub mod sessions;
pub mod workspaces;
// Flat re-exports for convenience
pub use client::HonchoClient;
pub use error::{HonchoError, Result};
+80
View File
@@ -0,0 +1,80 @@
use crate::{
client::HonchoClient,
error::Result,
models::*,
workspaces::page_query,
};
impl HonchoClient {
/// Add one or more messages to a session in a single request.
pub async fn add_messages(
&self,
workspace_id: &str,
session_id: &str,
batch: &MessageBatchCreate,
) -> Result<Vec<Message>> {
self.post(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/messages"),
batch,
)
.await
}
/// Convenience: add a single message.
pub async fn add_message(
&self,
workspace_id: &str,
session_id: &str,
msg: MessageCreate,
) -> Result<Vec<Message>> {
self.add_messages(
workspace_id,
session_id,
&MessageBatchCreate { messages: vec![msg] },
)
.await
}
pub async fn list_messages(
&self,
workspace_id: &str,
session_id: &str,
params: &PageParams,
filter: &MessageGet,
) -> Result<Page<Message>> {
self.post_with_query(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/messages/list"),
&page_query(params),
filter,
)
.await
}
pub async fn get_message(
&self,
workspace_id: &str,
session_id: &str,
message_id: &str,
) -> Result<Message> {
self.get(&format!(
"/v3/workspaces/{workspace_id}/sessions/{session_id}/messages/{message_id}"
))
.await
}
pub async fn update_message(
&self,
workspace_id: &str,
session_id: &str,
message_id: &str,
body: &MessageUpdate,
) -> Result<Message> {
self.put(
&format!(
"/v3/workspaces/{workspace_id}/sessions/{session_id}/messages/{message_id}"
),
body,
)
.await
}
}
+308
View File
@@ -0,0 +1,308 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
// ──────────────────────────────────────────
// Pagination
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Page<T> {
pub items: Vec<T>,
pub total: u64,
pub page: u64,
pub size: u64,
pub pages: u64,
}
// ──────────────────────────────────────────
// Workspace
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Workspace {
pub id: String,
pub metadata: Option<Value>,
pub configuration: Option<Value>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct WorkspaceCreate {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct WorkspaceUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
}
/// Filter body for `POST /workspaces/list`
#[derive(Debug, Clone, Serialize, Default)]
pub struct WorkspaceGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
// ──────────────────────────────────────────
// Peer
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Peer {
pub id: String,
pub workspace_id: String,
pub created_at: String,
pub metadata: Option<Value>,
pub configuration: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PeerCreate {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct PeerUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct PeerGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
// ──────────────────────────────────────────
// Session
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Session {
pub id: String,
pub is_active: bool,
pub workspace_id: String,
pub metadata: Option<Value>,
pub configuration: Option<Value>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SessionCreate {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
/// Map of peer_id → SessionPeerConfig
#[serde(skip_serializing_if = "Option::is_none")]
pub peers: Option<std::collections::HashMap<String, SessionPeerConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SessionUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SessionGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionPeerConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub observe_me: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub observe_others: Option<bool>,
}
// ──────────────────────────────────────────
// Message
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Message {
pub id: String,
pub content: String,
pub peer_id: String,
pub session_id: String,
pub workspace_id: String,
pub metadata: Option<Value>,
pub created_at: String,
pub token_count: i64,
}
#[derive(Debug, Clone, Serialize)]
pub struct MessageCreate {
pub content: String,
pub peer_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
/// RFC3339 datetime; if None the server assigns now
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MessageBatchCreate {
pub messages: Vec<MessageCreate>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct MessageGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct MessageUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}
// ──────────────────────────────────────────
// Conclusion
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Conclusion {
pub id: String,
pub content: String,
pub observer_id: String,
pub observed_id: String,
pub session_id: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConclusionCreate {
/// 165535 characters
pub content: String,
pub observer_id: String,
pub observed_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConclusionBatchCreate {
/// 1100 conclusions
pub conclusions: Vec<ConclusionCreate>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct ConclusionGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConclusionQuery {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
/// 0.01.0
#[serde(skip_serializing_if = "Option::is_none")]
pub distance: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
// ──────────────────────────────────────────
// Dialectic / Peer context
// ──────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct DialecticOptions {
/// Natural language query
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
/// peer_id to get the representation for (defaults to the caller)
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
/// minimal | low | medium | high | max
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_level: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct PeerRepresentationGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_query: Option<String>,
/// 1100
#[serde(skip_serializing_if = "Option::is_none")]
pub search_top_k: Option<u32>,
/// 0.01.0
#[serde(skip_serializing_if = "Option::is_none")]
pub search_max_distance: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_most_frequent: Option<bool>,
/// 1100
#[serde(skip_serializing_if = "Option::is_none")]
pub max_conclusions: Option<u32>,
}
// ──────────────────────────────────────────
// Search
// ──────────────────────────────────────────
#[derive(Debug, Clone, Serialize)]
pub struct MessageSearchOptions {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
// ──────────────────────────────────────────
// Queue
// ──────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct QueueStatus {
pub total_work_units: u64,
pub completed_work_units: u64,
pub in_progress_work_units: u64,
pub pending_work_units: u64,
pub sessions: Option<Value>,
}
// ──────────────────────────────────────────
// List query params (pagination)
// ──────────────────────────────────────────
#[derive(Debug, Clone, Default)]
pub struct PageParams {
pub page: Option<u64>,
pub size: Option<u64>,
pub reverse: Option<bool>,
}
+180
View File
@@ -0,0 +1,180 @@
use crate::{
client::HonchoClient,
error::Result,
models::*,
workspaces::page_query,
};
impl HonchoClient {
// ── CRUD ──────────────────────────────────────────────────────────────
pub async fn create_peer(&self, workspace_id: &str, body: &PeerCreate) -> Result<Peer> {
self.post(&format!("/v3/workspaces/{workspace_id}/peers"), body)
.await
}
pub async fn list_peers(
&self,
workspace_id: &str,
params: &PageParams,
filter: &PeerGet,
) -> Result<Page<Peer>> {
self.post_with_query(
&format!("/v3/workspaces/{workspace_id}/peers/list"),
&page_query(params),
filter,
)
.await
}
pub async fn update_peer(
&self,
workspace_id: &str,
peer_id: &str,
body: &PeerUpdate,
) -> Result<Peer> {
self.put(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}"),
body,
)
.await
}
// ── Sessions for a peer ───────────────────────────────────────────────
pub async fn list_peer_sessions(
&self,
workspace_id: &str,
peer_id: &str,
params: &PageParams,
filter: &SessionGet,
) -> Result<Page<Session>> {
self.post_with_query(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/sessions"),
&page_query(params),
filter,
)
.await
}
// ── Dialectic (memory query via LLM) ──────────────────────────────────
/// Query a peer's memory using natural language. Returns the LLM answer.
/// Note: `stream: true` is not supported by this client (use raw reqwest if needed).
pub async fn peer_chat(
&self,
workspace_id: &str,
peer_id: &str,
opts: &DialecticOptions,
) -> Result<serde_json::Value> {
self.post(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/chat"),
opts,
)
.await
}
// ── Representation ────────────────────────────────────────────────────
/// Get a structured representation of a peer's memory.
pub async fn peer_representation(
&self,
workspace_id: &str,
peer_id: &str,
opts: &PeerRepresentationGet,
) -> Result<serde_json::Value> {
self.post(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/representation"),
opts,
)
.await
}
// ── Context ───────────────────────────────────────────────────────────
/// Get context for a peer (conclusions + card, ready for injection into prompts).
pub async fn peer_context(
&self,
workspace_id: &str,
peer_id: &str,
opts: &PeerRepresentationGet,
) -> Result<serde_json::Value> {
let mut q: Vec<(&str, String)> = vec![];
if let Some(ref v) = opts.target {
q.push(("target", v.clone()));
}
if let Some(ref v) = opts.search_query {
q.push(("search_query", v.clone()));
}
if let Some(v) = opts.search_top_k {
q.push(("search_top_k", v.to_string()));
}
if let Some(v) = opts.search_max_distance {
q.push(("search_max_distance", v.to_string()));
}
if let Some(v) = opts.include_most_frequent {
q.push(("include_most_frequent", v.to_string()));
}
if let Some(v) = opts.max_conclusions {
q.push(("max_conclusions", v.to_string()));
}
self.get_with_query(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/context"),
&q,
)
.await
}
// ── Card ──────────────────────────────────────────────────────────────
pub async fn get_peer_card(
&self,
workspace_id: &str,
peer_id: &str,
target: Option<&str>,
) -> Result<serde_json::Value> {
let mut q: Vec<(&str, String)> = vec![];
if let Some(t) = target {
q.push(("target", t.to_owned()));
}
self.get_with_query(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/card"),
&q,
)
.await
}
pub async fn set_peer_card(
&self,
workspace_id: &str,
peer_id: &str,
target: Option<&str>,
card: serde_json::Value,
) -> Result<serde_json::Value> {
let mut q: Vec<(&str, String)> = vec![];
if let Some(t) = target {
q.push(("target", t.to_owned()));
}
self.put_with_query(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/card"),
&q,
&card,
)
.await
}
// ── Search ────────────────────────────────────────────────────────────
pub async fn search_peer(
&self,
workspace_id: &str,
peer_id: &str,
opts: &MessageSearchOptions,
) -> Result<Vec<Message>> {
self.post(
&format!("/v3/workspaces/{workspace_id}/peers/{peer_id}/search"),
opts,
)
.await
}
}
+211
View File
@@ -0,0 +1,211 @@
use std::collections::HashMap;
use crate::{
client::HonchoClient,
error::Result,
models::*,
workspaces::page_query,
};
impl HonchoClient {
// ── CRUD ──────────────────────────────────────────────────────────────
pub async fn create_session(
&self,
workspace_id: &str,
body: &SessionCreate,
) -> Result<Session> {
self.post(&format!("/v3/workspaces/{workspace_id}/sessions"), body)
.await
}
pub async fn list_sessions(
&self,
workspace_id: &str,
params: &PageParams,
filter: &SessionGet,
) -> Result<Page<Session>> {
self.post_with_query(
&format!("/v3/workspaces/{workspace_id}/sessions/list"),
&page_query(params),
filter,
)
.await
}
pub async fn update_session(
&self,
workspace_id: &str,
session_id: &str,
body: &SessionUpdate,
) -> Result<Session> {
self.put(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}"),
body,
)
.await
}
pub async fn delete_session(&self, workspace_id: &str, session_id: &str) -> Result<()> {
self.delete_ok(&format!(
"/v3/workspaces/{workspace_id}/sessions/{session_id}"
))
.await
}
/// Clone a session, optionally up to a specific message.
pub async fn clone_session(
&self,
workspace_id: &str,
session_id: &str,
up_to_message_id: Option<&str>,
) -> Result<Session> {
let q: Vec<(&str, String)> = up_to_message_id
.map(|mid| vec![("message_id", mid.to_owned())])
.unwrap_or_default();
self.post_with_query(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/clone"),
&q,
&serde_json::Value::Null, // no body
)
.await
}
// ── Peers in session ──────────────────────────────────────────────────
/// Add peers to a session.
pub async fn add_session_peers(
&self,
workspace_id: &str,
session_id: &str,
peers: &HashMap<String, SessionPeerConfig>,
) -> Result<serde_json::Value> {
self.post(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/peers"),
peers,
)
.await
}
/// Update peer configs in a session.
pub async fn update_session_peers(
&self,
workspace_id: &str,
session_id: &str,
peers: &HashMap<String, SessionPeerConfig>,
) -> Result<serde_json::Value> {
self.put(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/peers"),
peers,
)
.await
}
/// Remove peers from a session by their ids.
pub async fn remove_session_peers(
&self,
workspace_id: &str,
session_id: &str,
peer_ids: &[&str],
) -> Result<()> {
self.delete_json(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/peers"),
&peer_ids,
)
.await
}
/// List peers in a session.
pub async fn list_session_peers(
&self,
workspace_id: &str,
session_id: &str,
params: &PageParams,
) -> Result<Page<Peer>> {
self.get_with_query(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/peers"),
&page_query(params),
)
.await
}
/// Get config for a specific peer in a session.
pub async fn get_session_peer_config(
&self,
workspace_id: &str,
session_id: &str,
peer_id: &str,
) -> Result<SessionPeerConfig> {
self.get(&format!(
"/v3/workspaces/{workspace_id}/sessions/{session_id}/peers/{peer_id}/config"
))
.await
}
/// Update config for a specific peer in a session.
pub async fn update_session_peer_config(
&self,
workspace_id: &str,
session_id: &str,
peer_id: &str,
config: &SessionPeerConfig,
) -> Result<SessionPeerConfig> {
self.put(
&format!(
"/v3/workspaces/{workspace_id}/sessions/{session_id}/peers/{peer_id}/config"
),
config,
)
.await
}
// ── Context / Summaries ───────────────────────────────────────────────
/// Retrieve context for a session (messages + peer conclusions, token-budgeted).
pub async fn session_context(
&self,
workspace_id: &str,
session_id: &str,
tokens: Option<u32>,
search_query: Option<&str>,
) -> Result<serde_json::Value> {
let mut q: Vec<(&str, String)> = vec![];
if let Some(t) = tokens {
q.push(("tokens", t.to_string()));
}
if let Some(sq) = search_query {
q.push(("search_query", sq.to_owned()));
}
self.get_with_query(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/context"),
&q,
)
.await
}
pub async fn session_summaries(
&self,
workspace_id: &str,
session_id: &str,
) -> Result<serde_json::Value> {
self.get(&format!(
"/v3/workspaces/{workspace_id}/sessions/{session_id}/summaries"
))
.await
}
// ── Search ────────────────────────────────────────────────────────────
pub async fn search_session(
&self,
workspace_id: &str,
session_id: &str,
opts: &MessageSearchOptions,
) -> Result<Vec<Message>> {
self.post(
&format!("/v3/workspaces/{workspace_id}/sessions/{session_id}/search"),
opts,
)
.await
}
}
+97
View File
@@ -0,0 +1,97 @@
use crate::{
client::{parse_no_content, HonchoClient},
error::Result,
models::*,
};
impl HonchoClient {
// ── CRUD ──────────────────────────────────────────────────────────────
/// Create (or retrieve if already exists) a workspace.
pub async fn create_workspace(&self, body: &WorkspaceCreate) -> Result<Workspace> {
self.post("/v3/workspaces", body).await
}
/// List workspaces (admin only).
pub async fn list_workspaces(
&self,
params: &PageParams,
filter: &WorkspaceGet,
) -> Result<Page<Workspace>> {
self.post_with_query("/v3/workspaces/list", &page_query(params), filter)
.await
}
/// Update a workspace.
pub async fn update_workspace(
&self,
workspace_id: &str,
body: &WorkspaceUpdate,
) -> Result<Workspace> {
self.put(&format!("/v3/workspaces/{workspace_id}"), body)
.await
}
/// Delete a workspace (queued, async on server side).
pub async fn delete_workspace(&self, workspace_id: &str) -> Result<()> {
let url = self.url(&format!("/v3/workspaces/{workspace_id}"));
let resp = self
.http
.delete(&url)
.bearer_auth(&self.token)
.send()
.await?;
parse_no_content(resp).await
}
// ── Search ────────────────────────────────────────────────────────────
pub async fn search_workspace(
&self,
workspace_id: &str,
opts: &MessageSearchOptions,
) -> Result<Vec<Message>> {
self.post(&format!("/v3/workspaces/{workspace_id}/search"), opts)
.await
}
// ── Queue ─────────────────────────────────────────────────────────────
pub async fn queue_status(
&self,
workspace_id: &str,
observer_id: Option<&str>,
sender_id: Option<&str>,
session_id: Option<&str>,
) -> Result<QueueStatus> {
let mut q: Vec<(&str, String)> = vec![];
if let Some(v) = observer_id {
q.push(("observer_id", v.to_owned()));
}
if let Some(v) = sender_id {
q.push(("sender_id", v.to_owned()));
}
if let Some(v) = session_id {
q.push(("session_id", v.to_owned()));
}
self.get_with_query(&format!("/v3/workspaces/{workspace_id}/queue/status"), &q)
.await
}
}
// ── helpers ───────────────────────────────────────────────────────────────
pub(crate) fn page_query(p: &PageParams) -> Vec<(&'static str, String)> {
let mut q = vec![];
if let Some(v) = p.page {
q.push(("page", v.to_string()));
}
if let Some(v) = p.size {
q.push(("size", v.to_string()));
}
if let Some(v) = p.reverse {
q.push(("reverse", v.to_string()));
}
q
}