1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8pub use super::common::{
9 DreamConfiguration, PeerCardConfiguration, ReasoningConfiguration, SummaryConfiguration,
10};
11pub use super::dream::SessionQueueStatus;
12
13#[non_exhaustive]
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct SessionResponse {
17 pub id: String,
19 pub is_active: bool,
21 pub workspace_id: String,
23 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
25 pub metadata: HashMap<String, serde_json::Value>,
26 #[serde(default)]
28 pub configuration: SessionConfiguration,
29 pub created_at: DateTime<Utc>,
31}
32
33#[non_exhaustive]
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
36#[builder(on(String, into))]
37#[builder(finish_fn = build)]
38pub struct SessionCreate {
39 pub id: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub metadata: Option<HashMap<String, serde_json::Value>>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub peers: Option<HashMap<String, SessionPeerConfig>>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub configuration: Option<SessionConfiguration>,
50}
51
52impl SessionCreate {
53 pub fn validate(&self) -> crate::error::Result<()> {
55 if self.id.is_empty() {
56 return Err(crate::error::HonchoError::Validation(
57 "session id must not be empty".into(),
58 ));
59 }
60 if !self
61 .id
62 .chars()
63 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
64 {
65 return Err(crate::error::HonchoError::Validation(
66 "session id must contain only [a-zA-Z0-9_-]".into(),
67 ));
68 }
69 Ok(())
70 }
71}
72
73#[non_exhaustive]
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
76#[builder(on(String, into))]
77#[builder(finish_fn = build)]
78pub struct SessionUpdate {
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub metadata: Option<HashMap<String, serde_json::Value>>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub configuration: Option<SessionConfiguration>,
85}
86
87#[non_exhaustive]
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
90#[builder(on(String, into))]
91#[builder(finish_fn = build)]
92pub struct SessionGet {
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub filters: Option<HashMap<String, serde_json::Value>>,
96}
97
98#[non_exhaustive]
100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
101pub struct SessionMetadataSet {
102 pub metadata: HashMap<String, serde_json::Value>,
104}
105
106#[non_exhaustive]
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109pub struct SessionConfigurationSet {
110 pub configuration: HashMap<String, serde_json::Value>,
112}
113
114#[non_exhaustive]
119#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
120pub struct SessionConfiguration {
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub reasoning: Option<ReasoningConfiguration>,
124 #[serde(skip_serializing_if = "Option::is_none")]
129 pub peer_card: Option<PeerCardConfiguration>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub summary: Option<SummaryConfiguration>,
133 #[serde(skip_serializing_if = "Option::is_none")]
138 pub dream: Option<DreamConfiguration>,
139}
140
141#[non_exhaustive]
143#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
144pub struct SessionPeerConfig {
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub observe_me: Option<bool>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub observe_others: Option<bool>,
151}
152
153#[non_exhaustive]
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
156#[builder(on(String, into))]
157#[builder(finish_fn = build)]
158pub struct SessionContextOptions {
159 #[serde(default = "default_true")]
161 #[builder(default = true)]
162 pub summary: bool,
163 #[serde(default)]
165 #[builder(default)]
166 pub limit_to_session: bool,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub tokens: Option<u32>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub peer_target: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub peer_perspective: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub search_query: Option<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub search_top_k: Option<u32>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub search_max_distance: Option<f64>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub include_most_frequent: Option<bool>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub max_conclusions: Option<u32>,
191}
192
193pub(crate) const SEARCH_TOP_K_MIN: u32 = 1;
194pub(crate) const SEARCH_TOP_K_MAX: u32 = 100;
195pub(crate) const SEARCH_MAX_DISTANCE_MIN: f64 = 0.0;
196pub(crate) const SEARCH_MAX_DISTANCE_MAX: f64 = 1.0;
197pub(crate) const MAX_CONCLUSIONS_MIN: u32 = 1;
198pub(crate) const MAX_CONCLUSIONS_MAX: u32 = 100;
199
200pub(crate) fn validate_search_params(
206 search_top_k: Option<u32>,
207 search_max_distance: Option<f64>,
208 max_conclusions: Option<u32>,
209) -> crate::error::Result<()> {
210 if let Some(k) = search_top_k
211 && !(SEARCH_TOP_K_MIN..=SEARCH_TOP_K_MAX).contains(&k)
212 {
213 return Err(crate::error::HonchoError::Validation(format!(
214 "search_top_k must be between {SEARCH_TOP_K_MIN} and {SEARCH_TOP_K_MAX}, got {k}"
215 )));
216 }
217 if let Some(d) = search_max_distance
218 && !(SEARCH_MAX_DISTANCE_MIN..=SEARCH_MAX_DISTANCE_MAX).contains(&d)
219 {
220 return Err(crate::error::HonchoError::Validation(format!(
221 "search_max_distance must be between {SEARCH_MAX_DISTANCE_MIN} and {SEARCH_MAX_DISTANCE_MAX}, got {d}"
222 )));
223 }
224 if let Some(c) = max_conclusions
225 && !(MAX_CONCLUSIONS_MIN..=MAX_CONCLUSIONS_MAX).contains(&c)
226 {
227 return Err(crate::error::HonchoError::Validation(format!(
228 "max_conclusions must be between {MAX_CONCLUSIONS_MIN} and {MAX_CONCLUSIONS_MAX}, got {c}"
229 )));
230 }
231 Ok(())
232}
233
234impl SessionContextOptions {
235 pub fn validate(&self) -> crate::error::Result<()> {
249 if self.peer_perspective.is_some() && self.peer_target.is_none() {
250 return Err(crate::error::HonchoError::Validation(
251 "peer_perspective requires peer_target to be set".into(),
252 ));
253 }
254 if self.search_query.is_some() && self.peer_target.is_none() {
255 return Err(crate::error::HonchoError::Validation(
256 "search_query requires peer_target to be set".into(),
257 ));
258 }
259 validate_search_params(
260 self.search_top_k,
261 self.search_max_distance,
262 self.max_conclusions,
263 )?;
264 if let Some(t) = self.tokens
265 && t == 0
266 {
267 return Err(crate::error::HonchoError::Validation(
268 "tokens must be greater than 0".into(),
269 ));
270 }
271 Ok(())
272 }
273
274 pub(crate) fn to_query_params(&self) -> Vec<(&'static str, std::borrow::Cow<'_, str>)> {
282 use std::borrow::Cow;
283
284 let mut params: Vec<(&'static str, Cow<'_, str>)> = vec![
285 (
286 "summary",
287 Cow::Borrowed(if self.summary { "true" } else { "false" }),
288 ),
289 (
290 "limit_to_session",
291 Cow::Borrowed(if self.limit_to_session {
292 "true"
293 } else {
294 "false"
295 }),
296 ),
297 ];
298 if let Some(v) = self.tokens {
299 params.push(("tokens", Cow::Owned(v.to_string())));
300 }
301 if let Some(ref v) = self.peer_target {
302 params.push(("peer_target", Cow::Borrowed(v.as_str())));
303 }
304 if let Some(ref v) = self.peer_perspective {
305 params.push(("peer_perspective", Cow::Borrowed(v.as_str())));
306 }
307 if let Some(ref v) = self.search_query {
308 params.push(("search_query", Cow::Borrowed(v.as_str())));
309 }
310 if let Some(v) = self.search_top_k {
311 params.push(("search_top_k", Cow::Owned(v.to_string())));
312 }
313 if let Some(v) = self.search_max_distance {
314 params.push(("search_max_distance", Cow::Owned(v.to_string())));
315 }
316 if let Some(v) = self.include_most_frequent {
317 params.push((
318 "include_most_frequent",
319 Cow::Borrowed(if v { "true" } else { "false" }),
320 ));
321 }
322 if let Some(v) = self.max_conclusions {
323 params.push(("max_conclusions", Cow::Owned(v.to_string())));
324 }
325 params
326 }
327}
328
329fn default_true() -> bool {
330 true
331}
332
333fn escape_tag_value(value: &str) -> std::borrow::Cow<'_, str> {
344 if value.contains(['&', '<', '>']) {
345 std::borrow::Cow::Owned(
346 value
347 .replace('&', "&")
348 .replace('<', "<")
349 .replace('>', ">"),
350 )
351 } else {
352 std::borrow::Cow::Borrowed(value)
353 }
354}
355
356#[non_exhaustive]
358#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
359pub struct SessionContext {
360 pub id: String,
362 pub messages: Vec<super::message::MessageResponse>,
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub summary: Option<Summary>,
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub peer_representation: Option<String>,
370 #[serde(skip_serializing_if = "Option::is_none")]
372 pub peer_card: Option<Vec<String>>,
373}
374
375#[non_exhaustive]
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
378pub struct SessionSummaries {
379 pub id: String,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub short_summary: Option<Summary>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub long_summary: Option<Summary>,
387}
388
389#[non_exhaustive]
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(rename_all = "snake_case")]
393pub enum SummaryType {
394 Short,
396 Long,
398 #[serde(other)]
404 Unknown,
405}
406
407#[non_exhaustive]
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
410pub struct Summary {
411 pub content: String,
413 pub message_id: String,
415 pub summary_type: SummaryType,
417 pub created_at: DateTime<Utc>,
419 pub token_count: u32,
421}
422
423#[non_exhaustive]
425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
426#[builder(on(String, into))]
427#[builder(finish_fn = build)]
428pub struct SessionListOptions {
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub filters: Option<HashMap<String, serde_json::Value>>,
432 #[serde(default = "default_page")]
434 #[builder(default = default_page())]
435 pub page: u64,
436 #[serde(default = "default_size")]
438 #[builder(default = default_size())]
439 pub size: u64,
440 #[serde(default)]
442 #[builder(default)]
443 pub reverse: bool,
444}
445
446fn default_page() -> u64 {
447 1
448}
449
450fn default_size() -> u64 {
451 50
452}
453
454pub type SessionPage = super::pagination::Page<SessionResponse>;
456
457pub trait IntoAssistantRef {
465 fn as_assistant_name(&self) -> &str;
467}
468
469impl IntoAssistantRef for &str {
470 fn as_assistant_name(&self) -> &str {
471 self
472 }
473}
474
475impl IntoAssistantRef for String {
476 fn as_assistant_name(&self) -> &str {
477 self.as_str()
478 }
479}
480
481impl IntoAssistantRef for &crate::Peer {
482 fn as_assistant_name(&self) -> &str {
483 self.id()
484 }
485}
486
487impl SessionContext {
488 fn format_peer_card(card: &[String]) -> String {
495 let items: Vec<String> = card
496 .iter()
497 .map(|s| format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")))
498 .collect();
499 format!("[{}]", items.join(", "))
500 }
501
502 fn build_context_messages(&self) -> Vec<(&'static str, std::borrow::Cow<'_, str>)> {
507 let mut msgs = Vec::new();
508 if let Some(ref rep) = self.peer_representation {
509 msgs.push((
510 "peer_representation",
511 std::borrow::Cow::Borrowed(rep.as_str()),
512 ));
513 }
514 if let Some(ref card) = self.peer_card {
515 msgs.push((
516 "peer_card",
517 std::borrow::Cow::Owned(Self::format_peer_card(card)),
518 ));
519 }
520 if let Some(ref summary) = self.summary {
521 msgs.push((
522 "summary",
523 std::borrow::Cow::Borrowed(summary.content.as_str()),
524 ));
525 }
526 msgs
527 }
528
529 fn build_messages(
538 &self,
539 context_role: &'static str,
540 assistant: &str,
541 render_message: impl Fn(&super::message::MessageResponse, bool) -> serde_json::Value,
542 ) -> Vec<serde_json::Value> {
543 let mut result: Vec<serde_json::Value> = Vec::with_capacity(self.len());
544 for (tag, value) in self.build_context_messages() {
545 let value = escape_tag_value(&value);
546 result.push(serde_json::json!({
547 "role": context_role,
548 "content": format!("<{tag}>{value}</{tag}>"),
549 }));
550 }
551 for message in &self.messages {
552 let is_assistant = message.peer_id == assistant;
553 result.push(render_message(message, is_assistant));
554 }
555 result
556 }
557
558 #[must_use]
576 #[allow(clippy::needless_pass_by_value)]
577 pub fn to_openai(&self, assistant: impl IntoAssistantRef) -> Vec<serde_json::Value> {
578 let assistant = assistant.as_assistant_name();
579 self.build_messages("system", assistant, |message, is_assistant| {
580 serde_json::json!({
581 "role": if is_assistant { "assistant" } else { "user" },
582 "name": message.peer_id,
583 "content": message.content,
584 })
585 })
586 }
587
588 #[must_use]
597 #[allow(clippy::needless_pass_by_value)]
598 pub fn to_anthropic(&self, assistant: impl IntoAssistantRef) -> Vec<serde_json::Value> {
599 let assistant = assistant.as_assistant_name();
600 self.build_messages("user", assistant, |message, is_assistant| {
601 if is_assistant {
602 serde_json::json!({
603 "role": "assistant",
604 "content": message.content,
605 })
606 } else {
607 serde_json::json!({
608 "role": "user",
609 "content": format!("{}: {}", message.peer_id, message.content),
610 })
611 }
612 })
613 }
614
615 #[must_use]
617 pub fn len(&self) -> usize {
618 self.messages.len()
619 + usize::from(self.summary.is_some())
620 + usize::from(self.peer_representation.is_some())
621 + usize::from(self.peer_card.is_some())
622 }
623
624 #[must_use]
627 pub fn is_empty(&self) -> bool {
628 self.messages.is_empty()
629 && self.summary.is_none()
630 && self.peer_representation.is_none()
631 && self.peer_card.is_none()
632 }
633}