* feat: implement hierarchical configuration (system, tenant, bank) * feat: implement hierarchical configuration (system, tenant, bank) * docs: add instructions for hierarchical config in CLAUDE.md * feat: add ENABLE_BANK_CONFIG_API flag (disabled by default) - Add HINDSIGHT_API_ENABLE_BANK_CONFIG_API env var (default: false) - Return 403 Forbidden from bank config endpoints when disabled - Update tests to enable the flag - Update CLAUDE.md documentation This provides security control over the bank configuration API, ensuring it's only accessible when explicitly enabled. * docs: add hierarchical configuration section * feat(cli): add bank config commands (config, set-config, reset-config) - Add 'hindsight bank config' to view bank configuration - Add 'hindsight bank set-config' to update LLM settings per bank - Add 'hindsight bank reset-config' to reset to defaults - Implements client API calls to new bank config endpoints * fix(cli): fix compilation errors in bank config commands - Fix type signature: use ApiClient instead of api::Client - Fix confirmation: use ui::prompt_confirmation instead of ui::confirm - Fix error handling: use anyhow! macro instead of errors::Error - Fix type conversion: convert HashMap to serde_json::Map for API call * feat: implement type-safe hierarchical config with bank overrides Implements a production-ready hierarchical configuration system that prevents accidentally using global defaults when bank-specific overrides exist. - Created StaticConfigProxy that wraps HindsightConfig - get_config() now returns proxy that blocks access to bank-configurable fields - Raises ConfigFieldAccessError with clear message when accessing configurable fields - Added _get_raw_config() for internal use only - Forces developers to use resolve_full_config(bank_id, context) for bank settings - Added resolve_full_config() method that returns complete HindsightConfig - Resolves hierarchy: Global (env) → Tenant → Bank - No caching to support multi-server deployments (always fresh from DB) - LLM provider pooling handles expensive operations separately - Updated entire retain pipeline to pass resolved config through call chain - memory_engine.py: Resolves config at top level where bank_id/context available - orchestrator.py: Accepts and passes config to fact_extraction - fact_extraction.py: Uses passed config instead of get_config() - utils.py: Added optional config param for backward compatibility - consolidator.py: Uses resolve_full_config() for enable_observations check - memory_engine.py: Resolves config before triggering consolidation - Renamed "Memory Bank" to "Bank Configuration" with tabs - Combined Stats and Operations into "General" tab - Consolidated Profile and Configuration into "Configuration" tab - Moved Actions dropdown to page level (outside tabs) - Created new component for managing bank-specific config - Displays configurable fields: retain_chunk_size, retain_extraction_mode, etc. - Edit via dialog with form validation - Reset to defaults via AlertDialog confirmation - Shows field IDs in monospace for clarity - Visual separation with borders and hover effects - Removed inline edit mode, switched to dialog-based editing - Separate dialogs for Disposition and Mission editing - Read-only display with clear edit buttons - Removed duplicate stats cards and operations - bank-stats-view.tsx: Overview statistics (memories, links, documents, pending ops) - bank-operations-view.tsx: Background operations table with filtering **Problem**: Consolidation always used global enable_observations, ignoring bank overrides **Root Cause**: consolidator.py called get_config() instead of resolving bank-specific config **Solution**: Pass resolved config through the entire pipeline **Problem**: asyncpg returning JSONB as JSON string instead of parsed dict **Solution**: Explicit JSON parsing in config_resolver.py with type checking - All 19 API integration tests pass - All 10 hierarchical config tests pass - Retain operations work correctly with bank-specific config - Consolidation respects bank-specific enable_observations setting - Updated developer/configuration.md with type-safe config access pattern - Added examples showing correct usage patterns - Documented ConfigFieldAccessError and resolution methods - get_config() now returns StaticConfigProxy (blocks configurable field access) - Code accessing bank-configurable fields must use resolve_full_config() - Clear migration path with helpful error messages Fixes hierarchical configuration to be production-ready with proper type safety. * refactor: remove LLM client pool and simplify config resolver Since LLM config (provider, model, api_key) is now static and not bank-configurable, the LLMClientPool is no longer needed. Changes: - Remove hindsight_api/llm_client_pool.py (no longer needed) - Remove memory_engine._get_bank_llm_config() (dead code, never called) - Simplify config_resolver.py by eliminating duplication between resolve_full_config() and get_bank_config() - get_bank_config() now calls resolve_full_config() and filters results - Remove outdated "LLM provider pooling" comments from docstrings All tests pass (10 hierarchical config tests, 19 API integration tests) * fix: update tests to use _get_raw_config() for configurable fields Fixed test fixtures that were accessing configurable fields (like enable_observations) from get_config(), which now raises ConfigFieldAccessError due to type-safe config access. Changes: - test_consolidation.py: Changed enable_observations fixture to use _get_raw_config() instead of get_config() - test_consolidation.py: Updated test_consolidation_returns_disabled_status to set bank config instead of mocking get_config() - test_link_expansion_retrieval.py: Changed fixture to use _get_raw_config() - test_observations.py: Changed disable_observations fixture to use _get_raw_config() - Regenerated OpenAPI spec and clients All 39 previously failing tests now pass. * fix: add missing config parameter to test calls of extract_facts_from_text() Fixed 45 test failures where tests were calling extract_facts_from_text() without the new required config parameter. Changes: - Added config=_get_raw_config() to all extract_facts_from_text() calls - Fixed test_main_module.py to patch _get_raw_config instead of get_config - Updated 6 test files with 37 function call sites All tests should now pass. * fix: add missing config parameter to test_skip_podcast_meta_commentary One more test was missing the config parameter for extract_facts_from_text().
215 lines
9.1 KiB
Rust
215 lines
9.1 KiB
Rust
use colored::*;
|
|
|
|
pub fn handle_api_error(err: anyhow::Error, api_url: &str) -> ! {
|
|
eprintln!("{}", format_error_message(&err, api_url));
|
|
std::process::exit(1);
|
|
}
|
|
|
|
fn format_error_message(err: &anyhow::Error, api_url: &str) -> String {
|
|
let err_str = err.to_string();
|
|
|
|
// Connection refused
|
|
if err_str.contains("Connection refused") || err_str.contains("tcp connect error") || err_str.contains("error sending request") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n • {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"Cannot connect to Hindsight API".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"The Hindsight API server is not running".bright_white(),
|
|
format!("The server is running on a different address than {}", api_url).bright_white(),
|
|
"A firewall is blocking the connection".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Start the Hindsight API server and ensure it's accessible".bright_white()
|
|
);
|
|
}
|
|
|
|
// Timeout
|
|
if err_str.contains("timeout") || err_str.contains("Timeout") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n • {}\n • {}",
|
|
"✗".bright_red().bold(),
|
|
"Request timed out".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"The API server is slow to respond".bright_white(),
|
|
"Network latency is too high".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Check if the API server is healthy".bright_white(),
|
|
"Try again with a better network connection".bright_white()
|
|
);
|
|
}
|
|
|
|
// DNS/Host resolution
|
|
if err_str.contains("dns") || err_str.contains("DNS") || err_str.contains("failed to lookup") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"Cannot resolve API hostname".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"The hostname in the API URL is incorrect".bright_white(),
|
|
"DNS server is not responding".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Check the HINDSIGHT_API_URL environment variable".bright_white()
|
|
);
|
|
}
|
|
|
|
// 404 Not Found - check for disabled features first
|
|
if err_str.contains("404") {
|
|
if err_str.contains("Bank configuration API is disabled") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"Bank configuration API is disabled".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"This feature is disabled by default for security.".bright_yellow(),
|
|
"To enable, set HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true on the API server".bright_white(),
|
|
"Note:".bright_cyan(),
|
|
"This allows per-bank LLM configuration overrides via API".bright_white()
|
|
);
|
|
}
|
|
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"API endpoint not found (404)".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"The API endpoint path has changed".bright_white(),
|
|
"You're using an incompatible API version".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Check that you're using the correct Hindsight API version".bright_white()
|
|
);
|
|
}
|
|
|
|
// 401 Authentication failed
|
|
if err_str.contains("401") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"Authentication failed".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"API requires authentication".bright_white(),
|
|
"Invalid or missing credentials".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Check if the API requires an API key or token".bright_white()
|
|
);
|
|
}
|
|
|
|
// 403 Forbidden
|
|
if err_str.contains("403") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"Permission denied (403)".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"This operation is not allowed".bright_white(),
|
|
"The feature may be disabled on the server".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Check server configuration or contact your administrator".bright_white()
|
|
);
|
|
}
|
|
|
|
// 500 Server Error
|
|
if err_str.contains("500") || err_str.contains("502") || err_str.contains("503") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n • {}\n • {}",
|
|
"✗".bright_red().bold(),
|
|
"API server error".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"The server encountered an error:".bright_yellow(),
|
|
"Internal server error (500)".bright_white(),
|
|
"Service temporarily unavailable".bright_white(),
|
|
"Try:".bright_green(),
|
|
"Check the API server logs for details".bright_white(),
|
|
"Try again in a few moments".bright_white()
|
|
);
|
|
}
|
|
|
|
// Invalid URL
|
|
if err_str.contains("invalid URL") || err_str.contains("InvalidUri") {
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n {}\n\n{}\n {}",
|
|
"✗".bright_red().bold(),
|
|
"Invalid API URL".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"The API URL format is invalid.".bright_yellow(),
|
|
"Ensure it starts with http:// or https://".bright_white(),
|
|
"Example:".bright_green(),
|
|
"export HINDSIGHT_API_URL=http://localhost:8888".bright_white()
|
|
);
|
|
}
|
|
|
|
// JSON parsing error - show actual response
|
|
if err_str.contains("Failed to parse") || err_str.contains("error decoding") {
|
|
// Extract the actual response if available
|
|
let response_hint = if err_str.contains("Response was:") {
|
|
let parts: Vec<&str> = err_str.split("Response was:").collect();
|
|
if parts.len() > 1 {
|
|
format!("\n{}\n{}", "Actual response:".bright_yellow(), parts[1].trim().bright_white())
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
return format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n • {}{}\n\n{}\n • {}\n • {}",
|
|
"✗".bright_red().bold(),
|
|
"Invalid API response format".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Possible causes:".bright_yellow(),
|
|
"The API returned an unexpected response format".bright_white(),
|
|
"Version mismatch between CLI and API".bright_white(),
|
|
"The API endpoint doesn't exist or returned HTML instead of JSON".bright_white(),
|
|
response_hint,
|
|
"Try:".bright_green(),
|
|
"Run with --verbose flag to see the full request/response".bright_white(),
|
|
"Ensure you're using a compatible Hindsight API version".bright_white()
|
|
);
|
|
}
|
|
|
|
// Generic error with the full error message
|
|
format!(
|
|
"{} {}\n\n{}\n {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n • {}",
|
|
"✗".bright_red().bold(),
|
|
"API request failed".bright_red().bold(),
|
|
"API URL:".bright_yellow(),
|
|
api_url.bright_white(),
|
|
"Error:".bright_yellow(),
|
|
err_str.bright_white(),
|
|
"Suggestions:".bright_green(),
|
|
"Check that HINDSIGHT_API_URL is set correctly".bright_white(),
|
|
"Ensure the Hindsight API server is running".bright_white(),
|
|
"Verify network connectivity to the API server".bright_white()
|
|
)
|
|
}
|
|
|
|
pub fn print_config_help() {
|
|
println!("\n{}", "Configuration:".bright_cyan().bold());
|
|
println!(" Run the configure command to set the API URL:");
|
|
println!(" {}", "hindsight configure".bright_white());
|
|
println!();
|
|
println!(" Or set it directly:");
|
|
println!(" {}", "hindsight configure --api-url http://your-api:8888".bright_white());
|
|
println!();
|
|
println!(" {}", "Configuration priority:".bright_yellow());
|
|
println!(" 1. Environment variable (HINDSIGHT_API_URL) - highest priority");
|
|
println!(" 2. Config file (~/.hindsight/config)");
|
|
println!(" 3. Default (http://localhost:8888)");
|
|
println!();
|
|
}
|