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.

Files changed (111) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +31 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
  10. massgen/backend/chat_completions.py +182 -92
  11. massgen/backend/claude.py +115 -18
  12. massgen/backend/claude_code.py +378 -14
  13. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  14. massgen/backend/gemini.py +1275 -1607
  15. massgen/backend/gemini_mcp_manager.py +545 -0
  16. massgen/backend/gemini_trackers.py +344 -0
  17. massgen/backend/gemini_utils.py +43 -0
  18. massgen/backend/response.py +129 -70
  19. massgen/cli.py +577 -110
  20. massgen/config_builder.py +376 -27
  21. massgen/configs/README.md +111 -80
  22. massgen/configs/basic/multi/three_agents_default.yaml +1 -1
  23. massgen/configs/basic/single/single_agent.yaml +1 -1
  24. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  25. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  26. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  29. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  30. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  31. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  34. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  35. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  36. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  39. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  40. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  41. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  42. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  46. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  47. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  52. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  56. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  57. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  60. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  61. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  62. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  65. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  66. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  67. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  68. massgen/formatter/_chat_completions_formatter.py +104 -0
  69. massgen/formatter/_claude_formatter.py +120 -0
  70. massgen/formatter/_gemini_formatter.py +448 -0
  71. massgen/formatter/_response_formatter.py +88 -0
  72. massgen/frontend/coordination_ui.py +4 -2
  73. massgen/logger_config.py +35 -3
  74. massgen/message_templates.py +56 -6
  75. massgen/orchestrator.py +179 -10
  76. massgen/stream_chunk/base.py +3 -0
  77. massgen/tests/custom_tools_example.py +392 -0
  78. massgen/tests/mcp_test_server.py +17 -7
  79. massgen/tests/test_config_builder.py +423 -0
  80. massgen/tests/test_custom_tools.py +401 -0
  81. massgen/tests/test_tools.py +127 -0
  82. massgen/tool/README.md +935 -0
  83. massgen/tool/__init__.py +39 -0
  84. massgen/tool/_async_helpers.py +70 -0
  85. massgen/tool/_basic/__init__.py +8 -0
  86. massgen/tool/_basic/_two_num_tool.py +24 -0
  87. massgen/tool/_code_executors/__init__.py +10 -0
  88. massgen/tool/_code_executors/_python_executor.py +74 -0
  89. massgen/tool/_code_executors/_shell_executor.py +61 -0
  90. massgen/tool/_exceptions.py +39 -0
  91. massgen/tool/_file_handlers/__init__.py +10 -0
  92. massgen/tool/_file_handlers/_file_operations.py +218 -0
  93. massgen/tool/_manager.py +634 -0
  94. massgen/tool/_registered_tool.py +88 -0
  95. massgen/tool/_result.py +66 -0
  96. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  97. massgen/tool/docs/builtin_tools.md +681 -0
  98. massgen/tool/docs/exceptions.md +794 -0
  99. massgen/tool/docs/execution_results.md +691 -0
  100. massgen/tool/docs/manager.md +887 -0
  101. massgen/tool/docs/workflow_toolkits.md +529 -0
  102. massgen/tool/workflow_toolkits/__init__.py +57 -0
  103. massgen/tool/workflow_toolkits/base.py +55 -0
  104. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  105. massgen/tool/workflow_toolkits/vote.py +167 -0
  106. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Example script demonstrating custom tools usage with ResponseBackend.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Add parent directory to path
14
+ sys.path.insert(0, str(Path(__file__).parent.parent))
15
+
16
+ from massgen.backend.response import ResponseBackend # noqa: E402
17
+ from massgen.tool import ExecutionResult # noqa: E402
18
+ from massgen.tool._result import TextContent # noqa: E402
19
+
20
+ # ============================================================================
21
+ # Define custom tool functions
22
+ # ============================================================================
23
+
24
+
25
+ def calculator(operation: str, x: float, y: float) -> ExecutionResult:
26
+ """
27
+ Perform basic math operations.
28
+
29
+ Args:
30
+ operation: The operation to perform (add, subtract, multiply, divide)
31
+ x: First number
32
+ y: Second number
33
+
34
+ Returns:
35
+ ExecutionResult with the calculation result
36
+ """
37
+ operations = {
38
+ "add": lambda a, b: a + b,
39
+ "subtract": lambda a, b: a - b,
40
+ "multiply": lambda a, b: a * b,
41
+ "divide": lambda a, b: a / b if b != 0 else "Cannot divide by zero",
42
+ }
43
+
44
+ if operation in operations:
45
+ result = operations[operation](x, y)
46
+ return ExecutionResult(
47
+ output_blocks=[
48
+ TextContent(data=f"{operation}({x}, {y}) = {result}"),
49
+ ],
50
+ )
51
+ else:
52
+ return ExecutionResult(
53
+ output_blocks=[
54
+ TextContent(data=f"Unknown operation: {operation}"),
55
+ ],
56
+ )
57
+
58
+
59
+ def text_analyzer(text: str, analysis_type: str = "basic") -> ExecutionResult:
60
+ """
61
+ Analyze text and return statistics.
62
+
63
+ Args:
64
+ text: The text to analyze
65
+ analysis_type: Type of analysis (basic, detailed)
66
+
67
+ Returns:
68
+ ExecutionResult with text analysis
69
+ """
70
+ word_count = len(text.split())
71
+ char_count = len(text)
72
+ line_count = len(text.splitlines())
73
+
74
+ if analysis_type == "basic":
75
+ result = f"Words: {word_count}, Characters: {char_count}"
76
+ else:
77
+ unique_words = len(set(text.lower().split()))
78
+ result = f"Words: {word_count}, Unique words: {unique_words}, " f"Characters: {char_count}, Lines: {line_count}"
79
+
80
+ return ExecutionResult(
81
+ output_blocks=[TextContent(data=result)],
82
+ )
83
+
84
+
85
+ async def async_data_processor(data_type: str, count: int = 10) -> ExecutionResult:
86
+ """
87
+ Async function to simulate data processing.
88
+
89
+ Args:
90
+ data_type: Type of data to generate (numbers, strings)
91
+ count: Number of items to generate
92
+
93
+ Returns:
94
+ ExecutionResult with generated data
95
+ """
96
+ await asyncio.sleep(0.5) # Simulate processing time
97
+
98
+ if data_type == "numbers":
99
+ data = list(range(1, min(count + 1, 100)))
100
+ elif data_type == "strings":
101
+ data = [f"Item_{i}" for i in range(1, min(count + 1, 100))]
102
+ else:
103
+ data = ["Unknown data type"]
104
+
105
+ return ExecutionResult(
106
+ output_blocks=[
107
+ TextContent(data=f"Generated {len(data)} {data_type}: {data[:5]}..."),
108
+ ],
109
+ )
110
+
111
+
112
+ # ============================================================================
113
+ # Demo functions
114
+ # ============================================================================
115
+
116
+
117
+ async def demo_basic_usage():
118
+ """Demonstrate basic custom tools usage."""
119
+ print("=" * 60)
120
+ print("BASIC CUSTOM TOOLS DEMO")
121
+ print("=" * 60)
122
+
123
+ # Create ResponseBackend with custom tools
124
+ backend = ResponseBackend(
125
+ api_key=os.getenv("OPENAI_API_KEY", "test-key"),
126
+ custom_tools=[
127
+ {
128
+ "func": calculator,
129
+ "description": "Perform mathematical calculations",
130
+ "category": "math",
131
+ },
132
+ {
133
+ "func": text_analyzer,
134
+ "description": "Analyze text content",
135
+ "category": "text",
136
+ },
137
+ {
138
+ "func": async_data_processor,
139
+ "description": "Process data asynchronously",
140
+ "category": "data",
141
+ },
142
+ ],
143
+ )
144
+
145
+ print(f"\nRegistered {len(backend._custom_tool_names)} custom tools:")
146
+ for tool_name in backend._custom_tool_names:
147
+ print(f" - {tool_name}")
148
+
149
+ # Get tool schemas
150
+ schemas = backend._get_custom_tools_schemas()
151
+ print(f"\nGenerated {len(schemas)} tool schemas")
152
+
153
+ # Test tool execution
154
+ print("\n" + "-" * 40)
155
+ print("Testing tool execution:")
156
+ print("-" * 40)
157
+
158
+ # Test calculator
159
+ calc_input = {
160
+ "operation": "multiply",
161
+ "x": 7,
162
+ "y": 9,
163
+ }
164
+ call = {
165
+ "name": "calculator",
166
+ "call_id": "calc_1",
167
+ "arguments": json.dumps(calc_input),
168
+ }
169
+ print(f"\nCalculator input: {calc_input}")
170
+ result = await backend._execute_custom_tool(call)
171
+ print(f"Calculator result: {result}")
172
+
173
+ # Test text analyzer
174
+ text_input = {
175
+ "text": "This is a sample text for analysis. It has multiple words and sentences.",
176
+ "analysis_type": "detailed",
177
+ }
178
+ call = {
179
+ "name": "text_analyzer",
180
+ "call_id": "text_1",
181
+ "arguments": json.dumps(text_input),
182
+ }
183
+ print(f"\nText analyzer input: {text_input}")
184
+ result = await backend._execute_custom_tool(call)
185
+ print(f"Text analyzer result: {result}")
186
+
187
+ # Test async data processor
188
+ data_input = {
189
+ "data_type": "numbers",
190
+ "count": 20,
191
+ }
192
+ call = {
193
+ "name": "async_data_processor",
194
+ "call_id": "data_1",
195
+ "arguments": json.dumps(data_input),
196
+ }
197
+ print(f"\nData processor input: {data_input}")
198
+ result = await backend._execute_custom_tool(call)
199
+ print(f"Data processor result: {result}")
200
+
201
+
202
+ async def demo_with_presets():
203
+ """Demonstrate custom tools with preset arguments."""
204
+ print("\n" + "=" * 60)
205
+ print("CUSTOM TOOLS WITH PRESET ARGUMENTS")
206
+ print("=" * 60)
207
+
208
+ backend = ResponseBackend(
209
+ api_key="test-key",
210
+ custom_tools=[
211
+ {
212
+ "func": calculator,
213
+ "description": "Addition calculator",
214
+ "preset_args": {"operation": "add"}, # Preset the operation
215
+ "category": "math",
216
+ },
217
+ {
218
+ "func": text_analyzer,
219
+ "description": "Detailed text analyzer",
220
+ "preset_args": {"analysis_type": "detailed"}, # Always detailed
221
+ "category": "text",
222
+ },
223
+ ],
224
+ )
225
+
226
+ # Now calculator only needs x and y
227
+ calc_preset_input = {"x": 15, "y": 25} # operation is preset
228
+ call = {
229
+ "name": "calculator",
230
+ "call_id": "calc_2",
231
+ "arguments": json.dumps(calc_preset_input),
232
+ }
233
+ print(f"\nAddition calculator input (operation preset to 'add'): {calc_preset_input}")
234
+ result = await backend._execute_custom_tool(call)
235
+ print(f"Addition calculator result: {result}")
236
+
237
+ # Text analyzer only needs text
238
+ text_preset_input = {"text": "Short text"} # analysis_type is preset
239
+ call = {
240
+ "name": "text_analyzer",
241
+ "call_id": "text_2",
242
+ "arguments": json.dumps(text_preset_input),
243
+ }
244
+ print(f"\nDetailed analyzer input (analysis_type preset to 'detailed'): {text_preset_input}")
245
+ result = await backend._execute_custom_tool(call)
246
+ print(f"Detailed analyzer result: {result}")
247
+
248
+
249
+ async def demo_from_file():
250
+ """Demonstrate loading custom tools from Python files."""
251
+ print("\n" + "=" * 60)
252
+ print("LOADING CUSTOM TOOLS FROM FILES")
253
+ print("=" * 60)
254
+
255
+ # Create a temporary Python file with custom functions
256
+ custom_file = Path(__file__).parent / "my_custom_tools.py"
257
+ custom_file.write_text(
258
+ '''
259
+ def word_counter(text: str) -> str:
260
+ """Count words in text."""
261
+ from massgen.tool import ExecutionResult
262
+ from massgen.tool._result import TextContent
263
+
264
+ word_count = len(text.split())
265
+ return ExecutionResult(
266
+ output_blocks=[TextContent(data=f"Word count: {word_count}")]
267
+ )
268
+
269
+ def list_processor(items: list, action: str = "join") -> str:
270
+ """Process a list of items."""
271
+ from massgen.tool import ExecutionResult
272
+ from massgen.tool._result import TextContent
273
+
274
+ if action == "join":
275
+ result = ", ".join(str(item) for item in items)
276
+ elif action == "reverse":
277
+ result = str(items[::-1])
278
+ elif action == "sort":
279
+ result = str(sorted(items))
280
+ else:
281
+ result = str(items)
282
+
283
+ return ExecutionResult(
284
+ output_blocks=[TextContent(data=f"Result: {result}")]
285
+ )
286
+ ''',
287
+ )
288
+
289
+ try:
290
+ backend = ResponseBackend(
291
+ api_key="test-key",
292
+ custom_tools=[
293
+ {
294
+ "path": str(custom_file),
295
+ "func": "word_counter", # Specific function
296
+ "description": "Count words in text",
297
+ },
298
+ {
299
+ "path": str(custom_file),
300
+ "func": "list_processor", # Another function from same file
301
+ "description": "Process lists",
302
+ },
303
+ ],
304
+ )
305
+
306
+ print(f"\nLoaded tools from {custom_file.name}:")
307
+ for tool_name in backend._custom_tool_names:
308
+ print(f" - {tool_name}")
309
+
310
+ # Test word counter
311
+ word_input = {"text": "This is a test sentence with seven words"}
312
+ call = {
313
+ "name": "word_counter",
314
+ "call_id": "wc_1",
315
+ "arguments": json.dumps(word_input),
316
+ }
317
+ print(f"\nWord counter input: {word_input}")
318
+ result = await backend._execute_custom_tool(call)
319
+ print(f"Word counter result: {result}")
320
+
321
+ # Test list processor
322
+ list_input = {
323
+ "items": [3, 1, 4, 1, 5, 9, 2, 6],
324
+ "action": "sort",
325
+ }
326
+ call = {
327
+ "name": "list_processor",
328
+ "call_id": "lp_1",
329
+ "arguments": json.dumps(list_input),
330
+ }
331
+ print(f"\nList processor input: {list_input}")
332
+ result = await backend._execute_custom_tool(call)
333
+ print(f"List processor result: {result}")
334
+
335
+ finally:
336
+ # Cleanup
337
+ if custom_file.exists():
338
+ custom_file.unlink()
339
+ print(f"\nCleaned up temporary file: {custom_file}")
340
+
341
+
342
+ async def demo_builtin_tools():
343
+ """Demonstrate using built-in tools from the tool module."""
344
+ print("\n" + "=" * 60)
345
+ print("USING BUILT-IN TOOLS")
346
+ print("=" * 60)
347
+
348
+ backend = ResponseBackend(
349
+ api_key="test-key",
350
+ custom_tools=[
351
+ {
352
+ "func": "read_file_content", # Built-in tool name
353
+ "description": "Read file content",
354
+ },
355
+ ],
356
+ )
357
+
358
+ # This might fail if the built-in function isn't found
359
+ # but demonstrates the concept
360
+ if backend._custom_tool_names:
361
+ print(f"\nFound built-in tools: {backend._custom_tool_names}")
362
+ else:
363
+ print("\nNote: Built-in tools require the tool module to be properly set up")
364
+
365
+
366
+ # ============================================================================
367
+ # Main execution
368
+ # ============================================================================
369
+
370
+
371
+ async def main():
372
+ """Run all demos."""
373
+ try:
374
+ await demo_basic_usage()
375
+ await demo_with_presets()
376
+ await demo_from_file()
377
+ await demo_builtin_tools()
378
+
379
+ print("\n" + "=" * 60)
380
+ print("ALL DEMOS COMPLETED SUCCESSFULLY!")
381
+ print("=" * 60)
382
+
383
+ except Exception as e:
384
+ print(f"\nError during demo: {e}")
385
+ import traceback
386
+
387
+ traceback.print_exc()
388
+
389
+
390
+ if __name__ == "__main__":
391
+ # Run the async main function
392
+ asyncio.run(main())
@@ -10,7 +10,7 @@ It implements the MCP protocol over stdio transport.
10
10
  import asyncio
11
11
  import json
12
12
  import sys
13
- from typing import Any, Dict
13
+ from typing import Any, Dict, Optional
14
14
 
15
15
 
16
16
  class SimpleMCPServer:
@@ -86,12 +86,21 @@ class SimpleMCPServer:
86
86
  else:
87
87
  raise ValueError(f"Unknown tool: {tool_name}")
88
88
 
89
- async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
90
- """Handle incoming JSON-RPC request."""
89
+ async def handle_request(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
90
+ """Handle incoming JSON-RPC request or notification."""
91
91
  method = request.get("method")
92
92
  params = request.get("params", {})
93
93
  request_id = request.get("id")
94
94
 
95
+ # If no id, this is a notification - don't send response
96
+ if request_id is None:
97
+ # Handle notifications silently (no response needed)
98
+ if method == "notifications/initialized":
99
+ # Client has finished initialization - no action needed
100
+ pass
101
+ # Add other notification handlers here if needed
102
+ return None
103
+
95
104
  try:
96
105
  if method == "initialize":
97
106
  result = await self.handle_initialize(params)
@@ -126,10 +135,11 @@ class SimpleMCPServer:
126
135
  # Handle request
127
136
  response = await self.handle_request(request)
128
137
 
129
- # Send response to stdout
130
- json.dump(response, sys.stdout)
131
- sys.stdout.write("\n")
132
- sys.stdout.flush()
138
+ # Send response to stdout (only if not a notification)
139
+ if response is not None:
140
+ json.dump(response, sys.stdout)
141
+ sys.stdout.write("\n")
142
+ sys.stdout.flush()
133
143
 
134
144
  except KeyboardInterrupt:
135
145
  break