greentic_runner_host/
activity.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4/// High-level activity payload exchanged with Greentic hosts.
5#[derive(Clone, Debug, Serialize, Deserialize)]
6pub struct Activity {
7    #[serde(default)]
8    pub(crate) kind: ActivityKind,
9    #[serde(default, skip_serializing_if = "Option::is_none")]
10    tenant: Option<String>,
11    #[serde(default, skip_serializing_if = "Option::is_none")]
12    flow_id: Option<String>,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    flow_type: Option<String>,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    session_id: Option<String>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    provider_id: Option<String>,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    user_id: Option<String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    channel_id: Option<String>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    conversation_id: Option<String>,
25    #[serde(default)]
26    payload: Value,
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize, Default)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum ActivityKind {
32    /// Messaging-style activity (default).
33    #[default]
34    Message,
35    /// Custom activity with user-specified action + optional flow type override.
36    Custom {
37        action: String,
38        #[serde(default, skip_serializing_if = "Option::is_none")]
39        flow_type: Option<String>,
40    },
41}
42
43impl Activity {
44    /// Create a text messaging activity payload.
45    pub fn text(text: impl Into<String>) -> Self {
46        Self {
47            kind: ActivityKind::Message,
48            tenant: None,
49            flow_id: None,
50            flow_type: Some("messaging".into()),
51            session_id: None,
52            provider_id: None,
53            user_id: None,
54            channel_id: None,
55            conversation_id: None,
56            payload: json!({ "text": text.into() }),
57        }
58    }
59
60    /// Build a custom activity with a raw payload body.
61    pub fn custom(action: impl Into<String>, payload: Value) -> Self {
62        Self {
63            kind: ActivityKind::Custom {
64                action: action.into(),
65                flow_type: None,
66            },
67            tenant: None,
68            flow_id: None,
69            flow_type: None,
70            session_id: None,
71            provider_id: None,
72            user_id: None,
73            channel_id: None,
74            conversation_id: None,
75            payload,
76        }
77    }
78
79    /// Attach a tenant identifier to the activity.
80    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
81        self.tenant = Some(tenant.into());
82        self
83    }
84
85    /// Target a specific flow identifier.
86    pub fn with_flow(mut self, flow_id: impl Into<String>) -> Self {
87        self.flow_id = Some(flow_id.into());
88        self
89    }
90
91    /// Hint which flow type should handle the activity.
92    pub fn with_flow_type(mut self, flow_type: impl Into<String>) -> Self {
93        let flow_type = flow_type.into();
94        self.flow_type = Some(flow_type.clone());
95        if let ActivityKind::Custom {
96            flow_type: inner, ..
97        } = &mut self.kind
98        {
99            *inner = Some(flow_type);
100        }
101        self
102    }
103
104    /// Attach a session identifier used for retries/idempotency.
105    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
106        self.session_id = Some(session_id.into());
107        self
108    }
109
110    /// Attach a provider identifier for telemetry scoping.
111    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
112        self.provider_id = Some(provider.into());
113        self
114    }
115
116    /// Attach the originating user for messaging activities.
117    pub fn from_user(mut self, user: impl Into<String>) -> Self {
118        self.user_id = Some(user.into());
119        self
120    }
121
122    /// Attach a channel identifier (chat, room, or queue) for canonical session keys.
123    pub fn in_channel(mut self, channel: impl Into<String>) -> Self {
124        self.channel_id = Some(channel.into());
125        self
126    }
127
128    /// Attach a conversation/thread identifier for canonical session keys.
129    pub fn in_conversation(mut self, conversation: impl Into<String>) -> Self {
130        self.conversation_id = Some(conversation.into());
131        self
132    }
133
134    /// Return the resolved tenant identifier, if any.
135    pub fn tenant(&self) -> Option<&str> {
136        self.tenant.as_deref()
137    }
138
139    /// Return the resolved flow identifier hint.
140    pub fn flow_id(&self) -> Option<&str> {
141        self.flow_id.as_deref()
142    }
143
144    /// Return the resolved flow type hint.
145    pub fn flow_type(&self) -> Option<&str> {
146        self.flow_type
147            .as_deref()
148            .or_else(|| self.kind.flow_type_hint())
149    }
150
151    /// Return the originating session identifier, if supplied.
152    pub fn session_id(&self) -> Option<&str> {
153        self.session_id.as_deref()
154    }
155
156    /// Return the originating provider identifier, if supplied.
157    pub fn provider_id(&self) -> Option<&str> {
158        self.provider_id.as_deref()
159    }
160
161    /// Return the originating user identifier, if supplied.
162    pub fn user(&self) -> Option<&str> {
163        self.user_id.as_deref()
164    }
165
166    /// Return the channel identifier, if supplied.
167    pub fn channel(&self) -> Option<&str> {
168        self.channel_id.as_deref()
169    }
170
171    /// Return the conversation identifier, if supplied.
172    pub fn conversation(&self) -> Option<&str> {
173        self.conversation_id.as_deref()
174    }
175
176    /// Underlying payload body.
177    pub fn payload(&self) -> &Value {
178        &self.payload
179    }
180
181    pub(crate) fn action(&self) -> Option<&str> {
182        self.kind.action_hint()
183    }
184
185    pub(crate) fn into_payload(self) -> Value {
186        self.payload
187    }
188
189    pub(crate) fn ensure_tenant(mut self, tenant: &str) -> Self {
190        if self.tenant.is_none() {
191            self.tenant = Some(tenant.to_string());
192        }
193        self
194    }
195
196    pub(crate) fn from_output(payload: Value, tenant: &str) -> Self {
197        Activity::custom("response", payload).ensure_tenant(tenant)
198    }
199}
200
201impl ActivityKind {
202    fn flow_type_hint(&self) -> Option<&str> {
203        match self {
204            ActivityKind::Message => Some("messaging"),
205            ActivityKind::Custom { flow_type, .. } => flow_type.as_deref(),
206        }
207    }
208
209    fn action_hint(&self) -> Option<&str> {
210        match self {
211            ActivityKind::Message => Some("messaging"),
212            ActivityKind::Custom { action, .. } => Some(action.as_str()),
213        }
214    }
215}