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.
- iac_code/__init__.py +2 -0
- iac_code/acp/__init__.py +97 -0
- iac_code/acp/convert.py +423 -0
- iac_code/acp/http_sse.py +448 -0
- iac_code/acp/mcp.py +54 -0
- iac_code/acp/metrics.py +71 -0
- iac_code/acp/server.py +662 -0
- iac_code/acp/session.py +446 -0
- iac_code/acp/slash_registry.py +125 -0
- iac_code/acp/state.py +99 -0
- iac_code/acp/tools.py +112 -0
- iac_code/acp/types.py +13 -0
- iac_code/acp/version.py +26 -0
- iac_code/agent/__init__.py +19 -0
- iac_code/agent/agent_loop.py +640 -0
- iac_code/agent/agent_tool.py +269 -0
- iac_code/agent/agent_types.py +87 -0
- iac_code/agent/message.py +153 -0
- iac_code/agent/system_prompt.py +313 -0
- iac_code/cli/__init__.py +3 -0
- iac_code/cli/headless.py +114 -0
- iac_code/cli/main.py +246 -0
- iac_code/cli/output_formats.py +125 -0
- iac_code/commands/__init__.py +93 -0
- iac_code/commands/auth.py +1055 -0
- iac_code/commands/clear.py +34 -0
- iac_code/commands/compact.py +43 -0
- iac_code/commands/debug.py +45 -0
- iac_code/commands/effort.py +116 -0
- iac_code/commands/exit.py +10 -0
- iac_code/commands/help.py +49 -0
- iac_code/commands/model.py +130 -0
- iac_code/commands/registry.py +245 -0
- iac_code/commands/resume.py +49 -0
- iac_code/commands/tasks.py +41 -0
- iac_code/config.py +304 -0
- iac_code/i18n/__init__.py +141 -0
- iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
- iac_code/memory/__init__.py +1 -0
- iac_code/memory/memory_manager.py +92 -0
- iac_code/memory/memory_tools.py +88 -0
- iac_code/providers/__init__.py +1 -0
- iac_code/providers/anthropic_provider.py +284 -0
- iac_code/providers/base.py +128 -0
- iac_code/providers/dashscope_provider.py +47 -0
- iac_code/providers/deepseek_provider.py +36 -0
- iac_code/providers/manager.py +399 -0
- iac_code/providers/openai_provider.py +344 -0
- iac_code/providers/retry.py +58 -0
- iac_code/providers/stream_watchdog.py +47 -0
- iac_code/providers/thinking.py +164 -0
- iac_code/services/__init__.py +1 -0
- iac_code/services/agent_factory.py +127 -0
- iac_code/services/cloud_credentials.py +22 -0
- iac_code/services/context_manager.py +221 -0
- iac_code/services/providers/__init__.py +1 -0
- iac_code/services/providers/aliyun.py +232 -0
- iac_code/services/session_index.py +281 -0
- iac_code/services/session_storage.py +245 -0
- iac_code/services/telemetry/__init__.py +66 -0
- iac_code/services/telemetry/attributes.py +84 -0
- iac_code/services/telemetry/client.py +330 -0
- iac_code/services/telemetry/config.py +76 -0
- iac_code/services/telemetry/constants.py +75 -0
- iac_code/services/telemetry/content_serializer.py +124 -0
- iac_code/services/telemetry/events.py +42 -0
- iac_code/services/telemetry/fallback.py +59 -0
- iac_code/services/telemetry/identity.py +73 -0
- iac_code/services/telemetry/metrics.py +62 -0
- iac_code/services/telemetry/names.py +199 -0
- iac_code/services/telemetry/sanitize.py +88 -0
- iac_code/services/telemetry/sink.py +67 -0
- iac_code/services/telemetry/tracing.py +38 -0
- iac_code/services/telemetry/types.py +13 -0
- iac_code/services/token_budget.py +54 -0
- iac_code/services/token_counter.py +76 -0
- iac_code/skills/__init__.py +1 -0
- iac_code/skills/bundled/__init__.py +94 -0
- iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
- iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
- iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
- iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
- iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
- iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
- iac_code/skills/bundled/simplify.py +28 -0
- iac_code/skills/discovery.py +136 -0
- iac_code/skills/frontmatter.py +119 -0
- iac_code/skills/listing.py +92 -0
- iac_code/skills/loader.py +42 -0
- iac_code/skills/processor.py +81 -0
- iac_code/skills/renderer.py +157 -0
- iac_code/skills/skill_definition.py +82 -0
- iac_code/skills/skill_tool.py +261 -0
- iac_code/state/__init__.py +5 -0
- iac_code/state/app_state.py +122 -0
- iac_code/tasks/__init__.py +1 -0
- iac_code/tasks/notification_queue.py +28 -0
- iac_code/tasks/task_state.py +66 -0
- iac_code/tasks/task_tools.py +114 -0
- iac_code/tools/__init__.py +8 -0
- iac_code/tools/base.py +226 -0
- iac_code/tools/bash.py +133 -0
- iac_code/tools/cloud/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
- iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
- iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
- iac_code/tools/cloud/aliyun/ros_client.py +56 -0
- iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
- iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
- iac_code/tools/cloud/base_api.py +162 -0
- iac_code/tools/cloud/base_stack.py +242 -0
- iac_code/tools/cloud/registry.py +20 -0
- iac_code/tools/cloud/types.py +105 -0
- iac_code/tools/edit_file.py +121 -0
- iac_code/tools/glob.py +103 -0
- iac_code/tools/grep.py +254 -0
- iac_code/tools/list_files.py +104 -0
- iac_code/tools/read_file.py +127 -0
- iac_code/tools/result_storage.py +39 -0
- iac_code/tools/tool_executor.py +165 -0
- iac_code/tools/web_fetch.py +177 -0
- iac_code/tools/write_file.py +88 -0
- iac_code/types/__init__.py +40 -0
- iac_code/types/permissions.py +26 -0
- iac_code/types/skill_source.py +11 -0
- iac_code/types/stream_events.py +227 -0
- iac_code/ui/__init__.py +5 -0
- iac_code/ui/banner.py +110 -0
- iac_code/ui/components/__init__.py +0 -0
- iac_code/ui/components/dialog.py +142 -0
- iac_code/ui/components/divider.py +20 -0
- iac_code/ui/components/fuzzy_picker.py +308 -0
- iac_code/ui/components/progress_bar.py +54 -0
- iac_code/ui/components/search_box.py +165 -0
- iac_code/ui/components/select.py +319 -0
- iac_code/ui/components/status_icon.py +42 -0
- iac_code/ui/components/tabs.py +128 -0
- iac_code/ui/core/__init__.py +0 -0
- iac_code/ui/core/in_place_render.py +129 -0
- iac_code/ui/core/input_history.py +118 -0
- iac_code/ui/core/key_event.py +41 -0
- iac_code/ui/core/prompt_input.py +507 -0
- iac_code/ui/core/raw_input.py +302 -0
- iac_code/ui/core/screen.py +80 -0
- iac_code/ui/dialogs/__init__.py +0 -0
- iac_code/ui/dialogs/global_search.py +178 -0
- iac_code/ui/dialogs/history_search.py +100 -0
- iac_code/ui/dialogs/model_picker.py +280 -0
- iac_code/ui/dialogs/quick_open.py +108 -0
- iac_code/ui/dialogs/resume_picker.py +749 -0
- iac_code/ui/keybindings/__init__.py +0 -0
- iac_code/ui/keybindings/manager.py +124 -0
- iac_code/ui/renderer.py +1535 -0
- iac_code/ui/repl.py +772 -0
- iac_code/ui/spinner.py +112 -0
- iac_code/ui/suggestions/__init__.py +0 -0
- iac_code/ui/suggestions/aggregator.py +171 -0
- iac_code/ui/suggestions/command_provider.py +43 -0
- iac_code/ui/suggestions/directory_provider.py +95 -0
- iac_code/ui/suggestions/file_provider.py +121 -0
- iac_code/ui/suggestions/shell_history_provider.py +108 -0
- iac_code/ui/suggestions/token_extractor.py +77 -0
- iac_code/ui/suggestions/types.py +45 -0
- iac_code/ui/transcript_view.py +199 -0
- iac_code/utils/__init__.py +0 -0
- iac_code/utils/background_housekeeping.py +53 -0
- iac_code/utils/cleanup.py +68 -0
- iac_code/utils/json_utils.py +60 -0
- iac_code/utils/log.py +150 -0
- iac_code/utils/project_paths.py +74 -0
- iac_code/utils/tool_input_parser.py +62 -0
- iac_code-0.1.0.dist-info/LICENSE +201 -0
- iac_code-0.1.0.dist-info/METADATA +64 -0
- iac_code-0.1.0.dist-info/RECORD +184 -0
- iac_code-0.1.0.dist-info/WHEEL +5 -0
- iac_code-0.1.0.dist-info/entry_points.txt +2 -0
- 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())
|