First Version
This commit is contained in:
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
/// 1–65535 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 {
|
||||
/// 1–100 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.0–1.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>,
|
||||
/// 1–100
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub search_top_k: Option<u32>,
|
||||
/// 0.0–1.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>,
|
||||
/// 1–100
|
||||
#[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>,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user