use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub use super::common::{
DreamConfiguration, PeerCardConfiguration, ReasoningConfiguration, SummaryConfiguration,
};
pub use super::dream::SessionQueueStatus;
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionResponse {
pub id: String,
pub is_active: bool,
pub workspace_id: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default)]
pub configuration: SessionConfiguration,
pub created_at: DateTime<Utc>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
pub struct SessionCreate {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub peers: Option<HashMap<String, SessionPeerConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<SessionConfiguration>,
}
impl SessionCreate {
pub fn validate(&self) -> crate::error::Result<()> {
if self.id.is_empty() {
return Err(crate::error::HonchoError::Validation(
"session id must not be empty".into(),
));
}
if !self
.id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(crate::error::HonchoError::Validation(
"session id must contain only [a-zA-Z0-9_-]".into(),
));
}
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
pub struct SessionUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration: Option<SessionConfiguration>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
pub struct SessionGet {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<HashMap<String, serde_json::Value>>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SessionMetadataSet {
pub metadata: HashMap<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SessionConfigurationSet {
pub configuration: HashMap<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SessionConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfiguration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub peer_card: Option<PeerCardConfiguration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<SummaryConfiguration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dream: Option<DreamConfiguration>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
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>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
pub struct SessionContextOptions {
#[serde(default = "default_true")]
#[builder(default = true)]
pub summary: bool,
#[serde(default)]
#[builder(default)]
pub limit_to_session: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_target: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_perspective: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub search_query: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub search_top_k: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub search_max_distance: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_most_frequent: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_conclusions: Option<u32>,
}
pub(crate) const SEARCH_TOP_K_MIN: u32 = 1;
pub(crate) const SEARCH_TOP_K_MAX: u32 = 100;
pub(crate) const SEARCH_MAX_DISTANCE_MIN: f64 = 0.0;
pub(crate) const SEARCH_MAX_DISTANCE_MAX: f64 = 1.0;
pub(crate) const MAX_CONCLUSIONS_MIN: u32 = 1;
pub(crate) const MAX_CONCLUSIONS_MAX: u32 = 100;
pub(crate) fn validate_search_params(
search_top_k: Option<u32>,
search_max_distance: Option<f64>,
max_conclusions: Option<u32>,
) -> crate::error::Result<()> {
if let Some(k) = search_top_k
&& !(SEARCH_TOP_K_MIN..=SEARCH_TOP_K_MAX).contains(&k)
{
return Err(crate::error::HonchoError::Validation(format!(
"search_top_k must be between {SEARCH_TOP_K_MIN} and {SEARCH_TOP_K_MAX}, got {k}"
)));
}
if let Some(d) = search_max_distance
&& !(SEARCH_MAX_DISTANCE_MIN..=SEARCH_MAX_DISTANCE_MAX).contains(&d)
{
return Err(crate::error::HonchoError::Validation(format!(
"search_max_distance must be between {SEARCH_MAX_DISTANCE_MIN} and {SEARCH_MAX_DISTANCE_MAX}, got {d}"
)));
}
if let Some(c) = max_conclusions
&& !(MAX_CONCLUSIONS_MIN..=MAX_CONCLUSIONS_MAX).contains(&c)
{
return Err(crate::error::HonchoError::Validation(format!(
"max_conclusions must be between {MAX_CONCLUSIONS_MIN} and {MAX_CONCLUSIONS_MAX}, got {c}"
)));
}
Ok(())
}
impl SessionContextOptions {
pub fn validate(&self) -> crate::error::Result<()> {
if self.peer_perspective.is_some() && self.peer_target.is_none() {
return Err(crate::error::HonchoError::Validation(
"peer_perspective requires peer_target to be set".into(),
));
}
if self.search_query.is_some() && self.peer_target.is_none() {
return Err(crate::error::HonchoError::Validation(
"search_query requires peer_target to be set".into(),
));
}
validate_search_params(
self.search_top_k,
self.search_max_distance,
self.max_conclusions,
)?;
if let Some(t) = self.tokens
&& t == 0
{
return Err(crate::error::HonchoError::Validation(
"tokens must be greater than 0".into(),
));
}
Ok(())
}
pub(crate) fn to_query_params(&self) -> Vec<(&'static str, std::borrow::Cow<'_, str>)> {
use std::borrow::Cow;
let mut params: Vec<(&'static str, Cow<'_, str>)> = vec![
(
"summary",
Cow::Borrowed(if self.summary { "true" } else { "false" }),
),
(
"limit_to_session",
Cow::Borrowed(if self.limit_to_session {
"true"
} else {
"false"
}),
),
];
if let Some(v) = self.tokens {
params.push(("tokens", Cow::Owned(v.to_string())));
}
if let Some(ref v) = self.peer_target {
params.push(("peer_target", Cow::Borrowed(v.as_str())));
}
if let Some(ref v) = self.peer_perspective {
params.push(("peer_perspective", Cow::Borrowed(v.as_str())));
}
if let Some(ref v) = self.search_query {
params.push(("search_query", Cow::Borrowed(v.as_str())));
}
if let Some(v) = self.search_top_k {
params.push(("search_top_k", Cow::Owned(v.to_string())));
}
if let Some(v) = self.search_max_distance {
params.push(("search_max_distance", Cow::Owned(v.to_string())));
}
if let Some(v) = self.include_most_frequent {
params.push((
"include_most_frequent",
Cow::Borrowed(if v { "true" } else { "false" }),
));
}
if let Some(v) = self.max_conclusions {
params.push(("max_conclusions", Cow::Owned(v.to_string())));
}
params
}
}
fn default_true() -> bool {
true
}
fn escape_tag_value(value: &str) -> std::borrow::Cow<'_, str> {
if value.contains(['&', '<', '>']) {
std::borrow::Cow::Owned(
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">"),
)
} else {
std::borrow::Cow::Borrowed(value)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionContext {
pub id: String,
pub messages: Vec<super::message::MessageResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<Summary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub peer_representation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub peer_card: Option<Vec<String>>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionSummaries {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_summary: Option<Summary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub long_summary: Option<Summary>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SummaryType {
Short,
Long,
#[serde(other)]
Unknown,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Summary {
pub content: String,
pub message_id: String,
pub summary_type: SummaryType,
pub created_at: DateTime<Utc>,
pub token_count: u32,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
pub struct SessionListOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<HashMap<String, serde_json::Value>>,
#[serde(default = "default_page")]
#[builder(default = default_page())]
pub page: u64,
#[serde(default = "default_size")]
#[builder(default = default_size())]
pub size: u64,
#[serde(default)]
#[builder(default)]
pub reverse: bool,
}
fn default_page() -> u64 {
1
}
fn default_size() -> u64 {
50
}
pub type SessionPage = super::pagination::Page<SessionResponse>;
pub trait IntoAssistantRef {
fn as_assistant_name(&self) -> &str;
}
impl IntoAssistantRef for &str {
fn as_assistant_name(&self) -> &str {
self
}
}
impl IntoAssistantRef for String {
fn as_assistant_name(&self) -> &str {
self.as_str()
}
}
impl IntoAssistantRef for &crate::Peer {
fn as_assistant_name(&self) -> &str {
self.id()
}
}
impl SessionContext {
fn format_peer_card(card: &[String]) -> String {
let items: Vec<String> = card
.iter()
.map(|s| format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")))
.collect();
format!("[{}]", items.join(", "))
}
fn build_context_messages(&self) -> Vec<(&'static str, std::borrow::Cow<'_, str>)> {
let mut msgs = Vec::new();
if let Some(ref rep) = self.peer_representation {
msgs.push((
"peer_representation",
std::borrow::Cow::Borrowed(rep.as_str()),
));
}
if let Some(ref card) = self.peer_card {
msgs.push((
"peer_card",
std::borrow::Cow::Owned(Self::format_peer_card(card)),
));
}
if let Some(ref summary) = self.summary {
msgs.push((
"summary",
std::borrow::Cow::Borrowed(summary.content.as_str()),
));
}
msgs
}
fn build_messages(
&self,
context_role: &'static str,
assistant: &str,
render_message: impl Fn(&super::message::MessageResponse, bool) -> serde_json::Value,
) -> Vec<serde_json::Value> {
let mut result: Vec<serde_json::Value> = Vec::with_capacity(self.len());
for (tag, value) in self.build_context_messages() {
let value = escape_tag_value(&value);
result.push(serde_json::json!({
"role": context_role,
"content": format!("<{tag}>{value}</{tag}>"),
}));
}
for message in &self.messages {
let is_assistant = message.peer_id == assistant;
result.push(render_message(message, is_assistant));
}
result
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn to_openai(&self, assistant: impl IntoAssistantRef) -> Vec<serde_json::Value> {
let assistant = assistant.as_assistant_name();
self.build_messages("system", assistant, |message, is_assistant| {
serde_json::json!({
"role": if is_assistant { "assistant" } else { "user" },
"name": message.peer_id,
"content": message.content,
})
})
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn to_anthropic(&self, assistant: impl IntoAssistantRef) -> Vec<serde_json::Value> {
let assistant = assistant.as_assistant_name();
self.build_messages("user", assistant, |message, is_assistant| {
if is_assistant {
serde_json::json!({
"role": "assistant",
"content": message.content,
})
} else {
serde_json::json!({
"role": "user",
"content": format!("{}: {}", message.peer_id, message.content),
})
}
})
}
#[must_use]
pub fn len(&self) -> usize {
self.messages.len()
+ usize::from(self.summary.is_some())
+ usize::from(self.peer_representation.is_some())
+ usize::from(self.peer_card.is_some())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
&& self.summary.is_none()
&& self.peer_representation.is_none()
&& self.peer_card.is_none()
}
}