use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::info;
use langhub::LLMClient;
use langhub::types::ModelProvider;
use crate::core::registry::{generate_instances_registry, generate_skills_registry};
use crate::core::tasks::{NaturalLanguageTask, SkillMdTask};
use crate::core::welcome::generate_welcome_message;
use crate::executors::Executor;
use crate::{ConfigInitMethod, HippoxConfig, get_config, i18n, init_config_from_json_file, init_config_from_params_json_str, init_config_from_toml_file, t};
use crate::skill_loader::SkillLoader;
use crate::skill_scheduler::SkillScheduler;
use crate::tasks::{self, ExecutableTask, TaskStatus};
use crate::workflow::{WorkflowCallback, WorkflowExecutor, WorkflowMode};
#[derive(Clone)]
pub struct Hippox {
scheduler: SkillScheduler,
executor: Executor,
workflow_mode: WorkflowMode,
workflow_executor: WorkflowExecutor,
is_first_message: Arc<AtomicBool>,
}
impl Hippox {
pub async fn new(
provider: ModelProvider,
api_key: Option<String>,
extra_keys: Option<HashMap<String, String>>,
config_method: ConfigInitMethod,
) -> anyhow::Result<Self> {
Self::with_workflow_mode(
provider,
api_key,
extra_keys,
config_method,
WorkflowMode::default(),
)
.await
}
pub async fn with_workflow_mode(
provider: ModelProvider,
api_key: Option<String>,
extra_keys: Option<HashMap<String, String>>,
config_method: ConfigInitMethod,
workflow_mode: WorkflowMode,
) -> anyhow::Result<Self> {
info!(
"Initializing Hippox core with workflow mode: {}",
workflow_mode
);
match config_method {
ConfigInitMethod::TomlFile(path) => init_config_from_toml_file(&path)?,
ConfigInitMethod::JsonFile(path) => init_config_from_json_file(&path)?,
ConfigInitMethod::ParamsJsonStr(json) => init_config_from_params_json_str(&json)?,
}
let config = get_config();
i18n::set_language(&config.lang);
let llm = LLMClient::new_with_key(provider, api_key, extra_keys)?;
let scheduler = SkillScheduler::new(llm);
let executor = Executor::new();
let workflow_executor = WorkflowExecutor::new(workflow_mode);
Ok(Self {
scheduler,
executor,
workflow_mode,
workflow_executor,
is_first_message: Arc::new(AtomicBool::new(false)),
})
}
pub fn refresh_llm_skill_registry(&self) {
self.is_first_message.store(false, Ordering::SeqCst);
}
pub fn refresh_llm_instances(&self) {
self.is_first_message.store(false, Ordering::SeqCst);
}
pub fn get_skills_registry(&self) -> String {
generate_skills_registry()
}
pub fn get_instances_registry(&self) -> String {
generate_instances_registry()
}
pub fn get_welcome_message(&self) -> String {
let skills = self.get_skills_registry();
let instances = self.get_instances_registry();
generate_welcome_message(&skills, &instances)
}
pub fn handle_natural_language(
&self,
input: &str,
callback: Option<Arc<dyn WorkflowCallback>>,
) -> String {
let skills_registry = self.get_skills_registry();
let instances_registry = self.get_instances_registry();
let executable = Arc::new(NaturalLanguageTask::new(
input.to_string(),
self.workflow_executor.clone(),
self.scheduler.clone(),
skills_registry,
instances_registry,
));
let task_id = futures::executor::block_on(tasks::create_task_with_executable(
"natural_language".to_string(),
input.to_string(),
executable,
callback,
));
info!(
"Created natural language task: {} with input: {}",
task_id, input
);
task_id
}
pub fn handle_natural_language_batch(
&self,
inputs: Vec<(String, Option<String>, Option<Arc<dyn WorkflowCallback>>)>,
) -> Vec<String> {
inputs
.into_iter()
.map(|(input, _session_id, callback)| self.handle_natural_language(&input, callback))
.collect()
}
pub fn handle_skill_md(
&self,
skill_md_path: &str,
params: Option<HashMap<String, Value>>,
callback: Option<Arc<dyn WorkflowCallback>>,
) -> String {
let skills_registry = self.get_skills_registry();
let instances_registry = self.get_instances_registry();
let executable = Arc::new(SkillMdTask::new(
skill_md_path.to_string(),
params,
self.workflow_executor.clone(),
self.scheduler.clone(),
skills_registry,
instances_registry,
));
let task_id = futures::executor::block_on(tasks::create_task_with_executable(
"skill_md".to_string(),
skill_md_path.to_string(),
executable,
callback,
));
info!(
"Created SKILL.md task: {} for path: {}",
task_id, skill_md_path
);
task_id
}
pub fn handle_skill_md_batch(
&self,
tasks: Vec<(
String,
Option<HashMap<String, Value>>,
Option<Arc<dyn WorkflowCallback>>,
)>,
) -> Vec<String> {
tasks
.into_iter()
.map(|(path, params, callback)| self.handle_skill_md(&path, params, callback))
.collect()
}
pub async fn direct_handle_natural_language(
&self,
input: &str,
callback: Option<Arc<dyn WorkflowCallback>>,
) -> String {
let mut executor = self.workflow_executor.clone();
if let Some(cb) = callback {
executor = executor.with_callback(cb);
}
let skills_registry = self.get_skills_registry();
let instances_registry = self.get_instances_registry();
executor
.execute(
&self.scheduler,
input,
&skills_registry,
&instances_registry,
)
.await
}
pub async fn direct_handle_natural_language_batch(
&self,
inputs: Vec<(String, Option<Arc<dyn WorkflowCallback>>)>,
) -> Vec<String> {
let mut results = Vec::new();
for (input, callback) in inputs {
let result = self.direct_handle_natural_language(&input, callback).await;
results.push(result);
}
results
}
pub async fn direct_handle_skill_md(
&self,
skill_md_path: &str,
params: Option<HashMap<String, Value>>,
callback: Option<Arc<dyn WorkflowCallback>>,
) -> String {
let skill_file = match SkillLoader::load_from_path(skill_md_path) {
Ok(Some(file)) => file,
Ok(None) => {
return format!("{}: {}", t!("error.skill_not_found"), skill_md_path);
}
Err(e) => {
return format!("{}: {}", t!("error.load_skill_failed"), e);
}
};
info!(
"Executing SKILL.md directly (no task pool): {} with workflow mode: {}",
skill_file.name, self.workflow_mode
);
let skills_registry = self.get_skills_registry();
let instances_registry = self.get_instances_registry();
let mut executor = self.workflow_executor.clone();
if let Some(cb) = callback {
executor = executor.with_callback(cb);
}
executor
.execute_skill_md(
&self.scheduler,
&skill_file,
params.as_ref(),
&skills_registry,
&instances_registry,
)
.await
}
pub async fn direct_handle_skill_md_batch(
&self,
tasks: Vec<(
String,
Option<HashMap<String, Value>>,
Option<Arc<dyn WorkflowCallback>>,
)>,
) -> Vec<String> {
if tasks.is_empty() {
return Vec::new();
}
info!(
"Executing {} SKILL.md files directly (no task pool) with workflow mode: {:?}",
tasks.len(),
self.workflow_mode
);
let mut handles = Vec::new();
for (skill_md_path, params, callback) in tasks {
let self_clone = self.clone();
let handle = tokio::spawn(async move {
self_clone
.direct_handle_skill_md(&skill_md_path, params, callback)
.await
});
handles.push(handle);
}
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(format!("{}: {}", t!("error.task_panic"), e)),
}
}
results
}
pub fn get_task_status(&self, task_id: &str) -> Option<TaskStatus> {
futures::executor::block_on(tasks::get_task_status(task_id))
}
pub fn get_task(&self, task_id: &str) -> Option<tasks::Task> {
futures::executor::block_on(tasks::get_task(task_id))
}
pub fn cancel_task(&self, task_id: &str) -> bool {
futures::executor::block_on(tasks::cancel_task(task_id))
}
pub fn pause_task(&self, task_id: &str) -> bool {
futures::executor::block_on(tasks::pause_task(task_id))
}
pub fn resume_task(&self, task_id: &str) -> bool {
futures::executor::block_on(tasks::resume_task(task_id))
}
pub fn retry_task(&self, task_id: &str) -> bool {
futures::executor::block_on(tasks::retry_task(task_id))
}
pub fn list_atomic_skills(&self) -> String {
let skills = crate::executors::registry::list_skills();
if skills.is_empty() {
return t!("skill.no_skills_available").to_string();
}
let mut result = String::new();
for name in skills {
if let Some(skill) = crate::executors::registry::get_skill(&name) {
let emoji = match skill.category() {
"file" => "📁",
"net" => "🌐",
"math" => "🔢",
"time" => "🕐",
"system" => "💻",
"db" => "🗄️",
"devops" => "🚀",
"document" => "📄",
"message" => "💬",
"task" => "⏰",
_ => "⚙️",
};
result.push_str(&format!(
" {} - **{}**: {}\n",
emoji,
name,
skill.description()
));
}
}
result
}
pub fn list_skill_md_files(&self, skills_dir: &str) -> String {
match SkillLoader::load_all(skills_dir) {
Ok(skills) => {
if skills.is_empty() {
return t!("skill.no_skill_md_available").to_string();
}
let mut result = String::new();
for skill in skills {
let emoji = skill
.metadata
.as_ref()
.and_then(|m| m.emoji.as_ref())
.map(|e| e.as_str())
.unwrap_or("📋");
result.push_str(&format!(
" {} - **{}**: {}\n",
emoji, skill.name, skill.description
));
}
result
}
Err(e) => format!("{}: {}", t!("error.list_skills_failed"), e),
}
}
pub fn get_atomic_skill_names(&self) -> Vec<String> {
crate::executors::registry::list_skills()
}
pub fn get_skill_md_names(&self, skills_dir: &str) -> Vec<String> {
match SkillLoader::load_all(skills_dir) {
Ok(skills) => skills.into_iter().map(|s| s.name).collect(),
Err(_) => Vec::new(),
}
}
pub fn has_atomic_skills(&self) -> bool {
!crate::executors::registry::list_skills().is_empty()
}
pub fn executor(&self) -> &Executor {
&self.executor
}
pub fn scheduler(&self) -> &SkillScheduler {
&self.scheduler
}
pub fn workflow_mode(&self) -> WorkflowMode {
self.workflow_mode
}
pub fn update_config<F>(&self, f: F) -> anyhow::Result<()>
where
F: FnOnce(&mut HippoxConfig),
{
crate::config::update_config(f)
}
pub fn get_config(&self) -> HippoxConfig {
crate::config::get_config()
}
}