Skip to main content

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