mojentic 0.5.3__py3-none-any.whl → 0.5.5__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.
Files changed (37) hide show
  1. _examples/broker_examples.py +12 -22
  2. _examples/broker_image_examples.py +41 -0
  3. _examples/ephemeral_task_manager_example.py +48 -0
  4. _examples/oversized_embeddings.py +9 -0
  5. _examples/streaming.py +1 -1
  6. _examples/tell_user_example.py +43 -0
  7. mojentic/agents/iterative_problem_solver.py +5 -2
  8. mojentic/agents/simple_recursive_agent.py +2 -2
  9. mojentic/llm/gateways/openai.py +29 -6
  10. mojentic/llm/llm_broker_spec.py +0 -49
  11. mojentic/llm/message_composers_spec.py +0 -80
  12. mojentic/llm/tools/ask_user_tool.py +1 -1
  13. mojentic/llm/tools/ephemeral_task_manager/__init__.py +27 -0
  14. mojentic/llm/tools/ephemeral_task_manager/append_task_tool.py +77 -0
  15. mojentic/llm/tools/ephemeral_task_manager/append_task_tool_spec.py +34 -0
  16. mojentic/llm/tools/ephemeral_task_manager/clear_tasks_tool.py +57 -0
  17. mojentic/llm/tools/ephemeral_task_manager/clear_tasks_tool_spec.py +32 -0
  18. mojentic/llm/tools/ephemeral_task_manager/complete_task_tool.py +81 -0
  19. mojentic/llm/tools/ephemeral_task_manager/complete_task_tool_spec.py +43 -0
  20. mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list.py +202 -0
  21. mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list_spec.py +137 -0
  22. mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool.py +84 -0
  23. mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool_spec.py +42 -0
  24. mojentic/llm/tools/ephemeral_task_manager/list_tasks_tool.py +80 -0
  25. mojentic/llm/tools/ephemeral_task_manager/list_tasks_tool_spec.py +38 -0
  26. mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool.py +77 -0
  27. mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool_spec.py +34 -0
  28. mojentic/llm/tools/ephemeral_task_manager/start_task_tool.py +81 -0
  29. mojentic/llm/tools/ephemeral_task_manager/start_task_tool_spec.py +43 -0
  30. mojentic/llm/tools/organic_web_search.py +37 -0
  31. mojentic/llm/tools/tell_user_tool.py +27 -0
  32. {mojentic-0.5.3.dist-info → mojentic-0.5.5.dist-info}/METADATA +2 -1
  33. {mojentic-0.5.3.dist-info → mojentic-0.5.5.dist-info}/RECORD +36 -14
  34. {mojentic-0.5.3.dist-info → mojentic-0.5.5.dist-info}/WHEEL +1 -1
  35. mojentic/llm/tools/web_search.py +0 -35
  36. {mojentic-0.5.3.dist-info → mojentic-0.5.5.dist-info}/licenses/LICENSE.md +0 -0
  37. {mojentic-0.5.3.dist-info → mojentic-0.5.5.dist-info}/top_level.txt +0 -0
@@ -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
+ }
@@ -0,0 +1,42 @@
1
+ from unittest.mock import Mock
2
+
3
+ import pytest
4
+
5
+ from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, Task, TaskStatus
6
+ from mojentic.llm.tools.ephemeral_task_manager.insert_task_after_tool import InsertTaskAfterTool
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 insert_task_after_tool(mock_task_list):
17
+ return InsertTaskAfterTool(task_list=mock_task_list)
18
+
19
+
20
+ class DescribeInsertTaskAfterTool:
21
+ def should_call_insert_task_after_with_correct_parameters(self, insert_task_after_tool, mock_task_list):
22
+ mock_task = Task(id=2, description="Test task", status=TaskStatus.PENDING)
23
+ mock_task_list.insert_task_after.return_value = mock_task
24
+
25
+ insert_task_after_tool.run(existing_task_id=1, description="Test task")
26
+
27
+ mock_task_list.insert_task_after.assert_called_once_with(existing_task_id=1, description="Test task")
28
+
29
+ def should_convert_string_task_id_to_int(self, insert_task_after_tool, mock_task_list):
30
+ mock_task = Task(id=2, description="Test task", status=TaskStatus.PENDING)
31
+ mock_task_list.insert_task_after.return_value = mock_task
32
+
33
+ insert_task_after_tool.run(existing_task_id="1", description="Test task")
34
+
35
+ mock_task_list.insert_task_after.assert_called_once_with(existing_task_id=1, description="Test task")
36
+
37
+ def should_handle_error_when_insert_task_after_fails(self, insert_task_after_tool, mock_task_list):
38
+ mock_task_list.insert_task_after.side_effect = ValueError("No task with ID '999' exists")
39
+
40
+ insert_task_after_tool.run(existing_task_id=999, description="Test task")
41
+
42
+ mock_task_list.insert_task_after.assert_called_once_with(existing_task_id=999, description="Test task")
@@ -0,0 +1,80 @@
1
+ """
2
+ Tool for listing all tasks in the ephemeral task manager.
3
+ """
4
+
5
+ from typing import Dict, List
6
+
7
+ from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, Task
8
+ from mojentic.llm.tools.llm_tool import LLMTool
9
+
10
+
11
+ class ListTasksTool(LLMTool):
12
+ """
13
+ Tool for listing all tasks in 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
+ Get all tasks in the list.
28
+
29
+ Returns:
30
+ A dictionary with the result of the operation
31
+ """
32
+ tasks = self._task_list.list_tasks()
33
+
34
+ # Format tasks as a string for the summary
35
+ task_list_str = self._format_tasks(tasks)
36
+
37
+ return {
38
+ "count": str(len(tasks)),
39
+ "tasks": task_list_str,
40
+ "summary": f"Found {len(tasks)} tasks\n\n{task_list_str}"
41
+ }
42
+
43
+ def _format_tasks(self, tasks: List[Task]) -> str:
44
+ """
45
+ Format a list of tasks as a string.
46
+
47
+ Args:
48
+ tasks: The list of tasks to format
49
+
50
+ Returns:
51
+ A formatted string representation of the tasks
52
+ """
53
+ if not tasks:
54
+ return "No tasks found."
55
+
56
+ result = [f"{task.id}. {task.description} ({task.status.value})"
57
+ for i, task in enumerate(tasks)]
58
+
59
+ return "\n".join(result)
60
+
61
+ @property
62
+ def descriptor(self):
63
+ """
64
+ Get the descriptor for the tool.
65
+
66
+ Returns:
67
+ The descriptor dictionary
68
+ """
69
+ return {
70
+ "type": "function",
71
+ "function": {
72
+ "name": "list_tasks",
73
+ "description": "List all tasks in the task list.",
74
+ "parameters": {
75
+ "type": "object",
76
+ "properties": {},
77
+ "additionalProperties": False
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,38 @@
1
+ from unittest.mock import Mock
2
+
3
+ import pytest
4
+
5
+ from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, Task, TaskStatus
6
+ from mojentic.llm.tools.ephemeral_task_manager.list_tasks_tool import ListTasksTool
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 list_tasks_tool(mock_task_list):
17
+ return ListTasksTool(task_list=mock_task_list)
18
+
19
+
20
+ class DescribeListTasksTool:
21
+ def should_call_list_tasks(self, list_tasks_tool, mock_task_list):
22
+ mock_tasks = [
23
+ Task(id=1, description="Task 1", status=TaskStatus.PENDING),
24
+ Task(id=2, description="Task 2", status=TaskStatus.IN_PROGRESS),
25
+ Task(id=3, description="Task 3", status=TaskStatus.COMPLETED)
26
+ ]
27
+ mock_task_list.list_tasks.return_value = mock_tasks
28
+
29
+ list_tasks_tool.run()
30
+
31
+ mock_task_list.list_tasks.assert_called_once()
32
+
33
+ def should_handle_empty_list(self, list_tasks_tool, mock_task_list):
34
+ mock_task_list.list_tasks.return_value = []
35
+
36
+ list_tasks_tool.run()
37
+
38
+ mock_task_list.list_tasks.assert_called_once()
@@ -0,0 +1,77 @@
1
+ """
2
+ Tool for prepending a new task to the beginning of 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 PrependTaskTool(LLMTool):
12
+ """
13
+ Tool for prepending a new task to the beginning of 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, description: str) -> Dict[str, str]:
26
+ """
27
+ Prepend a new task to the beginning of the list.
28
+
29
+ Args:
30
+ description: The description of the task
31
+
32
+ Returns:
33
+ A dictionary with the result of the operation
34
+
35
+ Raises:
36
+ ValueError: If there's an error adding the task
37
+ """
38
+ try:
39
+ task = self._task_list.prepend_task(description=description)
40
+ return {
41
+ "id": task.id,
42
+ "description": task.description,
43
+ "status": task.status.value,
44
+ "summary": f"Task '{task.id}' prepended successfully"
45
+ }
46
+ except ValueError as e:
47
+ return {
48
+ "error": str(e),
49
+ "summary": f"Failed to prepend task: {str(e)}"
50
+ }
51
+
52
+ @property
53
+ def descriptor(self):
54
+ """
55
+ Get the descriptor for the tool.
56
+
57
+ Returns:
58
+ The descriptor dictionary
59
+ """
60
+ return {
61
+ "type": "function",
62
+ "function": {
63
+ "name": "prepend_task",
64
+ "description": "Prepend a new task to the beginning of the task list with a description. The task will start with 'pending' status.",
65
+ "parameters": {
66
+ "type": "object",
67
+ "properties": {
68
+ "description": {
69
+ "type": "string",
70
+ "description": "The description of the task"
71
+ }
72
+ },
73
+ "required": ["description"],
74
+ "additionalProperties": False
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,34 @@
1
+ from unittest.mock import Mock
2
+
3
+ import pytest
4
+
5
+ from mojentic.llm.tools.ephemeral_task_manager.ephemeral_task_list import EphemeralTaskList, Task, TaskStatus
6
+ from mojentic.llm.tools.ephemeral_task_manager.prepend_task_tool import PrependTaskTool
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 prepend_task_tool(mock_task_list):
17
+ return PrependTaskTool(task_list=mock_task_list)
18
+
19
+
20
+ class DescribePrependTaskTool:
21
+ def should_call_prepend_task_with_description(self, prepend_task_tool, mock_task_list):
22
+ mock_task = Task(id=1, description="Test task", status=TaskStatus.PENDING)
23
+ mock_task_list.prepend_task.return_value = mock_task
24
+
25
+ prepend_task_tool.run(description="Test task")
26
+
27
+ mock_task_list.prepend_task.assert_called_once_with(description="Test task")
28
+
29
+ def should_handle_error_when_prepend_task_fails(self, prepend_task_tool, mock_task_list):
30
+ mock_task_list.prepend_task.side_effect = ValueError("Test error")
31
+
32
+ prepend_task_tool.run(description="Test task")
33
+
34
+ mock_task_list.prepend_task.assert_called_once_with(description="Test task")
@@ -0,0 +1,81 @@
1
+ """
2
+ Tool for starting 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 StartTaskTool(LLMTool):
12
+ """
13
+ Tool for starting a task in the ephemeral task manager.
14
+
15
+ This tool changes a task's status from PENDING to IN_PROGRESS.
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
+ Start a task by changing its status from PENDING to IN_PROGRESS.
30
+
31
+ Args:
32
+ id: The ID of the task to start
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 PENDING 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.start_task(id=task_id)
44
+ return {
45
+ "id": task.id,
46
+ "description": task.description,
47
+ "status": task.status.value,
48
+ "summary": f"Task '{id}' started successfully"
49
+ }
50
+ except ValueError as e:
51
+ return {
52
+ "error": str(e),
53
+ "summary": f"Failed to start 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": "start_task",
68
+ "description": "Start a task by changing its status from PENDING to IN_PROGRESS.",
69
+ "parameters": {
70
+ "type": "object",
71
+ "properties": {
72
+ "id": {
73
+ "type": "integer",
74
+ "description": "The ID of the task to start"
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.ephemeral_task_list import EphemeralTaskList, Task, TaskStatus
6
+ from mojentic.llm.tools.ephemeral_task_manager.start_task_tool import StartTaskTool
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 start_task_tool(mock_task_list):
17
+ return StartTaskTool(task_list=mock_task_list)
18
+
19
+
20
+ class DescribeStartTaskTool:
21
+ def should_call_start_task_with_correct_id(self, start_task_tool, mock_task_list):
22
+ mock_task = Task(id=1, description="Test task", status=TaskStatus.IN_PROGRESS)
23
+ mock_task_list.start_task.return_value = mock_task
24
+
25
+ start_task_tool.run(id=1)
26
+
27
+ mock_task_list.start_task.assert_called_once_with(id=1)
28
+
29
+ def should_convert_string_id_to_int(self, start_task_tool, mock_task_list):
30
+ mock_task = Task(id=1, description="Test task", status=TaskStatus.IN_PROGRESS)
31
+ mock_task_list.start_task.return_value = mock_task
32
+
33
+ start_task_tool.run(id="1")
34
+
35
+ mock_task_list.start_task.assert_called_once_with(id=1)
36
+
37
+ def should_handle_error_when_start_task_fails(self, start_task_tool, mock_task_list):
38
+ error_message = "Task '1' cannot be started because it is not in PENDING status"
39
+ mock_task_list.start_task.side_effect = ValueError(error_message)
40
+
41
+ start_task_tool.run(id=1)
42
+
43
+ mock_task_list.start_task.assert_called_once_with(id=1)
@@ -0,0 +1,37 @@
1
+ import json
2
+ import os
3
+
4
+ import serpapi
5
+
6
+ from mojentic.llm.tools.llm_tool import LLMTool
7
+
8
+
9
+ class OrganicWebSearchTool(LLMTool):
10
+ def __init__(self, api_key: str = None):
11
+ self.api_key = api_key or os.getenv("SERPAPI_API_KEY")
12
+
13
+ def run(self, query: str, engine: str = "google", location: str = None, hl: str = "en", gl="ca") -> str:
14
+ results = serpapi.search(q=query, engine=engine, location=location, hl=hl, gl=gl, api_key=self.api_key)
15
+ # Limiting this to the organic results cuts this from 60K to 3K tokens
16
+ return json.dumps(results['organic_results'], indent=2)
17
+
18
+ @property
19
+ def descriptor(self):
20
+ return {
21
+ "type": "function",
22
+ "function": {
23
+ "name": "organic_web_search",
24
+ "description":
25
+ "Search the Internet for information matching the given query and return the organic search results.",
26
+ "parameters": {
27
+ "type": "object",
28
+ "properties": {
29
+ "query": {
30
+ "type": "string",
31
+ "description": "The search query."
32
+ },
33
+ },
34
+ "required": ["query"]
35
+ },
36
+ },
37
+ }
@@ -0,0 +1,27 @@
1
+ from mojentic.llm.tools.llm_tool import LLMTool
2
+
3
+
4
+ class TellUserTool(LLMTool):
5
+ def run(self, message: str) -> str:
6
+ print(f"\n\n\nMESSAGE FROM ASSISTANT:\n{message}")
7
+ return "Message delivered to user."
8
+
9
+ @property
10
+ def descriptor(self):
11
+ return {
12
+ "type": "function",
13
+ "function": {
14
+ "name": "tell_user",
15
+ "description": "Display a message to the user without expecting a response. Use this to send important intermediate information to the user as you work on completing their request.",
16
+ "parameters": {
17
+ "type": "object",
18
+ "properties": {
19
+ "message": {
20
+ "type": "string",
21
+ "description": "The important message you want to display to the user."
22
+ }
23
+ },
24
+ "required": ["message"]
25
+ },
26
+ },
27
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mojentic
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: Mojentic is an agentic framework that aims to provide a simple and flexible way to assemble teams of agents to solve complex problems.
5
5
  Author-email: Stacey Vetzal <stacey@vetzal.com>
6
6
  Project-URL: Homepage, https://github.com/mojility/mojentic
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE.md
14
14
  Requires-Dist: pydantic
15
15
  Requires-Dist: structlog
16
+ Requires-Dist: numpy
16
17
  Requires-Dist: ollama
17
18
  Requires-Dist: openai
18
19
  Requires-Dist: anthropic