massgen 0.1.0a3__py3-none-any.whl → 0.1.1__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.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +1 -1
- massgen/agent_config.py +17 -0
- massgen/api_params_handler/_api_params_handler_base.py +1 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
- massgen/api_params_handler/_claude_api_params_handler.py +8 -1
- massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
- massgen/api_params_handler/_response_api_params_handler.py +8 -1
- massgen/backend/base.py +31 -0
- massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
- massgen/backend/chat_completions.py +182 -92
- massgen/backend/claude.py +115 -18
- massgen/backend/claude_code.py +378 -14
- massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
- massgen/backend/gemini.py +1275 -1607
- massgen/backend/gemini_mcp_manager.py +545 -0
- massgen/backend/gemini_trackers.py +344 -0
- massgen/backend/gemini_utils.py +43 -0
- massgen/backend/response.py +129 -70
- massgen/cli.py +577 -110
- massgen/config_builder.py +376 -27
- massgen/configs/README.md +111 -80
- massgen/configs/basic/multi/three_agents_default.yaml +1 -1
- massgen/configs/basic/single/single_agent.yaml +1 -1
- massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
- massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
- massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
- massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
- massgen/formatter/_chat_completions_formatter.py +104 -0
- massgen/formatter/_claude_formatter.py +120 -0
- massgen/formatter/_gemini_formatter.py +448 -0
- massgen/formatter/_response_formatter.py +88 -0
- massgen/frontend/coordination_ui.py +4 -2
- massgen/logger_config.py +35 -3
- massgen/message_templates.py +56 -6
- massgen/orchestrator.py +179 -10
- massgen/stream_chunk/base.py +3 -0
- massgen/tests/custom_tools_example.py +392 -0
- massgen/tests/mcp_test_server.py +17 -7
- massgen/tests/test_config_builder.py +423 -0
- massgen/tests/test_custom_tools.py +401 -0
- massgen/tests/test_tools.py +127 -0
- massgen/tool/README.md +935 -0
- massgen/tool/__init__.py +39 -0
- massgen/tool/_async_helpers.py +70 -0
- massgen/tool/_basic/__init__.py +8 -0
- massgen/tool/_basic/_two_num_tool.py +24 -0
- massgen/tool/_code_executors/__init__.py +10 -0
- massgen/tool/_code_executors/_python_executor.py +74 -0
- massgen/tool/_code_executors/_shell_executor.py +61 -0
- massgen/tool/_exceptions.py +39 -0
- massgen/tool/_file_handlers/__init__.py +10 -0
- massgen/tool/_file_handlers/_file_operations.py +218 -0
- massgen/tool/_manager.py +634 -0
- massgen/tool/_registered_tool.py +88 -0
- massgen/tool/_result.py +66 -0
- massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
- massgen/tool/docs/builtin_tools.md +681 -0
- massgen/tool/docs/exceptions.md +794 -0
- massgen/tool/docs/execution_results.md +691 -0
- massgen/tool/docs/manager.md +887 -0
- massgen/tool/docs/workflow_toolkits.md +529 -0
- massgen/tool/workflow_toolkits/__init__.py +57 -0
- massgen/tool/workflow_toolkits/base.py +55 -0
- massgen/tool/workflow_toolkits/new_answer.py +126 -0
- massgen/tool/workflow_toolkits/vote.py +167 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
massgen/tool/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Tool module for MassGen framework."""
|
|
3
|
+
|
|
4
|
+
from ._code_executors import run_python_script, run_shell_script
|
|
5
|
+
from ._file_handlers import append_file_content, read_file_content, save_file_content
|
|
6
|
+
from ._manager import ToolManager
|
|
7
|
+
from ._result import ExecutionResult
|
|
8
|
+
from .workflow_toolkits import (
|
|
9
|
+
BaseToolkit,
|
|
10
|
+
NewAnswerToolkit,
|
|
11
|
+
ToolType,
|
|
12
|
+
VoteToolkit,
|
|
13
|
+
get_workflow_tools,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ToolManager",
|
|
18
|
+
"ExecutionResult",
|
|
19
|
+
"two_num_tool",
|
|
20
|
+
"run_python_script",
|
|
21
|
+
"run_shell_script",
|
|
22
|
+
"read_file_content",
|
|
23
|
+
"save_file_content",
|
|
24
|
+
"append_file_content",
|
|
25
|
+
"dashscope_generate_image",
|
|
26
|
+
"dashscope_generate_audio",
|
|
27
|
+
"dashscope_analyze_image",
|
|
28
|
+
"openai_generate_image",
|
|
29
|
+
"openai_generate_audio",
|
|
30
|
+
"openai_modify_image",
|
|
31
|
+
"openai_create_variation",
|
|
32
|
+
"openai_analyze_image",
|
|
33
|
+
"openai_transcribe_audio",
|
|
34
|
+
"BaseToolkit",
|
|
35
|
+
"ToolType",
|
|
36
|
+
"NewAnswerToolkit",
|
|
37
|
+
"VoteToolkit",
|
|
38
|
+
"get_workflow_tools",
|
|
39
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Async utility functions for wrapping different return types."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import AsyncGenerator, Callable, Generator, Optional
|
|
6
|
+
|
|
7
|
+
from ._result import ExecutionResult, TextContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def _apply_post_processing(
|
|
11
|
+
result: ExecutionResult,
|
|
12
|
+
processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
|
|
13
|
+
) -> ExecutionResult:
|
|
14
|
+
"""Apply post-processing to an execution result."""
|
|
15
|
+
if processor:
|
|
16
|
+
processed = processor(result)
|
|
17
|
+
if processed:
|
|
18
|
+
return processed
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def wrap_object_async(
|
|
23
|
+
obj: ExecutionResult,
|
|
24
|
+
processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
|
|
25
|
+
) -> AsyncGenerator[ExecutionResult, None]:
|
|
26
|
+
"""Convert a single ExecutionResult to async generator."""
|
|
27
|
+
yield await _apply_post_processing(obj, processor)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def wrap_sync_gen_async(
|
|
31
|
+
sync_gen: Generator[ExecutionResult, None, None],
|
|
32
|
+
processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
|
|
33
|
+
) -> AsyncGenerator[ExecutionResult, None]:
|
|
34
|
+
"""Convert sync generator to async generator."""
|
|
35
|
+
for chunk in sync_gen:
|
|
36
|
+
yield await _apply_post_processing(chunk, processor)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def wrap_as_async_generator(
|
|
40
|
+
async_gen: AsyncGenerator[ExecutionResult, None],
|
|
41
|
+
processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
|
|
42
|
+
) -> AsyncGenerator[ExecutionResult, None]:
|
|
43
|
+
"""Wrap async generator with interruption handling."""
|
|
44
|
+
|
|
45
|
+
previous_chunk = None
|
|
46
|
+
try:
|
|
47
|
+
async for chunk in async_gen:
|
|
48
|
+
processed = await _apply_post_processing(chunk, processor)
|
|
49
|
+
yield processed
|
|
50
|
+
previous_chunk = processed
|
|
51
|
+
|
|
52
|
+
except asyncio.CancelledError:
|
|
53
|
+
interrupt_msg = TextContent(
|
|
54
|
+
data="<system>Execution interrupted by user request</system>",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if previous_chunk:
|
|
58
|
+
previous_chunk.output_blocks.append(interrupt_msg)
|
|
59
|
+
previous_chunk.was_interrupted = True
|
|
60
|
+
previous_chunk.is_final = True
|
|
61
|
+
yield await _apply_post_processing(previous_chunk, processor)
|
|
62
|
+
else:
|
|
63
|
+
yield await _apply_post_processing(
|
|
64
|
+
ExecutionResult(
|
|
65
|
+
output_blocks=[interrupt_msg],
|
|
66
|
+
was_interrupted=True,
|
|
67
|
+
is_final=True,
|
|
68
|
+
),
|
|
69
|
+
processor,
|
|
70
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Sample math tool for testing purposes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from massgen.tool._result import ExecutionResult, TextContent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def two_num_tool(x: int, y: int) -> ExecutionResult:
|
|
10
|
+
"""Add two numbers together.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
x: First number
|
|
14
|
+
y: Second number
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Sum of the two numbers
|
|
18
|
+
"""
|
|
19
|
+
result = x + y
|
|
20
|
+
return ExecutionResult(
|
|
21
|
+
output_blocks=[
|
|
22
|
+
TextContent(data=f"The sum of {x} and {y} is {result}"),
|
|
23
|
+
],
|
|
24
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Python code execution tool implementation."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .._result import ExecutionResult, TextContent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def run_python_script(
|
|
15
|
+
source_code: str,
|
|
16
|
+
max_duration: float = 300,
|
|
17
|
+
**extra_kwargs: Any,
|
|
18
|
+
) -> ExecutionResult:
|
|
19
|
+
"""Execute Python code in an isolated temporary environment.
|
|
20
|
+
|
|
21
|
+
The code runs in a temporary file that is cleaned up after execution.
|
|
22
|
+
Results must be printed to be captured in the output.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
source_code: Python code to execute
|
|
26
|
+
max_duration: Maximum execution time in seconds (default: 300)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ExecutionResult containing execution status and output
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
with tempfile.TemporaryDirectory() as temp_workspace:
|
|
33
|
+
script_file = os.path.join(temp_workspace, f"script_{uuid.uuid4().hex}.py")
|
|
34
|
+
with open(script_file, "w", encoding="utf-8") as file:
|
|
35
|
+
file.write(source_code)
|
|
36
|
+
|
|
37
|
+
process = await asyncio.create_subprocess_exec(
|
|
38
|
+
sys.executable,
|
|
39
|
+
"-u",
|
|
40
|
+
script_file,
|
|
41
|
+
stdout=asyncio.subprocess.PIPE,
|
|
42
|
+
stderr=asyncio.subprocess.PIPE,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
await asyncio.wait_for(process.wait(), timeout=max_duration)
|
|
47
|
+
stdout_data, stderr_data = await process.communicate()
|
|
48
|
+
stdout_text = stdout_data.decode("utf-8")
|
|
49
|
+
stderr_text = stderr_data.decode("utf-8")
|
|
50
|
+
exit_code = process.returncode
|
|
51
|
+
|
|
52
|
+
except asyncio.TimeoutError:
|
|
53
|
+
timeout_msg = f"TimeoutError: Execution exceeded {max_duration} seconds limit"
|
|
54
|
+
exit_code = -1
|
|
55
|
+
try:
|
|
56
|
+
process.terminate()
|
|
57
|
+
stdout_data, stderr_data = await process.communicate()
|
|
58
|
+
stdout_text = stdout_data.decode("utf-8")
|
|
59
|
+
stderr_text = stderr_data.decode("utf-8")
|
|
60
|
+
if stderr_text:
|
|
61
|
+
stderr_text += f"\n{timeout_msg}"
|
|
62
|
+
else:
|
|
63
|
+
stderr_text = timeout_msg
|
|
64
|
+
except ProcessLookupError:
|
|
65
|
+
stdout_text = ""
|
|
66
|
+
stderr_text = timeout_msg
|
|
67
|
+
|
|
68
|
+
return ExecutionResult(
|
|
69
|
+
output_blocks=[
|
|
70
|
+
TextContent(
|
|
71
|
+
data=(f"<exit_code>{exit_code}</exit_code>" f"<stdout>{stdout_text}</stdout>" f"<stderr>{stderr_text}</stderr>"),
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Shell command execution tool implementation."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._result import ExecutionResult, TextContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def run_shell_script(
|
|
11
|
+
shell_command: str,
|
|
12
|
+
max_duration: int = 300,
|
|
13
|
+
**extra_kwargs: Any,
|
|
14
|
+
) -> ExecutionResult:
|
|
15
|
+
"""Execute shell commands and capture output.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
shell_command: Shell command to execute
|
|
19
|
+
max_duration: Maximum execution time in seconds (default: 300)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
ExecutionResult with exit code, stdout, and stderr
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
process = await asyncio.create_subprocess_shell(
|
|
26
|
+
shell_command,
|
|
27
|
+
stdout=asyncio.subprocess.PIPE,
|
|
28
|
+
stderr=asyncio.subprocess.PIPE,
|
|
29
|
+
bufsize=0,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
await asyncio.wait_for(process.wait(), timeout=max_duration)
|
|
34
|
+
stdout_data, stderr_data = await process.communicate()
|
|
35
|
+
stdout_text = stdout_data.decode("utf-8")
|
|
36
|
+
stderr_text = stderr_data.decode("utf-8")
|
|
37
|
+
exit_code = process.returncode
|
|
38
|
+
|
|
39
|
+
except asyncio.TimeoutError:
|
|
40
|
+
timeout_msg = f"TimeoutError: Command execution exceeded {max_duration} seconds limit"
|
|
41
|
+
exit_code = -1
|
|
42
|
+
try:
|
|
43
|
+
process.terminate()
|
|
44
|
+
stdout_data, stderr_data = await process.communicate()
|
|
45
|
+
stdout_text = stdout_data.decode("utf-8")
|
|
46
|
+
stderr_text = stderr_data.decode("utf-8")
|
|
47
|
+
if stderr_text:
|
|
48
|
+
stderr_text += f"\n{timeout_msg}"
|
|
49
|
+
else:
|
|
50
|
+
stderr_text = timeout_msg
|
|
51
|
+
except ProcessLookupError:
|
|
52
|
+
stdout_text = ""
|
|
53
|
+
stderr_text = timeout_msg
|
|
54
|
+
|
|
55
|
+
return ExecutionResult(
|
|
56
|
+
output_blocks=[
|
|
57
|
+
TextContent(
|
|
58
|
+
data=(f"<exit_code>{exit_code}</exit_code>" f"<stdout>{stdout_text}</stdout>" f"<stderr>{stderr_text}</stderr>"),
|
|
59
|
+
),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Exception classes for tool operations."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ToolException(Exception):
|
|
6
|
+
"""Base exception for tool-related errors."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidToolArgumentsException(ToolException):
|
|
10
|
+
"""Raised when tool receives invalid arguments."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, error_msg: str):
|
|
13
|
+
self.error_msg = error_msg
|
|
14
|
+
super().__init__(self.error_msg)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolNotFoundException(ToolException):
|
|
18
|
+
"""Raised when requested tool is not found."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tool_name: str):
|
|
21
|
+
self.tool_name = tool_name
|
|
22
|
+
super().__init__(f"Tool '{tool_name}' not found in registry")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolExecutionException(ToolException):
|
|
26
|
+
"""Raised when tool execution fails."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, tool_name: str, error_details: str):
|
|
29
|
+
self.tool_name = tool_name
|
|
30
|
+
self.error_details = error_details
|
|
31
|
+
super().__init__(f"Tool '{tool_name}' execution failed: {error_details}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CategoryNotFoundException(ToolException):
|
|
35
|
+
"""Raised when tool category is not found."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, category_name: str):
|
|
38
|
+
self.category_name = category_name
|
|
39
|
+
super().__init__(f"Category '{category_name}' not found")
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""File operation tools for reading and writing."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from .._result import ExecutionResult, TextContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def read_file_content(
|
|
11
|
+
target_path: str,
|
|
12
|
+
line_range: Optional[List[int]] = None,
|
|
13
|
+
) -> ExecutionResult:
|
|
14
|
+
"""Read file contents with optional line range specification.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
target_path: Path to the file to read
|
|
18
|
+
line_range: Optional [start, end] line numbers (1-based, inclusive).
|
|
19
|
+
Use negative numbers for lines from end (e.g., [-100, -1])
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
ExecutionResult with file content or error message
|
|
23
|
+
"""
|
|
24
|
+
if not os.path.exists(target_path):
|
|
25
|
+
return ExecutionResult(
|
|
26
|
+
output_blocks=[
|
|
27
|
+
TextContent(
|
|
28
|
+
data=f"Error: File '{target_path}' does not exist.",
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if not os.path.isfile(target_path):
|
|
34
|
+
return ExecutionResult(
|
|
35
|
+
output_blocks=[
|
|
36
|
+
TextContent(
|
|
37
|
+
data=f"Error: Path '{target_path}' is not a file.",
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with open(target_path, "r", encoding="utf-8") as file:
|
|
44
|
+
all_lines = file.readlines()
|
|
45
|
+
|
|
46
|
+
if line_range:
|
|
47
|
+
start_line, end_line = line_range
|
|
48
|
+
|
|
49
|
+
# Handle negative indices
|
|
50
|
+
if start_line < 0:
|
|
51
|
+
start_line = len(all_lines) + start_line + 1
|
|
52
|
+
if end_line < 0:
|
|
53
|
+
end_line = len(all_lines) + end_line + 1
|
|
54
|
+
|
|
55
|
+
# Validate range
|
|
56
|
+
if start_line < 1 or end_line > len(all_lines) or start_line > end_line:
|
|
57
|
+
return ExecutionResult(
|
|
58
|
+
output_blocks=[
|
|
59
|
+
TextContent(
|
|
60
|
+
data=f"Error: Invalid line range {line_range} for file with {len(all_lines)} lines.",
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Extract lines (convert to 0-based indexing)
|
|
66
|
+
selected_lines = all_lines[start_line - 1 : end_line]
|
|
67
|
+
content_with_numbers = "".join(f"{i + start_line:4d}│ {line}" for i, line in enumerate(selected_lines))
|
|
68
|
+
|
|
69
|
+
return ExecutionResult(
|
|
70
|
+
output_blocks=[
|
|
71
|
+
TextContent(
|
|
72
|
+
data=f"Content of {target_path} (lines {start_line}-{end_line}):\n```\n{content_with_numbers}```",
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
full_content = "".join(all_lines)
|
|
78
|
+
return ExecutionResult(
|
|
79
|
+
output_blocks=[
|
|
80
|
+
TextContent(
|
|
81
|
+
data=f"Content of {target_path}:\n```\n{full_content}```",
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
except Exception as error:
|
|
87
|
+
return ExecutionResult(
|
|
88
|
+
output_blocks=[
|
|
89
|
+
TextContent(
|
|
90
|
+
data=f"Error reading file: {error}",
|
|
91
|
+
),
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def save_file_content(
|
|
97
|
+
target_path: str,
|
|
98
|
+
file_content: str,
|
|
99
|
+
create_dirs: bool = True,
|
|
100
|
+
) -> ExecutionResult:
|
|
101
|
+
"""Write content to a file, creating directories if needed.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
target_path: Path where file will be saved
|
|
105
|
+
file_content: Content to write to the file
|
|
106
|
+
create_dirs: Whether to create parent directories if they don't exist
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
ExecutionResult indicating success or failure
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
# Create parent directories if requested
|
|
113
|
+
if create_dirs:
|
|
114
|
+
parent_dir = os.path.dirname(target_path)
|
|
115
|
+
if parent_dir:
|
|
116
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
with open(target_path, "w", encoding="utf-8") as file:
|
|
119
|
+
file.write(file_content)
|
|
120
|
+
|
|
121
|
+
return ExecutionResult(
|
|
122
|
+
output_blocks=[
|
|
123
|
+
TextContent(
|
|
124
|
+
data=f"Successfully wrote {len(file_content)} characters to {target_path}",
|
|
125
|
+
),
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
except Exception as error:
|
|
130
|
+
return ExecutionResult(
|
|
131
|
+
output_blocks=[
|
|
132
|
+
TextContent(
|
|
133
|
+
data=f"Error writing file: {error}",
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def append_file_content(
|
|
140
|
+
target_path: str,
|
|
141
|
+
additional_content: str,
|
|
142
|
+
line_position: Optional[int] = None,
|
|
143
|
+
) -> ExecutionResult:
|
|
144
|
+
"""Append content to a file or insert at specific line.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
target_path: Path to the file
|
|
148
|
+
additional_content: Content to append or insert
|
|
149
|
+
line_position: Optional line number to insert at (1-based).
|
|
150
|
+
If None, appends to end of file.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
ExecutionResult indicating success or failure
|
|
154
|
+
"""
|
|
155
|
+
if not os.path.exists(target_path):
|
|
156
|
+
return ExecutionResult(
|
|
157
|
+
output_blocks=[
|
|
158
|
+
TextContent(
|
|
159
|
+
data=f"Error: File '{target_path}' does not exist.",
|
|
160
|
+
),
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
with open(target_path, "r", encoding="utf-8") as file:
|
|
166
|
+
current_lines = file.readlines()
|
|
167
|
+
|
|
168
|
+
if line_position is None:
|
|
169
|
+
# Append to end
|
|
170
|
+
with open(target_path, "a", encoding="utf-8") as file:
|
|
171
|
+
file.write(additional_content)
|
|
172
|
+
|
|
173
|
+
return ExecutionResult(
|
|
174
|
+
output_blocks=[
|
|
175
|
+
TextContent(
|
|
176
|
+
data=f"Successfully appended {len(additional_content)} characters to {target_path}",
|
|
177
|
+
),
|
|
178
|
+
],
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# Insert at specific line
|
|
182
|
+
if line_position < 1 or line_position > len(current_lines) + 1:
|
|
183
|
+
return ExecutionResult(
|
|
184
|
+
output_blocks=[
|
|
185
|
+
TextContent(
|
|
186
|
+
data=f"Error: Invalid line position {line_position} for file with {len(current_lines)} lines.",
|
|
187
|
+
),
|
|
188
|
+
],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Insert content (convert to 0-based index)
|
|
192
|
+
insert_idx = line_position - 1
|
|
193
|
+
|
|
194
|
+
# Ensure content ends with newline for proper insertion
|
|
195
|
+
if not additional_content.endswith("\n"):
|
|
196
|
+
additional_content += "\n"
|
|
197
|
+
|
|
198
|
+
current_lines.insert(insert_idx, additional_content)
|
|
199
|
+
|
|
200
|
+
with open(target_path, "w", encoding="utf-8") as file:
|
|
201
|
+
file.writelines(current_lines)
|
|
202
|
+
|
|
203
|
+
return ExecutionResult(
|
|
204
|
+
output_blocks=[
|
|
205
|
+
TextContent(
|
|
206
|
+
data=f"Successfully inserted content at line {line_position} in {target_path}",
|
|
207
|
+
),
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
except Exception as error:
|
|
212
|
+
return ExecutionResult(
|
|
213
|
+
output_blocks=[
|
|
214
|
+
TextContent(
|
|
215
|
+
data=f"Error modifying file: {error}",
|
|
216
|
+
),
|
|
217
|
+
],
|
|
218
|
+
)
|