ouroboros-ai 0.1.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.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"""Downgrade management for tier optimization in Ouroboros.
|
|
2
|
+
|
|
3
|
+
This module implements automatic tier downgrade after sustained success,
|
|
4
|
+
enabling continuous cost optimization while maintaining quality.
|
|
5
|
+
|
|
6
|
+
Key Components:
|
|
7
|
+
- SuccessTracker: Tracks consecutive successes per task pattern
|
|
8
|
+
- PatternMatcher: Identifies similar task patterns using Jaccard similarity
|
|
9
|
+
- DowngradeManager: Coordinates downgrade decisions based on success history
|
|
10
|
+
|
|
11
|
+
Downgrade Rules:
|
|
12
|
+
- 5 consecutive successes trigger a downgrade
|
|
13
|
+
- Frontier -> Standard -> Frugal
|
|
14
|
+
- Tasks at Frugal tier remain at Frugal
|
|
15
|
+
- Similar patterns (>=80% similarity) inherit tier preferences
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
from ouroboros.routing.downgrade import DowngradeManager, PatternMatcher
|
|
19
|
+
|
|
20
|
+
# Create manager
|
|
21
|
+
manager = DowngradeManager()
|
|
22
|
+
|
|
23
|
+
# Record success and check for downgrade
|
|
24
|
+
result = manager.record_success("task_pattern_123", Tier.STANDARD)
|
|
25
|
+
if result.is_ok:
|
|
26
|
+
downgrade_result = result.value
|
|
27
|
+
if downgrade_result.should_downgrade:
|
|
28
|
+
new_tier = downgrade_result.recommended_tier
|
|
29
|
+
print(f"Downgrade to {new_tier.value}")
|
|
30
|
+
|
|
31
|
+
# Pattern matching for similar tasks
|
|
32
|
+
matcher = PatternMatcher()
|
|
33
|
+
similarity = matcher.calculate_similarity("fix typo in README", "fix typo in docs")
|
|
34
|
+
if similarity >= 0.8:
|
|
35
|
+
# Apply same tier preference
|
|
36
|
+
pass
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import TypeAlias
|
|
41
|
+
|
|
42
|
+
from ouroboros.core.types import Result
|
|
43
|
+
from ouroboros.observability.logging import get_logger
|
|
44
|
+
from ouroboros.routing.tiers import Tier
|
|
45
|
+
|
|
46
|
+
log = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Constants
|
|
50
|
+
DOWNGRADE_THRESHOLD = 5 # Consecutive successes needed for downgrade
|
|
51
|
+
SIMILARITY_THRESHOLD = 0.80 # Minimum similarity for pattern matching
|
|
52
|
+
|
|
53
|
+
# Type aliases
|
|
54
|
+
PatternId: TypeAlias = str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class SuccessTracker:
|
|
59
|
+
"""Tracks consecutive successes per task pattern.
|
|
60
|
+
|
|
61
|
+
This class maintains state for tracking how many consecutive successful
|
|
62
|
+
completions have occurred for each unique task pattern. It supports
|
|
63
|
+
both recording successes and resetting on failures.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
_success_counts: Internal dict mapping pattern IDs to consecutive success counts.
|
|
67
|
+
_tier_history: Internal dict mapping pattern IDs to their current tier.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
tracker = SuccessTracker()
|
|
71
|
+
|
|
72
|
+
# Record successes
|
|
73
|
+
tracker.record_success("pattern_1", Tier.STANDARD)
|
|
74
|
+
tracker.record_success("pattern_1", Tier.STANDARD)
|
|
75
|
+
|
|
76
|
+
# Get current count
|
|
77
|
+
count = tracker.get_success_count("pattern_1") # Returns 2
|
|
78
|
+
|
|
79
|
+
# Reset on failure
|
|
80
|
+
tracker.reset_on_failure("pattern_1")
|
|
81
|
+
count = tracker.get_success_count("pattern_1") # Returns 0
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
_success_counts: dict[PatternId, int] = field(default_factory=dict)
|
|
85
|
+
_tier_history: dict[PatternId, Tier] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
def record_success(self, pattern_id: PatternId, tier: Tier) -> int:
|
|
88
|
+
"""Record a successful completion for a pattern.
|
|
89
|
+
|
|
90
|
+
Increments the consecutive success counter for the given pattern
|
|
91
|
+
and updates the tier history.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
pattern_id: Unique identifier for the task pattern.
|
|
95
|
+
tier: The tier at which the task was executed.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The new consecutive success count for this pattern.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
tracker = SuccessTracker()
|
|
102
|
+
count = tracker.record_success("pattern_1", Tier.STANDARD)
|
|
103
|
+
# count is now 1
|
|
104
|
+
"""
|
|
105
|
+
current_count = self._success_counts.get(pattern_id, 0)
|
|
106
|
+
new_count = current_count + 1
|
|
107
|
+
self._success_counts[pattern_id] = new_count
|
|
108
|
+
self._tier_history[pattern_id] = tier
|
|
109
|
+
|
|
110
|
+
log.debug(
|
|
111
|
+
"success_tracker.recorded",
|
|
112
|
+
pattern_id=pattern_id,
|
|
113
|
+
tier=tier.value,
|
|
114
|
+
consecutive_count=new_count,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return new_count
|
|
118
|
+
|
|
119
|
+
def reset_on_failure(self, pattern_id: PatternId) -> None:
|
|
120
|
+
"""Reset the success counter for a pattern after a failure.
|
|
121
|
+
|
|
122
|
+
Sets the consecutive success count to 0 while preserving tier history.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
pattern_id: Unique identifier for the task pattern.
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
tracker = SuccessTracker()
|
|
129
|
+
tracker.record_success("pattern_1", Tier.STANDARD)
|
|
130
|
+
tracker.reset_on_failure("pattern_1")
|
|
131
|
+
# Success count is now 0
|
|
132
|
+
"""
|
|
133
|
+
previous_count = self._success_counts.get(pattern_id, 0)
|
|
134
|
+
self._success_counts[pattern_id] = 0
|
|
135
|
+
|
|
136
|
+
log.debug(
|
|
137
|
+
"success_tracker.reset",
|
|
138
|
+
pattern_id=pattern_id,
|
|
139
|
+
previous_count=previous_count,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_success_count(self, pattern_id: PatternId) -> int:
|
|
143
|
+
"""Get the current consecutive success count for a pattern.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
pattern_id: Unique identifier for the task pattern.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The consecutive success count, or 0 if pattern not tracked.
|
|
150
|
+
"""
|
|
151
|
+
return self._success_counts.get(pattern_id, 0)
|
|
152
|
+
|
|
153
|
+
def get_tier(self, pattern_id: PatternId) -> Tier | None:
|
|
154
|
+
"""Get the current tier for a pattern.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
pattern_id: Unique identifier for the task pattern.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The tier for this pattern, or None if not tracked.
|
|
161
|
+
"""
|
|
162
|
+
return self._tier_history.get(pattern_id)
|
|
163
|
+
|
|
164
|
+
def get_all_patterns(self) -> list[PatternId]:
|
|
165
|
+
"""Get all tracked pattern IDs.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of all pattern IDs being tracked.
|
|
169
|
+
"""
|
|
170
|
+
return list(self._success_counts.keys())
|
|
171
|
+
|
|
172
|
+
def clear(self) -> None:
|
|
173
|
+
"""Clear all tracking state.
|
|
174
|
+
|
|
175
|
+
Resets both success counts and tier history.
|
|
176
|
+
"""
|
|
177
|
+
self._success_counts.clear()
|
|
178
|
+
self._tier_history.clear()
|
|
179
|
+
log.debug("success_tracker.cleared")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True, slots=True)
|
|
183
|
+
class DowngradeResult:
|
|
184
|
+
"""Result of a downgrade evaluation.
|
|
185
|
+
|
|
186
|
+
Contains information about whether a downgrade should occur
|
|
187
|
+
and the recommended tier if so.
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
should_downgrade: Whether a downgrade is recommended.
|
|
191
|
+
current_tier: The tier the task was executed at.
|
|
192
|
+
recommended_tier: The recommended tier after evaluation.
|
|
193
|
+
consecutive_successes: Number of consecutive successes that led to this decision.
|
|
194
|
+
cost_savings_factor: Estimated cost savings if downgrade is applied (ratio).
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
result = DowngradeResult(
|
|
198
|
+
should_downgrade=True,
|
|
199
|
+
current_tier=Tier.STANDARD,
|
|
200
|
+
recommended_tier=Tier.FRUGAL,
|
|
201
|
+
consecutive_successes=5,
|
|
202
|
+
cost_savings_factor=10.0, # Standard (10x) to Frugal (1x)
|
|
203
|
+
)
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
should_downgrade: bool
|
|
207
|
+
current_tier: Tier
|
|
208
|
+
recommended_tier: Tier
|
|
209
|
+
consecutive_successes: int
|
|
210
|
+
cost_savings_factor: float
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _get_lower_tier(tier: Tier) -> Tier:
|
|
214
|
+
"""Get the next lower tier.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
tier: Current tier.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The next lower tier, or the same tier if already at Frugal.
|
|
221
|
+
"""
|
|
222
|
+
tier_order = [Tier.FRUGAL, Tier.STANDARD, Tier.FRONTIER]
|
|
223
|
+
current_index = tier_order.index(tier)
|
|
224
|
+
|
|
225
|
+
if current_index > 0:
|
|
226
|
+
return tier_order[current_index - 1]
|
|
227
|
+
return tier # Already at Frugal
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _calculate_cost_savings(from_tier: Tier, to_tier: Tier) -> float:
|
|
231
|
+
"""Calculate the cost savings factor from downgrading.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
from_tier: Original tier.
|
|
235
|
+
to_tier: Target tier.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Cost savings factor (ratio of cost multipliers).
|
|
239
|
+
"""
|
|
240
|
+
if from_tier == to_tier:
|
|
241
|
+
return 1.0
|
|
242
|
+
|
|
243
|
+
from_cost = from_tier.cost_multiplier
|
|
244
|
+
to_cost = to_tier.cost_multiplier
|
|
245
|
+
|
|
246
|
+
return from_cost / to_cost
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class PatternMatcher:
|
|
250
|
+
"""Matches similar task patterns using Jaccard similarity.
|
|
251
|
+
|
|
252
|
+
This class provides pattern similarity calculation using Jaccard similarity
|
|
253
|
+
on tokenized task descriptions. Similar patterns (>=80% similarity) can
|
|
254
|
+
inherit tier preferences from successful completions.
|
|
255
|
+
|
|
256
|
+
For MVP, we use simple word-based tokenization and Jaccard similarity
|
|
257
|
+
instead of embeddings. This provides a fast, interpretable similarity
|
|
258
|
+
measure suitable for initial implementation.
|
|
259
|
+
|
|
260
|
+
Jaccard Similarity:
|
|
261
|
+
J(A, B) = |A intersection B| / |A union B|
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
matcher = PatternMatcher()
|
|
265
|
+
|
|
266
|
+
# Check similarity between patterns
|
|
267
|
+
sim = matcher.calculate_similarity(
|
|
268
|
+
"fix typo in README",
|
|
269
|
+
"fix typo in documentation",
|
|
270
|
+
)
|
|
271
|
+
# sim ~= 0.4 (shares "fix", "typo", "in")
|
|
272
|
+
|
|
273
|
+
# Find similar patterns
|
|
274
|
+
patterns = ["fix typo", "add feature", "fix bug"]
|
|
275
|
+
similar = matcher.find_similar_patterns("fix issue", patterns)
|
|
276
|
+
# Returns patterns with similarity >= 0.8
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self, similarity_threshold: float = SIMILARITY_THRESHOLD) -> None:
|
|
280
|
+
"""Initialize the pattern matcher.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
similarity_threshold: Minimum similarity for patterns to be considered similar.
|
|
284
|
+
Defaults to 0.80 (80%).
|
|
285
|
+
"""
|
|
286
|
+
self._similarity_threshold = similarity_threshold
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def similarity_threshold(self) -> float:
|
|
290
|
+
"""Get the similarity threshold."""
|
|
291
|
+
return self._similarity_threshold
|
|
292
|
+
|
|
293
|
+
def _tokenize(self, text: str) -> set[str]:
|
|
294
|
+
"""Tokenize a text string into a set of lowercase words.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
text: The text to tokenize.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Set of lowercase word tokens.
|
|
301
|
+
"""
|
|
302
|
+
# Simple word tokenization: split on whitespace and punctuation
|
|
303
|
+
# Convert to lowercase for case-insensitive matching
|
|
304
|
+
words = text.lower().split()
|
|
305
|
+
# Remove common punctuation from tokens
|
|
306
|
+
cleaned_words = set()
|
|
307
|
+
for word in words:
|
|
308
|
+
# Strip punctuation from start and end
|
|
309
|
+
cleaned = word.strip(".,;:!?\"'()-[]{}/<>")
|
|
310
|
+
if cleaned: # Only add non-empty tokens
|
|
311
|
+
cleaned_words.add(cleaned)
|
|
312
|
+
return cleaned_words
|
|
313
|
+
|
|
314
|
+
def calculate_similarity(self, pattern_a: str, pattern_b: str) -> float:
|
|
315
|
+
"""Calculate Jaccard similarity between two patterns.
|
|
316
|
+
|
|
317
|
+
Jaccard similarity is defined as:
|
|
318
|
+
J(A, B) = |A intersection B| / |A union B|
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
pattern_a: First pattern string.
|
|
322
|
+
pattern_b: Second pattern string.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Similarity score between 0.0 and 1.0.
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
matcher = PatternMatcher()
|
|
329
|
+
sim = matcher.calculate_similarity("fix bug", "fix typo")
|
|
330
|
+
# sim = 1/3 (intersection={fix}, union={fix, bug, typo})
|
|
331
|
+
"""
|
|
332
|
+
tokens_a = self._tokenize(pattern_a)
|
|
333
|
+
tokens_b = self._tokenize(pattern_b)
|
|
334
|
+
|
|
335
|
+
if not tokens_a and not tokens_b:
|
|
336
|
+
return 1.0 # Both empty = identical
|
|
337
|
+
|
|
338
|
+
if not tokens_a or not tokens_b:
|
|
339
|
+
return 0.0 # One empty, one not = no similarity
|
|
340
|
+
|
|
341
|
+
intersection = tokens_a & tokens_b
|
|
342
|
+
union = tokens_a | tokens_b
|
|
343
|
+
|
|
344
|
+
similarity = len(intersection) / len(union)
|
|
345
|
+
|
|
346
|
+
log.debug(
|
|
347
|
+
"pattern_matcher.similarity_calculated",
|
|
348
|
+
pattern_a=pattern_a[:50], # Truncate for logging
|
|
349
|
+
pattern_b=pattern_b[:50],
|
|
350
|
+
similarity=similarity,
|
|
351
|
+
intersection_size=len(intersection),
|
|
352
|
+
union_size=len(union),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return similarity
|
|
356
|
+
|
|
357
|
+
def is_similar(self, pattern_a: str, pattern_b: str) -> bool:
|
|
358
|
+
"""Check if two patterns are similar based on threshold.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
pattern_a: First pattern string.
|
|
362
|
+
pattern_b: Second pattern string.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
True if similarity >= threshold.
|
|
366
|
+
"""
|
|
367
|
+
return self.calculate_similarity(pattern_a, pattern_b) >= self._similarity_threshold
|
|
368
|
+
|
|
369
|
+
def find_similar_patterns(
|
|
370
|
+
self,
|
|
371
|
+
target_pattern: str,
|
|
372
|
+
candidate_patterns: list[str],
|
|
373
|
+
) -> list[tuple[str, float]]:
|
|
374
|
+
"""Find all candidate patterns similar to the target.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
target_pattern: The pattern to match against.
|
|
378
|
+
candidate_patterns: List of patterns to compare.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
List of (pattern, similarity) tuples for patterns meeting threshold,
|
|
382
|
+
sorted by similarity in descending order.
|
|
383
|
+
"""
|
|
384
|
+
similar: list[tuple[str, float]] = []
|
|
385
|
+
|
|
386
|
+
for candidate in candidate_patterns:
|
|
387
|
+
similarity = self.calculate_similarity(target_pattern, candidate)
|
|
388
|
+
if similarity >= self._similarity_threshold:
|
|
389
|
+
similar.append((candidate, similarity))
|
|
390
|
+
|
|
391
|
+
# Sort by similarity descending
|
|
392
|
+
similar.sort(key=lambda x: x[1], reverse=True)
|
|
393
|
+
|
|
394
|
+
if similar:
|
|
395
|
+
log.debug(
|
|
396
|
+
"pattern_matcher.similar_patterns_found",
|
|
397
|
+
target_pattern=target_pattern[:50],
|
|
398
|
+
match_count=len(similar),
|
|
399
|
+
best_match_similarity=similar[0][1] if similar else 0.0,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return similar
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@dataclass
|
|
406
|
+
class DowngradeManager:
|
|
407
|
+
"""Manages tier downgrade decisions based on success history.
|
|
408
|
+
|
|
409
|
+
The DowngradeManager coordinates success tracking and pattern matching
|
|
410
|
+
to determine when tasks should be downgraded to a lower (cheaper) tier.
|
|
411
|
+
|
|
412
|
+
Downgrade Rules:
|
|
413
|
+
- 5 consecutive successes at a tier trigger a downgrade evaluation
|
|
414
|
+
- Tiers downgrade: Frontier -> Standard -> Frugal
|
|
415
|
+
- Frugal tier tasks cannot be downgraded further
|
|
416
|
+
- Similar patterns inherit tier preferences
|
|
417
|
+
|
|
418
|
+
Example:
|
|
419
|
+
manager = DowngradeManager()
|
|
420
|
+
|
|
421
|
+
# Record successes
|
|
422
|
+
for i in range(5):
|
|
423
|
+
result = manager.record_success("pattern_1", Tier.STANDARD)
|
|
424
|
+
|
|
425
|
+
# After 5 successes, should_downgrade will be True
|
|
426
|
+
assert result.is_ok
|
|
427
|
+
assert result.value.should_downgrade
|
|
428
|
+
assert result.value.recommended_tier == Tier.FRUGAL
|
|
429
|
+
|
|
430
|
+
# Record failure resets the counter
|
|
431
|
+
manager.record_failure("pattern_1")
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
_tracker: SuccessTracker = field(default_factory=SuccessTracker)
|
|
435
|
+
_pattern_matcher: PatternMatcher = field(default_factory=PatternMatcher)
|
|
436
|
+
_downgrade_threshold: int = DOWNGRADE_THRESHOLD
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def downgrade_threshold(self) -> int:
|
|
440
|
+
"""Get the downgrade threshold (consecutive successes needed)."""
|
|
441
|
+
return self._downgrade_threshold
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def tracker(self) -> SuccessTracker:
|
|
445
|
+
"""Get the internal success tracker."""
|
|
446
|
+
return self._tracker
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def pattern_matcher(self) -> PatternMatcher:
|
|
450
|
+
"""Get the internal pattern matcher."""
|
|
451
|
+
return self._pattern_matcher
|
|
452
|
+
|
|
453
|
+
def record_success(
|
|
454
|
+
self,
|
|
455
|
+
pattern_id: PatternId,
|
|
456
|
+
tier: Tier,
|
|
457
|
+
) -> Result[DowngradeResult, None]:
|
|
458
|
+
"""Record a successful task completion and evaluate for downgrade.
|
|
459
|
+
|
|
460
|
+
Increments the success counter for the pattern and checks if the
|
|
461
|
+
threshold for downgrade has been met.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
pattern_id: Unique identifier for the task pattern.
|
|
465
|
+
tier: The tier at which the task was executed.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Result containing DowngradeResult with the evaluation.
|
|
469
|
+
Always succeeds (Result.ok) with downgrade information.
|
|
470
|
+
|
|
471
|
+
Example:
|
|
472
|
+
manager = DowngradeManager()
|
|
473
|
+
|
|
474
|
+
# Record first success - no downgrade
|
|
475
|
+
result = manager.record_success("pattern_1", Tier.FRONTIER)
|
|
476
|
+
assert not result.value.should_downgrade
|
|
477
|
+
|
|
478
|
+
# After 5 successes, downgrade recommended
|
|
479
|
+
for _ in range(4):
|
|
480
|
+
result = manager.record_success("pattern_1", Tier.FRONTIER)
|
|
481
|
+
assert result.value.should_downgrade
|
|
482
|
+
"""
|
|
483
|
+
# Record the success
|
|
484
|
+
success_count = self._tracker.record_success(pattern_id, tier)
|
|
485
|
+
|
|
486
|
+
# Check if downgrade threshold met
|
|
487
|
+
should_downgrade = (
|
|
488
|
+
success_count >= self._downgrade_threshold and tier != Tier.FRUGAL
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Determine recommended tier
|
|
492
|
+
if should_downgrade:
|
|
493
|
+
recommended_tier = _get_lower_tier(tier)
|
|
494
|
+
cost_savings = _calculate_cost_savings(tier, recommended_tier)
|
|
495
|
+
|
|
496
|
+
log.info(
|
|
497
|
+
"downgrade.recommended",
|
|
498
|
+
pattern_id=pattern_id,
|
|
499
|
+
current_tier=tier.value,
|
|
500
|
+
recommended_tier=recommended_tier.value,
|
|
501
|
+
consecutive_successes=success_count,
|
|
502
|
+
cost_savings_factor=cost_savings,
|
|
503
|
+
)
|
|
504
|
+
else:
|
|
505
|
+
recommended_tier = tier
|
|
506
|
+
cost_savings = 1.0
|
|
507
|
+
|
|
508
|
+
log.debug(
|
|
509
|
+
"downgrade.not_ready",
|
|
510
|
+
pattern_id=pattern_id,
|
|
511
|
+
tier=tier.value,
|
|
512
|
+
consecutive_successes=success_count,
|
|
513
|
+
threshold=self._downgrade_threshold,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
downgrade_result = DowngradeResult(
|
|
517
|
+
should_downgrade=should_downgrade,
|
|
518
|
+
current_tier=tier,
|
|
519
|
+
recommended_tier=recommended_tier,
|
|
520
|
+
consecutive_successes=success_count,
|
|
521
|
+
cost_savings_factor=cost_savings,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return Result.ok(downgrade_result)
|
|
525
|
+
|
|
526
|
+
def record_failure(self, pattern_id: PatternId) -> None:
|
|
527
|
+
"""Record a failed task completion.
|
|
528
|
+
|
|
529
|
+
Resets the consecutive success counter for the pattern.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
pattern_id: Unique identifier for the task pattern.
|
|
533
|
+
|
|
534
|
+
Example:
|
|
535
|
+
manager = DowngradeManager()
|
|
536
|
+
|
|
537
|
+
# Record some successes
|
|
538
|
+
manager.record_success("pattern_1", Tier.STANDARD)
|
|
539
|
+
manager.record_success("pattern_1", Tier.STANDARD)
|
|
540
|
+
|
|
541
|
+
# Failure resets the counter
|
|
542
|
+
manager.record_failure("pattern_1")
|
|
543
|
+
# Success count is now 0
|
|
544
|
+
"""
|
|
545
|
+
self._tracker.reset_on_failure(pattern_id)
|
|
546
|
+
|
|
547
|
+
log.info(
|
|
548
|
+
"downgrade.failure_recorded",
|
|
549
|
+
pattern_id=pattern_id,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def get_recommended_tier_for_pattern(
|
|
553
|
+
self,
|
|
554
|
+
pattern_description: str,
|
|
555
|
+
default_tier: Tier = Tier.FRUGAL,
|
|
556
|
+
) -> Tier:
|
|
557
|
+
"""Get the recommended tier for a pattern based on similar patterns.
|
|
558
|
+
|
|
559
|
+
Looks for similar tracked patterns and returns the tier of the most
|
|
560
|
+
similar successful pattern, allowing new tasks to benefit from
|
|
561
|
+
learned tier preferences.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
pattern_description: Description of the task pattern.
|
|
565
|
+
default_tier: Tier to return if no similar patterns found.
|
|
566
|
+
Defaults to FRUGAL (optimistic for cost savings).
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
The recommended tier based on similar patterns.
|
|
570
|
+
|
|
571
|
+
Example:
|
|
572
|
+
manager = DowngradeManager()
|
|
573
|
+
|
|
574
|
+
# After tracking "fix typo in README" at Frugal tier
|
|
575
|
+
manager.record_success("fix typo in README", Tier.FRUGAL)
|
|
576
|
+
|
|
577
|
+
# Similar pattern gets same tier recommendation
|
|
578
|
+
tier = manager.get_recommended_tier_for_pattern("fix typo in docs")
|
|
579
|
+
# tier == Tier.FRUGAL (if similarity >= 80%)
|
|
580
|
+
"""
|
|
581
|
+
tracked_patterns = self._tracker.get_all_patterns()
|
|
582
|
+
|
|
583
|
+
if not tracked_patterns:
|
|
584
|
+
log.debug(
|
|
585
|
+
"downgrade.no_patterns_tracked",
|
|
586
|
+
pattern_description=pattern_description[:50],
|
|
587
|
+
default_tier=default_tier.value,
|
|
588
|
+
)
|
|
589
|
+
return default_tier
|
|
590
|
+
|
|
591
|
+
# Find similar patterns
|
|
592
|
+
similar_patterns = self._pattern_matcher.find_similar_patterns(
|
|
593
|
+
pattern_description,
|
|
594
|
+
tracked_patterns,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
if not similar_patterns:
|
|
598
|
+
log.debug(
|
|
599
|
+
"downgrade.no_similar_patterns",
|
|
600
|
+
pattern_description=pattern_description[:50],
|
|
601
|
+
default_tier=default_tier.value,
|
|
602
|
+
)
|
|
603
|
+
return default_tier
|
|
604
|
+
|
|
605
|
+
# Get the tier of the most similar pattern
|
|
606
|
+
best_match, best_similarity = similar_patterns[0]
|
|
607
|
+
matched_tier = self._tracker.get_tier(best_match)
|
|
608
|
+
|
|
609
|
+
if matched_tier is None:
|
|
610
|
+
return default_tier
|
|
611
|
+
|
|
612
|
+
log.info(
|
|
613
|
+
"downgrade.tier_inherited",
|
|
614
|
+
pattern_description=pattern_description[:50],
|
|
615
|
+
matched_pattern=best_match[:50],
|
|
616
|
+
similarity=best_similarity,
|
|
617
|
+
inherited_tier=matched_tier.value,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return matched_tier
|
|
621
|
+
|
|
622
|
+
def apply_downgrade(self, pattern_id: PatternId) -> None:
|
|
623
|
+
"""Apply a downgrade by resetting the success counter.
|
|
624
|
+
|
|
625
|
+
After a downgrade is applied, the success counter is reset
|
|
626
|
+
to allow the pattern to be evaluated again at the new tier.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
pattern_id: Unique identifier for the task pattern.
|
|
630
|
+
"""
|
|
631
|
+
current_tier = self._tracker.get_tier(pattern_id)
|
|
632
|
+
self._tracker._success_counts[pattern_id] = 0
|
|
633
|
+
|
|
634
|
+
if current_tier:
|
|
635
|
+
new_tier = _get_lower_tier(current_tier)
|
|
636
|
+
self._tracker._tier_history[pattern_id] = new_tier
|
|
637
|
+
|
|
638
|
+
log.info(
|
|
639
|
+
"downgrade.applied",
|
|
640
|
+
pattern_id=pattern_id,
|
|
641
|
+
from_tier=current_tier.value,
|
|
642
|
+
to_tier=new_tier.value,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def get_cost_savings_estimate(self, pattern_id: PatternId) -> float:
|
|
646
|
+
"""Estimate cost savings if downgrade is applied.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
pattern_id: Unique identifier for the task pattern.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Cost savings factor (>1.0 means savings).
|
|
653
|
+
"""
|
|
654
|
+
current_tier = self._tracker.get_tier(pattern_id)
|
|
655
|
+
if current_tier is None or current_tier == Tier.FRUGAL:
|
|
656
|
+
return 1.0
|
|
657
|
+
|
|
658
|
+
new_tier = _get_lower_tier(current_tier)
|
|
659
|
+
return _calculate_cost_savings(current_tier, new_tier)
|
|
660
|
+
|
|
661
|
+
def clear(self) -> None:
|
|
662
|
+
"""Clear all tracking state."""
|
|
663
|
+
self._tracker.clear()
|
|
664
|
+
log.info("downgrade.manager_cleared")
|