refactor(desktop): split kernel_commands/pipeline_commands into modules, add SaaS client libs and gateway modules
Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines) into focused sub-modules under kernel_commands/ and pipeline_commands/ directories. Add gateway module (commands, config, io, runtime), health_check, and 15 new TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel sub-systems (a2a, agent, chat, hands, skills, triggers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
210
desktop/src-tauri/src/pipeline_commands/adapters.rs
Normal file
210
desktop/src-tauri/src/pipeline_commands/adapters.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
//! Adapter structs to bridge zclaw-runtime/zclaw-kernel drivers into zclaw-pipeline action drivers.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use zclaw_runtime::{LlmDriver, CompletionRequest};
|
||||
use zclaw_skills::SkillContext;
|
||||
|
||||
use zclaw_pipeline::{
|
||||
LlmActionDriver,
|
||||
SkillActionDriver,
|
||||
HandActionDriver,
|
||||
};
|
||||
|
||||
use crate::kernel_commands::KernelState;
|
||||
|
||||
/// Adapter to connect zclaw-runtime LlmDriver to zclaw-pipeline LlmActionDriver
|
||||
pub struct RuntimeLlmAdapter {
|
||||
driver: Arc<dyn LlmDriver>,
|
||||
default_model: String,
|
||||
}
|
||||
|
||||
impl RuntimeLlmAdapter {
|
||||
pub fn new(driver: Arc<dyn LlmDriver>, default_model: Option<String>) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
default_model: default_model.unwrap_or_else(|| "claude-3-sonnet-20240229".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmActionDriver for RuntimeLlmAdapter {
|
||||
async fn generate(
|
||||
&self,
|
||||
prompt: String,
|
||||
input: HashMap<String, Value>,
|
||||
model: Option<String>,
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<u32>,
|
||||
json_mode: bool,
|
||||
) -> Result<Value, String> {
|
||||
tracing::debug!("[RuntimeLlmAdapter] generate called with prompt length: {}", prompt.len());
|
||||
tracing::debug!("[RuntimeLlmAdapter] input HashMap contents:");
|
||||
for (k, v) in &input {
|
||||
println!(" {} => {}", k, v);
|
||||
}
|
||||
|
||||
// Build user content from prompt and input
|
||||
let user_content = if input.is_empty() {
|
||||
tracing::debug!("[RuntimeLlmAdapter] WARNING: input is empty, using raw prompt");
|
||||
prompt.clone()
|
||||
} else {
|
||||
// Inject input values into prompt
|
||||
// Support multiple placeholder formats: {{key}}, {{ key }}, ${key}, ${inputs.key}
|
||||
let mut rendered = prompt.clone();
|
||||
tracing::debug!("[RuntimeLlmAdapter] Original prompt (first 500 chars): {}", &prompt[..prompt.len().min(500)]);
|
||||
for (key, value) in &input {
|
||||
let str_value = if let Some(s) = value.as_str() {
|
||||
s.to_string()
|
||||
} else {
|
||||
value.to_string()
|
||||
};
|
||||
|
||||
tracing::debug!("[RuntimeLlmAdapter] Replacing '{}' with '{}'", key, str_value);
|
||||
|
||||
// Replace all common placeholder formats
|
||||
rendered = rendered.replace(&format!("{{{{{key}}}}}"), &str_value); // {{key}}
|
||||
rendered = rendered.replace(&format!("{{{{ {key} }}}}"), &str_value); // {{ key }}
|
||||
rendered = rendered.replace(&format!("${{{key}}}"), &str_value); // ${key}
|
||||
rendered = rendered.replace(&format!("${{inputs.{key}}}"), &str_value); // ${inputs.key}
|
||||
}
|
||||
tracing::debug!("[RuntimeLlmAdapter] Rendered prompt (first 500 chars): {}", &rendered[..rendered.len().min(500)]);
|
||||
rendered
|
||||
};
|
||||
|
||||
// Create message using zclaw_types::Message enum
|
||||
let messages = vec![zclaw_types::Message::user(user_content)];
|
||||
|
||||
let request = CompletionRequest {
|
||||
model: model.unwrap_or_else(|| self.default_model.clone()),
|
||||
system: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
max_tokens,
|
||||
temperature,
|
||||
stop: Vec::new(),
|
||||
stream: false,
|
||||
};
|
||||
|
||||
let response = self.driver.complete(request)
|
||||
.await
|
||||
.map_err(|e| format!("LLM completion failed: {}", e))?;
|
||||
|
||||
// Extract text from response
|
||||
let text = response.content.iter()
|
||||
.find_map(|block| match block {
|
||||
zclaw_runtime::ContentBlock::Text { text } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Safe truncation for UTF-8 strings
|
||||
let truncated: String = text.chars().take(1000).collect();
|
||||
tracing::debug!("[RuntimeLlmAdapter] LLM response text (first 1000 chars): {}", truncated);
|
||||
|
||||
// Parse as JSON if json_mode, otherwise return as string
|
||||
if json_mode {
|
||||
// Try to extract JSON from the response (LLM might wrap it in markdown code blocks)
|
||||
let json_text = if text.contains("```json") {
|
||||
// Extract JSON from markdown code block
|
||||
let start = text.find("```json").map(|i| i + 7).unwrap_or(0);
|
||||
let end = text.rfind("```").unwrap_or(text.len());
|
||||
text[start..end].trim().to_string()
|
||||
} else if text.contains("```") {
|
||||
// Extract from generic code block
|
||||
let start = text.find("```").map(|i| i + 3).unwrap_or(0);
|
||||
let end = text.rfind("```").unwrap_or(text.len());
|
||||
text[start..end].trim().to_string()
|
||||
} else {
|
||||
text.clone()
|
||||
};
|
||||
|
||||
// Safe truncation for UTF-8 strings
|
||||
let truncated_json: String = json_text.chars().take(500).collect();
|
||||
tracing::debug!("[RuntimeLlmAdapter] JSON text to parse (first 500 chars): {}", truncated_json);
|
||||
|
||||
serde_json::from_str(&json_text)
|
||||
.map_err(|e| {
|
||||
tracing::debug!("[RuntimeLlmAdapter] JSON parse error: {}", e);
|
||||
format!("Failed to parse LLM response as JSON: {}\nResponse: {}", e, json_text)
|
||||
})
|
||||
} else {
|
||||
Ok(Value::String(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter to bridge Kernel skill execution into Pipeline SkillActionDriver
|
||||
pub struct PipelineSkillDriver {
|
||||
kernel_state: KernelState,
|
||||
}
|
||||
|
||||
impl PipelineSkillDriver {
|
||||
pub fn new(kernel_state: KernelState) -> Self {
|
||||
Self { kernel_state }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SkillActionDriver for PipelineSkillDriver {
|
||||
async fn execute(
|
||||
&self,
|
||||
skill_id: &str,
|
||||
input: HashMap<String, Value>,
|
||||
) -> Result<Value, String> {
|
||||
let kernel_lock = self.kernel_state.lock().await;
|
||||
let kernel = kernel_lock.as_ref()
|
||||
.ok_or_else(|| "Kernel 未初始化,无法执行技能".to_string())?;
|
||||
|
||||
let context = SkillContext::default();
|
||||
let input_value = Value::Object(input.into_iter().collect());
|
||||
|
||||
tracing::debug!("[PipelineSkillDriver] Executing skill: {}", skill_id);
|
||||
let result = kernel.execute_skill(skill_id, context, input_value).await
|
||||
.map_err(|e| format!("技能执行失败: {}", e))?;
|
||||
|
||||
Ok(result.output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter to bridge Kernel hand execution into Pipeline HandActionDriver
|
||||
pub struct PipelineHandDriver {
|
||||
kernel_state: KernelState,
|
||||
}
|
||||
|
||||
impl PipelineHandDriver {
|
||||
pub fn new(kernel_state: KernelState) -> Self {
|
||||
Self { kernel_state }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HandActionDriver for PipelineHandDriver {
|
||||
async fn execute(
|
||||
&self,
|
||||
hand_id: &str,
|
||||
action: &str,
|
||||
params: HashMap<String, Value>,
|
||||
) -> Result<Value, String> {
|
||||
let kernel_lock = self.kernel_state.lock().await;
|
||||
let kernel = kernel_lock.as_ref()
|
||||
.ok_or_else(|| "Kernel 未初始化,无法执行 Hand".to_string())?;
|
||||
|
||||
// Build hand input combining action and params
|
||||
let mut input_map = serde_json::Map::new();
|
||||
input_map.insert("action".to_string(), Value::String(action.to_string()));
|
||||
for (k, v) in params {
|
||||
input_map.insert(k, v);
|
||||
}
|
||||
let input_value = Value::Object(input_map);
|
||||
|
||||
tracing::debug!("[PipelineHandDriver] Executing hand: {} / {}", hand_id, action);
|
||||
let (result, _run_id) = kernel.execute_hand(hand_id, input_value).await
|
||||
.map_err(|e| format!("Hand 执行失败: {}", e))?;
|
||||
|
||||
Ok(result.output)
|
||||
}
|
||||
}
|
||||
230
desktop/src-tauri/src/pipeline_commands/crud.rs
Normal file
230
desktop/src-tauri/src/pipeline_commands/crud.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
//! Pipeline CRUD commands (Create / Update / Delete).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use zclaw_pipeline::{
|
||||
Pipeline,
|
||||
PipelineMetadata,
|
||||
PipelineSpec,
|
||||
PipelineStep,
|
||||
Action,
|
||||
ErrorStrategy,
|
||||
};
|
||||
|
||||
use super::{PipelineState, PipelineInfo};
|
||||
use super::helpers::{get_pipelines_directory, pipeline_to_info};
|
||||
|
||||
/// Create pipeline request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePipelineRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub steps: Vec<WorkflowStepInput>,
|
||||
}
|
||||
|
||||
/// Update pipeline request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdatePipelineRequest {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub steps: Option<Vec<WorkflowStepInput>>,
|
||||
}
|
||||
|
||||
/// Workflow step input from frontend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkflowStepInput {
|
||||
pub hand_name: String,
|
||||
pub name: Option<String>,
|
||||
pub params: Option<HashMap<String, Value>>,
|
||||
pub condition: Option<String>,
|
||||
}
|
||||
|
||||
/// Create a new pipeline as a YAML file
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_create(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
request: CreatePipelineRequest,
|
||||
) -> Result<PipelineInfo, String> {
|
||||
let name = request.name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
return Err("Pipeline name cannot be empty".to_string());
|
||||
}
|
||||
|
||||
let pipelines_dir = get_pipelines_directory()?;
|
||||
if !pipelines_dir.exists() {
|
||||
std::fs::create_dir_all(&pipelines_dir)
|
||||
.map_err(|e| format!("Failed to create pipelines directory: {}", e))?;
|
||||
}
|
||||
|
||||
// Generate pipeline ID from name
|
||||
let pipeline_id = name.to_lowercase()
|
||||
.replace(' ', "-")
|
||||
.replace(|c: char| !c.is_alphanumeric() && c != '-', "");
|
||||
|
||||
let file_path = pipelines_dir.join(format!("{}.yaml", pipeline_id));
|
||||
if file_path.exists() {
|
||||
return Err(format!("Pipeline file already exists: {}", file_path.display()));
|
||||
}
|
||||
|
||||
// Build Pipeline struct
|
||||
let steps: Vec<PipelineStep> = request.steps.into_iter().enumerate().map(|(i, s)| {
|
||||
let step_id = s.name.clone().unwrap_or_else(|| format!("step-{}", i + 1));
|
||||
PipelineStep {
|
||||
id: step_id,
|
||||
action: Action::Hand {
|
||||
hand_id: s.hand_name.clone(),
|
||||
hand_action: "execute".to_string(),
|
||||
params: s.params.unwrap_or_default().into_iter().map(|(k, v)| (k, v.to_string())).collect(),
|
||||
},
|
||||
description: s.name,
|
||||
when: s.condition,
|
||||
retry: None,
|
||||
timeout_secs: None,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let pipeline = Pipeline {
|
||||
api_version: "zclaw/v1".to_string(),
|
||||
kind: "Pipeline".to_string(),
|
||||
metadata: PipelineMetadata {
|
||||
name: pipeline_id.clone(),
|
||||
display_name: Some(name),
|
||||
description: request.description,
|
||||
category: None,
|
||||
industry: None,
|
||||
tags: vec![],
|
||||
icon: None,
|
||||
author: None,
|
||||
version: "1.0.0".to_string(),
|
||||
annotations: None,
|
||||
},
|
||||
spec: PipelineSpec {
|
||||
inputs: vec![],
|
||||
steps,
|
||||
outputs: HashMap::new(),
|
||||
on_error: ErrorStrategy::Stop,
|
||||
timeout_secs: 0,
|
||||
max_workers: 4,
|
||||
},
|
||||
};
|
||||
|
||||
// Serialize to YAML
|
||||
let yaml_content = serde_yaml::to_string(&pipeline)
|
||||
.map_err(|e| format!("Failed to serialize pipeline: {}", e))?;
|
||||
|
||||
std::fs::write(&file_path, yaml_content)
|
||||
.map_err(|e| format!("Failed to write pipeline file: {}", e))?;
|
||||
|
||||
// Register in state
|
||||
let mut state_pipelines = state.pipelines.write().await;
|
||||
let mut state_paths = state.pipeline_paths.write().await;
|
||||
state_pipelines.insert(pipeline_id.clone(), pipeline.clone());
|
||||
state_paths.insert(pipeline_id, file_path);
|
||||
|
||||
Ok(pipeline_to_info(&pipeline))
|
||||
}
|
||||
|
||||
/// Update an existing pipeline
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_update(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
pipeline_id: String,
|
||||
request: UpdatePipelineRequest,
|
||||
) -> Result<PipelineInfo, String> {
|
||||
let pipelines = state.pipelines.read().await;
|
||||
let paths = state.pipeline_paths.read().await;
|
||||
|
||||
let existing = pipelines.get(&pipeline_id)
|
||||
.ok_or_else(|| format!("Pipeline not found: {}", pipeline_id))?;
|
||||
let file_path = paths.get(&pipeline_id)
|
||||
.ok_or_else(|| format!("Pipeline file path not found: {}", pipeline_id))?
|
||||
.clone();
|
||||
|
||||
// Build updated pipeline
|
||||
let updated_metadata = PipelineMetadata {
|
||||
display_name: request.name.or(existing.metadata.display_name.clone()),
|
||||
description: request.description.or(existing.metadata.description.clone()),
|
||||
..existing.metadata.clone()
|
||||
};
|
||||
|
||||
let updated_steps = match request.steps {
|
||||
Some(steps) => steps.into_iter().enumerate().map(|(i, s)| {
|
||||
let step_id = s.name.clone().unwrap_or_else(|| format!("step-{}", i + 1));
|
||||
PipelineStep {
|
||||
id: step_id,
|
||||
action: Action::Hand {
|
||||
hand_id: s.hand_name.clone(),
|
||||
hand_action: "execute".to_string(),
|
||||
params: s.params.unwrap_or_default().into_iter().map(|(k, v)| (k, v.to_string())).collect(),
|
||||
},
|
||||
description: s.name,
|
||||
when: s.condition,
|
||||
retry: None,
|
||||
timeout_secs: None,
|
||||
}
|
||||
}).collect(),
|
||||
None => existing.spec.steps.clone(),
|
||||
};
|
||||
|
||||
let updated_pipeline = Pipeline {
|
||||
metadata: updated_metadata,
|
||||
spec: PipelineSpec {
|
||||
steps: updated_steps,
|
||||
..existing.spec.clone()
|
||||
},
|
||||
..existing.clone()
|
||||
};
|
||||
|
||||
// Write to file
|
||||
let yaml_content = serde_yaml::to_string(&updated_pipeline)
|
||||
.map_err(|e| format!("Failed to serialize pipeline: {}", e))?;
|
||||
|
||||
// Drop read locks before write
|
||||
drop(pipelines);
|
||||
drop(paths);
|
||||
|
||||
std::fs::write(file_path, yaml_content)
|
||||
.map_err(|e| format!("Failed to write pipeline file: {}", e))?;
|
||||
|
||||
// Update state
|
||||
let mut state_pipelines = state.pipelines.write().await;
|
||||
state_pipelines.insert(pipeline_id.clone(), updated_pipeline.clone());
|
||||
|
||||
Ok(pipeline_to_info(&updated_pipeline))
|
||||
}
|
||||
|
||||
/// Delete a pipeline
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_delete(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
pipeline_id: String,
|
||||
) -> Result<(), String> {
|
||||
let paths = state.pipeline_paths.read().await;
|
||||
|
||||
let file_path = paths.get(&pipeline_id)
|
||||
.ok_or_else(|| format!("Pipeline not found: {}", pipeline_id))?;
|
||||
|
||||
let path = file_path.clone();
|
||||
drop(paths);
|
||||
|
||||
// Remove file
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)
|
||||
.map_err(|e| format!("Failed to delete pipeline file: {}", e))?;
|
||||
}
|
||||
|
||||
// Remove from state
|
||||
let mut state_pipelines = state.pipelines.write().await;
|
||||
let mut state_paths = state.pipeline_paths.write().await;
|
||||
state_pipelines.remove(&pipeline_id);
|
||||
state_paths.remove(&pipeline_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
310
desktop/src-tauri/src/pipeline_commands/discovery.rs
Normal file
310
desktop/src-tauri/src/pipeline_commands/discovery.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
//! Pipeline discovery, listing, running, and monitoring commands.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
|
||||
use zclaw_pipeline::{
|
||||
RunStatus,
|
||||
parse_pipeline_yaml,
|
||||
PipelineExecutor,
|
||||
ActionRegistry,
|
||||
LlmActionDriver,
|
||||
SkillActionDriver,
|
||||
HandActionDriver,
|
||||
};
|
||||
|
||||
use super::{PipelineState, PipelineInfo, PipelineRunResponse, RunPipelineResponse, RunPipelineRequest};
|
||||
use super::adapters::{RuntimeLlmAdapter, PipelineSkillDriver, PipelineHandDriver};
|
||||
use super::helpers::{get_pipelines_directory, scan_pipelines_with_paths, scan_pipelines_full_sync, pipeline_to_info};
|
||||
|
||||
use crate::kernel_commands::KernelState;
|
||||
|
||||
/// Discover and list all available pipelines
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_list(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
category: Option<String>,
|
||||
industry: Option<String>,
|
||||
) -> Result<Vec<PipelineInfo>, String> {
|
||||
// Get pipelines directory
|
||||
let pipelines_dir = get_pipelines_directory()?;
|
||||
|
||||
tracing::debug!("[pipeline_list] Scanning directory: {:?}", pipelines_dir);
|
||||
tracing::debug!("[pipeline_list] Filters - category: {:?}, industry: {:?}", category, industry);
|
||||
|
||||
// Scan for pipeline files (returns both info and paths)
|
||||
let mut pipelines_with_paths: Vec<(PipelineInfo, std::path::PathBuf)> = Vec::new();
|
||||
if pipelines_dir.exists() {
|
||||
scan_pipelines_with_paths(&pipelines_dir, category.as_deref(), industry.as_deref(), &mut pipelines_with_paths)?;
|
||||
} else {
|
||||
tracing::warn!("[WARN pipeline_list] Pipelines directory does not exist: {:?}", pipelines_dir);
|
||||
}
|
||||
|
||||
tracing::debug!("[pipeline_list] Found {} pipelines", pipelines_with_paths.len());
|
||||
|
||||
// Debug: log all pipelines with their industry values
|
||||
for (info, _) in &pipelines_with_paths {
|
||||
tracing::debug!("[pipeline_list] Pipeline: {} -> category: {}, industry: '{}'", info.id, info.category, info.industry);
|
||||
}
|
||||
|
||||
// Update state
|
||||
let mut state_pipelines = state.pipelines.write().await;
|
||||
let mut state_paths = state.pipeline_paths.write().await;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for (info, path) in &pipelines_with_paths {
|
||||
// Load full pipeline into state
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
if let Ok(pipeline) = parse_pipeline_yaml(&content) {
|
||||
state_pipelines.insert(info.id.clone(), pipeline);
|
||||
state_paths.insert(info.id.clone(), path.clone());
|
||||
}
|
||||
}
|
||||
result.push(info.clone());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get pipeline details
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_get(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
pipeline_id: String,
|
||||
) -> Result<PipelineInfo, String> {
|
||||
let pipelines = state.pipelines.read().await;
|
||||
|
||||
let pipeline = pipelines.get(&pipeline_id)
|
||||
.ok_or_else(|| format!("Pipeline not found: {}", pipeline_id))?;
|
||||
|
||||
Ok(pipeline_to_info(pipeline))
|
||||
}
|
||||
|
||||
/// Run a pipeline
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_run(
|
||||
app: AppHandle,
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
kernel_state: State<'_, KernelState>,
|
||||
request: RunPipelineRequest,
|
||||
) -> Result<RunPipelineResponse, String> {
|
||||
tracing::debug!("[pipeline_run] Received request for pipeline_id: {}", request.pipeline_id);
|
||||
|
||||
// Get pipeline
|
||||
let pipelines = state.pipelines.read().await;
|
||||
tracing::debug!("[pipeline_run] State has {} pipelines loaded", pipelines.len());
|
||||
|
||||
// Debug: list all loaded pipeline IDs
|
||||
for (id, _) in pipelines.iter() {
|
||||
tracing::debug!("[pipeline_run] Loaded pipeline: {}", id);
|
||||
}
|
||||
|
||||
let pipeline = pipelines.get(&request.pipeline_id)
|
||||
.ok_or_else(|| {
|
||||
println!("[ERROR pipeline_run] Pipeline '{}' not found in state. Available: {:?}",
|
||||
request.pipeline_id,
|
||||
pipelines.keys().collect::<Vec<_>>());
|
||||
format!("Pipeline not found: {}", request.pipeline_id)
|
||||
})?
|
||||
.clone();
|
||||
drop(pipelines);
|
||||
|
||||
// Try to get LLM driver from Kernel
|
||||
let (llm_driver, skill_driver, hand_driver) = {
|
||||
let kernel_lock = kernel_state.lock().await;
|
||||
if let Some(kernel) = kernel_lock.as_ref() {
|
||||
tracing::debug!("[pipeline_run] Got LLM driver from Kernel");
|
||||
let llm = Some(Arc::new(RuntimeLlmAdapter::new(
|
||||
kernel.driver(),
|
||||
Some(kernel.config().llm.model.clone()),
|
||||
)) as Arc<dyn LlmActionDriver>);
|
||||
let kernel_arc = (*kernel_state).clone();
|
||||
let skill = Some(Arc::new(PipelineSkillDriver::new(kernel_arc.clone()))
|
||||
as Arc<dyn SkillActionDriver>);
|
||||
let hand = Some(Arc::new(PipelineHandDriver::new(kernel_arc))
|
||||
as Arc<dyn HandActionDriver>);
|
||||
(llm, skill, hand)
|
||||
} else {
|
||||
tracing::debug!("[pipeline_run] Kernel not initialized, no drivers available");
|
||||
(None, None, None)
|
||||
}
|
||||
};
|
||||
|
||||
// Create executor with all available drivers
|
||||
let executor = if let Some(driver) = llm_driver {
|
||||
let mut registry = ActionRegistry::new().with_llm_driver(driver);
|
||||
if let Some(skill) = skill_driver {
|
||||
registry = registry.with_skill_registry(skill);
|
||||
}
|
||||
if let Some(hand) = hand_driver {
|
||||
registry = registry.with_hand_registry(hand);
|
||||
}
|
||||
Arc::new(PipelineExecutor::new(Arc::new(registry)))
|
||||
} else {
|
||||
state.executor.clone()
|
||||
};
|
||||
|
||||
// Generate run ID upfront so we can return it to the caller
|
||||
let run_id = uuid::Uuid::new_v4().to_string();
|
||||
let pipeline_id = request.pipeline_id.clone();
|
||||
let inputs = request.inputs.clone();
|
||||
|
||||
// Clone for async task
|
||||
let run_id_for_spawn = run_id.clone();
|
||||
|
||||
// Run pipeline in background with the known run_id
|
||||
tokio::spawn(async move {
|
||||
tracing::debug!("[pipeline_run] Starting execution with run_id: {}", run_id_for_spawn);
|
||||
let result = executor.execute_with_id(&pipeline, inputs, &run_id_for_spawn).await;
|
||||
|
||||
tracing::debug!("[pipeline_run] Execution completed for run_id: {}, status: {:?}",
|
||||
run_id_for_spawn,
|
||||
result.as_ref().map(|r| r.status.clone()).unwrap_or(RunStatus::Failed));
|
||||
|
||||
// Emit completion event
|
||||
let _ = app.emit("pipeline-complete", &PipelineRunResponse {
|
||||
run_id: run_id_for_spawn.clone(),
|
||||
pipeline_id: pipeline_id.clone(),
|
||||
status: match &result {
|
||||
Ok(r) => r.status.to_string(),
|
||||
Err(_) => "failed".to_string(),
|
||||
},
|
||||
current_step: None,
|
||||
percentage: 100,
|
||||
message: match &result {
|
||||
Ok(_) => "Pipeline completed".to_string(),
|
||||
Err(e) => e.to_string(),
|
||||
},
|
||||
outputs: result.as_ref().ok().and_then(|r| r.outputs.clone()),
|
||||
error: result.as_ref().err().map(|e| e.to_string()),
|
||||
started_at: result.as_ref().map(|r| r.started_at.to_rfc3339()).unwrap_or_else(|_| chrono::Utc::now().to_rfc3339()),
|
||||
ended_at: result.as_ref().map(|r| r.ended_at.map(|t| t.to_rfc3339())).unwrap_or_else(|_| Some(chrono::Utc::now().to_rfc3339())),
|
||||
});
|
||||
});
|
||||
|
||||
// Return immediately with the known run ID
|
||||
tracing::debug!("[pipeline_run] Returning run_id: {} to caller", run_id);
|
||||
Ok(RunPipelineResponse {
|
||||
run_id,
|
||||
pipeline_id: request.pipeline_id,
|
||||
status: "running".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get pipeline run progress
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_progress(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
run_id: String,
|
||||
) -> Result<PipelineRunResponse, String> {
|
||||
let progress = state.executor.get_progress(&run_id).await
|
||||
.ok_or_else(|| format!("Run not found: {}", run_id))?;
|
||||
|
||||
let run = state.executor.get_run(&run_id).await;
|
||||
|
||||
Ok(PipelineRunResponse {
|
||||
run_id: progress.run_id,
|
||||
pipeline_id: run.as_ref().map(|r| r.pipeline_id.clone()).unwrap_or_default(),
|
||||
status: progress.status.to_string(),
|
||||
current_step: Some(progress.current_step),
|
||||
percentage: progress.percentage,
|
||||
message: progress.message,
|
||||
outputs: run.as_ref().and_then(|r| r.outputs.clone()),
|
||||
error: run.as_ref().and_then(|r| r.error.clone()),
|
||||
started_at: run.as_ref().map(|r| r.started_at.to_rfc3339()).unwrap_or_default(),
|
||||
ended_at: run.as_ref().and_then(|r| r.ended_at.map(|t| t.to_rfc3339())),
|
||||
})
|
||||
}
|
||||
|
||||
/// Cancel a pipeline run
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_cancel(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
run_id: String,
|
||||
) -> Result<(), String> {
|
||||
state.executor.cancel(&run_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get pipeline run result
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_result(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
run_id: String,
|
||||
) -> Result<PipelineRunResponse, String> {
|
||||
let run = state.executor.get_run(&run_id).await
|
||||
.ok_or_else(|| format!("Run not found: {}", run_id))?;
|
||||
|
||||
let current_step = run.current_step.clone();
|
||||
let status = run.status.clone();
|
||||
|
||||
Ok(PipelineRunResponse {
|
||||
run_id: run.id,
|
||||
pipeline_id: run.pipeline_id,
|
||||
status: status.to_string(),
|
||||
current_step: current_step.clone(),
|
||||
percentage: if status == RunStatus::Completed { 100 } else { 0 },
|
||||
message: current_step.unwrap_or_default(),
|
||||
outputs: run.outputs,
|
||||
error: run.error,
|
||||
started_at: run.started_at.to_rfc3339(),
|
||||
ended_at: run.ended_at.map(|t| t.to_rfc3339()),
|
||||
})
|
||||
}
|
||||
|
||||
/// List all runs
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_runs(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
) -> Result<Vec<PipelineRunResponse>, String> {
|
||||
let runs = state.executor.list_runs().await;
|
||||
|
||||
Ok(runs.into_iter().map(|run| {
|
||||
let current_step = run.current_step.clone();
|
||||
let status = run.status.clone();
|
||||
PipelineRunResponse {
|
||||
run_id: run.id,
|
||||
pipeline_id: run.pipeline_id,
|
||||
status: status.to_string(),
|
||||
current_step: current_step.clone(),
|
||||
percentage: if status == RunStatus::Completed { 100 } else if status == RunStatus::Running { 50 } else { 0 },
|
||||
message: current_step.unwrap_or_default(),
|
||||
outputs: run.outputs,
|
||||
error: run.error,
|
||||
started_at: run.started_at.to_rfc3339(),
|
||||
ended_at: run.ended_at.map(|t| t.to_rfc3339()),
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// Refresh pipeline discovery
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_refresh(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
) -> Result<Vec<PipelineInfo>, String> {
|
||||
let pipelines_dir = get_pipelines_directory()?;
|
||||
|
||||
if !pipelines_dir.exists() {
|
||||
std::fs::create_dir_all(&pipelines_dir)
|
||||
.map_err(|e| format!("Failed to create pipelines directory: {}", e))?;
|
||||
}
|
||||
|
||||
let mut state_pipelines = state.pipelines.write().await;
|
||||
let mut state_paths = state.pipeline_paths.write().await;
|
||||
|
||||
// Clear existing
|
||||
state_pipelines.clear();
|
||||
state_paths.clear();
|
||||
|
||||
// Scan and load all pipelines (synchronous)
|
||||
let mut pipelines = Vec::new();
|
||||
scan_pipelines_full_sync(&pipelines_dir, &mut pipelines)?;
|
||||
|
||||
for (path, pipeline) in &pipelines {
|
||||
let id = pipeline.metadata.name.clone();
|
||||
state_pipelines.insert(id.clone(), pipeline.clone());
|
||||
state_paths.insert(id, path.clone());
|
||||
}
|
||||
|
||||
Ok(pipelines.into_iter().map(|(_, p)| pipeline_to_info(&p)).collect())
|
||||
}
|
||||
167
desktop/src-tauri/src/pipeline_commands/helpers.rs
Normal file
167
desktop/src-tauri/src/pipeline_commands/helpers.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
//! Helper functions for Pipeline commands.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use zclaw_pipeline::{
|
||||
Pipeline,
|
||||
parse_pipeline_yaml,
|
||||
};
|
||||
|
||||
use super::types::{PipelineInfo, PipelineInputInfo};
|
||||
|
||||
pub(crate) fn get_pipelines_directory() -> Result<PathBuf, String> {
|
||||
// Try to find pipelines directory
|
||||
// Priority: ZCLAW_PIPELINES_DIR env > workspace pipelines/ > ~/.zclaw/pipelines/
|
||||
|
||||
if let Ok(dir) = std::env::var("ZCLAW_PIPELINES_DIR") {
|
||||
return Ok(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
// Try workspace directory
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace_pipelines = manifest_dir
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.join("pipelines"));
|
||||
|
||||
if let Some(ref dir) = workspace_pipelines {
|
||||
if dir.exists() {
|
||||
return Ok(dir.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to user home directory
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let dir = home.join(".zclaw").join("pipelines");
|
||||
return Ok(dir);
|
||||
}
|
||||
|
||||
Err("Could not determine pipelines directory".to_string())
|
||||
}
|
||||
|
||||
/// Scan pipelines with paths (returns both info and file paths)
|
||||
pub(crate) fn scan_pipelines_with_paths(
|
||||
dir: &PathBuf,
|
||||
category_filter: Option<&str>,
|
||||
industry_filter: Option<&str>,
|
||||
pipelines: &mut Vec<(PipelineInfo, PathBuf)>,
|
||||
) -> Result<(), String> {
|
||||
tracing::debug!("[scan] Entering directory: {:?}", dir);
|
||||
let entries = std::fs::read_dir(dir)
|
||||
.map_err(|e| format!("Failed to read pipelines directory: {}", e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively scan subdirectory
|
||||
scan_pipelines_with_paths(&path, category_filter, industry_filter, pipelines)?;
|
||||
} else if path.extension().map(|e| e == "yaml" || e == "yml").unwrap_or(false) {
|
||||
// Try to parse pipeline file
|
||||
tracing::debug!("[scan] Found YAML file: {:?}", path);
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
tracing::debug!("[scan] File content length: {} bytes", content.len());
|
||||
match parse_pipeline_yaml(&content) {
|
||||
Ok(pipeline) => {
|
||||
tracing::debug!(
|
||||
"[scan] Parsed YAML: {} -> category: {:?}, industry: {:?}",
|
||||
pipeline.metadata.name,
|
||||
pipeline.metadata.category,
|
||||
pipeline.metadata.industry
|
||||
);
|
||||
|
||||
// Apply category filter
|
||||
if let Some(filter) = category_filter {
|
||||
if pipeline.metadata.category.as_deref() != Some(filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply industry filter
|
||||
if let Some(filter) = industry_filter {
|
||||
if pipeline.metadata.industry.as_deref() != Some(filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("[scan] Found pipeline: {} at {:?}", pipeline.metadata.name, path);
|
||||
pipelines.push((pipeline_to_info(&pipeline), path));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("[scan] Failed to parse pipeline at {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scan_pipelines_full_sync(
|
||||
dir: &PathBuf,
|
||||
pipelines: &mut Vec<(PathBuf, Pipeline)>,
|
||||
) -> Result<(), String> {
|
||||
let entries = std::fs::read_dir(dir)
|
||||
.map_err(|e| format!("Failed to read pipelines directory: {}", e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
scan_pipelines_full_sync(&path, pipelines)?;
|
||||
} else if path.extension().map(|e| e == "yaml" || e == "yml").unwrap_or(false) {
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
if let Ok(pipeline) = parse_pipeline_yaml(&content) {
|
||||
pipelines.push((path, pipeline));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn pipeline_to_info(pipeline: &Pipeline) -> PipelineInfo {
|
||||
let industry = pipeline.metadata.industry.clone().unwrap_or_default();
|
||||
tracing::debug!(
|
||||
"[pipeline_to_info] Pipeline: {}, category: {:?}, industry: {:?}",
|
||||
pipeline.metadata.name,
|
||||
pipeline.metadata.category,
|
||||
pipeline.metadata.industry
|
||||
);
|
||||
|
||||
PipelineInfo {
|
||||
id: pipeline.metadata.name.clone(),
|
||||
display_name: pipeline.metadata.display_name.clone()
|
||||
.unwrap_or_else(|| pipeline.metadata.name.clone()),
|
||||
description: pipeline.metadata.description.clone().unwrap_or_default(),
|
||||
category: pipeline.metadata.category.clone().unwrap_or_default(),
|
||||
industry,
|
||||
tags: pipeline.metadata.tags.clone(),
|
||||
icon: pipeline.metadata.icon.clone().unwrap_or_else(|| "📦".to_string()),
|
||||
version: pipeline.metadata.version.clone(),
|
||||
author: pipeline.metadata.author.clone().unwrap_or_default(),
|
||||
inputs: pipeline.spec.inputs.iter().map(|input| {
|
||||
PipelineInputInfo {
|
||||
name: input.name.clone(),
|
||||
input_type: match input.input_type {
|
||||
zclaw_pipeline::InputType::String => "string".to_string(),
|
||||
zclaw_pipeline::InputType::Number => "number".to_string(),
|
||||
zclaw_pipeline::InputType::Boolean => "boolean".to_string(),
|
||||
zclaw_pipeline::InputType::Select => "select".to_string(),
|
||||
zclaw_pipeline::InputType::MultiSelect => "multi-select".to_string(),
|
||||
zclaw_pipeline::InputType::File => "file".to_string(),
|
||||
zclaw_pipeline::InputType::Text => "text".to_string(),
|
||||
},
|
||||
required: input.required,
|
||||
label: input.label.clone().unwrap_or_else(|| input.name.clone()),
|
||||
placeholder: input.placeholder.clone(),
|
||||
default: input.default.clone(),
|
||||
options: input.options.clone(),
|
||||
}
|
||||
}).collect(),
|
||||
}
|
||||
}
|
||||
293
desktop/src-tauri/src/pipeline_commands/intent_router.rs
Normal file
293
desktop/src-tauri/src/pipeline_commands/intent_router.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
//! Intent routing commands and LLM driver creation from config.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use secrecy::SecretString;
|
||||
|
||||
use zclaw_pipeline::LlmActionDriver;
|
||||
|
||||
use super::adapters::RuntimeLlmAdapter;
|
||||
use super::PipelineState;
|
||||
|
||||
use crate::kernel_commands::KernelState;
|
||||
|
||||
/// Route result for frontend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum RouteResultResponse {
|
||||
Matched {
|
||||
pipeline_id: String,
|
||||
display_name: Option<String>,
|
||||
mode: String,
|
||||
params: HashMap<String, Value>,
|
||||
confidence: f32,
|
||||
missing_params: Vec<MissingParamInfo>,
|
||||
},
|
||||
Ambiguous {
|
||||
candidates: Vec<PipelineCandidateInfo>,
|
||||
},
|
||||
NoMatch {
|
||||
suggestions: Vec<PipelineCandidateInfo>,
|
||||
},
|
||||
NeedMoreInfo {
|
||||
prompt: String,
|
||||
related_pipeline: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Missing parameter info
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MissingParamInfo {
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub param_type: String,
|
||||
pub required: bool,
|
||||
pub default: Option<Value>,
|
||||
}
|
||||
|
||||
/// Pipeline candidate info
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelineCandidateInfo {
|
||||
pub id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub match_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Route user input to matching pipeline
|
||||
#[tauri::command]
|
||||
pub async fn route_intent(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
kernel_state: State<'_, KernelState>,
|
||||
user_input: String,
|
||||
) -> Result<RouteResultResponse, String> {
|
||||
use zclaw_pipeline::{TriggerParser, Trigger, TriggerParam, compile_trigger};
|
||||
|
||||
tracing::debug!("[route_intent] Routing user input: {}", user_input);
|
||||
|
||||
// Build trigger parser from loaded pipelines
|
||||
let pipelines = state.pipelines.read().await;
|
||||
let mut parser = TriggerParser::new();
|
||||
|
||||
for (id, pipeline) in pipelines.iter() {
|
||||
// Derive trigger info from pipeline metadata (tags as keywords, description)
|
||||
let trigger = Trigger {
|
||||
keywords: pipeline.metadata.tags.clone(),
|
||||
patterns: vec![], // Patterns not defined in Pipeline struct
|
||||
description: pipeline.metadata.description.clone(),
|
||||
examples: vec![], // Examples not defined in Pipeline struct
|
||||
};
|
||||
|
||||
// Convert pipeline inputs to trigger params
|
||||
let param_defs: Vec<TriggerParam> = pipeline.spec.inputs.iter().map(|input| {
|
||||
TriggerParam {
|
||||
name: input.name.clone(),
|
||||
param_type: match input.input_type {
|
||||
zclaw_pipeline::InputType::String => "string".to_string(),
|
||||
zclaw_pipeline::InputType::Number => "number".to_string(),
|
||||
zclaw_pipeline::InputType::Boolean => "boolean".to_string(),
|
||||
zclaw_pipeline::InputType::Select => "select".to_string(),
|
||||
zclaw_pipeline::InputType::MultiSelect => "multi-select".to_string(),
|
||||
zclaw_pipeline::InputType::File => "file".to_string(),
|
||||
zclaw_pipeline::InputType::Text => "text".to_string(),
|
||||
},
|
||||
required: input.required,
|
||||
label: input.label.clone(),
|
||||
default: input.default.clone(),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
match compile_trigger(
|
||||
id.clone(),
|
||||
pipeline.metadata.display_name.clone(),
|
||||
&trigger,
|
||||
param_defs,
|
||||
) {
|
||||
Ok(compiled) => parser.register(compiled),
|
||||
Err(e) => {
|
||||
tracing::warn!("[WARN route_intent] Failed to compile trigger for {}: {}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quick match
|
||||
if let Some(match_result) = parser.quick_match(&user_input) {
|
||||
let trigger = parser.get_trigger(&match_result.pipeline_id);
|
||||
|
||||
// Determine input mode
|
||||
let mode = if let Some(t) = &trigger {
|
||||
let required_count = t.param_defs.iter().filter(|p| p.required).count();
|
||||
if required_count > 3 || t.param_defs.len() > 5 {
|
||||
"form"
|
||||
} else if t.param_defs.is_empty() {
|
||||
"conversation"
|
||||
} else {
|
||||
"conversation"
|
||||
}
|
||||
} else {
|
||||
"auto"
|
||||
};
|
||||
|
||||
// Find missing params
|
||||
let missing_params: Vec<MissingParamInfo> = trigger
|
||||
.map(|t| {
|
||||
t.param_defs.iter()
|
||||
.filter(|p| p.required && !match_result.params.contains_key(&p.name) && p.default.is_none())
|
||||
.map(|p| MissingParamInfo {
|
||||
name: p.name.clone(),
|
||||
label: p.label.clone(),
|
||||
param_type: p.param_type.clone(),
|
||||
required: p.required,
|
||||
default: p.default.clone(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
return Ok(RouteResultResponse::Matched {
|
||||
pipeline_id: match_result.pipeline_id,
|
||||
display_name: trigger.and_then(|t| t.display_name.clone()),
|
||||
mode: mode.to_string(),
|
||||
params: match_result.params,
|
||||
confidence: match_result.confidence,
|
||||
missing_params,
|
||||
});
|
||||
}
|
||||
|
||||
// Semantic match via LLM (if kernel is initialized)
|
||||
let triggers = parser.triggers();
|
||||
if !triggers.is_empty() {
|
||||
let llm_driver = {
|
||||
let kernel_lock = kernel_state.lock().await;
|
||||
kernel_lock.as_ref().map(|k| k.driver())
|
||||
};
|
||||
|
||||
if let Some(driver) = llm_driver {
|
||||
use zclaw_pipeline::{RuntimeLlmIntentDriver, LlmIntentDriver};
|
||||
let intent_driver = RuntimeLlmIntentDriver::new(driver);
|
||||
|
||||
if let Some(result) = intent_driver.semantic_match(&user_input, &triggers).await {
|
||||
tracing::debug!(
|
||||
"[route_intent] Semantic match: pipeline={}, confidence={}",
|
||||
result.pipeline_id, result.confidence
|
||||
);
|
||||
|
||||
let trigger = parser.get_trigger(&result.pipeline_id);
|
||||
let mode = "auto".to_string();
|
||||
|
||||
let missing_params: Vec<MissingParamInfo> = trigger
|
||||
.map(|t| {
|
||||
t.param_defs.iter()
|
||||
.filter(|p| p.required && !result.params.contains_key(&p.name) && p.default.is_none())
|
||||
.map(|p| MissingParamInfo {
|
||||
name: p.name.clone(),
|
||||
label: p.label.clone(),
|
||||
param_type: p.param_type.clone(),
|
||||
required: p.required,
|
||||
default: p.default.clone(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
return Ok(RouteResultResponse::Matched {
|
||||
pipeline_id: result.pipeline_id,
|
||||
display_name: trigger.and_then(|t| t.display_name.clone()),
|
||||
mode,
|
||||
params: result.params,
|
||||
confidence: result.confidence,
|
||||
missing_params,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match - return suggestions
|
||||
let suggestions: Vec<PipelineCandidateInfo> = parser.triggers()
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|t| PipelineCandidateInfo {
|
||||
id: t.pipeline_id.clone(),
|
||||
display_name: t.display_name.clone(),
|
||||
description: t.description.clone(),
|
||||
icon: None,
|
||||
category: None,
|
||||
match_reason: Some("推荐".to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(RouteResultResponse::NoMatch { suggestions })
|
||||
}
|
||||
|
||||
/// Create an LLM driver from configuration file or environment variables
|
||||
pub(crate) fn create_llm_driver_from_config() -> Option<Arc<dyn LlmActionDriver>> {
|
||||
// Try to read config file
|
||||
let config_path = dirs::config_dir()
|
||||
.map(|p| p.join("zclaw").join("config.toml"))?;
|
||||
|
||||
if !config_path.exists() {
|
||||
tracing::debug!("[create_llm_driver] Config file not found at {:?}", config_path);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read and parse config
|
||||
let config_content = std::fs::read_to_string(&config_path).ok()?;
|
||||
let config: toml::Value = toml::from_str(&config_content).ok()?;
|
||||
|
||||
// Extract LLM config
|
||||
let llm_config = config.get("llm")?;
|
||||
|
||||
let provider = llm_config.get("provider")?.as_str()?.to_string();
|
||||
let api_key = llm_config.get("api_key")?.as_str()?.to_string();
|
||||
let base_url = llm_config.get("base_url").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let model = llm_config.get("model").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
|
||||
tracing::debug!("[create_llm_driver] Found LLM config: provider={}, model={:?}", provider, model);
|
||||
|
||||
// Convert api_key to SecretString
|
||||
let secret_key = SecretString::new(api_key);
|
||||
|
||||
// Create the runtime driver — use with_base_url when a custom endpoint is configured
|
||||
// (essential for Chinese providers like doubao, qwen, deepseek, kimi)
|
||||
let runtime_driver: Arc<dyn zclaw_runtime::LlmDriver> = match provider.as_str() {
|
||||
"anthropic" => {
|
||||
if let Some(url) = base_url {
|
||||
Arc::new(zclaw_runtime::AnthropicDriver::with_base_url(secret_key, url))
|
||||
} else {
|
||||
Arc::new(zclaw_runtime::AnthropicDriver::new(secret_key))
|
||||
}
|
||||
}
|
||||
"openai" | "doubao" | "qwen" | "deepseek" | "kimi" | "zhipu" => {
|
||||
// Chinese providers typically need a custom base_url
|
||||
if let Some(url) = base_url {
|
||||
Arc::new(zclaw_runtime::OpenAiDriver::with_base_url(secret_key, url))
|
||||
} else {
|
||||
Arc::new(zclaw_runtime::OpenAiDriver::new(secret_key))
|
||||
}
|
||||
}
|
||||
"gemini" => {
|
||||
if let Some(url) = base_url {
|
||||
Arc::new(zclaw_runtime::GeminiDriver::with_base_url(secret_key, url))
|
||||
} else {
|
||||
Arc::new(zclaw_runtime::GeminiDriver::new(secret_key))
|
||||
}
|
||||
}
|
||||
"local" | "ollama" => {
|
||||
let url = base_url.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
Arc::new(zclaw_runtime::LocalDriver::new(&url))
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("[WARN create_llm_driver] Unknown provider: {}", provider);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Arc::new(RuntimeLlmAdapter::new(runtime_driver, model)))
|
||||
}
|
||||
63
desktop/src-tauri/src/pipeline_commands/mod.rs
Normal file
63
desktop/src-tauri/src/pipeline_commands/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! Pipeline commands for Tauri
|
||||
//!
|
||||
//! Commands for discovering, running, and monitoring Pipelines.
|
||||
|
||||
pub mod adapters;
|
||||
pub mod types;
|
||||
pub mod discovery;
|
||||
pub mod crud;
|
||||
pub mod helpers;
|
||||
pub mod intent_router;
|
||||
pub mod presentation;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use zclaw_pipeline::{Pipeline, PipelineExecutor, ActionRegistry};
|
||||
|
||||
// Re-export key types from sub-modules for external consumers
|
||||
#[allow(unused_imports)]
|
||||
pub use adapters::{RuntimeLlmAdapter, PipelineSkillDriver, PipelineHandDriver};
|
||||
#[allow(unused_imports)]
|
||||
pub use types::{PipelineInfo, PipelineInputInfo, RunPipelineRequest, RunPipelineResponse, PipelineRunResponse};
|
||||
#[allow(unused_imports)]
|
||||
pub use crud::{CreatePipelineRequest, UpdatePipelineRequest, WorkflowStepInput};
|
||||
#[allow(unused_imports)]
|
||||
pub use intent_router::{RouteResultResponse, MissingParamInfo, PipelineCandidateInfo};
|
||||
#[allow(unused_imports)]
|
||||
pub use presentation::PipelineTemplateInfo;
|
||||
|
||||
/// Pipeline state wrapper for Tauri
|
||||
pub struct PipelineState {
|
||||
/// Pipeline executor
|
||||
pub executor: Arc<PipelineExecutor>,
|
||||
/// Discovered pipelines (id -> Pipeline)
|
||||
pub pipelines: RwLock<HashMap<String, Pipeline>>,
|
||||
/// Pipeline file paths (id -> path)
|
||||
pub pipeline_paths: RwLock<HashMap<String, PathBuf>>,
|
||||
}
|
||||
|
||||
impl PipelineState {
|
||||
pub fn new(action_registry: Arc<ActionRegistry>) -> Self {
|
||||
Self {
|
||||
executor: Arc::new(PipelineExecutor::new(action_registry)),
|
||||
pipelines: RwLock::new(HashMap::new()),
|
||||
pipeline_paths: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create pipeline state with default action registry
|
||||
pub fn create_pipeline_state() -> Arc<PipelineState> {
|
||||
// Try to create an LLM driver from environment/config
|
||||
let action_registry = if let Some(driver) = intent_router::create_llm_driver_from_config() {
|
||||
tracing::debug!("[create_pipeline_state] LLM driver configured successfully");
|
||||
Arc::new(ActionRegistry::new().with_llm_driver(driver))
|
||||
} else {
|
||||
tracing::debug!("[create_pipeline_state] No LLM driver configured - pipelines requiring LLM will fail");
|
||||
Arc::new(ActionRegistry::new())
|
||||
};
|
||||
Arc::new(PipelineState::new(action_registry))
|
||||
}
|
||||
103
desktop/src-tauri/src/pipeline_commands/presentation.rs
Normal file
103
desktop/src-tauri/src/pipeline_commands/presentation.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! Presentation analysis and template listing commands.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::types::PipelineInputInfo;
|
||||
use super::PipelineState;
|
||||
|
||||
/// Analyze presentation data
|
||||
#[tauri::command]
|
||||
pub async fn analyze_presentation(
|
||||
data: Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
use zclaw_pipeline::presentation::PresentationAnalyzer;
|
||||
|
||||
let analyzer = PresentationAnalyzer::new();
|
||||
let analysis = analyzer.analyze(&data);
|
||||
|
||||
// Convert analysis to JSON
|
||||
serde_json::to_value(&analysis).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Pipeline template metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelineTemplateInfo {
|
||||
pub id: String,
|
||||
pub display_name: String,
|
||||
pub description: String,
|
||||
pub category: String,
|
||||
pub industry: String,
|
||||
pub tags: Vec<String>,
|
||||
pub icon: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub inputs: Vec<PipelineInputInfo>,
|
||||
}
|
||||
|
||||
/// List available pipeline templates from the `_templates/` directory.
|
||||
///
|
||||
/// Templates are pipeline YAML files that users can browse and instantiate.
|
||||
/// They live in `pipelines/_templates/` and are not directly runnable
|
||||
/// (they serve as blueprints).
|
||||
#[tauri::command]
|
||||
pub async fn pipeline_templates(
|
||||
state: State<'_, Arc<PipelineState>>,
|
||||
) -> Result<Vec<PipelineTemplateInfo>, String> {
|
||||
let pipelines = state.pipelines.read().await;
|
||||
|
||||
// Filter pipelines that have `is_template: true` in metadata
|
||||
// or are in the _templates directory
|
||||
let templates: Vec<PipelineTemplateInfo> = pipelines.iter()
|
||||
.filter_map(|(_id, pipeline)| {
|
||||
// Check if this pipeline has template metadata
|
||||
let is_template = pipeline.metadata.annotations
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("is_template"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_template {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(PipelineTemplateInfo {
|
||||
id: pipeline.metadata.name.clone(),
|
||||
display_name: pipeline.metadata.display_name.clone()
|
||||
.unwrap_or_else(|| pipeline.metadata.name.clone()),
|
||||
description: pipeline.metadata.description.clone().unwrap_or_default(),
|
||||
category: pipeline.metadata.category.clone().unwrap_or_default(),
|
||||
industry: pipeline.metadata.industry.clone().unwrap_or_default(),
|
||||
tags: pipeline.metadata.tags.clone(),
|
||||
icon: pipeline.metadata.icon.clone().unwrap_or_else(|| "📦".to_string()),
|
||||
version: pipeline.metadata.version.clone(),
|
||||
author: pipeline.metadata.author.clone().unwrap_or_default(),
|
||||
inputs: pipeline.spec.inputs.iter().map(|input| {
|
||||
PipelineInputInfo {
|
||||
name: input.name.clone(),
|
||||
input_type: match input.input_type {
|
||||
zclaw_pipeline::InputType::String => "string".to_string(),
|
||||
zclaw_pipeline::InputType::Number => "number".to_string(),
|
||||
zclaw_pipeline::InputType::Boolean => "boolean".to_string(),
|
||||
zclaw_pipeline::InputType::Select => "select".to_string(),
|
||||
zclaw_pipeline::InputType::MultiSelect => "multi-select".to_string(),
|
||||
zclaw_pipeline::InputType::File => "file".to_string(),
|
||||
zclaw_pipeline::InputType::Text => "text".to_string(),
|
||||
},
|
||||
required: input.required,
|
||||
label: input.label.clone().unwrap_or_else(|| input.name.clone()),
|
||||
placeholder: input.placeholder.clone(),
|
||||
default: input.default.clone(),
|
||||
options: input.options.clone(),
|
||||
}
|
||||
}).collect(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
tracing::debug!("[pipeline_templates] Found {} templates", templates.len());
|
||||
Ok(templates)
|
||||
}
|
||||
99
desktop/src-tauri/src/pipeline_commands/types.rs
Normal file
99
desktop/src-tauri/src/pipeline_commands/types.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
//! Public types for Pipeline commands.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Pipeline info for list display
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelineInfo {
|
||||
/// Pipeline ID (name)
|
||||
pub id: String,
|
||||
/// Display name
|
||||
pub display_name: String,
|
||||
/// Description
|
||||
pub description: String,
|
||||
/// Category (functional classification)
|
||||
pub category: String,
|
||||
/// Industry classification (e.g., "internet", "finance", "healthcare")
|
||||
pub industry: String,
|
||||
/// Tags
|
||||
pub tags: Vec<String>,
|
||||
/// Icon (emoji)
|
||||
pub icon: String,
|
||||
/// Version
|
||||
pub version: String,
|
||||
/// Author
|
||||
pub author: String,
|
||||
/// Input parameters
|
||||
pub inputs: Vec<PipelineInputInfo>,
|
||||
}
|
||||
|
||||
/// Pipeline input parameter info
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelineInputInfo {
|
||||
/// Parameter name
|
||||
pub name: String,
|
||||
/// Input type
|
||||
pub input_type: String,
|
||||
/// Is required
|
||||
pub required: bool,
|
||||
/// Label
|
||||
pub label: String,
|
||||
/// Placeholder
|
||||
pub placeholder: Option<String>,
|
||||
/// Default value
|
||||
pub default: Option<Value>,
|
||||
/// Options (for select/multi-select)
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
/// Run pipeline request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RunPipelineRequest {
|
||||
/// Pipeline ID
|
||||
pub pipeline_id: String,
|
||||
/// Input values
|
||||
pub inputs: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Run pipeline response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RunPipelineResponse {
|
||||
/// Run ID
|
||||
pub run_id: String,
|
||||
/// Pipeline ID
|
||||
pub pipeline_id: String,
|
||||
/// Status
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Pipeline run status response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelineRunResponse {
|
||||
/// Run ID
|
||||
pub run_id: String,
|
||||
/// Pipeline ID
|
||||
pub pipeline_id: String,
|
||||
/// Status
|
||||
pub status: String,
|
||||
/// Current step
|
||||
pub current_step: Option<String>,
|
||||
/// Progress percentage
|
||||
pub percentage: u8,
|
||||
/// Message
|
||||
pub message: String,
|
||||
/// Outputs (if completed)
|
||||
pub outputs: Option<Value>,
|
||||
/// Error (if failed)
|
||||
pub error: Option<String>,
|
||||
/// Started at
|
||||
pub started_at: String,
|
||||
/// Ended at
|
||||
pub ended_at: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user