mojentic 0.5.4__py3-none-any.whl → 0.5.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _examples/broker_examples.py +12 -22
- _examples/broker_image_examples.py +41 -0
- _examples/ephemeral_task_manager_example.py +48 -0
- _examples/streaming.py +1 -1
- _examples/tell_user_example.py +43 -0
- mojentic/llm/gateways/models.py +29 -1
- mojentic/llm/llm_broker_spec.py +0 -49
- mojentic/llm/message_composers_spec.py +0 -80
- mojentic/llm/tools/ask_user_tool.py +1 -1
- mojentic/llm/tools/ephemeral_task_manager/__init__.py +27 -0
- mojentic/llm/tools/ephemeral_task_manager/append_task_tool.py +77 -0
- mojentic/llm/tools/ephemeral_task_manager/append_task_tool_spec.py +34 -0
- mojentic/llm/tools/ephemeral_task_manager/clear_tasks_tool.py +57 -0
- mojentic/llm/tools/ephemeral_task_manager/clear_tasks_tool_spec.py +32 -0
- mojentic/llm/tools/ephemeral_task_manager/complete_task_tool.py +81 -0
- mojentic/llm/tools/ephemeral_task_manager/complete_task_tool_spec.py +43 -0
- mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list.py +202 -0
- mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list_spec.py +137 -0
- mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool.py +84 -0
- mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool_spec.py +42 -0
- mojentic/llm/tools/ephemeral_task_manager/list_tasks_tool.py +80 -0
- mojentic/llm/tools/ephemeral_task_manager/list_tasks_tool_spec.py +38 -0
- mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool.py +77 -0
- mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool_spec.py +34 -0
- mojentic/llm/tools/ephemeral_task_manager/start_task_tool.py +81 -0
- mojentic/llm/tools/ephemeral_task_manager/start_task_tool_spec.py +43 -0
- mojentic/llm/tools/llm_tool.py +15 -0
- mojentic/llm/tools/llm_tool_spec.py +68 -0
- mojentic/llm/tools/organic_web_search.py +37 -0
- mojentic/llm/tools/tell_user_tool.py +27 -0
- {mojentic-0.5.4.dist-info → mojentic-0.5.6.dist-info}/METADATA +1 -1
- {mojentic-0.5.4.dist-info → mojentic-0.5.6.dist-info}/RECORD +35 -13
- {mojentic-0.5.4.dist-info → mojentic-0.5.6.dist-info}/WHEEL +1 -1
- mojentic/llm/tools/web_search.py +0 -35
- {mojentic-0.5.4.dist-info → mojentic-0.5.6.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.5.4.dist-info → mojentic-0.5.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from mojentic.llm.tools.ephemeral_task_manager.append_task_tool import AppendTaskTool
|
|
6
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, Task, TaskStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_task_list():
|
|
11
|
+
mock = Mock(spec=EphemeralTaskList)
|
|
12
|
+
return mock
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def append_task_tool(mock_task_list):
|
|
17
|
+
return AppendTaskTool(task_list=mock_task_list)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DescribeAppendTaskTool:
|
|
21
|
+
def should_call_append_task_with_description(self, append_task_tool, mock_task_list):
|
|
22
|
+
mock_task = Task(id=1, description="Test task", status=TaskStatus.PENDING)
|
|
23
|
+
mock_task_list.append_task.return_value = mock_task
|
|
24
|
+
|
|
25
|
+
append_task_tool.run(description="Test task")
|
|
26
|
+
|
|
27
|
+
mock_task_list.append_task.assert_called_once_with(description="Test task")
|
|
28
|
+
|
|
29
|
+
def should_handle_error_when_append_task_fails(self, append_task_tool, mock_task_list):
|
|
30
|
+
mock_task_list.append_task.side_effect = ValueError("Test error")
|
|
31
|
+
|
|
32
|
+
append_task_tool.run(description="Test task")
|
|
33
|
+
|
|
34
|
+
mock_task_list.append_task.assert_called_once_with(description="Test task")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool for clearing all tasks from the ephemeral task manager.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from mojentic.llm.tools.llm_tool import LLMTool
|
|
8
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClearTasksTool(LLMTool):
|
|
12
|
+
"""
|
|
13
|
+
Tool for clearing all tasks from the ephemeral task manager.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, task_list: EphemeralTaskList):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the tool with a shared task list.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
task_list: The shared task list to use
|
|
22
|
+
"""
|
|
23
|
+
self._task_list = task_list
|
|
24
|
+
|
|
25
|
+
def run(self) -> Dict[str, str]:
|
|
26
|
+
"""
|
|
27
|
+
Remove all tasks from the list.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A dictionary with the result of the operation
|
|
31
|
+
"""
|
|
32
|
+
count = self._task_list.clear_tasks()
|
|
33
|
+
return {
|
|
34
|
+
"count": str(count),
|
|
35
|
+
"summary": f"Cleared {count} tasks from the list"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def descriptor(self):
|
|
40
|
+
"""
|
|
41
|
+
Get the descriptor for the tool.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The descriptor dictionary
|
|
45
|
+
"""
|
|
46
|
+
return {
|
|
47
|
+
"type": "function",
|
|
48
|
+
"function": {
|
|
49
|
+
"name": "clear_tasks",
|
|
50
|
+
"description": "Remove all tasks from the task list.",
|
|
51
|
+
"parameters": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {},
|
|
54
|
+
"additionalProperties": False
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
from mojentic.llm.tools.ephemeral_task_manager.clear_tasks_tool import ClearTasksTool
|
|
5
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def mock_task_list():
|
|
10
|
+
mock = Mock(spec=EphemeralTaskList)
|
|
11
|
+
return mock
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def clear_tasks_tool(mock_task_list):
|
|
16
|
+
return ClearTasksTool(task_list=mock_task_list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DescribeClearTasksTool:
|
|
20
|
+
def should_call_clear_tasks(self, clear_tasks_tool, mock_task_list):
|
|
21
|
+
mock_task_list.clear_tasks.return_value = 3 # Simulate clearing 3 tasks
|
|
22
|
+
|
|
23
|
+
clear_tasks_tool.run()
|
|
24
|
+
|
|
25
|
+
mock_task_list.clear_tasks.assert_called_once()
|
|
26
|
+
|
|
27
|
+
def should_handle_empty_list(self, clear_tasks_tool, mock_task_list):
|
|
28
|
+
mock_task_list.clear_tasks.return_value = 0 # Simulate clearing 0 tasks
|
|
29
|
+
|
|
30
|
+
clear_tasks_tool.run()
|
|
31
|
+
|
|
32
|
+
mock_task_list.clear_tasks.assert_called_once()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool for completing a task in the ephemeral task manager.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from mojentic.llm.tools.llm_tool import LLMTool
|
|
8
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CompleteTaskTool(LLMTool):
|
|
12
|
+
"""
|
|
13
|
+
Tool for completing a task in the ephemeral task manager.
|
|
14
|
+
|
|
15
|
+
This tool changes a task's status from IN_PROGRESS to COMPLETED.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, task_list: EphemeralTaskList):
|
|
19
|
+
"""
|
|
20
|
+
Initialize the tool with a shared task list.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
task_list: The shared task list to use
|
|
24
|
+
"""
|
|
25
|
+
self._task_list = task_list
|
|
26
|
+
|
|
27
|
+
def run(self, id: int) -> Dict[str, str]:
|
|
28
|
+
"""
|
|
29
|
+
Complete a task by changing its status from IN_PROGRESS to COMPLETED.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
id: The ID of the task to complete
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A dictionary with the result of the operation
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: If no task with the given ID exists or if the task is not in IN_PROGRESS status
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
# Convert id to int if it's a string
|
|
42
|
+
task_id = int(id) if isinstance(id, str) else id
|
|
43
|
+
task = self._task_list.complete_task(id=task_id)
|
|
44
|
+
return {
|
|
45
|
+
"id": task.id,
|
|
46
|
+
"description": task.description,
|
|
47
|
+
"status": task.status.value,
|
|
48
|
+
"summary": f"Task '{id}' completed successfully"
|
|
49
|
+
}
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
return {
|
|
52
|
+
"error": str(e),
|
|
53
|
+
"summary": f"Failed to complete task: {str(e)}"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def descriptor(self):
|
|
58
|
+
"""
|
|
59
|
+
Get the descriptor for the tool.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The descriptor dictionary
|
|
63
|
+
"""
|
|
64
|
+
return {
|
|
65
|
+
"type": "function",
|
|
66
|
+
"function": {
|
|
67
|
+
"name": "complete_task",
|
|
68
|
+
"description": "Complete a task by changing its status from IN_PROGRESS to COMPLETED.",
|
|
69
|
+
"parameters": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"id": {
|
|
73
|
+
"type": "integer",
|
|
74
|
+
"description": "The ID of the task to complete"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"required": ["id"],
|
|
78
|
+
"additionalProperties": False
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from mojentic.llm.tools.ephemeral_task_manager.complete_task_tool import CompleteTaskTool
|
|
6
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, Task, TaskStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_task_list():
|
|
11
|
+
mock = Mock(spec=EphemeralTaskList)
|
|
12
|
+
return mock
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def complete_task_tool(mock_task_list):
|
|
17
|
+
return CompleteTaskTool(task_list=mock_task_list)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DescribeCompleteTaskTool:
|
|
21
|
+
def should_call_complete_task_with_correct_id(self, complete_task_tool, mock_task_list):
|
|
22
|
+
mock_task = Task(id=1, description="Test task", status=TaskStatus.COMPLETED)
|
|
23
|
+
mock_task_list.complete_task.return_value = mock_task
|
|
24
|
+
|
|
25
|
+
complete_task_tool.run(id=1)
|
|
26
|
+
|
|
27
|
+
mock_task_list.complete_task.assert_called_once_with(id=1)
|
|
28
|
+
|
|
29
|
+
def should_convert_string_id_to_int(self, complete_task_tool, mock_task_list):
|
|
30
|
+
mock_task = Task(id=1, description="Test task", status=TaskStatus.COMPLETED)
|
|
31
|
+
mock_task_list.complete_task.return_value = mock_task
|
|
32
|
+
|
|
33
|
+
complete_task_tool.run(id="1")
|
|
34
|
+
|
|
35
|
+
mock_task_list.complete_task.assert_called_once_with(id=1)
|
|
36
|
+
|
|
37
|
+
def should_handle_error_when_complete_task_fails(self, complete_task_tool, mock_task_list):
|
|
38
|
+
error_message = "Task '1' cannot be completed because it is not in IN_PROGRESS status"
|
|
39
|
+
mock_task_list.complete_task.side_effect = ValueError(error_message)
|
|
40
|
+
|
|
41
|
+
complete_task_tool.run(id=1)
|
|
42
|
+
|
|
43
|
+
mock_task_list.complete_task.assert_called_once_with(id=1)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task list for the ephemeral task manager.
|
|
3
|
+
|
|
4
|
+
This module provides a class for managing a list of tasks with state transitions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskStatus(str, Enum):
|
|
13
|
+
"""
|
|
14
|
+
Enumeration of possible task statuses.
|
|
15
|
+
|
|
16
|
+
Tasks follow a state machine that transitions from PENDING through IN_PROGRESS to COMPLETED.
|
|
17
|
+
"""
|
|
18
|
+
PENDING = "pending"
|
|
19
|
+
IN_PROGRESS = "in_progress"
|
|
20
|
+
COMPLETED = "completed"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Task(BaseModel):
|
|
24
|
+
"""
|
|
25
|
+
Represents a task with an identifier, description, and status.
|
|
26
|
+
"""
|
|
27
|
+
id: int
|
|
28
|
+
description: str
|
|
29
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EphemeralTaskList:
|
|
33
|
+
"""
|
|
34
|
+
Manages a list of tasks for the ephemeral task manager.
|
|
35
|
+
|
|
36
|
+
This class provides methods for adding, starting, completing, and listing tasks.
|
|
37
|
+
Tasks follow a state machine that transitions from PENDING through IN_PROGRESS to COMPLETED.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
"""
|
|
42
|
+
Initialize an empty task list and ID counter.
|
|
43
|
+
"""
|
|
44
|
+
self._tasks: List[Task] = []
|
|
45
|
+
self._next_id: int = 1
|
|
46
|
+
|
|
47
|
+
def _claim_next_id(self) -> int:
|
|
48
|
+
"""
|
|
49
|
+
Claim the next available ID and increment the counter.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The claimed ID
|
|
53
|
+
"""
|
|
54
|
+
id = self._next_id
|
|
55
|
+
self._next_id += 1
|
|
56
|
+
return id
|
|
57
|
+
|
|
58
|
+
def append_task(self, description: str) -> Task:
|
|
59
|
+
"""
|
|
60
|
+
Add a new task to the end of the list.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
description: The description of the task
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The newly created task with PENDING status
|
|
67
|
+
"""
|
|
68
|
+
# Generate a new ID
|
|
69
|
+
id = self._claim_next_id()
|
|
70
|
+
|
|
71
|
+
task = Task(id=id, description=description, status=TaskStatus.PENDING)
|
|
72
|
+
self._tasks.append(task)
|
|
73
|
+
return task
|
|
74
|
+
|
|
75
|
+
def prepend_task(self, description: str) -> Task:
|
|
76
|
+
"""
|
|
77
|
+
Add a new task to the beginning of the list.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
description: The description of the task
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The newly created task with PENDING status
|
|
84
|
+
"""
|
|
85
|
+
# Generate a new ID
|
|
86
|
+
id = self._claim_next_id()
|
|
87
|
+
|
|
88
|
+
task = Task(id=id, description=description, status=TaskStatus.PENDING)
|
|
89
|
+
self._tasks.insert(0, task)
|
|
90
|
+
return task
|
|
91
|
+
|
|
92
|
+
def insert_task_after(self, existing_task_id: int, description: str) -> Task:
|
|
93
|
+
"""
|
|
94
|
+
Insert a new task after an existing task.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
existing_task_id: The ID of the existing task after which to insert the new task
|
|
98
|
+
description: The description of the new task
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The newly created task with PENDING status
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If no task with the given ID exists
|
|
105
|
+
"""
|
|
106
|
+
# Find the position of the existing task
|
|
107
|
+
position = None
|
|
108
|
+
for i, task in enumerate(self._tasks):
|
|
109
|
+
if task.id == existing_task_id:
|
|
110
|
+
position = i
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
if position is None:
|
|
114
|
+
raise ValueError(f"No task with ID '{existing_task_id}' exists")
|
|
115
|
+
|
|
116
|
+
# Generate a new ID
|
|
117
|
+
id = self._claim_next_id()
|
|
118
|
+
|
|
119
|
+
task = Task(id=id, description=description, status=TaskStatus.PENDING)
|
|
120
|
+
self._tasks.insert(position + 1, task)
|
|
121
|
+
return task
|
|
122
|
+
|
|
123
|
+
def start_task(self, id: int) -> Task:
|
|
124
|
+
"""
|
|
125
|
+
Start a task by changing its status from PENDING to IN_PROGRESS.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
id: The ID of the task to start
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The started task
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If no task with the given ID exists or if the task is not in PENDING status
|
|
135
|
+
"""
|
|
136
|
+
task = self._get_task(id)
|
|
137
|
+
|
|
138
|
+
if task.status != TaskStatus.PENDING:
|
|
139
|
+
raise ValueError(f"Task '{id}' cannot be started because it is not in PENDING status")
|
|
140
|
+
|
|
141
|
+
task.status = TaskStatus.IN_PROGRESS
|
|
142
|
+
return task
|
|
143
|
+
|
|
144
|
+
def complete_task(self, id: int) -> Task:
|
|
145
|
+
"""
|
|
146
|
+
Complete a task by changing its status from IN_PROGRESS to COMPLETED.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
id: The ID of the task to complete
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The completed task
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If no task with the given ID exists or if the task is not in IN_PROGRESS status
|
|
156
|
+
"""
|
|
157
|
+
task = self._get_task(id)
|
|
158
|
+
|
|
159
|
+
if task.status != TaskStatus.IN_PROGRESS:
|
|
160
|
+
raise ValueError(f"Task '{id}' cannot be completed because it is not in IN_PROGRESS status")
|
|
161
|
+
|
|
162
|
+
task.status = TaskStatus.COMPLETED
|
|
163
|
+
return task
|
|
164
|
+
|
|
165
|
+
def list_tasks(self) -> List[Task]:
|
|
166
|
+
"""
|
|
167
|
+
Get all tasks in the list.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A list of all tasks
|
|
171
|
+
"""
|
|
172
|
+
return self._tasks.copy()
|
|
173
|
+
|
|
174
|
+
def clear_tasks(self) -> int:
|
|
175
|
+
"""
|
|
176
|
+
Remove all tasks from the list.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The number of tasks that were cleared
|
|
180
|
+
"""
|
|
181
|
+
count = len(self._tasks)
|
|
182
|
+
self._tasks = []
|
|
183
|
+
return count
|
|
184
|
+
|
|
185
|
+
def _get_task(self, id: int) -> Task:
|
|
186
|
+
"""
|
|
187
|
+
Get a task by its ID.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
id: The ID of the task to get
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The task with the given ID
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: If no task with the given ID exists
|
|
197
|
+
"""
|
|
198
|
+
for task in self._tasks:
|
|
199
|
+
if task.id == id:
|
|
200
|
+
return task
|
|
201
|
+
|
|
202
|
+
raise ValueError(f"No task with ID '{id}' exists")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, TaskStatus
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def task_list():
|
|
8
|
+
return EphemeralTaskList()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def populated_task_list():
|
|
13
|
+
task_list = EphemeralTaskList()
|
|
14
|
+
task_list.append_task("Task 1")
|
|
15
|
+
task_list.append_task("Task 2")
|
|
16
|
+
task_list.append_task("Task 3")
|
|
17
|
+
return task_list
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DescribeEphemeralTaskList:
|
|
21
|
+
|
|
22
|
+
def should_initialize_with_empty_task_list(self, task_list):
|
|
23
|
+
tasks = task_list.list_tasks()
|
|
24
|
+
assert len(tasks) == 0
|
|
25
|
+
|
|
26
|
+
def should_append_task(self, task_list):
|
|
27
|
+
task = task_list.append_task("Test task")
|
|
28
|
+
|
|
29
|
+
tasks = task_list.list_tasks()
|
|
30
|
+
assert len(tasks) == 1
|
|
31
|
+
assert tasks[0].id == task.id
|
|
32
|
+
assert tasks[0].description == "Test task"
|
|
33
|
+
assert tasks[0].status == TaskStatus.PENDING
|
|
34
|
+
|
|
35
|
+
def should_prepend_task(self, task_list):
|
|
36
|
+
task_list.append_task("Existing task")
|
|
37
|
+
task = task_list.prepend_task("New task")
|
|
38
|
+
|
|
39
|
+
tasks = task_list.list_tasks()
|
|
40
|
+
assert len(tasks) == 2
|
|
41
|
+
assert tasks[0].id == task.id
|
|
42
|
+
assert tasks[0].description == "New task"
|
|
43
|
+
assert tasks[0].status == TaskStatus.PENDING
|
|
44
|
+
|
|
45
|
+
def should_insert_task_after(self, populated_task_list):
|
|
46
|
+
tasks_before = populated_task_list.list_tasks()
|
|
47
|
+
existing_task_id = tasks_before[1].id
|
|
48
|
+
|
|
49
|
+
task = populated_task_list.insert_task_after(existing_task_id, "Inserted task")
|
|
50
|
+
|
|
51
|
+
tasks_after = populated_task_list.list_tasks()
|
|
52
|
+
assert len(tasks_after) == 4
|
|
53
|
+
assert tasks_after[2].id == task.id
|
|
54
|
+
assert tasks_after[2].description == "Inserted task"
|
|
55
|
+
assert tasks_after[2].status == TaskStatus.PENDING
|
|
56
|
+
|
|
57
|
+
def should_raise_error_when_inserting_after_nonexistent_task(self, task_list):
|
|
58
|
+
with pytest.raises(ValueError) as e:
|
|
59
|
+
task_list.insert_task_after(999, "This should fail")
|
|
60
|
+
|
|
61
|
+
assert "No task with ID '999' exists" in str(e.value)
|
|
62
|
+
|
|
63
|
+
def should_start_task(self, populated_task_list):
|
|
64
|
+
tasks = populated_task_list.list_tasks()
|
|
65
|
+
task_id = tasks[0].id
|
|
66
|
+
|
|
67
|
+
started_task = populated_task_list.start_task(task_id)
|
|
68
|
+
|
|
69
|
+
assert started_task.status == TaskStatus.IN_PROGRESS
|
|
70
|
+
|
|
71
|
+
def should_raise_error_when_starting_non_pending_task(self, populated_task_list):
|
|
72
|
+
tasks = populated_task_list.list_tasks()
|
|
73
|
+
task_id = tasks[0].id
|
|
74
|
+
|
|
75
|
+
populated_task_list.start_task(task_id)
|
|
76
|
+
|
|
77
|
+
# Second start should fail
|
|
78
|
+
with pytest.raises(ValueError) as e:
|
|
79
|
+
populated_task_list.start_task(task_id)
|
|
80
|
+
|
|
81
|
+
assert f"Task '{task_id}' cannot be started because it is not in PENDING status" in str(e.value)
|
|
82
|
+
|
|
83
|
+
def should_complete_task(self, populated_task_list):
|
|
84
|
+
tasks = populated_task_list.list_tasks()
|
|
85
|
+
task_id = tasks[0].id
|
|
86
|
+
|
|
87
|
+
# Start the task first
|
|
88
|
+
populated_task_list.start_task(task_id)
|
|
89
|
+
|
|
90
|
+
# Now complete it
|
|
91
|
+
completed_task = populated_task_list.complete_task(task_id)
|
|
92
|
+
|
|
93
|
+
assert completed_task.status == TaskStatus.COMPLETED
|
|
94
|
+
|
|
95
|
+
def should_raise_error_when_completing_non_in_progress_task(self, populated_task_list):
|
|
96
|
+
tasks = populated_task_list.list_tasks()
|
|
97
|
+
task_id = tasks[0].id
|
|
98
|
+
|
|
99
|
+
with pytest.raises(ValueError) as excinfo:
|
|
100
|
+
populated_task_list.complete_task(task_id)
|
|
101
|
+
|
|
102
|
+
assert f"Task '{task_id}' cannot be completed because it is not in IN_PROGRESS status" in str(excinfo.value)
|
|
103
|
+
|
|
104
|
+
def should_clear_tasks(self, populated_task_list):
|
|
105
|
+
populated_task_list.clear_tasks()
|
|
106
|
+
|
|
107
|
+
tasks_after = populated_task_list.list_tasks()
|
|
108
|
+
assert len(tasks_after) == 0
|
|
109
|
+
|
|
110
|
+
def should_maintain_task_ids_across_operations(self, task_list):
|
|
111
|
+
# Add some tasks
|
|
112
|
+
task1 = task_list.append_task("Task 1")
|
|
113
|
+
task2 = task_list.append_task("Task 2")
|
|
114
|
+
|
|
115
|
+
# Start and complete task1
|
|
116
|
+
task_list.start_task(task1.id)
|
|
117
|
+
task_list.complete_task(task1.id)
|
|
118
|
+
|
|
119
|
+
# Add another task
|
|
120
|
+
task3 = task_list.append_task("Task 3")
|
|
121
|
+
|
|
122
|
+
# Verify all tasks have correct IDs and statuses
|
|
123
|
+
tasks = task_list.list_tasks()
|
|
124
|
+
assert len(tasks) == 3
|
|
125
|
+
|
|
126
|
+
# Find tasks by ID
|
|
127
|
+
task1_in_list = next((t for t in tasks if t.id == task1.id), None)
|
|
128
|
+
task2_in_list = next((t for t in tasks if t.id == task2.id), None)
|
|
129
|
+
task3_in_list = next((t for t in tasks if t.id == task3.id), None)
|
|
130
|
+
|
|
131
|
+
assert task1_in_list is not None
|
|
132
|
+
assert task2_in_list is not None
|
|
133
|
+
assert task3_in_list is not None
|
|
134
|
+
|
|
135
|
+
assert task1_in_list.status == TaskStatus.COMPLETED
|
|
136
|
+
assert task2_in_list.status == TaskStatus.PENDING
|
|
137
|
+
assert task3_in_list.status == TaskStatus.PENDING
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool for inserting a new task after an existing task in the ephemeral task manager list.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from mojentic.llm.tools.llm_tool import LLMTool
|
|
8
|
+
from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InsertTaskAfterTool(LLMTool):
|
|
12
|
+
"""
|
|
13
|
+
Tool for inserting a new task after an existing task in the ephemeral task manager list.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, task_list: EphemeralTaskList):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the tool with a shared task list.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
task_list: The shared task list to use
|
|
22
|
+
"""
|
|
23
|
+
self._task_list = task_list
|
|
24
|
+
|
|
25
|
+
def run(self, existing_task_id: int, description: str) -> Dict[str, str]:
|
|
26
|
+
"""
|
|
27
|
+
Insert a new task after an existing task.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
existing_task_id: The ID of the existing task after which to insert the new task
|
|
31
|
+
description: The description of the new task
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary with the result of the operation
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If no task with the given ID exists
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Convert existing_task_id to int if it's a string
|
|
41
|
+
task_id = int(existing_task_id) if isinstance(existing_task_id, str) else existing_task_id
|
|
42
|
+
task = self._task_list.insert_task_after(existing_task_id=task_id, description=description)
|
|
43
|
+
return {
|
|
44
|
+
"id": task.id,
|
|
45
|
+
"description": task.description,
|
|
46
|
+
"status": task.status.value,
|
|
47
|
+
"summary": f"Task '{task.id}' inserted after task '{existing_task_id}' successfully"
|
|
48
|
+
}
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
return {
|
|
51
|
+
"error": str(e),
|
|
52
|
+
"summary": f"Failed to insert task: {str(e)}"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def descriptor(self):
|
|
57
|
+
"""
|
|
58
|
+
Get the descriptor for the tool.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The descriptor dictionary
|
|
62
|
+
"""
|
|
63
|
+
return {
|
|
64
|
+
"type": "function",
|
|
65
|
+
"function": {
|
|
66
|
+
"name": "insert_task_after",
|
|
67
|
+
"description": "Insert a new task after an existing task in the task list. The task will start with 'pending' status.",
|
|
68
|
+
"parameters": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"existing_task_id": {
|
|
72
|
+
"type": "integer",
|
|
73
|
+
"description": "The ID of the existing task after which to insert the new task"
|
|
74
|
+
},
|
|
75
|
+
"description": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "The description of the new task"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"required": ["existing_task_id", "description"],
|
|
81
|
+
"additionalProperties": False
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|