Compare commits

..

2 commits

Author SHA1 Message Date
f9e976c224 a
All checks were successful
/ upload (release) Successful in 36s
2026-02-20 18:42:59 +01:00
7384bcaf40 idk
All checks were successful
/ upload (release) Successful in 41s
2026-02-20 18:00:43 +01:00
7 changed files with 740 additions and 96 deletions

View file

@ -0,0 +1,593 @@
{
"openapi": "3.1.0",
"info": {
"title": "calendar",
"description": "",
"license": {
"name": ""
},
"version": "0.1.0"
},
"paths": {
"/auth/me": {
"get": {
"tags": [
"crate::handlers::auth"
],
"operationId": "me",
"responses": {
"200": {
"description": "Current user profile",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentUser"
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"oidc": []
}
]
}
},
"/events": {
"get": {
"tags": [
"crate::handlers::event"
],
"operationId": "list_events",
"parameters": [
{
"name": "upcoming",
"in": "query",
"required": false,
"schema": {
"type": [
"boolean",
"null"
]
}
}
],
"responses": {
"200": {
"description": "List of events",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Model"
}
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"oidc": []
}
]
},
"post": {
"tags": [
"crate::handlers::event"
],
"operationId": "create_event",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateEventRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Event created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Model"
}
}
}
},
"400": {
"description": "Invalid request payload"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"oidc": []
}
]
}
},
"/events/{id}": {
"get": {
"tags": [
"crate::handlers::event"
],
"operationId": "get_event",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Event database id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Event details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Model"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Event not found"
}
},
"security": [
{
"oidc": []
}
]
},
"put": {
"tags": [
"crate::handlers::event"
],
"operationId": "update_event",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Event database id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateEventRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Event updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Model"
}
}
}
},
"400": {
"description": "Invalid request payload"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Event not found"
}
},
"security": [
{
"oidc": []
}
]
},
"delete": {
"tags": [
"crate::handlers::event"
],
"operationId": "delete_event",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Event database id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"204": {
"description": "Event deleted successfully"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Event not found"
}
},
"security": [
{
"oidc": []
}
]
}
},
"/service/v1/events": {
"get": {
"tags": [
"crate::handlers::service"
],
"operationId": "service_list_events",
"parameters": [
{
"name": "user_id",
"in": "query",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int32"
}
},
{
"name": "upcoming",
"in": "query",
"required": false,
"schema": {
"type": [
"boolean",
"null"
]
}
}
],
"responses": {
"200": {
"description": "List of events",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Model"
}
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"oidc": []
}
]
},
"post": {
"tags": [
"crate::handlers::service"
],
"operationId": "service_create_event",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceCreateEventRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Event created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Model"
}
}
}
},
"400": {
"description": "Invalid request payload"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"oidc": []
}
]
}
},
"/service/v1/events/{id}": {
"get": {
"tags": [
"crate::handlers::service"
],
"operationId": "service_get_event",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Event database id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Event details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Model"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Event not found"
}
},
"security": [
{
"oidc": []
}
]
},
"put": {
"tags": [
"crate::handlers::service"
],
"operationId": "service_update_event",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Event database id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceCreateEventRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Event updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Model"
}
}
}
},
"400": {
"description": "Invalid request payload"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Event not found"
}
},
"security": [
{
"oidc": []
}
]
},
"delete": {
"tags": [
"crate::handlers::service"
],
"operationId": "service_delete_event",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Event database id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"204": {
"description": "Event deleted successfully"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Event not found"
}
},
"security": [
{
"oidc": []
}
]
}
}
},
"components": {
"schemas": {
"CreateEventRequest": {
"type": "object",
"required": [
"name",
"from",
"to"
],
"properties": {
"from": {
"type": "string"
},
"name": {
"type": "string"
},
"to": {
"type": "string"
}
}
},
"CurrentUser": {
"type": "object",
"required": [
"id",
"sub",
"email",
"name"
],
"properties": {
"email": {
"type": "string"
},
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
},
"sub": {
"type": "string"
}
}
},
"Model": {
"type": "object",
"required": [
"id",
"name",
"from",
"to"
],
"properties": {
"from": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"to": {
"type": "string",
"format": "date-time"
},
"user_id": {
"type": [
"integer",
"null"
],
"format": "int32"
}
}
},
"ServiceCreateEventRequest": {
"type": "object",
"required": [
"name",
"from",
"to"
],
"properties": {
"from": {
"type": "string"
},
"name": {
"type": "string"
},
"to": {
"type": "string"
},
"user_id": {
"type": [
"integer",
"null"
],
"format": "int32"
}
}
}
}
},
"tags": [
{
"name": "calendar",
"description": "Calendar Management API"
}
]
}

View file

@ -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")

View file

@ -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,17 +135,39 @@ 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;
} }
if !finished {
self.messages.push(Message {
role: "system".to_string(),
content: Some(
"continue, use the finish tool to submit your final answer".to_string(),
),
tool_calls: None,
tool_call_id: None,
});
}
tracing::info!("Finished turn");
} }
self.log("\n--- Execution Finished ---"); self.log("\n--- Execution Finished ---");
@ -148,10 +175,12 @@ impl Agent {
} }
pub async fn execute_turn(&mut self) -> AppResult<Message> { pub async fn execute_turn(&mut self) -> AppResult<Message> {
let max_sub_turns = 10; let max_sub_turns = 20;
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(
@ -167,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;
@ -209,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),
@ -231,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));
@ -241,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)

View file

@ -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('/')

View file

@ -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),
} }

View file

@ -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> {

View file

@ -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,