moly_kit/
protocol.rs

1use crate::mcp_manager::McpManagerClient;
2use makepad_widgets::{Cx, LiveDependency, LiveId, LivePtr, WidgetRef};
3
4// Re-export relevant, protocol related, async types.
5pub use crate::utils::asynchronous::{BoxPlatformSendFuture, BoxPlatformSendStream};
6
7#[derive(Clone, Debug, PartialEq)]
8#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
9pub struct Tool {
10    pub name: String,
11    pub description: Option<String>,
12    /// JSON Schema object defining the expected parameters for the tool
13    #[cfg_attr(feature = "json", serde(default))]
14    pub input_schema: std::sync::Arc<serde_json::Map<String, serde_json::Value>>,
15}
16
17impl Tool {
18    pub fn new(name: String, description: Option<String>) -> Self {
19        use serde_json::Map;
20        use std::sync::Arc;
21
22        Tool {
23            name,
24            description,
25            input_schema: Arc::new(Map::new()),
26        }
27    }
28}
29
30// Conversion traits for rmcp interop on native platforms
31#[cfg(not(target_arch = "wasm32"))]
32impl From<rmcp::model::Tool> for Tool {
33    fn from(rmcp_tool: rmcp::model::Tool) -> Self {
34        Tool {
35            name: rmcp_tool.name.into_owned(),
36            description: rmcp_tool.description.map(|d| d.into_owned()),
37            input_schema: rmcp_tool.input_schema,
38        }
39    }
40}
41
42#[cfg(not(target_arch = "wasm32"))]
43impl From<Tool> for rmcp::model::Tool {
44    fn from(tool: Tool) -> Self {
45        rmcp::model::Tool {
46            name: tool.name.into(),
47            description: tool.description.map(|d| d.into()),
48            input_schema: tool.input_schema,
49            output_schema: None,
50            annotations: None,
51        }
52    }
53}
54
55use chrono::{DateTime, Utc};
56use serde::{Deserialize, Serialize};
57use std::{
58    collections::{HashMap, HashSet},
59    error::Error,
60    fmt,
61    sync::{Arc, Mutex},
62};
63
64mod attachment;
65pub use attachment::*;
66
67/// Upgrade types for enhanced communication modes
68#[derive(Debug, Clone, PartialEq)]
69pub enum Upgrade {
70    /// Realtime audio/voice communication
71    Realtime(RealtimeChannel),
72}
73
74/// Channel for realtime communication events
75#[derive(Debug, Clone)]
76pub struct RealtimeChannel {
77    /// Sender for realtime events to the UI
78    pub event_sender: futures::channel::mpsc::UnboundedSender<RealtimeEvent>,
79    /// Receiver for realtime events from the client
80    pub event_receiver:
81        Arc<Mutex<Option<futures::channel::mpsc::UnboundedReceiver<RealtimeEvent>>>>,
82    /// Sender for commands to the realtime client
83    pub command_sender: futures::channel::mpsc::UnboundedSender<RealtimeCommand>,
84}
85
86impl PartialEq for RealtimeChannel {
87    fn eq(&self, _other: &Self) -> bool {
88        // For now, we'll consider all channels equal since we can't compare the actual channels
89        true
90    }
91}
92
93/// Events sent from the realtime client to the UI
94#[derive(Debug, Clone)]
95pub enum RealtimeEvent {
96    /// Session is ready for communication
97    SessionReady,
98    /// Audio data received (PCM16 format)
99    AudioData(Vec<u8>),
100    /// Text transcript of received audio (delta)
101    AudioTranscript(String),
102    /// Complete AI audio transcript
103    AudioTranscriptCompleted(String, String), // (transcript, item_id)
104    /// Complete user audio transcript
105    UserTranscriptCompleted(String, String), // (transcript, item_id)
106    /// User started speaking
107    SpeechStarted,
108    /// User stopped speaking
109    SpeechStopped,
110    /// AI response completed
111    ResponseCompleted,
112    /// Function call requested by AI
113    FunctionCallRequest {
114        name: String,
115        call_id: String,
116        arguments: String,
117    },
118    /// Error occurred
119    Error(String),
120}
121
122/// Commands sent from the UI to the realtime client
123#[derive(Debug, Clone)]
124pub enum RealtimeCommand {
125    /// Stop the realtime session
126    StopSession,
127    /// Send audio data (PCM16 format)
128    SendAudio(Vec<u8>),
129    /// Send text message
130    SendText(String),
131    /// Interrupt current AI response
132    Interrupt,
133    /// Update session configuration
134    UpdateSessionConfig {
135        voice: String,
136        transcription_model: String,
137    },
138    /// Create a greeting response from AI
139    CreateGreetingResponse,
140    /// Send function call result back to AI
141    SendFunctionCallResult { call_id: String, output: String },
142}
143
144/// The picture/avatar of an entity that may be represented/encoded in different ways.
145#[derive(Clone, Debug)]
146pub enum Picture {
147    // TODO: could be reduced to avoid allocation
148    Grapheme(String),
149    Image(String),
150    // TODO: could be downed to a more concrete type
151    Dependency(LiveDependency),
152}
153
154/// Indentify the entities that are recognized by this crate, mainly in a chat.
155#[derive(Clone, PartialEq, Eq, Hash, Debug, Default)]
156#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
157pub enum EntityId {
158    /// Represents the user operating this app.
159    User,
160
161    /// Represents the `system`/`developer` expected by many LLMs in the chat
162    /// context to customize the chat experience and behavior.
163    System,
164
165    /// Represents a bot, which is an automated assistant of any kind (model, agent, etc).
166    Bot(BotId),
167
168    /// Represents tool execution results and tool-related system messages.
169    /// Maps to the "tool" role in LLM APIs.
170    Tool,
171
172    /// This app itself. Normally appears when app specific information must be displayed
173    /// (like inline errors).
174    ///
175    /// It's not supposed to be sent as part of a conversation to bots.
176    #[default]
177    App,
178}
179
180/// Represents the capabilities of a bot
181#[derive(Clone, Debug, PartialEq, Eq, Hash)]
182#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
183pub enum BotCapability {
184    /// Bot supports realtime audio communication
185    Realtime,
186    /// Bot supports image/file attachments
187    Attachments,
188    /// Bot supports function calling
189    FunctionCalling,
190}
191
192/// Set of capabilities that a bot supports
193#[derive(Clone, Debug, Default)]
194#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
195pub struct BotCapabilities {
196    capabilities: HashSet<BotCapability>,
197}
198
199impl BotCapabilities {
200    pub fn new() -> Self {
201        Self {
202            capabilities: HashSet::new(),
203        }
204    }
205
206    pub fn with_capability(mut self, capability: BotCapability) -> Self {
207        self.capabilities.insert(capability);
208        self
209    }
210
211    pub fn add_capability(&mut self, capability: BotCapability) {
212        self.capabilities.insert(capability);
213    }
214
215    pub fn has_capability(&self, capability: &BotCapability) -> bool {
216        self.capabilities.contains(capability)
217    }
218
219    pub fn supports_realtime(&self) -> bool {
220        self.has_capability(&BotCapability::Realtime)
221    }
222
223    pub fn supports_attachments(&self) -> bool {
224        self.has_capability(&BotCapability::Attachments)
225    }
226
227    pub fn supports_function_calling(&self) -> bool {
228        self.has_capability(&BotCapability::FunctionCalling)
229    }
230
231    pub fn iter(&self) -> impl Iterator<Item = &BotCapability> {
232        self.capabilities.iter()
233    }
234}
235
236#[derive(Clone, Debug)]
237pub struct Bot {
238    /// Unique internal identifier for the bot across all providers
239    pub id: BotId,
240    pub name: String,
241    pub avatar: Picture,
242    pub capabilities: BotCapabilities,
243}
244
245/// Identifies any kind of bot, local or remote, model or agent, whatever.
246///
247/// It MUST be globally unique and stable. It should be generated from a provider
248/// local id and the domain or url of that provider.
249///
250/// For serialization, this is encoded as a single string.
251#[derive(Clone, PartialEq, Eq, Hash, Debug, Default)]
252#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
253pub struct BotId(Arc<str>);
254
255impl BotId {
256    pub fn as_str(&self) -> &str {
257        &self.0
258    }
259
260    /// Creates a new bot id from a provider local id and a provider domain or url.
261    pub fn new(id: &str, provider: &str) -> Self {
262        // The id is encoded as: <id_len>;<id>@<provider>.
263        // `@` is simply a semantic separator, meaning (literally) "at".
264        // The length is what is actually used for separating components allowing
265        // these to include `@` characters.
266        let id = format!("{};{}@{}", id.len(), id, provider);
267        BotId(id.into())
268    }
269
270    fn deconstruct(&self) -> (usize, &str) {
271        let (id_length, raw) = self.0.split_once(';').expect("malformed bot id");
272        let id_length = id_length.parse::<usize>().expect("malformed bot id");
273        (id_length, raw)
274    }
275
276    /// The id of the bot as it is known by its provider.
277    ///
278    /// This may not be globally unique.
279    pub fn id(&self) -> &str {
280        let (id_length, raw) = self.deconstruct();
281        &raw[..id_length]
282    }
283
284    /// The provider component of this bot id.
285    pub fn provider(&self) -> &str {
286        let (id_length, raw) = self.deconstruct();
287        // + 1 skips the semantic `@` separator
288        &raw[id_length + 1..]
289    }
290}
291
292impl fmt::Display for BotId {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        write!(f, "{}", self.0)
295    }
296}
297
298/// Permission status for tool call execution
299#[derive(Clone, PartialEq, Debug, Default)]
300#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
301pub enum ToolCallPermissionStatus {
302    /// Waiting for user decision
303    #[default]
304    Pending,
305    /// User approved execution
306    Approved,
307    /// User denied execution
308    Denied,
309}
310
311/// Represents a function/tool call made by the AI
312#[derive(Clone, PartialEq, Debug, Default)]
313#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
314pub struct ToolCall {
315    /// Unique identifier for this tool call
316    pub id: String,
317    /// Name of the tool/function to call
318    pub name: String,
319    /// Arguments passed to the tool (JSON)
320    pub arguments: serde_json::Map<String, serde_json::Value>,
321    /// Permission status for this tool call
322    #[cfg_attr(feature = "json", serde(default))]
323    pub permission_status: ToolCallPermissionStatus,
324}
325
326/// Represents the result of a tool call execution
327#[derive(Clone, PartialEq, Debug)]
328#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
329pub struct ToolResult {
330    /// The tool call ID this result corresponds to
331    pub tool_call_id: String,
332    /// The result content from the tool execution
333    pub content: String,
334    /// Whether the tool call was successful
335    pub is_error: bool,
336}
337
338/// Standard message content format.
339#[derive(Clone, Debug, PartialEq, Default)]
340#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
341pub struct MessageContent {
342    /// The main body/document of this message.
343    ///
344    /// This would normally be written in somekind of document format like
345    /// markdown, html, plain text, etc. Only markdown is expected by default.
346    pub text: String,
347
348    /// List of citations/sources (urls) associated with this message.
349    pub citations: Vec<String>,
350
351    /// The reasoning/thinking content of this message.
352    #[cfg_attr(
353        feature = "json",
354        serde(deserialize_with = "crate::utils::serde::deserialize_default_on_error")
355    )]
356    pub reasoning: String,
357
358    /// File attachments in this content.
359    #[cfg_attr(feature = "json", serde(default))]
360    pub attachments: Vec<Attachment>,
361
362    /// Tool calls made by the AI (for assistant messages)
363    #[cfg_attr(feature = "json", serde(default))]
364    pub tool_calls: Vec<ToolCall>,
365
366    /// Tool call results (for tool messages)
367    #[cfg_attr(feature = "json", serde(default))]
368    pub tool_results: Vec<ToolResult>,
369
370    /// Non-standard data contained by this message.
371    ///
372    /// May be used by clients for tracking purposes or to represent unsupported
373    /// content.
374    ///
375    /// This is not expected to be used by most clients.
376    // TODO: Using `String` for now because:
377    //
378    // - `Box<dyn Trait>` can't be `Deserialize`.
379    // - `serde_json::Value` would force `serde_json` usage.
380    // - `Vec<u8>` has unefficient serialization format and doesn't have many
381    //   advantages over `String`.
382    //
383    // A wrapper type over Value and Box exposing a unified interface could be
384    // a solution for later.
385    pub data: Option<String>,
386
387    /// Optional upgrade to realtime communication
388    #[cfg_attr(feature = "json", serde(skip))]
389    pub upgrade: Option<Upgrade>,
390}
391
392impl MessageContent {
393    /// Checks if the content is absolutely empty (contains no data at all).
394    pub fn is_empty(&self) -> bool {
395        self.text.is_empty()
396            && self.citations.is_empty()
397            && self.data.is_none()
398            && self.reasoning.is_empty()
399            && self.attachments.is_empty()
400            && self.tool_calls.is_empty()
401            && self.tool_results.is_empty()
402            && self.upgrade.is_none()
403    }
404}
405
406/// Metadata automatically tracked by MolyKit for each message.
407///
408/// "Metadata" basically means "data about data". Like tracking timestamps for
409/// data modification.
410#[derive(Clone, Debug, PartialEq)]
411#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
412pub struct MessageMetadata {
413    /// Runtime flag indicating that the message is still incomplete (being written).
414    ///
415    /// Skipped during serialization.
416    #[cfg_attr(feature = "json", serde(skip))]
417    pub is_writing: bool,
418
419    /// When the message got created.
420    ///
421    /// Default to epoch if missing during deserialization. Otherwise, if constructed
422    /// by [`MessageMetadata::default`], it defaults to "now".
423    #[cfg_attr(feature = "json", serde(default))]
424    pub created_at: DateTime<Utc>,
425
426    /// Last time the reasoning/thinking content was updated.
427    ///
428    /// Default to epoch if missing during deserialization. Otherwise, if constructed
429    /// by [`MessageMetadata::default`], it defaults to "now".
430    #[cfg_attr(feature = "json", serde(default))]
431    pub reasoning_updated_at: DateTime<Utc>,
432
433    /// Last time the main text was updated.
434    ///
435    /// Default to epoch if missing during deserialization. Otherwise, if constructed
436    /// by [`MessageMetadata::default`], it defaults to "now".
437    #[cfg_attr(feature = "json", serde(default))]
438    pub text_updated_at: DateTime<Utc>,
439}
440
441impl Default for MessageMetadata {
442    fn default() -> Self {
443        // Use the same timestamp for all fields.
444        let now = Utc::now();
445        MessageMetadata {
446            is_writing: false,
447            created_at: now,
448            reasoning_updated_at: now,
449            text_updated_at: now,
450        }
451    }
452}
453
454impl MessageMetadata {
455    /// Same behavior as [`MessageMetadata::default`].
456    pub fn new() -> Self {
457        MessageMetadata::default()
458    }
459
460    /// Create a new metadata with all fields set to default but timestamps set to epoch.
461    pub fn epoch() -> Self {
462        MessageMetadata {
463            is_writing: false,
464            created_at: DateTime::UNIX_EPOCH,
465            reasoning_updated_at: DateTime::UNIX_EPOCH,
466            text_updated_at: DateTime::UNIX_EPOCH,
467        }
468    }
469}
470
471impl MessageMetadata {
472    /// The inferred amount of time the reasoning step took, in seconds (with milliseconds).
473    pub fn reasoning_time_taken_seconds(&self) -> f32 {
474        let delta = self.reasoning_updated_at - self.created_at;
475        delta.as_seconds_f32()
476    }
477
478    pub fn is_idle(&self) -> bool {
479        !self.is_writing
480    }
481
482    pub fn is_writing(&self) -> bool {
483        self.is_writing
484    }
485}
486
487/// A message that is part of a conversation.
488#[derive(Clone, PartialEq, Debug, Default)]
489#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
490pub struct Message {
491    /// The id of who sent this message.
492    pub from: EntityId,
493
494    /// Auto-generated metadata for this message.
495    ///
496    /// If missing during deserialization, uses [`MessageMetadata::epoch`] instead
497    /// of [`MessageMetadata::default`].
498    #[cfg_attr(feature = "json", serde(default = "MessageMetadata::epoch"))]
499    pub metadata: MessageMetadata,
500
501    /// The parsed content of this message ready to present.
502    pub content: MessageContent,
503}
504
505impl Message {
506    /// Shorthand for constructing an app error message.
507    pub fn app_error(error: impl fmt::Display) -> Self {
508        Message {
509            from: EntityId::App,
510            content: MessageContent {
511                text: format!("Error: {}", error),
512                ..MessageContent::default()
513            },
514            ..Default::default()
515        }
516    }
517
518    /// Set the content of a message as a whole (also updates metadata).
519    pub fn set_content(&mut self, content: MessageContent) {
520        self.update_content(|c| {
521            *c = content;
522        });
523    }
524
525    /// Update specific parts of the content of a message (also updates metadata).
526    pub fn update_content(&mut self, f: impl FnOnce(&mut MessageContent)) {
527        let bk = self.content.clone();
528        let now = Utc::now();
529
530        f(&mut self.content);
531
532        if self.content.text != bk.text {
533            self.metadata.text_updated_at = now;
534        }
535
536        if self.content.reasoning != bk.reasoning {
537            self.metadata.reasoning_updated_at = now;
538        }
539    }
540}
541
542/// The standard error kinds a client implementatiin should facilitate.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544pub enum ClientErrorKind {
545    /// The network connection could not be established properly or was lost.
546    Network,
547
548    /// The connection could be established, but the remote server/peer gave us
549    /// an error.
550    ///
551    /// Example: On a centralized HTTP server, this would happen when it returns
552    /// an HTTP error code.
553    Response,
554
555    /// The remote server/peer returned a successful response, but we can't parse
556    /// its content.
557    ///
558    /// Example: When working with JSON APIs, this can happen when the schema of
559    /// the JSON response is not what we expected or is not JSON at all.
560    Format,
561
562    /// A kind of error that is not contemplated by MolyKit at the client layer.
563    Unknown,
564}
565
566impl ClientErrorKind {
567    pub fn to_human_readable(&self) -> &str {
568        match self {
569            ClientErrorKind::Network => "Network error",
570            ClientErrorKind::Response => "Remote error",
571            ClientErrorKind::Format => "Format error",
572            ClientErrorKind::Unknown => "Unknown error",
573        }
574    }
575}
576
577/// Standard error returned from client operations.
578#[derive(Debug, Clone)]
579pub struct ClientError {
580    kind: ClientErrorKind,
581    message: String,
582    source: Option<Arc<dyn Error + Send + Sync + 'static>>,
583}
584
585impl fmt::Display for ClientError {
586    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587        write!(f, "{}: {}", self.kind.to_human_readable(), self.message)
588    }
589}
590
591impl Error for ClientError {
592    fn source(&self) -> Option<&(dyn Error + 'static)> {
593        self.source.as_ref().map(|s| &**s as _)
594    }
595}
596
597impl From<ClientError> for Vec<ClientError> {
598    fn from(error: ClientError) -> Self {
599        vec![error]
600    }
601}
602
603impl<T> From<ClientError> for ClientResult<T> {
604    fn from(error: ClientError) -> Self {
605        ClientResult::new_err(vec![error])
606    }
607}
608
609impl ClientError {
610    /// Construct a simple client error without source.
611    ///
612    /// If you have an underlying error you want to include as the source, use
613    /// [ClientError::new_with_source] instead.
614    pub fn new(kind: ClientErrorKind, message: String) -> Self {
615        ClientError {
616            kind,
617            message,
618            source: None,
619        }
620    }
621
622    /// Construct a client error using an underlying error as the source.
623    pub fn new_with_source<S>(kind: ClientErrorKind, message: String, source: Option<S>) -> Self
624    where
625        S: Error + Send + Sync + 'static,
626    {
627        ClientError {
628            kind,
629            message,
630            source: source.map(|s| Arc::new(s) as _),
631        }
632    }
633
634    /// Error kind accessor.
635    pub fn kind(&self) -> ClientErrorKind {
636        self.kind
637    }
638
639    /// Error message accessor.
640    pub fn message(&self) -> &str {
641        self.message.as_str()
642    }
643}
644
645/// The outcome of a client operation.
646///
647/// Different from the standard Result, this one may contain more than one error.
648/// And at the same time, even if an error ocurrs, there may be a value to rescue.
649///
650/// It would be mistake if this contains no value and no errors at the same time.
651/// This is taken care on creation time, and it can't be modified afterwards.
652#[derive(Debug)]
653#[must_use]
654pub struct ClientResult<T> {
655    errors: Vec<ClientError>,
656    value: Option<T>,
657}
658
659impl<T> ClientResult<T> {
660    /// Creates a result containing a successful value and no errors.
661    pub fn new_ok(value: T) -> Self {
662        ClientResult {
663            errors: Vec::new(),
664            value: Some(value),
665        }
666    }
667
668    /// Creates a result containing errors and no value to rescue.
669    ///
670    /// The errors list should be non empty. If it's empty a default error will
671    /// be added to avoid the invariant of having no value and no errors at the
672    /// same time.
673    pub fn new_err(errors: Vec<ClientError>) -> Self {
674        let errors = if errors.is_empty() {
675            vec![ClientError::new(
676                ClientErrorKind::Unknown,
677                "An error ocurred, but no details were provided.".into(),
678            )]
679        } else {
680            errors
681        };
682
683        ClientResult {
684            errors,
685            value: None,
686        }
687    }
688
689    /// Creates a result containing errors and a value to rescue.
690    ///
691    /// This method should only be used when there are both errors and a value.
692    /// - If there are no errors, use [ClientResult::new_ok] instead.
693    /// - Similar to [ClientResult::new_err], if the errors list is empty, a default
694    /// error will be added.
695    pub fn new_ok_and_err(value: T, errors: Vec<ClientError>) -> Self {
696        let errors = if errors.is_empty() {
697            vec![ClientError::new(
698                ClientErrorKind::Unknown,
699                "An error ocurred, but no details were provided.".into(),
700            )]
701        } else {
702            errors
703        };
704
705        ClientResult {
706            errors,
707            value: Some(value),
708        }
709    }
710
711    /// Returns the successful value if there is one.
712    pub fn value(&self) -> Option<&T> {
713        self.value.as_ref()
714    }
715
716    /// Returns the errors list.
717    pub fn errors(&self) -> &[ClientError] {
718        &self.errors
719    }
720
721    /// Returns true if there is a successful value.
722    pub fn has_value(&self) -> bool {
723        self.value.is_some()
724    }
725
726    /// Returns true if there are errors.
727    pub fn has_errors(&self) -> bool {
728        !self.errors.is_empty()
729    }
730
731    /// Consume the result and return the successful value if there is one.
732    pub fn into_value(self) -> Option<T> {
733        self.value
734    }
735
736    /// Consume the result and return the errors list.
737    pub fn into_errors(self) -> Vec<ClientError> {
738        self.errors
739    }
740
741    /// Consume the result and return the successful value and the errors list.
742    pub fn into_value_and_errors(self) -> (Option<T>, Vec<ClientError>) {
743        (self.value, self.errors)
744    }
745
746    /// Consume the result to convert it into a standard Result.
747    pub fn into_result(self) -> Result<T, Vec<ClientError>> {
748        if self.errors.is_empty() {
749            Ok(self.value.expect("ClientResult has no value nor errors"))
750        } else {
751            Err(self.errors)
752        }
753    }
754}
755
756/// A standard interface to fetch bots information and send messages to them.
757///
758/// Warning: Expect this to be cloned to avoid borrow checking issues with
759/// makepad's widgets. Also, it may be cloned inside async contexts. So keep this
760/// cheap to clone and synced.
761///
762/// Note: Generics do not play well with makepad's widgets, so this trait relies
763/// on dynamic dispatch (with its limitations).
764pub trait BotClient: Send {
765    /// Send a message to a bot with support for streamed response.
766    ///
767    /// Each message yielded by the stream should be a snapshot of the full
768    /// message as it is being built.
769    ///
770    /// You are free to add, modify or remove content on-the-go.
771    fn send(
772        &mut self,
773        bot_id: &BotId,
774        messages: &[Message],
775        tools: &[Tool],
776    ) -> BoxPlatformSendStream<'static, ClientResult<MessageContent>>;
777
778    /// Interrupt the bot's current operation.
779    // TODO: There may be many chats with the same bot/model/agent so maybe this
780    // should be implemented by using cancellation tokens.
781    // fn stop(&mut self, bot: BotId);
782
783    /// Bots available under this client.
784    // NOTE: Could be a stream, but may add complexity rarely needed.
785    // TODO: Support partial results with errors for an union multi client/service
786    // later.
787    fn bots(&self) -> BoxPlatformSendFuture<'static, ClientResult<Vec<Bot>>>;
788
789    /// Make a boxed dynamic clone of this client to pass around.
790    fn clone_box(&self) -> Box<dyn BotClient>;
791
792    /// Optionally override how the content of a message is rendered by Makepad.
793    ///
794    /// Not expected to be implemented by most clients, however if this client
795    /// interfaces with a service that gives content in non-standard formats,
796    /// this can be used to extend moly-kit to support it.
797    ///
798    /// Prefer reusing previous widget if matches the expected type instead of
799    /// creating a new one on every call to preserve state and avoid perfomance
800    /// issues.
801    fn content_widget(
802        &mut self,
803        _cx: &mut Cx,
804        _previous_widget: WidgetRef,
805        _templates: &HashMap<LiveId, LivePtr>,
806        _content: &MessageContent,
807    ) -> Option<WidgetRef> {
808        None
809    }
810}
811
812impl Clone for Box<dyn BotClient> {
813    fn clone(&self) -> Self {
814        self.clone_box()
815    }
816}
817
818struct InnerBotContext {
819    client: Box<dyn BotClient>,
820    bots: Vec<Bot>,
821    tool_manager: Option<McpManagerClient>,
822}
823
824/// A sharable wrapper around a [BotClient] that holds loadeed bots and provides
825/// synchronous APIs to access them, mainly from the UI.
826///
827/// Passed down through widgets from this crate.
828///
829/// Separate chat widgets can share the same [BotContext] to avoid loading the same
830/// bots multiple times.
831pub struct BotContext(Arc<Mutex<InnerBotContext>>);
832
833impl Clone for BotContext {
834    fn clone(&self) -> Self {
835        BotContext(self.0.clone())
836    }
837}
838
839impl PartialEq for BotContext {
840    fn eq(&self, other: &Self) -> bool {
841        self.id() == other.id()
842    }
843}
844
845impl BotContext {
846    /// Differenciates [BotContext]s.
847    ///
848    /// Two [BotContext]s are equal and share the same underlying data if they have
849    /// the same id.
850    pub fn id(&self) -> usize {
851        Arc::as_ptr(&self.0) as usize
852    }
853
854    /// Fetches the bots and keeps them to be read synchronously later.
855    ///
856    /// It errors with whatever the underlying client errors with.
857    pub fn load(&mut self) -> BoxPlatformSendFuture<'_, ClientResult<()>> {
858        let future = async move {
859            let result = self.client().bots().await;
860            let (new_bots, errors) = result.into_value_and_errors();
861
862            if let Some(new_bots) = new_bots {
863                self.0.lock().unwrap().bots = new_bots;
864            }
865
866            if errors.is_empty() {
867                ClientResult::new_ok(())
868            } else {
869                ClientResult::new_err(errors)
870            }
871        };
872
873        Box::pin(future)
874    }
875    pub fn client(&self) -> Box<dyn BotClient> {
876        self.0.lock().unwrap().client.clone_box()
877    }
878
879    pub fn bots(&self) -> Vec<Bot> {
880        self.0.lock().unwrap().bots.clone()
881    }
882
883    pub fn get_bot(&self, id: &BotId) -> Option<Bot> {
884        self.bots().into_iter().find(|bot| bot.id == *id)
885    }
886
887    pub fn tool_manager(&self) -> Option<McpManagerClient> {
888        self.0.lock().unwrap().tool_manager.clone()
889    }
890
891    pub fn set_tool_manager(&mut self, tool_manager: McpManagerClient) {
892        self.0.lock().unwrap().tool_manager = Some(tool_manager);
893    }
894
895    pub fn replace_tool_manager(&mut self, tool_manager: McpManagerClient) {
896        self.0.lock().unwrap().tool_manager = Some(tool_manager);
897    }
898}
899
900impl<T: BotClient + 'static> From<T> for BotContext {
901    fn from(client: T) -> Self {
902        BotContext(Arc::new(Mutex::new(InnerBotContext {
903            client: Box::new(client),
904            bots: Vec::new(),
905            tool_manager: None,
906        })))
907    }
908}
909
910#[cfg(test)]
911mod tests {
912    use super::*;
913
914    #[test]
915    fn test_bot_id() {
916        // Simple
917        let id = BotId::new("123", "example.com");
918        assert_eq!(id.as_str(), "3;123@example.com");
919        assert_eq!(id.id(), "123");
920        assert_eq!(id.provider(), "example.com");
921
922        // Dirty
923        let id = BotId::new("a;b@c", "https://ex@a@m;ple.co@m");
924        assert_eq!(id.as_str(), "5;a;b@c@https://ex@a@m;ple.co@m");
925        assert_eq!(id.id(), "a;b@c");
926        assert_eq!(id.provider(), "https://ex@a@m;ple.co@m");
927
928        // Similar yet different
929        let id1 = BotId::new("a@", "b");
930        let id2 = BotId::new("a", "@b");
931        assert_ne!(id1.as_str(), id2.as_str());
932        assert_ne!(id1, id2);
933    }
934}