Skip to content

Commit cf6ede0

Browse files
committed
feat: add support for Tasks in API and integrate UI
1 parent f6d2297 commit cf6ede0

File tree

16 files changed

+384
-79
lines changed

16 files changed

+384
-79
lines changed

examples/chat/chat.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from pydantic import BaseModel, ConfigDict, Field
2828

29+
from ragbits.agents.tools.todo import Task, TaskStatus
2930
from ragbits.chat.interface import ChatInterface
3031
from ragbits.chat.interface.forms import FeedbackConfig, UserSettings
3132
from ragbits.chat.interface.types import ChatContext, ChatResponse, LiveUpdateType
@@ -141,10 +142,25 @@ async def chat(
141142
),
142143
]
143144

145+
parentTask = Task(id="task_id_1", description="Example task with a subtask")
146+
subtaskTask = Task(
147+
id="task_id_2", description="Example subtask", status=TaskStatus.IN_PROGRESS, parent_id="task_id_1"
148+
)
149+
144150
for live_update in example_live_updates:
145151
yield live_update
146152
await asyncio.sleep(2)
147153

154+
yield self.create_todo_item_response(parentTask)
155+
yield self.create_todo_item_response(subtaskTask)
156+
157+
await asyncio.sleep(1)
158+
subtaskTask.status = TaskStatus.COMPLETED
159+
parentTask.status = TaskStatus.COMPLETED
160+
161+
yield self.create_todo_item_response(parentTask)
162+
yield self.create_todo_item_response(subtaskTask)
163+
148164
streaming_result = self.llm.generate_streaming([*history, {"role": "user", "content": message}])
149165
async for chunk in streaming_result:
150166
yield self.create_text_response(chunk)

packages/ragbits-agents/src/ragbits/agents/tools/todo.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
"""Todo list management tool for agents."""
22

33
import uuid
4+
from collections.abc import Callable
45
from dataclasses import dataclass, field
56
from enum import Enum
6-
from typing import Any, Literal, Callable
7+
from typing import Any, Literal
8+
9+
from pydantic import BaseModel
710

811

912
class TaskStatus(str, Enum):
1013
"""Task status options."""
14+
1115
PENDING = "pending"
1216
IN_PROGRESS = "in_progress"
1317
COMPLETED = "completed"
1418

1519

16-
@dataclass
17-
class Task:
20+
class Task(BaseModel):
1821
"""Simple task representation."""
22+
1923
id: str
2024
description: str
2125
status: TaskStatus = TaskStatus.PENDING
2226
order: int = 0
2327
summary: str | None = None
28+
parent_id: str | None = None
2429

2530

2631
@dataclass
2732
class TodoList:
2833
"""Simple todo list for one agent run."""
34+
2935
tasks: list[Task] = field(default_factory=list)
3036
current_index: int = 0
3137

@@ -35,7 +41,7 @@ def get_current_task(self) -> Task | None:
3541
return self.tasks[self.current_index]
3642
return None
3743

38-
def advance_to_next(self):
44+
def advance_to_next(self) -> None:
3945
"""Move to next task."""
4046
self.current_index += 1
4147

@@ -49,18 +55,14 @@ def create_tasks(self, task_descriptions: list[str]) -> dict[str, Any]:
4955
self.current_index = 0
5056

5157
for i, desc in enumerate(task_descriptions):
52-
task = Task(
53-
id=str(uuid.uuid4()),
54-
description=desc.strip(),
55-
order=i
56-
)
58+
task = Task(id=str(uuid.uuid4()), description=desc.strip(), order=i)
5759
self.tasks.append(task)
5860

5961
return {
6062
"action": "create",
6163
"tasks": [{"id": t.id, "description": t.description, "order": t.order} for t in self.tasks],
6264
"total_count": len(self.tasks),
63-
"message": f"Created {len(task_descriptions)} tasks"
65+
"message": f"Created {len(task_descriptions)} tasks",
6466
}
6567

6668
def get_current(self) -> dict[str, Any]:
@@ -71,14 +73,14 @@ def get_current(self) -> dict[str, Any]:
7173
"action": "get_current",
7274
"current_task": None,
7375
"all_completed": True,
74-
"message": "All tasks completed!"
76+
"message": "All tasks completed!",
7577
}
7678

7779
return {
7880
"action": "get_current",
7981
"current_task": {"id": current.id, "description": current.description, "status": current.status.value},
8082
"progress": f"{self.current_index + 1}/{len(self.tasks)}",
81-
"message": f"Current task: {current.description}"
83+
"message": f"Current task: {current.description}",
8284
}
8385

8486
def start_current_task(self) -> dict[str, Any]:
@@ -91,7 +93,7 @@ def start_current_task(self) -> dict[str, Any]:
9193
return {
9294
"action": "start_task",
9395
"task": {"id": current.id, "description": current.description, "status": current.status.value},
94-
"message": f"Started task: {current.description}"
96+
"message": f"Started task: {current.description}",
9597
}
9698

9799
def complete_current_task(self, summary: str) -> dict[str, Any]:
@@ -119,19 +121,15 @@ def complete_current_task(self, summary: str) -> dict[str, Any]:
119121
"next_task": {"id": next_task.id, "description": next_task.description} if next_task else None,
120122
"progress": f"{completed_count}/{len(self.tasks)}",
121123
"all_completed": next_task is None,
122-
"message": f"Completed: {current.description}"
124+
"message": f"Completed: {current.description}",
123125
}
124126

125127
def get_final_summary(self) -> dict[str, Any]:
126128
"""Get comprehensive final summary of all completed work."""
127129
completed_tasks = [t for t in self.tasks if t.status == TaskStatus.COMPLETED]
128130

129131
if not completed_tasks:
130-
return {
131-
"action": "get_final_summary",
132-
"final_summary": "",
133-
"message": "No completed tasks found."
134-
}
132+
return {"action": "get_final_summary", "final_summary": "", "message": "No completed tasks found."}
135133

136134
# Create comprehensive final summary
137135
final_content = []
@@ -147,7 +145,7 @@ def get_final_summary(self) -> dict[str, Any]:
147145
"action": "get_final_summary",
148146
"final_summary": final_summary,
149147
"total_completed": len(completed_tasks),
150-
"message": f"Final summary with {len(completed_tasks)} completed tasks."
148+
"message": f"Final summary with {len(completed_tasks)} completed tasks.",
151149
}
152150

153151

@@ -163,6 +161,7 @@ def create_todo_manager(todo_list: TodoList) -> Callable[..., dict[str, Any]]:
163161
Returns:
164162
A todo_manager function that operates on the provided TodoList
165163
"""
164+
166165
def todo_manager(
167166
action: Literal["create", "get_current", "start_task", "complete_task", "get_final_summary"],
168167
tasks: list[str] | None = None,
@@ -216,4 +215,4 @@ def get_todo_instruction_tpl(task_range: tuple[int, int] = (3, 5)) -> str:
216215
217216
IMPORTANT: Task summaries should be DETAILED and COMPREHENSIVE (3-5 sentences).
218217
Include specific information, recommendations, and actionable details.
219-
"""
218+
"""

packages/ragbits-chat/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
## 1.3.0 (2025-09-11)
6+
- Add todo list component to the UI, add support for todo events in API (#827)
67

78
### Changed
89

packages/ragbits-chat/src/ragbits/chat/interface/_interface.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from collections.abc import AsyncGenerator, Callable
1010
from typing import Any
1111

12+
from ragbits.agents.tools.todo import Task
1213
from ragbits.chat.interface.ui_customization import UICustomization
1314
from ragbits.core.audit.metrics import record_metric
1415
from ragbits.core.audit.metrics.base import MetricType
@@ -251,6 +252,10 @@ def create_usage_response(usage: Usage) -> ChatResponse:
251252
content={model: MessageUsage.from_usage(usage) for model, usage in usage.model_breakdown.items()},
252253
)
253254

255+
@staticmethod
256+
def create_todo_item_response(task: Task) -> ChatResponse:
257+
return ChatResponse(type=ChatResponseType.TODO_ITEM, content=task)
258+
254259
@staticmethod
255260
def _sign_state(state: dict[str, Any]) -> str:
256261
"""

packages/ragbits-chat/src/ragbits/chat/interface/types.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from pydantic import BaseModel, ConfigDict, Field
55

6+
from ragbits.agents.tools.todo import Task
67
from ragbits.chat.auth.types import User
78
from ragbits.chat.interface.forms import UserSettings
89
from ragbits.chat.interface.ui_customization import UICustomization
@@ -122,6 +123,7 @@ class ChatResponseType(str, Enum):
122123
CHUNKED_CONTENT = "chunked_content"
123124
CLEAR_MESSAGE = "clear_message"
124125
USAGE = "usage"
126+
TODO_ITEM = "todo_item"
125127

126128

127129
class ChatContext(BaseModel):
@@ -140,7 +142,16 @@ class ChatResponse(BaseModel):
140142

141143
type: ChatResponseType
142144
content: (
143-
str | Reference | StateUpdate | LiveUpdate | list[str] | Image | dict[str, MessageUsage] | ChunkedContent | None
145+
str
146+
| Reference
147+
| StateUpdate
148+
| LiveUpdate
149+
| list[str]
150+
| Image
151+
| dict[str, MessageUsage]
152+
| ChunkedContent
153+
| None
154+
| Task
144155
)
145156

146157
def as_text(self) -> str | None:
@@ -217,6 +228,12 @@ def as_usage(self) -> dict[str, MessageUsage] | None:
217228
"""
218229
return cast(dict[str, MessageUsage], self.content) if self.type == ChatResponseType.USAGE else None
219230

231+
def as_task(self) -> Task | None:
232+
"""
233+
Return the content as Task if this is an todo_item response, else None.
234+
"""
235+
return cast(Task, self.content) if self.type == ChatResponseType.TODO_ITEM else None
236+
220237

221238
class ChatMessageRequest(BaseModel):
222239
"""Client-side chat request interface."""

packages/ragbits-chat/src/ragbits/chat/providers/model_provider.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from pydantic import BaseModel
1212

13+
from ragbits.agents.tools.todo import Task, TaskStatus
1314
from ragbits.chat.interface.types import AuthType
1415

1516

@@ -82,6 +83,7 @@ def get_models(self) -> dict[str, type[BaseModel | Enum]]:
8283
"FeedbackType": FeedbackType,
8384
"LiveUpdateType": LiveUpdateType,
8485
"MessageRole": MessageRole,
86+
"TaskStatus": TaskStatus,
8587
# Core data models
8688
"ChatContext": ChatContext,
8789
"ChunkedContent": ChunkedContent,
@@ -93,6 +95,7 @@ def get_models(self) -> dict[str, type[BaseModel | Enum]]:
9395
"FeedbackItem": FeedbackItem,
9496
"Image": Image,
9597
"MessageUsage": MessageUsage,
98+
"Task": Task,
9699
# Configuration models
97100
"HeaderCustomization": HeaderCustomization,
98101
"UICustomization": UICustomization,
@@ -151,6 +154,8 @@ def get_categories(self) -> dict[str, list[str]]:
151154
"JWTToken",
152155
"User",
153156
"MessageUsage",
157+
"Task",
158+
"TaskStatus",
154159
],
155160
"configuration": [
156161
"HeaderCustomization",

scripts/generate_typescript_from_json_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ def _generate_chat_response_union_type() -> str:
201201
("ImageChatResponse", "image", "Image"),
202202
("ClearMessageResponse", "clear_message", "never"),
203203
("MessageUsageChatResponse", "usage", "Record<string, MessageUsage>"),
204+
("TodoItemChatResonse", "todo_item", "Task"),
204205
]
205206

206207
internal_response_interfaces = [

typescript/@ragbits/api-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/deepsense-ai/ragbits"
8-
},
8+
},
99
"main": "dist/index.cjs",
1010
"module": "dist/index.js",
1111
"types": "dist/index.d.ts",

typescript/@ragbits/api-client/src/autogen.types.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const ChatResponseType = {
2323
ChunkedContent: 'chunked_content',
2424
ClearMessage: 'clear_message',
2525
Usage: 'usage',
26+
TodoItem: 'todo_item',
2627
} as const
2728

2829
export type ChatResponseType = TypeFrom<typeof ChatResponseType>
@@ -58,6 +59,17 @@ export const MessageRole = {
5859

5960
export type MessageRole = TypeFrom<typeof MessageRole>
6061

62+
/**
63+
* Represents the TaskStatus enum
64+
*/
65+
export const TaskStatus = {
66+
Pending: 'pending',
67+
InProgress: 'in_progress',
68+
Completed: 'completed',
69+
} as const
70+
71+
export type TaskStatus = TypeFrom<typeof TaskStatus>
72+
6173
/**
6274
* Represents the AuthType enum
6375
*/
@@ -170,6 +182,21 @@ export interface MessageUsage {
170182
total_tokens: number
171183
}
172184

185+
/**
186+
* Simple task representation.
187+
*/
188+
export interface Task {
189+
id: string
190+
description: string
191+
/**
192+
* Task status options.
193+
*/
194+
status: 'pending' | 'in_progress' | 'completed'
195+
order: number
196+
summary: string | null
197+
parent_id: string | null
198+
}
199+
173200
/**
174201
* Customization for the header section of the UI.
175202
*/
@@ -465,6 +492,11 @@ export interface MessageUsageChatResponse {
465492
content: Record<string, MessageUsage>
466493
}
467494

495+
export interface TodoItemChatResonse {
496+
type: 'todo_item'
497+
content: Task
498+
}
499+
468500
export interface ChunkedChatResponse {
469501
type: 'chunked_content'
470502
content: ChunkedContent
@@ -484,3 +516,4 @@ export type ChatResponse =
484516
| ImageChatResponse
485517
| ClearMessageResponse
486518
| MessageUsageChatResponse
519+
| TodoItemChatResonse

0 commit comments

Comments
 (0)