Phase 5: AI provider system with local and cloud support
- Implement AIProvider base interface with chat() and is_available()
- Add LocalProvider connecting to bundled llama-server via OpenAI SDK
- Add OpenAIProvider for direct OpenAI API access
- Add AnthropicProvider for Anthropic Claude API
- Add LiteLLMProvider for multi-provider gateway
- Build AIProviderService with provider routing, auto-selection,
and transcript context injection
- Add ai.chat IPC handler supporting chat, list_providers, set_provider,
and configure actions
- Add ai_chat, ai_list_providers, ai_configure Tauri commands
- Build interactive AIChatPanel with message history, quick actions
(Summarize, Action Items), and transcript context awareness
- Tests: 30 Python, 6 Rust, 0 Svelte errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:25:10 -08:00
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
|
|
|
|
|
use crate::sidecar::messages::IPCMessage;
|
|
|
|
|
use crate::sidecar::SidecarManager;
|
|
|
|
|
|
|
|
|
|
fn get_sidecar() -> Result<SidecarManager, String> {
|
|
|
|
|
let python_path = std::env::current_dir()
|
|
|
|
|
.map_err(|e| e.to_string())?
|
|
|
|
|
.join("../python")
|
|
|
|
|
.canonicalize()
|
|
|
|
|
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
|
|
|
|
|
|
|
|
|
let manager = SidecarManager::new();
|
|
|
|
|
manager.start(&python_path.to_string_lossy())?;
|
|
|
|
|
Ok(manager)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Send a chat message to the AI provider via the Python sidecar.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn ai_chat(
|
|
|
|
|
messages: Value,
|
|
|
|
|
transcript_context: Option<String>,
|
|
|
|
|
provider: Option<String>,
|
|
|
|
|
) -> Result<Value, String> {
|
|
|
|
|
let manager = get_sidecar()?;
|
|
|
|
|
|
|
|
|
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
|
|
|
let mut payload = json!({
|
|
|
|
|
"action": "chat",
|
|
|
|
|
"messages": messages,
|
|
|
|
|
"transcript_context": transcript_context.unwrap_or_default(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If a specific provider is requested, set it first
|
|
|
|
|
if let Some(p) = provider {
|
|
|
|
|
let set_msg = IPCMessage::new(
|
|
|
|
|
&uuid::Uuid::new_v4().to_string(),
|
|
|
|
|
"ai.chat",
|
|
|
|
|
json!({ "action": "set_provider", "provider": p }),
|
|
|
|
|
);
|
|
|
|
|
let _ = manager.send_and_receive(&set_msg)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let msg = IPCMessage::new(&request_id, "ai.chat", payload);
|
|
|
|
|
let response = manager.send_and_receive(&msg)?;
|
|
|
|
|
|
|
|
|
|
if response.msg_type == "error" {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"AI error: {}",
|
|
|
|
|
response.payload.get("message").and_then(|v| v.as_str()).unwrap_or("unknown")
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(response.payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List available AI providers.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn ai_list_providers() -> Result<Value, String> {
|
|
|
|
|
let manager = get_sidecar()?;
|
|
|
|
|
|
|
|
|
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
|
|
|
let msg = IPCMessage::new(
|
|
|
|
|
&request_id,
|
|
|
|
|
"ai.chat",
|
|
|
|
|
json!({ "action": "list_providers" }),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = manager.send_and_receive(&msg)?;
|
|
|
|
|
Ok(response.payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Configure an AI provider with API key/settings.
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn ai_configure(provider: String, config: Value) -> Result<Value, String> {
|
|
|
|
|
let manager = get_sidecar()?;
|
|
|
|
|
|
|
|
|
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
|
|
|
let msg = IPCMessage::new(
|
|
|
|
|
&request_id,
|
|
|
|
|
"ai.chat",
|
|
|
|
|
json!({
|
|
|
|
|
"action": "configure",
|
|
|
|
|
"provider": provider,
|
|
|
|
|
"config": config,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = manager.send_and_receive(&msg)?;
|
|
|
|
|
Ok(response.payload)
|
|
|
|
|
}
|