omnimancer-cli 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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Complete File Modification Workflow with User Approval.
|
|
3
|
+
|
|
4
|
+
This module provides the end-to-end workflow for file modifications,
|
|
5
|
+
integrating all UI components and approval mechanisms into a cohesive system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.progress import Progress
|
|
17
|
+
|
|
18
|
+
from ...cli.approval_formatter import CLIApprovalFormatter
|
|
19
|
+
from ...cli.approval_prompt import (
|
|
20
|
+
ApprovalDecision,
|
|
21
|
+
ApprovalDecisionType,
|
|
22
|
+
CLIApprovalPrompt,
|
|
23
|
+
)
|
|
24
|
+
from ..security.approval_workflow import (
|
|
25
|
+
RiskLevel,
|
|
26
|
+
)
|
|
27
|
+
from .approval_manager import EnhancedApprovalManager
|
|
28
|
+
from .file_content_display import (
|
|
29
|
+
FileDisplayConfig,
|
|
30
|
+
UnifiedFileContentDisplay,
|
|
31
|
+
)
|
|
32
|
+
from .file_system_manager import FileSystemManager
|
|
33
|
+
from .proposed_changes_integration import (
|
|
34
|
+
ChangeDisplayMode,
|
|
35
|
+
ChangeSet,
|
|
36
|
+
ProposedChangesIntegration,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WorkflowState(Enum):
|
|
43
|
+
"""States of the file modification workflow."""
|
|
44
|
+
|
|
45
|
+
INITIALIZED = "initialized"
|
|
46
|
+
DISPLAYING_CHANGES = "displaying_changes"
|
|
47
|
+
AWAITING_APPROVAL = "awaiting_approval"
|
|
48
|
+
PROCESSING_APPROVAL = "processing_approval"
|
|
49
|
+
APPLYING_CHANGES = "applying_changes"
|
|
50
|
+
COMPLETED = "completed"
|
|
51
|
+
CANCELLED = "cancelled"
|
|
52
|
+
ERROR = "error"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WorkflowResult(Enum):
|
|
56
|
+
"""Possible results of the workflow."""
|
|
57
|
+
|
|
58
|
+
APPROVED_AND_APPLIED = "approved_and_applied"
|
|
59
|
+
APPROVED_NOT_APPLIED = "approved_not_applied"
|
|
60
|
+
DENIED = "denied"
|
|
61
|
+
CANCELLED = "cancelled"
|
|
62
|
+
ERROR = "error"
|
|
63
|
+
TIMEOUT = "timeout"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class WorkflowConfig:
|
|
68
|
+
"""Configuration for the file modification workflow."""
|
|
69
|
+
|
|
70
|
+
# Display settings
|
|
71
|
+
default_display_mode: ChangeDisplayMode = ChangeDisplayMode.UNIFIED
|
|
72
|
+
show_file_stats: bool = True
|
|
73
|
+
syntax_highlighting: bool = True
|
|
74
|
+
|
|
75
|
+
# Approval settings
|
|
76
|
+
require_confirmation: bool = True
|
|
77
|
+
auto_apply_approved: bool = True
|
|
78
|
+
remember_decisions: bool = True
|
|
79
|
+
approval_timeout_seconds: int = 300 # 5 minutes
|
|
80
|
+
|
|
81
|
+
# Risk thresholds
|
|
82
|
+
auto_approve_risk_threshold: RiskLevel = RiskLevel.LOW
|
|
83
|
+
require_explicit_approval: RiskLevel = RiskLevel.HIGH
|
|
84
|
+
|
|
85
|
+
# Batch settings
|
|
86
|
+
batch_approval_threshold: int = 5 # Show batch interface for 5+ changes
|
|
87
|
+
allow_selective_approval: bool = True
|
|
88
|
+
|
|
89
|
+
# Performance settings
|
|
90
|
+
max_concurrent_operations: int = 5 # Maximum concurrent file operations
|
|
91
|
+
|
|
92
|
+
# Persistence
|
|
93
|
+
save_workflow_history: bool = True
|
|
94
|
+
history_retention_days: int = 30
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class WorkflowContext:
|
|
99
|
+
"""Context information for the workflow."""
|
|
100
|
+
|
|
101
|
+
operation_id: str
|
|
102
|
+
operation_type: str
|
|
103
|
+
initiated_by: str = "user"
|
|
104
|
+
initiated_at: datetime = field(default_factory=datetime.now)
|
|
105
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
106
|
+
|
|
107
|
+
# State tracking
|
|
108
|
+
current_state: WorkflowState = WorkflowState.INITIALIZED
|
|
109
|
+
state_history: List[Dict[str, Any]] = field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
# Results
|
|
112
|
+
final_result: Optional[WorkflowResult] = None
|
|
113
|
+
user_decisions: List[ApprovalDecision] = field(default_factory=list)
|
|
114
|
+
applied_changes: List[str] = field(default_factory=list)
|
|
115
|
+
failed_changes: List[tuple] = field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class FileModificationWorkflow:
|
|
119
|
+
"""
|
|
120
|
+
Complete file modification workflow with integrated user approval.
|
|
121
|
+
|
|
122
|
+
Orchestrates the entire process from displaying changes to applying
|
|
123
|
+
approved modifications, with comprehensive error handling and state management.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
file_system_manager: Optional[FileSystemManager] = None,
|
|
129
|
+
approval_manager: Optional[EnhancedApprovalManager] = None,
|
|
130
|
+
console: Optional[Console] = None,
|
|
131
|
+
config: Optional[WorkflowConfig] = None,
|
|
132
|
+
):
|
|
133
|
+
"""
|
|
134
|
+
Initialize the file modification workflow.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
file_system_manager: File system manager instance
|
|
138
|
+
approval_manager: Approval manager instance
|
|
139
|
+
console: Rich console for output
|
|
140
|
+
config: Workflow configuration
|
|
141
|
+
"""
|
|
142
|
+
self.console = console or Console()
|
|
143
|
+
self.config = config or WorkflowConfig()
|
|
144
|
+
self.file_system_manager = file_system_manager or FileSystemManager()
|
|
145
|
+
self.approval_manager = approval_manager or EnhancedApprovalManager()
|
|
146
|
+
|
|
147
|
+
# Initialize UI components
|
|
148
|
+
self.unified_display = UnifiedFileContentDisplay(
|
|
149
|
+
console=self.console,
|
|
150
|
+
config=FileDisplayConfig(
|
|
151
|
+
syntax_highlighting=self.config.syntax_highlighting,
|
|
152
|
+
show_file_stats=self.config.show_file_stats,
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.changes_integration = ProposedChangesIntegration(
|
|
157
|
+
file_system_manager=self.file_system_manager,
|
|
158
|
+
approval_manager=self.approval_manager,
|
|
159
|
+
console=self.console,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.approval_prompt = CLIApprovalPrompt(
|
|
163
|
+
console=self.console,
|
|
164
|
+
default_timeout_seconds=self.config.approval_timeout_seconds,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.approval_formatter = CLIApprovalFormatter(console=self.console)
|
|
168
|
+
|
|
169
|
+
# Workflow tracking
|
|
170
|
+
self.active_workflows: Dict[str, WorkflowContext] = {}
|
|
171
|
+
self.workflow_history: List[WorkflowContext] = []
|
|
172
|
+
|
|
173
|
+
# Callbacks
|
|
174
|
+
self.on_state_change: Optional[Callable] = None
|
|
175
|
+
self.on_approval_decision: Optional[Callable] = None
|
|
176
|
+
self.on_workflow_complete: Optional[Callable] = None
|
|
177
|
+
|
|
178
|
+
async def execute_file_modification_workflow(
|
|
179
|
+
self,
|
|
180
|
+
operation_id: str,
|
|
181
|
+
file_paths: Optional[List[str]] = None,
|
|
182
|
+
context_metadata: Optional[Dict[str, Any]] = None,
|
|
183
|
+
) -> WorkflowContext:
|
|
184
|
+
"""
|
|
185
|
+
Execute the complete file modification workflow.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
operation_id: ID of the operation to process
|
|
189
|
+
file_paths: Optional list of specific files to process
|
|
190
|
+
context_metadata: Additional context information
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
WorkflowContext containing the complete workflow state and results
|
|
194
|
+
"""
|
|
195
|
+
# Initialize workflow context
|
|
196
|
+
workflow_context = WorkflowContext(
|
|
197
|
+
operation_id=operation_id,
|
|
198
|
+
operation_type="file_modification",
|
|
199
|
+
metadata=context_metadata or {},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self.active_workflows[operation_id] = workflow_context
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Step 1: Fetch proposed changes
|
|
206
|
+
await self._transition_state(
|
|
207
|
+
workflow_context, WorkflowState.DISPLAYING_CHANGES
|
|
208
|
+
)
|
|
209
|
+
change_set = await self.changes_integration.fetch_proposed_changes(
|
|
210
|
+
operation_id, file_paths
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if not change_set.changes:
|
|
214
|
+
workflow_context.final_result = WorkflowResult.CANCELLED
|
|
215
|
+
await self._transition_state(workflow_context, WorkflowState.COMPLETED)
|
|
216
|
+
self.console.print("[yellow]No changes found to process.[/yellow]")
|
|
217
|
+
return workflow_context
|
|
218
|
+
|
|
219
|
+
# Step 2: Display changes and get approval
|
|
220
|
+
await self._transition_state(
|
|
221
|
+
workflow_context, WorkflowState.AWAITING_APPROVAL
|
|
222
|
+
)
|
|
223
|
+
approval_result = await self._handle_change_approval(
|
|
224
|
+
workflow_context, change_set
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if not approval_result.get("approved", False):
|
|
228
|
+
workflow_context.final_result = WorkflowResult.DENIED
|
|
229
|
+
await self._transition_state(workflow_context, WorkflowState.COMPLETED)
|
|
230
|
+
return workflow_context
|
|
231
|
+
|
|
232
|
+
# Step 3: Apply approved changes
|
|
233
|
+
if self.config.auto_apply_approved:
|
|
234
|
+
await self._transition_state(
|
|
235
|
+
workflow_context, WorkflowState.APPLYING_CHANGES
|
|
236
|
+
)
|
|
237
|
+
apply_result = await self._apply_approved_changes(
|
|
238
|
+
workflow_context, change_set, approval_result
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if apply_result["success"]:
|
|
242
|
+
workflow_context.final_result = WorkflowResult.APPROVED_AND_APPLIED
|
|
243
|
+
else:
|
|
244
|
+
workflow_context.final_result = WorkflowResult.APPROVED_NOT_APPLIED
|
|
245
|
+
else:
|
|
246
|
+
workflow_context.final_result = WorkflowResult.APPROVED_NOT_APPLIED
|
|
247
|
+
|
|
248
|
+
await self._transition_state(workflow_context, WorkflowState.COMPLETED)
|
|
249
|
+
|
|
250
|
+
except asyncio.TimeoutError:
|
|
251
|
+
workflow_context.final_result = WorkflowResult.TIMEOUT
|
|
252
|
+
await self._transition_state(workflow_context, WorkflowState.ERROR)
|
|
253
|
+
self.console.print("[red]Workflow timed out waiting for user input.[/red]")
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"Error in workflow {operation_id}: {e}")
|
|
257
|
+
workflow_context.final_result = WorkflowResult.ERROR
|
|
258
|
+
await self._transition_state(workflow_context, WorkflowState.ERROR)
|
|
259
|
+
self.console.print(f"[red]Workflow error: {str(e)}[/red]")
|
|
260
|
+
|
|
261
|
+
finally:
|
|
262
|
+
# Clean up and store history
|
|
263
|
+
await self._finalize_workflow(workflow_context)
|
|
264
|
+
|
|
265
|
+
return workflow_context
|
|
266
|
+
|
|
267
|
+
async def execute_workflow(self, changeset: ChangeSet) -> WorkflowContext:
|
|
268
|
+
"""
|
|
269
|
+
Execute workflow for a given ChangeSet.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
changeset: The ChangeSet to process
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
WorkflowContext containing the complete workflow state and results
|
|
276
|
+
"""
|
|
277
|
+
operation_id = changeset.id
|
|
278
|
+
|
|
279
|
+
# Initialize workflow context
|
|
280
|
+
workflow_context = WorkflowContext(
|
|
281
|
+
operation_id=operation_id,
|
|
282
|
+
operation_type="file_modification",
|
|
283
|
+
metadata={},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
self.active_workflows[operation_id] = workflow_context
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
# Step 1: Display changes (already have them in changeset)
|
|
290
|
+
await self._transition_state(
|
|
291
|
+
workflow_context, WorkflowState.DISPLAYING_CHANGES
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Step 2: Handle approval
|
|
295
|
+
await self._transition_state(
|
|
296
|
+
workflow_context, WorkflowState.AWAITING_APPROVAL
|
|
297
|
+
)
|
|
298
|
+
approval_result = await self._handle_change_approval(
|
|
299
|
+
workflow_context, changeset
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if not approval_result.get("approved", False):
|
|
303
|
+
workflow_context.current_state = WorkflowState.CANCELLED
|
|
304
|
+
return workflow_context
|
|
305
|
+
|
|
306
|
+
# Step 3: Apply changes
|
|
307
|
+
await self._transition_state(
|
|
308
|
+
workflow_context, WorkflowState.APPLYING_CHANGES
|
|
309
|
+
)
|
|
310
|
+
apply_result = await self._apply_approved_changes(
|
|
311
|
+
workflow_context, changeset, approval_result
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Step 4: Complete
|
|
315
|
+
if apply_result.get("success", False):
|
|
316
|
+
await self._transition_state(workflow_context, WorkflowState.COMPLETED)
|
|
317
|
+
else:
|
|
318
|
+
await self._transition_state(workflow_context, WorkflowState.ERROR)
|
|
319
|
+
workflow_context.failed_changes = apply_result.get("failed_changes", [])
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
workflow_context.current_state = WorkflowState.ERROR
|
|
323
|
+
workflow_context.failed_changes.append((str(e), "Exception"))
|
|
324
|
+
finally:
|
|
325
|
+
# Clean up and store history
|
|
326
|
+
await self._finalize_workflow(workflow_context)
|
|
327
|
+
|
|
328
|
+
return workflow_context
|
|
329
|
+
|
|
330
|
+
async def execute_single_file_workflow(
|
|
331
|
+
self,
|
|
332
|
+
file_path: str,
|
|
333
|
+
operation_type: str,
|
|
334
|
+
current_content: Optional[str] = None,
|
|
335
|
+
new_content: Optional[str] = None,
|
|
336
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
337
|
+
) -> WorkflowContext:
|
|
338
|
+
"""
|
|
339
|
+
Execute workflow for a single file modification.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
file_path: Path to the file
|
|
343
|
+
operation_type: Type of operation (create, modify, delete)
|
|
344
|
+
current_content: Current file content (for modify operations)
|
|
345
|
+
new_content: New content to write (for create/modify operations)
|
|
346
|
+
metadata: Additional metadata
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
WorkflowContext with results
|
|
350
|
+
"""
|
|
351
|
+
operation_id = f"single-file-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
352
|
+
|
|
353
|
+
workflow_context = WorkflowContext(
|
|
354
|
+
operation_id=operation_id,
|
|
355
|
+
operation_type=f"single_file_{operation_type}",
|
|
356
|
+
metadata=metadata or {},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
await self._transition_state(
|
|
361
|
+
workflow_context, WorkflowState.DISPLAYING_CHANGES
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Display based on operation type
|
|
365
|
+
current_model = workflow_context.metadata.get(
|
|
366
|
+
"current_model", "Omnimancer AI"
|
|
367
|
+
)
|
|
368
|
+
if operation_type == "create":
|
|
369
|
+
display_result = await self.unified_display.display_file_creation(
|
|
370
|
+
file_path,
|
|
371
|
+
new_content or "",
|
|
372
|
+
{"interactive": True, "current_model": current_model},
|
|
373
|
+
)
|
|
374
|
+
elif operation_type == "modify":
|
|
375
|
+
display_result = await self.unified_display.display_file_modification(
|
|
376
|
+
file_path,
|
|
377
|
+
current_content or "",
|
|
378
|
+
new_content or "",
|
|
379
|
+
{"interactive": True, "current_model": current_model},
|
|
380
|
+
)
|
|
381
|
+
elif operation_type == "delete":
|
|
382
|
+
display_result = await self.unified_display.display_file_deletion(
|
|
383
|
+
file_path,
|
|
384
|
+
current_content or "",
|
|
385
|
+
{"interactive": True, "current_model": current_model},
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
raise ValueError(f"Unknown operation type: {operation_type}")
|
|
389
|
+
|
|
390
|
+
# Process approval decision
|
|
391
|
+
if display_result.get("approved", False):
|
|
392
|
+
workflow_context.final_result = WorkflowResult.APPROVED_AND_APPLIED
|
|
393
|
+
|
|
394
|
+
# Apply the change (bypass autonomous approval since we already got approval)
|
|
395
|
+
if operation_type == "create" and new_content is not None:
|
|
396
|
+
# Call original write_file directly to bypass autonomous approval wrapper
|
|
397
|
+
write_method = getattr(
|
|
398
|
+
self.file_system_manager,
|
|
399
|
+
"_original_write_file",
|
|
400
|
+
self.file_system_manager.write_file,
|
|
401
|
+
)
|
|
402
|
+
await write_method(
|
|
403
|
+
path=file_path,
|
|
404
|
+
content=new_content,
|
|
405
|
+
read_before_write=False, # Skip read_before_write since we already got user approval
|
|
406
|
+
)
|
|
407
|
+
elif operation_type == "modify" and new_content is not None:
|
|
408
|
+
# Call original write_file directly to bypass autonomous approval wrapper
|
|
409
|
+
write_method = getattr(
|
|
410
|
+
self.file_system_manager,
|
|
411
|
+
"_original_write_file",
|
|
412
|
+
self.file_system_manager.write_file,
|
|
413
|
+
)
|
|
414
|
+
await write_method(
|
|
415
|
+
path=file_path,
|
|
416
|
+
content=new_content,
|
|
417
|
+
read_before_write=False, # Skip read_before_write since we already got user approval
|
|
418
|
+
)
|
|
419
|
+
elif operation_type == "delete":
|
|
420
|
+
await self.file_system_manager.delete_file(file_path)
|
|
421
|
+
|
|
422
|
+
workflow_context.applied_changes.append(file_path)
|
|
423
|
+
else:
|
|
424
|
+
workflow_context.final_result = WorkflowResult.DENIED
|
|
425
|
+
|
|
426
|
+
await self._transition_state(workflow_context, WorkflowState.COMPLETED)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Error in single file workflow: {e}")
|
|
430
|
+
workflow_context.final_result = WorkflowResult.ERROR
|
|
431
|
+
await self._transition_state(workflow_context, WorkflowState.ERROR)
|
|
432
|
+
|
|
433
|
+
return workflow_context
|
|
434
|
+
|
|
435
|
+
async def get_workflow_status(self, operation_id: str) -> Optional[Dict[str, Any]]:
|
|
436
|
+
"""
|
|
437
|
+
Get current status of a workflow.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
operation_id: ID of the workflow to check
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Status dictionary or None if workflow not found
|
|
444
|
+
"""
|
|
445
|
+
workflow = self.active_workflows.get(operation_id)
|
|
446
|
+
if not workflow:
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
"operation_id": workflow.operation_id,
|
|
451
|
+
"current_state": workflow.current_state.value,
|
|
452
|
+
"initiated_at": workflow.initiated_at.isoformat(),
|
|
453
|
+
"elapsed_seconds": (datetime.now() - workflow.initiated_at).total_seconds(),
|
|
454
|
+
"decisions_count": len(workflow.user_decisions),
|
|
455
|
+
"applied_changes": workflow.applied_changes,
|
|
456
|
+
"failed_changes": workflow.failed_changes,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async def cancel_workflow(
|
|
460
|
+
self, operation_id: str, reason: str = "User cancelled"
|
|
461
|
+
) -> bool:
|
|
462
|
+
"""
|
|
463
|
+
Cancel an active workflow.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
operation_id: ID of the workflow to cancel
|
|
467
|
+
reason: Reason for cancellation
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
True if successfully cancelled, False if workflow not found
|
|
471
|
+
"""
|
|
472
|
+
workflow = self.active_workflows.get(operation_id)
|
|
473
|
+
if not workflow:
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
workflow.final_result = WorkflowResult.CANCELLED
|
|
477
|
+
workflow.metadata["cancellation_reason"] = reason
|
|
478
|
+
|
|
479
|
+
await self._transition_state(workflow, WorkflowState.CANCELLED)
|
|
480
|
+
await self._finalize_workflow(workflow)
|
|
481
|
+
|
|
482
|
+
return True
|
|
483
|
+
|
|
484
|
+
async def _handle_change_approval(
|
|
485
|
+
self, workflow_context: WorkflowContext, change_set: ChangeSet
|
|
486
|
+
) -> Dict[str, Any]:
|
|
487
|
+
"""Handle the approval process for changes."""
|
|
488
|
+
# Determine if we should use batch interface
|
|
489
|
+
use_batch_interface = (
|
|
490
|
+
len(change_set.changes) >= self.config.batch_approval_threshold
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if use_batch_interface:
|
|
494
|
+
return await self.changes_integration.display_proposed_changes(
|
|
495
|
+
change_set,
|
|
496
|
+
display_mode=self.config.default_display_mode,
|
|
497
|
+
interactive=True,
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
# Handle individual approval for each change
|
|
501
|
+
approved_changes = []
|
|
502
|
+
|
|
503
|
+
for i, change in enumerate(change_set.changes):
|
|
504
|
+
self.console.print(
|
|
505
|
+
f"\n[bold]Change {i+1} of {len(change_set.changes)}[/bold]"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Create single-change set for display
|
|
509
|
+
single_change_set = ChangeSet(
|
|
510
|
+
id=f"{change_set.id}-{i}",
|
|
511
|
+
description=f"Change for {change.file_path}",
|
|
512
|
+
changes=[change],
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
result = await self.changes_integration.display_proposed_changes(
|
|
516
|
+
single_change_set,
|
|
517
|
+
display_mode=self.config.default_display_mode,
|
|
518
|
+
interactive=True,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if result.get("approved", False):
|
|
522
|
+
approved_changes.append(i)
|
|
523
|
+
|
|
524
|
+
# Record decision
|
|
525
|
+
decision = ApprovalDecision(
|
|
526
|
+
decision=(
|
|
527
|
+
ApprovalDecisionType.APPROVED
|
|
528
|
+
if result.get("approved")
|
|
529
|
+
else ApprovalDecisionType.DENIED
|
|
530
|
+
),
|
|
531
|
+
user_notes=result.get("reason", ""),
|
|
532
|
+
)
|
|
533
|
+
workflow_context.user_decisions.append(decision)
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
"approved": len(approved_changes) > 0,
|
|
537
|
+
"selected_changes": approved_changes,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async def _apply_approved_changes(
|
|
541
|
+
self,
|
|
542
|
+
workflow_context: WorkflowContext,
|
|
543
|
+
change_set: ChangeSet,
|
|
544
|
+
approval_result: Dict[str, Any],
|
|
545
|
+
) -> Dict[str, Any]:
|
|
546
|
+
"""Apply the approved changes."""
|
|
547
|
+
selected_changes = approval_result.get("selected_changes")
|
|
548
|
+
|
|
549
|
+
# Show progress for multiple changes
|
|
550
|
+
if len(change_set.changes) > 1:
|
|
551
|
+
with Progress(console=self.console) as progress:
|
|
552
|
+
task = progress.add_task(
|
|
553
|
+
"Applying changes...", total=len(change_set.changes)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
apply_result = await self.changes_integration.apply_proposed_changes(
|
|
557
|
+
change_set.id, selected_changes
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
progress.update(task, completed=len(change_set.changes))
|
|
561
|
+
else:
|
|
562
|
+
apply_result = await self.changes_integration.apply_proposed_changes(
|
|
563
|
+
change_set.id, selected_changes
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Update workflow context
|
|
567
|
+
workflow_context.applied_changes.extend(apply_result.get("applied", []))
|
|
568
|
+
workflow_context.failed_changes.extend(apply_result.get("failed", []))
|
|
569
|
+
|
|
570
|
+
# Display results
|
|
571
|
+
if apply_result["success"]:
|
|
572
|
+
self.console.print(
|
|
573
|
+
f"[green]✓ Successfully applied {len(apply_result.get('applied', []))} changes[/green]"
|
|
574
|
+
)
|
|
575
|
+
else:
|
|
576
|
+
failed_count = len(apply_result.get("failed", []))
|
|
577
|
+
self.console.print(f"[red]✗ {failed_count} changes failed to apply[/red]")
|
|
578
|
+
|
|
579
|
+
for file_path, error in apply_result.get("failed", []):
|
|
580
|
+
self.console.print(f" [red]• {file_path}: {error}[/red]")
|
|
581
|
+
|
|
582
|
+
return apply_result
|
|
583
|
+
|
|
584
|
+
async def _transition_state(
|
|
585
|
+
self, workflow_context: WorkflowContext, new_state: WorkflowState
|
|
586
|
+
):
|
|
587
|
+
"""Transition workflow to a new state."""
|
|
588
|
+
old_state = workflow_context.current_state
|
|
589
|
+
workflow_context.current_state = new_state
|
|
590
|
+
|
|
591
|
+
# Record state change
|
|
592
|
+
workflow_context.state_history.append(
|
|
593
|
+
{
|
|
594
|
+
"from_state": old_state.value,
|
|
595
|
+
"to_state": new_state.value,
|
|
596
|
+
"timestamp": datetime.now().isoformat(),
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Call callback if registered
|
|
601
|
+
if self.on_state_change:
|
|
602
|
+
await self.on_state_change(workflow_context, old_state, new_state)
|
|
603
|
+
|
|
604
|
+
async def _finalize_workflow(self, workflow_context: WorkflowContext):
|
|
605
|
+
"""Finalize workflow and clean up."""
|
|
606
|
+
# Remove from active workflows
|
|
607
|
+
if workflow_context.operation_id in self.active_workflows:
|
|
608
|
+
del self.active_workflows[workflow_context.operation_id]
|
|
609
|
+
|
|
610
|
+
# Add to history if configured
|
|
611
|
+
if self.config.save_workflow_history:
|
|
612
|
+
self.workflow_history.append(workflow_context)
|
|
613
|
+
|
|
614
|
+
# Clean old history
|
|
615
|
+
cutoff_date = datetime.now() - timedelta(
|
|
616
|
+
days=self.config.history_retention_days
|
|
617
|
+
)
|
|
618
|
+
self.workflow_history = [
|
|
619
|
+
w for w in self.workflow_history if w.initiated_at > cutoff_date
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
# Call completion callback
|
|
623
|
+
if self.on_workflow_complete:
|
|
624
|
+
await self.on_workflow_complete(workflow_context)
|
|
625
|
+
|
|
626
|
+
def get_workflow_statistics(self) -> Dict[str, Any]:
|
|
627
|
+
"""Get statistics about workflow usage."""
|
|
628
|
+
total_workflows = len(self.workflow_history)
|
|
629
|
+
if total_workflows == 0:
|
|
630
|
+
return {"total_workflows": 0}
|
|
631
|
+
|
|
632
|
+
# Count results
|
|
633
|
+
result_counts = {}
|
|
634
|
+
for workflow in self.workflow_history:
|
|
635
|
+
result = workflow.final_result.value if workflow.final_result else "unknown"
|
|
636
|
+
result_counts[result] = result_counts.get(result, 0) + 1
|
|
637
|
+
|
|
638
|
+
# Calculate success rate
|
|
639
|
+
successful = result_counts.get("approved_and_applied", 0)
|
|
640
|
+
success_rate = (
|
|
641
|
+
(successful / total_workflows) * 100 if total_workflows > 0 else 0
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
"total_workflows": total_workflows,
|
|
646
|
+
"active_workflows": len(self.active_workflows),
|
|
647
|
+
"result_distribution": result_counts,
|
|
648
|
+
"success_rate": success_rate,
|
|
649
|
+
"average_changes_per_workflow": (
|
|
650
|
+
sum(
|
|
651
|
+
len(w.applied_changes) + len(w.failed_changes)
|
|
652
|
+
for w in self.workflow_history
|
|
653
|
+
)
|
|
654
|
+
/ total_workflows
|
|
655
|
+
if total_workflows > 0
|
|
656
|
+
else 0
|
|
657
|
+
),
|
|
658
|
+
}
|