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)]
22    payload: Value,
23}
24
25#[derive(Clone, Debug, Serialize, Deserialize, Default)]
26#[serde(tag = "kind", rename_all = "snake_case")]
27pub enum ActivityKind {
28    /// Messaging-style activity (default).
29    #[default]
30    Message,
31    /// Custom activity with user-specified action + optional flow type override.
32    Custom {
33        action: String,
34        #[serde(default, skip_serializing_if = "Option::is_none")]
35        flow_type: Option<String>,
36    },
37}
38
39impl Activity {
40    /// Create a text messaging activity payload.
41    pub fn text(text: impl Into<String>) -> Self {
42        Self {
43            kind: ActivityKind::Message,
44            tenant: None,
45            flow_id: None,
46            flow_type: Some("messaging".into()),
47            session_id: None,
48            provider_id: None,
49            user_id: None,
50            payload: json!({ "text": text.into() }),
51        }
52    }
53
54    /// Build a custom activity with a raw payload body.
55    pub fn custom(action: impl Into<String>, payload: Value) -> Self {
56        Self {
57            kind: ActivityKind::Custom {
58                action: action.into(),
59                flow_type: None,
60            },
61            tenant: None,
62            flow_id: None,
63            flow_type: None,
64            session_id: None,
65            provider_id: None,
66            user_id: None,
67            payload,
68        }
69    }
70
71    /// Attach a tenant identifier to the activity.
72    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
73        self.tenant = Some(tenant.into());
74        self
75    }
76
77    /// Target a specific flow identifier.
78    pub fn with_flow(mut self, flow_id: impl Into<String>) -> Self {
79        self.flow_id = Some(flow_id.into());
80        self
81    }
82
83    /// Hint which flow type should handle the activity.
84    pub fn with_flow_type(mut self, flow_type: impl Into<String>) -> Self {
85        let flow_type = flow_type.into();
86        self.flow_type = Some(flow_type.clone());
87        if let ActivityKind::Custom {
88            flow_type: inner, ..
89        } = &mut self.kind
90        {
91            *inner = Some(flow_type);
92        }
93        self
94    }
95
96    /// Attach a session identifier used for retries/idempotency.
97    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
98        self.session_id = Some(session_id.into());
99        self
100    }
101
102    /// Attach a provider identifier for telemetry scoping.
103    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
104        self.provider_id = Some(provider.into());
105        self
106    }
107
108    /// Attach the originating user for messaging activities.
109    pub fn from_user(mut self, user: impl Into<String>) -> Self {
110        self.user_id = Some(user.into());
111        self
112    }
113
114    /// Return the resolved tenant identifier, if any.
115    pub fn tenant(&self) -> Option<&str> {
116        self.tenant.as_deref()
117    }
118
119    /// Return the resolved flow identifier hint.
120    pub fn flow_id(&self) -> Option<&str> {
121        self.flow_id.as_deref()
122    }
123
124    /// Return the resolved flow type hint.
125    pub fn flow_type(&self) -> Option<&str> {
126        self.flow_type
127            .as_deref()
128            .or_else(|| self.kind.flow_type_hint())
129    }
130
131    /// Return the originating session identifier, if supplied.
132    pub fn session_id(&self) -> Option<&str> {
133        self.session_id.as_deref()
134    }
135
136    /// Return the originating provider identifier, if supplied.
137    pub fn provider_id(&self) -> Option<&str> {
138        self.provider_id.as_deref()
139    }
140
141    /// Underlying payload body.
142    pub fn payload(&self) -> &Value {
143        &self.payload
144    }
145
146    pub(crate) fn action(&self) -> Option<&str> {
147        self.kind.action_hint()
148    }
149
150    pub(crate) fn into_payload(self) -> Value {
151        self.payload
152    }
153
154    pub(crate) fn ensure_tenant(mut self, tenant: &str) -> Self {
155        if self.tenant.is_none() {
156            self.tenant = Some(tenant.to_string());
157        }
158        self
159    }
160
161    pub(crate) fn from_output(payload: Value, tenant: &str) -> Self {
162        Activity::custom("response", payload).ensure_tenant(tenant)
163    }
164}
165
166impl ActivityKind {
167    fn flow_type_hint(&self) -> Option<&str> {
168        match self {
169            ActivityKind::Message => Some("messaging"),
170            ActivityKind::Custom { flow_type, .. } => flow_type.as_deref(),
171        }
172    }
173
174    fn action_hint(&self) -> Option<&str> {
175        match self {
176            ActivityKind::Message => Some("messaging"),
177            ActivityKind::Custom { action, .. } => Some(action.as_str()),
178        }
179    }
180}