use crate::core::types::{RouteDecision, SearchResult, TaxonomyEntry};
use rmcp::schemars::{self, JsonSchema};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SearchRequest {
pub query: String,
pub wing: Option<String>,
pub room: Option<String>,
pub top_k: Option<usize>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct SearchResponse {
pub results: Vec<SearchResultDto>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct SearchResultDto {
pub drawer_id: String,
pub content: String,
pub wing: String,
pub room: Option<String>,
pub source_file: String,
pub similarity: f32,
pub route: RouteDecisionDto,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tunnel_hints: Vec<String>,
pub entities: Vec<String>,
pub topics: Vec<String>,
pub flags: Vec<String>,
pub emotions: Vec<String>,
pub importance_stars: u8,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct RouteDecisionDto {
pub wing: Option<String>,
pub room: Option<String>,
pub confidence: f32,
pub reason: String,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct IngestRequest {
pub content: String,
pub wing: String,
pub room: Option<String>,
pub source: Option<String>,
pub dry_run: Option<bool>,
pub importance: Option<i32>,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct DeleteRequest {
pub drawer_id: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct DeleteResponse {
pub drawer_id: String,
pub deleted: bool,
pub message: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct IngestResponse {
pub drawer_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub duplicate_warning: Option<DuplicateWarning>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct DuplicateWarning {
pub similar_drawer_id: String,
pub similarity: f32,
pub preview: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct StatusResponse {
pub schema_version: u32,
pub drawer_count: i64,
pub taxonomy_count: i64,
pub db_size_bytes: u64,
pub scopes: Vec<ScopeCount>,
pub aaak_spec: String,
pub memory_protocol: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ScopeCount {
pub wing: String,
pub room: Option<String>,
pub drawer_count: i64,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct TaxonomyRequest {
pub action: String,
pub wing: Option<String>,
pub room: Option<String>,
pub keywords: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct TaxonomyResponse {
pub action: String,
pub entries: Vec<TaxonomyEntryDto>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct TaxonomyEntryDto {
pub wing: String,
pub room: String,
pub display_name: Option<String>,
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct KgRequest {
pub action: String,
pub subject: Option<String>,
pub predicate: Option<String>,
pub object: Option<String>,
pub triple_id: Option<String>,
pub active_only: Option<bool>,
pub source_drawer: Option<String>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct KgResponse {
pub action: String,
pub triples: Vec<TripleDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stats: Option<KgStatsDto>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct KgStatsDto {
pub total: i64,
pub active: i64,
pub expired: i64,
pub entities: i64,
pub top_predicates: Vec<(String, i64)>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct TripleDto {
pub id: String,
pub subject: String,
pub predicate: String,
pub object: String,
pub valid_from: Option<String>,
pub valid_to: Option<String>,
pub confidence: f64,
pub source_drawer: Option<String>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct TunnelsResponse {
pub tunnels: Vec<TunnelDto>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct TunnelDto {
pub room: String,
pub wings: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct PeekPartnerRequest {
pub tool: String,
pub limit: Option<usize>,
pub since: Option<String>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct PeekPartnerResponse {
pub partner_tool: String,
pub session_path: Option<String>,
pub session_mtime: Option<String>,
pub partner_active: bool,
pub messages: Vec<PeekMessageDto>,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct PeekMessageDto {
pub role: String,
pub at: String,
pub text: String,
}
impl From<crate::cowork::PeekMessage> for PeekMessageDto {
fn from(m: crate::cowork::PeekMessage) -> Self {
Self {
role: m.role,
at: m.at,
text: m.text,
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CoworkPushRequest {
pub content: String,
#[serde(default)]
pub target_tool: Option<String>,
pub cwd: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CoworkPushResponse {
pub target_tool: String,
pub inbox_path: String,
pub pushed_at: String,
pub inbox_size_after: u64,
}
impl SearchResultDto {
pub fn with_signals_from_result(value: SearchResult) -> Self {
let signals = crate::aaak::analyze(&value.content);
Self {
drawer_id: value.drawer_id,
content: value.content,
wing: value.wing,
room: value.room,
source_file: value.source_file,
similarity: value.similarity,
route: value.route.into(),
tunnel_hints: value.tunnel_hints,
entities: signals.entities,
topics: signals.topics,
flags: signals.flags,
emotions: signals.emotions,
importance_stars: signals.importance_stars,
}
}
}
impl From<RouteDecision> for RouteDecisionDto {
fn from(value: RouteDecision) -> Self {
Self {
wing: value.wing,
room: value.room,
confidence: value.confidence,
reason: value.reason,
}
}
}
impl From<TaxonomyEntry> for TaxonomyEntryDto {
fn from(value: TaxonomyEntry) -> Self {
Self {
wing: value.wing,
room: value.room,
display_name: value.display_name,
keywords: value.keywords,
}
}
}
#[cfg(test)]
mod tests {
use crate::core::types::{RouteDecision, SearchResult};
use super::SearchResultDto;
fn sample_result(content: &str) -> SearchResult {
SearchResult {
drawer_id: "drawer-1".to_string(),
content: content.to_string(),
wing: "mempal".to_string(),
room: Some("signals".to_string()),
source_file: "/tmp/signals.md".to_string(),
similarity: 0.91,
route: RouteDecision {
wing: Some("mempal".to_string()),
room: Some("signals".to_string()),
confidence: 0.88,
reason: "unit test".to_string(),
},
tunnel_hints: vec!["docs".to_string()],
}
}
#[test]
fn test_with_signals_preserves_raw_content_and_citations() {
let original = "We decided to use Arc<Mutex<>> for state because shared ownership mattered";
let dto = SearchResultDto::with_signals_from_result(sample_result(original));
assert_eq!(dto.content, original);
assert!(!dto.content.starts_with("V1|"));
assert!(!dto.content.contains('★'));
assert_eq!(dto.drawer_id, "drawer-1");
assert_eq!(dto.source_file, "/tmp/signals.md");
assert_eq!(dto.tunnel_hints, vec!["docs".to_string()]);
assert!(dto.flags.contains(&"DECISION".to_string()));
assert!(dto.importance_stars >= 2);
assert!(!dto.entities.is_empty());
}
#[test]
fn test_with_signals_applies_empty_content_sentinels() {
let dto = SearchResultDto::with_signals_from_result(sample_result(""));
assert_eq!(dto.entities, vec!["UNK".to_string()]);
assert_eq!(dto.flags, vec!["CORE".to_string()]);
assert_eq!(dto.emotions, vec!["determ".to_string()]);
assert!(dto.topics.is_empty());
assert_eq!(dto.importance_stars, 2);
}
}