use ai::error::{AiError, AiResult}; use ai::tool::tools::FunctionCall; use async_trait::async_trait; use db::sqlx; use serde_json::{Value, json}; use uuid::Uuid; use super::helpers::{arg_str, git_ctx, require_workspace_member}; use crate::agent::run::AppAgentContext; pub struct WorkspaceInfoTool; impl WorkspaceInfoTool { pub fn new() -> Self { Self } } impl Default for WorkspaceInfoTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for WorkspaceInfoTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "workspace_info" } fn description(&self) -> &'static str { "Get information about a workspace: name, description, avatar." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" } }, "required": ["workspace"] }) } async fn call( &self, ctx: &mut AppAgentContext, args: Value, ) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let _wk_id = require_workspace_member(git, ctx.user_id, workspace).await?; let row = sqlx::query_as::<_, (String, String, String, chrono::DateTime)>( "SELECT name, description, avatar_url, created_at FROM workspace WHERE name = $1", ) .bind(workspace) .fetch_optional(git.db.reader()) .await .map_err(AiError::Database)? .ok_or_else(|| AiError::Config(format!("workspace '{workspace}' not found")))?; let member_count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM wk_member WHERE wk = (SELECT id FROM workspace WHERE name = $1) AND leave_at IS NULL", ) .bind(workspace) .fetch_one(git.db.reader()) .await .map_err(AiError::Database)?; Ok(json!({ "name": row.0, "description": row.1, "avatar_url": row.2, "created_at": row.3.to_rfc3339(), "member_count": member_count, })) } } pub struct WorkspaceMembersTool; impl WorkspaceMembersTool { pub fn new() -> Self { Self } } impl Default for WorkspaceMembersTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for WorkspaceMembersTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "workspace_members" } fn description(&self) -> &'static str { "List all members of a workspace with their roles." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" }, "limit": { "type": "integer", "description": "Max results (default 50)" } }, "required": ["workspace"] }) } async fn call( &self, ctx: &mut AppAgentContext, args: Value, ) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let wk_id = require_workspace_member(git, ctx.user_id, workspace).await?; let limit = args .get("limit") .and_then(|v| v.as_i64()) .unwrap_or(50) .min(200); #[derive(sqlx::FromRow)] struct MemberRow { username: String, display_name: Option, owner: bool, admin: bool, } let rows: Vec = sqlx::query_as( "SELECT u.username, u.display_name, m.owner, m.admin \ FROM wk_member m \ INNER JOIN \"user\" u ON u.id = m.\"user\" \ WHERE m.wk = $1 AND m.leave_at IS NULL \ ORDER BY m.owner DESC, m.admin DESC \ LIMIT $2", ) .bind(wk_id) .bind(limit) .fetch_all(git.db.reader()) .await .map_err(AiError::Database)?; let members: Vec = rows.iter().map(|r| json!({ "username": r.username, "display_name": r.display_name, "role": if r.owner { "owner" } else if r.admin { "admin" } else { "member" }, })).collect(); Ok(json!({ "members": members, "count": members.len() })) } } pub struct WorkspaceGroupsTool; impl WorkspaceGroupsTool { pub fn new() -> Self { Self } } impl Default for WorkspaceGroupsTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for WorkspaceGroupsTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "workspace_groups" } fn description(&self) -> &'static str { "List all user groups in a workspace." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" } }, "required": ["workspace"] }) } async fn call( &self, ctx: &mut AppAgentContext, args: Value, ) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let wk_id = require_workspace_member(git, ctx.user_id, workspace).await?; #[derive(sqlx::FromRow)] struct GroupRow { id: Uuid, name: String, created_at: chrono::DateTime, } let rows: Vec = sqlx::query_as( "SELECT id, name, created_at FROM wk_group \ WHERE wk = $1 AND is_deleted = FALSE ORDER BY name ASC", ) .bind(wk_id) .fetch_all(git.db.reader()) .await .map_err(AiError::Database)?; // For each group, count members let mut groups: Vec = Vec::new(); for row in &rows { let count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM wk_gp_member WHERE gp = $1", ) .bind(row.id) .fetch_one(git.db.reader()) .await .map_err(AiError::Database)?; groups.push(json!({ "name": row.name, "member_count": count, "created_at": row.created_at.to_rfc3339(), })); } Ok(json!({ "groups": groups, "count": groups.len() })) } } pub struct WorkspaceGroupMembersTool; impl WorkspaceGroupMembersTool { pub fn new() -> Self { Self } } impl Default for WorkspaceGroupMembersTool { fn default() -> Self { Self::new() } } #[async_trait] impl FunctionCall for WorkspaceGroupMembersTool { type Context = AppAgentContext; fn name(&self) -> &'static str { "workspace_group_members" } fn description(&self) -> &'static str { "List members of a specific workspace group." } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "workspace": { "type": "string", "description": "Workspace name" }, "group_name": { "type": "string", "description": "Group name" } }, "required": ["workspace", "group_name"] }) } async fn call( &self, ctx: &mut AppAgentContext, args: Value, ) -> AiResult { let git = git_ctx(ctx)?; let workspace = arg_str(&args, "workspace")?; let group_name = arg_str(&args, "group_name")?; let wk_id = require_workspace_member(git, ctx.user_id, workspace).await?; #[derive(sqlx::FromRow)] struct MemberRow { username: String, display_name: Option, } let rows: Vec = sqlx::query_as( "SELECT u.username, u.display_name \ FROM wk_gp_member gm \ INNER JOIN wk_group g ON g.id = gm.gp \ INNER JOIN \"user\" u ON u.id = gm.\"user\" \ WHERE g.wk = $1 AND g.name = $2 AND g.is_deleted = FALSE", ) .bind(wk_id) .bind(group_name) .fetch_all(git.db.reader()) .await .map_err(AiError::Database)?; let members: Vec = rows .iter() .map(|r| { json!({ "username": r.username, "display_name": r.display_name, }) }) .collect(); Ok( json!({ "group": group_name, "members": members, "count": members.len() }), ) } }