use crate::prompt::{PromptRegistry, PromptTemplate};
use mofa_kernel::plugin::PluginResult;
use rhai::Engine;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
#[async_trait::async_trait]
pub trait PromptTemplatePlugin: Send + Sync {
async fn get_prompt_template(&self, scenario: &str) -> Option<Arc<PromptTemplate>>;
async fn get_current_template(&self) -> Option<Arc<PromptTemplate>> {
let active = self.get_active_scenario().await;
self.get_prompt_template(&active).await
}
async fn get_active_scenario(&self) -> String;
async fn set_active_scenario(&self, scenario: &str);
async fn get_available_scenarios(&self) -> Vec<String>;
async fn refresh_templates(&self) -> PluginResult<()>;
}
pub struct RhaiScriptPromptPlugin {
script_path: PathBuf,
registry: Arc<RwLock<PromptRegistry>>,
active_scenario: RwLock<String>,
}
impl RhaiScriptPromptPlugin {
pub fn new(script_path: impl Into<PathBuf>) -> Self {
Self {
script_path: script_path.into(),
registry: Arc::new(RwLock::new(PromptRegistry::new())),
active_scenario: RwLock::new("default".to_string()),
}
}
pub async fn set_active_scenario(&self, scenario: impl Into<String>) {
let mut active = self.active_scenario.write().await;
*active = scenario.into();
}
pub async fn get_current_template(&self) -> Option<Arc<PromptTemplate>> {
let active = self.active_scenario.read().await;
self.get_prompt_template(&active).await
}
pub fn script_path(&self) -> &PathBuf {
&self.script_path
}
}
#[async_trait::async_trait]
impl PromptTemplatePlugin for RhaiScriptPromptPlugin {
async fn get_prompt_template(&self, scenario: &str) -> Option<Arc<PromptTemplate>> {
let registry = self.registry.read().await;
registry.get(scenario).cloned().ok().map(Arc::new)
}
async fn get_active_scenario(&self) -> String {
let active = self.active_scenario.read().await;
active.clone()
}
async fn set_active_scenario(&self, scenario: &str) {
let mut active = self.active_scenario.write().await;
*active = scenario.to_string();
}
async fn get_available_scenarios(&self) -> Vec<String> {
let registry = self.registry.read().await;
registry
.list_ids()
.into_iter()
.map(|id| id.to_string())
.collect()
}
async fn refresh_templates(&self) -> PluginResult<()> {
use std::fs;
let mut registry = self.registry.write().await;
registry.clear();
if !self.script_path.exists() {
tracing::warn!("Script path does not exist: {:?}", self.script_path);
return Ok(());
}
let entries = fs::read_dir(&self.script_path)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "rhai") {
tracing::info!("Loading prompt template from: {:?}", path);
let script = fs::read_to_string(&path)?;
let engine = Engine::new();
let script = format!(
"
let template = {};
template
",
script
);
let template_dyn: rhai::Dynamic = match engine.eval(&script) {
Ok(obj) => obj,
Err(e) => {
tracing::warn!("Failed to evaluate Rhai script: {:?}, error: {}", path, e);
continue;
}
};
let template_obj = match template_dyn.as_map_ref() {
Ok(map) => map,
Err(_) => {
tracing::warn!("Rhai script did not return a Map: {:?}", path);
continue;
}
};
let json_str = rhai::format_map_as_json(&template_obj);
match serde_json::from_str::<PromptTemplate>(&json_str) {
Ok(template) => {
registry.register(template.clone());
tracing::info!("Successfully registered prompt template: {}", template.id);
}
Err(e) => {
tracing::warn!("Failed to parse prompt template: {:?}, error: {}", path, e);
continue;
}
}
}
}
tracing::info!(
"Successfully refreshed prompt templates from path: {:?}",
self.script_path
);
Ok(())
}
}
#[async_trait::async_trait]
impl mofa_kernel::plugin::AgentPlugin for RhaiScriptPromptPlugin {
fn metadata(&self) -> &mofa_kernel::plugin::PluginMetadata {
use mofa_kernel::plugin::{PluginMetadata, PluginType};
lazy_static::lazy_static! {
static ref METADATA: PluginMetadata = PluginMetadata::new(
"rhai-prompt-template-plugin",
"Rhai Prompt Template Plugin",
PluginType::Tool
)
.with_capability("prompt-template");
}
&METADATA
}
fn state(&self) -> mofa_kernel::plugin::PluginState {
mofa_kernel::plugin::PluginState::Loaded
}
async fn load(
&mut self,
_ctx: &mofa_kernel::plugin::PluginContext,
) -> mofa_kernel::plugin::PluginResult<()> {
self.refresh_templates().await?;
Ok(())
}
async fn init_plugin(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
Ok(())
}
async fn start(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
Ok(())
}
async fn stop(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
Ok(())
}
async fn unload(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
Ok(())
}
async fn execute(&mut self, input: String) -> mofa_kernel::plugin::PluginResult<String> {
if input.starts_with("set_scenario:") {
let scenario = input
.strip_prefix("set_scenario:")
.ok_or_else(|| anyhow::anyhow!("Invalid scenario"))?;
self.set_active_scenario(scenario).await;
Ok(format!("Successfully switched to scenario: {}", scenario))
} else if input.starts_with("get_template:") {
let scenario = input
.strip_prefix("get_template:")
.ok_or_else(|| anyhow::anyhow!("Invalid scenario"))?;
if let Some(template) = self.get_prompt_template(scenario).await {
Ok(serde_json::to_string(&template)?)
} else {
Ok(format!("Template not found: {}", scenario))
}
} else if input == "list_scenarios" {
let scenarios = self.get_available_scenarios().await;
Ok(serde_json::to_string(&scenarios)?)
} else if input == "refresh_templates" {
self.refresh_templates().await?;
Ok("Successfully refreshed templates".to_string())
} else {
if let Some(template) = self.get_current_template().await {
Ok(template.content.clone())
} else {
Ok("No active template found".to_string())
}
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
self
}
}