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
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
# ExecutionResult Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ExecutionResult` is the standardized container for all tool execution outputs in the MassGen Tool System. It provides a structured way to return results from tools, supporting text, images, audio, streaming outputs, and metadata.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
**What is ExecutionResult?**
|
|
10
|
+
Think of ExecutionResult as a package that tools send back to agents. Like a delivery package, it contains:
|
|
11
|
+
- The main content (text, images, audio)
|
|
12
|
+
- Metadata (extra information about the result)
|
|
13
|
+
- Status flags (is it streaming? is it final? was it interrupted?)
|
|
14
|
+
- A unique ID for tracking
|
|
15
|
+
|
|
16
|
+
**Why use ExecutionResult?**
|
|
17
|
+
Instead of returning raw strings or objects, ExecutionResult provides:
|
|
18
|
+
- **Type Safety**: Structured format prevents errors
|
|
19
|
+
- **Multimodal Support**: Can include text, images, and audio together
|
|
20
|
+
- **Streaming**: Support for progressive results
|
|
21
|
+
- **Metadata**: Extra information without cluttering main content
|
|
22
|
+
- **Tracking**: Unique IDs for debugging and logging
|
|
23
|
+
|
|
24
|
+
## Class Definition
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from typing import List, Optional, Union
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ExecutionResult:
|
|
33
|
+
"""Result container for tool execution outputs."""
|
|
34
|
+
|
|
35
|
+
output_blocks: List[Union[TextContent, ImageContent, AudioContent]]
|
|
36
|
+
"""The execution output blocks from the tool."""
|
|
37
|
+
|
|
38
|
+
meta_info: Optional[dict] = None
|
|
39
|
+
"""Additional metadata accessible within the system."""
|
|
40
|
+
|
|
41
|
+
is_streaming: bool = False
|
|
42
|
+
"""Indicates if the output is being streamed."""
|
|
43
|
+
|
|
44
|
+
is_final: bool = True
|
|
45
|
+
"""Indicates if this is the final result in a stream."""
|
|
46
|
+
|
|
47
|
+
was_interrupted: bool = False
|
|
48
|
+
"""Indicates if the execution was interrupted."""
|
|
49
|
+
|
|
50
|
+
result_id: str = field(default_factory=_generate_id)
|
|
51
|
+
"""Unique identifier for this result."""
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Content Block Types
|
|
55
|
+
|
|
56
|
+
### TextContent
|
|
57
|
+
|
|
58
|
+
**What it does**: Wraps text output from tools. This is the most common content type for returning strings, error messages, or formatted text.
|
|
59
|
+
|
|
60
|
+
**When to use it**: For any text-based output - results, logs, error messages, JSON strings, etc.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from massgen.tool import TextContent
|
|
64
|
+
|
|
65
|
+
# Simple text
|
|
66
|
+
text = TextContent(data="Hello, World!")
|
|
67
|
+
|
|
68
|
+
# Formatted output
|
|
69
|
+
text = TextContent(data="""
|
|
70
|
+
Results:
|
|
71
|
+
- Item 1: Success
|
|
72
|
+
- Item 2: Failed
|
|
73
|
+
- Item 3: Pending
|
|
74
|
+
""")
|
|
75
|
+
|
|
76
|
+
# JSON output
|
|
77
|
+
import json
|
|
78
|
+
data_dict = {"status": "ok", "count": 42}
|
|
79
|
+
text = TextContent(data=json.dumps(data_dict, indent=2))
|
|
80
|
+
|
|
81
|
+
# Error message
|
|
82
|
+
text = TextContent(data="Error: File not found at /path/to/file")
|
|
83
|
+
|
|
84
|
+
# Large text
|
|
85
|
+
with open("report.txt") as f:
|
|
86
|
+
text = TextContent(data=f.read())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### ImageContent
|
|
90
|
+
|
|
91
|
+
**What it does**: Wraps image data (typically base64-encoded) from tools that generate or process images.
|
|
92
|
+
|
|
93
|
+
**When to use it**: For image generation, image processing results, charts, visualizations, or any visual output.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from massgen.tool import ImageContent
|
|
97
|
+
import base64
|
|
98
|
+
|
|
99
|
+
# From file
|
|
100
|
+
with open("chart.png", "rb") as f:
|
|
101
|
+
image_data = base64.b64encode(f.read()).decode()
|
|
102
|
+
image = ImageContent(data=image_data)
|
|
103
|
+
|
|
104
|
+
# From generation API
|
|
105
|
+
# Assume dalle.generate() returns base64 image
|
|
106
|
+
image_data = await dalle.generate("A sunset over mountains")
|
|
107
|
+
image = ImageContent(data=image_data)
|
|
108
|
+
|
|
109
|
+
# Multiple images
|
|
110
|
+
images = [
|
|
111
|
+
ImageContent(data=encode_image("image1.png")),
|
|
112
|
+
ImageContent(data=encode_image("image2.png")),
|
|
113
|
+
ImageContent(data=encode_image("image3.png"))
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### AudioContent
|
|
118
|
+
|
|
119
|
+
**What it does**: Wraps audio data (typically base64-encoded) from tools that generate or process audio.
|
|
120
|
+
|
|
121
|
+
**When to use it**: For text-to-speech, audio generation, audio processing, or any audio output.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from massgen.tool import AudioContent
|
|
125
|
+
import base64
|
|
126
|
+
|
|
127
|
+
# From file
|
|
128
|
+
with open("speech.mp3", "rb") as f:
|
|
129
|
+
audio_data = base64.b64encode(f.read()).decode()
|
|
130
|
+
audio = AudioContent(data=audio_data)
|
|
131
|
+
|
|
132
|
+
# From TTS API
|
|
133
|
+
audio_data = await tts.synthesize("Hello, World!")
|
|
134
|
+
audio = AudioContent(data=audio_data)
|
|
135
|
+
|
|
136
|
+
# Podcast episode
|
|
137
|
+
with open("episode_1.mp3", "rb") as f:
|
|
138
|
+
audio = AudioContent(data=base64.b64encode(f.read()).decode())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Creating ExecutionResults
|
|
142
|
+
|
|
143
|
+
### Simple Text Result
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from massgen.tool import ExecutionResult, TextContent
|
|
147
|
+
|
|
148
|
+
# Basic success result
|
|
149
|
+
result = ExecutionResult(
|
|
150
|
+
output_blocks=[TextContent(data="Operation completed successfully")]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# With metadata
|
|
154
|
+
result = ExecutionResult(
|
|
155
|
+
output_blocks=[TextContent(data="Processed 100 items")],
|
|
156
|
+
meta_info={"processed_count": 100, "duration_ms": 1250}
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Multimodal Result
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from massgen.tool import ExecutionResult, TextContent, ImageContent
|
|
164
|
+
|
|
165
|
+
# Text + Image
|
|
166
|
+
result = ExecutionResult(
|
|
167
|
+
output_blocks=[
|
|
168
|
+
TextContent(data="Generated chart for dataset:"),
|
|
169
|
+
ImageContent(data=chart_image_base64),
|
|
170
|
+
TextContent(data="Analysis complete. See chart above.")
|
|
171
|
+
],
|
|
172
|
+
meta_info={"chart_type": "bar", "data_points": 20}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Multiple images with descriptions
|
|
176
|
+
result = ExecutionResult(
|
|
177
|
+
output_blocks=[
|
|
178
|
+
TextContent(data="Comparison results:"),
|
|
179
|
+
TextContent(data="Original image:"),
|
|
180
|
+
ImageContent(data=original_image),
|
|
181
|
+
TextContent(data="Processed image:"),
|
|
182
|
+
ImageContent(data=processed_image),
|
|
183
|
+
TextContent(data="Difference highlighted in red")
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Error Result
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# Simple error
|
|
192
|
+
error_result = ExecutionResult(
|
|
193
|
+
output_blocks=[TextContent(data="Error: File not found")]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Detailed error with metadata
|
|
197
|
+
error_result = ExecutionResult(
|
|
198
|
+
output_blocks=[
|
|
199
|
+
TextContent(data="Error: Connection timeout"),
|
|
200
|
+
TextContent(data="Failed to connect to server after 3 attempts")
|
|
201
|
+
],
|
|
202
|
+
meta_info={
|
|
203
|
+
"error_type": "ConnectionTimeout",
|
|
204
|
+
"attempts": 3,
|
|
205
|
+
"timeout_seconds": 30
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Exception formatting
|
|
210
|
+
try:
|
|
211
|
+
risky_operation()
|
|
212
|
+
except Exception as e:
|
|
213
|
+
error_result = ExecutionResult(
|
|
214
|
+
output_blocks=[
|
|
215
|
+
TextContent(data=f"Error: {type(e).__name__}"),
|
|
216
|
+
TextContent(data=f"Details: {str(e)}")
|
|
217
|
+
],
|
|
218
|
+
meta_info={"traceback": traceback.format_exc()}
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Streaming Results
|
|
223
|
+
|
|
224
|
+
### Basic Streaming
|
|
225
|
+
|
|
226
|
+
**What it does**: Allows tools to send results progressively rather than all at once. Useful for long-running operations where you want to show progress.
|
|
227
|
+
|
|
228
|
+
**Why use it**: Users see progress immediately instead of waiting for completion. Better user experience for slow operations.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from typing import AsyncGenerator
|
|
232
|
+
from massgen.tool import ExecutionResult, TextContent
|
|
233
|
+
import asyncio
|
|
234
|
+
|
|
235
|
+
async def streaming_tool() -> AsyncGenerator[ExecutionResult, None]:
|
|
236
|
+
"""Tool that streams progress updates."""
|
|
237
|
+
|
|
238
|
+
# Start
|
|
239
|
+
yield ExecutionResult(
|
|
240
|
+
output_blocks=[TextContent(data="Starting process...")],
|
|
241
|
+
is_streaming=True,
|
|
242
|
+
is_final=False
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Progress updates
|
|
246
|
+
for i in range(1, 6):
|
|
247
|
+
await asyncio.sleep(1) # Simulate work
|
|
248
|
+
yield ExecutionResult(
|
|
249
|
+
output_blocks=[TextContent(data=f"Processing step {i}/5...")],
|
|
250
|
+
is_streaming=True,
|
|
251
|
+
is_final=False
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Final result
|
|
255
|
+
yield ExecutionResult(
|
|
256
|
+
output_blocks=[TextContent(data="Process complete!")],
|
|
257
|
+
is_streaming=True,
|
|
258
|
+
is_final=True # Mark as final
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Usage
|
|
262
|
+
async for result in streaming_tool():
|
|
263
|
+
print(result.output_blocks[0].data)
|
|
264
|
+
if result.is_final:
|
|
265
|
+
print("Done!")
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Streaming with Accumulation
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
async def accumulating_stream() -> AsyncGenerator[ExecutionResult, None]:
|
|
272
|
+
"""Stream results that build on each other."""
|
|
273
|
+
|
|
274
|
+
accumulated = []
|
|
275
|
+
|
|
276
|
+
for i in range(5):
|
|
277
|
+
await asyncio.sleep(0.5)
|
|
278
|
+
accumulated.append(f"Item {i+1}")
|
|
279
|
+
|
|
280
|
+
# Send accumulated results each time
|
|
281
|
+
yield ExecutionResult(
|
|
282
|
+
output_blocks=[
|
|
283
|
+
TextContent(data=f"Found {len(accumulated)} items so far:"),
|
|
284
|
+
TextContent(data="\n".join(accumulated))
|
|
285
|
+
],
|
|
286
|
+
is_streaming=True,
|
|
287
|
+
is_final=(i == 4),
|
|
288
|
+
meta_info={"total_items": len(accumulated)}
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Streaming with Status
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
async def status_streaming() -> AsyncGenerator[ExecutionResult, None]:
|
|
296
|
+
"""Stream with different status updates."""
|
|
297
|
+
|
|
298
|
+
phases = [
|
|
299
|
+
("Initializing", 2),
|
|
300
|
+
("Processing", 3),
|
|
301
|
+
("Validating", 2),
|
|
302
|
+
("Finalizing", 1)
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
for phase_name, duration in phases:
|
|
306
|
+
yield ExecutionResult(
|
|
307
|
+
output_blocks=[TextContent(data=f"Phase: {phase_name}")],
|
|
308
|
+
is_streaming=True,
|
|
309
|
+
is_final=False,
|
|
310
|
+
meta_info={"current_phase": phase_name}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
await asyncio.sleep(duration)
|
|
314
|
+
|
|
315
|
+
# Final
|
|
316
|
+
yield ExecutionResult(
|
|
317
|
+
output_blocks=[TextContent(data="All phases complete")],
|
|
318
|
+
is_streaming=True,
|
|
319
|
+
is_final=True,
|
|
320
|
+
meta_info={"total_phases": len(phases)}
|
|
321
|
+
)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Handling Interruption
|
|
325
|
+
|
|
326
|
+
**What it does**: The `was_interrupted` flag indicates that execution was cancelled or stopped early.
|
|
327
|
+
|
|
328
|
+
**Why use it**: Allows tools to signal that they didn't complete normally, helping agents understand partial results.
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
async def interruptible_tool() -> AsyncGenerator[ExecutionResult, None]:
|
|
332
|
+
"""Tool that can be cancelled."""
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
for i in range(100):
|
|
336
|
+
await asyncio.sleep(0.1)
|
|
337
|
+
|
|
338
|
+
yield ExecutionResult(
|
|
339
|
+
output_blocks=[TextContent(data=f"Progress: {i}%")],
|
|
340
|
+
is_streaming=True,
|
|
341
|
+
is_final=False
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
except asyncio.CancelledError:
|
|
345
|
+
# Handle cancellation gracefully
|
|
346
|
+
yield ExecutionResult(
|
|
347
|
+
output_blocks=[
|
|
348
|
+
TextContent(data="<system>Operation was cancelled</system>")
|
|
349
|
+
],
|
|
350
|
+
is_streaming=True,
|
|
351
|
+
is_final=True,
|
|
352
|
+
was_interrupted=True # Mark as interrupted
|
|
353
|
+
)
|
|
354
|
+
raise # Re-raise to propagate cancellation
|
|
355
|
+
|
|
356
|
+
# Usage
|
|
357
|
+
import asyncio
|
|
358
|
+
|
|
359
|
+
async def run_with_timeout():
|
|
360
|
+
gen = interruptible_tool()
|
|
361
|
+
try:
|
|
362
|
+
async for result in gen:
|
|
363
|
+
print(result.output_blocks[0].data)
|
|
364
|
+
# Simulate timeout after 3 results
|
|
365
|
+
if "Progress: 2%" in result.output_blocks[0].data:
|
|
366
|
+
raise asyncio.TimeoutError()
|
|
367
|
+
except asyncio.TimeoutError:
|
|
368
|
+
# Generator will send interrupted result
|
|
369
|
+
pass
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Metadata Usage
|
|
373
|
+
|
|
374
|
+
**What it does**: `meta_info` stores additional data that shouldn't be shown directly to users but is useful for logging, debugging, or system processing.
|
|
375
|
+
|
|
376
|
+
**Why use it**: Separate user-facing content from system-level information. Keep output clean while preserving detailed data.
|
|
377
|
+
|
|
378
|
+
### Common Metadata Patterns
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
# Execution metrics
|
|
382
|
+
result = ExecutionResult(
|
|
383
|
+
output_blocks=[TextContent(data="Analysis complete")],
|
|
384
|
+
meta_info={
|
|
385
|
+
"execution_time_ms": 1234,
|
|
386
|
+
"memory_used_mb": 45,
|
|
387
|
+
"api_calls": 3
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Data provenance
|
|
392
|
+
result = ExecutionResult(
|
|
393
|
+
output_blocks=[TextContent(data="Data retrieved")],
|
|
394
|
+
meta_info={
|
|
395
|
+
"source": "database",
|
|
396
|
+
"query": "SELECT * FROM users",
|
|
397
|
+
"row_count": 100,
|
|
398
|
+
"timestamp": datetime.now().isoformat()
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Model information
|
|
403
|
+
result = ExecutionResult(
|
|
404
|
+
output_blocks=[ImageContent(data=image_data)],
|
|
405
|
+
meta_info={
|
|
406
|
+
"model": "dall-e-3",
|
|
407
|
+
"prompt": "a sunset",
|
|
408
|
+
"size": "1024x1024",
|
|
409
|
+
"revised_prompt": "a beautiful sunset over mountains"
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Error details
|
|
414
|
+
result = ExecutionResult(
|
|
415
|
+
output_blocks=[TextContent(data="Error: Request failed")],
|
|
416
|
+
meta_info={
|
|
417
|
+
"error_code": "TIMEOUT",
|
|
418
|
+
"retry_count": 3,
|
|
419
|
+
"last_error": "Connection timeout after 30s",
|
|
420
|
+
"failed_at": datetime.now().isoformat()
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Processing statistics
|
|
425
|
+
result = ExecutionResult(
|
|
426
|
+
output_blocks=[TextContent(data="Processed dataset")],
|
|
427
|
+
meta_info={
|
|
428
|
+
"input_rows": 1000,
|
|
429
|
+
"output_rows": 950,
|
|
430
|
+
"filtered_rows": 50,
|
|
431
|
+
"null_values_removed": 25,
|
|
432
|
+
"duplicates_removed": 25,
|
|
433
|
+
"processing_stages": ["clean", "filter", "deduplicate"]
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Result ID Tracking
|
|
439
|
+
|
|
440
|
+
**What it does**: Each ExecutionResult gets a unique ID automatically generated from timestamp and random components.
|
|
441
|
+
|
|
442
|
+
**Why use it**: Track results through the system, correlate logs, debug issues, and identify specific execution instances.
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
# Automatic ID generation
|
|
446
|
+
result1 = ExecutionResult(output_blocks=[TextContent(data="Result 1")])
|
|
447
|
+
result2 = ExecutionResult(output_blocks=[TextContent(data="Result 2")])
|
|
448
|
+
|
|
449
|
+
print(f"Result 1 ID: {result1.result_id}")
|
|
450
|
+
print(f"Result 2 ID: {result2.result_id}")
|
|
451
|
+
# Output:
|
|
452
|
+
# Result 1 ID: 20240315_143022_123456
|
|
453
|
+
# Result 2 ID: 20240315_143022_123457
|
|
454
|
+
|
|
455
|
+
# Use for logging
|
|
456
|
+
import logging
|
|
457
|
+
|
|
458
|
+
logger = logging.getLogger(__name__)
|
|
459
|
+
|
|
460
|
+
async def logged_tool() -> ExecutionResult:
|
|
461
|
+
result = ExecutionResult(
|
|
462
|
+
output_blocks=[TextContent(data="Operation complete")]
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
logger.info(f"Tool execution complete - ID: {result.result_id}")
|
|
466
|
+
logger.debug(f"Result details: {result.output_blocks[0].data}")
|
|
467
|
+
|
|
468
|
+
return result
|
|
469
|
+
|
|
470
|
+
# Use for tracking
|
|
471
|
+
execution_history = {}
|
|
472
|
+
|
|
473
|
+
async def tracked_tool() -> ExecutionResult:
|
|
474
|
+
result = ExecutionResult(
|
|
475
|
+
output_blocks=[TextContent(data="Processed")],
|
|
476
|
+
meta_info={"processed_items": 100}
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Store in history
|
|
480
|
+
execution_history[result.result_id] = {
|
|
481
|
+
"timestamp": datetime.now(),
|
|
482
|
+
"output": result.output_blocks[0].data,
|
|
483
|
+
"metadata": result.meta_info
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return result
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Complete Examples
|
|
490
|
+
|
|
491
|
+
### Example 1: Data Processing Tool
|
|
492
|
+
|
|
493
|
+
```python
|
|
494
|
+
from massgen.tool import ExecutionResult, TextContent
|
|
495
|
+
import json
|
|
496
|
+
|
|
497
|
+
async def analyze_csv(file_path: str, column: str) -> ExecutionResult:
|
|
498
|
+
"""Analyze a CSV file column.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
file_path: Path to CSV file
|
|
502
|
+
column: Column to analyze
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
ExecutionResult with statistics
|
|
506
|
+
"""
|
|
507
|
+
# Load data (simplified)
|
|
508
|
+
import pandas as pd
|
|
509
|
+
df = pd.read_csv(file_path)
|
|
510
|
+
|
|
511
|
+
# Calculate statistics
|
|
512
|
+
stats = {
|
|
513
|
+
"mean": df[column].mean(),
|
|
514
|
+
"median": df[column].median(),
|
|
515
|
+
"std": df[column].std(),
|
|
516
|
+
"min": df[column].min(),
|
|
517
|
+
"max": df[column].max()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
# Format output
|
|
521
|
+
output = f"Statistics for column '{column}':\n"
|
|
522
|
+
output += json.dumps(stats, indent=2)
|
|
523
|
+
|
|
524
|
+
return ExecutionResult(
|
|
525
|
+
output_blocks=[TextContent(data=output)],
|
|
526
|
+
meta_info={
|
|
527
|
+
"file": file_path,
|
|
528
|
+
"column": column,
|
|
529
|
+
"row_count": len(df),
|
|
530
|
+
"null_count": df[column].isnull().sum()
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Example 2: Image Generation Tool
|
|
536
|
+
|
|
537
|
+
```python
|
|
538
|
+
from massgen.tool import ExecutionResult, TextContent, ImageContent
|
|
539
|
+
import base64
|
|
540
|
+
|
|
541
|
+
async def generate_chart(
|
|
542
|
+
data: list,
|
|
543
|
+
chart_type: str = "bar"
|
|
544
|
+
) -> ExecutionResult:
|
|
545
|
+
"""Generate a chart from data.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
data: Data points for chart
|
|
549
|
+
chart_type: Type of chart (bar, line, pie)
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
ExecutionResult with chart image
|
|
553
|
+
"""
|
|
554
|
+
# Generate chart (simplified)
|
|
555
|
+
import matplotlib.pyplot as plt
|
|
556
|
+
import io
|
|
557
|
+
|
|
558
|
+
fig, ax = plt.subplots()
|
|
559
|
+
|
|
560
|
+
if chart_type == "bar":
|
|
561
|
+
ax.bar(range(len(data)), data)
|
|
562
|
+
elif chart_type == "line":
|
|
563
|
+
ax.plot(range(len(data)), data)
|
|
564
|
+
|
|
565
|
+
# Save to bytes
|
|
566
|
+
buf = io.BytesIO()
|
|
567
|
+
plt.savefig(buf, format='png')
|
|
568
|
+
buf.seek(0)
|
|
569
|
+
image_data = base64.b64encode(buf.read()).decode()
|
|
570
|
+
|
|
571
|
+
plt.close()
|
|
572
|
+
|
|
573
|
+
return ExecutionResult(
|
|
574
|
+
output_blocks=[
|
|
575
|
+
TextContent(data=f"Generated {chart_type} chart with {len(data)} points"),
|
|
576
|
+
ImageContent(data=image_data)
|
|
577
|
+
],
|
|
578
|
+
meta_info={
|
|
579
|
+
"chart_type": chart_type,
|
|
580
|
+
"data_points": len(data),
|
|
581
|
+
"min_value": min(data),
|
|
582
|
+
"max_value": max(data)
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Example 3: Streaming File Processor
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
from typing import AsyncGenerator
|
|
591
|
+
from massgen.tool import ExecutionResult, TextContent
|
|
592
|
+
import asyncio
|
|
593
|
+
|
|
594
|
+
async def process_large_file(
|
|
595
|
+
file_path: str,
|
|
596
|
+
operation: str
|
|
597
|
+
) -> AsyncGenerator[ExecutionResult, None]:
|
|
598
|
+
"""Process a large file with progress updates.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
file_path: Path to file
|
|
602
|
+
operation: Operation to perform (count, validate, transform)
|
|
603
|
+
|
|
604
|
+
Yields:
|
|
605
|
+
ExecutionResult objects with progress
|
|
606
|
+
"""
|
|
607
|
+
# Open file
|
|
608
|
+
with open(file_path, 'r') as f:
|
|
609
|
+
lines = f.readlines()
|
|
610
|
+
total = len(lines)
|
|
611
|
+
|
|
612
|
+
# Initial status
|
|
613
|
+
yield ExecutionResult(
|
|
614
|
+
output_blocks=[TextContent(data=f"Processing {total} lines...")],
|
|
615
|
+
is_streaming=True,
|
|
616
|
+
is_final=False
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Process in batches
|
|
620
|
+
batch_size = 1000
|
|
621
|
+
processed = 0
|
|
622
|
+
|
|
623
|
+
for i in range(0, total, batch_size):
|
|
624
|
+
batch = lines[i:i+batch_size]
|
|
625
|
+
|
|
626
|
+
# Simulate processing
|
|
627
|
+
await asyncio.sleep(0.1)
|
|
628
|
+
processed += len(batch)
|
|
629
|
+
|
|
630
|
+
# Progress update
|
|
631
|
+
progress = int((processed / total) * 100)
|
|
632
|
+
yield ExecutionResult(
|
|
633
|
+
output_blocks=[
|
|
634
|
+
TextContent(data=f"Progress: {processed}/{total} lines ({progress}%)")
|
|
635
|
+
],
|
|
636
|
+
is_streaming=True,
|
|
637
|
+
is_final=False,
|
|
638
|
+
meta_info={"processed_lines": processed, "total_lines": total}
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Final result
|
|
642
|
+
yield ExecutionResult(
|
|
643
|
+
output_blocks=[
|
|
644
|
+
TextContent(data=f"Processing complete! Processed {total} lines")
|
|
645
|
+
],
|
|
646
|
+
is_streaming=True,
|
|
647
|
+
is_final=True,
|
|
648
|
+
meta_info={
|
|
649
|
+
"operation": operation,
|
|
650
|
+
"total_lines": total,
|
|
651
|
+
"file_path": file_path
|
|
652
|
+
}
|
|
653
|
+
)
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
## Best Practices
|
|
657
|
+
|
|
658
|
+
### Content Organization
|
|
659
|
+
|
|
660
|
+
1. **Use Multiple Blocks**: Separate different types of content
|
|
661
|
+
2. **Clear Descriptions**: Precede images/audio with descriptive text
|
|
662
|
+
3. **Structured Output**: Use formatting for readability
|
|
663
|
+
4. **Error Context**: Include context with error messages
|
|
664
|
+
|
|
665
|
+
### Metadata Guidelines
|
|
666
|
+
|
|
667
|
+
1. **Don't Duplicate**: Don't repeat output_blocks content in meta_info
|
|
668
|
+
2. **Use for System Data**: Metrics, timestamps, internal IDs
|
|
669
|
+
3. **Keep JSON-Serializable**: Only basic types (str, int, float, bool, list, dict)
|
|
670
|
+
4. **Meaningful Keys**: Use clear, descriptive key names
|
|
671
|
+
|
|
672
|
+
### Streaming Best Practices
|
|
673
|
+
|
|
674
|
+
1. **Regular Updates**: Send updates frequently enough to show progress
|
|
675
|
+
2. **Mark Final**: Always set `is_final=True` on last result
|
|
676
|
+
3. **Handle Cancellation**: Use try/except for asyncio.CancelledError
|
|
677
|
+
4. **Meaningful Progress**: Show actual progress, not just "working..."
|
|
678
|
+
|
|
679
|
+
### Error Handling
|
|
680
|
+
|
|
681
|
+
1. **Structured Errors**: Use consistent error message format
|
|
682
|
+
2. **Include Details**: Provide context for debugging
|
|
683
|
+
3. **Use Metadata**: Store stack traces and error codes in meta_info
|
|
684
|
+
4. **User-Friendly**: Main message should be clear to non-technical users
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
For more information, see:
|
|
689
|
+
- [ToolManager Documentation](manager.md)
|
|
690
|
+
- [Built-in Tools Guide](builtin_tools.md)
|
|
691
|
+
- [Workflow Toolkits](workflow_toolkits.md)
|