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.
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +6 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +17 -19
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +6 -3
- autobyteus/agent/handlers/tool_result_event_handler.py +61 -18
- autobyteus/agent/handlers/user_input_message_event_handler.py +19 -10
- autobyteus/agent/hooks/base_phase_hook.py +17 -0
- autobyteus/agent/hooks/hook_registry.py +15 -27
- autobyteus/agent/input_processor/base_user_input_processor.py +17 -1
- autobyteus/agent/input_processor/processor_registry.py +15 -27
- autobyteus/agent/llm_response_processor/base_processor.py +17 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +15 -24
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +14 -0
- autobyteus/agent/message/agent_input_user_message.py +15 -2
- autobyteus/agent/message/send_message_to.py +1 -1
- autobyteus/agent/processor_option.py +17 -0
- autobyteus/agent/sender_type.py +1 -0
- autobyteus/agent/system_prompt_processor/base_processor.py +17 -1
- autobyteus/agent/system_prompt_processor/processor_registry.py +15 -27
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +10 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +17 -1
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +15 -1
- autobyteus/agent/workspace/base_workspace.py +1 -1
- autobyteus/agent/workspace/workspace_definition.py +1 -1
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +1 -1
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +2 -2
- autobyteus/agent_team/task_notification/__init__.py +4 -0
- autobyteus/agent_team/task_notification/activation_policy.py +70 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +56 -122
- autobyteus/agent_team/task_notification/task_activator.py +66 -0
- autobyteus/cli/agent_team_tui/state.py +17 -20
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +1 -1
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +1 -1
- autobyteus/clients/__init__.py +10 -0
- autobyteus/clients/autobyteus_client.py +318 -0
- autobyteus/clients/cert_utils.py +105 -0
- autobyteus/clients/certificates/cert.pem +34 -0
- autobyteus/events/event_types.py +2 -2
- autobyteus/llm/api/autobyteus_llm.py +1 -1
- autobyteus/llm/api/gemini_llm.py +45 -54
- autobyteus/llm/api/qwen_llm.py +25 -0
- autobyteus/llm/api/zhipu_llm.py +26 -0
- autobyteus/llm/autobyteus_provider.py +9 -3
- autobyteus/llm/llm_factory.py +39 -0
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -0
- autobyteus/llm/token_counter/zhipu_token_counter.py +24 -0
- autobyteus/multimedia/audio/api/autobyteus_audio_client.py +5 -2
- autobyteus/multimedia/audio/api/gemini_audio_client.py +84 -153
- autobyteus/multimedia/audio/audio_client_factory.py +47 -22
- autobyteus/multimedia/audio/audio_model.py +13 -6
- autobyteus/multimedia/audio/autobyteus_audio_provider.py +9 -3
- autobyteus/multimedia/audio/base_audio_client.py +3 -1
- autobyteus/multimedia/image/api/autobyteus_image_client.py +13 -6
- autobyteus/multimedia/image/api/gemini_image_client.py +72 -130
- autobyteus/multimedia/image/api/openai_image_client.py +4 -2
- autobyteus/multimedia/image/autobyteus_image_provider.py +9 -3
- autobyteus/multimedia/image/base_image_client.py +6 -2
- autobyteus/multimedia/image/image_client_factory.py +20 -19
- autobyteus/multimedia/image/image_model.py +13 -6
- autobyteus/multimedia/providers.py +1 -0
- autobyteus/task_management/__init__.py +10 -10
- autobyteus/task_management/base_task_board.py +14 -6
- autobyteus/task_management/converters/__init__.py +0 -2
- autobyteus/task_management/converters/task_board_converter.py +7 -16
- autobyteus/task_management/events.py +6 -6
- autobyteus/task_management/in_memory_task_board.py +48 -38
- autobyteus/task_management/schemas/__init__.py +2 -2
- autobyteus/task_management/schemas/{plan_definition.py → task_definition.py} +6 -7
- autobyteus/task_management/schemas/task_status_report.py +1 -2
- autobyteus/task_management/task.py +60 -0
- autobyteus/task_management/tools/__init__.py +6 -2
- autobyteus/task_management/tools/assign_task_to.py +125 -0
- autobyteus/task_management/tools/get_my_tasks.py +80 -0
- autobyteus/task_management/tools/get_task_board_status.py +3 -3
- autobyteus/task_management/tools/publish_task.py +77 -0
- autobyteus/task_management/tools/publish_tasks.py +74 -0
- autobyteus/task_management/tools/update_task_status.py +5 -5
- autobyteus/tools/__init__.py +54 -16
- autobyteus/tools/base_tool.py +4 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +1 -1
- autobyteus/tools/browser/standalone/navigate_to.py +1 -1
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +1 -1
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +1 -1
- autobyteus/tools/browser/standalone/webpage_reader.py +1 -1
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +1 -1
- autobyteus/tools/download_media_tool.py +136 -0
- autobyteus/tools/file/file_editor.py +200 -0
- autobyteus/tools/functional_tool.py +1 -1
- autobyteus/tools/google_search.py +1 -1
- autobyteus/tools/mcp/factory.py +1 -1
- autobyteus/tools/mcp/schema_mapper.py +1 -1
- autobyteus/tools/mcp/tool.py +1 -1
- autobyteus/tools/multimedia/__init__.py +2 -0
- autobyteus/tools/multimedia/audio_tools.py +10 -20
- autobyteus/tools/multimedia/image_tools.py +21 -22
- autobyteus/tools/multimedia/media_reader_tool.py +117 -0
- autobyteus/tools/pydantic_schema_converter.py +1 -1
- autobyteus/tools/registry/tool_definition.py +1 -1
- autobyteus/tools/timer.py +1 -1
- autobyteus/tools/tool_meta.py +1 -1
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +59 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +1 -1
- autobyteus/tools/usage/parsers/_string_decoders.py +18 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +9 -1
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +15 -1
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +4 -1
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +4 -1
- autobyteus/{tools → utils}/parameter_schema.py +1 -1
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/METADATA +4 -3
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/RECORD +122 -108
- examples/run_poem_writer.py +1 -1
- autobyteus/task_management/converters/task_plan_converter.py +0 -48
- autobyteus/task_management/task_plan.py +0 -110
- autobyteus/task_management/tools/publish_task_plan.py +0 -101
- autobyteus/tools/image_downloader.py +0 -99
- autobyteus/tools/pdf_downloader.py +0 -89
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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.
|
|
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:
|
autobyteus/tools/mcp/factory.py
CHANGED
|
@@ -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.
|
|
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.
|
|
5
|
+
from autobyteus.utils.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
6
6
|
|
|
7
7
|
logger = logging.getLogger(__name__)
|
|
8
8
|
|
autobyteus/tools/mcp/tool.py
CHANGED
|
@@ -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.
|
|
6
|
+
from autobyteus.utils.parameter_schema import ParameterSchema
|
|
7
7
|
from .server.proxy import McpServerProxy
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
@@ -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.
|
|
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
|
-
|
|
38
|
-
|
|
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=
|
|
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.
|
|
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
|
-
|
|
38
|
-
|
|
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(
|
|
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 = "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
autobyteus/tools/tool_meta.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
6
|
+
from autobyteus.utils.parameter_schema import ParameterType, ParameterDefinition, ParameterSchema
|
|
7
7
|
from .base_formatter import BaseExampleFormatter
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|