attune-ai 2.1.4__py3-none-any.whl → 2.2.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 (123) hide show
  1. attune/cli/__init__.py +3 -55
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +7 -15
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +34 -28
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/core.py +190 -0
  20. attune/dashboard/app.py +4 -2
  21. attune/dashboard/simple_server.py +3 -1
  22. attune/dashboard/standalone_server.py +7 -3
  23. attune/mcp/server.py +54 -102
  24. attune/memory/long_term.py +0 -2
  25. attune/memory/short_term/__init__.py +84 -0
  26. attune/memory/short_term/base.py +467 -0
  27. attune/memory/short_term/batch.py +219 -0
  28. attune/memory/short_term/caching.py +227 -0
  29. attune/memory/short_term/conflicts.py +265 -0
  30. attune/memory/short_term/cross_session.py +122 -0
  31. attune/memory/short_term/facade.py +655 -0
  32. attune/memory/short_term/pagination.py +215 -0
  33. attune/memory/short_term/patterns.py +271 -0
  34. attune/memory/short_term/pubsub.py +286 -0
  35. attune/memory/short_term/queues.py +244 -0
  36. attune/memory/short_term/security.py +300 -0
  37. attune/memory/short_term/sessions.py +250 -0
  38. attune/memory/short_term/streams.py +249 -0
  39. attune/memory/short_term/timelines.py +234 -0
  40. attune/memory/short_term/transactions.py +186 -0
  41. attune/memory/short_term/working.py +252 -0
  42. attune/meta_workflows/cli_commands/__init__.py +3 -0
  43. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  44. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  45. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  48. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  49. attune/meta_workflows/workflow.py +1 -1
  50. attune/models/adaptive_routing.py +4 -8
  51. attune/models/auth_cli.py +3 -9
  52. attune/models/auth_strategy.py +2 -4
  53. attune/models/provider_config.py +20 -1
  54. attune/models/telemetry/analytics.py +0 -2
  55. attune/models/telemetry/backend.py +0 -3
  56. attune/models/telemetry/storage.py +0 -2
  57. attune/orchestration/_strategies/__init__.py +156 -0
  58. attune/orchestration/_strategies/base.py +231 -0
  59. attune/orchestration/_strategies/conditional_strategies.py +373 -0
  60. attune/orchestration/_strategies/conditions.py +369 -0
  61. attune/orchestration/_strategies/core_strategies.py +491 -0
  62. attune/orchestration/_strategies/data_classes.py +64 -0
  63. attune/orchestration/_strategies/nesting.py +233 -0
  64. attune/orchestration/execution_strategies.py +58 -1567
  65. attune/orchestration/meta_orchestrator.py +1 -3
  66. attune/project_index/scanner.py +1 -3
  67. attune/project_index/scanner_parallel.py +7 -5
  68. attune/socratic_router.py +1 -3
  69. attune/telemetry/agent_coordination.py +9 -3
  70. attune/telemetry/agent_tracking.py +16 -3
  71. attune/telemetry/approval_gates.py +22 -5
  72. attune/telemetry/cli.py +3 -3
  73. attune/telemetry/commands/dashboard_commands.py +24 -8
  74. attune/telemetry/event_streaming.py +8 -2
  75. attune/telemetry/feedback_loop.py +10 -2
  76. attune/tools.py +1 -0
  77. attune/workflow_commands.py +1 -3
  78. attune/workflows/__init__.py +53 -10
  79. attune/workflows/autonomous_test_gen.py +160 -104
  80. attune/workflows/base.py +48 -664
  81. attune/workflows/batch_processing.py +2 -4
  82. attune/workflows/compat.py +156 -0
  83. attune/workflows/cost_mixin.py +141 -0
  84. attune/workflows/data_classes.py +92 -0
  85. attune/workflows/document_gen/workflow.py +11 -14
  86. attune/workflows/history.py +62 -37
  87. attune/workflows/llm_base.py +2 -4
  88. attune/workflows/migration.py +422 -0
  89. attune/workflows/output.py +3 -9
  90. attune/workflows/parsing_mixin.py +427 -0
  91. attune/workflows/perf_audit.py +3 -1
  92. attune/workflows/progress.py +10 -13
  93. attune/workflows/release_prep.py +5 -1
  94. attune/workflows/routing.py +0 -2
  95. attune/workflows/secure_release.py +2 -1
  96. attune/workflows/security_audit.py +19 -14
  97. attune/workflows/security_audit_phase3.py +28 -22
  98. attune/workflows/seo_optimization.py +29 -29
  99. attune/workflows/test_gen/test_templates.py +1 -4
  100. attune/workflows/test_gen/workflow.py +0 -2
  101. attune/workflows/test_gen_behavioral.py +7 -20
  102. attune/workflows/test_gen_parallel.py +6 -4
  103. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
  104. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/RECORD +119 -94
  105. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
  106. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  107. attune_llm/agent_factory/__init__.py +6 -6
  108. attune_llm/commands/__init__.py +10 -10
  109. attune_llm/commands/models.py +3 -3
  110. attune_llm/config/__init__.py +8 -8
  111. attune_llm/learning/__init__.py +3 -3
  112. attune_llm/learning/extractor.py +5 -3
  113. attune_llm/learning/storage.py +5 -3
  114. attune_llm/security/__init__.py +17 -17
  115. attune_llm/utils/tokens.py +3 -1
  116. attune/cli_legacy.py +0 -3957
  117. attune/memory/short_term.py +0 -2192
  118. attune/workflows/manage_docs.py +0 -87
  119. attune/workflows/test5.py +0 -125
  120. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
  121. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
  122. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  123. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,373 @@
1
+ """Conditional and nested execution strategies.
2
+
3
+ This module contains strategies that implement conditional branching and
4
+ nested workflow composition:
5
+
6
+ 1. ConditionalStrategy - if/then/else branching based on gates
7
+ 2. MultiConditionalStrategy - switch/case pattern
8
+ 3. NestedStrategy - recursive workflow execution
9
+ 4. NestedSequentialStrategy - sequential steps with nested workflow support
10
+
11
+ Security:
12
+ - Condition predicates validated (no code execution)
13
+ - Cycle detection prevents infinite recursion
14
+ - Max depth limits enforced
15
+ - No eval() or exec() usage
16
+
17
+ Copyright 2025 Smart-AI-Memory
18
+ Licensed under Fair Source License 0.9
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from dataclasses import dataclass
25
+ from typing import TYPE_CHECKING, Any
26
+
27
+ from .base import ExecutionStrategy
28
+ from .conditions import Condition, ConditionEvaluator
29
+ from .data_classes import AgentResult, StrategyResult
30
+ from .nesting import (
31
+ NestingContext,
32
+ WorkflowReference,
33
+ get_workflow,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from ..agent_templates import AgentTemplate
38
+ from .conditions import Branch
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class ConditionalStrategy(ExecutionStrategy):
44
+ """Conditional branching (if X then A else B).
45
+
46
+ The 7th grammar rule enabling dynamic workflow decisions based on gates.
47
+
48
+ Use when:
49
+ - Quality gates determine next steps
50
+ - Error handling requires different paths
51
+ - Agent consensus affects workflow
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ condition: Condition,
57
+ then_branch: Branch,
58
+ else_branch: Branch | None = None,
59
+ ):
60
+ """Initialize conditional strategy."""
61
+ self.condition = condition
62
+ self.then_branch = then_branch
63
+ self.else_branch = else_branch
64
+ self.evaluator = ConditionEvaluator()
65
+
66
+ async def execute(
67
+ self, agents: list[AgentTemplate], context: dict[str, Any]
68
+ ) -> StrategyResult:
69
+ """Execute conditional branching."""
70
+ # Import here to avoid circular import
71
+ from . import get_strategy
72
+
73
+ logger.info(f"Conditional: Evaluating '{self.condition.description or 'condition'}'")
74
+
75
+ condition_met = self.evaluator.evaluate(self.condition, context)
76
+ logger.info(f"Conditional: Condition evaluated to {condition_met}")
77
+
78
+ if condition_met:
79
+ selected_branch = self.then_branch
80
+ branch_label = "then"
81
+ else:
82
+ if self.else_branch is None:
83
+ return StrategyResult(
84
+ success=True,
85
+ outputs=[],
86
+ aggregated_output={"branch_taken": None},
87
+ total_duration=0.0,
88
+ )
89
+ selected_branch = self.else_branch
90
+ branch_label = "else"
91
+
92
+ logger.info(f"Conditional: Taking '{branch_label}' branch")
93
+
94
+ branch_strategy = get_strategy(selected_branch.strategy)
95
+ branch_context = context.copy()
96
+ branch_context["_conditional"] = {"condition_met": condition_met, "branch": branch_label}
97
+
98
+ result = await branch_strategy.execute(selected_branch.agents, branch_context)
99
+ result.aggregated_output["_conditional"] = {
100
+ "condition_met": condition_met,
101
+ "branch_taken": branch_label,
102
+ }
103
+ return result
104
+
105
+
106
+ class MultiConditionalStrategy(ExecutionStrategy):
107
+ """Multiple conditional branches (switch/case pattern)."""
108
+
109
+ def __init__(
110
+ self,
111
+ conditions: list[tuple[Condition, Branch]],
112
+ default_branch: Branch | None = None,
113
+ ):
114
+ """Initialize multi-conditional strategy."""
115
+ self.conditions = conditions
116
+ self.default_branch = default_branch
117
+ self.evaluator = ConditionEvaluator()
118
+
119
+ async def execute(
120
+ self, agents: list[AgentTemplate], context: dict[str, Any]
121
+ ) -> StrategyResult:
122
+ """Execute multi-conditional branching."""
123
+ # Import here to avoid circular import
124
+ from . import get_strategy
125
+
126
+ for i, (condition, branch) in enumerate(self.conditions):
127
+ if self.evaluator.evaluate(condition, context):
128
+ logger.info(f"MultiConditional: Condition {i + 1} matched")
129
+ branch_strategy = get_strategy(branch.strategy)
130
+ result = await branch_strategy.execute(branch.agents, context)
131
+ result.aggregated_output["_matched_index"] = i
132
+ return result
133
+
134
+ if self.default_branch:
135
+ branch_strategy = get_strategy(self.default_branch.strategy)
136
+ return await branch_strategy.execute(self.default_branch.agents, context)
137
+
138
+ return StrategyResult(
139
+ success=True,
140
+ outputs=[],
141
+ aggregated_output={"reason": "No conditions matched"},
142
+ total_duration=0.0,
143
+ )
144
+
145
+
146
+ class NestedStrategy(ExecutionStrategy):
147
+ """Nested workflow execution (sentences within sentences).
148
+
149
+ Enables recursive composition where workflows invoke other workflows.
150
+ Implements the "subordinate clause" pattern in the grammar metaphor.
151
+
152
+ Features:
153
+ - Reference workflows by ID or define inline
154
+ - Configurable max depth (default: 3)
155
+ - Cycle detection prevents infinite recursion
156
+ - Full context inheritance from parent to child
157
+
158
+ Use when:
159
+ - Complex multi-stage pipelines need modular sub-workflows
160
+ - Reusable workflow components should be shared
161
+ - Hierarchical team structures (teams containing sub-teams)
162
+
163
+ Example:
164
+ >>> # Parent workflow with nested sub-workflow
165
+ >>> strategy = NestedStrategy(
166
+ ... workflow_ref=WorkflowReference(workflow_id="security-audit"),
167
+ ... max_depth=3
168
+ ... )
169
+ >>> result = await strategy.execute([], context)
170
+
171
+ Example (inline):
172
+ >>> strategy = NestedStrategy(
173
+ ... workflow_ref=WorkflowReference(
174
+ ... inline=InlineWorkflow(
175
+ ... agents=[analyzer, reviewer],
176
+ ... strategy="parallel"
177
+ ... )
178
+ ... )
179
+ ... )
180
+ """
181
+
182
+ def __init__(
183
+ self,
184
+ workflow_ref: WorkflowReference,
185
+ max_depth: int = NestingContext.DEFAULT_MAX_DEPTH,
186
+ ):
187
+ """Initialize nested strategy.
188
+
189
+ Args:
190
+ workflow_ref: Reference to workflow (by ID or inline)
191
+ max_depth: Maximum nesting depth allowed
192
+ """
193
+ self.workflow_ref = workflow_ref
194
+ self.max_depth = max_depth
195
+
196
+ async def execute(
197
+ self, agents: list[AgentTemplate], context: dict[str, Any]
198
+ ) -> StrategyResult:
199
+ """Execute nested workflow.
200
+
201
+ Args:
202
+ agents: Ignored (workflow_ref defines agents)
203
+ context: Parent execution context (inherited by child)
204
+
205
+ Returns:
206
+ StrategyResult from nested workflow execution
207
+
208
+ Raises:
209
+ RecursionError: If max depth exceeded or cycle detected
210
+ """
211
+ # Import here to avoid circular import
212
+ from . import get_strategy
213
+
214
+ # Get or create nesting context
215
+ nesting = NestingContext.from_context(context)
216
+
217
+ # Resolve workflow
218
+ if self.workflow_ref.workflow_id:
219
+ workflow_id = self.workflow_ref.workflow_id
220
+ workflow = get_workflow(workflow_id)
221
+ workflow_agents = workflow.agents
222
+ strategy_name = workflow.strategy
223
+ else:
224
+ workflow_id = f"inline_{id(self.workflow_ref.inline)}"
225
+ workflow_agents = self.workflow_ref.inline.agents
226
+ strategy_name = self.workflow_ref.inline.strategy
227
+
228
+ # Check nesting limits
229
+ if not nesting.can_nest(workflow_id):
230
+ if nesting.current_depth >= nesting.max_depth:
231
+ error_msg = (
232
+ f"Maximum nesting depth ({nesting.max_depth}) exceeded. "
233
+ f"Current stack: {' → '.join(nesting.workflow_stack)}"
234
+ )
235
+ else:
236
+ error_msg = (
237
+ f"Cycle detected: workflow '{workflow_id}' already in stack. "
238
+ f"Stack: {' → '.join(nesting.workflow_stack)}"
239
+ )
240
+ logger.error(error_msg)
241
+ raise RecursionError(error_msg)
242
+
243
+ logger.info(f"Nested: Entering '{workflow_id}' at depth {nesting.current_depth + 1}")
244
+
245
+ # Create child context with updated nesting
246
+ child_nesting = nesting.enter(workflow_id)
247
+ child_context = child_nesting.to_context(context.copy())
248
+
249
+ # Execute nested workflow
250
+ strategy = get_strategy(strategy_name)
251
+ result = await strategy.execute(workflow_agents, child_context)
252
+
253
+ # Augment result with nesting metadata
254
+ result.aggregated_output["_nested"] = {
255
+ "workflow_id": workflow_id,
256
+ "depth": child_nesting.current_depth,
257
+ "parent_stack": nesting.workflow_stack,
258
+ }
259
+
260
+ # Store result under specified key if provided
261
+ if self.workflow_ref.result_key:
262
+ result.aggregated_output[self.workflow_ref.result_key] = result.aggregated_output.copy()
263
+
264
+ logger.info(f"Nested: Exiting '{workflow_id}'")
265
+
266
+ return result
267
+
268
+
269
+ @dataclass
270
+ class StepDefinition:
271
+ """Definition of a step in NestedSequentialStrategy.
272
+
273
+ Either agent OR workflow_ref must be provided (mutually exclusive).
274
+
275
+ Attributes:
276
+ agent: Agent to execute directly
277
+ workflow_ref: Nested workflow to execute
278
+ """
279
+
280
+ agent: AgentTemplate | None = None
281
+ workflow_ref: WorkflowReference | None = None
282
+
283
+ def __post_init__(self):
284
+ """Validate that exactly one step type is provided."""
285
+ if bool(self.agent) == bool(self.workflow_ref):
286
+ raise ValueError("StepDefinition must have exactly one of: agent or workflow_ref")
287
+
288
+
289
+ class NestedSequentialStrategy(ExecutionStrategy):
290
+ """Sequential execution with nested workflow support.
291
+
292
+ Like SequentialStrategy but steps can be either agents OR workflow references.
293
+ Enables mixing direct agent execution with nested sub-workflows.
294
+
295
+ Example:
296
+ >>> strategy = NestedSequentialStrategy(
297
+ ... steps=[
298
+ ... StepDefinition(agent=analyzer),
299
+ ... StepDefinition(workflow_ref=WorkflowReference(workflow_id="review-team")),
300
+ ... StepDefinition(agent=reporter),
301
+ ... ]
302
+ ... )
303
+ """
304
+
305
+ def __init__(
306
+ self,
307
+ steps: list[StepDefinition],
308
+ max_depth: int = NestingContext.DEFAULT_MAX_DEPTH,
309
+ ):
310
+ """Initialize nested sequential strategy.
311
+
312
+ Args:
313
+ steps: List of step definitions (agents or workflow refs)
314
+ max_depth: Maximum nesting depth
315
+ """
316
+ self.steps = steps
317
+ self.max_depth = max_depth
318
+
319
+ async def execute(
320
+ self, agents: list[AgentTemplate], context: dict[str, Any]
321
+ ) -> StrategyResult:
322
+ """Execute steps sequentially, handling both agents and nested workflows."""
323
+ if not self.steps:
324
+ raise ValueError("steps list cannot be empty")
325
+
326
+ logger.info(f"NestedSequential: Executing {len(self.steps)} steps")
327
+
328
+ results: list[AgentResult] = []
329
+ current_context = context.copy()
330
+ total_duration = 0.0
331
+
332
+ for i, step in enumerate(self.steps):
333
+ logger.info(f"NestedSequential: Step {i + 1}/{len(self.steps)}")
334
+
335
+ if step.agent:
336
+ # Direct agent execution
337
+ result = await self._execute_agent(step.agent, current_context)
338
+ results.append(result)
339
+ total_duration += result.duration_seconds
340
+
341
+ if result.success:
342
+ current_context[f"{step.agent.id}_output"] = result.output
343
+ else:
344
+ # Nested workflow execution
345
+ nested_strategy = NestedStrategy(
346
+ workflow_ref=step.workflow_ref,
347
+ max_depth=self.max_depth,
348
+ )
349
+ nested_result = await nested_strategy.execute([], current_context)
350
+ total_duration += nested_result.total_duration
351
+
352
+ # Convert to AgentResult for consistency
353
+ results.append(
354
+ AgentResult(
355
+ agent_id=f"nested_{step.workflow_ref.workflow_id or 'inline'}",
356
+ success=nested_result.success,
357
+ output=nested_result.aggregated_output,
358
+ confidence=nested_result.aggregated_output.get("avg_confidence", 0.0),
359
+ duration_seconds=nested_result.total_duration,
360
+ )
361
+ )
362
+
363
+ if nested_result.success:
364
+ key = step.workflow_ref.result_key or f"step_{i}_output"
365
+ current_context[key] = nested_result.aggregated_output
366
+
367
+ return StrategyResult(
368
+ success=all(r.success for r in results),
369
+ outputs=results,
370
+ aggregated_output=self._aggregate_results(results),
371
+ total_duration=total_duration,
372
+ errors=[r.error for r in results if not r.success],
373
+ )