attune-ai 2.1.5__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.
- attune/cli/__init__.py +3 -59
- attune/cli/commands/batch.py +4 -12
- attune/cli/commands/cache.py +7 -15
- attune/cli/commands/provider.py +17 -0
- attune/cli/commands/routing.py +3 -1
- attune/cli/commands/setup.py +122 -0
- attune/cli/commands/tier.py +1 -3
- attune/cli/commands/workflow.py +31 -0
- attune/cli/parsers/cache.py +1 -0
- attune/cli/parsers/help.py +1 -3
- attune/cli/parsers/provider.py +7 -0
- attune/cli/parsers/routing.py +1 -3
- attune/cli/parsers/setup.py +7 -0
- attune/cli/parsers/status.py +1 -3
- attune/cli/parsers/tier.py +1 -3
- attune/cli_minimal.py +9 -3
- attune/cli_router.py +9 -7
- attune/cli_unified.py +3 -0
- attune/dashboard/app.py +3 -1
- attune/dashboard/simple_server.py +3 -1
- attune/dashboard/standalone_server.py +7 -3
- attune/mcp/server.py +54 -102
- attune/memory/long_term.py +0 -2
- attune/memory/short_term/__init__.py +84 -0
- attune/memory/short_term/base.py +467 -0
- attune/memory/short_term/batch.py +219 -0
- attune/memory/short_term/caching.py +227 -0
- attune/memory/short_term/conflicts.py +265 -0
- attune/memory/short_term/cross_session.py +122 -0
- attune/memory/short_term/facade.py +655 -0
- attune/memory/short_term/pagination.py +215 -0
- attune/memory/short_term/patterns.py +271 -0
- attune/memory/short_term/pubsub.py +286 -0
- attune/memory/short_term/queues.py +244 -0
- attune/memory/short_term/security.py +300 -0
- attune/memory/short_term/sessions.py +250 -0
- attune/memory/short_term/streams.py +249 -0
- attune/memory/short_term/timelines.py +234 -0
- attune/memory/short_term/transactions.py +186 -0
- attune/memory/short_term/working.py +252 -0
- attune/meta_workflows/cli_commands/__init__.py +3 -0
- attune/meta_workflows/cli_commands/agent_commands.py +0 -4
- attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
- attune/meta_workflows/cli_commands/config_commands.py +0 -5
- attune/meta_workflows/cli_commands/memory_commands.py +0 -5
- attune/meta_workflows/cli_commands/template_commands.py +0 -5
- attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
- attune/models/adaptive_routing.py +4 -8
- attune/models/auth_cli.py +3 -9
- attune/models/auth_strategy.py +2 -4
- attune/models/telemetry/analytics.py +0 -2
- attune/models/telemetry/backend.py +0 -3
- attune/models/telemetry/storage.py +0 -2
- attune/orchestration/_strategies/__init__.py +156 -0
- attune/orchestration/_strategies/base.py +231 -0
- attune/orchestration/_strategies/conditional_strategies.py +373 -0
- attune/orchestration/_strategies/conditions.py +369 -0
- attune/orchestration/_strategies/core_strategies.py +491 -0
- attune/orchestration/_strategies/data_classes.py +64 -0
- attune/orchestration/_strategies/nesting.py +233 -0
- attune/orchestration/execution_strategies.py +58 -1567
- attune/orchestration/meta_orchestrator.py +1 -3
- attune/project_index/scanner.py +1 -3
- attune/project_index/scanner_parallel.py +7 -5
- attune/socratic_router.py +1 -3
- attune/telemetry/agent_coordination.py +9 -3
- attune/telemetry/agent_tracking.py +16 -3
- attune/telemetry/approval_gates.py +22 -5
- attune/telemetry/cli.py +1 -3
- attune/telemetry/commands/dashboard_commands.py +24 -8
- attune/telemetry/event_streaming.py +8 -2
- attune/telemetry/feedback_loop.py +10 -2
- attune/tools.py +1 -0
- attune/workflow_commands.py +1 -3
- attune/workflows/__init__.py +53 -10
- attune/workflows/autonomous_test_gen.py +158 -102
- attune/workflows/base.py +48 -672
- attune/workflows/batch_processing.py +1 -3
- attune/workflows/compat.py +156 -0
- attune/workflows/cost_mixin.py +141 -0
- attune/workflows/data_classes.py +92 -0
- attune/workflows/document_gen/workflow.py +11 -14
- attune/workflows/history.py +62 -37
- attune/workflows/llm_base.py +1 -3
- attune/workflows/migration.py +422 -0
- attune/workflows/output.py +2 -7
- attune/workflows/parsing_mixin.py +427 -0
- attune/workflows/perf_audit.py +3 -1
- attune/workflows/progress.py +9 -11
- attune/workflows/release_prep.py +5 -1
- attune/workflows/routing.py +0 -2
- attune/workflows/secure_release.py +2 -1
- attune/workflows/security_audit.py +19 -14
- attune/workflows/security_audit_phase3.py +28 -22
- attune/workflows/seo_optimization.py +27 -27
- attune/workflows/test_gen/test_templates.py +1 -4
- attune/workflows/test_gen/workflow.py +0 -2
- attune/workflows/test_gen_behavioral.py +6 -19
- attune/workflows/test_gen_parallel.py +6 -4
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/RECORD +116 -91
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
- attune_healthcare/monitors/monitoring/__init__.py +9 -9
- attune_llm/agent_factory/__init__.py +6 -6
- attune_llm/commands/__init__.py +10 -10
- attune_llm/commands/models.py +3 -3
- attune_llm/config/__init__.py +8 -8
- attune_llm/learning/__init__.py +3 -3
- attune_llm/learning/extractor.py +5 -3
- attune_llm/learning/storage.py +5 -3
- attune_llm/security/__init__.py +17 -17
- attune_llm/utils/tokens.py +3 -1
- attune/cli_legacy.py +0 -3978
- attune/memory/short_term.py +0 -2192
- attune/workflows/manage_docs.py +0 -87
- attune/workflows/test5.py +0 -125
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Base class for agent composition strategies.
|
|
2
|
+
|
|
3
|
+
This module defines the ExecutionStrategy abstract base class that all
|
|
4
|
+
strategy implementations must inherit from.
|
|
5
|
+
|
|
6
|
+
Security:
|
|
7
|
+
- All agent outputs validated before passing to next agent
|
|
8
|
+
- No eval() or exec() usage
|
|
9
|
+
- Timeout enforcement at strategy level
|
|
10
|
+
|
|
11
|
+
Copyright 2025 Smart-AI-Memory
|
|
12
|
+
Licensed under Fair Source License 0.9
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from .data_classes import AgentResult, StrategyResult
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ..agent_templates import AgentTemplate
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExecutionStrategy(ABC):
|
|
30
|
+
"""Base class for agent composition strategies.
|
|
31
|
+
|
|
32
|
+
All strategies must implement execute() method to define
|
|
33
|
+
how agents are coordinated and results aggregated.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def execute(
|
|
38
|
+
self, agents: list[AgentTemplate], context: dict[str, Any]
|
|
39
|
+
) -> StrategyResult:
|
|
40
|
+
"""Execute agents using this strategy.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
agents: List of agent templates to execute
|
|
44
|
+
context: Initial context for execution
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
StrategyResult with aggregated outputs
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If agents list is empty
|
|
51
|
+
TimeoutError: If execution exceeds timeout
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
async def _execute_agent(
|
|
56
|
+
self, agent: AgentTemplate, context: dict[str, Any]
|
|
57
|
+
) -> AgentResult:
|
|
58
|
+
"""Execute a single agent with real analysis tools.
|
|
59
|
+
|
|
60
|
+
Maps agent capabilities to real tool implementations and executes them.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
agent: Agent template to execute
|
|
64
|
+
context: Execution context
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
AgentResult with execution outcome
|
|
68
|
+
"""
|
|
69
|
+
import time
|
|
70
|
+
|
|
71
|
+
from ..real_tools import (
|
|
72
|
+
RealCodeQualityAnalyzer,
|
|
73
|
+
RealCoverageAnalyzer,
|
|
74
|
+
RealDocumentationAnalyzer,
|
|
75
|
+
RealSecurityAuditor,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
logger.info(f"Executing agent: {agent.id} ({agent.role})")
|
|
79
|
+
start_time = time.perf_counter()
|
|
80
|
+
|
|
81
|
+
# Get project root from context
|
|
82
|
+
project_root = context.get("project_root", ".")
|
|
83
|
+
target_path = context.get("target_path", "src")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Map agent ID to real tool implementation
|
|
87
|
+
if agent.id == "security_auditor" or "security" in agent.role.lower():
|
|
88
|
+
auditor = RealSecurityAuditor(project_root)
|
|
89
|
+
report = auditor.audit(target_path)
|
|
90
|
+
|
|
91
|
+
output = {
|
|
92
|
+
"agent_role": agent.role,
|
|
93
|
+
"total_issues": report.total_issues,
|
|
94
|
+
"critical_issues": report.critical_count, # Match workflow field name
|
|
95
|
+
"high_issues": report.high_count, # Match workflow field name
|
|
96
|
+
"medium_issues": report.medium_count, # Match workflow field name
|
|
97
|
+
"passed": report.passed,
|
|
98
|
+
"issues_by_file": report.issues_by_file,
|
|
99
|
+
}
|
|
100
|
+
success = report.passed
|
|
101
|
+
confidence = 1.0 if report.total_issues == 0 else 0.7
|
|
102
|
+
|
|
103
|
+
elif agent.id == "test_coverage_analyzer" or "coverage" in agent.role.lower():
|
|
104
|
+
analyzer = RealCoverageAnalyzer(project_root)
|
|
105
|
+
report = analyzer.analyze() # Analyzes all packages automatically
|
|
106
|
+
|
|
107
|
+
output = {
|
|
108
|
+
"agent_role": agent.role,
|
|
109
|
+
"coverage_percent": report.total_coverage, # Match workflow field name
|
|
110
|
+
"total_coverage": report.total_coverage, # Keep for compatibility
|
|
111
|
+
"files_analyzed": report.files_analyzed,
|
|
112
|
+
"uncovered_files": report.uncovered_files,
|
|
113
|
+
"passed": report.total_coverage >= 80.0,
|
|
114
|
+
}
|
|
115
|
+
success = report.total_coverage >= 80.0
|
|
116
|
+
confidence = min(report.total_coverage / 100.0, 1.0)
|
|
117
|
+
|
|
118
|
+
elif agent.id == "code_reviewer" or "quality" in agent.role.lower():
|
|
119
|
+
analyzer = RealCodeQualityAnalyzer(project_root)
|
|
120
|
+
report = analyzer.analyze(target_path)
|
|
121
|
+
|
|
122
|
+
output = {
|
|
123
|
+
"agent_role": agent.role,
|
|
124
|
+
"quality_score": report.quality_score,
|
|
125
|
+
"ruff_issues": report.ruff_issues,
|
|
126
|
+
"mypy_issues": report.mypy_issues,
|
|
127
|
+
"total_files": report.total_files,
|
|
128
|
+
"passed": report.passed,
|
|
129
|
+
}
|
|
130
|
+
success = report.passed
|
|
131
|
+
confidence = report.quality_score / 10.0
|
|
132
|
+
|
|
133
|
+
elif agent.id == "documentation_writer" or "documentation" in agent.role.lower():
|
|
134
|
+
analyzer = RealDocumentationAnalyzer(project_root)
|
|
135
|
+
report = analyzer.analyze(target_path)
|
|
136
|
+
|
|
137
|
+
output = {
|
|
138
|
+
"agent_role": agent.role,
|
|
139
|
+
"completeness": report.completeness_percentage,
|
|
140
|
+
"coverage_percent": report.completeness_percentage, # Match Release Prep field name
|
|
141
|
+
"total_functions": report.total_functions,
|
|
142
|
+
"documented_functions": report.documented_functions,
|
|
143
|
+
"total_classes": report.total_classes,
|
|
144
|
+
"documented_classes": report.documented_classes,
|
|
145
|
+
"missing_docstrings": report.missing_docstrings,
|
|
146
|
+
"passed": report.passed,
|
|
147
|
+
}
|
|
148
|
+
success = report.passed
|
|
149
|
+
confidence = report.completeness_percentage / 100.0
|
|
150
|
+
|
|
151
|
+
elif agent.id == "performance_optimizer" or "performance" in agent.role.lower():
|
|
152
|
+
# Performance analysis placeholder - mark as passed for now
|
|
153
|
+
# TODO: Implement real performance profiling
|
|
154
|
+
logger.warning("Performance analysis not yet implemented, returning placeholder")
|
|
155
|
+
output = {
|
|
156
|
+
"agent_role": agent.role,
|
|
157
|
+
"message": "Performance analysis not yet implemented",
|
|
158
|
+
"passed": True,
|
|
159
|
+
"placeholder": True,
|
|
160
|
+
}
|
|
161
|
+
success = True
|
|
162
|
+
confidence = 1.0
|
|
163
|
+
|
|
164
|
+
elif agent.id == "test_generator":
|
|
165
|
+
# Test generation requires different handling (LLM-based)
|
|
166
|
+
logger.info("Test generation requires manual invocation, returning placeholder")
|
|
167
|
+
output = {
|
|
168
|
+
"agent_role": agent.role,
|
|
169
|
+
"message": "Test generation requires manual invocation",
|
|
170
|
+
"passed": True,
|
|
171
|
+
}
|
|
172
|
+
success = True
|
|
173
|
+
confidence = 0.8
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
# Unknown agent type - log warning and return placeholder
|
|
177
|
+
logger.warning(f"Unknown agent type: {agent.id}, returning placeholder")
|
|
178
|
+
output = {
|
|
179
|
+
"agent_role": agent.role,
|
|
180
|
+
"agent_id": agent.id,
|
|
181
|
+
"message": "Unknown agent type - no real implementation",
|
|
182
|
+
"passed": True,
|
|
183
|
+
}
|
|
184
|
+
success = True
|
|
185
|
+
confidence = 0.5
|
|
186
|
+
|
|
187
|
+
duration = time.perf_counter() - start_time
|
|
188
|
+
|
|
189
|
+
logger.info(
|
|
190
|
+
f"Agent {agent.id} completed: success={success}, "
|
|
191
|
+
f"confidence={confidence:.2f}, duration={duration:.2f}s"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return AgentResult(
|
|
195
|
+
agent_id=agent.id,
|
|
196
|
+
success=success,
|
|
197
|
+
output=output,
|
|
198
|
+
confidence=confidence,
|
|
199
|
+
duration_seconds=duration,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
duration = time.perf_counter() - start_time
|
|
204
|
+
logger.error(f"Agent {agent.id} failed: {e}")
|
|
205
|
+
|
|
206
|
+
return AgentResult(
|
|
207
|
+
agent_id=agent.id,
|
|
208
|
+
success=False,
|
|
209
|
+
output={"agent_role": agent.role, "error_details": str(e)},
|
|
210
|
+
error=str(e),
|
|
211
|
+
confidence=0.0,
|
|
212
|
+
duration_seconds=duration,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _aggregate_results(self, results: list[AgentResult]) -> dict[str, Any]:
|
|
216
|
+
"""Aggregate results from multiple agents.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
results: List of agent results
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Aggregated output dictionary
|
|
223
|
+
"""
|
|
224
|
+
return {
|
|
225
|
+
"num_agents": len(results),
|
|
226
|
+
"all_succeeded": all(r.success for r in results),
|
|
227
|
+
"avg_confidence": (
|
|
228
|
+
sum(r.confidence for r in results) / len(results) if results else 0.0
|
|
229
|
+
),
|
|
230
|
+
"outputs": [r.output for r in results],
|
|
231
|
+
}
|
|
@@ -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
|
+
)
|