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
@@ -13,9 +13,9 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any
15
15
 
16
- from attune_llm.providers import AnthropicBatchProvider
17
16
  from attune.config import _validate_file_path
18
17
  from attune.models import get_model
18
+ from attune_llm.providers import AnthropicBatchProvider
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -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
- "name": node.name,
1262
- "args": args_list,
1263
- "return_type": return_type,
1264
- "docstring": docstring,
1265
- "lineno": node.lineno,
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
-
@@ -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
- 1
188
- if isinstance(result.final_output, dict)
189
- and result.final_output.get("xml_parsed")
190
- else 0,
191
- result.final_output.get("summary")
192
- if isinstance(result.final_output, dict)
193
- else None,
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")
@@ -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
@@ -331,7 +329,7 @@ class TestGeneratorLLM(LLMWorkflowGenerator):
331
329
  Template test file
332
330
  """
333
331
  module_name = context.get("module_name", "unknown")
334
- module_path = context.get("module_path", "unknown")
332
+ # module_path available in context but not used in basic template
335
333
 
336
334
  return f'''"""Behavioral tests for {module_name}.
337
335