attune-ai 2.1.4__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. attune/cli/__init__.py +3 -55
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +7 -15
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +34 -28
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/core.py +190 -0
  20. attune/dashboard/app.py +4 -2
  21. attune/dashboard/simple_server.py +3 -1
  22. attune/dashboard/standalone_server.py +7 -3
  23. attune/mcp/server.py +54 -102
  24. attune/memory/long_term.py +0 -2
  25. attune/memory/short_term/__init__.py +84 -0
  26. attune/memory/short_term/base.py +467 -0
  27. attune/memory/short_term/batch.py +219 -0
  28. attune/memory/short_term/caching.py +227 -0
  29. attune/memory/short_term/conflicts.py +265 -0
  30. attune/memory/short_term/cross_session.py +122 -0
  31. attune/memory/short_term/facade.py +655 -0
  32. attune/memory/short_term/pagination.py +215 -0
  33. attune/memory/short_term/patterns.py +271 -0
  34. attune/memory/short_term/pubsub.py +286 -0
  35. attune/memory/short_term/queues.py +244 -0
  36. attune/memory/short_term/security.py +300 -0
  37. attune/memory/short_term/sessions.py +250 -0
  38. attune/memory/short_term/streams.py +249 -0
  39. attune/memory/short_term/timelines.py +234 -0
  40. attune/memory/short_term/transactions.py +186 -0
  41. attune/memory/short_term/working.py +252 -0
  42. attune/meta_workflows/cli_commands/__init__.py +3 -0
  43. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  44. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  45. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  48. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  49. attune/meta_workflows/workflow.py +1 -1
  50. attune/models/adaptive_routing.py +4 -8
  51. attune/models/auth_cli.py +3 -9
  52. attune/models/auth_strategy.py +2 -4
  53. attune/models/provider_config.py +20 -1
  54. attune/models/telemetry/analytics.py +0 -2
  55. attune/models/telemetry/backend.py +0 -3
  56. attune/models/telemetry/storage.py +0 -2
  57. attune/orchestration/_strategies/__init__.py +156 -0
  58. attune/orchestration/_strategies/base.py +231 -0
  59. attune/orchestration/_strategies/conditional_strategies.py +373 -0
  60. attune/orchestration/_strategies/conditions.py +369 -0
  61. attune/orchestration/_strategies/core_strategies.py +491 -0
  62. attune/orchestration/_strategies/data_classes.py +64 -0
  63. attune/orchestration/_strategies/nesting.py +233 -0
  64. attune/orchestration/execution_strategies.py +58 -1567
  65. attune/orchestration/meta_orchestrator.py +1 -3
  66. attune/project_index/scanner.py +1 -3
  67. attune/project_index/scanner_parallel.py +7 -5
  68. attune/socratic_router.py +1 -3
  69. attune/telemetry/agent_coordination.py +9 -3
  70. attune/telemetry/agent_tracking.py +16 -3
  71. attune/telemetry/approval_gates.py +22 -5
  72. attune/telemetry/cli.py +3 -3
  73. attune/telemetry/commands/dashboard_commands.py +24 -8
  74. attune/telemetry/event_streaming.py +8 -2
  75. attune/telemetry/feedback_loop.py +10 -2
  76. attune/tools.py +1 -0
  77. attune/workflow_commands.py +1 -3
  78. attune/workflows/__init__.py +53 -10
  79. attune/workflows/autonomous_test_gen.py +160 -104
  80. attune/workflows/base.py +48 -664
  81. attune/workflows/batch_processing.py +2 -4
  82. attune/workflows/compat.py +156 -0
  83. attune/workflows/cost_mixin.py +141 -0
  84. attune/workflows/data_classes.py +92 -0
  85. attune/workflows/document_gen/workflow.py +11 -14
  86. attune/workflows/history.py +62 -37
  87. attune/workflows/llm_base.py +2 -4
  88. attune/workflows/migration.py +422 -0
  89. attune/workflows/output.py +3 -9
  90. attune/workflows/parsing_mixin.py +427 -0
  91. attune/workflows/perf_audit.py +3 -1
  92. attune/workflows/progress.py +10 -13
  93. attune/workflows/release_prep.py +5 -1
  94. attune/workflows/routing.py +0 -2
  95. attune/workflows/secure_release.py +2 -1
  96. attune/workflows/security_audit.py +19 -14
  97. attune/workflows/security_audit_phase3.py +28 -22
  98. attune/workflows/seo_optimization.py +29 -29
  99. attune/workflows/test_gen/test_templates.py +1 -4
  100. attune/workflows/test_gen/workflow.py +0 -2
  101. attune/workflows/test_gen_behavioral.py +7 -20
  102. attune/workflows/test_gen_parallel.py +6 -4
  103. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
  104. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/RECORD +119 -94
  105. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
  106. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  107. attune_llm/agent_factory/__init__.py +6 -6
  108. attune_llm/commands/__init__.py +10 -10
  109. attune_llm/commands/models.py +3 -3
  110. attune_llm/config/__init__.py +8 -8
  111. attune_llm/learning/__init__.py +3 -3
  112. attune_llm/learning/extractor.py +5 -3
  113. attune_llm/learning/storage.py +5 -3
  114. attune_llm/security/__init__.py +17 -17
  115. attune_llm/utils/tokens.py +3 -1
  116. attune/cli_legacy.py +0 -3957
  117. attune/memory/short_term.py +0 -2192
  118. attune/workflows/manage_docs.py +0 -87
  119. attune/workflows/test5.py +0 -125
  120. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
  121. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
  122. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  123. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,252 @@
1
+ """Working memory operations - stash and retrieve.
2
+
3
+ This module provides the primary interface for storing and retrieving
4
+ agent working memory:
5
+ - Stash: Store data with optional TTL and metadata
6
+ - Retrieve: Get data by key
7
+ - Clear: Remove all working memory for an agent
8
+
9
+ Key Prefix: PREFIX_WORKING = "empathy:working:"
10
+
11
+ Classes:
12
+ WorkingMemory: Core stash/retrieve operations
13
+
14
+ Example:
15
+ >>> from attune.memory.short_term.working import WorkingMemory
16
+ >>> from attune.memory.types import AgentCredentials, AccessTier
17
+ >>> # Typically composed into RedisShortTermMemory facade
18
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
19
+ >>> working = WorkingMemory(base_ops, security_sanitizer)
20
+ >>> working.stash("key", {"data": 123}, creds)
21
+ >>> result = working.retrieve("key", creds)
22
+
23
+ Copyright 2025 Smart-AI-Memory
24
+ Licensed under Fair Source License 0.9
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ from datetime import datetime
31
+ from typing import TYPE_CHECKING, Any
32
+
33
+ import structlog
34
+
35
+ from attune.memory.types import (
36
+ AgentCredentials,
37
+ TTLStrategy,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ from attune.memory.short_term.base import BaseOperations
42
+ from attune.memory.short_term.security import DataSanitizer
43
+
44
+ logger = structlog.get_logger(__name__)
45
+
46
+
47
+ class WorkingMemory:
48
+ """Working memory operations for agent data storage.
49
+
50
+ Provides stash (store) and retrieve operations with:
51
+ - Access control based on agent credentials
52
+ - Optional PII scrubbing and secrets detection
53
+ - Configurable TTL strategies
54
+ - Agent-scoped key namespacing
55
+
56
+ The class is designed to be composed with BaseOperations and
57
+ DataSanitizer for dependency injection.
58
+
59
+ Attributes:
60
+ PREFIX_WORKING: Key prefix for working memory namespace
61
+
62
+ Example:
63
+ >>> working = WorkingMemory(base_ops, sanitizer)
64
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
65
+ >>> working.stash("analysis", {"score": 95}, creds)
66
+ True
67
+ >>> working.retrieve("analysis", creds)
68
+ {'score': 95}
69
+ """
70
+
71
+ PREFIX_WORKING = "empathy:working:"
72
+
73
+ def __init__(
74
+ self,
75
+ base: BaseOperations,
76
+ sanitizer: DataSanitizer | None = None,
77
+ ) -> None:
78
+ """Initialize working memory operations.
79
+
80
+ Args:
81
+ base: BaseOperations instance for storage access
82
+ sanitizer: Optional DataSanitizer for PII/secrets handling
83
+ """
84
+ self._base = base
85
+ self._sanitizer = sanitizer
86
+
87
+ def stash(
88
+ self,
89
+ key: str,
90
+ data: Any,
91
+ credentials: AgentCredentials,
92
+ ttl: TTLStrategy = TTLStrategy.WORKING_RESULTS,
93
+ skip_sanitization: bool = False,
94
+ ) -> bool:
95
+ """Stash data in short-term memory.
96
+
97
+ Stores data with automatic TTL expiration and optional
98
+ security sanitization (PII scrubbing, secrets detection).
99
+
100
+ Args:
101
+ key: Unique key for the data
102
+ data: Data to store (will be JSON serialized)
103
+ credentials: Agent credentials for access control
104
+ ttl: Time-to-live strategy (default: WORKING_RESULTS)
105
+ skip_sanitization: Skip PII scrubbing and secrets detection
106
+
107
+ Returns:
108
+ True if successful
109
+
110
+ Raises:
111
+ ValueError: If key is empty or invalid
112
+ PermissionError: If credentials lack write access
113
+ SecurityError: If secrets are detected (when enabled)
114
+
115
+ Note:
116
+ PII (emails, SSNs, etc.) is automatically scrubbed unless
117
+ skip_sanitization=True. Secrets block storage by default.
118
+
119
+ Example:
120
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
121
+ >>> working.stash("analysis_v1", {"findings": [...]}, creds)
122
+ True
123
+ """
124
+ # Pattern 1: String ID validation
125
+ if not key or not key.strip():
126
+ raise ValueError(f"key cannot be empty. Got: {key!r}")
127
+
128
+ if not credentials.can_stage():
129
+ raise PermissionError(
130
+ f"Agent {credentials.agent_id} (Tier {credentials.tier.name}) "
131
+ "cannot write to memory. Requires CONTRIBUTOR or higher.",
132
+ )
133
+
134
+ # Sanitize data (PII scrubbing + secrets detection)
135
+ pii_count = 0
136
+ if not skip_sanitization and self._sanitizer is not None:
137
+ data, pii_count = self._sanitizer.sanitize(data)
138
+ if pii_count > 0:
139
+ logger.info(
140
+ "stash_pii_scrubbed",
141
+ key=key,
142
+ agent_id=credentials.agent_id,
143
+ pii_count=pii_count,
144
+ )
145
+
146
+ full_key = f"{self.PREFIX_WORKING}{credentials.agent_id}:{key}"
147
+ payload = {
148
+ "data": data,
149
+ "agent_id": credentials.agent_id,
150
+ "stashed_at": datetime.now().isoformat(),
151
+ }
152
+ return self._base._set(full_key, json.dumps(payload), ttl.value)
153
+
154
+ def retrieve(
155
+ self,
156
+ key: str,
157
+ credentials: AgentCredentials,
158
+ agent_id: str | None = None,
159
+ ) -> Any | None:
160
+ """Retrieve data from short-term memory.
161
+
162
+ Args:
163
+ key: Key to retrieve
164
+ credentials: Agent credentials
165
+ agent_id: Owner agent ID (defaults to credentials agent)
166
+
167
+ Returns:
168
+ Retrieved data or None if not found
169
+
170
+ Raises:
171
+ ValueError: If key is empty or invalid
172
+
173
+ Example:
174
+ >>> data = working.retrieve("analysis_v1", creds)
175
+ >>> if data:
176
+ ... print(f"Found: {data}")
177
+ """
178
+ # Pattern 1: String ID validation
179
+ if not key or not key.strip():
180
+ raise ValueError(f"key cannot be empty. Got: {key!r}")
181
+
182
+ owner = agent_id or credentials.agent_id
183
+ full_key = f"{self.PREFIX_WORKING}{owner}:{key}"
184
+ raw = self._base._get(full_key)
185
+
186
+ if raw is None:
187
+ return None
188
+
189
+ payload = json.loads(raw)
190
+ return payload.get("data")
191
+
192
+ def clear(self, credentials: AgentCredentials) -> int:
193
+ """Clear all working memory for an agent.
194
+
195
+ Removes all keys in the working memory namespace for
196
+ the given agent.
197
+
198
+ Args:
199
+ credentials: Agent credentials (must own the memory or be Steward)
200
+
201
+ Returns:
202
+ Number of keys deleted
203
+
204
+ Example:
205
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
206
+ >>> deleted = working.clear(creds)
207
+ >>> print(f"Deleted {deleted} keys")
208
+ """
209
+ pattern = f"{self.PREFIX_WORKING}{credentials.agent_id}:*"
210
+ keys = self._base._keys(pattern)
211
+ count = 0
212
+ for key in keys:
213
+ if self._base._delete(key):
214
+ count += 1
215
+ return count
216
+
217
+ def exists(
218
+ self,
219
+ key: str,
220
+ credentials: AgentCredentials,
221
+ agent_id: str | None = None,
222
+ ) -> bool:
223
+ """Check if a key exists in working memory.
224
+
225
+ Args:
226
+ key: Key to check
227
+ credentials: Agent credentials
228
+ agent_id: Owner agent ID (defaults to credentials agent)
229
+
230
+ Returns:
231
+ True if key exists
232
+ """
233
+ if not key or not key.strip():
234
+ return False
235
+
236
+ owner = agent_id or credentials.agent_id
237
+ full_key = f"{self.PREFIX_WORKING}{owner}:{key}"
238
+ return self._base._get(full_key) is not None
239
+
240
+ def list_keys(self, credentials: AgentCredentials) -> list[str]:
241
+ """List all working memory keys for an agent.
242
+
243
+ Args:
244
+ credentials: Agent credentials
245
+
246
+ Returns:
247
+ List of key names (without prefix)
248
+ """
249
+ pattern = f"{self.PREFIX_WORKING}{credentials.agent_id}:*"
250
+ keys = self._base._keys(pattern)
251
+ prefix_len = len(f"{self.PREFIX_WORKING}{credentials.agent_id}:")
252
+ return [k[prefix_len:] for k in keys]
@@ -6,6 +6,9 @@ Copyright 2025 Smart-AI-Memory
6
6
  Licensed under Fair Source License 0.9
7
7
  """
8
8
 
9
+ # ruff: noqa: E402
10
+ # Typer app must be created before importing commands that use it
11
+
9
12
  import typer
10
13
 
11
14
  # Create Typer app for meta-workflow commands
@@ -6,7 +6,6 @@ Copyright 2025 Smart-AI-Memory
6
6
  Licensed under Fair Source License 0.9
7
7
  """
8
8
 
9
-
10
9
  import typer
11
10
  from rich.console import Console
12
11
  from rich.panel import Panel
@@ -151,7 +150,6 @@ def create_agent(
151
150
  console.print(f" {costs.get(tier, costs['capable'])} per execution[/dim]\n")
152
151
 
153
152
 
154
-
155
153
  @meta_workflow_app.command("create-team")
156
154
  def create_team(
157
155
  interactive: bool = typer.Option(
@@ -317,5 +315,3 @@ def create_team(
317
315
 
318
316
  if __name__ == "__main__":
319
317
  meta_workflow_app()
320
-
321
-
@@ -139,7 +139,6 @@ def show_analytics(
139
139
  # =============================================================================
140
140
 
141
141
 
142
-
143
142
  @meta_workflow_app.command("list-runs")
144
143
  def list_runs(
145
144
  template_id: str | None = typer.Option(
@@ -233,7 +232,6 @@ def list_runs(
233
232
  raise typer.Exit(code=1)
234
233
 
235
234
 
236
-
237
235
  @meta_workflow_app.command("show")
238
236
  def show_execution(
239
237
  run_id: str = typer.Argument(..., help="Run ID to display"),
@@ -322,7 +320,6 @@ def show_execution(
322
320
  # =============================================================================
323
321
 
324
322
 
325
-
326
323
  @meta_workflow_app.command("cleanup")
327
324
  def cleanup_executions(
328
325
  older_than_days: int = typer.Option(
@@ -437,6 +434,3 @@ def cleanup_executions(
437
434
  # =============================================================================
438
435
  # Memory Search Commands
439
436
  # =============================================================================
440
-
441
-
442
-
@@ -6,7 +6,6 @@ Copyright 2025 Smart-AI-Memory
6
6
  Licensed under Fair Source License 0.9
7
7
  """
8
8
 
9
-
10
9
  import typer
11
10
  from rich.console import Console
12
11
  from rich.table import Table
@@ -115,7 +114,6 @@ def suggest_defaults_cmd(
115
114
  # =============================================================================
116
115
 
117
116
 
118
-
119
117
  @meta_workflow_app.command("migrate")
120
118
  def show_migration_guide(
121
119
  crew_name: str | None = typer.Argument(
@@ -227,6 +225,3 @@ def show_migration_guide(
227
225
  # =============================================================================
228
226
  # Dynamic Agent/Team Creation Commands (v4.4)
229
227
  # =============================================================================
230
-
231
-
232
-
@@ -6,7 +6,6 @@ Copyright 2025 Smart-AI-Memory
6
6
  Licensed under Fair Source License 0.9
7
7
  """
8
8
 
9
-
10
9
  import typer
11
10
  from rich.console import Console
12
11
  from rich.panel import Panel
@@ -103,7 +102,6 @@ def search_memory(
103
102
  # =============================================================================
104
103
 
105
104
 
106
-
107
105
  @meta_workflow_app.command("session-stats")
108
106
  def show_session_stats(
109
107
  session_id: str | None = typer.Option(
@@ -177,6 +175,3 @@ def show_session_stats(
177
175
  except Exception as e:
178
176
  console.print(f"[red]Error:[/red] {e}")
179
177
  raise typer.Exit(code=1)
180
-
181
-
182
-
@@ -108,7 +108,6 @@ def list_templates(
108
108
  raise typer.Exit(code=1)
109
109
 
110
110
 
111
-
112
111
  @meta_workflow_app.command("inspect")
113
112
  def inspect_template(
114
113
  template_id: str = typer.Argument(..., help="Template ID to inspect"),
@@ -207,7 +206,6 @@ def inspect_template(
207
206
  # =============================================================================
208
207
 
209
208
 
210
-
211
209
  @meta_workflow_app.command("plan")
212
210
  def generate_plan_cmd(
213
211
  template_id: str = typer.Argument(..., help="Template ID to generate plan for"),
@@ -349,6 +347,3 @@ def generate_plan_cmd(
349
347
  # =============================================================================
350
348
  # Execution Commands
351
349
  # =============================================================================
352
-
353
-
354
-
@@ -6,7 +6,6 @@ Copyright 2025 Smart-AI-Memory
6
6
  Licensed under Fair Source License 0.9
7
7
  """
8
8
 
9
-
10
9
  import typer
11
10
  from rich.console import Console
12
11
  from rich.panel import Panel
@@ -208,7 +207,6 @@ def run_workflow(
208
207
  raise typer.Exit(code=1)
209
208
 
210
209
 
211
-
212
210
  @meta_workflow_app.command("ask")
213
211
  def natural_language_run(
214
212
  request: str = typer.Argument(..., help="Natural language description of what you need"),
@@ -317,7 +315,6 @@ def natural_language_run(
317
315
  raise typer.Exit(code=1)
318
316
 
319
317
 
320
-
321
318
  @meta_workflow_app.command("detect")
322
319
  def detect_intent(
323
320
  request: str = typer.Argument(..., help="Natural language request to analyze"),
@@ -377,6 +374,3 @@ def detect_intent(
377
374
  # =============================================================================
378
375
  # Analytics Commands
379
376
  # =============================================================================
380
-
381
-
382
-
@@ -41,7 +41,6 @@ from datetime import datetime
41
41
  from pathlib import Path
42
42
  from typing import TYPE_CHECKING, Any
43
43
 
44
- from attune_llm.routing.model_router import ModelRouter, ModelTier
45
44
  from attune.config import _validate_file_path
46
45
  from attune.meta_workflows.agent_creator import DynamicAgentCreator
47
46
  from attune.meta_workflows.form_engine import SocraticFormEngine
@@ -56,6 +55,7 @@ from attune.meta_workflows.models import (
56
55
  from attune.meta_workflows.template_registry import TemplateRegistry
57
56
  from attune.orchestration.agent_templates import get_template
58
57
  from attune.telemetry.usage_tracker import UsageTracker
58
+ from attune_llm.routing.model_router import ModelRouter, ModelTier
59
59
 
60
60
  if TYPE_CHECKING:
61
61
  from attune.meta_workflows.pattern_learner import PatternLearner
@@ -30,6 +30,7 @@ def _get_registry():
30
30
  global _model_registry
31
31
  if _model_registry is None:
32
32
  from .registry import MODEL_REGISTRY
33
+
33
34
  _model_registry = MODEL_REGISTRY
34
35
  return _model_registry
35
36
 
@@ -241,9 +242,7 @@ class AdaptiveModelRouter:
241
242
 
242
243
  return best.model_id
243
244
 
244
- def recommend_tier_upgrade(
245
- self, workflow: str, stage: str
246
- ) -> tuple[bool, str]:
245
+ def recommend_tier_upgrade(self, workflow: str, stage: str) -> tuple[bool, str]:
247
246
  """Check if tier should be upgraded based on failure rate.
248
247
 
249
248
  Analyzes recent telemetry (last 20 calls) for this workflow/stage.
@@ -335,8 +334,7 @@ class AdaptiveModelRouter:
335
334
  performance_by_model[model] = {
336
335
  "calls": len(model_entries),
337
336
  "success_rate": model_successes / len(model_entries),
338
- "avg_cost": sum(e.get("cost", 0.0) for e in model_entries)
339
- / len(model_entries),
337
+ "avg_cost": sum(e.get("cost", 0.0) for e in model_entries) / len(model_entries),
340
338
  "avg_latency_ms": sum(e.get("duration_ms", 0) for e in model_entries)
341
339
  / len(model_entries),
342
340
  }
@@ -385,9 +383,7 @@ class AdaptiveModelRouter:
385
383
  successes = sum(1 for e in model_entries if e.get("success", True))
386
384
  success_rate = successes / total
387
385
 
388
- avg_latency = (
389
- sum(e.get("duration_ms", 0) for e in model_entries) / total
390
- )
386
+ avg_latency = sum(e.get("duration_ms", 0) for e in model_entries) / total
391
387
  avg_cost = sum(e.get("cost", 0.0) for e in model_entries) / total
392
388
 
393
389
  # Analyze recent failures (last 20 calls)
attune/models/auth_cli.py CHANGED
@@ -123,9 +123,7 @@ def cmd_auth_status(args: Any) -> int:
123
123
  "✅ Yes\n" if strategy.setup_completed else "❌ No (run 'empathy auth setup')\n"
124
124
  )
125
125
 
126
- console.print(
127
- Panel(config_text, title="Authentication Strategy", border_style="blue")
128
- )
126
+ console.print(Panel(config_text, title="Authentication Strategy", border_style="blue"))
129
127
 
130
128
  # Module size thresholds
131
129
  threshold_table = Table(title="Module Size Thresholds", show_header=True)
@@ -353,9 +351,7 @@ def cmd_auth_recommend(args: Any) -> int:
353
351
  if cost_estimate["mode"] == "subscription":
354
352
  print("Monetary Cost: $0.00")
355
353
  print(f"Quota Cost: {cost_estimate['quota_cost']}")
356
- print(
357
- f"Fits in 200K: {'Yes' if cost_estimate['fits_in_context'] else 'No'}"
358
- )
354
+ print(f"Fits in 200K: {'Yes' if cost_estimate['fits_in_context'] else 'No'}")
359
355
  else:
360
356
  print(f"Monetary Cost: ${cost_estimate['monetary_cost']:.4f}")
361
357
  print("Quota Cost: None")
@@ -396,9 +392,7 @@ Examples:
396
392
  subparsers.add_parser("setup", help="Run interactive authentication strategy setup")
397
393
 
398
394
  # Status command
399
- status_parser = subparsers.add_parser(
400
- "status", help="Show current authentication strategy"
401
- )
395
+ status_parser = subparsers.add_parser("status", help="Show current authentication strategy")
402
396
  status_parser.add_argument(
403
397
  "--json", action="store_true", help="Output as JSON instead of formatted table"
404
398
  )
@@ -335,7 +335,7 @@ def configure_auth_interactive(module_lines: int = 1000) -> AuthStrategy:
335
335
 
336
336
  comparison = strategy.get_pros_cons(module_lines)
337
337
 
338
- for mode_key, mode_data in comparison.items():
338
+ for _mode_key, mode_data in comparison.items():
339
339
  print(f"\n### {mode_data['name']}")
340
340
  print(f"Cost: {mode_data['cost']}")
341
341
  print("\nPros:")
@@ -374,9 +374,7 @@ def configure_auth_interactive(module_lines: int = 1000) -> AuthStrategy:
374
374
  # Show recommendation
375
375
  print(f"\n✓ Using {default_mode.value} mode")
376
376
  if default_mode == AuthMode.AUTO:
377
- print(
378
- f" Small/medium modules (< {strategy.medium_module_threshold} LOC) → Subscription"
379
- )
377
+ print(f" Small/medium modules (< {strategy.medium_module_threshold} LOC) → Subscription")
380
378
  print(f" Large modules (> {strategy.medium_module_threshold} LOC) → API")
381
379
 
382
380
  return strategy
@@ -22,6 +22,7 @@ class ProviderMode(str, Enum):
22
22
  """Provider selection mode (Anthropic-only as of v5.0.0)."""
23
23
 
24
24
  SINGLE = "single" # Anthropic for all tiers
25
+ HYBRID = "hybrid" # Deprecated: kept for backward compatibility
25
26
 
26
27
 
27
28
  @dataclass
@@ -150,7 +151,7 @@ class ProviderConfig:
150
151
  if path is None:
151
152
  path = Path.home() / ".empathy" / "provider_config.json"
152
153
  path.parent.mkdir(parents=True, exist_ok=True)
153
- validated_path = _validate_file_path(str(path))
154
+ validated_path = _validate_file_path(str(path)) # type: ignore[misc]
154
155
  with open(validated_path, "w") as f:
155
156
  json.dump(self.to_dict(), f, indent=2)
156
157
 
@@ -280,3 +281,21 @@ def reset_provider_config() -> None:
280
281
  """Reset the global provider configuration (forces reload)."""
281
282
  global _global_config
282
283
  _global_config = None
284
+
285
+
286
+ def configure_hybrid_interactive() -> ProviderConfig:
287
+ """Interactive hybrid provider configuration (DEPRECATED in v5.0.0).
288
+
289
+ Hybrid mode is no longer supported as of v5.0.0 (Claude-native).
290
+ This function now configures Anthropic as the sole provider.
291
+
292
+ Returns:
293
+ ProviderConfig configured for Anthropic
294
+ """
295
+ print("\n" + "=" * 60)
296
+ print("NOTE: Hybrid mode is deprecated (v5.0.0)")
297
+ print("=" * 60)
298
+ print("\nAttune AI is now Claude-native and uses Anthropic exclusively.")
299
+ print("Configuring Anthropic as your provider...\n")
300
+
301
+ return configure_provider_interactive()
@@ -590,5 +590,3 @@ class TelemetryAnalytics:
590
590
 
591
591
  # Singleton for global telemetry
592
592
  _telemetry_store: TelemetryStore | None = None
593
-
594
-
@@ -42,7 +42,6 @@ def _parse_timestamp(timestamp_str: str) -> datetime:
42
42
  return dt
43
43
 
44
44
 
45
-
46
45
  class TelemetryBackend(Protocol):
47
46
  """Protocol for telemetry storage backends.
48
47
 
@@ -192,5 +191,3 @@ def _parse_timestamp(timestamp_str: str) -> datetime:
192
191
  dt = dt.replace(tzinfo=None)
193
192
 
194
193
  return dt
195
-
196
-
@@ -485,5 +485,3 @@ class TelemetryStore:
485
485
  results.append(record)
486
486
 
487
487
  return results
488
-
489
-