Skip to main content

mofa_foundation/prompt/
plugin.rs

1//! Prompt Template Plugin
2//!
3//! 提供基于插件的动态 Prompt 模板管理功能
4//!
5//! # 示例
6//!
7//! ```rust,ignore
8//! // 创建一个基于 Rhai 脚本的 Prompt 模板插件
9//! let plugin = RhaiScriptPromptPlugin::new(Path::new("./prompts/"));
10//!
11//! // 添加到 Agent
12//! agent.add_plugin(Box::new(plugin));
13//! ```
14use crate::prompt::{PromptRegistry, PromptTemplate};
15use mofa_kernel::plugin::PluginResult;
16use rhai::Engine;
17use std::path::PathBuf;
18use std::sync::Arc;
19use tokio::sync::RwLock;
20
21/// Prompt 模板插件 trait
22#[async_trait::async_trait]
23pub trait PromptTemplatePlugin: Send + Sync {
24    /// 获取当前场景的 Prompt 模板
25    async fn get_prompt_template(&self, scenario: &str) -> Option<Arc<PromptTemplate>>;
26
27    /// 获取当前活动场景的模板
28    async fn get_current_template(&self) -> Option<Arc<PromptTemplate>> {
29        let active = self.get_active_scenario().await;
30        self.get_prompt_template(&active).await
31    }
32
33    /// 获取当前活动场景
34    async fn get_active_scenario(&self) -> String;
35
36    /// 设置当前活动的场景
37    async fn set_active_scenario(&self, scenario: &str);
38
39    /// 获取所有可用的场景
40    async fn get_available_scenarios(&self) -> Vec<String>;
41
42    /// 刷新模板
43    async fn refresh_templates(&self) -> PluginResult<()>;
44}
45
46/// 基于 Rhai 脚本的 Prompt 模板插件
47pub struct RhaiScriptPromptPlugin {
48    /// 脚本文件夹路径
49    script_path: PathBuf,
50    /// Prompt 注册中心
51    registry: Arc<RwLock<PromptRegistry>>,
52    /// 当前活动的场景
53    active_scenario: RwLock<String>,
54}
55
56impl RhaiScriptPromptPlugin {
57    /// 创建新的 Rhai 脚本 Prompt 模板插件
58    pub fn new(script_path: impl Into<PathBuf>) -> Self {
59        Self {
60            script_path: script_path.into(),
61            registry: Arc::new(RwLock::new(PromptRegistry::new())),
62            active_scenario: RwLock::new("default".to_string()),
63        }
64    }
65
66    /// 设置当前活动的场景
67    pub async fn set_active_scenario(&self, scenario: impl Into<String>) {
68        let mut active = self.active_scenario.write().await;
69        *active = scenario.into();
70    }
71
72    /// 获取当前活动场景的模板
73    pub async fn get_current_template(&self) -> Option<Arc<PromptTemplate>> {
74        let active = self.active_scenario.read().await;
75        self.get_prompt_template(&active).await
76    }
77
78    /// 获取脚本文件夹路径
79    pub fn script_path(&self) -> &PathBuf {
80        &self.script_path
81    }
82}
83
84#[async_trait::async_trait]
85impl PromptTemplatePlugin for RhaiScriptPromptPlugin {
86    async fn get_prompt_template(&self, scenario: &str) -> Option<Arc<PromptTemplate>> {
87        let registry = self.registry.read().await;
88        registry.get(scenario).cloned().ok().map(Arc::new)
89    }
90
91    async fn get_active_scenario(&self) -> String {
92        let active = self.active_scenario.read().await;
93        active.clone()
94    }
95
96    async fn set_active_scenario(&self, scenario: &str) {
97        let mut active = self.active_scenario.write().await;
98        *active = scenario.to_string();
99    }
100
101    async fn get_available_scenarios(&self) -> Vec<String> {
102        let registry = self.registry.read().await;
103        registry
104            .list_ids()
105            .into_iter()
106            .map(|id| id.to_string())
107            .collect()
108    }
109
110    async fn refresh_templates(&self) -> PluginResult<()> {
111        use std::fs;
112
113        let mut registry = self.registry.write().await;
114
115        // Clear the registry to avoid duplicates
116        registry.clear();
117
118        // Check if the script path exists
119        if !self.script_path.exists() {
120            tracing::warn!("Script path does not exist: {:?}", self.script_path);
121            return Ok(());
122        }
123
124        // Read all .rhai files from the directory
125        let entries = fs::read_dir(&self.script_path)?;
126
127        for entry in entries {
128            let entry = entry?;
129            let path = entry.path();
130
131            // Process only Rhai files
132            if path.is_file() && path.extension().is_some_and(|ext| ext == "rhai") {
133                tracing::info!("Loading prompt template from: {:?}", path);
134
135                // Read the script content
136                let script = fs::read_to_string(&path)?;
137
138                // Create a Rhai engine
139                let engine = Engine::new();
140
141                // Wrap the script to return the template object
142                let script = format!(
143                    "
144                    let template = {};
145                    template
146                ",
147                    script
148                );
149
150                // Evaluate the script to get Rhai Dynamic object
151                let template_dyn: rhai::Dynamic = match engine.eval(&script) {
152                    Ok(obj) => obj,
153                    Err(e) => {
154                        tracing::warn!("Failed to evaluate Rhai script: {:?}, error: {}", path, e);
155                        continue;
156                    }
157                };
158
159                // Convert Dynamic to Map
160                let template_obj = match template_dyn.as_map_ref() {
161                    Ok(map) => map,
162                    Err(_) => {
163                        tracing::warn!("Rhai script did not return a Map: {:?}", path);
164                        continue;
165                    }
166                };
167
168                // Convert Rhai Map to JSON string
169                let json_str = rhai::format_map_as_json(&template_obj);
170
171                // Parse into PromptTemplate
172                match serde_json::from_str::<PromptTemplate>(&json_str) {
173                    Ok(template) => {
174                        // Register the template in the registry
175                        registry.register(template.clone());
176                        tracing::info!("Successfully registered prompt template: {}", template.id);
177                    }
178                    Err(e) => {
179                        tracing::warn!("Failed to parse prompt template: {:?}, error: {}", path, e);
180                        continue;
181                    }
182                }
183            }
184        }
185
186        tracing::info!(
187            "Successfully refreshed prompt templates from path: {:?}",
188            self.script_path
189        );
190        Ok(())
191    }
192}
193
194#[async_trait::async_trait]
195impl mofa_kernel::plugin::AgentPlugin for RhaiScriptPromptPlugin {
196    fn metadata(&self) -> &mofa_kernel::plugin::PluginMetadata {
197        use mofa_kernel::plugin::{PluginMetadata, PluginType};
198
199        lazy_static::lazy_static! {
200            static ref METADATA: PluginMetadata = PluginMetadata::new(
201                "rhai-prompt-template-plugin",
202                "Rhai Prompt Template Plugin",
203                PluginType::Tool
204            )
205            .with_capability("prompt-template");
206        }
207
208        &METADATA
209    }
210
211    fn state(&self) -> mofa_kernel::plugin::PluginState {
212        mofa_kernel::plugin::PluginState::Loaded
213    }
214
215    async fn load(
216        &mut self,
217        _ctx: &mofa_kernel::plugin::PluginContext,
218    ) -> mofa_kernel::plugin::PluginResult<()> {
219        // Load templates on plugin load
220        self.refresh_templates().await?;
221        Ok(())
222    }
223
224    async fn init_plugin(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
225        Ok(())
226    }
227
228    async fn start(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
229        Ok(())
230    }
231
232    async fn stop(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
233        Ok(())
234    }
235
236    async fn unload(&mut self) -> mofa_kernel::plugin::PluginResult<()> {
237        Ok(())
238    }
239
240    async fn execute(&mut self, input: String) -> mofa_kernel::plugin::PluginResult<String> {
241        // Parse input to decide what to do
242        // This could support commands like "set_scenario:promotion" or "get_template:outage"
243        if input.starts_with("set_scenario:") {
244            let scenario = input
245                .strip_prefix("set_scenario:")
246                .ok_or_else(|| anyhow::anyhow!("Invalid scenario"))?;
247            self.set_active_scenario(scenario).await;
248            Ok(format!("Successfully switched to scenario: {}", scenario))
249        } else if input.starts_with("get_template:") {
250            let scenario = input
251                .strip_prefix("get_template:")
252                .ok_or_else(|| anyhow::anyhow!("Invalid scenario"))?;
253            if let Some(template) = self.get_prompt_template(scenario).await {
254                Ok(serde_json::to_string(&template)?)
255            } else {
256                Ok(format!("Template not found: {}", scenario))
257            }
258        } else if input == "list_scenarios" {
259            let scenarios = self.get_available_scenarios().await;
260            Ok(serde_json::to_string(&scenarios)?)
261        } else if input == "refresh_templates" {
262            self.refresh_templates().await?;
263            Ok("Successfully refreshed templates".to_string())
264        } else {
265            // Default: return current template
266            if let Some(template) = self.get_current_template().await {
267                Ok(template.content.clone())
268            } else {
269                Ok("No active template found".to_string())
270            }
271        }
272    }
273
274    fn as_any(&self) -> &dyn std::any::Any {
275        self
276    }
277
278    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
279        self
280    }
281
282    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
283        self
284    }
285}