Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions examples/agents/todo_tools_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
from ragbits.core.llms import LiteLLM, ToolCall


async def main():
async def main() -> None:
"""Demonstrate the new instance-based todo approach with streaming and logging."""

# Create a dedicated TodoList instance for this agent
my_todo_list = TodoList()
my_todo_manager = create_todo_manager(my_todo_list)
Expand All @@ -32,14 +31,21 @@ async def main():
- Transportation details with times, costs, parking info
- Weather considerations and backup plans
- Safety information and emergency contacts
""" + get_todo_instruction_tpl(task_range=(3, 5)),
"""
+ get_todo_instruction_tpl(task_range=(3, 5)),
tools=[my_todo_manager], # Use the instance-specific todo manager
default_options=AgentOptions(max_turns=30)
default_options=AgentOptions(max_turns=30),
)

query = "Plan a 1-day hiking trip for 2 people in Tatra Mountains, Poland. Focus on scenic routes under 15km, avoiding crowds."
query = (
"Plan a 1-day hiking trip for 2 people in Tatra Mountains, Poland. ",
"Focus on scenic routes under 15km, avoiding crowds.",
)
# query = "How long is hike to Giewont from Kuźnice?"
# query = "Is it difficult to finish Orla Perć? Would you recommend me to go there if I've never been in mountains before?"
# query = (
# "Is it difficult to finish Orla Perć? Would you recommend me ",
# "to go there if I've never been in mountains before?",
# )

stream = my_agent.run_streaming(query)

Expand All @@ -63,9 +69,9 @@ async def main():
for i, task in enumerate(tasks, 1):
print(f" {i}. {task}")

print("\n\n" + "="*50)
print("\n\n" + "=" * 50)
print("🎉 Systematic hiking trip planning completed!")


if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
19 changes: 19 additions & 0 deletions examples/chat/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from pydantic import BaseModel, ConfigDict, Field

from ragbits.agents.tools.todo import Task, TaskStatus
from ragbits.chat.interface import ChatInterface
from ragbits.chat.interface.forms import FeedbackConfig, UserSettings
from ragbits.chat.interface.types import ChatContext, ChatResponse, LiveUpdateType
Expand Down Expand Up @@ -141,10 +142,28 @@ async def chat(
),
]

parentTask = Task(id="task_id_1", description="Example task with a subtask")
subtaskTask = Task(id="task_id_2", description="Example subtask", parent_id="task_id_1")

for live_update in example_live_updates:
yield live_update
await asyncio.sleep(2)

yield self.create_todo_item_response(parentTask)
yield self.create_todo_item_response(subtaskTask)

await asyncio.sleep(2)
parentTask.status = TaskStatus.IN_PROGRESS
yield self.create_todo_item_response(parentTask)
await asyncio.sleep(2)
subtaskTask.status = TaskStatus.IN_PROGRESS
yield self.create_todo_item_response(subtaskTask)
await asyncio.sleep(2)
parentTask.status = TaskStatus.COMPLETED
subtaskTask.status = TaskStatus.COMPLETED
yield self.create_todo_item_response(subtaskTask)
yield self.create_todo_item_response(parentTask)

streaming_result = self.llm.generate_streaming([*history, {"role": "user", "content": message}])
async for chunk in streaming_result:
yield self.create_text_response(chunk)
Expand Down
1 change: 1 addition & 0 deletions packages/ragbits-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG

## Unreleased
- Add support for todo lists generated by agents with examples (#823)

## 1.3.0 (2025-09-11)
### Changed
Expand Down
4 changes: 2 additions & 2 deletions packages/ragbits-agents/src/ragbits/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
AgentRunContext,
ToolCallResult,
)
from ragbits.agents.tools import get_todo_instruction_tpl, create_todo_manager
from ragbits.agents.tools import create_todo_manager, get_todo_instruction_tpl
from ragbits.agents.types import QuestionAnswerAgent, QuestionAnswerPromptInput, QuestionAnswerPromptOutput

__all__ = [
Expand All @@ -21,6 +21,6 @@
"QuestionAnswerPromptInput",
"QuestionAnswerPromptOutput",
"ToolCallResult",
"get_todo_instruction_tpl",
"create_todo_manager",
"get_todo_instruction_tpl",
]
4 changes: 2 additions & 2 deletions packages/ragbits-agents/src/ragbits/agents/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Agent tools for extending functionality."""

from .todo import get_todo_instruction_tpl, create_todo_manager
from .todo import create_todo_manager, get_todo_instruction_tpl

__all__ = ["create_todo_manager", "get_todo_instruction_tpl"]
__all__ = ["create_todo_manager", "get_todo_instruction_tpl"]
41 changes: 20 additions & 21 deletions packages/ragbits-agents/src/ragbits/agents/tools/todo.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
"""Todo list management tool for agents."""

import uuid
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Literal, Callable
from typing import Any, Literal

from pydantic import BaseModel


class TaskStatus(str, Enum):
"""Task status options."""

PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"


@dataclass
class Task:
class Task(BaseModel):
"""Simple task representation."""

id: str
description: str
status: TaskStatus = TaskStatus.PENDING
order: int = 0
summary: str | None = None
parent_id: str | None = None


@dataclass
class TodoList:
"""Simple todo list for one agent run."""

tasks: list[Task] = field(default_factory=list)
current_index: int = 0

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

def advance_to_next(self):
def advance_to_next(self) -> None:
"""Move to next task."""
self.current_index += 1

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

for i, desc in enumerate(task_descriptions):
task = Task(
id=str(uuid.uuid4()),
description=desc.strip(),
order=i
)
task = Task(id=str(uuid.uuid4()), description=desc.strip(), order=i)
self.tasks.append(task)

return {
"action": "create",
"tasks": [{"id": t.id, "description": t.description, "order": t.order} for t in self.tasks],
"total_count": len(self.tasks),
"message": f"Created {len(task_descriptions)} tasks"
"message": f"Created {len(task_descriptions)} tasks",
}

def get_current(self) -> dict[str, Any]:
Expand All @@ -71,14 +73,14 @@ def get_current(self) -> dict[str, Any]:
"action": "get_current",
"current_task": None,
"all_completed": True,
"message": "All tasks completed!"
"message": "All tasks completed!",
}

return {
"action": "get_current",
"current_task": {"id": current.id, "description": current.description, "status": current.status.value},
"progress": f"{self.current_index + 1}/{len(self.tasks)}",
"message": f"Current task: {current.description}"
"message": f"Current task: {current.description}",
}

def start_current_task(self) -> dict[str, Any]:
Expand All @@ -91,7 +93,7 @@ def start_current_task(self) -> dict[str, Any]:
return {
"action": "start_task",
"task": {"id": current.id, "description": current.description, "status": current.status.value},
"message": f"Started task: {current.description}"
"message": f"Started task: {current.description}",
}

def complete_current_task(self, summary: str) -> dict[str, Any]:
Expand Down Expand Up @@ -119,19 +121,15 @@ def complete_current_task(self, summary: str) -> dict[str, Any]:
"next_task": {"id": next_task.id, "description": next_task.description} if next_task else None,
"progress": f"{completed_count}/{len(self.tasks)}",
"all_completed": next_task is None,
"message": f"Completed: {current.description}"
"message": f"Completed: {current.description}",
}

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

if not completed_tasks:
return {
"action": "get_final_summary",
"final_summary": "",
"message": "No completed tasks found."
}
return {"action": "get_final_summary", "final_summary": "", "message": "No completed tasks found."}

# Create comprehensive final summary
final_content = []
Expand All @@ -147,7 +145,7 @@ def get_final_summary(self) -> dict[str, Any]:
"action": "get_final_summary",
"final_summary": final_summary,
"total_completed": len(completed_tasks),
"message": f"Final summary with {len(completed_tasks)} completed tasks."
"message": f"Final summary with {len(completed_tasks)} completed tasks.",
}


Expand All @@ -163,6 +161,7 @@ def create_todo_manager(todo_list: TodoList) -> Callable[..., dict[str, Any]]:
Returns:
A todo_manager function that operates on the provided TodoList
"""

def todo_manager(
action: Literal["create", "get_current", "start_task", "complete_task", "get_final_summary"],
tasks: list[str] | None = None,
Expand Down Expand Up @@ -216,4 +215,4 @@ def get_todo_instruction_tpl(task_range: tuple[int, int] = (3, 5)) -> str:

IMPORTANT: Task summaries should be DETAILED and COMPREHENSIVE (3-5 sentences).
Include specific information, recommendations, and actionable details.
"""
"""
1 change: 1 addition & 0 deletions packages/ragbits-chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG

## Unreleased
- Add todo list component to the UI, add support for todo events in API (#827)

## 1.3.0 (2025-09-11)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from collections.abc import AsyncGenerator, Callable
from typing import Any

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

@staticmethod
def create_todo_item_response(task: Task) -> ChatResponse:
return ChatResponse(type=ChatResponseType.TODO_ITEM, content=task)

@staticmethod
def _sign_state(state: dict[str, Any]) -> str:
"""
Expand Down
19 changes: 18 additions & 1 deletion packages/ragbits-chat/src/ragbits/chat/interface/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from pydantic import BaseModel, ConfigDict, Field

from ragbits.agents.tools.todo import Task
from ragbits.chat.auth.types import User
from ragbits.chat.interface.forms import UserSettings
from ragbits.chat.interface.ui_customization import UICustomization
Expand Down Expand Up @@ -122,6 +123,7 @@ class ChatResponseType(str, Enum):
CHUNKED_CONTENT = "chunked_content"
CLEAR_MESSAGE = "clear_message"
USAGE = "usage"
TODO_ITEM = "todo_item"


class ChatContext(BaseModel):
Expand All @@ -140,7 +142,16 @@ class ChatResponse(BaseModel):

type: ChatResponseType
content: (
str | Reference | StateUpdate | LiveUpdate | list[str] | Image | dict[str, MessageUsage] | ChunkedContent | None
str
| Reference
| StateUpdate
| LiveUpdate
| list[str]
| Image
| dict[str, MessageUsage]
| ChunkedContent
| None
| Task
)

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

def as_task(self) -> Task | None:
"""
Return the content as Task if this is an todo_item response, else None.
"""
return cast(Task, self.content) if self.type == ChatResponseType.TODO_ITEM else None


class ChatMessageRequest(BaseModel):
"""Client-side chat request interface."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from pydantic import BaseModel

from ragbits.agents.tools.todo import Task, TaskStatus
from ragbits.chat.interface.types import AuthType


Expand Down Expand Up @@ -82,6 +83,7 @@ def get_models(self) -> dict[str, type[BaseModel | Enum]]:
"FeedbackType": FeedbackType,
"LiveUpdateType": LiveUpdateType,
"MessageRole": MessageRole,
"TaskStatus": TaskStatus,
# Core data models
"ChatContext": ChatContext,
"ChunkedContent": ChunkedContent,
Expand All @@ -93,6 +95,7 @@ def get_models(self) -> dict[str, type[BaseModel | Enum]]:
"FeedbackItem": FeedbackItem,
"Image": Image,
"MessageUsage": MessageUsage,
"Task": Task,
# Configuration models
"HeaderCustomization": HeaderCustomization,
"UICustomization": UICustomization,
Expand Down Expand Up @@ -151,6 +154,8 @@ def get_categories(self) -> dict[str, list[str]]:
"JWTToken",
"User",
"MessageUsage",
"Task",
"TaskStatus",
],
"configuration": [
"HeaderCustomization",
Expand Down
1 change: 1 addition & 0 deletions scripts/generate_typescript_from_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def _generate_chat_response_union_type() -> str:
("ImageChatResponse", "image", "Image"),
("ClearMessageResponse", "clear_message", "never"),
("MessageUsageChatResponse", "usage", "Record<string, MessageUsage>"),
("TodoItemChatResonse", "todo_item", "Task"),
]

internal_response_interfaces = [
Expand Down
2 changes: 1 addition & 1 deletion typescript/@ragbits/api-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"repository": {
"type": "git",
"url": "https://github.com/deepsense-ai/ragbits"
},
},
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Loading
Loading