pub mod discover;
pub mod draft;
pub mod orchestrate;
pub mod publish;
pub mod queue;
pub mod thread_plan;
#[cfg(test)]
mod e2e_tests;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use serde::Serialize;
use crate::content::frameworks::ReplyArchetype;
use crate::context::retrieval::VaultCitation;
use crate::error::XApiError;
use crate::llm::{GenerationParams, LlmProvider, LlmResponse};
use crate::toolkit::ToolkitError;
use crate::LlmError;
#[derive(Debug, thiserror::Error)]
pub enum WorkflowError {
#[error(transparent)]
Toolkit(#[from] ToolkitError),
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("storage error: {0}")]
Storage(#[from] crate::error::StorageError),
#[error("LLM provider not configured")]
LlmNotConfigured,
#[error("LLM error: {0}")]
Llm(#[from] LlmError),
#[error("X API client not configured")]
XNotConfigured,
#[error("invalid input: {0}")]
InvalidInput(String),
}
pub(crate) struct SharedProvider(pub Arc<dyn LlmProvider>);
#[async_trait::async_trait]
impl LlmProvider for SharedProvider {
fn name(&self) -> &str {
self.0.name()
}
async fn complete(
&self,
system: &str,
user_message: &str,
params: &GenerationParams,
) -> Result<LlmResponse, LlmError> {
self.0.complete(system, user_message, params).await
}
async fn health_check(&self) -> Result<(), LlmError> {
self.0.health_check().await
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ScoredCandidate {
pub tweet_id: String,
pub author_username: String,
pub author_followers: u64,
pub text: String,
pub created_at: String,
pub score_total: f32,
pub score_breakdown: ScoreBreakdown,
pub matched_keywords: Vec<String>,
pub recommended_action: String,
pub already_replied: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ScoreBreakdown {
pub keyword_relevance: f32,
pub follower: f32,
pub recency: f32,
pub engagement: f32,
pub reply_count: f32,
pub content_type: f32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "status")]
pub enum DraftResult {
#[serde(rename = "success")]
Success {
candidate_id: String,
draft_text: String,
archetype: String,
char_count: usize,
confidence: String,
risks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
vault_citations: Vec<VaultCitation>,
},
#[serde(rename = "error")]
Error {
candidate_id: String,
error_code: String,
error_message: String,
},
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "status")]
pub enum ProposeResult {
#[serde(rename = "queued")]
Queued {
candidate_id: String,
approval_queue_id: i64,
},
#[serde(rename = "executed")]
Executed {
candidate_id: String,
reply_tweet_id: String,
},
#[serde(rename = "blocked")]
Blocked {
candidate_id: String,
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct QueueItem {
pub candidate_id: String,
pub pre_drafted_text: Option<String>,
}
pub fn parse_archetype(s: &str) -> Option<ReplyArchetype> {
match s.to_lowercase().replace(' ', "_").as_str() {
"agree_and_expand" | "agreeandexpand" => Some(ReplyArchetype::AgreeAndExpand),
"respectful_disagree" | "respectfuldisagree" => Some(ReplyArchetype::RespectfulDisagree),
"add_data" | "adddata" => Some(ReplyArchetype::AddData),
"ask_question" | "askquestion" => Some(ReplyArchetype::AskQuestion),
"share_experience" | "shareexperience" => Some(ReplyArchetype::ShareExperience),
_ => None,
}
}
pub(crate) fn make_content_gen(
llm: &Arc<dyn LlmProvider>,
business: &crate::config::BusinessProfile,
) -> crate::content::ContentGenerator {
let provider = Box::new(SharedProvider(Arc::clone(llm)));
crate::content::ContentGenerator::new(provider, business.clone())
}
impl WorkflowError {
pub fn from_x_api(e: XApiError) -> Self {
Self::Toolkit(ToolkitError::XApi(e))
}
}
pub use discover::{DiscoverInput, DiscoverOutput};
pub use draft::DraftInput;
pub use orchestrate::{CycleInput, CycleReport};
pub use publish::PublishOutput;
pub use queue::QueueInput;
pub use thread_plan::{ThreadPlanInput, ThreadPlanOutput};