autobyteus 1.1.8__py3-none-any.whl → 1.2.0__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 (127) hide show
  1. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +6 -2
  2. autobyteus/agent/handlers/inter_agent_message_event_handler.py +17 -19
  3. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +6 -3
  4. autobyteus/agent/handlers/tool_result_event_handler.py +61 -18
  5. autobyteus/agent/handlers/user_input_message_event_handler.py +19 -10
  6. autobyteus/agent/hooks/base_phase_hook.py +17 -0
  7. autobyteus/agent/hooks/hook_registry.py +15 -27
  8. autobyteus/agent/input_processor/base_user_input_processor.py +17 -1
  9. autobyteus/agent/input_processor/processor_registry.py +15 -27
  10. autobyteus/agent/llm_response_processor/base_processor.py +17 -1
  11. autobyteus/agent/llm_response_processor/processor_registry.py +15 -24
  12. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +14 -0
  13. autobyteus/agent/message/agent_input_user_message.py +15 -2
  14. autobyteus/agent/message/send_message_to.py +1 -1
  15. autobyteus/agent/processor_option.py +17 -0
  16. autobyteus/agent/sender_type.py +1 -0
  17. autobyteus/agent/system_prompt_processor/base_processor.py +17 -1
  18. autobyteus/agent/system_prompt_processor/processor_registry.py +15 -27
  19. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +10 -0
  20. autobyteus/agent/tool_execution_result_processor/base_processor.py +17 -1
  21. autobyteus/agent/tool_execution_result_processor/processor_registry.py +15 -1
  22. autobyteus/agent/workspace/base_workspace.py +1 -1
  23. autobyteus/agent/workspace/workspace_definition.py +1 -1
  24. autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +1 -1
  25. autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +2 -2
  26. autobyteus/agent_team/task_notification/__init__.py +4 -0
  27. autobyteus/agent_team/task_notification/activation_policy.py +70 -0
  28. autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +56 -122
  29. autobyteus/agent_team/task_notification/task_activator.py +66 -0
  30. autobyteus/cli/agent_team_tui/state.py +17 -20
  31. autobyteus/cli/agent_team_tui/widgets/focus_pane.py +1 -1
  32. autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +1 -1
  33. autobyteus/clients/__init__.py +10 -0
  34. autobyteus/clients/autobyteus_client.py +318 -0
  35. autobyteus/clients/cert_utils.py +105 -0
  36. autobyteus/clients/certificates/cert.pem +34 -0
  37. autobyteus/events/event_types.py +2 -2
  38. autobyteus/llm/api/autobyteus_llm.py +1 -1
  39. autobyteus/llm/api/gemini_llm.py +45 -54
  40. autobyteus/llm/api/qwen_llm.py +25 -0
  41. autobyteus/llm/api/zhipu_llm.py +26 -0
  42. autobyteus/llm/autobyteus_provider.py +9 -3
  43. autobyteus/llm/llm_factory.py +39 -0
  44. autobyteus/llm/ollama_provider_resolver.py +1 -0
  45. autobyteus/llm/providers.py +1 -0
  46. autobyteus/llm/token_counter/token_counter_factory.py +3 -0
  47. autobyteus/llm/token_counter/zhipu_token_counter.py +24 -0
  48. autobyteus/multimedia/audio/api/autobyteus_audio_client.py +5 -2
  49. autobyteus/multimedia/audio/api/gemini_audio_client.py +84 -153
  50. autobyteus/multimedia/audio/audio_client_factory.py +47 -22
  51. autobyteus/multimedia/audio/audio_model.py +13 -6
  52. autobyteus/multimedia/audio/autobyteus_audio_provider.py +9 -3
  53. autobyteus/multimedia/audio/base_audio_client.py +3 -1
  54. autobyteus/multimedia/image/api/autobyteus_image_client.py +13 -6
  55. autobyteus/multimedia/image/api/gemini_image_client.py +72 -130
  56. autobyteus/multimedia/image/api/openai_image_client.py +4 -2
  57. autobyteus/multimedia/image/autobyteus_image_provider.py +9 -3
  58. autobyteus/multimedia/image/base_image_client.py +6 -2
  59. autobyteus/multimedia/image/image_client_factory.py +20 -19
  60. autobyteus/multimedia/image/image_model.py +13 -6
  61. autobyteus/multimedia/providers.py +1 -0
  62. autobyteus/task_management/__init__.py +10 -10
  63. autobyteus/task_management/base_task_board.py +14 -6
  64. autobyteus/task_management/converters/__init__.py +0 -2
  65. autobyteus/task_management/converters/task_board_converter.py +7 -16
  66. autobyteus/task_management/events.py +6 -6
  67. autobyteus/task_management/in_memory_task_board.py +48 -38
  68. autobyteus/task_management/schemas/__init__.py +2 -2
  69. autobyteus/task_management/schemas/{plan_definition.py → task_definition.py} +6 -7
  70. autobyteus/task_management/schemas/task_status_report.py +1 -2
  71. autobyteus/task_management/task.py +60 -0
  72. autobyteus/task_management/tools/__init__.py +6 -2
  73. autobyteus/task_management/tools/assign_task_to.py +125 -0
  74. autobyteus/task_management/tools/get_my_tasks.py +80 -0
  75. autobyteus/task_management/tools/get_task_board_status.py +3 -3
  76. autobyteus/task_management/tools/publish_task.py +77 -0
  77. autobyteus/task_management/tools/publish_tasks.py +74 -0
  78. autobyteus/task_management/tools/update_task_status.py +5 -5
  79. autobyteus/tools/__init__.py +54 -16
  80. autobyteus/tools/base_tool.py +4 -4
  81. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +1 -1
  82. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +1 -1
  83. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +1 -1
  84. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +1 -1
  85. autobyteus/tools/browser/standalone/navigate_to.py +1 -1
  86. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +1 -1
  87. autobyteus/tools/browser/standalone/webpage_image_downloader.py +1 -1
  88. autobyteus/tools/browser/standalone/webpage_reader.py +1 -1
  89. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +1 -1
  90. autobyteus/tools/download_media_tool.py +136 -0
  91. autobyteus/tools/file/file_editor.py +200 -0
  92. autobyteus/tools/functional_tool.py +1 -1
  93. autobyteus/tools/google_search.py +1 -1
  94. autobyteus/tools/mcp/factory.py +1 -1
  95. autobyteus/tools/mcp/schema_mapper.py +1 -1
  96. autobyteus/tools/mcp/tool.py +1 -1
  97. autobyteus/tools/multimedia/__init__.py +2 -0
  98. autobyteus/tools/multimedia/audio_tools.py +10 -20
  99. autobyteus/tools/multimedia/image_tools.py +21 -22
  100. autobyteus/tools/multimedia/media_reader_tool.py +117 -0
  101. autobyteus/tools/pydantic_schema_converter.py +1 -1
  102. autobyteus/tools/registry/tool_definition.py +1 -1
  103. autobyteus/tools/timer.py +1 -1
  104. autobyteus/tools/tool_meta.py +1 -1
  105. autobyteus/tools/usage/formatters/default_json_example_formatter.py +1 -1
  106. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +1 -1
  107. autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +59 -3
  108. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +1 -1
  109. autobyteus/tools/usage/formatters/google_json_example_formatter.py +1 -1
  110. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +1 -1
  111. autobyteus/tools/usage/parsers/_string_decoders.py +18 -0
  112. autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +9 -1
  113. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +15 -1
  114. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +4 -1
  115. autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +4 -1
  116. autobyteus/{tools → utils}/parameter_schema.py +1 -1
  117. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/METADATA +4 -3
  118. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/RECORD +122 -108
  119. examples/run_poem_writer.py +1 -1
  120. autobyteus/task_management/converters/task_plan_converter.py +0 -48
  121. autobyteus/task_management/task_plan.py +0 -110
  122. autobyteus/task_management/tools/publish_task_plan.py +0 -101
  123. autobyteus/tools/image_downloader.py +0 -99
  124. autobyteus/tools/pdf_downloader.py +0 -89
  125. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/WHEEL +0 -0
  126. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,200 @@
1
+ import os
2
+ import re
3
+ import logging
4
+ from typing import TYPE_CHECKING, List
5
+
6
+ from autobyteus.tools.functional_tool import tool
7
+ from autobyteus.tools.tool_category import ToolCategory
8
+
9
+ if TYPE_CHECKING:
10
+ from autobyteus.agent.context import AgentContext
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _HUNK_HEADER_RE = re.compile(r"^@@ -(?P<old_start>\d+)(?:,(?P<old_count>\d+))? \+(?P<new_start>\d+)(?:,(?P<new_count>\d+))? @@")
15
+
16
+ class PatchApplicationError(ValueError):
17
+ """Raised when a unified diff patch cannot be applied to the target file."""
18
+
19
+
20
+ def _resolve_file_path(context: 'AgentContext', path: str) -> str:
21
+ """Resolves an absolute path for the given input, using the agent workspace when needed."""
22
+ if os.path.isabs(path):
23
+ final_path = path
24
+ logger.debug("FileEdit: provided path '%s' is absolute.", path)
25
+ else:
26
+ if not context.workspace:
27
+ error_msg = ("Relative path '%s' provided, but no workspace is configured for agent '%s'. "
28
+ "A workspace is required to resolve relative paths.")
29
+ logger.error(error_msg, path, context.agent_id)
30
+ raise ValueError(error_msg % (path, context.agent_id))
31
+ base_path = context.workspace.get_base_path()
32
+ if not base_path or not isinstance(base_path, str):
33
+ error_msg = ("Agent '%s' has a configured workspace, but it provided an invalid base path ('%s'). "
34
+ "Cannot resolve relative path '%s'.")
35
+ logger.error(error_msg, context.agent_id, base_path, path)
36
+ raise ValueError(error_msg % (context.agent_id, base_path, path))
37
+ final_path = os.path.join(base_path, path)
38
+ logger.debug("FileEdit: resolved relative path '%s' against workspace base '%s' to '%s'.", path, base_path, final_path)
39
+
40
+ normalized_path = os.path.normpath(final_path)
41
+ logger.debug("FileEdit: normalized path to '%s'.", normalized_path)
42
+ return normalized_path
43
+
44
+
45
+ def _apply_unified_diff(original_lines: List[str], patch: str) -> List[str]:
46
+ """Applies a unified diff patch to the provided original lines and returns the patched lines."""
47
+ if not patch or not patch.strip():
48
+ raise PatchApplicationError("Patch content is empty; nothing to apply.")
49
+
50
+ patched_lines: List[str] = []
51
+ orig_idx = 0
52
+ patch_lines = patch.splitlines(keepends=True)
53
+ line_idx = 0
54
+
55
+ while line_idx < len(patch_lines):
56
+ line = patch_lines[line_idx]
57
+
58
+ if line.startswith('---') or line.startswith('+++'):
59
+ logger.debug("FileEdit: skipping diff header line '%s'.", line.strip())
60
+ line_idx += 1
61
+ continue
62
+
63
+ if not line.startswith('@@'):
64
+ stripped = line.strip()
65
+ if stripped == '':
66
+ line_idx += 1
67
+ continue
68
+ raise PatchApplicationError(f"Unexpected content outside of hunk header: '{stripped}'.")
69
+
70
+ match = _HUNK_HEADER_RE.match(line)
71
+ if not match:
72
+ raise PatchApplicationError(f"Malformed hunk header: '{line.strip()}'.")
73
+
74
+ old_start = int(match.group('old_start'))
75
+ old_count = int(match.group('old_count') or '1')
76
+ new_start = int(match.group('new_start'))
77
+ new_count = int(match.group('new_count') or '1')
78
+ logger.debug("FileEdit: processing hunk old_start=%s old_count=%s new_start=%s new_count=%s.",
79
+ old_start, old_count, new_start, new_count)
80
+
81
+ target_idx = old_start - 1 if old_start > 0 else 0
82
+ if target_idx > len(original_lines):
83
+ raise PatchApplicationError("Patch hunk starts beyond end of file.")
84
+ if target_idx < orig_idx:
85
+ raise PatchApplicationError("Patch hunks overlap or are out of order.")
86
+
87
+ patched_lines.extend(original_lines[orig_idx:target_idx])
88
+ orig_idx = target_idx
89
+
90
+ line_idx += 1
91
+ hunk_consumed = 0
92
+ removed = 0
93
+ added = 0
94
+
95
+ while line_idx < len(patch_lines):
96
+ hunk_line = patch_lines[line_idx]
97
+ if hunk_line.startswith('@@'):
98
+ break
99
+
100
+ if hunk_line.startswith('-'):
101
+ if orig_idx >= len(original_lines):
102
+ raise PatchApplicationError("Patch attempts to remove lines beyond file length.")
103
+ if original_lines[orig_idx] != hunk_line[1:]:
104
+ raise PatchApplicationError("Patch removal does not match file content.")
105
+ orig_idx += 1
106
+ hunk_consumed += 1
107
+ removed += 1
108
+ elif hunk_line.startswith('+'):
109
+ patched_lines.append(hunk_line[1:])
110
+ added += 1
111
+ elif hunk_line.startswith(' '):
112
+ if orig_idx >= len(original_lines):
113
+ raise PatchApplicationError("Patch context exceeds file length.")
114
+ if original_lines[orig_idx] != hunk_line[1:]:
115
+ raise PatchApplicationError("Patch context does not match file content.")
116
+ patched_lines.append(original_lines[orig_idx])
117
+ orig_idx += 1
118
+ hunk_consumed += 1
119
+ elif hunk_line.startswith('\\'):
120
+ if hunk_line.strip() == '\':
121
+ if patched_lines:
122
+ patched_lines[-1] = patched_lines[-1].rstrip('\n')
123
+ else:
124
+ raise PatchApplicationError(f"Unsupported patch directive: '{hunk_line.strip()}'.")
125
+ elif hunk_line.strip() == '':
126
+ patched_lines.append(hunk_line)
127
+ else:
128
+ raise PatchApplicationError(f"Unsupported patch line: '{hunk_line.strip()}'.")
129
+
130
+ line_idx += 1
131
+
132
+ consumed_total = hunk_consumed
133
+ if old_count == 0:
134
+ if consumed_total != 0:
135
+ raise PatchApplicationError("Patch expects zero original lines but consumed some context.")
136
+ else:
137
+ if consumed_total != old_count:
138
+ raise PatchApplicationError(
139
+ f"Patch expected to consume {old_count} original lines but consumed {consumed_total}.")
140
+
141
+ context_lines = consumed_total - removed
142
+ expected_new_lines = context_lines + added
143
+ if new_count == 0:
144
+ if expected_new_lines != 0:
145
+ raise PatchApplicationError("Patch declares zero new lines but produced changes.")
146
+ else:
147
+ if expected_new_lines != new_count:
148
+ raise PatchApplicationError(
149
+ f"Patch expected to produce {new_count} new lines but produced {expected_new_lines}.")
150
+
151
+ patched_lines.extend(original_lines[orig_idx:])
152
+ return patched_lines
153
+
154
+
155
+ @tool(name="FileEdit", category=ToolCategory.FILE_SYSTEM)
156
+ async def file_edit(context: 'AgentContext', path: str, patch: str, create_if_missing: bool = False) -> str:
157
+ """Applies a unified diff patch to update a text file without overwriting unrelated content.
158
+
159
+ Args:
160
+ path: Path to the target file. Relative paths are resolved against the agent workspace when available.
161
+ patch: Unified diff patch describing the edits to apply.
162
+ create_if_missing: When True, allows applying a patch that introduces content to a non-existent file.
163
+
164
+ Raises:
165
+ FileNotFoundError: If the file does not exist and create_if_missing is False.
166
+ PatchApplicationError: If the patch content cannot be applied cleanly.
167
+ IOError: If file reading or writing fails.
168
+ """
169
+ logger.debug("FileEdit: requested edit for agent '%s' on path '%s'.", context.agent_id, path)
170
+ final_path = _resolve_file_path(context, path)
171
+
172
+ dir_path = os.path.dirname(final_path)
173
+ if dir_path and not os.path.exists(dir_path) and create_if_missing:
174
+ os.makedirs(dir_path, exist_ok=True)
175
+
176
+ file_exists = os.path.exists(final_path)
177
+ if not file_exists and not create_if_missing:
178
+ raise FileNotFoundError(f"The file at resolved path {final_path} does not exist.")
179
+
180
+ try:
181
+ original_lines: List[str]
182
+ if file_exists:
183
+ with open(final_path, 'r', encoding='utf-8') as source:
184
+ original_lines = source.read().splitlines(keepends=True)
185
+ else:
186
+ original_lines = []
187
+
188
+ patched_lines = _apply_unified_diff(original_lines, patch)
189
+
190
+ with open(final_path, 'w', encoding='utf-8') as destination:
191
+ destination.writelines(patched_lines)
192
+
193
+ logger.info("FileEdit: successfully applied patch to '%s'.", final_path)
194
+ return f"File edited successfully at {final_path}"
195
+ except PatchApplicationError as patch_err:
196
+ logger.error("FileEdit: failed to apply patch to '%s': %s", final_path, patch_err, exc_info=True)
197
+ raise patch_err
198
+ except Exception as exc: # pragma: no cover - general safeguard
199
+ logger.error("FileEdit: unexpected error while editing '%s': %s", final_path, exc, exc_info=True)
200
+ raise IOError(f"Could not edit file at '{final_path}': {exc}")
@@ -5,7 +5,7 @@ import asyncio
5
5
  from typing import Callable, Optional, Any, Dict, Tuple, Union, get_origin, get_args, List as TypingList, TYPE_CHECKING, Type
6
6
 
7
7
  from autobyteus.tools.base_tool import BaseTool
8
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
8
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
9
9
  from autobyteus.tools.tool_config import ToolConfig
10
10
  from autobyteus.tools.registry import default_tool_registry, ToolDefinition
11
11
  from autobyteus.tools.tool_origin import ToolOrigin
@@ -6,7 +6,7 @@ from typing import Optional, TYPE_CHECKING, Any, Dict, List
6
6
 
7
7
  from autobyteus.tools.base_tool import BaseTool
8
8
  from autobyteus.tools.tool_config import ToolConfig
9
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
9
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
10
10
  from autobyteus.tools.tool_category import ToolCategory
11
11
 
12
12
  if TYPE_CHECKING:
@@ -6,7 +6,7 @@ from .tool import GenericMcpTool
6
6
  from autobyteus.tools.factory.tool_factory import ToolFactory
7
7
 
8
8
  if TYPE_CHECKING:
9
- from autobyteus.tools.parameter_schema import ParameterSchema
9
+ from autobyteus.utils.parameter_schema import ParameterSchema
10
10
  from autobyteus.tools.tool_config import ToolConfig
11
11
  from autobyteus.tools.base_tool import BaseTool
12
12
 
@@ -2,7 +2,7 @@
2
2
  import logging
3
3
  from typing import Dict, Any, List, Optional
4
4
 
5
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
5
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import Any, Optional, TYPE_CHECKING
4
4
 
5
5
  from autobyteus.tools.base_tool import BaseTool
6
- from autobyteus.tools.parameter_schema import ParameterSchema
6
+ from autobyteus.utils.parameter_schema import ParameterSchema
7
7
  from .server.proxy import McpServerProxy
8
8
 
9
9
  if TYPE_CHECKING:
@@ -1,8 +1,10 @@
1
1
  from .image_tools import GenerateImageTool, EditImageTool
2
2
  from .audio_tools import GenerateSpeechTool
3
+ from .media_reader_tool import ReadMediaFile
3
4
 
4
5
  __all__ = [
5
6
  "GenerateImageTool",
6
7
  "EditImageTool",
7
8
  "GenerateSpeechTool",
9
+ "ReadMediaFile",
8
10
  ]
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import Optional, List
4
4
 
5
5
  from autobyteus.tools.base_tool import BaseTool
6
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
6
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
7
7
  from autobyteus.tools.tool_category import ToolCategory
8
8
  from autobyteus.multimedia.audio import audio_client_factory, AudioModel, AudioClientFactory
9
9
 
@@ -34,24 +34,8 @@ def _build_dynamic_audio_schema(base_params: List[ParameterDefinition], model_en
34
34
  logger.error(f"Cannot generate audio tool schema. Check environment and model registry. Error: {e}")
35
35
  raise RuntimeError(f"Failed to configure audio tool. Error: {e}")
36
36
 
37
- config_schema = ParameterSchema()
38
- if model.parameter_schema:
39
- for name, meta in model.parameter_schema.items():
40
- param_type_str = meta.get("type", "string").upper()
41
- param_type = getattr(ParameterType, param_type_str, ParameterType.STRING)
42
-
43
- allowed_values = meta.get("allowed_values")
44
- if param_type == ParameterType.STRING and allowed_values:
45
- param_type = ParameterType.ENUM
46
-
47
- config_schema.add_parameter(ParameterDefinition(
48
- name=name,
49
- param_type=param_type,
50
- description=meta.get("description", ""),
51
- required=False,
52
- default_value=meta.get("default"),
53
- enum_values=allowed_values
54
- ))
37
+ # The model's parameter schema is now a ParameterSchema object, so we can use it directly.
38
+ config_schema = model.parameter_schema
55
39
 
56
40
  schema = ParameterSchema()
57
41
  for param in base_params:
@@ -70,6 +54,7 @@ def _build_dynamic_audio_schema(base_params: List[ParameterDefinition], model_en
70
54
 
71
55
  class GenerateSpeechTool(BaseTool):
72
56
  """
57
+
73
58
  An agent tool for generating speech from text using a Text-to-Speech (TTS) model.
74
59
  """
75
60
  CATEGORY = ToolCategory.MULTIMEDIA
@@ -93,7 +78,12 @@ class GenerateSpeechTool(BaseTool):
93
78
  ParameterDefinition(
94
79
  name="prompt",
95
80
  param_type=ParameterType.STRING,
96
- description="The text to be converted into spoken audio.",
81
+ description=(
82
+ "The text to be converted into spoken audio. For multi-speaker mode, you must format the prompt "
83
+ "with speaker labels that match the speakers defined in 'speaker_mapping'. "
84
+ "CRITICAL: Each speaker's dialogue MUST be on a new line. "
85
+ "Example: 'Joe: Hello Jane.\\nJane: Hi Joe, how are you?'"
86
+ ),
97
87
  required=True
98
88
  )
99
89
  ]
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import Optional, List
4
4
 
5
5
  from autobyteus.tools.base_tool import BaseTool
6
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
6
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
7
7
  from autobyteus.tools.tool_category import ToolCategory
8
8
  from autobyteus.multimedia.image import image_client_factory, ImageModel, ImageClientFactory
9
9
 
@@ -34,24 +34,8 @@ def _build_dynamic_image_schema(base_params: List[ParameterDefinition], model_en
34
34
  logger.error(f"Cannot generate image tool schema. Check environment and model registry. Error: {e}")
35
35
  raise RuntimeError(f"Failed to configure image tool. Error: {e}")
36
36
 
37
- config_schema = ParameterSchema()
38
- if model.parameter_schema:
39
- for name, meta in model.parameter_schema.items():
40
- param_type_str = meta.get("type", "string").upper()
41
- param_type = getattr(ParameterType, param_type_str, ParameterType.STRING)
42
-
43
- allowed_values = meta.get("allowed_values")
44
- if param_type == ParameterType.STRING and allowed_values:
45
- param_type = ParameterType.ENUM
46
-
47
- config_schema.add_parameter(ParameterDefinition(
48
- name=name,
49
- param_type=param_type,
50
- description=meta.get("description", ""),
51
- required=False,
52
- default_value=meta.get("default"),
53
- enum_values=allowed_values
54
- ))
37
+ # The model's parameter schema is now a ParameterSchema object, so we can use it directly.
38
+ config_schema = model.parameter_schema
55
39
 
56
40
  schema = ParameterSchema()
57
41
  for param in base_params:
@@ -84,6 +68,7 @@ class GenerateImageTool(BaseTool):
84
68
  def get_description(cls) -> str:
85
69
  return (
86
70
  "Generates one or more images based on a textual description (prompt) using the system's default image model. "
71
+ "Can optionally accept reference images to influence the style or content. "
87
72
  "Returns a list of URLs to the generated images upon success."
88
73
  )
89
74
 
@@ -95,17 +80,31 @@ class GenerateImageTool(BaseTool):
95
80
  param_type=ParameterType.STRING,
96
81
  description="A detailed textual description of the image to generate.",
97
82
  required=True
83
+ ),
84
+ ParameterDefinition(
85
+ name="input_image_urls",
86
+ param_type=ParameterType.STRING,
87
+ description="Optional. A comma-separated string of URLs to reference images. The generated image will try to match the style or content of these images.",
88
+ required=False
98
89
  )
99
90
  ]
100
91
  return _build_dynamic_image_schema(base_params, cls.MODEL_ENV_VAR, cls.DEFAULT_MODEL)
101
92
 
102
- async def _execute(self, context, prompt: str, generation_config: Optional[dict] = None) -> List[str]:
93
+ async def _execute(self, context, prompt: str, input_image_urls: Optional[str] = None, generation_config: Optional[dict] = None) -> List[str]:
103
94
  model_identifier = _get_configured_model_identifier(self.MODEL_ENV_VAR, self.DEFAULT_MODEL)
104
95
  logger.info(f"GenerateImageTool executing with configured model '{model_identifier}'.")
105
96
  client = None
106
97
  try:
98
+ urls_list = None
99
+ if input_image_urls:
100
+ urls_list = [url.strip() for url in input_image_urls.split(',') if url.strip()]
101
+
107
102
  client = image_client_factory.create_image_client(model_identifier=model_identifier)
108
- response = await client.generate_image(prompt=prompt, generation_config=generation_config)
103
+ response = await client.generate_image(
104
+ prompt=prompt,
105
+ input_image_urls=urls_list,
106
+ generation_config=generation_config
107
+ )
109
108
 
110
109
  if not response.image_urls:
111
110
  raise ValueError("Image generation failed to return any image URLs.")
@@ -121,7 +120,7 @@ class EditImageTool(BaseTool):
121
120
  An agent tool for editing an existing image using a text prompt and a pre-configured model.
122
121
  """
123
122
  CATEGORY = ToolCategory.MULTIMEDIA
124
- MODEL_ENV_VAR = "DEFAULT_IMAGE_GENERATION_MODEL"
123
+ MODEL_ENV_VAR = "DEFAULT_IMAGE_EDIT_MODEL"
125
124
  DEFAULT_MODEL = "gpt-image-1"
126
125
 
127
126
  @classmethod
@@ -0,0 +1,117 @@
1
+ # file: autobyteus/autobyteus/tools/multimedia/media_reader_tool.py
2
+ import logging
3
+ import os
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from autobyteus.tools.base_tool import BaseTool
7
+ from autobyteus.tools.tool_category import ToolCategory
8
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
9
+ from autobyteus.agent.message.context_file import ContextFile
10
+
11
+ if TYPE_CHECKING:
12
+ from autobyteus.agent.context import AgentContext
13
+ from autobyteus.agent.workspace.base_workspace import BaseAgentWorkspace
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class ReadMediaFile(BaseTool):
18
+ """
19
+ A tool that loads a media file (image, audio, video) into the context for
20
+ the next LLM turn. This allows a multimodal LLM to directly 'see' or 'hear'
21
+ the file's content. The tool's result is a structured object that the system
22
+ uses to construct a multimodal prompt, not plain text.
23
+ """
24
+ TOOL_NAME = "ReadMediaFile"
25
+ CATEGORY = ToolCategory.MULTIMEDIA
26
+
27
+ @classmethod
28
+ def get_name(cls) -> str:
29
+ return cls.TOOL_NAME
30
+
31
+ @classmethod
32
+ def get_description(cls) -> str:
33
+ return (
34
+ "Loads a media file (image, audio, video) into the context for the next turn, "
35
+ "allowing the LLM to directly analyze its content. Use this when you need to 'see' an image, "
36
+ "'listen' to audio, or 'watch' a video that you know exists in the workspace or file system."
37
+ )
38
+
39
+ @classmethod
40
+ def get_argument_schema(cls) -> Optional[ParameterSchema]:
41
+ schema = ParameterSchema()
42
+ schema.add_parameter(ParameterDefinition(
43
+ name="file_path",
44
+ param_type=ParameterType.STRING,
45
+ description="The absolute path or workspace-relative path to the media file.",
46
+ required=True
47
+ ))
48
+ return schema
49
+
50
+ async def _execute(self,
51
+ context: 'AgentContext',
52
+ file_path: str) -> 'ContextFile':
53
+ """
54
+ Resolves the file path and returns a ContextFile object, which signals
55
+ the system to include this file in the next multimodal LLM prompt.
56
+ It handles both absolute paths and paths relative to the agent's workspace.
57
+ """
58
+ logger.debug(f"Tool '{self.get_name()}': Received request to read media file at '{file_path}'.")
59
+
60
+ absolute_path: str
61
+ workspace: Optional['BaseAgentWorkspace'] = context.workspace
62
+
63
+ if os.path.isabs(file_path):
64
+ absolute_path = os.path.normpath(file_path)
65
+ logger.debug(f"Path '{file_path}' is absolute. Using resolved path: '{absolute_path}'.")
66
+
67
+ # Security Note: This allows reading from outside the workspace.
68
+ # We log a warning if this occurs.
69
+ if workspace and hasattr(workspace, 'get_base_path'):
70
+ try:
71
+ workspace_root = os.path.abspath(workspace.get_base_path())
72
+ resolved_target = os.path.abspath(absolute_path)
73
+ if not os.path.commonpath([workspace_root]) == os.path.commonpath([workspace_root, resolved_target]):
74
+ logger.warning(
75
+ f"Security Note: Tool '{self.get_name()}' is accessing an absolute path "
76
+ f"'{absolute_path}' which is outside the agent's workspace '{workspace_root}'."
77
+ )
78
+ except Exception:
79
+ # Failsafe if get_base_path has an issue.
80
+ pass
81
+ else:
82
+ # Handle relative paths, which MUST be resolved against a workspace that supports file paths.
83
+ if not (workspace and hasattr(workspace, 'get_base_path') and callable(getattr(workspace, 'get_base_path'))):
84
+ raise ValueError(
85
+ f"A relative path '{file_path}' was provided, but the agent's workspace does not support "
86
+ "file system path resolution. Please provide an absolute path or configure a suitable workspace."
87
+ )
88
+
89
+ try:
90
+ base_path = os.path.abspath(workspace.get_base_path())
91
+ # Securely join the path and resolve it to a final absolute path
92
+ absolute_path = os.path.abspath(os.path.join(base_path, file_path))
93
+
94
+ # Security Check: Ensure the resolved path is still within the workspace directory.
95
+ if os.path.commonpath([base_path]) != os.path.commonpath([base_path, absolute_path]):
96
+ raise ValueError(f"Security error: Path '{file_path}' attempts to access files outside the agent's workspace.")
97
+
98
+ logger.debug(f"Path '{file_path}' is relative. Resolved against workspace to '{absolute_path}'.")
99
+
100
+ except ValueError as e:
101
+ # Re-raise security errors with more context.
102
+ logger.error(f"Tool '{self.get_name()}': Security error resolving relative path '{file_path}': {e}")
103
+ raise
104
+
105
+ try:
106
+ if not os.path.exists(absolute_path) or not os.path.isfile(absolute_path):
107
+ raise FileNotFoundError(f"The file '{file_path}' does not exist or is not a regular file at the resolved path '{absolute_path}'.")
108
+
109
+ logger.info(f"Tool '{self.get_name()}': Staging file '{absolute_path}' for next LLM turn.")
110
+
111
+ # The ContextFile constructor will automatically infer file_type from the path.
112
+ # This is the special object that the ToolResultEventHandler will look for.
113
+ return ContextFile(uri=absolute_path)
114
+
115
+ except (ValueError, FileNotFoundError) as e:
116
+ logger.error(f"Tool '{self.get_name()}': Error processing path '{file_path}': {e}")
117
+ raise
@@ -2,7 +2,7 @@
2
2
  import logging
3
3
  from typing import Type, get_origin, get_args, Union, List, Dict
4
4
  from pydantic import BaseModel
5
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
5
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
@@ -5,7 +5,7 @@ from typing import Dict, Any, List as TypingList, Type, TYPE_CHECKING, Optional,
5
5
 
6
6
  from autobyteus.llm.providers import LLMProvider
7
7
  from autobyteus.tools.tool_config import ToolConfig
8
- from autobyteus.tools.parameter_schema import ParameterSchema
8
+ from autobyteus.utils.parameter_schema import ParameterSchema
9
9
  from autobyteus.tools.tool_origin import ToolOrigin
10
10
  # Import default formatters directly to provide convenience methods
11
11
  from autobyteus.tools.usage.formatters import (
autobyteus/tools/timer.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
  from typing import Optional, TYPE_CHECKING, Any
3
3
  from autobyteus.tools.base_tool import BaseTool
4
4
  from autobyteus.tools.tool_config import ToolConfig
5
- from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
5
+ from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
6
6
  from autobyteus.tools.tool_category import ToolCategory
7
7
  from autobyteus.events.event_emitter import EventEmitter
8
8
  from autobyteus.events.event_types import EventType
@@ -4,7 +4,7 @@ from abc import ABCMeta
4
4
  from typing import Dict, Any
5
5
 
6
6
  from autobyteus.tools.registry import default_tool_registry, ToolDefinition
7
- from autobyteus.tools.parameter_schema import ParameterSchema
7
+ from autobyteus.utils.parameter_schema import ParameterSchema
8
8
  from autobyteus.tools.tool_origin import ToolOrigin
9
9
  from autobyteus.tools.tool_category import ToolCategory
10
10
 
@@ -2,7 +2,7 @@
2
2
  import json
3
3
  from typing import Dict, Any, TYPE_CHECKING, List, Optional, Union
4
4
 
5
- from autobyteus.tools.parameter_schema import ParameterType, ParameterDefinition, ParameterSchema
5
+ from autobyteus.utils.parameter_schema import ParameterType, ParameterDefinition, ParameterSchema
6
6
  from .base_formatter import BaseExampleFormatter
7
7
 
8
8
  if TYPE_CHECKING:
@@ -3,7 +3,7 @@ import xml.sax.saxutils
3
3
  import re
4
4
  from typing import Any, TYPE_CHECKING, List, Optional
5
5
 
6
- from autobyteus.tools.parameter_schema import ParameterType, ParameterDefinition, ParameterSchema
6
+ from autobyteus.utils.parameter_schema import ParameterType, ParameterDefinition, ParameterSchema
7
7
  from .base_formatter import BaseExampleFormatter
8
8
 
9
9
  if TYPE_CHECKING: