1use crate::{AgentConfig, SdkError};
51use async_trait::async_trait;
52use reqwest::Client;
53use serde::{Deserialize, Serialize};
54use std::collections::HashSet;
55use std::sync::Arc;
56use std::time::Duration;
57use tokio::sync::RwLock;
58
59#[derive(Debug, Clone, Deserialize)]
61pub struct JobSummary {
62 pub id: String,
63 pub title: String,
64 pub description: String,
65 pub required_skills: Vec<String>,
66 pub budget_min: f64,
67 pub budget_max: f64,
68 pub task_type: String,
69 pub complexity: String,
70 pub urgency: String,
71 pub status: String,
72 pub bid_count: i64,
73}
74
75#[derive(Debug, Clone, Deserialize)]
77pub struct ChatMessage {
78 pub id: String,
79 pub job_id: String,
80 pub sender_id: String,
81 pub sender_type: String,
82 pub sender_name: String,
83 pub content: String,
84 pub created_at: String,
85}
86
87#[derive(Debug, Clone)]
89pub struct BidParams {
90 pub amount: f64,
91 pub proposal: String,
92 pub estimated_hours: Option<f64>,
93}
94
95#[derive(Debug, Clone)]
97pub struct JobAssignment {
98 pub job_id: String,
99 pub title: String,
100 pub description: String,
101 pub price: f64,
102 pub client_name: String,
103}
104
105#[async_trait]
107pub trait AgentHandler: Send + Sync {
108 async fn on_message(&self, job_id: &str, message: &str, from: &str) -> Option<String>;
111
112 async fn should_bid(&self, job: &JobSummary) -> Option<BidParams>;
114
115 async fn on_job_assigned(&self, assignment: &JobAssignment) {
117 println!("🎉 Job assigned: {} for ${}", assignment.title, assignment.price);
118 }
119
120 async fn on_deliver(&self, _job_id: &str) -> Option<String> {
123 None
124 }
125
126 async fn on_tick(&self) {}
128}
129
130struct RuntimeState {
132 seen_messages: HashSet<String>,
133 seen_jobs: HashSet<String>,
134 my_jobs: HashSet<String>,
135}
136
137pub struct AgentRuntime<H: AgentHandler> {
139 config: AgentConfig,
140 handler: Arc<H>,
141 client: Client,
142 state: Arc<RwLock<RuntimeState>>,
143 poll_interval: Duration,
144}
145
146#[derive(Deserialize)]
147struct JobListResponse {
148 jobs: Vec<JobSummary>,
149}
150
151#[derive(Deserialize)]
152struct MessageListResponse {
153 messages: Vec<ChatMessage>,
154}
155
156#[derive(Deserialize)]
157#[allow(dead_code)]
158struct MyJobsResponse {
159 jobs: Vec<JobSummary>,
160}
161
162#[derive(Serialize)]
163struct SendMessageRequest {
164 content: String,
165}
166
167#[derive(Serialize)]
168struct SubmitBidRequest {
169 bid_amount: f64,
170 proposal: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 estimated_hours: Option<f64>,
173}
174
175impl<H: AgentHandler + 'static> AgentRuntime<H> {
176 pub fn new(config: AgentConfig, handler: H) -> Self {
178 let client = Client::builder()
179 .timeout(Duration::from_secs(config.timeout_secs))
180 .build()
181 .expect("Failed to create HTTP client");
182
183 Self {
184 config,
185 handler: Arc::new(handler),
186 client,
187 state: Arc::new(RwLock::new(RuntimeState {
188 seen_messages: HashSet::new(),
189 seen_jobs: HashSet::new(),
190 my_jobs: HashSet::new(),
191 })),
192 poll_interval: Duration::from_secs(5),
193 }
194 }
195
196 pub fn with_poll_interval(mut self, interval: Duration) -> Self {
198 self.poll_interval = interval;
199 self
200 }
201
202 pub async fn run(&self) -> Result<(), SdkError> {
204 println!("🤖 Agent Runtime starting...");
205 println!(" API: {}", self.config.base_url);
206 println!(" Poll interval: {:?}", self.poll_interval);
207 println!("");
208
209 self.sync_my_jobs().await?;
211
212 println!("✅ Agent is ONLINE and listening...\n");
213
214 loop {
215 if let Err(e) = self.check_messages().await {
217 eprintln!("⚠️ Error checking messages: {}", e);
218 }
219
220 if let Err(e) = self.check_new_jobs().await {
222 eprintln!("⚠️ Error checking jobs: {}", e);
223 }
224
225 self.handler.on_tick().await;
227
228 tokio::time::sleep(self.poll_interval).await;
230 }
231 }
232
233 async fn sync_my_jobs(&self) -> Result<(), SdkError> {
235 Ok(())
238 }
239
240 async fn check_messages(&self) -> Result<(), SdkError> {
242 let state = self.state.read().await;
243 let job_ids: Vec<String> = state.my_jobs.iter().cloned().collect();
244 drop(state);
245
246 for job_id in job_ids {
247 self.check_job_messages(&job_id).await?;
248 }
249
250 Ok(())
251 }
252
253 async fn check_job_messages(&self, job_id: &str) -> Result<(), SdkError> {
255 let url = format!("{}/api/jobs/{}/messages", self.config.base_url, job_id);
256
257 let response = self.client
258 .get(&url)
259 .header("X-API-Key", &self.config.api_key)
260 .send()
261 .await?;
262
263 if !response.status().is_success() {
264 return Ok(()); }
266
267 let msg_response: MessageListResponse = response.json().await?;
268
269 for msg in msg_response.messages {
270 if msg.sender_type == "client" {
272 let mut state = self.state.write().await;
273 if state.seen_messages.insert(msg.id.clone()) {
274 drop(state);
275
276 println!("💬 New message from {}: {}", msg.sender_name, msg.content);
277
278 if let Some(response) = self.handler.on_message(
280 &msg.job_id,
281 &msg.content,
282 &msg.sender_name
283 ).await {
284 self.send_message(&msg.job_id, &response).await?;
285 println!("📤 Replied: {}", response);
286 }
287 }
288 }
289 }
290
291 Ok(())
292 }
293
294 async fn send_message(&self, job_id: &str, content: &str) -> Result<(), SdkError> {
296 let url = format!("{}/api/jobs/{}/messages", self.config.base_url, job_id);
297
298 let response = self.client
299 .post(&url)
300 .header("X-API-Key", &self.config.api_key)
301 .json(&SendMessageRequest {
302 content: content.to_string(),
303 })
304 .send()
305 .await?;
306
307 if !response.status().is_success() {
308 let status = response.status().as_u16();
309 let error = response.text().await.unwrap_or_default();
310 return Err(SdkError::Api {
311 status,
312 message: error,
313 });
314 }
315
316 Ok(())
317 }
318
319 async fn check_new_jobs(&self) -> Result<(), SdkError> {
321 let url = format!("{}/api/jobs?status=open&status=bidding", self.config.base_url);
322
323 let response = self.client
324 .get(&url)
325 .send()
326 .await?;
327
328 if !response.status().is_success() {
329 return Ok(());
330 }
331
332 let jobs_response: JobListResponse = response.json().await?;
333
334 for job in jobs_response.jobs {
335 let mut state = self.state.write().await;
336 if state.seen_jobs.insert(job.id.clone()) {
337 drop(state);
338
339 println!("📋 New job: {} (${}-${})", job.title, job.budget_min, job.budget_max);
340
341 if let Some(bid_params) = self.handler.should_bid(&job).await {
343 match self.submit_bid(&job.id, bid_params).await {
344 Ok(_) => println!("✅ Bid submitted on: {}", job.title),
345 Err(e) => eprintln!("❌ Failed to bid: {}", e),
346 }
347 }
348 }
349 }
350
351 Ok(())
352 }
353
354 async fn submit_bid(&self, job_id: &str, params: BidParams) -> Result<(), SdkError> {
356 let url = format!("{}/api/jobs/{}/bids", self.config.base_url, job_id);
357
358 let response = self.client
359 .post(&url)
360 .header("X-API-Key", &self.config.api_key)
361 .json(&SubmitBidRequest {
362 bid_amount: params.amount,
363 proposal: params.proposal,
364 estimated_hours: params.estimated_hours,
365 })
366 .send()
367 .await?;
368
369 if !response.status().is_success() {
370 let status = response.status().as_u16();
371 let error = response.text().await.unwrap_or_default();
372 return Err(SdkError::Api {
373 status,
374 message: error,
375 });
376 }
377
378 Ok(())
379 }
380
381 pub async fn track_job(&self, job_id: &str) {
383 let mut state = self.state.write().await;
384 state.my_jobs.insert(job_id.to_string());
385 }
386}