attune-ai 2.1.5__py3-none-any.whl → 2.2.1__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 +8 -16
- 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 +465 -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 +653 -0
- attune/memory/short_term/pagination.py +207 -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 +242 -0
- attune/memory/short_term/timelines.py +234 -0
- attune/memory/short_term/transactions.py +184 -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/meta_workflows/plan_generator.py +2 -4
- 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/monitoring/alerts.py +6 -10
- attune/orchestration/_strategies/__init__.py +156 -0
- attune/orchestration/_strategies/base.py +227 -0
- attune/orchestration/_strategies/conditional_strategies.py +365 -0
- attune/orchestration/_strategies/conditions.py +369 -0
- attune/orchestration/_strategies/core_strategies.py +479 -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/storage.py +2 -4
- 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 +2 -1
- attune/workflow_commands.py +1 -3
- attune/workflow_patterns/structural.py +4 -8
- attune/workflows/__init__.py +54 -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 +16 -9
- attune/workflows/llm_base.py +1 -3
- attune/workflows/migration.py +432 -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 +4 -1
- attune/workflows/security_audit.py +20 -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 +8 -6
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/METADATA +4 -3
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/RECORD +121 -96
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.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/agent_factory/adapters/haystack_adapter.py +1 -4
- 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.1.dist-info}/WHEEL +0 -0
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE +0 -0
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/top_level.txt +0 -0
|
@@ -192,9 +192,7 @@ class BatchProcessingWorkflow:
|
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
elif result_type == "expired":
|
|
195
|
-
results.append(
|
|
196
|
-
BatchResult(task_id=task_id, success=False, error="Request expired")
|
|
197
|
-
)
|
|
195
|
+
results.append(BatchResult(task_id=task_id, success=False, error="Request expired"))
|
|
198
196
|
|
|
199
197
|
elif result_type == "canceled":
|
|
200
198
|
results.append(
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Backward compatibility module for deprecated workflow enums.
|
|
2
|
+
|
|
3
|
+
This module contains deprecated enums that were originally in base.py:
|
|
4
|
+
- ModelTier: Use attune.models.ModelTier instead
|
|
5
|
+
- ModelProvider: Use attune.models.ModelProvider instead
|
|
6
|
+
|
|
7
|
+
These are maintained for backward compatibility only.
|
|
8
|
+
New code should use attune.models imports directly.
|
|
9
|
+
|
|
10
|
+
Migration guide:
|
|
11
|
+
# Old (deprecated):
|
|
12
|
+
from attune.workflows.base import ModelTier, ModelProvider
|
|
13
|
+
|
|
14
|
+
# New (recommended):
|
|
15
|
+
from attune.models import ModelTier, ModelProvider
|
|
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 warnings
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
# Import unified types for conversion
|
|
28
|
+
from attune.models import ModelProvider as UnifiedModelProvider
|
|
29
|
+
from attune.models import ModelTier as UnifiedModelTier
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelTier(Enum):
|
|
36
|
+
"""DEPRECATED: Model tier for cost optimization.
|
|
37
|
+
|
|
38
|
+
This enum is deprecated and will be removed in v5.0.
|
|
39
|
+
Use attune.models.ModelTier instead.
|
|
40
|
+
|
|
41
|
+
Migration:
|
|
42
|
+
# Old:
|
|
43
|
+
from attune.workflows.base import ModelTier
|
|
44
|
+
|
|
45
|
+
# New:
|
|
46
|
+
from attune.models import ModelTier
|
|
47
|
+
|
|
48
|
+
Why deprecated:
|
|
49
|
+
- Creates confusion with dual definitions
|
|
50
|
+
- attune.models.ModelTier is the canonical location
|
|
51
|
+
- Simplifies imports and reduces duplication
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
CHEAP = "cheap" # Haiku/GPT-4o-mini - $0.25-1.25/M tokens
|
|
55
|
+
CAPABLE = "capable" # Sonnet/GPT-4o - $3-15/M tokens
|
|
56
|
+
PREMIUM = "premium" # Opus/o1 - $15-75/M tokens
|
|
57
|
+
|
|
58
|
+
def __init__(self, value: str):
|
|
59
|
+
"""Initialize with deprecation warning."""
|
|
60
|
+
# Only warn once per process, not per instance
|
|
61
|
+
if not hasattr(self.__class__, "_deprecation_warned"):
|
|
62
|
+
warnings.warn(
|
|
63
|
+
"workflows.base.ModelTier is deprecated and will be removed in v5.0. "
|
|
64
|
+
"Use attune.models.ModelTier instead. "
|
|
65
|
+
"Update imports: from attune.models import ModelTier",
|
|
66
|
+
DeprecationWarning,
|
|
67
|
+
stacklevel=4,
|
|
68
|
+
)
|
|
69
|
+
self.__class__._deprecation_warned = True
|
|
70
|
+
|
|
71
|
+
def to_unified(self) -> UnifiedModelTier:
|
|
72
|
+
"""Convert to unified ModelTier from attune.models."""
|
|
73
|
+
return UnifiedModelTier(self.value)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ModelProvider(Enum):
|
|
77
|
+
"""DEPRECATED: Supported model providers.
|
|
78
|
+
|
|
79
|
+
This enum is deprecated and will be removed in v5.0.
|
|
80
|
+
Use attune.models.ModelProvider instead.
|
|
81
|
+
|
|
82
|
+
Migration:
|
|
83
|
+
# Old:
|
|
84
|
+
from attune.workflows.base import ModelProvider
|
|
85
|
+
|
|
86
|
+
# New:
|
|
87
|
+
from attune.models import ModelProvider
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
ANTHROPIC = "anthropic"
|
|
91
|
+
OPENAI = "openai"
|
|
92
|
+
GOOGLE = "google" # Google Gemini models
|
|
93
|
+
OLLAMA = "ollama"
|
|
94
|
+
HYBRID = "hybrid" # Mix of best models from different providers
|
|
95
|
+
CUSTOM = "custom" # User-defined custom models
|
|
96
|
+
|
|
97
|
+
def to_unified(self) -> UnifiedModelProvider:
|
|
98
|
+
"""Convert to unified ModelProvider from attune.models.
|
|
99
|
+
|
|
100
|
+
As of v5.0.0, framework is Claude-native. All providers map to ANTHROPIC.
|
|
101
|
+
"""
|
|
102
|
+
# v5.0.0: Framework is Claude-native, only ANTHROPIC supported
|
|
103
|
+
return UnifiedModelProvider.ANTHROPIC
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_provider_models() -> dict[ModelProvider, dict[ModelTier, str]]:
|
|
107
|
+
"""Build PROVIDER_MODELS from MODEL_REGISTRY.
|
|
108
|
+
|
|
109
|
+
This ensures PROVIDER_MODELS stays in sync with the single source of truth.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dictionary mapping ModelProvider -> ModelTier -> model_id
|
|
113
|
+
"""
|
|
114
|
+
# Lazy import to avoid circular dependencies
|
|
115
|
+
from attune.models import MODEL_REGISTRY
|
|
116
|
+
|
|
117
|
+
result: dict[ModelProvider, dict[ModelTier, str]] = {}
|
|
118
|
+
|
|
119
|
+
# Map string provider names to ModelProvider enum
|
|
120
|
+
provider_map = {
|
|
121
|
+
"anthropic": ModelProvider.ANTHROPIC,
|
|
122
|
+
"openai": ModelProvider.OPENAI,
|
|
123
|
+
"google": ModelProvider.GOOGLE,
|
|
124
|
+
"ollama": ModelProvider.OLLAMA,
|
|
125
|
+
"hybrid": ModelProvider.HYBRID,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Map string tier names to ModelTier enum
|
|
129
|
+
tier_map = {
|
|
130
|
+
"cheap": ModelTier.CHEAP,
|
|
131
|
+
"capable": ModelTier.CAPABLE,
|
|
132
|
+
"premium": ModelTier.PREMIUM,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for provider_str, tiers in MODEL_REGISTRY.items():
|
|
136
|
+
if provider_str not in provider_map:
|
|
137
|
+
continue # Skip custom providers
|
|
138
|
+
provider_enum = provider_map[provider_str]
|
|
139
|
+
result[provider_enum] = {}
|
|
140
|
+
for tier_str, model_info in tiers.items():
|
|
141
|
+
if tier_str in tier_map:
|
|
142
|
+
result[provider_enum][tier_map[tier_str]] = model_info.id
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Build PROVIDER_MODELS at module load time
|
|
148
|
+
PROVIDER_MODELS: dict[ModelProvider, dict[ModelTier, str]] = _build_provider_models()
|
|
149
|
+
|
|
150
|
+
# Expose all public symbols
|
|
151
|
+
__all__ = [
|
|
152
|
+
"ModelTier",
|
|
153
|
+
"ModelProvider",
|
|
154
|
+
"PROVIDER_MODELS",
|
|
155
|
+
"_build_provider_models",
|
|
156
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Cost tracking mixin for workflow classes.
|
|
2
|
+
|
|
3
|
+
This module provides methods for calculating and reporting workflow costs,
|
|
4
|
+
including tier-based pricing, baseline comparisons, and cache savings.
|
|
5
|
+
|
|
6
|
+
Extracted from base.py for improved maintainability and import performance.
|
|
7
|
+
|
|
8
|
+
Copyright 2025 Smart-AI-Memory
|
|
9
|
+
Licensed under Fair Source License 0.9
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .data_classes import CostReport
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CostTrackingMixin:
|
|
21
|
+
"""Mixin providing cost tracking capabilities for workflows.
|
|
22
|
+
|
|
23
|
+
This mixin adds methods for calculating costs, baseline comparisons,
|
|
24
|
+
and generating cost reports with cache savings.
|
|
25
|
+
|
|
26
|
+
Methods:
|
|
27
|
+
_calculate_cost: Calculate cost for a stage based on tier and tokens
|
|
28
|
+
_calculate_baseline_cost: Calculate premium-tier baseline cost
|
|
29
|
+
_generate_cost_report: Generate comprehensive cost report
|
|
30
|
+
|
|
31
|
+
Note:
|
|
32
|
+
This mixin expects the class to have:
|
|
33
|
+
- _stages_run: list[WorkflowStage] - stages executed in workflow
|
|
34
|
+
- _get_cache_stats(): method returning cache statistics dict
|
|
35
|
+
- ModelTier enum available
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# These will be provided by the main class
|
|
39
|
+
_stages_run: list[Any]
|
|
40
|
+
|
|
41
|
+
def _calculate_cost(self, tier: Any, input_tokens: int, output_tokens: int) -> float:
|
|
42
|
+
"""Calculate cost for a stage.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
tier: ModelTier enum value for the stage
|
|
46
|
+
input_tokens: Number of input tokens used
|
|
47
|
+
output_tokens: Number of output tokens generated
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Total cost in dollars for this stage
|
|
51
|
+
"""
|
|
52
|
+
from attune.cost_tracker import MODEL_PRICING
|
|
53
|
+
|
|
54
|
+
tier_name = tier.value
|
|
55
|
+
pricing = MODEL_PRICING.get(tier_name, MODEL_PRICING["capable"])
|
|
56
|
+
input_cost = (input_tokens / 1_000_000) * pricing["input"]
|
|
57
|
+
output_cost = (output_tokens / 1_000_000) * pricing["output"]
|
|
58
|
+
return input_cost + output_cost
|
|
59
|
+
|
|
60
|
+
def _calculate_baseline_cost(self, input_tokens: int, output_tokens: int) -> float:
|
|
61
|
+
"""Calculate what the cost would be using premium tier.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
input_tokens: Number of input tokens used
|
|
65
|
+
output_tokens: Number of output tokens generated
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Cost in dollars if premium tier was used
|
|
69
|
+
"""
|
|
70
|
+
from attune.cost_tracker import MODEL_PRICING
|
|
71
|
+
|
|
72
|
+
pricing = MODEL_PRICING["premium"]
|
|
73
|
+
input_cost = (input_tokens / 1_000_000) * pricing["input"]
|
|
74
|
+
output_cost = (output_tokens / 1_000_000) * pricing["output"]
|
|
75
|
+
return input_cost + output_cost
|
|
76
|
+
|
|
77
|
+
def _generate_cost_report(self) -> CostReport:
|
|
78
|
+
"""Generate cost report from completed stages.
|
|
79
|
+
|
|
80
|
+
Calculates total costs, baseline comparisons, savings percentages,
|
|
81
|
+
and includes cache performance metrics.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
CostReport with comprehensive cost breakdown and savings analysis
|
|
85
|
+
"""
|
|
86
|
+
from .data_classes import CostReport
|
|
87
|
+
|
|
88
|
+
total_cost = 0.0
|
|
89
|
+
baseline_cost = 0.0
|
|
90
|
+
by_stage: dict[str, float] = {}
|
|
91
|
+
by_tier: dict[str, float] = {}
|
|
92
|
+
|
|
93
|
+
for stage in self._stages_run:
|
|
94
|
+
if stage.skipped:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
total_cost += stage.cost
|
|
98
|
+
by_stage[stage.name] = stage.cost
|
|
99
|
+
|
|
100
|
+
tier_name = stage.tier.value
|
|
101
|
+
by_tier[tier_name] = by_tier.get(tier_name, 0.0) + stage.cost
|
|
102
|
+
|
|
103
|
+
# Calculate what this would cost at premium tier
|
|
104
|
+
baseline_cost += self._calculate_baseline_cost(stage.input_tokens, stage.output_tokens)
|
|
105
|
+
|
|
106
|
+
savings = baseline_cost - total_cost
|
|
107
|
+
savings_percent = (savings / baseline_cost * 100) if baseline_cost > 0 else 0.0
|
|
108
|
+
|
|
109
|
+
# Calculate cache metrics using CachingMixin
|
|
110
|
+
cache_stats = self._get_cache_stats()
|
|
111
|
+
cache_hits = cache_stats["hits"]
|
|
112
|
+
cache_misses = cache_stats["misses"]
|
|
113
|
+
cache_hit_rate = cache_stats["hit_rate"]
|
|
114
|
+
estimated_cost_without_cache = total_cost
|
|
115
|
+
savings_from_cache = 0.0
|
|
116
|
+
|
|
117
|
+
# Estimate cost without cache (assumes cache hits would have incurred full cost)
|
|
118
|
+
if cache_hits > 0:
|
|
119
|
+
avg_cost_per_call = total_cost / cache_misses if cache_misses > 0 else 0.0
|
|
120
|
+
estimated_additional_cost = cache_hits * avg_cost_per_call
|
|
121
|
+
estimated_cost_without_cache = total_cost + estimated_additional_cost
|
|
122
|
+
savings_from_cache = estimated_additional_cost
|
|
123
|
+
|
|
124
|
+
return CostReport(
|
|
125
|
+
total_cost=total_cost,
|
|
126
|
+
baseline_cost=baseline_cost,
|
|
127
|
+
savings=savings,
|
|
128
|
+
savings_percent=savings_percent,
|
|
129
|
+
by_stage=by_stage,
|
|
130
|
+
by_tier=by_tier,
|
|
131
|
+
cache_hits=cache_hits,
|
|
132
|
+
cache_misses=cache_misses,
|
|
133
|
+
cache_hit_rate=cache_hit_rate,
|
|
134
|
+
estimated_cost_without_cache=estimated_cost_without_cache,
|
|
135
|
+
savings_from_cache=savings_from_cache,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _get_cache_stats(self) -> dict[str, Any]:
|
|
139
|
+
"""Get cache statistics. Override in subclass or mixin."""
|
|
140
|
+
# Default implementation - CachingMixin provides the real one
|
|
141
|
+
return {"hits": 0, "misses": 0, "hit_rate": 0.0}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Data classes for workflow execution.
|
|
2
|
+
|
|
3
|
+
This module contains the core data structures used across all workflows:
|
|
4
|
+
- WorkflowStage: Represents a single stage in a workflow
|
|
5
|
+
- CostReport: Cost breakdown for a workflow execution
|
|
6
|
+
- StageQualityMetrics: Quality metrics for stage output validation
|
|
7
|
+
- WorkflowResult: Result of a workflow execution
|
|
8
|
+
|
|
9
|
+
Copyright 2025 Smart-AI-Memory
|
|
10
|
+
Licensed under Fair Source License 0.9
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
# Use unified ModelTier for type hints (avoids circular imports)
|
|
21
|
+
from attune.models import ModelTier
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class WorkflowStage:
|
|
26
|
+
"""Represents a single stage in a workflow."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
tier: ModelTier
|
|
30
|
+
description: str
|
|
31
|
+
input_tokens: int = 0
|
|
32
|
+
output_tokens: int = 0
|
|
33
|
+
cost: float = 0.0
|
|
34
|
+
result: Any = None
|
|
35
|
+
duration_ms: int = 0
|
|
36
|
+
skipped: bool = False
|
|
37
|
+
skip_reason: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CostReport:
|
|
42
|
+
"""Cost breakdown for a workflow execution."""
|
|
43
|
+
|
|
44
|
+
total_cost: float
|
|
45
|
+
baseline_cost: float # If all stages used premium
|
|
46
|
+
savings: float
|
|
47
|
+
savings_percent: float
|
|
48
|
+
by_stage: dict[str, float] = field(default_factory=dict)
|
|
49
|
+
by_tier: dict[str, float] = field(default_factory=dict)
|
|
50
|
+
# Cache metrics
|
|
51
|
+
cache_hits: int = 0
|
|
52
|
+
cache_misses: int = 0
|
|
53
|
+
cache_hit_rate: float = 0.0
|
|
54
|
+
estimated_cost_without_cache: float = 0.0
|
|
55
|
+
savings_from_cache: float = 0.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class StageQualityMetrics:
|
|
60
|
+
"""Quality metrics for stage output validation."""
|
|
61
|
+
|
|
62
|
+
execution_succeeded: bool
|
|
63
|
+
output_valid: bool
|
|
64
|
+
quality_improved: bool # Workflow-specific (e.g., health score improved)
|
|
65
|
+
error_type: str | None
|
|
66
|
+
validation_error: str | None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class WorkflowResult:
|
|
71
|
+
"""Result of a workflow execution."""
|
|
72
|
+
|
|
73
|
+
success: bool
|
|
74
|
+
stages: list[WorkflowStage]
|
|
75
|
+
final_output: Any
|
|
76
|
+
cost_report: CostReport
|
|
77
|
+
started_at: datetime
|
|
78
|
+
completed_at: datetime
|
|
79
|
+
total_duration_ms: int
|
|
80
|
+
provider: str = "unknown"
|
|
81
|
+
error: str | None = None
|
|
82
|
+
# Structured error taxonomy for reliability
|
|
83
|
+
error_type: str | None = None # "config" | "runtime" | "provider" | "timeout" | "validation"
|
|
84
|
+
transient: bool = False # True if retry is reasonable (e.g., provider timeout)
|
|
85
|
+
# Optional metadata and summary for extended reporting
|
|
86
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
summary: str | None = None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def duration_seconds(self) -> float:
|
|
91
|
+
"""Get duration in seconds (computed from total_duration_ms)."""
|
|
92
|
+
return self.total_duration_ms / 1000.0
|
|
@@ -377,8 +377,7 @@ class DocumentGenerationWorkflow(BaseWorkflow):
|
|
|
377
377
|
)
|
|
378
378
|
else: # API
|
|
379
379
|
logger.info(
|
|
380
|
-
f"Cost: ~${cost_estimate['monetary_cost']:.4f} "
|
|
381
|
-
f"(1M context window)"
|
|
380
|
+
f"Cost: ~${cost_estimate['monetary_cost']:.4f} " f"(1M context window)"
|
|
382
381
|
)
|
|
383
382
|
|
|
384
383
|
except Exception as e:
|
|
@@ -1257,13 +1256,15 @@ Make all code examples complete and executable."""
|
|
|
1257
1256
|
# Extract docstring
|
|
1258
1257
|
docstring = ast.get_docstring(node) or ""
|
|
1259
1258
|
|
|
1260
|
-
functions.append(
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1259
|
+
functions.append(
|
|
1260
|
+
{
|
|
1261
|
+
"name": node.name,
|
|
1262
|
+
"args": args_list,
|
|
1263
|
+
"return_type": return_type,
|
|
1264
|
+
"docstring": docstring,
|
|
1265
|
+
"lineno": node.lineno,
|
|
1266
|
+
}
|
|
1267
|
+
)
|
|
1267
1268
|
|
|
1268
1269
|
return functions
|
|
1269
1270
|
|
|
@@ -1407,9 +1408,7 @@ None
|
|
|
1407
1408
|
func_name = func_info["name"]
|
|
1408
1409
|
logger.debug(f"Generating API reference for {func_name}()")
|
|
1409
1410
|
|
|
1410
|
-
api_section = await self._generate_api_section_for_function(
|
|
1411
|
-
func_info, tier
|
|
1412
|
-
)
|
|
1411
|
+
api_section = await self._generate_api_section_for_function(func_info, tier)
|
|
1413
1412
|
api_sections.append(api_section)
|
|
1414
1413
|
|
|
1415
1414
|
# Append API reference section to narrative doc
|
|
@@ -1422,5 +1421,3 @@ None
|
|
|
1422
1421
|
logger.info(f"Added {len(api_sections)} API reference sections")
|
|
1423
1422
|
|
|
1424
1423
|
return full_doc
|
|
1425
|
-
|
|
1426
|
-
|
attune/workflows/history.py
CHANGED
|
@@ -184,13 +184,18 @@ class WorkflowHistoryStore:
|
|
|
184
184
|
result.error,
|
|
185
185
|
result.error_type,
|
|
186
186
|
1 if result.transient else 0,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
(
|
|
188
|
+
1
|
|
189
|
+
if isinstance(result.final_output, dict)
|
|
190
|
+
and result.final_output.get("xml_parsed")
|
|
191
|
+
else 0
|
|
192
|
+
),
|
|
193
|
+
(
|
|
194
|
+
str(result.final_output.get("summary"))
|
|
195
|
+
if isinstance(result.final_output, dict)
|
|
196
|
+
and result.final_output.get("summary") is not None
|
|
197
|
+
else None
|
|
198
|
+
),
|
|
194
199
|
),
|
|
195
200
|
)
|
|
196
201
|
|
|
@@ -480,12 +485,14 @@ class WorkflowHistoryStore:
|
|
|
480
485
|
# Actual data (run_ids) passed as parameters - SQL injection safe
|
|
481
486
|
placeholders = ",".join("?" * len(run_ids))
|
|
482
487
|
cursor.execute(
|
|
483
|
-
f"DELETE FROM workflow_stages WHERE run_id IN ({placeholders})",
|
|
488
|
+
f"DELETE FROM workflow_stages WHERE run_id IN ({placeholders})", # nosec B608
|
|
489
|
+
run_ids,
|
|
484
490
|
)
|
|
485
491
|
|
|
486
492
|
# Delete runs (same safe parameterization pattern)
|
|
487
493
|
cursor.execute(
|
|
488
|
-
f"DELETE FROM workflow_runs WHERE run_id IN ({placeholders})",
|
|
494
|
+
f"DELETE FROM workflow_runs WHERE run_id IN ({placeholders})", # nosec B608
|
|
495
|
+
run_ids,
|
|
489
496
|
)
|
|
490
497
|
|
|
491
498
|
self.conn.commit()
|
attune/workflows/llm_base.py
CHANGED
|
@@ -262,9 +262,7 @@ class LLMWorkflowGenerator(ABC):
|
|
|
262
262
|
# Calculate rates
|
|
263
263
|
total_requests = stats["llm_requests"]
|
|
264
264
|
if total_requests > 0:
|
|
265
|
-
stats["llm_success_rate"] = (
|
|
266
|
-
total_requests - stats["llm_failures"]
|
|
267
|
-
) / total_requests
|
|
265
|
+
stats["llm_success_rate"] = (total_requests - stats["llm_failures"]) / total_requests
|
|
268
266
|
stats["template_fallback_rate"] = stats["template_fallbacks"] / total_requests
|
|
269
267
|
else:
|
|
270
268
|
stats["llm_success_rate"] = 0.0
|