refactoring
This commit is contained in:
parent
04ece6afb1
commit
13e17770ca
12 changed files with 634 additions and 598 deletions
114
src/domain/agent/api.rs
Normal file
114
src/domain/agent/api.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub role: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_call_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ToolCall {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub call_type: String,
|
||||
pub function: FunctionCall,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FunctionCall {
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct ChatRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<Message>,
|
||||
pub tools: Option<Vec<Tool>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatResponse {
|
||||
pub choices: Vec<Choice>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Choice {
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Tool {
|
||||
#[serde(rename = "type")]
|
||||
pub tool_type: String,
|
||||
pub function: FunctionDefinition,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct FunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct TavilyResponse {
|
||||
results: Vec<TavilyResult>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct TavilyResult {
|
||||
title: String,
|
||||
url: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
pub async fn perform_search(
|
||||
query: &str,
|
||||
api_key: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
let response = client
|
||||
.post("https://api.tavily.com/search")
|
||||
.json(&serde_json::json!({
|
||||
"api_key": api_key,
|
||||
"query": query,
|
||||
"search_depth": "basic",
|
||||
"include_answer": false,
|
||||
"include_images": false,
|
||||
"include_raw_content": false,
|
||||
"max_results": 5,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Tavily API error: {}", response.status()).into());
|
||||
}
|
||||
|
||||
let search_data: TavilyResponse = response.json().await?;
|
||||
let mut results_text = String::new();
|
||||
|
||||
for (i, result) in search_data.results.iter().enumerate() {
|
||||
results_text.push_str(&format!(
|
||||
"{}. {} ({})\nSnippet: {}\n\n",
|
||||
i + 1,
|
||||
result.title,
|
||||
result.url,
|
||||
result.content
|
||||
));
|
||||
}
|
||||
|
||||
if results_text.is_empty() {
|
||||
Ok("No results found.".to_string())
|
||||
} else {
|
||||
Ok(results_text)
|
||||
}
|
||||
}
|
||||
168
src/domain/agent/mod.rs
Normal file
168
src/domain/agent/mod.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
pub mod api;
|
||||
pub mod tools;
|
||||
|
||||
use chrono::Utc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use self::api::{ChatRequest, ChatResponse, Message, Tool};
|
||||
|
||||
pub struct Agent {
|
||||
client: reqwest::Client,
|
||||
url: String,
|
||||
zen_api_key: Option<String>,
|
||||
tavily_api_key: Option<String>,
|
||||
messages: Vec<Message>,
|
||||
tools: Option<Vec<Tool>>,
|
||||
logs: String,
|
||||
answer: Option<String>,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
pub fn new(
|
||||
zen_api_key: Option<String>,
|
||||
tavily_api_key: Option<String>,
|
||||
initial_message: String,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let intro = format!(
|
||||
"You are an autonomous agent. You have access to tools that can help
|
||||
you achieve your goals. Use them wisely. The user is unable to respond to you
|
||||
so do not ask for clarification and use the
|
||||
answer tool once you to give your final answer. current date is {}",
|
||||
Utc::now().format("%B %d, %Y %H:%M:%S UTC").to_string()
|
||||
);
|
||||
let messages = vec![
|
||||
Message {
|
||||
role: "system".to_string(),
|
||||
content: Some(intro),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
},
|
||||
Message {
|
||||
role: "user".to_string(),
|
||||
content: Some(initial_message),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
},
|
||||
];
|
||||
|
||||
let tools = Some(tools::get_tools());
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
url: "https://opencode.ai/zen/v1/chat/completions".to_string(),
|
||||
zen_api_key,
|
||||
tavily_api_key,
|
||||
messages,
|
||||
tools,
|
||||
logs: String::new(),
|
||||
answer: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn log(&mut self, message: &str) {
|
||||
self.logs.push_str(message);
|
||||
self.logs.push('\n');
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
config: &crate::config::Config,
|
||||
) -> Result<(String, Option<String>), Box<dyn std::error::Error>> {
|
||||
let mut finished = false;
|
||||
let start_time = Instant::now();
|
||||
let max_duration = Duration::from_secs(config.agent_max_duration_secs);
|
||||
let max_turns = config.agent_max_turns;
|
||||
let mut turns = 0;
|
||||
|
||||
while !finished {
|
||||
if start_time.elapsed() > max_duration {
|
||||
return Err("Agent run timed out".into());
|
||||
}
|
||||
|
||||
if turns >= max_turns {
|
||||
return Err("Agent run exceeded max turns".into());
|
||||
}
|
||||
|
||||
turns += 1;
|
||||
let request = ChatRequest {
|
||||
model: "kimi-k2.5".to_string(),
|
||||
messages: self.messages.clone(),
|
||||
tools: self.tools.clone(),
|
||||
};
|
||||
|
||||
self.log(&format!(
|
||||
"--- Sending request to Zen API (Role: {}) ---",
|
||||
self.messages.last().unwrap().role
|
||||
));
|
||||
|
||||
let mut request_builder = self.client.post(&self.url).json(&request);
|
||||
|
||||
if let Some(key) = &self.zen_api_key {
|
||||
request_builder =
|
||||
request_builder.header("Authorization", format!("Bearer {}", key));
|
||||
}
|
||||
|
||||
let response = request_builder.send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await?;
|
||||
self.log(&format!("Error: API request failed with status {}", status));
|
||||
self.log(&format!("Error details: {}", error_text));
|
||||
return Err(format!("API request failed: {}", status).into());
|
||||
}
|
||||
|
||||
let chat_response: ChatResponse = response.json().await?;
|
||||
let assistant_message = chat_response
|
||||
.choices
|
||||
.get(0)
|
||||
.ok_or("Missing assistant response")?
|
||||
.message
|
||||
.clone();
|
||||
|
||||
self.messages.push(assistant_message.clone());
|
||||
|
||||
if let Some(content) = &assistant_message.content {
|
||||
if !content.is_empty() {
|
||||
self.log(&format!("\nAssistant response:\n{}\n", content));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tool_calls) = assistant_message.tool_calls {
|
||||
for tool_call in tool_calls {
|
||||
let (tool_message, written, tool_answer) =
|
||||
tools::handle_tool_call(&tool_call, &self.tavily_api_key).await?;
|
||||
|
||||
if let Some(ans) = tool_answer {
|
||||
self.answer = Some(ans);
|
||||
}
|
||||
|
||||
if let Some(content) = &tool_message.content {
|
||||
self.log(&format!(
|
||||
"Tool result ({}): {}",
|
||||
tool_call.function.name, content
|
||||
));
|
||||
}
|
||||
|
||||
self.messages.push(tool_message);
|
||||
if written {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
// Continue the loop to send tool results back
|
||||
continue;
|
||||
}
|
||||
|
||||
// No tool calls from assistant, but we only exit if the task was finished
|
||||
if !finished {
|
||||
self.log("--- Assistant didn't finish yet. Waiting for next turn... ---");
|
||||
}
|
||||
}
|
||||
|
||||
Ok((self.logs.clone(), self.answer.clone()))
|
||||
}
|
||||
}
|
||||
83
src/domain/agent/tools.rs
Normal file
83
src/domain/agent/tools.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use super::api::{self, FunctionDefinition, Message, Tool, ToolCall};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn get_tools() -> Vec<Tool> {
|
||||
vec![
|
||||
Tool {
|
||||
tool_type: "function".to_string(),
|
||||
function: FunctionDefinition {
|
||||
name: "google_search".to_string(),
|
||||
description: "Search the web for information".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}),
|
||||
},
|
||||
},
|
||||
Tool {
|
||||
tool_type: "function".to_string(),
|
||||
function: FunctionDefinition {
|
||||
name: "finish".to_string(),
|
||||
description: "Finish the task".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string",
|
||||
"description": "The result of the task"
|
||||
}
|
||||
},
|
||||
"required": ["result"]
|
||||
}),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub async fn handle_tool_call(
|
||||
tool_call: &ToolCall,
|
||||
tavily_api_key: &Option<String>,
|
||||
) -> Result<(Message, bool, Option<String>), Box<dyn std::error::Error>> {
|
||||
let mut answer = None;
|
||||
let name = &tool_call.function.name;
|
||||
|
||||
let (content, written) = if name == "google_search" {
|
||||
let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?;
|
||||
let query = args.get("query").ok_or("Missing query argument")?;
|
||||
|
||||
let search_result = if let Some(key) = tavily_api_key {
|
||||
match api::perform_search(query, key).await {
|
||||
Ok(results) => results,
|
||||
Err(e) => format!("Search error: {}", e),
|
||||
}
|
||||
} else {
|
||||
"Error: TAVILY_API_KEY is not set. Cannot perform real search.".to_string()
|
||||
};
|
||||
(search_result, false)
|
||||
} else if name == "finish" {
|
||||
let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?;
|
||||
let result = args.get("result").ok_or("Missing result argument")?;
|
||||
|
||||
answer = Some(result.clone());
|
||||
(result.clone(), true)
|
||||
} else {
|
||||
(format!("Error: Unknown tool {}", name), false)
|
||||
};
|
||||
|
||||
Ok((
|
||||
Message {
|
||||
role: "tool".to_string(),
|
||||
content: Some(content),
|
||||
tool_calls: None,
|
||||
tool_call_id: Some(tool_call.id.clone()),
|
||||
},
|
||||
written,
|
||||
answer,
|
||||
))
|
||||
}
|
||||
193
src/domain/auth.rs
Normal file
193
src/domain/auth.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub exp: usize,
|
||||
pub iat: usize,
|
||||
pub iss: String,
|
||||
pub aud: Audience,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Audience {
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct Jwk {
|
||||
#[serde(rename = "kty")]
|
||||
_kty: String,
|
||||
kid: String,
|
||||
n: String,
|
||||
e: String,
|
||||
#[serde(rename = "alg")]
|
||||
_alg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Jwks {
|
||||
keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
pub struct JwksVerifier {
|
||||
issuer: String,
|
||||
audience: String,
|
||||
jwks_uri: String,
|
||||
keys: Arc<RwLock<Vec<Jwk>>>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl JwksVerifier {
|
||||
pub async fn new(issuer: String, audience: String) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
let discovery_url = format!(
|
||||
"{}/.well-known/openid-configuration",
|
||||
issuer.trim_end_matches('/')
|
||||
);
|
||||
let config: serde_json::Value = client.get(&discovery_url).send().await?.json().await?;
|
||||
|
||||
let jwks_uri = config["jwks_uri"]
|
||||
.as_str()
|
||||
.ok_or("Missing jwks_uri in discovery")?
|
||||
.to_string();
|
||||
|
||||
let verifier = Self {
|
||||
issuer,
|
||||
audience,
|
||||
jwks_uri,
|
||||
keys: Arc::new(RwLock::new(Vec::new())),
|
||||
client,
|
||||
};
|
||||
|
||||
verifier.refresh_keys().await?;
|
||||
Ok(verifier)
|
||||
}
|
||||
|
||||
pub async fn refresh_keys(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let jwks: Jwks = self.client.get(&self.jwks_uri).send().await?.json().await?;
|
||||
let mut keys = self.keys.write().await;
|
||||
*keys = jwks.keys;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify(&self, token: &str) -> Result<Claims, Box<dyn std::error::Error>> {
|
||||
let header = decode_header(token)?;
|
||||
let kid = header.kid.ok_or("Missing kid in token header")?;
|
||||
|
||||
let jwk = {
|
||||
let keys = self.keys.read().await;
|
||||
keys.iter().find(|k| k.kid == kid).cloned()
|
||||
};
|
||||
|
||||
let jwk = match jwk {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
self.refresh_keys().await?;
|
||||
let keys = self.keys.read().await;
|
||||
keys.iter()
|
||||
.find(|k| k.kid == kid)
|
||||
.cloned()
|
||||
.ok_or("Key not found in JWKS")?
|
||||
}
|
||||
};
|
||||
|
||||
let decoding_key = DecodingKey::from_rsa_components(&jwk.n, &jwk.e)?;
|
||||
|
||||
let mut validation = Validation::new(Algorithm::RS256);
|
||||
validation.set_issuer(&[self.issuer.clone()]);
|
||||
validation.set_audience(&[self.audience.clone()]);
|
||||
validation.validate_aud = true;
|
||||
|
||||
let token_data = decode::<Claims>(token, &decoding_key, &validation)?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Authenticator {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
token_url: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Authenticator {
|
||||
pub async fn new(
|
||||
issuer: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
let discovery_url = format!(
|
||||
"{}/.well-known/openid-configuration",
|
||||
issuer.trim_end_matches('/')
|
||||
);
|
||||
let config: serde_json::Value = client.get(&discovery_url).send().await?.json().await?;
|
||||
|
||||
let token_url = config["token_endpoint"]
|
||||
.as_str()
|
||||
.ok_or("Missing token_endpoint in discovery")?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
client_id,
|
||||
client_secret,
|
||||
token_url,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn exchange_code(
|
||||
&self,
|
||||
code: String,
|
||||
redirect_uri: String,
|
||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
let params = [
|
||||
("grant_type", "authorization_code"),
|
||||
("code", &code),
|
||||
("redirect_uri", &redirect_uri),
|
||||
("client_id", &self.client_id),
|
||||
("client_secret", &self.client_secret),
|
||||
];
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(&self.token_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn refresh_token(
|
||||
&self,
|
||||
refresh_token: String,
|
||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
let params = [
|
||||
("grant_type", "refresh_token"),
|
||||
("refresh_token", &refresh_token),
|
||||
("client_id", &self.client_id),
|
||||
("client_secret", &self.client_secret),
|
||||
];
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(&self.token_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
3
src/domain/mod.rs
Normal file
3
src/domain/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod tasks;
|
||||
131
src/domain/tasks.rs
Normal file
131
src/domain/tasks.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::domain::agent::Agent;
|
||||
use crate::entities::task::Entity as Task;
|
||||
use crate::entities::task_run::{self, Entity as TaskRun};
|
||||
use crate::scheduler::Scheduler;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateTaskRequest {
|
||||
pub goal: String,
|
||||
pub cron: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateTaskRequest {
|
||||
pub goal: String,
|
||||
pub cron: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TaskResponse {
|
||||
pub id: Uuid,
|
||||
pub goal: String,
|
||||
pub cron: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::FixedOffset>,
|
||||
pub runs: Vec<TaskRunResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TaskRunResponse {
|
||||
pub id: Uuid,
|
||||
pub status: String,
|
||||
pub logs: String,
|
||||
pub answer: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::FixedOffset>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RecentRunResponse {
|
||||
pub id: Uuid,
|
||||
pub task_id: Uuid,
|
||||
pub goal: String,
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<chrono::FixedOffset>,
|
||||
}
|
||||
|
||||
pub async fn execute_agent_run(
|
||||
db: &DatabaseConnection,
|
||||
_scheduler: &Arc<Scheduler>,
|
||||
config: &Arc<Config>,
|
||||
task_id: Uuid,
|
||||
goal: String,
|
||||
) -> Result<TaskResponse, Box<dyn std::error::Error>> {
|
||||
let run_id = Uuid::new_v4();
|
||||
|
||||
let new_run = task_run::ActiveModel {
|
||||
id: Set(run_id),
|
||||
task_id: Set(task_id),
|
||||
status: Set("running".to_string()),
|
||||
logs: Set(String::new()),
|
||||
answer: Set(None),
|
||||
created_at: Set(Utc::now().into()),
|
||||
};
|
||||
|
||||
new_run.insert(db).await?;
|
||||
|
||||
let mut agent = Agent::new(
|
||||
config.zen_api_key.clone(),
|
||||
config.tavily_api_key.clone(),
|
||||
goal.clone(),
|
||||
)?;
|
||||
|
||||
let (logs, answer, status) = match agent.run(config).await {
|
||||
Ok((logs, answer)) => (logs, answer, "completed".to_string()),
|
||||
Err(e) => (
|
||||
format!("Execution failed: {}", e),
|
||||
None,
|
||||
"failed".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let mut run: task_run::ActiveModel = TaskRun::find_by_id(run_id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or("Run not found after insert")?
|
||||
.into();
|
||||
|
||||
run.logs = Set(logs.clone());
|
||||
run.answer = Set(answer.clone());
|
||||
run.status = Set(status);
|
||||
|
||||
run.update(db).await?;
|
||||
|
||||
get_task_inner(task_id, db).await
|
||||
}
|
||||
|
||||
pub async fn get_task_inner(
|
||||
id: Uuid,
|
||||
db: &DatabaseConnection,
|
||||
) -> Result<TaskResponse, Box<dyn std::error::Error>> {
|
||||
let results = Task::find_by_id(id)
|
||||
.find_with_related(TaskRun)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let (t, mut runs) = results.into_iter().next().ok_or("Task not found")?;
|
||||
|
||||
runs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
Ok(TaskResponse {
|
||||
id: t.id,
|
||||
goal: t.goal,
|
||||
cron: t.cron,
|
||||
created_at: t.created_at,
|
||||
runs: runs
|
||||
.into_iter()
|
||||
.map(|r| TaskRunResponse {
|
||||
id: r.id,
|
||||
status: r.status,
|
||||
logs: r.logs,
|
||||
answer: r.answer,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue