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
|
@@ -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
|
@@ -67,7 +67,8 @@ class WorkflowHistoryStore:
|
|
|
67
67
|
Idempotent - safe to call multiple times.
|
|
68
68
|
"""
|
|
69
69
|
# Main workflow runs table
|
|
70
|
-
self.conn.execute(
|
|
70
|
+
self.conn.execute(
|
|
71
|
+
"""
|
|
71
72
|
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
72
73
|
run_id TEXT PRIMARY KEY,
|
|
73
74
|
workflow_name TEXT NOT NULL,
|
|
@@ -87,10 +88,12 @@ class WorkflowHistoryStore:
|
|
|
87
88
|
summary TEXT,
|
|
88
89
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
89
90
|
)
|
|
90
|
-
"""
|
|
91
|
+
"""
|
|
92
|
+
)
|
|
91
93
|
|
|
92
94
|
# Workflow stages (1:many relationship)
|
|
93
|
-
self.conn.execute(
|
|
95
|
+
self.conn.execute(
|
|
96
|
+
"""
|
|
94
97
|
CREATE TABLE IF NOT EXISTS workflow_stages (
|
|
95
98
|
stage_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
99
|
run_id TEXT NOT NULL,
|
|
@@ -104,34 +107,45 @@ class WorkflowHistoryStore:
|
|
|
104
107
|
output_tokens INTEGER DEFAULT 0,
|
|
105
108
|
FOREIGN KEY (run_id) REFERENCES workflow_runs(run_id)
|
|
106
109
|
)
|
|
107
|
-
"""
|
|
110
|
+
"""
|
|
111
|
+
)
|
|
108
112
|
|
|
109
113
|
# Indexes for common queries
|
|
110
|
-
self.conn.execute(
|
|
114
|
+
self.conn.execute(
|
|
115
|
+
"""
|
|
111
116
|
CREATE INDEX IF NOT EXISTS idx_workflow_name
|
|
112
117
|
ON workflow_runs(workflow_name)
|
|
113
|
-
"""
|
|
118
|
+
"""
|
|
119
|
+
)
|
|
114
120
|
|
|
115
|
-
self.conn.execute(
|
|
121
|
+
self.conn.execute(
|
|
122
|
+
"""
|
|
116
123
|
CREATE INDEX IF NOT EXISTS idx_started_at
|
|
117
124
|
ON workflow_runs(started_at DESC)
|
|
118
|
-
"""
|
|
125
|
+
"""
|
|
126
|
+
)
|
|
119
127
|
|
|
120
|
-
self.conn.execute(
|
|
128
|
+
self.conn.execute(
|
|
129
|
+
"""
|
|
121
130
|
CREATE INDEX IF NOT EXISTS idx_provider
|
|
122
131
|
ON workflow_runs(provider)
|
|
123
|
-
"""
|
|
132
|
+
"""
|
|
133
|
+
)
|
|
124
134
|
|
|
125
|
-
self.conn.execute(
|
|
135
|
+
self.conn.execute(
|
|
136
|
+
"""
|
|
126
137
|
CREATE INDEX IF NOT EXISTS idx_success
|
|
127
138
|
ON workflow_runs(success)
|
|
128
|
-
"""
|
|
139
|
+
"""
|
|
140
|
+
)
|
|
129
141
|
|
|
130
142
|
# Index for stage queries
|
|
131
|
-
self.conn.execute(
|
|
143
|
+
self.conn.execute(
|
|
144
|
+
"""
|
|
132
145
|
CREATE INDEX IF NOT EXISTS idx_run_stages
|
|
133
146
|
ON workflow_stages(run_id)
|
|
134
|
-
"""
|
|
147
|
+
"""
|
|
148
|
+
)
|
|
135
149
|
|
|
136
150
|
self.conn.commit()
|
|
137
151
|
logger.debug(f"History store initialized: {self.db_path}")
|
|
@@ -184,13 +198,18 @@ class WorkflowHistoryStore:
|
|
|
184
198
|
result.error,
|
|
185
199
|
result.error_type,
|
|
186
200
|
1 if result.transient else 0,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
(
|
|
202
|
+
1
|
|
203
|
+
if isinstance(result.final_output, dict)
|
|
204
|
+
and result.final_output.get("xml_parsed")
|
|
205
|
+
else 0
|
|
206
|
+
),
|
|
207
|
+
(
|
|
208
|
+
str(result.final_output.get("summary"))
|
|
209
|
+
if isinstance(result.final_output, dict)
|
|
210
|
+
and result.final_output.get("summary") is not None
|
|
211
|
+
else None
|
|
212
|
+
),
|
|
194
213
|
),
|
|
195
214
|
)
|
|
196
215
|
|
|
@@ -328,7 +347,8 @@ class WorkflowHistoryStore:
|
|
|
328
347
|
cursor = self.conn.cursor()
|
|
329
348
|
|
|
330
349
|
# Total runs by workflow
|
|
331
|
-
cursor.execute(
|
|
350
|
+
cursor.execute(
|
|
351
|
+
"""
|
|
332
352
|
SELECT
|
|
333
353
|
workflow_name,
|
|
334
354
|
COUNT(*) as runs,
|
|
@@ -337,41 +357,49 @@ class WorkflowHistoryStore:
|
|
|
337
357
|
SUM(success) as successful
|
|
338
358
|
FROM workflow_runs
|
|
339
359
|
GROUP BY workflow_name
|
|
340
|
-
"""
|
|
360
|
+
"""
|
|
361
|
+
)
|
|
341
362
|
by_workflow = {row["workflow_name"]: dict(row) for row in cursor.fetchall()}
|
|
342
363
|
|
|
343
364
|
# Total runs by provider
|
|
344
|
-
cursor.execute(
|
|
365
|
+
cursor.execute(
|
|
366
|
+
"""
|
|
345
367
|
SELECT
|
|
346
368
|
provider,
|
|
347
369
|
COUNT(*) as runs,
|
|
348
370
|
SUM(total_cost) as cost
|
|
349
371
|
FROM workflow_runs
|
|
350
372
|
GROUP BY provider
|
|
351
|
-
"""
|
|
373
|
+
"""
|
|
374
|
+
)
|
|
352
375
|
by_provider = {row["provider"]: dict(row) for row in cursor.fetchall()}
|
|
353
376
|
|
|
354
377
|
# Total cost by tier
|
|
355
|
-
cursor.execute(
|
|
378
|
+
cursor.execute(
|
|
379
|
+
"""
|
|
356
380
|
SELECT
|
|
357
381
|
tier,
|
|
358
382
|
SUM(cost) as total_cost
|
|
359
383
|
FROM workflow_stages
|
|
360
384
|
WHERE skipped = 0
|
|
361
385
|
GROUP BY tier
|
|
362
|
-
"""
|
|
386
|
+
"""
|
|
387
|
+
)
|
|
363
388
|
by_tier = {row["tier"]: row["total_cost"] for row in cursor.fetchall()}
|
|
364
389
|
|
|
365
390
|
# Recent runs (last 10)
|
|
366
|
-
cursor.execute(
|
|
391
|
+
cursor.execute(
|
|
392
|
+
"""
|
|
367
393
|
SELECT * FROM workflow_runs
|
|
368
394
|
ORDER BY started_at DESC
|
|
369
395
|
LIMIT 10
|
|
370
|
-
"""
|
|
396
|
+
"""
|
|
397
|
+
)
|
|
371
398
|
recent_runs = [dict(row) for row in cursor.fetchall()]
|
|
372
399
|
|
|
373
400
|
# Totals
|
|
374
|
-
cursor.execute(
|
|
401
|
+
cursor.execute(
|
|
402
|
+
"""
|
|
375
403
|
SELECT
|
|
376
404
|
COUNT(*) as total_runs,
|
|
377
405
|
SUM(success) as successful_runs,
|
|
@@ -379,7 +407,8 @@ class WorkflowHistoryStore:
|
|
|
379
407
|
SUM(savings) as total_savings,
|
|
380
408
|
AVG(CASE WHEN success = 1 THEN savings_percent ELSE NULL END) as avg_savings_percent
|
|
381
409
|
FROM workflow_runs
|
|
382
|
-
"""
|
|
410
|
+
"""
|
|
411
|
+
)
|
|
383
412
|
totals = dict(cursor.fetchone())
|
|
384
413
|
|
|
385
414
|
return {
|
|
@@ -479,14 +508,10 @@ class WorkflowHistoryStore:
|
|
|
479
508
|
# Security Note: f-string builds placeholder list only ("?, ?, ?")
|
|
480
509
|
# Actual data (run_ids) passed as parameters - SQL injection safe
|
|
481
510
|
placeholders = ",".join("?" * len(run_ids))
|
|
482
|
-
cursor.execute(
|
|
483
|
-
f"DELETE FROM workflow_stages WHERE run_id IN ({placeholders})", run_ids
|
|
484
|
-
)
|
|
511
|
+
cursor.execute(f"DELETE FROM workflow_stages WHERE run_id IN ({placeholders})", run_ids)
|
|
485
512
|
|
|
486
513
|
# Delete runs (same safe parameterization pattern)
|
|
487
|
-
cursor.execute(
|
|
488
|
-
f"DELETE FROM workflow_runs WHERE run_id IN ({placeholders})", run_ids
|
|
489
|
-
)
|
|
514
|
+
cursor.execute(f"DELETE FROM workflow_runs WHERE run_id IN ({placeholders})", run_ids)
|
|
490
515
|
|
|
491
516
|
self.conn.commit()
|
|
492
517
|
logger.info(f"Cleaned up {len(run_ids)} runs older than {keep_days} days")
|
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
|