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::error::XApiError;
30use crate::llm::{GenerationParams, LlmProvider, LlmResponse};
31use crate::toolkit::ToolkitError;
32use crate::LlmError;
33
34#[derive(Debug, thiserror::Error)]
40pub enum WorkflowError {
41 #[error(transparent)]
43 Toolkit(#[from] ToolkitError),
44
45 #[error("database error: {0}")]
47 Database(#[from] sqlx::Error),
48
49 #[error("storage error: {0}")]
51 Storage(#[from] crate::error::StorageError),
52
53 #[error("LLM provider not configured")]
55 LlmNotConfigured,
56
57 #[error("LLM error: {0}")]
59 Llm(#[from] LlmError),
60
61 #[error("X API client not configured")]
63 XNotConfigured,
64
65 #[error("invalid input: {0}")]
67 InvalidInput(String),
68}
69
70pub(crate) struct SharedProvider(pub Arc<dyn LlmProvider>);
78
79#[async_trait::async_trait]
80impl LlmProvider for SharedProvider {
81 fn name(&self) -> &str {
82 self.0.name()
83 }
84
85 async fn complete(
86 &self,
87 system: &str,
88 user_message: &str,
89 params: &GenerationParams,
90 ) -> Result<LlmResponse, LlmError> {
91 self.0.complete(system, user_message, params).await
92 }
93
94 async fn health_check(&self) -> Result<(), LlmError> {
95 self.0.health_check().await
96 }
97}
98
99#[derive(Debug, Clone, Serialize)]
103pub struct ScoredCandidate {
104 pub tweet_id: String,
105 pub author_username: String,
106 pub author_followers: u64,
107 pub text: String,
108 pub created_at: String,
109 pub score_total: f32,
110 pub score_breakdown: ScoreBreakdown,
111 pub matched_keywords: Vec<String>,
112 pub recommended_action: String,
113 pub already_replied: bool,
114}
115
116#[derive(Debug, Clone, Serialize)]
118pub struct ScoreBreakdown {
119 pub keyword_relevance: f32,
120 pub follower: f32,
121 pub recency: f32,
122 pub engagement: f32,
123 pub reply_count: f32,
124 pub content_type: f32,
125}
126
127#[derive(Debug, Clone, Serialize)]
129#[serde(tag = "status")]
130pub enum DraftResult {
131 #[serde(rename = "success")]
132 Success {
133 candidate_id: String,
134 draft_text: String,
135 archetype: String,
136 char_count: usize,
137 confidence: String,
138 risks: Vec<String>,
139 },
140 #[serde(rename = "error")]
141 Error {
142 candidate_id: String,
143 error_code: String,
144 error_message: String,
145 },
146}
147
148#[derive(Debug, Clone, Serialize)]
150#[serde(tag = "status")]
151pub enum ProposeResult {
152 #[serde(rename = "queued")]
153 Queued {
154 candidate_id: String,
155 approval_queue_id: i64,
156 },
157 #[serde(rename = "executed")]
158 Executed {
159 candidate_id: String,
160 reply_tweet_id: String,
161 },
162 #[serde(rename = "blocked")]
163 Blocked {
164 candidate_id: String,
165 reason: String,
166 },
167}
168
169#[derive(Debug, Clone)]
171pub struct QueueItem {
172 pub candidate_id: String,
174 pub pre_drafted_text: Option<String>,
176}
177
178pub fn parse_archetype(s: &str) -> Option<ReplyArchetype> {
182 match s.to_lowercase().replace(' ', "_").as_str() {
183 "agree_and_expand" | "agreeandexpand" => Some(ReplyArchetype::AgreeAndExpand),
184 "respectful_disagree" | "respectfuldisagree" => Some(ReplyArchetype::RespectfulDisagree),
185 "add_data" | "adddata" => Some(ReplyArchetype::AddData),
186 "ask_question" | "askquestion" => Some(ReplyArchetype::AskQuestion),
187 "share_experience" | "shareexperience" => Some(ReplyArchetype::ShareExperience),
188 _ => None,
189 }
190}
191
192pub(crate) fn make_content_gen(
196 llm: &Arc<dyn LlmProvider>,
197 business: &crate::config::BusinessProfile,
198) -> crate::content::ContentGenerator {
199 let provider = Box::new(SharedProvider(Arc::clone(llm)));
200 crate::content::ContentGenerator::new(provider, business.clone())
201}
202
203impl WorkflowError {
206 pub fn from_x_api(e: XApiError) -> Self {
208 Self::Toolkit(ToolkitError::XApi(e))
209 }
210}
211
212pub use discover::{DiscoverInput, DiscoverOutput};
215pub use draft::DraftInput;
216pub use orchestrate::{CycleInput, CycleReport};
217pub use publish::PublishOutput;
218pub use queue::QueueInput;
219pub use thread_plan::{ThreadPlanInput, ThreadPlanOutput};