iac-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. iac_code/__init__.py +2 -0
  2. iac_code/acp/__init__.py +97 -0
  3. iac_code/acp/convert.py +423 -0
  4. iac_code/acp/http_sse.py +448 -0
  5. iac_code/acp/mcp.py +54 -0
  6. iac_code/acp/metrics.py +71 -0
  7. iac_code/acp/server.py +662 -0
  8. iac_code/acp/session.py +446 -0
  9. iac_code/acp/slash_registry.py +125 -0
  10. iac_code/acp/state.py +99 -0
  11. iac_code/acp/tools.py +112 -0
  12. iac_code/acp/types.py +13 -0
  13. iac_code/acp/version.py +26 -0
  14. iac_code/agent/__init__.py +19 -0
  15. iac_code/agent/agent_loop.py +640 -0
  16. iac_code/agent/agent_tool.py +269 -0
  17. iac_code/agent/agent_types.py +87 -0
  18. iac_code/agent/message.py +153 -0
  19. iac_code/agent/system_prompt.py +313 -0
  20. iac_code/cli/__init__.py +3 -0
  21. iac_code/cli/headless.py +114 -0
  22. iac_code/cli/main.py +246 -0
  23. iac_code/cli/output_formats.py +125 -0
  24. iac_code/commands/__init__.py +93 -0
  25. iac_code/commands/auth.py +1055 -0
  26. iac_code/commands/clear.py +34 -0
  27. iac_code/commands/compact.py +43 -0
  28. iac_code/commands/debug.py +45 -0
  29. iac_code/commands/effort.py +116 -0
  30. iac_code/commands/exit.py +10 -0
  31. iac_code/commands/help.py +49 -0
  32. iac_code/commands/model.py +130 -0
  33. iac_code/commands/registry.py +245 -0
  34. iac_code/commands/resume.py +49 -0
  35. iac_code/commands/tasks.py +41 -0
  36. iac_code/config.py +304 -0
  37. iac_code/i18n/__init__.py +141 -0
  38. iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
  39. iac_code/memory/__init__.py +1 -0
  40. iac_code/memory/memory_manager.py +92 -0
  41. iac_code/memory/memory_tools.py +88 -0
  42. iac_code/providers/__init__.py +1 -0
  43. iac_code/providers/anthropic_provider.py +284 -0
  44. iac_code/providers/base.py +128 -0
  45. iac_code/providers/dashscope_provider.py +47 -0
  46. iac_code/providers/deepseek_provider.py +36 -0
  47. iac_code/providers/manager.py +399 -0
  48. iac_code/providers/openai_provider.py +344 -0
  49. iac_code/providers/retry.py +58 -0
  50. iac_code/providers/stream_watchdog.py +47 -0
  51. iac_code/providers/thinking.py +164 -0
  52. iac_code/services/__init__.py +1 -0
  53. iac_code/services/agent_factory.py +127 -0
  54. iac_code/services/cloud_credentials.py +22 -0
  55. iac_code/services/context_manager.py +221 -0
  56. iac_code/services/providers/__init__.py +1 -0
  57. iac_code/services/providers/aliyun.py +232 -0
  58. iac_code/services/session_index.py +281 -0
  59. iac_code/services/session_storage.py +245 -0
  60. iac_code/services/telemetry/__init__.py +66 -0
  61. iac_code/services/telemetry/attributes.py +84 -0
  62. iac_code/services/telemetry/client.py +330 -0
  63. iac_code/services/telemetry/config.py +76 -0
  64. iac_code/services/telemetry/constants.py +75 -0
  65. iac_code/services/telemetry/content_serializer.py +124 -0
  66. iac_code/services/telemetry/events.py +42 -0
  67. iac_code/services/telemetry/fallback.py +59 -0
  68. iac_code/services/telemetry/identity.py +73 -0
  69. iac_code/services/telemetry/metrics.py +62 -0
  70. iac_code/services/telemetry/names.py +199 -0
  71. iac_code/services/telemetry/sanitize.py +88 -0
  72. iac_code/services/telemetry/sink.py +67 -0
  73. iac_code/services/telemetry/tracing.py +38 -0
  74. iac_code/services/telemetry/types.py +13 -0
  75. iac_code/services/token_budget.py +54 -0
  76. iac_code/services/token_counter.py +76 -0
  77. iac_code/skills/__init__.py +1 -0
  78. iac_code/skills/bundled/__init__.py +94 -0
  79. iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
  80. iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
  81. iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
  82. iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
  83. iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
  84. iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
  85. iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
  86. iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
  87. iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
  88. iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
  89. iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
  90. iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
  91. iac_code/skills/bundled/simplify.py +28 -0
  92. iac_code/skills/discovery.py +136 -0
  93. iac_code/skills/frontmatter.py +119 -0
  94. iac_code/skills/listing.py +92 -0
  95. iac_code/skills/loader.py +42 -0
  96. iac_code/skills/processor.py +81 -0
  97. iac_code/skills/renderer.py +157 -0
  98. iac_code/skills/skill_definition.py +82 -0
  99. iac_code/skills/skill_tool.py +261 -0
  100. iac_code/state/__init__.py +5 -0
  101. iac_code/state/app_state.py +122 -0
  102. iac_code/tasks/__init__.py +1 -0
  103. iac_code/tasks/notification_queue.py +28 -0
  104. iac_code/tasks/task_state.py +66 -0
  105. iac_code/tasks/task_tools.py +114 -0
  106. iac_code/tools/__init__.py +8 -0
  107. iac_code/tools/base.py +226 -0
  108. iac_code/tools/bash.py +133 -0
  109. iac_code/tools/cloud/__init__.py +0 -0
  110. iac_code/tools/cloud/aliyun/__init__.py +0 -0
  111. iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
  112. iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
  113. iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
  114. iac_code/tools/cloud/aliyun/ros_client.py +56 -0
  115. iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
  116. iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
  117. iac_code/tools/cloud/base_api.py +162 -0
  118. iac_code/tools/cloud/base_stack.py +242 -0
  119. iac_code/tools/cloud/registry.py +20 -0
  120. iac_code/tools/cloud/types.py +105 -0
  121. iac_code/tools/edit_file.py +121 -0
  122. iac_code/tools/glob.py +103 -0
  123. iac_code/tools/grep.py +254 -0
  124. iac_code/tools/list_files.py +104 -0
  125. iac_code/tools/read_file.py +127 -0
  126. iac_code/tools/result_storage.py +39 -0
  127. iac_code/tools/tool_executor.py +165 -0
  128. iac_code/tools/web_fetch.py +177 -0
  129. iac_code/tools/write_file.py +88 -0
  130. iac_code/types/__init__.py +40 -0
  131. iac_code/types/permissions.py +26 -0
  132. iac_code/types/skill_source.py +11 -0
  133. iac_code/types/stream_events.py +227 -0
  134. iac_code/ui/__init__.py +5 -0
  135. iac_code/ui/banner.py +110 -0
  136. iac_code/ui/components/__init__.py +0 -0
  137. iac_code/ui/components/dialog.py +142 -0
  138. iac_code/ui/components/divider.py +20 -0
  139. iac_code/ui/components/fuzzy_picker.py +308 -0
  140. iac_code/ui/components/progress_bar.py +54 -0
  141. iac_code/ui/components/search_box.py +165 -0
  142. iac_code/ui/components/select.py +319 -0
  143. iac_code/ui/components/status_icon.py +42 -0
  144. iac_code/ui/components/tabs.py +128 -0
  145. iac_code/ui/core/__init__.py +0 -0
  146. iac_code/ui/core/in_place_render.py +129 -0
  147. iac_code/ui/core/input_history.py +118 -0
  148. iac_code/ui/core/key_event.py +41 -0
  149. iac_code/ui/core/prompt_input.py +507 -0
  150. iac_code/ui/core/raw_input.py +302 -0
  151. iac_code/ui/core/screen.py +80 -0
  152. iac_code/ui/dialogs/__init__.py +0 -0
  153. iac_code/ui/dialogs/global_search.py +178 -0
  154. iac_code/ui/dialogs/history_search.py +100 -0
  155. iac_code/ui/dialogs/model_picker.py +280 -0
  156. iac_code/ui/dialogs/quick_open.py +108 -0
  157. iac_code/ui/dialogs/resume_picker.py +749 -0
  158. iac_code/ui/keybindings/__init__.py +0 -0
  159. iac_code/ui/keybindings/manager.py +124 -0
  160. iac_code/ui/renderer.py +1535 -0
  161. iac_code/ui/repl.py +772 -0
  162. iac_code/ui/spinner.py +112 -0
  163. iac_code/ui/suggestions/__init__.py +0 -0
  164. iac_code/ui/suggestions/aggregator.py +171 -0
  165. iac_code/ui/suggestions/command_provider.py +43 -0
  166. iac_code/ui/suggestions/directory_provider.py +95 -0
  167. iac_code/ui/suggestions/file_provider.py +121 -0
  168. iac_code/ui/suggestions/shell_history_provider.py +108 -0
  169. iac_code/ui/suggestions/token_extractor.py +77 -0
  170. iac_code/ui/suggestions/types.py +45 -0
  171. iac_code/ui/transcript_view.py +199 -0
  172. iac_code/utils/__init__.py +0 -0
  173. iac_code/utils/background_housekeeping.py +53 -0
  174. iac_code/utils/cleanup.py +68 -0
  175. iac_code/utils/json_utils.py +60 -0
  176. iac_code/utils/log.py +150 -0
  177. iac_code/utils/project_paths.py +74 -0
  178. iac_code/utils/tool_input_parser.py +62 -0
  179. iac_code-0.1.0.dist-info/LICENSE +201 -0
  180. iac_code-0.1.0.dist-info/METADATA +64 -0
  181. iac_code-0.1.0.dist-info/RECORD +184 -0
  182. iac_code-0.1.0.dist-info/WHEEL +5 -0
  183. iac_code-0.1.0.dist-info/entry_points.txt +2 -0
  184. iac_code-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,247 @@
1
+ """ROS StackGroup Instances tool for Alibaba Cloud Resource Orchestration Service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import time
8
+ from typing import Any
9
+
10
+ from alibabacloud_ros20190910 import models as ros_models
11
+
12
+ from iac_code.i18n import _
13
+ from iac_code.services.cloud_credentials import CloudCredentials
14
+ from iac_code.tools.base import Tool, ToolContext, ToolResult
15
+ from iac_code.tools.cloud.aliyun.ros_client import RosClientFactory
16
+ from iac_code.tools.cloud.types import InstanceStatus
17
+ from iac_code.types.stream_events import StackInstancesProgressEvent
18
+
19
+ SUPPORTED_ACTIONS = [
20
+ "CreateStackInstances",
21
+ "UpdateStackInstances",
22
+ "DeleteStackInstances",
23
+ ]
24
+
25
+ TERMINAL_STATUSES = {"SUCCEEDED", "FAILED", "STOPPED"}
26
+ DONE_STATUSES = {"SUCCEEDED", "CURRENT", "FAILED", "STOPPED"}
27
+
28
+
29
+ class RosStackInstances(Tool):
30
+ """Alibaba Cloud ROS StackGroup instances lifecycle tool.
31
+
32
+ Manages creating, updating, and deleting stack instances within a stack group,
33
+ with real-time progress polling via operation ID.
34
+ """
35
+
36
+ poll_interval: int = 5
37
+
38
+ @property
39
+ def timeout(self) -> float | None:
40
+ """Stack instance operations may run for a long time; default to 1 hour."""
41
+ return 3600.0
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ return "ros_stack_instances"
46
+
47
+ @property
48
+ def supported_actions(self) -> list[str]:
49
+ return SUPPORTED_ACTIONS
50
+
51
+ @property
52
+ def description(self) -> str:
53
+ return (
54
+ "Manage Alibaba Cloud ROS (Resource Orchestration Service) StackGroup instances lifecycle. "
55
+ "Supports creating, updating, and deleting stack instances with "
56
+ "real-time progress polling via operation ID."
57
+ )
58
+
59
+ def _get_default_region(self) -> str:
60
+ credentials = CloudCredentials()
61
+ cred = credentials.get_provider("aliyun")
62
+ return cred.region_id if cred else ""
63
+
64
+ @property
65
+ def input_schema(self) -> dict[str, Any]:
66
+ region_desc = "The region to perform the action in."
67
+ default_region = self._get_default_region()
68
+ if default_region:
69
+ region_desc += f" Defaults to '{default_region}'."
70
+ return {
71
+ "type": "object",
72
+ "properties": {
73
+ "action": {
74
+ "type": "string",
75
+ "enum": self.supported_actions,
76
+ "description": "The stack instances lifecycle action to perform.",
77
+ },
78
+ "params": {
79
+ "type": "object",
80
+ "description": "Parameters to pass to the action.",
81
+ },
82
+ "region_id": {
83
+ "type": "string",
84
+ "description": region_desc,
85
+ },
86
+ },
87
+ "required": ["action"],
88
+ }
89
+
90
+ def is_read_only(self, input: dict | None = None) -> bool:
91
+ return False
92
+
93
+ def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool:
94
+ return False
95
+
96
+ def is_destructive(self, input: dict | None = None) -> bool:
97
+ return True
98
+
99
+ def user_facing_name(self, input: dict | None = None) -> str:
100
+ return _("CloudStackInstances")
101
+
102
+ def _resolve_region(self, input: dict) -> str:
103
+ return input.get("region_id") or self._get_default_region()
104
+
105
+ def render_tool_use_message(self, input: dict, *, verbose: bool = False) -> str | None:
106
+ action = input.get("action", "")
107
+ region = self._resolve_region(input)
108
+ parts = [p for p in [action, region] if p]
109
+ return " ".join(parts) if parts else None
110
+
111
+ def get_activity_description(self, input: dict | None = None) -> str | None:
112
+ if input is None:
113
+ return None
114
+ action = input.get("action", "")
115
+ region = self._resolve_region(input)
116
+ display = f"{action} {region}" if region else action
117
+ return _("Running {action}...").format(action=display)
118
+
119
+ def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None:
120
+ if verbose:
121
+ return output.strip()
122
+ try:
123
+ data = json.loads(output)
124
+ except (json.JSONDecodeError, TypeError):
125
+ return output.strip()[:200]
126
+ name = data.get("stack_group_name", "")
127
+ status = data.get("status", "")
128
+ elapsed = data.get("elapsed_seconds", 0)
129
+ return f"{name} {status} ({elapsed}s)"
130
+
131
+ def _get_client(self, region: str) -> Any:
132
+ credentials = CloudCredentials()
133
+ cred = credentials.get_provider("aliyun")
134
+ return RosClientFactory.create(cred, region_id=region)
135
+
136
+ async def _initiate(self, client: Any, action: str, params: dict) -> str:
137
+ """Start the stack instances operation and return the operation_id."""
138
+ if action == "CreateStackInstances":
139
+ request = ros_models.CreateStackInstancesRequest().from_map(params)
140
+ response = client.create_stack_instances(request)
141
+ return response.body.operation_id
142
+ elif action == "UpdateStackInstances":
143
+ request = ros_models.UpdateStackInstancesRequest().from_map(params)
144
+ response = client.update_stack_instances(request)
145
+ return response.body.operation_id
146
+ elif action == "DeleteStackInstances":
147
+ request = ros_models.DeleteStackInstancesRequest().from_map(params)
148
+ response = client.delete_stack_instances(request)
149
+ return response.body.operation_id
150
+ raise ValueError(f"Unsupported action: {action}")
151
+
152
+ async def _get_operation_status(self, client: Any, operation_id: str, region: str) -> str:
153
+ """Poll the current status of a stack group operation."""
154
+ request = ros_models.GetStackGroupOperationRequest(operation_id=operation_id, region_id=region)
155
+ response = client.get_stack_group_operation(request)
156
+ return response.body.to_map().get("Status", "RUNNING")
157
+
158
+ async def _get_instances(self, client: Any, stack_group_name: str, region: str) -> list[InstanceStatus]:
159
+ """Get the current list of stack instances for a stack group."""
160
+ request = ros_models.ListStackInstancesRequest(stack_group_name=stack_group_name, region_id=region)
161
+ response = client.list_stack_instances(request)
162
+ data = response.body.to_map()
163
+ instances = []
164
+ for item in data.get("StackInstances", []):
165
+ instances.append(
166
+ InstanceStatus(
167
+ account_id=item.get("AccountId", ""),
168
+ region_id=item.get("RegionId", ""),
169
+ status=item.get("Status", ""),
170
+ status_reason=item.get("StatusReason", ""),
171
+ elapsed_seconds=item.get("ElapsedSeconds", 0),
172
+ )
173
+ )
174
+ return instances
175
+
176
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
177
+ action = tool_input.get("action", "")
178
+ if action not in self.supported_actions:
179
+ return ToolResult.error(f"Invalid action '{action}'. Supported actions: {self.supported_actions}")
180
+
181
+ params = tool_input.get("params") or {}
182
+ region = self._resolve_region(tool_input)
183
+ stack_group_name = params.get("StackGroupName", "")
184
+
185
+ client = self._get_client(region)
186
+
187
+ try:
188
+ operation_id = await self._initiate(client, action, params)
189
+ except Exception as e:
190
+ return ToolResult.error(f"[{action}] {e}")
191
+
192
+ start_time = time.monotonic()
193
+
194
+ while True:
195
+ await asyncio.sleep(self.__class__.poll_interval)
196
+
197
+ try:
198
+ status = await self._get_operation_status(client, operation_id, region)
199
+ except Exception as e:
200
+ return ToolResult.error(f"[GetStackGroupOperation] {e}")
201
+
202
+ try:
203
+ instances = await self._get_instances(client, stack_group_name, region)
204
+ except Exception as e:
205
+ return ToolResult.error(f"[ListStackInstances] {e}")
206
+
207
+ elapsed = int(time.monotonic() - start_time)
208
+
209
+ # Calculate progress percentage based on done instances
210
+ total_count = len(instances)
211
+ done_count = sum(1 for inst in instances if inst.status in DONE_STATUSES)
212
+ progress_percentage = int(done_count / total_count * 100) if total_count > 0 else 0
213
+
214
+ if context.event_queue is not None:
215
+ event = StackInstancesProgressEvent(
216
+ stack_group_name=stack_group_name,
217
+ operation_id=operation_id,
218
+ status=status,
219
+ progress_percentage=progress_percentage,
220
+ instances=[
221
+ {
222
+ "account_id": inst.account_id,
223
+ "region_id": inst.region_id,
224
+ "status": inst.status,
225
+ "status_reason": inst.status_reason,
226
+ "elapsed_seconds": inst.elapsed_seconds,
227
+ }
228
+ for inst in instances
229
+ ],
230
+ elapsed_seconds=elapsed,
231
+ )
232
+ await context.event_queue.put(event)
233
+
234
+ if status in TERMINAL_STATUSES:
235
+ is_success = status == "SUCCEEDED"
236
+ result_data = {
237
+ "stack_group_name": stack_group_name,
238
+ "operation_id": operation_id,
239
+ "status": status,
240
+ "progress_percentage": progress_percentage,
241
+ "elapsed_seconds": elapsed,
242
+ "is_success": is_success,
243
+ }
244
+ if is_success:
245
+ return ToolResult.success(json.dumps(result_data, ensure_ascii=False, indent=2))
246
+ else:
247
+ return ToolResult.error(json.dumps(result_data, ensure_ascii=False, indent=2))
@@ -0,0 +1,162 @@
1
+ """Abstract base class for cloud provider API tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from abc import abstractmethod
7
+ from typing import Any
8
+
9
+ from iac_code.i18n import _
10
+ from iac_code.tools.base import Tool, ToolContext, ToolResult
11
+
12
+
13
+ class BaseCloudApi(Tool):
14
+ """Abstract base class for cloud provider API tools.
15
+
16
+ Subclasses must implement:
17
+ - provider_name: Identifies the cloud provider (e.g. "ros", "aws")
18
+ - supported_actions: List of valid API action names
19
+ - call_action: Executes the actual API call
20
+ """
21
+
22
+ @property
23
+ @abstractmethod
24
+ def provider_name(self) -> str:
25
+ """The cloud provider name (e.g. 'ros')."""
26
+ ...
27
+
28
+ @property
29
+ @abstractmethod
30
+ def supported_actions(self) -> list[str]:
31
+ """List of supported API action names."""
32
+ ...
33
+
34
+ @abstractmethod
35
+ async def call_action(self, action: str, params: dict, region: str) -> dict:
36
+ """Execute a cloud API action.
37
+
38
+ Args:
39
+ action: The action name to call.
40
+ params: Parameters for the action.
41
+ region: The region to call the action in.
42
+
43
+ Returns:
44
+ The response dict from the cloud API.
45
+ """
46
+ ...
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return f"{self.provider_name}_api"
51
+
52
+ def _get_default_region(self) -> str:
53
+ """Return the configured default region, or empty string if unknown."""
54
+ return ""
55
+
56
+ @property
57
+ def input_schema(self) -> dict[str, Any]:
58
+ region_desc = "The region to call the action in."
59
+ default_region = self._get_default_region()
60
+ if default_region:
61
+ region_desc += f" Defaults to '{default_region}'."
62
+ return {
63
+ "type": "object",
64
+ "properties": {
65
+ "action": {
66
+ "type": "string",
67
+ "enum": self.supported_actions,
68
+ "description": "The API action to call.",
69
+ },
70
+ "params": {
71
+ "type": "object",
72
+ "description": "Parameters to pass to the action.",
73
+ },
74
+ "region_id": {
75
+ "type": "string",
76
+ "description": region_desc,
77
+ },
78
+ },
79
+ "required": ["action"],
80
+ }
81
+
82
+ def is_read_only(self, input: dict | None = None) -> bool:
83
+ if input is None:
84
+ return False
85
+ action = input.get("action", "")
86
+ return action.startswith(("Get", "List", "Describe", "Query", "Validate"))
87
+
88
+ def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool:
89
+ return self.is_read_only(tool_input)
90
+
91
+ def user_facing_name(self, input: dict | None = None) -> str:
92
+ return _("CloudAPI")
93
+
94
+ def _resolve_region(self, input: dict) -> str:
95
+ return input.get("region_id") or self._get_default_region()
96
+
97
+ def _get_action_display_detail(self, input: dict) -> str:
98
+ """Return the key detail to display alongside the action name.
99
+
100
+ Defaults to region. Subclasses can override for action-specific display.
101
+ """
102
+ return self._resolve_region(input)
103
+
104
+ def render_tool_use_message(self, input: dict, *, verbose: bool = False) -> str | None:
105
+ action = input.get("action", "")
106
+ detail = self._get_action_display_detail(input)
107
+ parts = [p for p in [action, detail] if p]
108
+ return " ".join(parts) if parts else None
109
+
110
+ def get_activity_description(self, input: dict | None = None) -> str | None:
111
+ if input is None:
112
+ return None
113
+ action = input.get("action", "")
114
+ detail = self._get_action_display_detail(input)
115
+ display = f"{action} {detail}" if detail else action
116
+ return _("Calling {action}...").format(action=display)
117
+
118
+ def _summarize_success_result(self, action: str, result: dict) -> str:
119
+ """Generate a smart summary of a successful API result.
120
+
121
+ Subclasses can override for provider-specific logic.
122
+ """
123
+ return _("Call succeeded")
124
+
125
+ @staticmethod
126
+ def _clean_error_message(msg: str) -> str:
127
+ """Strip raw API response data from error messages."""
128
+ # Remove trailing " Response: {...}" from SDK exception strings
129
+ idx = msg.find(" Response: {")
130
+ if idx > 0:
131
+ msg = msg[:idx]
132
+ return msg.strip()
133
+
134
+ def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None:
135
+ if is_error:
136
+ return self._clean_error_message(output)
137
+ if verbose:
138
+ return output.strip()
139
+ action = getattr(self, "_last_action", "")
140
+ result = getattr(self, "_last_result", None)
141
+ if action and result is not None:
142
+ return self._summarize_success_result(action, result)
143
+ lines = output.strip().splitlines()
144
+ return _("Received response ({count} lines)").format(count=len(lines))
145
+
146
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
147
+ action = tool_input.get("action", "")
148
+ if action not in self.supported_actions:
149
+ return ToolResult.error(f"Invalid action '{action}'. Supported actions: {self.supported_actions}")
150
+
151
+ params = tool_input.get("params") or {}
152
+ region = self._resolve_region(tool_input)
153
+
154
+ try:
155
+ result = await self.call_action(action, params, region)
156
+ self._last_action = action
157
+ self._last_result = result
158
+ return ToolResult.success(json.dumps(result, ensure_ascii=False, indent=2))
159
+ except Exception as e:
160
+ self._last_action = ""
161
+ self._last_result = None
162
+ return ToolResult.error(str(e))
@@ -0,0 +1,242 @@
1
+ """Abstract base class for cloud provider stack lifecycle tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import time
8
+ from abc import abstractmethod
9
+ from typing import Any
10
+
11
+ from iac_code.i18n import _
12
+ from iac_code.tools.base import Tool, ToolContext, ToolResult
13
+ from iac_code.tools.cloud.types import ResourceStatus, StackStatus, translate_status
14
+ from iac_code.types.stream_events import StackProgressEvent
15
+
16
+ POLL_INTERVAL = 5
17
+
18
+
19
+ class BaseCloudStack(Tool):
20
+ """Abstract base class for cloud provider stack lifecycle tools.
21
+
22
+ Subclasses must implement:
23
+ - provider_name: Identifies the cloud provider (e.g. "ros")
24
+ - supported_actions: List of valid stack action names
25
+ - call_action: Starts the stack operation and returns the stack_id
26
+ - get_stack_status: Polls the current status of a stack
27
+ - get_stack_resources: Gets the current resource list for a stack
28
+ """
29
+
30
+ poll_interval: int = POLL_INTERVAL
31
+
32
+ @property
33
+ def timeout(self) -> float | None:
34
+ """Stack operations may run for a long time; default to 1 hour."""
35
+ return 3600.0
36
+
37
+ @property
38
+ @abstractmethod
39
+ def provider_name(self) -> str:
40
+ """The cloud provider name (e.g. 'ros')."""
41
+ ...
42
+
43
+ @property
44
+ @abstractmethod
45
+ def supported_actions(self) -> list[str]:
46
+ """List of supported stack action names."""
47
+ ...
48
+
49
+ @abstractmethod
50
+ async def call_action(self, action: str, params: dict, region: str) -> str:
51
+ """Start a stack operation and return the stack_id.
52
+
53
+ Args:
54
+ action: The action name to call.
55
+ params: Parameters for the action.
56
+ region: The region to perform the operation in.
57
+
58
+ Returns:
59
+ The stack_id for the created/modified/deleted stack.
60
+ """
61
+ ...
62
+
63
+ @abstractmethod
64
+ async def get_stack_status(self, stack_id: str, region: str) -> StackStatus:
65
+ """Poll the current status of a stack.
66
+
67
+ Args:
68
+ stack_id: The stack identifier.
69
+ region: The region the stack is in.
70
+
71
+ Returns:
72
+ Current StackStatus.
73
+ """
74
+ ...
75
+
76
+ @abstractmethod
77
+ async def get_stack_resources(self, stack_id: str, region: str) -> list[ResourceStatus]:
78
+ """Get the current resource list for a stack.
79
+
80
+ Args:
81
+ stack_id: The stack identifier.
82
+ region: The region the stack is in.
83
+
84
+ Returns:
85
+ List of ResourceStatus objects.
86
+ """
87
+ ...
88
+
89
+ @property
90
+ def name(self) -> str:
91
+ return f"{self.provider_name}_stack"
92
+
93
+ def _get_default_region(self) -> str:
94
+ """Return the configured default region, or empty string if unknown."""
95
+ return ""
96
+
97
+ @property
98
+ def input_schema(self) -> dict[str, Any]:
99
+ region_desc = "The region to perform the action in."
100
+ default_region = self._get_default_region()
101
+ if default_region:
102
+ region_desc += f" Defaults to '{default_region}'."
103
+ return {
104
+ "type": "object",
105
+ "properties": {
106
+ "action": {
107
+ "type": "string",
108
+ "enum": self.supported_actions,
109
+ "description": "The stack lifecycle action to perform.",
110
+ },
111
+ "params": {
112
+ "type": "object",
113
+ "description": "Parameters to pass to the action.",
114
+ },
115
+ "region_id": {
116
+ "type": "string",
117
+ "description": region_desc,
118
+ },
119
+ },
120
+ "required": ["action"],
121
+ }
122
+
123
+ def is_read_only(self, input: dict | None = None) -> bool:
124
+ return False
125
+
126
+ def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool:
127
+ return False
128
+
129
+ def is_destructive(self, input: dict | None = None) -> bool:
130
+ return True
131
+
132
+ def user_facing_name(self, input: dict | None = None) -> str:
133
+ return _("CloudStack")
134
+
135
+ def _resolve_region(self, input: dict) -> str:
136
+ return input.get("region_id") or self._get_default_region()
137
+
138
+ def render_tool_use_message(self, input: dict, *, verbose: bool = False) -> str | None:
139
+ action = input.get("action", "")
140
+ region = self._resolve_region(input)
141
+ parts = [p for p in [action, region] if p]
142
+ return " ".join(parts) if parts else None
143
+
144
+ def get_activity_description(self, input: dict | None = None) -> str | None:
145
+ if input is None:
146
+ return None
147
+ action = input.get("action", "")
148
+ region = self._resolve_region(input)
149
+ display = f"{action} {region}" if region else action
150
+ return _("Running {action}...").format(action=display)
151
+
152
+ @staticmethod
153
+ def _clean_error_message(msg: str) -> str:
154
+ """Strip raw API response data from error messages."""
155
+ idx = msg.find(" Response: {")
156
+ if idx > 0:
157
+ msg = msg[:idx]
158
+ return msg.strip()
159
+
160
+ def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None:
161
+ if verbose:
162
+ return output.strip()
163
+ try:
164
+ data = json.loads(output)
165
+ except (json.JSONDecodeError, TypeError):
166
+ if is_error:
167
+ return self._clean_error_message(output)
168
+ return output.strip()[:200]
169
+ name = data.get("stack_name", "")
170
+ stack_id = data.get("stack_id", "")
171
+ status = translate_status(data.get("status", ""))
172
+ elapsed = data.get("elapsed_seconds", 0)
173
+ label = f"{name}({stack_id})" if stack_id else name
174
+ return f"{label} {status} ({elapsed}s)"
175
+
176
+ async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
177
+ action = tool_input.get("action", "")
178
+ if action not in self.supported_actions:
179
+ return ToolResult.error(f"Invalid action '{action}'. Supported actions: {self.supported_actions}")
180
+
181
+ params = tool_input.get("params") or {}
182
+ region = self._resolve_region(tool_input)
183
+
184
+ try:
185
+ stack_id = await self.call_action(action, params, region)
186
+ except Exception as e:
187
+ return ToolResult.error(f"[{action}] {e}")
188
+
189
+ start_time = time.monotonic()
190
+
191
+ while True:
192
+ await asyncio.sleep(self._poll_interval)
193
+
194
+ try:
195
+ status = await self.get_stack_status(stack_id, region)
196
+ except Exception as e:
197
+ return ToolResult.error(f"[GetStackStatus] {e}")
198
+
199
+ try:
200
+ resources = await self.get_stack_resources(stack_id, region)
201
+ except Exception as e:
202
+ return ToolResult.error(f"[GetStackResources] {e}")
203
+
204
+ elapsed = int(time.monotonic() - start_time)
205
+
206
+ if context.event_queue is not None:
207
+ event = StackProgressEvent(
208
+ stack_id=status.stack_id,
209
+ stack_name=status.stack_name,
210
+ status=status.status,
211
+ progress_percentage=status.progress_percentage,
212
+ resources=[
213
+ {
214
+ "name": r.name,
215
+ "resource_type": r.resource_type,
216
+ "status": r.status,
217
+ "status_reason": r.status_reason,
218
+ }
219
+ for r in resources
220
+ ],
221
+ elapsed_seconds=elapsed,
222
+ )
223
+ await context.event_queue.put(event)
224
+
225
+ if status.is_terminal:
226
+ result_data = {
227
+ "stack_id": status.stack_id,
228
+ "stack_name": status.stack_name,
229
+ "status": status.status,
230
+ "status_reason": status.status_reason,
231
+ "progress_percentage": status.progress_percentage,
232
+ "elapsed_seconds": elapsed,
233
+ "is_success": status.is_success,
234
+ }
235
+ if status.is_success:
236
+ return ToolResult.success(json.dumps(result_data, ensure_ascii=False, indent=2))
237
+ else:
238
+ return ToolResult.error(json.dumps(result_data, ensure_ascii=False, indent=2))
239
+
240
+ @property
241
+ def _poll_interval(self) -> int:
242
+ return self.__class__.poll_interval
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from iac_code.services.cloud_credentials import CloudCredentials
7
+ from iac_code.tools.base import ToolRegistry
8
+
9
+
10
+ def register_cloud_tools(registry: "ToolRegistry", credentials: "CloudCredentials") -> None:
11
+ if credentials.has_provider("aliyun"):
12
+ from iac_code.tools.cloud.aliyun.aliyun_api import AliyunApi
13
+ from iac_code.tools.cloud.aliyun.aliyun_doc_search import AliyunDocSearch
14
+ from iac_code.tools.cloud.aliyun.ros_stack import RosStack
15
+ from iac_code.tools.cloud.aliyun.ros_stack_instances import RosStackInstances
16
+
17
+ registry.register(AliyunApi())
18
+ registry.register(AliyunDocSearch())
19
+ registry.register(RosStack())
20
+ registry.register(RosStackInstances())