tuitbot_core/workflow/
mod.rs1pub mod discover;
13pub mod draft;
14pub mod orchestrate;
15pub mod publish;
16pub mod queue;
17pub mod thread_plan;
18
19#[cfg(test)]
20mod e2e_tests;
21#[cfg(test)]
22mod tests;
23
24use std::sync::Arc;
25
26use serde::Serialize;
27
28use crate::content::frameworks::ReplyArchetype;
29use crate::context::retrieval::VaultCitation;
30use crate::error::XApiError;
31use crate::llm::{GenerationParams, LlmProvider, LlmResponse};
32use crate::toolkit::ToolkitError;
33use crate::LlmError;
34
35#[derive(Debug, thiserror::Error)]
41pub enum WorkflowError {
42 #[error(transparent)]
44 Toolkit(#[from] ToolkitError),
45
46 #[error("database error: {0}")]
48 Database(#[from] sqlx::Error),
49
50 #[error("storage error: {0}")]
52 Storage(#[from] crate::error::StorageError),
53
54 #[error("LLM provider not configured")]
56 LlmNotConfigured,
57
58 #[error("LLM error: {0}")]
60 Llm(#[from] LlmError),
61
62 #[error("X API client not configured")]
64 XNotConfigured,
65
66 #[error("invalid input: {0}")]
68 InvalidInput(String),
69}
70
71pub(crate) struct SharedProvider(pub Arc<dyn LlmProvider>);
79
80#[async_trait::async_trait]
81impl LlmProvider for SharedProvider {
82 fn name(&self) -> &str {
83 self.0.name()
84 }
85
86 async fn complete(
87 &self,
88 system: &str,
89 user_message: &str,
90 params: &GenerationParams,
91 ) -> Result<LlmResponse, LlmError> {
92 self.0.complete(system, user_message, params).await
93 }
94
95 async fn health_check(&self) -> Result<(), LlmError> {
96 self.0.health_check().await
97 }
98}
99
100#[derive(Debug, Clone, Serialize)]
104pub struct ScoredCandidate {
105 pub tweet_id: String,
106 pub author_username: String,
107 pub author_followers: u64,
108 pub text: String,
109 pub created_at: String,
110 pub score_total: f32,
111 pub score_breakdown: ScoreBreakdown,
112 pub matched_keywords: Vec<String>,
113 pub recommended_action: String,
114 pub already_replied: bool,
115}
116
117#[derive(Debug, Clone, Serialize)]
119pub struct ScoreBreakdown {
120 pub keyword_relevance: f32,
121 pub follower: f32,
122 pub recency: f32,
123 pub engagement: f32,
124 pub reply_count: f32,
125 pub content_type: f32,
126}
127
128#[derive(Debug, Clone, Serialize)]
130#[serde(tag = "status")]
131pub enum DraftResult {
132 #[serde(rename = "success")]
133 Success {
134 candidate_id: String,
135 draft_text: String,
136 archetype: String,
137 char_count: usize,
138 confidence: String,
139 risks: Vec<String>,
140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
141 vault_citations: Vec<VaultCitation>,
142 },
143 #[serde(rename = "error")]
144 Error {
145 candidate_id: String,
146 error_code: String,
147 error_message: String,
148 },
149}
150
151#[derive(Debug, Clone, Serialize)]
153#[serde(tag = "status")]
154pub enum ProposeResult {
155 #[serde(rename = "queued")]
156 Queued {
157 candidate_id: String,
158 approval_queue_id: i64,
159 },
160 #[serde(rename = "executed")]
161 Executed {
162 candidate_id: String,
163 reply_tweet_id: String,
164 },
165 #[serde(rename = "blocked")]
166 Blocked {
167 candidate_id: String,
168 reason: String,
169 },
170}
171
172#[derive(Debug, Clone)]
174pub struct QueueItem {
175 pub candidate_id: String,
177 pub pre_drafted_text: Option<String>,
179}
180
181pub fn parse_archetype(s: &str) -> Option<ReplyArchetype> {
185 match s.to_lowercase().replace(' ', "_").as_str() {
186 "agree_and_expand" | "agreeandexpand" => Some(ReplyArchetype::AgreeAndExpand),
187 "respectful_disagree" | "respectfuldisagree" => Some(ReplyArchetype::RespectfulDisagree),
188 "add_data" | "adddata" => Some(ReplyArchetype::AddData),
189 "ask_question" | "askquestion" => Some(ReplyArchetype::AskQuestion),
190 "share_experience" | "shareexperience" => Some(ReplyArchetype::ShareExperience),
191 _ => None,
192 }
193}
194
195pub(crate) fn make_content_gen(
199 llm: &Arc<dyn LlmProvider>,
200 business: &crate::config::BusinessProfile,
201) -> crate::content::ContentGenerator {
202 let provider = Box::new(SharedProvider(Arc::clone(llm)));
203 crate::content::ContentGenerator::new(provider, business.clone())
204}
205
206impl WorkflowError {
209 pub fn from_x_api(e: XApiError) -> Self {
211 Self::Toolkit(ToolkitError::XApi(e))
212 }
213}
214
215pub use discover::{DiscoverInput, DiscoverOutput};
218pub use draft::DraftInput;
219pub use orchestrate::{CycleInput, CycleReport};
220pub use publish::PublishOutput;
221pub use queue::QueueInput;
222pub use thread_plan::{ThreadPlanInput, ThreadPlanOutput};