ai_agent/utils/plugins/
load_plugin_output_styles.rs1#![allow(dead_code)]
3
4use std::collections::HashSet;
5use std::path::Path;
6use std::sync::Mutex;
7
8use once_cell::sync::Lazy;
9
10use super::frontmatter_parser::parse_frontmatter;
11use super::loader::load_all_plugins_cache_only;
12use super::walk_plugin_markdown::{WalkPluginMarkdownOpts, walk_plugin_markdown};
13
14static OUTPUT_STYLE_CACHE: Lazy<Mutex<Option<Vec<OutputStyleConfig>>>> =
15 Lazy::new(|| Mutex::new(None));
16
17#[derive(Clone, Debug)]
19pub struct OutputStyleConfig {
20 pub name: String,
21 pub description: String,
22 pub prompt: String,
23 pub source: String,
24 pub force_for_plugin: Option<bool>,
25}
26
27async fn load_output_styles_from_directory(
29 output_styles_path: &Path,
30 plugin_name: &str,
31 loaded_paths: &mut HashSet<String>,
32) -> Result<Vec<OutputStyleConfig>, Box<dyn std::error::Error + Send + Sync>> {
33 use std::sync::Arc;
34 use tokio::sync::Mutex;
35
36 let styles: Arc<Mutex<Vec<OutputStyleConfig>>> = Arc::new(Mutex::new(Vec::new()));
37
38 walk_plugin_markdown(
39 output_styles_path,
40 |full_path, _namespace| {
41 let plugin_name = plugin_name.to_string();
42 let styles = Arc::clone(&styles);
43
44 Box::pin(async move {
45 match load_output_style_from_file(&full_path, &plugin_name, &mut HashSet::new())
46 .await
47 {
48 Ok(Some(style)) => styles.lock().await.push(style),
49 _ => {}
50 }
51 })
52 },
53 WalkPluginMarkdownOpts {
54 stop_at_skill_dir: Some(false),
55 log_label: Some("output-styles".to_string()),
56 },
57 )
58 .await
59 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
60
61 Ok(Arc::try_unwrap(styles).unwrap().into_inner())
62}
63
64async fn load_output_style_from_file(
66 file_path: &str,
67 plugin_name: &str,
68 loaded_paths: &mut HashSet<String>,
69) -> Result<Option<OutputStyleConfig>, Box<dyn std::error::Error + Send + Sync>> {
70 if loaded_paths.contains(file_path) {
71 return Ok(None);
72 }
73 loaded_paths.insert(file_path.to_string());
74
75 let content = tokio::fs::read_to_string(file_path)
76 .await
77 .map_err(|e| format!("Failed to read {}: {}", file_path, e))?;
78 let (frontmatter, markdown_content) = parse_frontmatter(&content, file_path);
79
80 let file_name = std::path::Path::new(file_path)
81 .file_stem()
82 .map(|s| s.to_string_lossy().to_string())
83 .unwrap_or_default();
84
85 let base_style_name = frontmatter
86 .get("name")
87 .and_then(|v| v.as_str())
88 .unwrap_or(&file_name);
89
90 let name = format!("{}:{}", plugin_name, base_style_name);
91
92 let description = frontmatter
93 .get("description")
94 .and_then(|v| v.as_str())
95 .unwrap_or(&format!("Output style from {} plugin", plugin_name))
96 .to_string();
97
98 let force_for_plugin = frontmatter.get("force-for-plugin").and_then(|v| match v {
99 serde_json::Value::Bool(b) => Some(*b),
100 serde_json::Value::String(s) => match s.as_str() {
101 "true" => Some(true),
102 "false" => Some(false),
103 _ => None,
104 },
105 _ => None,
106 });
107
108 Ok(Some(OutputStyleConfig {
109 name,
110 description,
111 prompt: markdown_content.trim().to_string(),
112 source: "plugin".to_string(),
113 force_for_plugin,
114 }))
115}
116
117pub async fn load_plugin_output_styles()
119-> Result<Vec<OutputStyleConfig>, Box<dyn std::error::Error + Send + Sync>> {
120 {
121 let cache = OUTPUT_STYLE_CACHE.lock().unwrap();
122 if let Some(ref styles) = *cache {
123 return Ok(styles.clone());
124 }
125 }
126
127 let plugin_result = load_all_plugins_cache_only().await?;
128 let mut all_styles = Vec::new();
129
130 for plugin in &plugin_result.enabled {
131 let mut loaded_paths = HashSet::new();
132
133 if let Some(ref output_styles_path) = plugin.output_styles_path {
135 match load_output_styles_from_directory(
136 Path::new(output_styles_path),
137 &plugin.name,
138 &mut loaded_paths,
139 )
140 .await
141 {
142 Ok(styles) => {
143 log::debug!(
144 "Loaded {} output styles from plugin {} default directory",
145 styles.len(),
146 plugin.name
147 );
148 all_styles.extend(styles);
149 }
150 Err(e) => log::debug!(
151 "Failed to load output styles from plugin {} default directory: {}",
152 plugin.name,
153 e
154 ),
155 }
156 }
157
158 if let Some(ref output_styles_paths) = plugin.output_styles_paths {
160 for style_path in output_styles_paths {
161 let metadata = match tokio::fs::metadata(style_path).await {
162 Ok(m) => m,
163 Err(_) => continue,
164 };
165
166 if metadata.is_dir() {
167 if let Ok(styles) = load_output_styles_from_directory(
168 Path::new(style_path),
169 &plugin.name,
170 &mut loaded_paths,
171 )
172 .await
173 {
174 all_styles.extend(styles);
175 }
176 } else if metadata.is_file()
177 && Path::new(style_path)
178 .extension()
179 .map(|e| e.to_string_lossy() == "md")
180 .unwrap_or(false)
181 {
182 if let Ok(Some(style)) =
183 load_output_style_from_file(style_path, &plugin.name, &mut loaded_paths)
184 .await
185 {
186 all_styles.push(style);
187 }
188 }
189 }
190 }
191 }
192
193 log::debug!("Total plugin output styles loaded: {}", all_styles.len());
194
195 {
196 let mut cache = OUTPUT_STYLE_CACHE.lock().unwrap();
197 *cache = Some(all_styles.clone());
198 }
199
200 Ok(all_styles)
201}
202
203pub fn clear_plugin_output_style_cache() {
205 let mut cache = OUTPUT_STYLE_CACHE.lock().unwrap();
206 *cache = None;
207}