Skip to main content

synapto_interface/
plugin.rs

1use crate::audio_recorder::AudioRecorderPlugin;
2use crate::call::CallPlugin;
3use crate::camera::CameraPlugin;
4use crate::chat::ChatPlugin;
5use crate::cognitive_output_audio::AudioOutputPlugin;
6use crate::document::{DocumentProviderPlugin, DocumentsPlugin};
7use crate::gui::GuiPlugin;
8use crate::interaction::{InteractionObserver, RetrospectiveConsolidationPlugin};
9use crate::peer_input_audio::AudioInputPlugin;
10use crate::rollout::RolloutController;
11use crate::speech_to_text::{DiarizationPlugin, STTPlugin, TTSPlugin};
12use async_trait::async_trait;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16pub struct PluginInitContext<'a> {
17    llm_executor: std::sync::Arc<dyn crate::llm::LlmExecutor>,
18    plugin_config: &'a serde_json::Value,
19    storage: std::sync::Arc<crate::storage::StorageRegistry>,
20    plugin_namespace: &'a str,
21    storage_config_resolver: std::sync::Arc<dyn crate::storage::StorageConfigResolver>,
22}
23
24impl<'a> PluginInitContext<'a> {
25    #[doc = " Internal constructor used by the Core AI engine."]
26    #[doc(hidden)]
27    pub fn new(
28        llm_executor: std::sync::Arc<dyn crate::llm::LlmExecutor>,
29        plugin_config: &'a serde_json::Value,
30        storage: std::sync::Arc<crate::storage::StorageRegistry>,
31        plugin_namespace: &'a str,
32        storage_config_resolver: std::sync::Arc<dyn crate::storage::StorageConfigResolver>,
33    ) -> Self {
34        Self {
35            llm_executor,
36            plugin_config,
37            storage,
38            plugin_namespace,
39            storage_config_resolver,
40        }
41    }
42
43    #[doc = " Deserializes the raw JSON configuration into the plugin's requested config struct."]
44    #[doc = ""]
45    #[doc = " **Note on Serde Configuration Defaults:**"]
46    #[doc = " This performs strict structural deserialization. If the JSON object provided by the"]
47    #[doc = " `ConfigProvider` is missing a field that your Rust struct (which must derive `Deserialize`) expects, `serde` will"]
48    #[doc = " return a `missing field` error — even if your struct implements `Default`."]
49    #[doc = ""]
50    #[doc = " To make a configuration field optional, use the `#[serde(default)]`"]
51    #[doc = " attribute on the struct field. This instructs `serde` to fall back to `Default::default()`"]
52    #[doc = " when the key is omitted."]
53    pub fn config<C: serde::de::DeserializeOwned>(&self) -> Result<C, String> {
54        serde_json::from_value(self.plugin_config.clone())
55            .map_err(|e| format!("Failed to parse plugin config: {}", e))
56    }
57
58    #[doc = " Extracts the configuration if present, or returns `None` if the configuration is completely empty or null."]
59    #[doc = " This allows plugins to have a mandatory configuration schema when provided, but remain optional overall."]
60    pub fn optional_config<C: serde::de::DeserializeOwned>(&self) -> Result<Option<C>, String> {
61        if self.plugin_config.is_null()
62            || self.plugin_config.as_object().is_some_and(|m| m.is_empty())
63        {
64            return Ok(None);
65        }
66        self.config().map(Some)
67    }
68
69    #[doc = " Initializes and returns a database connection scoped strictly to this plugin's namespace."]
70    pub async fn store<S: crate::storage::StorageConnection>(&self) -> Result<S, String> {
71        let full_path = std::any::type_name::<S>();
72        let crate_name = full_path
73            .split("::")
74            .next()
75            .unwrap_or("")
76            .to_string()
77            .replace('-', "_");
78        let base_path = full_path.split('<').next().unwrap_or(full_path);
79        let storage_type_name = base_path.split("::").last().unwrap_or("").to_string();
80        let config_val = self
81            .storage_config_resolver
82            .resolve_config(&crate_name, &storage_type_name)
83            .unwrap_or_else(|| serde_json::json!({}));
84        let config: S::Config = serde_json::from_value(config_val).map_err(|e| {
85            format!(
86                "Failed to parse config for storage '{}::{}': {}",
87                crate_name, storage_type_name, e
88            )
89        })?;
90        S::connect(config, self.storage.clone(), self.plugin_namespace).await
91    }
92
93    pub fn llm_executor(&self) -> std::sync::Arc<dyn crate::llm::LlmExecutor> {
94        self.llm_executor.clone()
95    }
96}
97
98pub trait PluginRegistry {
99    fn register_gui<P: GuiPlugin>(&mut self, plugin: std::sync::Arc<P>);
100    fn register_audio_input<P: AudioInputPlugin>(&mut self, plugin: std::sync::Arc<P>);
101    fn register_audio_output<P: AudioOutputPlugin>(&mut self, plugin: std::sync::Arc<P>);
102    fn register_stt<P: STTPlugin>(&mut self, plugin: std::sync::Arc<P>);
103    fn register_tts<P: TTSPlugin>(&mut self, plugin: std::sync::Arc<P>);
104    fn register_diarization<P: DiarizationPlugin>(&mut self, plugin: std::sync::Arc<P>);
105    fn register_chat<P: ChatPlugin>(&mut self, plugin: std::sync::Arc<P>);
106    fn register_documents<P: DocumentsPlugin>(&mut self, plugin: std::sync::Arc<P>);
107    fn register_document_provider<P: DocumentProviderPlugin>(&mut self, plugin: std::sync::Arc<P>);
108    fn register_interaction_observer<P: InteractionObserver>(&mut self, plugin: std::sync::Arc<P>);
109    fn register_rollout_controller<P: RolloutController>(&mut self, plugin: std::sync::Arc<P>);
110    fn register_retrospective_consolidation<P: RetrospectiveConsolidationPlugin>(
111        &mut self,
112        plugin: std::sync::Arc<P>,
113    );
114    fn register_camera<P: CameraPlugin>(&mut self, plugin: std::sync::Arc<P>);
115    fn register_context_provider<P: crate::context::ContextProvider>(
116        &mut self,
117        provider: std::sync::Arc<P>,
118    );
119    fn register_command<C: crate::command::Command>(&mut self, command: C);
120    #[doc = " Registers a static tool implementation."]
121    fn register_tool<T: crate::tool::Tool>(&mut self, tool: T);
122    #[doc = " Registers a type-erased tool implementation (e.g. dynamically discovered at runtime)."]
123    fn register_erased_tool(&mut self, tool: std::sync::Arc<dyn crate::tool::ErasedTool>);
124    fn register_call<P: CallPlugin>(
125        &mut self,
126        plugin: std::sync::Arc<P>,
127        capability: Option<&'static str>,
128    );
129    fn register_recorder<P: AudioRecorderPlugin>(&mut self, plugin: std::sync::Arc<P>);
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct EmptyPluginConfig {}
134
135#[async_trait]
136pub trait Plugin: Send + Sync + 'static {
137    #[doc = " Compile-time semantic description of this plugin's capability for the LLM."]
138    const CAPABILITY: Option<&'static str> = None;
139    #[doc = " This is the method for instantiating plugins, allowing them to await"]
140    #[doc = " their database connections (via `context.store::<S>().await`) before returning."]
141    #[doc = ""]
142    #[doc = " **Note on Configuration:** Configuration in Synapto plugins is entirely optional."]
143    #[doc = " If your plugin requires no configuration, simply do not define a config struct"]
144    #[doc = " and do not call `context.config()?`."]
145    #[doc = ""]
146    #[doc = " If you do define a config struct: when calling `context.config()?` to extract your configuration,"]
147    #[doc = " ensure any optional fields in your struct are marked with `#[serde(default)]`. Otherwise,"]
148    #[doc = " omitted fields in the config file will cause strict deserialization errors."]
149    #[doc = " Alternatively, use `context.optional_config()?` to allow the entire config block to be safely omitted."]
150    async fn create(context: &crate::plugin::PluginInitContext<'_>) -> Result<Self, String>
151    where
152        Self: Sized;
153    fn register<R: PluginRegistry + ?Sized>(self: std::sync::Arc<Self>, registry: &mut R)
154    where
155        Self: Sized;
156}
157
158#[doc = " An opaque channel identifier used to route messages within the system."]
159#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
160pub struct MessageChannel {
161    #[doc = " Opaque JSON context provided by plugins or core modules."]
162    pub context: serde_json::Value,
163}