Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| f9e976c224 |
6 changed files with 135 additions and 95 deletions
|
|
@ -75,6 +75,8 @@ pub async fn perform_search(
|
||||||
tracing::info!(query = %query, "Performing Tavily web search");
|
tracing::info!(query = %query, "Performing Tavily web search");
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.pool_idle_timeout(std::time::Duration::from_secs(60))
|
||||||
.build()?;
|
.build()?;
|
||||||
let response = client
|
let response = client
|
||||||
.post("https://api.tavily.com/search")
|
.post("https://api.tavily.com/search")
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,9 @@ impl Agent {
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(120))
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.tcp_keepalive(std::time::Duration::from_secs(30))
|
||||||
|
.pool_idle_timeout(std::time::Duration::from_secs(60))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppError::Internal(format!("Failed to build HTTP client: {}", e)))?;
|
.map_err(|e| AppError::Internal(format!("Failed to build HTTP client: {}", e)))?;
|
||||||
|
|
||||||
|
|
@ -119,6 +122,8 @@ impl Agent {
|
||||||
return Err(AppError::Internal("Agent run exceeded max turns".into()));
|
return Err(AppError::Internal("Agent run exceeded max turns".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("Turn {}", turns);
|
||||||
|
|
||||||
turns += 1;
|
turns += 1;
|
||||||
let current_role = self
|
let current_role = self
|
||||||
.messages
|
.messages
|
||||||
|
|
@ -130,15 +135,24 @@ impl Agent {
|
||||||
turns, current_role
|
turns, current_role
|
||||||
));
|
));
|
||||||
|
|
||||||
let assistant_message = self.execute_turn().await?;
|
let assistant_message =
|
||||||
|
match tokio::time::timeout(Duration::from_secs(180), self.execute_turn()).await {
|
||||||
|
Ok(res) => res?,
|
||||||
|
Err(_) => {
|
||||||
|
tracing::error!("Agent execution turn timed out after 180s");
|
||||||
|
return Err(AppError::Internal("Agent execution turn timed out".into()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(tool_calls) = &assistant_message.tool_calls {
|
if let Some(tool_calls) = &assistant_message.tool_calls {
|
||||||
|
tracing::info!("Assistant tool calls: {:#?}", tool_calls);
|
||||||
if tool_calls.iter().any(|tc| tc.function.name == "answer") {
|
if tool_calls.iter().any(|tc| tc.function.name == "answer") {
|
||||||
finished = true;
|
finished = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.answer.is_some() {
|
if self.answer.is_some() {
|
||||||
|
tracing::info!("Answer: {}", self.answer.as_ref().unwrap());
|
||||||
finished = true;
|
finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +166,8 @@ impl Agent {
|
||||||
tool_call_id: None,
|
tool_call_id: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("Finished turn");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.log("\n--- Execution Finished ---");
|
self.log("\n--- Execution Finished ---");
|
||||||
|
|
@ -163,6 +179,8 @@ impl Agent {
|
||||||
let mut sub_turns = 0;
|
let mut sub_turns = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
tracing::info!("Sub turn {}", sub_turns);
|
||||||
|
|
||||||
sub_turns += 1;
|
sub_turns += 1;
|
||||||
if sub_turns > max_sub_turns {
|
if sub_turns > max_sub_turns {
|
||||||
return Err(AppError::Internal(
|
return Err(AppError::Internal(
|
||||||
|
|
@ -178,15 +196,19 @@ impl Agent {
|
||||||
.message
|
.message
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
|
tracing::info!("Assistant message: {:#?}", assistant_message);
|
||||||
|
|
||||||
self.messages.push(assistant_message.clone());
|
self.messages.push(assistant_message.clone());
|
||||||
|
|
||||||
if let Some(content) = &assistant_message.content {
|
if let Some(content) = &assistant_message.content {
|
||||||
|
tracing::info!("Assistant content: {}", content);
|
||||||
if !content.is_empty() {
|
if !content.is_empty() {
|
||||||
self.log(&format!("\nAssistant: {}", content));
|
self.log(&format!("\nAssistant: {}", content));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tool_calls) = &assistant_message.tool_calls {
|
if let Some(tool_calls) = &assistant_message.tool_calls {
|
||||||
|
tracing::info!("Assistant tool calls: {:#?}", tool_calls);
|
||||||
let mut is_final_cycle = false;
|
let mut is_final_cycle = false;
|
||||||
let mut final_answer = None;
|
let mut final_answer = None;
|
||||||
|
|
||||||
|
|
@ -220,6 +242,7 @@ impl Agent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_final_cycle {
|
if is_final_cycle {
|
||||||
|
tracing::info!("Final answer: {}", final_answer.as_ref().unwrap());
|
||||||
return Ok(Message {
|
return Ok(Message {
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
content: final_answer.or(assistant_message.content),
|
content: final_answer.or(assistant_message.content),
|
||||||
|
|
@ -242,7 +265,11 @@ impl Agent {
|
||||||
tools: self.tools.clone(),
|
tools: self.tools.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut request_builder = self.client.post(&self.url).json(&request);
|
let mut request_builder = self
|
||||||
|
.client
|
||||||
|
.post(&self.url)
|
||||||
|
.json(&request)
|
||||||
|
.timeout(Duration::from_secs(60));
|
||||||
|
|
||||||
if let Some(key) = &self.zen_api_key {
|
if let Some(key) = &self.zen_api_key {
|
||||||
request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
|
request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
|
||||||
|
|
@ -252,10 +279,12 @@ impl Agent {
|
||||||
let response = request_builder.send().await.map_err(|e| {
|
let response = request_builder.send().await.map_err(|e| {
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
let is_timeout = e.is_timeout();
|
let is_timeout = e.is_timeout();
|
||||||
|
let is_connect = e.is_connect();
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Network error after {:?} during LLM call (Timeout: {}): {:?}",
|
"Network error after {:?} during LLM call (Timeout: {}, Connect: {}): {:?}",
|
||||||
duration,
|
duration,
|
||||||
is_timeout,
|
is_timeout,
|
||||||
|
is_connect,
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
AppError::Network(e)
|
AppError::Network(e)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ pub struct JwksVerifier {
|
||||||
|
|
||||||
impl JwksVerifier {
|
impl JwksVerifier {
|
||||||
pub async fn new(issuer: String, audience: String) -> Result<Self, Box<dyn std::error::Error>> {
|
pub async fn new(issuer: String, audience: String) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let client = Client::new();
|
let client = Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()?;
|
||||||
let discovery_url = format!(
|
let discovery_url = format!(
|
||||||
"{}/.well-known/openid-configuration",
|
"{}/.well-known/openid-configuration",
|
||||||
issuer.trim_end_matches('/')
|
issuer.trim_end_matches('/')
|
||||||
|
|
@ -123,7 +126,10 @@ impl Authenticator {
|
||||||
client_id: String,
|
client_id: String,
|
||||||
client_secret: String,
|
client_secret: String,
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let client = Client::new();
|
let client = Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()?;
|
||||||
let discovery_url = format!(
|
let discovery_url = format!(
|
||||||
"{}/.well-known/openid-configuration",
|
"{}/.well-known/openid-configuration",
|
||||||
issuer.trim_end_matches('/')
|
issuer.trim_end_matches('/')
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,15 @@ pub struct CalendarClient {
|
||||||
|
|
||||||
impl CalendarClient {
|
impl CalendarClient {
|
||||||
pub fn new(base_url: String, authenticator: Arc<Authenticator>) -> Self {
|
pub fn new(base_url: String, authenticator: Arc<Authenticator>) -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| Client::new());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base_url: base_url.trim_end_matches('/').to_string(),
|
base_url: base_url.trim_end_matches('/').to_string(),
|
||||||
client: Client::new(),
|
client,
|
||||||
authenticator,
|
authenticator,
|
||||||
token_state: RwLock::new(None),
|
token_state: RwLock::new(None),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ pub async fn execute_agent_run(
|
||||||
goal: String,
|
goal: String,
|
||||||
) -> AppResult<TaskResponse> {
|
) -> AppResult<TaskResponse> {
|
||||||
let run_id = Uuid::new_v4();
|
let run_id = Uuid::new_v4();
|
||||||
tracing::info!(%task_id, %run_id, "Starting agent execution run");
|
tracing::info!(%task_id, %run_id, "Starting background agent execution run");
|
||||||
|
|
||||||
let new_run = task_run::ActiveModel {
|
let new_run = task_run::ActiveModel {
|
||||||
id: Set(run_id),
|
id: Set(run_id),
|
||||||
|
|
@ -98,13 +98,19 @@ pub async fn execute_agent_run(
|
||||||
goal.clone(),
|
goal.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (logs, answer, status) = match agent.run(config).await {
|
let db_bg = db.clone();
|
||||||
|
let config_bg = config.clone();
|
||||||
|
let scheduler_bg = _scheduler.clone();
|
||||||
|
let task_id_bg = task_id;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let (logs, answer, status) = match agent.run(&config_bg).await {
|
||||||
Ok((logs, answer)) => {
|
Ok((logs, answer)) => {
|
||||||
tracing::info!(%task_id, %run_id, "Agent execution completed successfully");
|
tracing::info!(task_id = %task_id_bg, run_id = %run_id, "Agent execution completed successfully");
|
||||||
(logs, answer, "completed".to_string())
|
(logs, answer, "completed".to_string())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(%task_id, %run_id, error = %e, "Agent execution failed");
|
tracing::error!(task_id = %task_id_bg, run_id = %run_id, error = %e, "Agent execution failed");
|
||||||
(
|
(
|
||||||
format!("Execution failed: {}", e),
|
format!("Execution failed: {}", e),
|
||||||
None,
|
None,
|
||||||
|
|
@ -113,59 +119,41 @@ pub async fn execute_agent_run(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let run: task_run::ActiveModel = TaskRun::find_by_id(run_id)
|
let run_update = task_run::ActiveModel {
|
||||||
.one(db)
|
id: Set(run_id),
|
||||||
.await
|
logs: Set(logs),
|
||||||
.map_err(crate::error::AppError::Database)?
|
answer: Set(answer),
|
||||||
.ok_or_else(|| crate::error::AppError::NotFound("Run not found after insert".into()))?
|
status: Set(status.clone()),
|
||||||
.into();
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut run = run;
|
if let Err(e) = run_update.update(&db_bg).await {
|
||||||
run.logs = Set(logs.clone());
|
tracing::error!(task_id = %task_id_bg, run_id = %run_id, error = %e, "Failed to update run record");
|
||||||
run.answer = Set(answer.clone());
|
}
|
||||||
run.status = Set(status.clone());
|
|
||||||
|
|
||||||
run.update(db)
|
if let Ok(task_response) = get_task_inner(task_id_bg, &db_bg).await {
|
||||||
.await
|
let _ = scheduler_bg
|
||||||
.map_err(crate::error::AppError::Database)?;
|
|
||||||
|
|
||||||
let task_response = get_task_inner(task_id, db).await?;
|
|
||||||
let _ = _scheduler
|
|
||||||
.tx
|
.tx
|
||||||
.send(crate::server::notifications::WsEvent::RunFinished(
|
.send(crate::server::notifications::WsEvent::RunFinished(
|
||||||
task_response.clone(),
|
task_response.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Send Push Notifications to subscribers
|
// Send Push Notifications to subscribers
|
||||||
let subscriptions = task_subscription::Entity::find()
|
if let Ok(subscriptions) = task_subscription::Entity::find()
|
||||||
.filter(task_subscription::Column::TaskId.eq(task_id))
|
.filter(task_subscription::Column::TaskId.eq(task_id_bg))
|
||||||
.all(db)
|
.all(&db_bg)
|
||||||
.await
|
.await
|
||||||
.map_err(crate::error::AppError::Database)?;
|
{
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Found {} task subscriptions for task {}",
|
|
||||||
subscriptions.len(),
|
|
||||||
task_id
|
|
||||||
);
|
|
||||||
|
|
||||||
for sub in subscriptions {
|
for sub in subscriptions {
|
||||||
let push_subs = push_subscription::Entity::find()
|
if let Ok(push_subs) = push_subscription::Entity::find()
|
||||||
.filter(push_subscription::Column::UserSub.eq(sub.user_sub.clone()))
|
.filter(push_subscription::Column::UserSub.eq(sub.user_sub.clone()))
|
||||||
.all(db)
|
.all(&db_bg)
|
||||||
.await
|
.await
|
||||||
.map_err(crate::error::AppError::Database)?;
|
{
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Found {} push subscriptions for user {}",
|
|
||||||
push_subs.len(),
|
|
||||||
sub.user_sub
|
|
||||||
);
|
|
||||||
|
|
||||||
for push_sub in push_subs {
|
for push_sub in push_subs {
|
||||||
let sender = _scheduler.push_sender.clone();
|
let sender = scheduler_bg.push_sender.clone();
|
||||||
let goal = task_response.goal.clone();
|
let goal = task_response.goal.clone();
|
||||||
let status = status.clone();
|
let status_bg = status.clone();
|
||||||
let sub_data = crate::domain::notifications::push::PushSubscription {
|
let sub_data = crate::domain::notifications::push::PushSubscription {
|
||||||
endpoint: push_sub.endpoint,
|
endpoint: push_sub.endpoint,
|
||||||
p256dh: push_sub.p256dh,
|
p256dh: push_sub.p256dh,
|
||||||
|
|
@ -173,23 +161,24 @@ pub async fn execute_agent_run(
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = sender
|
let _ = sender
|
||||||
.send_notification(
|
.send_notification(
|
||||||
&sub_data,
|
&sub_data,
|
||||||
&format!("Task Completed: {}", status),
|
&format!("Task Completed: {}", status_bg),
|
||||||
&goal,
|
&goal,
|
||||||
Some(task_id),
|
Some(task_id_bg),
|
||||||
Some(run_id),
|
Some(run_id),
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
{
|
|
||||||
tracing::error!("Failed to send notification in background task: {}", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(task_response)
|
get_task_inner(task_id, db).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_task_inner(id: Uuid, db: &DatabaseConnection) -> AppResult<TaskResponse> {
|
pub async fn get_task_inner(id: Uuid, db: &DatabaseConnection) -> AppResult<TaskResponse> {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,15 @@ pub async fn chat_handler(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
tracing::info!("Starting interactive agent turn");
|
tracing::info!("Starting interactive agent turn");
|
||||||
let assistant_message = agent.execute_turn().await?;
|
let assistant_message =
|
||||||
|
match tokio::time::timeout(std::time::Duration::from_secs(180), agent.execute_turn()).await
|
||||||
|
{
|
||||||
|
Ok(res) => res?,
|
||||||
|
Err(_) => {
|
||||||
|
tracing::error!("Interactive chat agent turn timed out after 180s");
|
||||||
|
return Err(AppError::Internal("Agent turn timed out".into()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Json(ChatResult {
|
Ok(Json(ChatResult {
|
||||||
message: assistant_message,
|
message: assistant_message,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue