use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RobotResponse<T: Serialize> {
pub success: bool,
pub meta: ResponseMeta,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<RobotError>,
}
impl<T: Serialize> RobotResponse<T> {
pub fn success(data: T, meta: ResponseMeta) -> Self {
Self {
success: true,
meta,
data: Some(data),
errors: vec![],
}
}
pub fn error(errors: Vec<RobotError>, meta: ResponseMeta) -> RobotResponse<()> {
RobotResponse {
success: false,
meta,
data: None,
errors,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseMeta {
pub command: String,
pub elapsed_ms: u64,
pub timestamp: DateTime<Utc>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_corrected: Option<AutoCorrection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination: Option<Pagination>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_budget: Option<TokenBudget>,
}
impl ResponseMeta {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
elapsed_ms: 0,
timestamp: Utc::now(),
version: env!("CARGO_PKG_VERSION").to_string(),
auto_corrected: None,
pagination: None,
token_budget: None,
}
}
pub fn with_elapsed(mut self, elapsed_ms: u64) -> Self {
self.elapsed_ms = elapsed_ms;
self
}
pub fn with_auto_correction(mut self, auto_corrected: AutoCorrection) -> Self {
self.auto_corrected = Some(auto_corrected);
self
}
pub fn with_pagination(mut self, pagination: Pagination) -> Self {
self.pagination = Some(pagination);
self
}
pub fn with_token_budget(mut self, token_budget: TokenBudget) -> Self {
self.token_budget = Some(token_budget);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoCorrection {
pub original: String,
pub corrected: String,
pub distance: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pagination {
pub total: usize,
pub returned: usize,
pub offset: usize,
pub has_more: bool,
}
impl Pagination {
pub fn new(total: usize, returned: usize, offset: usize) -> Self {
Self {
total,
returned,
offset,
has_more: offset + returned < total,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenBudget {
pub max_tokens: usize,
pub estimated_tokens: usize,
pub truncated: bool,
}
impl TokenBudget {
pub fn new(max_tokens: usize) -> Self {
Self {
max_tokens,
estimated_tokens: 0,
truncated: false,
}
}
pub fn with_estimate(mut self, estimated_tokens: usize) -> Self {
self.estimated_tokens = estimated_tokens;
self.truncated = estimated_tokens >= self.max_tokens;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RobotError {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
}
impl RobotError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
details: None,
suggestion: None,
}
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn unknown_command(command: &str, suggestions: &[String]) -> Self {
let mut err = Self::new("E001", format!("Unknown command: {}", command));
if !suggestions.is_empty() {
err = err.with_suggestion(format!("Did you mean: {}?", suggestions.join(", ")));
}
err
}
pub fn invalid_argument(arg: &str, reason: &str) -> Self {
Self::new("E002", format!("Invalid argument '{}': {}", arg, reason))
}
pub fn missing_argument(arg: &str) -> Self {
Self::new("E003", format!("Missing required argument: {}", arg))
.with_suggestion(format!("Provide the {} argument", arg))
}
pub fn index_not_found(index_name: &str) -> Self {
Self::new("E004", format!("Index not found: {}", index_name))
.with_suggestion("Initialize the index first")
}
pub fn no_results(query: &str) -> Self {
Self::new("E005", format!("No results found for: {}", query))
.with_suggestion("Try a broader search query")
}
pub fn network_error(message: &str) -> Self {
Self::new("E006", format!("Network error: {}", message))
}
pub fn timeout_error(operation: &str, timeout_ms: u64) -> Self {
Self::new(
"E007",
format!("Operation '{}' timed out after {}ms", operation, timeout_ms),
)
}
pub fn parse_error(message: &str) -> Self {
Self::new("E008", format!("Parse error: {}", message))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResultsData {
pub results: Vec<SearchResultItem>,
pub total_matches: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub concepts_matched: Vec<String>,
#[serde(default)]
pub wildcard_fallback: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResultItem {
pub rank: usize,
pub id: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
pub score: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub preview_truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilitiesData {
pub name: String,
pub version: String,
pub description: String,
pub features: FeatureFlags,
pub commands: Vec<String>,
pub supported_formats: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_status: Option<IndexStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureFlags {
pub search: bool,
pub chat: bool,
pub mcp_tools: bool,
pub file_operations: bool,
pub web_operations: bool,
pub vm_execution: bool,
pub session_search: bool,
pub knowledge_graph: bool,
}
impl Default for FeatureFlags {
fn default() -> Self {
Self {
search: true,
chat: cfg!(feature = "repl-chat"),
mcp_tools: cfg!(feature = "repl-mcp"),
file_operations: cfg!(feature = "repl-file"),
web_operations: cfg!(feature = "repl-web"),
vm_execution: true,
session_search: false, knowledge_graph: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexStatus {
pub documents_indexed: usize,
pub sessions_indexed: usize,
pub last_updated: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_robot_response_success() {
let meta = ResponseMeta::new("search");
let response = RobotResponse::success("test data", meta);
assert!(response.success);
assert!(response.data.is_some());
assert!(response.errors.is_empty());
}
#[test]
fn test_robot_response_error() {
let meta = ResponseMeta::new("search");
let errors = vec![RobotError::no_results("test query")];
let response = RobotResponse::<()>::error(errors, meta);
assert!(!response.success);
assert!(response.data.is_none());
assert!(!response.errors.is_empty());
}
#[test]
fn test_pagination() {
let pagination = Pagination::new(100, 10, 0);
assert!(pagination.has_more);
let pagination = Pagination::new(100, 10, 90);
assert!(!pagination.has_more);
}
#[test]
fn test_robot_error_serialization() {
let error = RobotError::unknown_command("serach", &["search".to_string()]);
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("E001"));
assert!(json.contains("serach"));
}
}