use std::collections::HashMap;
use crate::types::pagination::Page;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConclusionLevel {
#[default]
Explicit,
Deductive,
Inductive,
Contradiction,
#[serde(other)]
Unknown,
}
impl std::fmt::Display for ConclusionLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Explicit => "explicit",
Self::Deductive => "deductive",
Self::Inductive => "inductive",
Self::Contradiction => "contradiction",
Self::Unknown => "unknown",
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConclusionResponse {
pub id: String,
pub content: String,
pub observer_id: String,
pub observed_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub level: ConclusionLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
#[non_exhaustive]
pub struct ConclusionCreate {
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, Deserialize, PartialEq, Eq, bon::Builder)]
#[builder(finish_fn = build)]
#[non_exhaustive]
pub struct ConclusionBatchCreate {
pub conclusions: Vec<ConclusionCreate>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, bon::Builder)]
#[builder(finish_fn = build)]
#[non_exhaustive]
pub struct ConclusionGet {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filters: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, bon::Builder)]
#[builder(on(String, into))]
#[builder(finish_fn = build)]
#[non_exhaustive]
pub struct ConclusionQuery {
pub query: String,
#[serde(default = "default_top_k")]
#[builder(default = DEFAULT_TOP_K)]
pub top_k: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub distance: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filters: Option<HashMap<String, serde_json::Value>>,
}
const DEFAULT_TOP_K: u32 = 10;
fn default_top_k() -> u32 {
DEFAULT_TOP_K
}
pub type ConclusionPage = Page<ConclusionResponse>;
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)]
use super::*;
#[test]
fn top_k_serializes_even_when_equal_to_default() {
let query = ConclusionQuery::builder().query("hello").build();
assert_eq!(query.top_k, DEFAULT_TOP_K);
let value = serde_json::to_value(&query).unwrap();
assert_eq!(value["top_k"], serde_json::json!(DEFAULT_TOP_K));
}
#[test]
fn explicit_top_k_round_trips() {
let query = ConclusionQuery::builder().query("hello").top_k(5).build();
let value = serde_json::to_value(&query).unwrap();
assert_eq!(value["top_k"], serde_json::json!(5));
let decoded: ConclusionQuery = serde_json::from_value(value).unwrap();
assert_eq!(decoded.top_k, 5);
}
#[test]
fn missing_top_k_deserializes_to_default() {
let decoded: ConclusionQuery =
serde_json::from_value(serde_json::json!({ "query": "hi" })).unwrap();
assert_eq!(decoded.top_k, DEFAULT_TOP_K);
}
#[test]
fn conclusion_response_level_defaults_to_explicit_when_absent() {
let decoded: ConclusionResponse = serde_json::from_value(serde_json::json!({
"id": "c1",
"content": "x",
"observer_id": "o",
"observed_id": "d",
"created_at": "2025-01-01T00:00:00Z",
}))
.unwrap();
assert_eq!(decoded.level, ConclusionLevel::Explicit);
let v = serde_json::to_value(&decoded).unwrap();
assert_eq!(v["level"], serde_json::json!("explicit"));
}
#[test]
fn conclusion_response_level_round_trips_each_variant() {
for s in ["explicit", "deductive", "inductive", "contradiction"] {
let decoded: ConclusionLevel = serde_json::from_value(serde_json::json!(s)).unwrap();
let re = serde_json::to_value(decoded).unwrap();
assert_eq!(
re,
serde_json::json!(s),
"level variant {s} must round-trip"
);
}
}
#[test]
fn conclusion_level_unknown_variant_does_not_error() {
let decoded: ConclusionLevel =
serde_json::from_value(serde_json::json!("abductive")).unwrap();
assert_eq!(decoded, ConclusionLevel::Unknown);
let re = serde_json::to_value(decoded).unwrap();
assert_eq!(re, serde_json::json!("unknown"));
}
#[test]
fn conclusion_level_display() {
assert_eq!(ConclusionLevel::Explicit.to_string(), "explicit");
assert_eq!(ConclusionLevel::Deductive.to_string(), "deductive");
assert_eq!(ConclusionLevel::Inductive.to_string(), "inductive");
assert_eq!(ConclusionLevel::Contradiction.to_string(), "contradiction");
assert_eq!(ConclusionLevel::Unknown.to_string(), "unknown");
}
}