use anyhow::Result;
use skill_runtime::{InstanceManager, LocalSkillLoader, SkillEngine, SkillManifest};
use skill_runtime::search::SearchPipeline;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::info;
use crate::analytics::SearchAnalyticsDb;
use crate::execution_history::ExecutionHistoryDb;
use crate::routes::{create_app, create_app_with_ui};
use crate::types::{ExecutionHistoryEntry, ServiceStatus, SkillServiceRequirement, SkillSummary};
#[derive(Debug, Clone)]
pub struct HttpServerConfig {
pub host: String,
pub port: u16,
pub enable_cors: bool,
pub enable_tracing: bool,
pub enable_web_ui: bool,
pub working_dir: Option<PathBuf>,
}
impl Default for HttpServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3000,
enable_cors: true,
enable_tracing: true,
enable_web_ui: false,
working_dir: None,
}
}
}
pub struct TrackedService {
pub name: String,
pub process: Option<std::process::Child>,
pub port: u16,
}
pub struct AppState {
pub started_at: Instant,
pub skills: RwLock<HashMap<String, SkillSummary>>,
pub execution_history: RwLock<Vec<ExecutionHistoryEntry>>,
pub execution_history_db: RwLock<Option<Arc<ExecutionHistoryDb>>>,
pub config: HttpServerConfig,
pub engine: Arc<SkillEngine>,
pub manifest: RwLock<Option<SkillManifest>>,
pub instance_manager: InstanceManager,
pub local_loader: LocalSkillLoader,
pub working_dir: PathBuf,
pub services: RwLock<HashMap<String, TrackedService>>,
pub search_pipeline: RwLock<Option<Arc<SearchPipeline>>>,
pub analytics_db: RwLock<Option<Arc<SearchAnalyticsDb>>>,
}
impl AppState {
pub fn new(config: HttpServerConfig) -> Result<Self> {
let working_dir = config.working_dir.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let engine = Arc::new(SkillEngine::new()?);
let instance_manager = InstanceManager::new()?;
let local_loader = LocalSkillLoader::new()?;
let manifest = SkillManifest::find(&working_dir)
.and_then(|path| SkillManifest::load(&path).ok());
Ok(Self {
started_at: Instant::now(),
skills: RwLock::new(HashMap::new()),
execution_history: RwLock::new(Vec::new()),
execution_history_db: RwLock::new(None),
config,
engine,
manifest: RwLock::new(manifest),
instance_manager,
local_loader,
working_dir,
services: RwLock::new(HashMap::new()),
search_pipeline: RwLock::new(None),
analytics_db: RwLock::new(None),
})
}
pub async fn initialize_search_pipeline(&self) -> Result<()> {
use skill_runtime::search_config::SearchConfig;
let config = SearchConfig::default();
let pipeline = SearchPipeline::from_config(config).await?;
let mut search_pipeline = self.search_pipeline.write().await;
*search_pipeline = Some(Arc::new(pipeline));
Ok(())
}
pub async fn initialize_analytics_db(&self) -> Result<()> {
let db_path = dirs::home_dir()
.map(|p| p.join(".skill-engine/analytics.db"))
.unwrap_or_else(|| PathBuf::from(".skill-engine/analytics.db"))
.to_string_lossy()
.to_string();
let db = SearchAnalyticsDb::new(&db_path).await?;
let mut analytics_db = self.analytics_db.write().await;
*analytics_db = Some(Arc::new(db));
info!("Analytics database initialized at: {}", db_path);
Ok(())
}
pub async fn initialize_execution_history_db(&self) -> Result<()> {
let db_path = dirs::home_dir()
.map(|p| p.join(".skill-engine/execution-history.db"))
.unwrap_or_else(|| PathBuf::from(".skill-engine/execution-history.db"))
.to_string_lossy()
.to_string();
let db = ExecutionHistoryDb::new(&db_path).await?;
let recent_history = db.list_executions(1000, 0).await?;
let mut history = self.execution_history.write().await;
*history = recent_history;
let mut execution_history_db = self.execution_history_db.write().await;
*execution_history_db = Some(Arc::new(db));
info!("Execution history database initialized at: {}", db_path);
Ok(())
}
pub async fn load_skills_from_manifest(&self) -> Result<()> {
let skill_infos: Vec<_> = {
let manifest = self.manifest.read().await;
if let Some(manifest) = manifest.as_ref() {
manifest.skills.iter().map(|(name, skill_def)| {
let instances_count = if skill_def.instances.is_empty() { 1 } else { skill_def.instances.len() };
let runtime_str = match skill_def.runtime {
skill_runtime::SkillRuntime::Wasm => "wasm",
skill_runtime::SkillRuntime::Docker => "docker",
skill_runtime::SkillRuntime::Native => "native",
};
let source_path = if skill_def.source.starts_with("./") || skill_def.source.starts_with('/') {
manifest.base_dir.join(&skill_def.source)
} else {
let home = dirs::home_dir().unwrap_or_default();
home.join(".skill-engine").join("registry").join(name)
};
(
name.clone(),
skill_def.description.clone().unwrap_or_default(),
skill_def.source.clone(),
runtime_str.to_string(),
instances_count,
skill_def.runtime == skill_runtime::SkillRuntime::Wasm,
source_path,
skill_def.services.clone(),
)
}).collect()
} else {
vec![]
}
};
let mut skills_to_insert = Vec::new();
for (name, description, source, runtime, instances_count, is_wasm, source_path, services) in skill_infos {
let tools_count = if source_path.exists() {
use skill_runtime::skill_md::find_skill_md;
if let Some(skill_md_path) = find_skill_md(&source_path) {
match skill_runtime::skill_md::parse_skill_md(&skill_md_path) {
Ok(skill_content) => skill_content.tool_docs.len(),
Err(_) => {
if is_wasm {
self.load_skill_tools_count(&name, &source_path).await
} else {
0
}
}
}
} else if is_wasm {
self.load_skill_tools_count(&name, &source_path).await
} else {
0
}
} else {
0
};
let required_services: Vec<SkillServiceRequirement> = services.iter().map(|s| {
SkillServiceRequirement {
name: s.name.clone(),
description: s.description.clone(),
optional: s.optional,
default_port: s.default_port,
status: ServiceStatus {
name: s.name.clone(),
running: false,
pid: None,
port: s.default_port,
url: None,
error: None,
},
}
}).collect();
let skill_summary = SkillSummary {
name: name.clone(),
version: "0.1.0".to_string(),
description,
source,
runtime,
tools_count,
instances_count,
last_used: None,
execution_count: 0,
required_services,
};
skills_to_insert.push((name, skill_summary));
}
let mut skills = self.skills.write().await;
for (name, summary) in skills_to_insert {
skills.insert(name, summary);
}
info!("Loaded {} skills from manifest", skills.len());
Ok(())
}
async fn load_skill_tools_count(&self, name: &str, source_path: &PathBuf) -> usize {
match self.local_loader.load_skill(source_path, &self.engine).await {
Ok(component) => {
let instance_config = skill_runtime::instance::InstanceConfig::default();
match skill_runtime::SkillExecutor::from_component(
self.engine.clone(),
component,
name.to_string(),
"default".to_string(),
instance_config,
) {
Ok(executor) => {
match executor.get_tools().await {
Ok(tools) => {
info!(skill = %name, tools = tools.len(), "Loaded skill tools");
tools.len()
}
Err(e) => {
tracing::warn!(skill = %name, error = %e, "Failed to get tools");
0
}
}
}
Err(e) => {
tracing::warn!(skill = %name, error = %e, "Failed to create executor");
0
}
}
}
Err(e) => {
tracing::warn!(skill = %name, error = %e, "Failed to load skill");
0
}
}
}
}
pub struct HttpServer {
config: HttpServerConfig,
}
impl HttpServer {
pub fn new() -> Result<Self> {
Ok(Self {
config: HttpServerConfig::default(),
})
}
pub fn with_config(config: HttpServerConfig) -> Result<Self> {
Ok(Self { config })
}
pub async fn run(&self) -> Result<()> {
let state = Arc::new(AppState::new(self.config.clone())?);
if let Err(e) = state.initialize_execution_history_db().await {
tracing::warn!("Failed to initialize execution history database: {}", e);
}
if let Err(e) = state.initialize_analytics_db().await {
tracing::warn!("Failed to initialize analytics database: {}", e);
}
state.load_skills_from_manifest().await?;
let mut app = if self.config.enable_web_ui {
create_app_with_ui(state)
} else {
create_app(state)
};
if self.config.enable_cors {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
app = app.layer(cors);
}
if self.config.enable_tracing {
app = app.layer(TraceLayer::new_for_http());
}
let addr = format!("{}:{}", self.config.host, self.config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
info!(
address = %addr,
cors = self.config.enable_cors,
tracing = self.config.enable_tracing,
web_ui = self.config.enable_web_ui,
"HTTP server starting"
);
if self.config.enable_web_ui {
println!("Skill Engine Web UI available at http://{}", addr);
println!(" Web interface: http://{}/", addr);
println!(" API endpoints: http://{}/api/...", addr);
} else {
println!("Skill Engine HTTP API listening on http://{}", addr);
println!(" API endpoints: http://{}/api/...", addr);
println!(" Health check: http://{}/api/health", addr);
}
axum::serve(listener, app).await?;
Ok(())
}
}
impl Default for HttpServer {
fn default() -> Self {
Self::new().expect("Failed to create default HttpServer")
}
}