mofa_foundation/prompt/
plugin.rs1use 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#[async_trait::async_trait]
23pub trait PromptTemplatePlugin: Send + Sync {
24 async fn get_prompt_template(&self, scenario: &str) -> Option<Arc<PromptTemplate>>;
26
27 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 async fn get_active_scenario(&self) -> String;
35
36 async fn set_active_scenario(&self, scenario: &str);
38
39 async fn get_available_scenarios(&self) -> Vec<String>;
41
42 async fn refresh_templates(&self) -> PluginResult<()>;
44}
45
46pub struct RhaiScriptPromptPlugin {
48 script_path: PathBuf,
50 registry: Arc<RwLock<PromptRegistry>>,
52 active_scenario: RwLock<String>,
54}
55
56impl RhaiScriptPromptPlugin {
57 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 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 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 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 registry.clear();
117
118 if !self.script_path.exists() {
120 tracing::warn!("Script path does not exist: {:?}", self.script_path);
121 return Ok(());
122 }
123
124 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 if path.is_file() && path.extension().is_some_and(|ext| ext == "rhai") {
133 tracing::info!("Loading prompt template from: {:?}", path);
134
135 let script = fs::read_to_string(&path)?;
137
138 let engine = Engine::new();
140
141 let script = format!(
143 "
144 let template = {};
145 template
146 ",
147 script
148 );
149
150 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 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 let json_str = rhai::format_map_as_json(&template_obj);
170
171 match serde_json::from_str::<PromptTemplate>(&json_str) {
173 Ok(template) => {
174 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 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 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 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}