fleet-memory/hindsight-cli/src/main.rs.bak5
2025-11-25 19:28:26 +01:00

1048 lines
36 KiB
Text

mod api;
mod config;
mod errors;
mod output;
mod ui;
use anyhow::{Context, Result};
use api::{ApiClient, BatchMemoryRequest, MemoryItem, SearchRequest, ThinkRequest};
use clap::{Parser, Subcommand, ValueEnum};
use config::Config;
use output::OutputFormat;
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Format {
Pretty,
Json,
Yaml,
}
impl From<Format> for OutputFormat {
fn from(f: Format) -> Self {
match f {
Format::Pretty => OutputFormat::Pretty,
Format::Json => OutputFormat::Json,
Format::Yaml => OutputFormat::Yaml,
}
}
}
#[derive(Parser)]
#[command(name = "memora")]
#[command(about = "Memora CLI - Semantic memory system", long_about = None)]
#[command(version)]
struct Cli {
/// Output format (pretty, json, yaml)
#[arg(short = 'o', long, global = true, default_value = "pretty")]
output: Format,
/// Show verbose output including full requests and responses
#[arg(short = 'v', long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Manage agents (list, profile, stats)
#[command(subcommand)]
Agent(AgentCommands),
/// Manage memories (search, think, put, delete)
#[command(subcommand)]
Memory(MemoryCommands),
/// Manage documents (list, get, delete)
#[command(subcommand)]
Document(DocumentCommands),
/// Manage async operations (list, cancel)
#[command(subcommand)]
Operation(OperationCommands),
}
#[derive(Subcommand)]
enum AgentCommands {
/// List all agents
List,
/// Get agent profile (personality + background)
Profile {
/// Agent ID
agent_id: String,
},
/// Get memory statistics for an agent
Stats {
/// Agent ID
agent_id: String,
},
/// Update agent personality traits
SetPersonality {
/// Agent ID
agent_id: String,
/// Openness to experience (0.0-1.0)
#[arg(long)]
openness: f32,
/// Conscientiousness (0.0-1.0)
#[arg(long)]
conscientiousness: f32,
/// Extraversion (0.0-1.0)
#[arg(long)]
extraversion: f32,
/// Agreeableness (0.0-1.0)
#[arg(long)]
agreeableness: f32,
/// Neuroticism (0.0-1.0)
#[arg(long)]
neuroticism: f32,
/// Bias strength (0.0-1.0)
#[arg(long)]
bias_strength: f32,
},
/// Set or merge agent background
SetBackground {
/// Agent ID
agent_id: String,
/// Background content
content: String,
/// Skip automatic personality inference
#[arg(long)]
no_update_personality: bool,
},
}
#[derive(Subcommand)]
enum MemoryCommands {
/// Search for memories using semantic search
Search {
/// Agent ID
agent_id: String,
/// Search query
query: String,
/// Fact types to search (world, agent, opinion)
#[arg(short = 't', long, value_delimiter = ',', default_values = &["world", "agent", "opinion"])]
fact_type: Vec<String>,
/// Thinking budget
#[arg(short = 'b', long, default_value = "100")]
budget: i32,
/// Maximum tokens for results
#[arg(long, default_value = "4096")]
max_tokens: i32,
/// Show trace information
#[arg(long)]
trace: bool,
},
/// Generate answers using agent identity
Think {
/// Agent ID
agent_id: String,
/// Query to think about
query: String,
/// Thinking budget
#[arg(short = 'b', long, default_value = "50")]
budget: i32,
/// Additional context
#[arg(short = 'c', long)]
context: Option<String>,
},
/// Store a single memory
Put {
/// Agent ID
agent_id: String,
/// Memory content
content: String,
/// Document ID (auto-generated if not provided)
#[arg(short = 'd', long)]
doc_id: Option<String>,
/// Context for the memory
#[arg(short = 'c', long)]
context: Option<String>,
/// Queue for background processing
#[arg(long)]
r#async: bool,
},
/// Bulk import memories from files
PutFiles {
/// Agent ID
agent_id: String,
/// Path to file or directory
path: PathBuf,
/// Search directories recursively
#[arg(short = 'r', long, default_value = "true")]
recursive: bool,
/// Context for all memories
#[arg(short = 'c', long)]
context: Option<String>,
/// Queue for background processing
#[arg(long)]
r#async: bool,
},
/// Delete a memory unit
Delete {
/// Agent ID
agent_id: String,
/// Memory unit ID
unit_id: String,
},
}
#[derive(Subcommand)]
enum DocumentCommands {
/// List documents for an agent
List {
/// Agent ID
agent_id: String,
/// Search query to filter documents
#[arg(short = 'q', long)]
query: Option<String>,
/// Maximum number of results
#[arg(short = 'l', long, default_value = "100")]
limit: i32,
/// Offset for pagination
#[arg(short = 's', long, default_value = "0")]
offset: i32,
},
/// Get a specific document by ID
Get {
/// Agent ID
agent_id: String,
/// Document ID
document_id: String,
},
/// Delete a document and all its memory units
Delete {
/// Agent ID
agent_id: String,
/// Document ID
document_id: String,
},
}
#[derive(Subcommand)]
enum OperationCommands {
/// List async operations for an agent
List {
/// Agent ID
agent_id: String,
},
/// Cancel a pending async operation
Cancel {
/// Agent ID
agent_id: String,
/// Operation ID
operation_id: String,
},
}
fn main() {
if let Err(e) = run() {
std::process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
let output_format: OutputFormat = cli.output.into();
let verbose = cli.verbose;
// Load configuration
let config = Config::from_env().unwrap_or_else(|e| {
ui::print_error(&format!("Configuration error: {}", e));
errors::print_config_help();
std::process::exit(1);
});
let api_url = config.api_url().to_string();
// Create API client
let client = ApiClient::new(api_url.clone()).unwrap_or_else(|e| {
errors::handle_api_error(e, &api_url);
});
// Execute command and handle errors
let result: Result<()> = match cli.command {
Commands::Agent(agent_cmd) => match agent_cmd {
AgentCommands::List => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching agents..."))
} else {
None
};
let response = client.list_agents(verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(agents_list) => {
if output_format == OutputFormat::Pretty {
if agents_list.agents.is_empty() {
ui::print_warning("No agents found");
} else {
ui::print_info(&format!("Found {} agent(s)", agents_list.agents.len()));
for agent in &agents_list.agents {
println!(" - {}", agent);
}
}
} else {
output::print_output(&agents_list, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
AgentCommands::Profile { agent_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching profile..."))
} else {
None
};
let response = client.get_profile(&agent_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(profile) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Profile for agent '{}'", agent_id));
if let Some(personality) = &profile.personality {
println!("\n Personality Traits:");
println!(" Openness: {:.2}", personality.openness);
println!(" Conscientiousness: {:.2}", personality.conscientiousness);
println!(" Extraversion: {:.2}", personality.extraversion);
println!(" Agreeableness: {:.2}", personality.agreeableness);
println!(" Neuroticism: {:.2}", personality.neuroticism);
println!(" Bias Strength: {:.2}", personality.bias_strength);
}
if !profile.background.is_empty() {
println!("\n Background:\n{}", profile.background);
}
} else {
output::print_output(&profile, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
AgentCommands::Stats { agent_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching statistics..."))
} else {
None
};
let response = client.get_stats(&agent_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(stats) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Statistics for agent '{}'", agent_id));
println!();
println!(" 📊 Overview");
println!(" Total Memory Units: {}", stats.total_nodes);
println!(" Total Links: {}", stats.total_links);
println!(" Total Documents: {}", stats.total_documents);
println!();
println!(" 🧠 Memory Units by Type");
let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect();
fact_types.sort_by_key(|(k, _)| *k);
for (fact_type, count) in fact_types {
let icon = match fact_type.as_str() {
"world" => "🌍",
"agent" => "🤖",
"opinion" => "💭",
_ => "•"
};
println!(" {} {:<10} {}", icon, fact_type, count);
}
println!();
println!(" 🔗 Links by Type");
let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect();
link_types.sort_by_key(|(k, _)| *k);
for (link_type, count) in link_types {
let icon = match link_type.as_str() {
"temporal" => "⏰",
"semantic" => "🔤",
"entity" => "🏷️",
_ => "•"
};
println!(" {} {:<10} {}", icon, link_type, count);
}
println!();
println!(" 🔗 Links by Fact Type");
let mut fact_type_links: Vec<_> = stats.links_by_fact_type.iter().collect();
fact_type_links.sort_by_key(|(k, _)| *k);
for (fact_type, count) in fact_type_links {
let icon = match fact_type.as_str() {
"world" => "🌍",
"agent" => "🤖",
"opinion" => "💭",
_ => "•"
};
println!(" {} {:<10} {}", icon, fact_type, count);
}
println!();
if !stats.links_breakdown.is_empty() {
println!(" 📈 Detailed Link Breakdown");
let mut fact_types: Vec<_> = stats.links_breakdown.iter().collect();
fact_types.sort_by_key(|(k, _)| *k);
for (fact_type, link_types) in fact_types {
let icon = match fact_type.as_str() {
"world" => "🌍",
"agent" => "🤖",
"opinion" => "💭",
_ => "•"
};
println!(" {} {}", icon, fact_type);
let mut sorted_links: Vec<_> = link_types.iter().collect();
sorted_links.sort_by_key(|(k, _)| *k);
for (link_type, count) in sorted_links {
println!(" - {:<10} {}", link_type, count);
}
}
println!();
}
if stats.pending_operations > 0 || stats.failed_operations > 0 {
println!(" ⚙️ Operations");
if stats.pending_operations > 0 {
println!(" ⏳ Pending: {}", stats.pending_operations);
}
if stats.failed_operations > 0 {
println!(" ❌ Failed: {}", stats.failed_operations);
}
}
} else {
output::print_output(&stats, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
AgentCommands::SetPersonality {
agent_id,
openness,
conscientiousness,
extraversion,
agreeableness,
neuroticism,
bias_strength,
} => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Updating personality..."))
} else {
None
};
let personality = PersonalityTraits {
openness,
conscientiousness,
extraversion,
agreeableness,
neuroticism,
bias_strength,
};
let response = client.set_personality(&agent_id, personality, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(profile) => {
if output_format == OutputFormat::Pretty {
ui::print_success("Personality updated successfully");
if let Some(p) = &profile.personality {
println!(" Openness: {:.2}", p.openness);
println!(" Conscientiousness: {:.2}", p.conscientiousness);
println!(" Extraversion: {:.2}", p.extraversion);
println!(" Agreeableness: {:.2}", p.agreeableness);
println!(" Neuroticism: {:.2}", p.neuroticism);
println!(" Bias Strength: {:.2}", p.bias_strength);
}
} else {
output::print_output(&profile, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
AgentCommands::SetBackground {
agent_id,
content,
no_update_personality,
} => {
let current_profile = if !no_update_personality {
client.get_profile(&agent_id, verbose).ok()
} else {
None
};
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Merging background..."))
} else {
None
};
let response = client.add_background(
&agent_id,
&content,
!no_update_personality,
verbose,
);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(profile) => {
if output_format == OutputFormat::Pretty {
ui::print_success("Background updated successfully");
println!("\n{}", profile.background);
if !no_update_personality {
if let (Some(old_p), Some(new_p)) =
(current_profile.as_ref().map(|p| p.personality), &profile.personality)
{
println!("\nPersonality changes:");
println!(" Openness: {:.2} → {:.2}", old_p.openness, new_p.openness);
println!(" Conscientiousness: {:.2} → {:.2}", old_p.conscientiousness, new_p.conscientiousness);
println!(" Extraversion: {:.2} → {:.2}", old_p.extraversion, new_p.extraversion);
println!(" Agreeableness: {:.2} → {:.2}", old_p.agreeableness, new_p.agreeableness);
println!(" Neuroticism: {:.2} → {:.2}", old_p.neuroticism, new_p.neuroticism);
}
}
} else {
output::print_output(&profile, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
},
Commands::Memory(memory_cmd) => match memory_cmd {
MemoryCommands::Search {
agent_id,
query,
fact_type,
budget,
max_tokens,
trace,
} => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Searching memories..."))
} else {
None
};
let request = SearchRequest {
query,
fact_type,
thinking_budget: budget,
max_tokens,
trace,
};
let response = client.search(&agent_id, request, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
output::print_output(&result, output_format)?;
Ok(())
}
Err(e) => Err(e)
}
}
MemoryCommands::Think {
agent_id,
query,
budget,
context,
} => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Thinking..."))
} else {
None
};
let request = ThinkRequest {
query,
thinking_budget: budget,
context,
};
let response = client.think(&agent_id, request, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
output::print_output(&result, output_format)?;
Ok(())
}
Err(e) => Err(e)
}
}
MemoryCommands::Put {
agent_id,
content,
doc_id,
context,
r#async,
} => {
let doc_id = doc_id.unwrap_or_else(config::generate_doc_id);
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Storing memory..."))
} else {
None
};
let item = MemoryItem {
content: content.clone(),
context,
};
let request = BatchMemoryRequest {
items: vec![item],
document_id: Some(doc_id.clone()),
};
let response = client.put_memories(&agent_id, request, r#async, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!(
"Memory stored successfully (document: {})",
doc_id
));
if let Some(op_id) = result.job_id {
println!(" Operation ID: {}", op_id);
println!(" Status: queued for background processing");
} else {
println!(" Stored count: {}", result.stored_count.unwrap_or(0));
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
MemoryCommands::PutFiles {
agent_id,
path,
recursive,
context,
r#async,
} => {
if !path.exists() {
anyhow::bail!("Path does not exist: {}", path.display());
}
let mut files = Vec::new();
if path.is_file() {
files.push(path);
} else if path.is_dir() {
if recursive {
for entry in WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "txt" || ext == "md" {
files.push(path.to_path_buf());
}
}
}
} else {
for entry in fs::read_dir(&path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "txt" || ext == "md" {
files.push(path);
}
}
}
}
}
}
if files.is_empty() {
ui::print_warning("No .txt or .md files found");
return Ok(());
}
ui::print_info(&format!("Found {} files to import", files.len()));
let pb = ui::create_progress_bar(files.len() as u64, "Processing files");
let mut items = Vec::new();
let mut document_id = None;
for file_path in &files {
let content = fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
let doc_id = file_path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
.unwrap_or_else(config::generate_doc_id);
if document_id.is_none() {
document_id = Some(doc_id);
}
items.push(MemoryItem {
content,
context: context.clone(),
});
pb.inc(1);
}
pb.finish_with_message("Files processed");
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Uploading memories..."))
} else {
None
};
let request = BatchMemoryRequest {
items,
document_id,
};
let response = client.put_memories(&agent_id, request, r#async, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_success("Files imported successfully");
if let Some(op_id) = result.job_id {
println!(" Operation ID: {}", op_id);
println!(" Status: queued for background processing");
} else {
println!(" Total units created: {}", result.stored_count.unwrap_or(0));
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
MemoryCommands::Delete { agent_id, unit_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Deleting memory unit..."))
} else {
None
};
let response = client.delete_memory(&agent_id, &unit_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
ui::print_success(&result.message);
} else {
ui::print_error(&result.message);
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
},
Commands::Document(doc_cmd) => match doc_cmd {
DocumentCommands::List {
agent_id,
query,
limit,
offset,
} => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching documents..."))
} else {
None
};
let response = client.list_documents(&agent_id, query.as_deref(), Some(limit), Some(offset), verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(docs_response) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Documents for agent '{}' (total: {})", agent_id, docs_response.total));
for doc in &docs_response.items {
println!("\n Document ID: {}", doc.id);
println!(" Created: {}", doc.created_at);
println!(" Updated: {}", doc.updated_at);
println!(" Text Length: {}", doc.text_length);
println!(" Memory Units: {}", doc.memory_unit_count);
}
} else {
output::print_output(&docs_response, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
DocumentCommands::Get { agent_id, document_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching document..."))
} else {
None
};
let response = client.get_document(&agent_id, &document_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(doc) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Document: {}", doc.id));
println!(" Agent ID: {}", doc.agent_id);
println!(" Created: {}", doc.created_at);
println!(" Updated: {}", doc.updated_at);
println!(" Memory Units: {}", doc.memory_unit_count);
println!("\n Text:\n{}", doc.original_text);
} else {
output::print_output(&doc, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
DocumentCommands::Delete { agent_id, document_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Deleting document..."))
} else {
None
};
let response = client.delete_document(&agent_id, &document_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
ui::print_success(&result.message);
} else {
ui::print_error(&result.message);
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
},
Commands::Operation(op_cmd) => match op_cmd {
OperationCommands::List { agent_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching operations..."))
} else {
None
};
let response = client.list_operations(&agent_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(ops_response) => {
if output_format == OutputFormat::Pretty {
if ops_response.operations.is_empty() {
ui::print_info("No operations found");
} else {
ui::print_info(&format!("Found {} operation(s)", ops_response.operations.len()));
for op in &ops_response.operations {
println!("\n Operation ID: {}", op.id);
println!(" Type: {}", op.task_type);
println!(" Status: {}", op.status);
println!(" Items: {}", op.items_count);
if let Some(doc_id) = &op.document_id {
println!(" Document ID: {}", doc_id);
}
}
}
} else {
output::print_output(&ops_response, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
OperationCommands::Cancel { agent_id, operation_id } => {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Cancelling operation..."))
} else {
None
};
let response = client.cancel_operation(&agent_id, &operation_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
ui::print_success(&result.message);
} else {
ui::print_error(&result.message);
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
},
};
// Handle API errors with nice messages
if let Err(e) = result {
errors::handle_api_error(e, &api_url);
}
Ok(())
}