use crate::{AgentConfig, SdkError};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Deserialize)]
pub struct JobSummary {
pub id: String,
pub title: String,
pub description: String,
pub required_skills: Vec<String>,
pub budget_min: f64,
pub budget_max: f64,
pub task_type: String,
pub complexity: String,
pub urgency: String,
pub status: String,
pub bid_count: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChatMessage {
pub id: String,
pub job_id: String,
pub sender_id: String,
pub sender_type: String,
pub sender_name: String,
pub content: String,
pub created_at: String,
}
#[derive(Debug, Clone)]
pub struct BidParams {
pub amount: f64,
pub proposal: String,
pub estimated_hours: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct JobAssignment {
pub job_id: String,
pub title: String,
pub description: String,
pub price: f64,
pub client_name: String,
}
#[async_trait]
pub trait AgentHandler: Send + Sync {
async fn on_message(&self, job_id: &str, message: &str, from: &str) -> Option<String>;
async fn should_bid(&self, job: &JobSummary) -> Option<BidParams>;
async fn on_job_assigned(&self, assignment: &JobAssignment) {
println!("🎉 Job assigned: {} for ${}", assignment.title, assignment.price);
}
async fn on_deliver(&self, _job_id: &str) -> Option<String> {
None
}
async fn on_tick(&self) {}
}
struct RuntimeState {
seen_messages: HashSet<String>,
seen_jobs: HashSet<String>,
my_jobs: HashSet<String>,
}
pub struct AgentRuntime<H: AgentHandler> {
config: AgentConfig,
handler: Arc<H>,
client: Client,
state: Arc<RwLock<RuntimeState>>,
poll_interval: Duration,
}
#[derive(Deserialize)]
struct JobListResponse {
jobs: Vec<JobSummary>,
}
#[derive(Deserialize)]
struct MessageListResponse {
messages: Vec<ChatMessage>,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct MyJobsResponse {
jobs: Vec<JobSummary>,
}
#[derive(Serialize)]
struct SendMessageRequest {
content: String,
}
#[derive(Serialize)]
struct SubmitBidRequest {
bid_amount: f64,
proposal: String,
#[serde(skip_serializing_if = "Option::is_none")]
estimated_hours: Option<f64>,
}
impl<H: AgentHandler + 'static> AgentRuntime<H> {
pub fn new(config: AgentConfig, handler: H) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.expect("Failed to create HTTP client");
Self {
config,
handler: Arc::new(handler),
client,
state: Arc::new(RwLock::new(RuntimeState {
seen_messages: HashSet::new(),
seen_jobs: HashSet::new(),
my_jobs: HashSet::new(),
})),
poll_interval: Duration::from_secs(5),
}
}
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
pub async fn run(&self) -> Result<(), SdkError> {
println!("🤖 Agent Runtime starting...");
println!(" API: {}", self.config.base_url);
println!(" Poll interval: {:?}", self.poll_interval);
println!("");
self.sync_my_jobs().await?;
println!("✅ Agent is ONLINE and listening...\n");
loop {
if let Err(e) = self.check_messages().await {
eprintln!("⚠️ Error checking messages: {}", e);
}
if let Err(e) = self.check_new_jobs().await {
eprintln!("⚠️ Error checking jobs: {}", e);
}
self.handler.on_tick().await;
tokio::time::sleep(self.poll_interval).await;
}
}
async fn sync_my_jobs(&self) -> Result<(), SdkError> {
Ok(())
}
async fn check_messages(&self) -> Result<(), SdkError> {
let state = self.state.read().await;
let job_ids: Vec<String> = state.my_jobs.iter().cloned().collect();
drop(state);
for job_id in job_ids {
self.check_job_messages(&job_id).await?;
}
Ok(())
}
async fn check_job_messages(&self, job_id: &str) -> Result<(), SdkError> {
let url = format!("{}/api/jobs/{}/messages", self.config.base_url, job_id);
let response = self.client
.get(&url)
.header("X-API-Key", &self.config.api_key)
.send()
.await?;
if !response.status().is_success() {
return Ok(()); }
let msg_response: MessageListResponse = response.json().await?;
for msg in msg_response.messages {
if msg.sender_type == "client" {
let mut state = self.state.write().await;
if state.seen_messages.insert(msg.id.clone()) {
drop(state);
println!("💬 New message from {}: {}", msg.sender_name, msg.content);
if let Some(response) = self.handler.on_message(
&msg.job_id,
&msg.content,
&msg.sender_name
).await {
self.send_message(&msg.job_id, &response).await?;
println!("📤 Replied: {}", response);
}
}
}
}
Ok(())
}
async fn send_message(&self, job_id: &str, content: &str) -> Result<(), SdkError> {
let url = format!("{}/api/jobs/{}/messages", self.config.base_url, job_id);
let response = self.client
.post(&url)
.header("X-API-Key", &self.config.api_key)
.json(&SendMessageRequest {
content: content.to_string(),
})
.send()
.await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let error = response.text().await.unwrap_or_default();
return Err(SdkError::Api {
status,
message: error,
});
}
Ok(())
}
async fn check_new_jobs(&self) -> Result<(), SdkError> {
let url = format!("{}/api/jobs?status=open&status=bidding", self.config.base_url);
let response = self.client
.get(&url)
.send()
.await?;
if !response.status().is_success() {
return Ok(());
}
let jobs_response: JobListResponse = response.json().await?;
for job in jobs_response.jobs {
let mut state = self.state.write().await;
if state.seen_jobs.insert(job.id.clone()) {
drop(state);
println!("📋 New job: {} (${}-${})", job.title, job.budget_min, job.budget_max);
if let Some(bid_params) = self.handler.should_bid(&job).await {
match self.submit_bid(&job.id, bid_params).await {
Ok(_) => println!("✅ Bid submitted on: {}", job.title),
Err(e) => eprintln!("❌ Failed to bid: {}", e),
}
}
}
}
Ok(())
}
async fn submit_bid(&self, job_id: &str, params: BidParams) -> Result<(), SdkError> {
let url = format!("{}/api/jobs/{}/bids", self.config.base_url, job_id);
let response = self.client
.post(&url)
.header("X-API-Key", &self.config.api_key)
.json(&SubmitBidRequest {
bid_amount: params.amount,
proposal: params.proposal,
estimated_hours: params.estimated_hours,
})
.send()
.await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let error = response.text().await.unwrap_or_default();
return Err(SdkError::Api {
status,
message: error,
});
}
Ok(())
}
pub async fn track_job(&self, job_id: &str) {
let mut state = self.state.write().await;
state.my_jobs.insert(job_id.to_string());
}
}