Compare commits

..

No commits in common. "main" and "0.0.20" have entirely different histories.

7 changed files with 96 additions and 740 deletions

View file

@ -1,593 +0,0 @@
{
"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,8 +75,6 @@ pub async fn perform_search(
tracing::info!(query = %query, "Performing Tavily web search");
let client = reqwest::Client::builder()
.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()?;
let response = client
.post("https://api.tavily.com/search")

View file

@ -77,9 +77,6 @@ impl Agent {
let client = reqwest::Client::builder()
.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()
.map_err(|e| AppError::Internal(format!("Failed to build HTTP client: {}", e)))?;
@ -122,8 +119,6 @@ impl Agent {
return Err(AppError::Internal("Agent run exceeded max turns".into()));
}
tracing::info!("Turn {}", turns);
turns += 1;
let current_role = self
.messages
@ -135,39 +130,17 @@ impl Agent {
turns, current_role
));
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()));
}
};
let assistant_message = self.execute_turn().await?;
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") {
finished = true;
}
}
if self.answer.is_some() {
tracing::info!("Answer: {}", self.answer.as_ref().unwrap());
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 ---");
@ -175,12 +148,10 @@ impl Agent {
}
pub async fn execute_turn(&mut self) -> AppResult<Message> {
let max_sub_turns = 20;
let max_sub_turns = 10;
let mut sub_turns = 0;
loop {
tracing::info!("Sub turn {}", sub_turns);
sub_turns += 1;
if sub_turns > max_sub_turns {
return Err(AppError::Internal(
@ -196,19 +167,15 @@ impl Agent {
.message
.clone();
tracing::info!("Assistant message: {:#?}", assistant_message);
self.messages.push(assistant_message.clone());
if let Some(content) = &assistant_message.content {
tracing::info!("Assistant content: {}", content);
if !content.is_empty() {
self.log(&format!("\nAssistant: {}", content));
}
}
if let Some(tool_calls) = &assistant_message.tool_calls {
tracing::info!("Assistant tool calls: {:#?}", tool_calls);
let mut is_final_cycle = false;
let mut final_answer = None;
@ -242,7 +209,6 @@ impl Agent {
}
if is_final_cycle {
tracing::info!("Final answer: {}", final_answer.as_ref().unwrap());
return Ok(Message {
role: "assistant".to_string(),
content: final_answer.or(assistant_message.content),
@ -265,11 +231,7 @@ impl Agent {
tools: self.tools.clone(),
};
let mut request_builder = self
.client
.post(&self.url)
.json(&request)
.timeout(Duration::from_secs(60));
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));
@ -279,12 +241,10 @@ impl Agent {
let response = request_builder.send().await.map_err(|e| {
let duration = start.elapsed();
let is_timeout = e.is_timeout();
let is_connect = e.is_connect();
tracing::error!(
"Network error after {:?} during LLM call (Timeout: {}, Connect: {}): {:?}",
"Network error after {:?} during LLM call (Timeout: {}): {:?}",
duration,
is_timeout,
is_connect,
e
);
AppError::Network(e)

View file

@ -46,10 +46,7 @@ pub struct JwksVerifier {
impl JwksVerifier {
pub async fn new(issuer: String, audience: String) -> Result<Self, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(10))
.build()?;
let client = Client::new();
let discovery_url = format!(
"{}/.well-known/openid-configuration",
issuer.trim_end_matches('/')
@ -126,10 +123,7 @@ impl Authenticator {
client_id: String,
client_secret: String,
) -> Result<Self, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(10))
.build()?;
let client = Client::new();
let discovery_url = format!(
"{}/.well-known/openid-configuration",
issuer.trim_end_matches('/')

View file

@ -36,15 +36,9 @@ pub struct CalendarClient {
impl CalendarClient {
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 {
base_url: base_url.trim_end_matches('/').to_string(),
client,
client: Client::new(),
authenticator,
token_state: RwLock::new(None),
}

View file

@ -61,7 +61,7 @@ pub async fn execute_agent_run(
goal: String,
) -> AppResult<TaskResponse> {
let run_id = Uuid::new_v4();
tracing::info!(%task_id, %run_id, "Starting background agent execution run");
tracing::info!(%task_id, %run_id, "Starting agent execution run");
let new_run = task_run::ActiveModel {
id: Set(run_id),
@ -98,19 +98,13 @@ pub async fn execute_agent_run(
goal.clone(),
)?;
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 {
let (logs, answer, status) = match agent.run(config).await {
Ok((logs, answer)) => {
tracing::info!(task_id = %task_id_bg, run_id = %run_id, "Agent execution completed successfully");
tracing::info!(%task_id, %run_id, "Agent execution completed successfully");
(logs, answer, "completed".to_string())
}
Err(e) => {
tracing::error!(task_id = %task_id_bg, run_id = %run_id, error = %e, "Agent execution failed");
tracing::error!(%task_id, %run_id, error = %e, "Agent execution failed");
(
format!("Execution failed: {}", e),
None,
@ -119,41 +113,59 @@ pub async fn execute_agent_run(
}
};
let run_update = task_run::ActiveModel {
id: Set(run_id),
logs: Set(logs),
answer: Set(answer),
status: Set(status.clone()),
..Default::default()
};
let run: task_run::ActiveModel = TaskRun::find_by_id(run_id)
.one(db)
.await
.map_err(crate::error::AppError::Database)?
.ok_or_else(|| crate::error::AppError::NotFound("Run not found after insert".into()))?
.into();
if let Err(e) = run_update.update(&db_bg).await {
tracing::error!(task_id = %task_id_bg, run_id = %run_id, error = %e, "Failed to update run record");
}
let mut run = run;
run.logs = Set(logs.clone());
run.answer = Set(answer.clone());
run.status = Set(status.clone());
if let Ok(task_response) = get_task_inner(task_id_bg, &db_bg).await {
let _ = scheduler_bg
run.update(db)
.await
.map_err(crate::error::AppError::Database)?;
let task_response = get_task_inner(task_id, db).await?;
let _ = _scheduler
.tx
.send(crate::server::notifications::WsEvent::RunFinished(
task_response.clone(),
));
// Send Push Notifications to subscribers
if let Ok(subscriptions) = task_subscription::Entity::find()
.filter(task_subscription::Column::TaskId.eq(task_id_bg))
.all(&db_bg)
let subscriptions = task_subscription::Entity::find()
.filter(task_subscription::Column::TaskId.eq(task_id))
.all(db)
.await
{
.map_err(crate::error::AppError::Database)?;
tracing::info!(
"Found {} task subscriptions for task {}",
subscriptions.len(),
task_id
);
for sub in subscriptions {
if let Ok(push_subs) = push_subscription::Entity::find()
let push_subs = push_subscription::Entity::find()
.filter(push_subscription::Column::UserSub.eq(sub.user_sub.clone()))
.all(&db_bg)
.all(db)
.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 {
let sender = scheduler_bg.push_sender.clone();
let sender = _scheduler.push_sender.clone();
let goal = task_response.goal.clone();
let status_bg = status.clone();
let status = status.clone();
let sub_data = crate::domain::notifications::push::PushSubscription {
endpoint: push_sub.endpoint,
p256dh: push_sub.p256dh,
@ -161,24 +173,23 @@ pub async fn execute_agent_run(
};
tokio::spawn(async move {
let _ = sender
if let Err(e) = sender
.send_notification(
&sub_data,
&format!("Task Completed: {}", status_bg),
&format!("Task Completed: {}", status),
&goal,
Some(task_id_bg),
Some(task_id),
Some(run_id),
)
.await;
});
}
}
}
}
.await
{
tracing::error!("Failed to send notification in background task: {}", e);
}
});
}
}
get_task_inner(task_id, db).await
Ok(task_response)
}
pub async fn get_task_inner(id: Uuid, db: &DatabaseConnection) -> AppResult<TaskResponse> {

View file

@ -52,15 +52,7 @@ pub async fn chat_handler(
)?;
tracing::info!("Starting interactive agent turn");
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()));
}
};
let assistant_message = agent.execute_turn().await?;
Ok(Json(ChatResult {
message: assistant_message,