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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,729 @@
1
+ """
2
+ Batch Approval Filtering and Sorting Logic for Omnimancer CLI.
3
+
4
+ This module provides filtering and sorting capabilities to handle large
5
+ batches of approval requests efficiently with complex query support.
6
+ """
7
+
8
+ import fnmatch
9
+ import logging
10
+ import re
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass
13
+ from datetime import datetime
14
+ from enum import Enum
15
+ from typing import Any, Dict, List, Optional, Tuple
16
+
17
+ from ..core.agent.approval_manager import BatchApprovalRequest, ChangePreview
18
+ from ..core.agent.types import Operation, OperationType
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class FilterOperator(Enum):
24
+ """Filter operators for query expressions."""
25
+
26
+ AND = "AND"
27
+ OR = "OR"
28
+ NOT = "NOT"
29
+
30
+
31
+ class SortDirection(Enum):
32
+ """Sort direction options."""
33
+
34
+ ASCENDING = "asc"
35
+ DESCENDING = "desc"
36
+
37
+
38
+ class SortBy(Enum):
39
+ """Sort criteria options."""
40
+
41
+ TIMESTAMP = "timestamp"
42
+ RISK_LEVEL = "risk"
43
+ ACTION_TYPE = "type"
44
+ TARGET = "target"
45
+ STATUS = "status"
46
+ ALPHABETICAL = "alpha"
47
+
48
+
49
+ @dataclass
50
+ class FilterCriteria:
51
+ """Represents a single filter criterion."""
52
+
53
+ field: str
54
+ operator: str
55
+ value: str
56
+ negated: bool = False
57
+
58
+
59
+ @dataclass
60
+ class SortCriteria:
61
+ """Represents sort configuration."""
62
+
63
+ sort_by: SortBy
64
+ direction: SortDirection = SortDirection.ASCENDING
65
+ secondary_sort: Optional["SortCriteria"] = None
66
+
67
+
68
+ class BatchFilter(ABC):
69
+ """Abstract base class for batch filters."""
70
+
71
+ @abstractmethod
72
+ def matches(
73
+ self, operation: Operation, preview: Optional[ChangePreview] = None
74
+ ) -> bool:
75
+ """Check if operation matches this filter."""
76
+ pass
77
+
78
+ @abstractmethod
79
+ def get_description(self) -> str:
80
+ """Get human-readable description of this filter."""
81
+ pass
82
+
83
+
84
+ class ActionTypeFilter(BatchFilter):
85
+ """Filter operations by action type."""
86
+
87
+ def __init__(self, operation_types: List[OperationType]):
88
+ """
89
+ Initialize action type filter.
90
+
91
+ Args:
92
+ operation_types: List of operation types to match
93
+ """
94
+ self.operation_types = set(operation_types)
95
+
96
+ def matches(
97
+ self, operation: Operation, preview: Optional[ChangePreview] = None
98
+ ) -> bool:
99
+ """Check if operation type matches."""
100
+ return operation.type in self.operation_types
101
+
102
+ def get_description(self) -> str:
103
+ """Get description of this filter."""
104
+ types = [op.value for op in self.operation_types]
105
+ return f"Type: {', '.join(types)}"
106
+
107
+
108
+ class RiskLevelFilter(BatchFilter):
109
+ """Filter operations by risk level."""
110
+
111
+ def __init__(self, risk_levels: List[str]):
112
+ """
113
+ Initialize risk level filter.
114
+
115
+ Args:
116
+ risk_levels: List of risk levels to match (low, medium, high, critical)
117
+ """
118
+ self.risk_levels = set(level.lower() for level in risk_levels)
119
+
120
+ def matches(
121
+ self, operation: Operation, preview: Optional[ChangePreview] = None
122
+ ) -> bool:
123
+ """Check if risk level matches."""
124
+ if not preview or not preview.risk_assessment:
125
+ return "unknown" in self.risk_levels
126
+
127
+ risk_level = self._extract_risk_level(preview.risk_assessment)
128
+ return risk_level in self.risk_levels
129
+
130
+ def _extract_risk_level(self, risk_assessment: str) -> str:
131
+ """Extract risk level from assessment string."""
132
+ risk_lower = risk_assessment.lower()
133
+ if "critical" in risk_lower:
134
+ return "critical"
135
+ elif "high" in risk_lower:
136
+ return "high"
137
+ elif "medium" in risk_lower:
138
+ return "medium"
139
+ elif "low" in risk_lower:
140
+ return "low"
141
+ else:
142
+ return "unknown"
143
+
144
+ def get_description(self) -> str:
145
+ """Get description of this filter."""
146
+ return f"Risk: {', '.join(self.risk_levels)}"
147
+
148
+
149
+ class TargetPatternFilter(BatchFilter):
150
+ """Filter operations by target pattern using glob matching."""
151
+
152
+ def __init__(self, patterns: List[str]):
153
+ """
154
+ Initialize target pattern filter.
155
+
156
+ Args:
157
+ patterns: List of glob patterns to match against targets
158
+ """
159
+ self.patterns = patterns
160
+
161
+ def matches(
162
+ self, operation: Operation, preview: Optional[ChangePreview] = None
163
+ ) -> bool:
164
+ """Check if operation target matches any pattern."""
165
+ target = self._extract_target(operation)
166
+ if not target:
167
+ return False
168
+
169
+ return any(
170
+ fnmatch.fnmatch(target.lower(), pattern.lower())
171
+ for pattern in self.patterns
172
+ )
173
+
174
+ def _extract_target(self, operation: Operation) -> Optional[str]:
175
+ """Extract target from operation data."""
176
+ if operation.data:
177
+ # Try common target fields
178
+ for field in ["path", "url", "command", "target"]:
179
+ if field in operation.data:
180
+ return str(operation.data[field])
181
+
182
+ return operation.description
183
+
184
+ def get_description(self) -> str:
185
+ """Get description of this filter."""
186
+ return f"Target: {', '.join(self.patterns)}"
187
+
188
+
189
+ class StatusFilter(BatchFilter):
190
+ """Filter operations by approval status."""
191
+
192
+ def __init__(self, statuses: List[str], batch_request: BatchApprovalRequest):
193
+ """
194
+ Initialize status filter.
195
+
196
+ Args:
197
+ statuses: List of statuses to match (pending, approved)
198
+ batch_request: Batch request for status lookup
199
+ """
200
+ self.statuses = set(status.lower() for status in statuses)
201
+ self.batch_request = batch_request
202
+
203
+ def matches(
204
+ self, operation: Operation, preview: Optional[ChangePreview] = None
205
+ ) -> bool:
206
+ """Check if operation status matches."""
207
+ # Find operation index in batch
208
+ try:
209
+ op_index = self.batch_request.operations.index(operation)
210
+ is_approved = op_index in self.batch_request.approved_operations
211
+
212
+ current_status = "approved" if is_approved else "pending"
213
+ return current_status in self.statuses
214
+ except ValueError:
215
+ return False
216
+
217
+ def get_description(self) -> str:
218
+ """Get description of this filter."""
219
+ return f"Status: {', '.join(self.statuses)}"
220
+
221
+
222
+ class CustomQueryFilter(BatchFilter):
223
+ """Filter operations using custom query expressions."""
224
+
225
+ def __init__(self, query: str, batch_request: BatchApprovalRequest):
226
+ """
227
+ Initialize custom query filter.
228
+
229
+ Args:
230
+ query: Query expression (e.g., "risk:high AND type:file_write")
231
+ batch_request: Batch request for context
232
+ """
233
+ self.query = query
234
+ self.batch_request = batch_request
235
+ self.parsed_filters = self._parse_query(query)
236
+
237
+ def matches(
238
+ self, operation: Operation, preview: Optional[ChangePreview] = None
239
+ ) -> bool:
240
+ """Check if operation matches the query."""
241
+ return self._evaluate_filters(self.parsed_filters, operation, preview)
242
+
243
+ def _parse_query(self, query: str) -> List[Any]:
244
+ """Parse query into filter components."""
245
+ # Simple query parser - supports basic expressions like:
246
+ # "risk:high", "type:file_write", "target:*.py"
247
+ # "risk:high AND type:file_write"
248
+ # "NOT status:approved"
249
+
250
+ # Tokenize the query
251
+ tokens = re.findall(
252
+ r"\w+:\w+|\w+:\*?\.\w+|\w+:\*|AND|OR|NOT|\(|\)",
253
+ query,
254
+ re.IGNORECASE,
255
+ )
256
+
257
+ filters = []
258
+ current_filter = None
259
+ operator = None
260
+ negated = False
261
+
262
+ for token in tokens:
263
+ token_upper = token.upper()
264
+
265
+ if token_upper == "AND":
266
+ if current_filter:
267
+ filters.append(current_filter)
268
+ operator = FilterOperator.AND
269
+ current_filter = None
270
+ elif token_upper == "OR":
271
+ if current_filter:
272
+ filters.append(current_filter)
273
+ operator = FilterOperator.OR
274
+ current_filter = None
275
+ elif token_upper == "NOT":
276
+ negated = True
277
+ elif ":" in token:
278
+ # Field:value expression
279
+ field, value = token.split(":", 1)
280
+ filter_obj = self._create_field_filter(field.lower(), value.lower())
281
+ if filter_obj:
282
+ if negated:
283
+ filter_obj = NegatedFilter(filter_obj)
284
+ negated = False
285
+
286
+ if operator:
287
+ filters.append((operator, filter_obj))
288
+ operator = None
289
+ else:
290
+ current_filter = filter_obj
291
+
292
+ if current_filter:
293
+ filters.append(current_filter)
294
+
295
+ return filters
296
+
297
+ def _create_field_filter(self, field: str, value: str) -> Optional[BatchFilter]:
298
+ """Create appropriate filter based on field and value."""
299
+ if field == "risk":
300
+ return RiskLevelFilter([value])
301
+ elif field == "type":
302
+ # Map common type names to OperationType
303
+ type_map = {
304
+ "file_read": OperationType.FILE_READ,
305
+ "file_write": OperationType.FILE_WRITE,
306
+ "file_delete": OperationType.FILE_DELETE,
307
+ "command": OperationType.COMMAND_EXECUTE,
308
+ "web": OperationType.WEB_REQUEST,
309
+ "mcp": OperationType.MCP_TOOL_CALL,
310
+ }
311
+ op_type = type_map.get(value)
312
+ if op_type:
313
+ return ActionTypeFilter([op_type])
314
+ elif field == "target":
315
+ return TargetPatternFilter([value])
316
+ elif field == "status":
317
+ return StatusFilter([value], self.batch_request)
318
+
319
+ return None
320
+
321
+ def _evaluate_filters(
322
+ self,
323
+ filters: List[Any],
324
+ operation: Operation,
325
+ preview: Optional[ChangePreview],
326
+ ) -> bool:
327
+ """Evaluate parsed filters against operation."""
328
+ if not filters:
329
+ return True
330
+
331
+ result = True
332
+
333
+ for item in filters:
334
+ if isinstance(item, tuple) and len(item) == 2:
335
+ # Operator and filter
336
+ op, filter_obj = item
337
+ filter_result = filter_obj.matches(operation, preview)
338
+
339
+ if op == FilterOperator.AND:
340
+ result = result and filter_result
341
+ elif op == FilterOperator.OR:
342
+ result = result or filter_result
343
+ elif isinstance(item, BatchFilter):
344
+ # Direct filter
345
+ filter_result = item.matches(operation, preview)
346
+ result = result and filter_result
347
+
348
+ return result
349
+
350
+ def get_description(self) -> str:
351
+ """Get description of this filter."""
352
+ return f"Query: {self.query}"
353
+
354
+
355
+ class NegatedFilter(BatchFilter):
356
+ """Wrapper for negated filters."""
357
+
358
+ def __init__(self, base_filter: BatchFilter):
359
+ """Initialize negated filter."""
360
+ self.base_filter = base_filter
361
+
362
+ def matches(
363
+ self, operation: Operation, preview: Optional[ChangePreview] = None
364
+ ) -> bool:
365
+ """Return negated result of base filter."""
366
+ return not self.base_filter.matches(operation, preview)
367
+
368
+ def get_description(self) -> str:
369
+ """Get description of negated filter."""
370
+ return f"NOT ({self.base_filter.get_description()})"
371
+
372
+
373
+ class BatchFilterManager:
374
+ """Manages multiple filters and applies them to batch requests."""
375
+
376
+ def __init__(self):
377
+ """Initialize batch filter manager."""
378
+ self.active_filters: List[BatchFilter] = []
379
+
380
+ def add_filter(self, batch_filter: BatchFilter):
381
+ """Add a filter to the active filters."""
382
+ self.active_filters.append(batch_filter)
383
+
384
+ def remove_filter(self, index: int) -> bool:
385
+ """Remove filter by index."""
386
+ if 0 <= index < len(self.active_filters):
387
+ del self.active_filters[index]
388
+ return True
389
+ return False
390
+
391
+ def clear_filters(self):
392
+ """Remove all active filters."""
393
+ self.active_filters.clear()
394
+
395
+ def apply_filters(
396
+ self, batch_request: BatchApprovalRequest
397
+ ) -> Tuple[List[Operation], List[ChangePreview]]:
398
+ """
399
+ Apply active filters to batch request.
400
+
401
+ Args:
402
+ batch_request: Batch request to filter
403
+
404
+ Returns:
405
+ Tuple of filtered operations and previews
406
+ """
407
+ if not self.active_filters:
408
+ return batch_request.operations, batch_request.previews
409
+
410
+ filtered_ops = []
411
+ filtered_previews = []
412
+
413
+ for i, (operation, preview) in enumerate(
414
+ zip(batch_request.operations, batch_request.previews)
415
+ ):
416
+ if self._matches_all_filters(operation, preview):
417
+ filtered_ops.append(operation)
418
+ filtered_previews.append(preview)
419
+
420
+ return filtered_ops, filtered_previews
421
+
422
+ def _matches_all_filters(
423
+ self, operation: Operation, preview: Optional[ChangePreview]
424
+ ) -> bool:
425
+ """Check if operation matches all active filters."""
426
+ return all(f.matches(operation, preview) for f in self.active_filters)
427
+
428
+ def get_filter_descriptions(self) -> List[str]:
429
+ """Get descriptions of all active filters."""
430
+ return [f.get_description() for f in self.active_filters]
431
+
432
+ def get_filter_count(self) -> int:
433
+ """Get number of active filters."""
434
+ return len(self.active_filters)
435
+
436
+
437
+ class BatchSorter:
438
+ """Handles sorting of batch operations."""
439
+
440
+ def __init__(self):
441
+ """Initialize batch sorter."""
442
+ self.sort_functions = {
443
+ SortBy.TIMESTAMP: self._sort_by_timestamp,
444
+ SortBy.RISK_LEVEL: self._sort_by_risk,
445
+ SortBy.ACTION_TYPE: self._sort_by_action_type,
446
+ SortBy.TARGET: self._sort_by_target,
447
+ SortBy.STATUS: self._sort_by_status,
448
+ SortBy.ALPHABETICAL: self._sort_by_alphabetical,
449
+ }
450
+
451
+ def sort_batch(
452
+ self,
453
+ operations: List[Operation],
454
+ previews: List[ChangePreview],
455
+ criteria: SortCriteria,
456
+ batch_request: Optional[BatchApprovalRequest] = None,
457
+ ) -> Tuple[List[Operation], List[ChangePreview]]:
458
+ """
459
+ Sort operations and previews according to criteria.
460
+
461
+ Args:
462
+ operations: Operations to sort
463
+ previews: Corresponding previews
464
+ criteria: Sort criteria
465
+ batch_request: Optional batch request for additional context
466
+
467
+ Returns:
468
+ Tuple of sorted operations and previews
469
+ """
470
+ if not operations:
471
+ return operations, previews
472
+
473
+ # Create list of tuples for sorting
474
+ items = list(zip(operations, previews))
475
+
476
+ # Get sort function
477
+ sort_func = self.sort_functions.get(criteria.sort_by)
478
+ if not sort_func:
479
+ logger.warning(f"Unknown sort criteria: {criteria.sort_by}")
480
+ return operations, previews
481
+
482
+ # Sort items
483
+ sorted_items = sort_func(items, criteria.direction, batch_request)
484
+
485
+ # Apply secondary sort if specified
486
+ if criteria.secondary_sort:
487
+ secondary_func = self.sort_functions.get(criteria.secondary_sort.sort_by)
488
+ if secondary_func:
489
+ # Stable sort to preserve primary ordering
490
+ sorted_items = secondary_func(
491
+ sorted_items,
492
+ criteria.secondary_sort.direction,
493
+ batch_request,
494
+ )
495
+
496
+ # Unzip back to separate lists
497
+ if sorted_items:
498
+ sorted_ops, sorted_previews = zip(*sorted_items)
499
+ return list(sorted_ops), list(sorted_previews)
500
+ else:
501
+ return [], []
502
+
503
+ def _sort_by_timestamp(
504
+ self,
505
+ items: List[Tuple[Operation, ChangePreview]],
506
+ direction: SortDirection,
507
+ batch_request: Optional[BatchApprovalRequest],
508
+ ) -> List[Tuple[Operation, ChangePreview]]:
509
+ """Sort by timestamp (using batch creation time as proxy)."""
510
+ # Since operations don't have individual timestamps, use batch timestamp
511
+ # In practice, this would sort by when each operation was added
512
+ batch_request.created_at if batch_request else datetime.now()
513
+
514
+ # For now, maintain original order since all operations have same timestamp
515
+ return items if direction == SortDirection.ASCENDING else list(reversed(items))
516
+
517
+ def _sort_by_risk(
518
+ self,
519
+ items: List[Tuple[Operation, ChangePreview]],
520
+ direction: SortDirection,
521
+ batch_request: Optional[BatchApprovalRequest],
522
+ ) -> List[Tuple[Operation, ChangePreview]]:
523
+ """Sort by risk level."""
524
+ risk_order = {
525
+ "critical": 4,
526
+ "high": 3,
527
+ "medium": 2,
528
+ "low": 1,
529
+ "unknown": 0,
530
+ }
531
+
532
+ def get_risk_score(item):
533
+ operation, preview = item
534
+ if preview and preview.risk_assessment:
535
+ risk_level = self._extract_risk_level(preview.risk_assessment)
536
+ return risk_order.get(risk_level, 0)
537
+ return 0
538
+
539
+ reverse = direction == SortDirection.DESCENDING
540
+ return sorted(items, key=get_risk_score, reverse=reverse)
541
+
542
+ def _sort_by_action_type(
543
+ self,
544
+ items: List[Tuple[Operation, ChangePreview]],
545
+ direction: SortDirection,
546
+ batch_request: Optional[BatchApprovalRequest],
547
+ ) -> List[Tuple[Operation, ChangePreview]]:
548
+ """Sort by action type."""
549
+
550
+ def get_type_name(item):
551
+ operation, preview = item
552
+ return operation.type.value
553
+
554
+ reverse = direction == SortDirection.DESCENDING
555
+ return sorted(items, key=get_type_name, reverse=reverse)
556
+
557
+ def _sort_by_target(
558
+ self,
559
+ items: List[Tuple[Operation, ChangePreview]],
560
+ direction: SortDirection,
561
+ batch_request: Optional[BatchApprovalRequest],
562
+ ) -> List[Tuple[Operation, ChangePreview]]:
563
+ """Sort by operation target."""
564
+
565
+ def get_target(item):
566
+ operation, preview = item
567
+ if operation.data:
568
+ for field in ["path", "url", "command", "target"]:
569
+ if field in operation.data:
570
+ return str(operation.data[field]).lower()
571
+ return operation.description.lower() if operation.description else ""
572
+
573
+ reverse = direction == SortDirection.DESCENDING
574
+ return sorted(items, key=get_target, reverse=reverse)
575
+
576
+ def _sort_by_status(
577
+ self,
578
+ items: List[Tuple[Operation, ChangePreview]],
579
+ direction: SortDirection,
580
+ batch_request: Optional[BatchApprovalRequest],
581
+ ) -> List[Tuple[Operation, ChangePreview]]:
582
+ """Sort by approval status."""
583
+
584
+ def get_status_score(item):
585
+ if not batch_request:
586
+ return 0
587
+
588
+ operation, preview = item
589
+ try:
590
+ op_index = batch_request.operations.index(operation)
591
+ is_approved = op_index in batch_request.approved_operations
592
+ return 1 if is_approved else 0
593
+ except ValueError:
594
+ return 0
595
+
596
+ reverse = direction == SortDirection.DESCENDING
597
+ return sorted(items, key=get_status_score, reverse=reverse)
598
+
599
+ def _sort_by_alphabetical(
600
+ self,
601
+ items: List[Tuple[Operation, ChangePreview]],
602
+ direction: SortDirection,
603
+ batch_request: Optional[BatchApprovalRequest],
604
+ ) -> List[Tuple[Operation, ChangePreview]]:
605
+ """Sort alphabetically by description."""
606
+
607
+ def get_description(item):
608
+ operation, preview = item
609
+ return operation.description.lower() if operation.description else ""
610
+
611
+ reverse = direction == SortDirection.DESCENDING
612
+ return sorted(items, key=get_description, reverse=reverse)
613
+
614
+ def _extract_risk_level(self, risk_assessment: str) -> str:
615
+ """Extract risk level from assessment string."""
616
+ risk_lower = risk_assessment.lower()
617
+ if "critical" in risk_lower:
618
+ return "critical"
619
+ elif "high" in risk_lower:
620
+ return "high"
621
+ elif "medium" in risk_lower:
622
+ return "medium"
623
+ elif "low" in risk_lower:
624
+ return "low"
625
+ else:
626
+ return "unknown"
627
+
628
+
629
+ class BatchPaginator:
630
+ """Handles pagination of filtered and sorted results."""
631
+
632
+ def __init__(self, page_size: int = 10):
633
+ """
634
+ Initialize paginator.
635
+
636
+ Args:
637
+ page_size: Number of items per page
638
+ """
639
+ self.page_size = page_size
640
+
641
+ def paginate(
642
+ self,
643
+ operations: List[Operation],
644
+ previews: List[ChangePreview],
645
+ page: int = 0,
646
+ ) -> Tuple[List[Operation], List[ChangePreview], Dict[str, Any]]:
647
+ """
648
+ Paginate operations and previews.
649
+
650
+ Args:
651
+ operations: Operations to paginate
652
+ previews: Corresponding previews
653
+ page: Page number (0-based)
654
+
655
+ Returns:
656
+ Tuple of (page_operations, page_previews, pagination_info)
657
+ """
658
+ total_items = len(operations)
659
+ total_pages = (total_items + self.page_size - 1) // self.page_size
660
+
661
+ # Ensure page is within bounds
662
+ page = max(0, min(page, total_pages - 1))
663
+
664
+ start_idx = page * self.page_size
665
+ end_idx = min(start_idx + self.page_size, total_items)
666
+
667
+ page_operations = operations[start_idx:end_idx]
668
+ page_previews = previews[start_idx:end_idx]
669
+
670
+ pagination_info = {
671
+ "current_page": page,
672
+ "total_pages": total_pages,
673
+ "page_size": self.page_size,
674
+ "total_items": total_items,
675
+ "start_index": start_idx,
676
+ "end_index": end_idx,
677
+ "has_previous": page > 0,
678
+ "has_next": page < total_pages - 1,
679
+ }
680
+
681
+ return page_operations, page_previews, pagination_info
682
+
683
+
684
+ def create_quick_filters() -> Dict[str, BatchFilter]:
685
+ """Create commonly used quick filters."""
686
+ return {
687
+ "high_risk": RiskLevelFilter(["high", "critical"]),
688
+ "file_ops": ActionTypeFilter(
689
+ [OperationType.FILE_WRITE, OperationType.FILE_DELETE]
690
+ ),
691
+ "read_only": ActionTypeFilter([OperationType.FILE_READ]),
692
+ "commands": ActionTypeFilter([OperationType.COMMAND_EXECUTE]),
693
+ "web_requests": ActionTypeFilter([OperationType.WEB_REQUEST]),
694
+ "python_files": TargetPatternFilter(["*.py"]),
695
+ "config_files": TargetPatternFilter(
696
+ ["*.conf", "*.config", "*.json", "*.yaml", "*.yml"]
697
+ ),
698
+ }
699
+
700
+
701
+ def parse_filter_expression(
702
+ expression: str, batch_request: BatchApprovalRequest
703
+ ) -> Optional[BatchFilter]:
704
+ """
705
+ Parse a filter expression into a BatchFilter.
706
+
707
+ Args:
708
+ expression: Filter expression string
709
+ batch_request: Batch request for context
710
+
711
+ Returns:
712
+ BatchFilter instance or None if parsing failed
713
+ """
714
+ try:
715
+ return CustomQueryFilter(expression, batch_request)
716
+ except Exception as e:
717
+ logger.error(f"Failed to parse filter expression '{expression}': {e}")
718
+ return None
719
+
720
+
721
+ def create_default_sort_criteria() -> SortCriteria:
722
+ """Create default sort criteria (by risk level, descending)."""
723
+ return SortCriteria(
724
+ sort_by=SortBy.RISK_LEVEL,
725
+ direction=SortDirection.DESCENDING,
726
+ secondary_sort=SortCriteria(
727
+ sort_by=SortBy.ACTION_TYPE, direction=SortDirection.ASCENDING
728
+ ),
729
+ )