1use anyhow::Result;
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::sync::{Arc, Mutex};
12
13use super::common::parameters_schema;
14
15crate::define_tool_error!(WorkspaceError);
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Workspace {
21 #[serde(skip)]
22 data: Arc<Mutex<WorkspaceData>>,
23}
24
25#[derive(Debug, Default)]
26struct WorkspaceData {
27 notes: Vec<String>,
28 tasks: Vec<WorkspaceTask>,
29}
30
31#[derive(Debug, Clone)]
32struct WorkspaceTask {
33 description: String,
34 status: TaskStatus,
35 priority: TaskPriority,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
40#[serde(rename_all = "snake_case")]
41pub enum WorkspaceAction {
42 AddNote,
44 AddTask,
46 UpdateTask,
48 #[default]
50 GetSummary,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
55#[serde(rename_all = "lowercase")]
56pub enum TaskPriority {
57 Low,
58 #[default]
59 Medium,
60 High,
61 Critical,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum TaskStatus {
68 #[default]
69 Pending,
70 InProgress,
71 Completed,
72 Blocked,
73}
74
75impl std::fmt::Display for TaskStatus {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 TaskStatus::Pending => write!(f, "pending"),
79 TaskStatus::InProgress => write!(f, "in_progress"),
80 TaskStatus::Completed => write!(f, "completed"),
81 TaskStatus::Blocked => write!(f, "blocked"),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
87pub struct WorkspaceArgs {
88 pub action: WorkspaceAction,
90 #[serde(default)]
92 pub content: Option<String>,
93 #[serde(default)]
95 pub priority: Option<TaskPriority>,
96 #[serde(default)]
98 pub task_index: Option<usize>,
99 #[serde(default)]
101 pub status: Option<TaskStatus>,
102}
103
104impl Default for Workspace {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl Workspace {
111 #[must_use]
112 pub fn new() -> Self {
113 Self {
114 data: Arc::new(Mutex::new(WorkspaceData::default())),
115 }
116 }
117}
118
119impl Tool for Workspace {
120 const NAME: &'static str = "workspace";
121 type Error = WorkspaceError;
122 type Args = WorkspaceArgs;
123 type Output = String;
124
125 async fn definition(&self, _: String) -> ToolDefinition {
126 ToolDefinition {
127 name: "workspace".to_string(),
128 description: "Iris's personal workspace for notes and task management. Use this to track progress, take notes on findings, and manage complex workflows.".to_string(),
129 parameters: parameters_schema::<WorkspaceArgs>(),
130 }
131 }
132
133 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
134 let mut data = self
135 .data
136 .lock()
137 .map_err(|e| WorkspaceError(e.to_string()))?;
138
139 let result = match args.action {
140 WorkspaceAction::AddNote => {
141 let content = args
142 .content
143 .ok_or_else(|| WorkspaceError("Content required for add_note".to_string()))?;
144 data.notes.push(content);
145 json!({
146 "success": true,
147 "message": "Note added successfully",
148 "note_count": data.notes.len()
149 })
150 }
151 WorkspaceAction::AddTask => {
152 let content = args
153 .content
154 .ok_or_else(|| WorkspaceError("Content required for add_task".to_string()))?;
155 let priority = args.priority.unwrap_or_default();
156
157 data.tasks.push(WorkspaceTask {
158 description: content,
159 status: TaskStatus::Pending,
160 priority,
161 });
162
163 json!({
164 "success": true,
165 "message": "Task added successfully",
166 "task_count": data.tasks.len()
167 })
168 }
169 WorkspaceAction::UpdateTask => {
170 let task_index = args.task_index.ok_or_else(|| {
171 WorkspaceError("task_index required for update_task".to_string())
172 })?;
173 let status = args
174 .status
175 .ok_or_else(|| WorkspaceError("status required for update_task".to_string()))?;
176
177 if task_index >= data.tasks.len() {
178 return Err(WorkspaceError(format!(
179 "Task index {task_index} out of range"
180 )));
181 }
182
183 data.tasks[task_index].status = status.clone();
184
185 json!({
186 "success": true,
187 "message": format!("Task {} updated to {}", task_index, status),
188 })
189 }
190 WorkspaceAction::GetSummary => {
191 let pending = data
192 .tasks
193 .iter()
194 .filter(|t| matches!(t.status, TaskStatus::Pending))
195 .count();
196 let in_progress = data
197 .tasks
198 .iter()
199 .filter(|t| matches!(t.status, TaskStatus::InProgress))
200 .count();
201 let completed = data
202 .tasks
203 .iter()
204 .filter(|t| matches!(t.status, TaskStatus::Completed))
205 .count();
206
207 json!({
208 "notes_count": data.notes.len(),
209 "tasks": {
210 "total": data.tasks.len(),
211 "pending": pending,
212 "in_progress": in_progress,
213 "completed": completed
214 },
215 "recent_notes": data.notes.iter().rev().take(3).collect::<Vec<_>>(),
216 "active_tasks": data.tasks.iter()
217 .filter(|t| !matches!(t.status, TaskStatus::Completed))
218 .map(|t| json!({
219 "description": t.description,
220 "status": t.status.to_string(),
221 "priority": format!("{:?}", t.priority).to_lowercase()
222 }))
223 .collect::<Vec<_>>()
224 })
225 }
226 };
227
228 serde_json::to_string_pretty(&result).map_err(|e| WorkspaceError(e.to_string()))
229 }
230}