empathy-framework 3.2.3__py3-none-any.whl → 3.8.2__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 (328) hide show
  1. coach_wizards/__init__.py +11 -12
  2. coach_wizards/accessibility_wizard.py +12 -12
  3. coach_wizards/api_wizard.py +12 -12
  4. coach_wizards/base_wizard.py +26 -20
  5. coach_wizards/cicd_wizard.py +15 -13
  6. coach_wizards/code_reviewer_README.md +60 -0
  7. coach_wizards/code_reviewer_wizard.py +180 -0
  8. coach_wizards/compliance_wizard.py +12 -12
  9. coach_wizards/database_wizard.py +12 -12
  10. coach_wizards/debugging_wizard.py +12 -12
  11. coach_wizards/documentation_wizard.py +12 -12
  12. coach_wizards/generate_wizards.py +1 -2
  13. coach_wizards/localization_wizard.py +101 -19
  14. coach_wizards/migration_wizard.py +12 -12
  15. coach_wizards/monitoring_wizard.py +12 -12
  16. coach_wizards/observability_wizard.py +12 -12
  17. coach_wizards/performance_wizard.py +12 -12
  18. coach_wizards/prompt_engineering_wizard.py +22 -25
  19. coach_wizards/refactoring_wizard.py +12 -12
  20. coach_wizards/scaling_wizard.py +12 -12
  21. coach_wizards/security_wizard.py +12 -12
  22. coach_wizards/testing_wizard.py +12 -12
  23. {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/METADATA +513 -58
  24. empathy_framework-3.8.2.dist-info/RECORD +333 -0
  25. empathy_framework-3.8.2.dist-info/entry_points.txt +22 -0
  26. {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/top_level.txt +5 -1
  27. empathy_healthcare_plugin/__init__.py +1 -2
  28. empathy_healthcare_plugin/monitors/__init__.py +9 -0
  29. empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
  30. empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
  31. empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
  32. empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
  33. empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
  34. empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
  35. empathy_llm_toolkit/__init__.py +7 -7
  36. empathy_llm_toolkit/agent_factory/__init__.py +53 -0
  37. empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
  38. empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
  39. empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
  40. empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
  41. empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
  42. empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
  43. empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
  44. empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
  45. empathy_llm_toolkit/agent_factory/base.py +305 -0
  46. empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
  47. empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
  48. empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
  49. empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
  50. empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
  51. empathy_llm_toolkit/agent_factory/decorators.py +286 -0
  52. empathy_llm_toolkit/agent_factory/factory.py +558 -0
  53. empathy_llm_toolkit/agent_factory/framework.py +192 -0
  54. empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
  55. empathy_llm_toolkit/agent_factory/resilient.py +320 -0
  56. empathy_llm_toolkit/claude_memory.py +14 -15
  57. empathy_llm_toolkit/cli/__init__.py +8 -0
  58. empathy_llm_toolkit/cli/sync_claude.py +487 -0
  59. empathy_llm_toolkit/code_health.py +177 -22
  60. empathy_llm_toolkit/config/__init__.py +29 -0
  61. empathy_llm_toolkit/config/unified.py +295 -0
  62. empathy_llm_toolkit/contextual_patterns.py +11 -12
  63. empathy_llm_toolkit/core.py +51 -49
  64. empathy_llm_toolkit/git_pattern_extractor.py +16 -12
  65. empathy_llm_toolkit/levels.py +6 -13
  66. empathy_llm_toolkit/pattern_confidence.py +14 -18
  67. empathy_llm_toolkit/pattern_resolver.py +10 -12
  68. empathy_llm_toolkit/pattern_summary.py +13 -11
  69. empathy_llm_toolkit/providers.py +194 -28
  70. empathy_llm_toolkit/routing/__init__.py +32 -0
  71. empathy_llm_toolkit/routing/model_router.py +362 -0
  72. empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
  73. empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
  74. empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  75. empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
  76. empathy_llm_toolkit/security/README.md +262 -0
  77. empathy_llm_toolkit/security/__init__.py +62 -0
  78. empathy_llm_toolkit/security/audit_logger.py +929 -0
  79. empathy_llm_toolkit/security/audit_logger_example.py +152 -0
  80. empathy_llm_toolkit/security/pii_scrubber.py +640 -0
  81. empathy_llm_toolkit/security/secrets_detector.py +678 -0
  82. empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
  83. empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
  84. empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
  85. empathy_llm_toolkit/session_status.py +18 -20
  86. empathy_llm_toolkit/state.py +20 -21
  87. empathy_llm_toolkit/wizards/__init__.py +38 -0
  88. empathy_llm_toolkit/wizards/base_wizard.py +364 -0
  89. empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
  90. empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
  91. empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
  92. empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
  93. empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
  94. empathy_os/__init__.py +76 -77
  95. empathy_os/adaptive/__init__.py +13 -0
  96. empathy_os/adaptive/task_complexity.py +127 -0
  97. empathy_os/{monitoring.py → agent_monitoring.py} +27 -27
  98. empathy_os/cache/__init__.py +117 -0
  99. empathy_os/cache/base.py +166 -0
  100. empathy_os/cache/dependency_manager.py +253 -0
  101. empathy_os/cache/hash_only.py +248 -0
  102. empathy_os/cache/hybrid.py +390 -0
  103. empathy_os/cache/storage.py +282 -0
  104. empathy_os/cli.py +515 -109
  105. empathy_os/cli_unified.py +189 -42
  106. empathy_os/config/__init__.py +63 -0
  107. empathy_os/config/xml_config.py +239 -0
  108. empathy_os/config.py +87 -36
  109. empathy_os/coordination.py +48 -54
  110. empathy_os/core.py +90 -99
  111. empathy_os/cost_tracker.py +20 -23
  112. empathy_os/dashboard/__init__.py +15 -0
  113. empathy_os/dashboard/server.py +743 -0
  114. empathy_os/discovery.py +9 -11
  115. empathy_os/emergence.py +20 -21
  116. empathy_os/exceptions.py +18 -30
  117. empathy_os/feedback_loops.py +27 -30
  118. empathy_os/levels.py +31 -34
  119. empathy_os/leverage_points.py +27 -28
  120. empathy_os/logging_config.py +11 -12
  121. empathy_os/memory/__init__.py +195 -0
  122. empathy_os/memory/claude_memory.py +466 -0
  123. empathy_os/memory/config.py +224 -0
  124. empathy_os/memory/control_panel.py +1298 -0
  125. empathy_os/memory/edges.py +179 -0
  126. empathy_os/memory/graph.py +567 -0
  127. empathy_os/memory/long_term.py +1194 -0
  128. empathy_os/memory/nodes.py +179 -0
  129. empathy_os/memory/redis_bootstrap.py +540 -0
  130. empathy_os/memory/security/__init__.py +31 -0
  131. empathy_os/memory/security/audit_logger.py +930 -0
  132. empathy_os/memory/security/pii_scrubber.py +640 -0
  133. empathy_os/memory/security/secrets_detector.py +678 -0
  134. empathy_os/memory/short_term.py +2119 -0
  135. empathy_os/memory/storage/__init__.py +15 -0
  136. empathy_os/memory/summary_index.py +583 -0
  137. empathy_os/memory/unified.py +619 -0
  138. empathy_os/metrics/__init__.py +12 -0
  139. empathy_os/metrics/prompt_metrics.py +190 -0
  140. empathy_os/models/__init__.py +136 -0
  141. empathy_os/models/__main__.py +13 -0
  142. empathy_os/models/cli.py +655 -0
  143. empathy_os/models/empathy_executor.py +354 -0
  144. empathy_os/models/executor.py +252 -0
  145. empathy_os/models/fallback.py +671 -0
  146. empathy_os/models/provider_config.py +563 -0
  147. empathy_os/models/registry.py +382 -0
  148. empathy_os/models/tasks.py +302 -0
  149. empathy_os/models/telemetry.py +548 -0
  150. empathy_os/models/token_estimator.py +378 -0
  151. empathy_os/models/validation.py +274 -0
  152. empathy_os/monitoring/__init__.py +52 -0
  153. empathy_os/monitoring/alerts.py +23 -0
  154. empathy_os/monitoring/alerts_cli.py +268 -0
  155. empathy_os/monitoring/multi_backend.py +271 -0
  156. empathy_os/monitoring/otel_backend.py +363 -0
  157. empathy_os/optimization/__init__.py +19 -0
  158. empathy_os/optimization/context_optimizer.py +272 -0
  159. empathy_os/pattern_library.py +29 -28
  160. empathy_os/persistence.py +30 -34
  161. empathy_os/platform_utils.py +261 -0
  162. empathy_os/plugins/__init__.py +28 -0
  163. empathy_os/plugins/base.py +361 -0
  164. empathy_os/plugins/registry.py +268 -0
  165. empathy_os/project_index/__init__.py +30 -0
  166. empathy_os/project_index/cli.py +335 -0
  167. empathy_os/project_index/crew_integration.py +430 -0
  168. empathy_os/project_index/index.py +425 -0
  169. empathy_os/project_index/models.py +501 -0
  170. empathy_os/project_index/reports.py +473 -0
  171. empathy_os/project_index/scanner.py +538 -0
  172. empathy_os/prompts/__init__.py +61 -0
  173. empathy_os/prompts/config.py +77 -0
  174. empathy_os/prompts/context.py +177 -0
  175. empathy_os/prompts/parser.py +285 -0
  176. empathy_os/prompts/registry.py +313 -0
  177. empathy_os/prompts/templates.py +208 -0
  178. empathy_os/redis_config.py +144 -58
  179. empathy_os/redis_memory.py +53 -56
  180. empathy_os/resilience/__init__.py +56 -0
  181. empathy_os/resilience/circuit_breaker.py +256 -0
  182. empathy_os/resilience/fallback.py +179 -0
  183. empathy_os/resilience/health.py +300 -0
  184. empathy_os/resilience/retry.py +209 -0
  185. empathy_os/resilience/timeout.py +135 -0
  186. empathy_os/routing/__init__.py +43 -0
  187. empathy_os/routing/chain_executor.py +433 -0
  188. empathy_os/routing/classifier.py +217 -0
  189. empathy_os/routing/smart_router.py +234 -0
  190. empathy_os/routing/wizard_registry.py +307 -0
  191. empathy_os/templates.py +12 -11
  192. empathy_os/trust/__init__.py +28 -0
  193. empathy_os/trust/circuit_breaker.py +579 -0
  194. empathy_os/trust_building.py +44 -36
  195. empathy_os/validation/__init__.py +19 -0
  196. empathy_os/validation/xml_validator.py +281 -0
  197. empathy_os/wizard_factory_cli.py +170 -0
  198. empathy_os/{workflows.py → workflow_commands.py} +123 -31
  199. empathy_os/workflows/__init__.py +360 -0
  200. empathy_os/workflows/base.py +1660 -0
  201. empathy_os/workflows/bug_predict.py +962 -0
  202. empathy_os/workflows/code_review.py +960 -0
  203. empathy_os/workflows/code_review_adapters.py +310 -0
  204. empathy_os/workflows/code_review_pipeline.py +720 -0
  205. empathy_os/workflows/config.py +600 -0
  206. empathy_os/workflows/dependency_check.py +648 -0
  207. empathy_os/workflows/document_gen.py +1069 -0
  208. empathy_os/workflows/documentation_orchestrator.py +1205 -0
  209. empathy_os/workflows/health_check.py +679 -0
  210. empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
  211. empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
  212. empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
  213. empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
  214. empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
  215. empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
  216. empathy_os/workflows/manage_documentation.py +804 -0
  217. empathy_os/workflows/new_sample_workflow1.py +146 -0
  218. empathy_os/workflows/new_sample_workflow1_README.md +150 -0
  219. empathy_os/workflows/perf_audit.py +687 -0
  220. empathy_os/workflows/pr_review.py +748 -0
  221. empathy_os/workflows/progress.py +445 -0
  222. empathy_os/workflows/progress_server.py +322 -0
  223. empathy_os/workflows/refactor_plan.py +693 -0
  224. empathy_os/workflows/release_prep.py +808 -0
  225. empathy_os/workflows/research_synthesis.py +404 -0
  226. empathy_os/workflows/secure_release.py +585 -0
  227. empathy_os/workflows/security_adapters.py +297 -0
  228. empathy_os/workflows/security_audit.py +1046 -0
  229. empathy_os/workflows/step_config.py +234 -0
  230. empathy_os/workflows/test5.py +125 -0
  231. empathy_os/workflows/test5_README.md +158 -0
  232. empathy_os/workflows/test_gen.py +1855 -0
  233. empathy_os/workflows/test_lifecycle.py +526 -0
  234. empathy_os/workflows/test_maintenance.py +626 -0
  235. empathy_os/workflows/test_maintenance_cli.py +590 -0
  236. empathy_os/workflows/test_maintenance_crew.py +821 -0
  237. empathy_os/workflows/xml_enhanced_crew.py +285 -0
  238. empathy_software_plugin/__init__.py +1 -2
  239. empathy_software_plugin/cli/__init__.py +120 -0
  240. empathy_software_plugin/cli/inspect.py +362 -0
  241. empathy_software_plugin/cli.py +35 -26
  242. empathy_software_plugin/plugin.py +4 -8
  243. empathy_software_plugin/wizards/__init__.py +42 -0
  244. empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
  245. empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
  246. empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
  247. empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
  248. empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
  249. empathy_software_plugin/wizards/base_wizard.py +288 -0
  250. empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
  251. empathy_software_plugin/wizards/code_review_wizard.py +606 -0
  252. empathy_software_plugin/wizards/debugging/__init__.py +50 -0
  253. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
  254. empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
  255. empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
  256. empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
  257. empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
  258. empathy_software_plugin/wizards/debugging/verification.py +369 -0
  259. empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
  260. empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
  261. empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
  262. empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
  263. empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
  264. empathy_software_plugin/wizards/performance/__init__.py +9 -0
  265. empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
  266. empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
  267. empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
  268. empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
  269. empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
  270. empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
  271. empathy_software_plugin/wizards/security/__init__.py +32 -0
  272. empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
  273. empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
  274. empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
  275. empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
  276. empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
  277. empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
  278. empathy_software_plugin/wizards/testing/__init__.py +27 -0
  279. empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
  280. empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
  281. empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
  282. empathy_software_plugin/wizards/testing_wizard.py +274 -0
  283. hot_reload/README.md +473 -0
  284. hot_reload/__init__.py +62 -0
  285. hot_reload/config.py +84 -0
  286. hot_reload/integration.py +228 -0
  287. hot_reload/reloader.py +298 -0
  288. hot_reload/watcher.py +179 -0
  289. hot_reload/websocket.py +176 -0
  290. scaffolding/README.md +589 -0
  291. scaffolding/__init__.py +35 -0
  292. scaffolding/__main__.py +14 -0
  293. scaffolding/cli.py +240 -0
  294. test_generator/__init__.py +38 -0
  295. test_generator/__main__.py +14 -0
  296. test_generator/cli.py +226 -0
  297. test_generator/generator.py +325 -0
  298. test_generator/risk_analyzer.py +216 -0
  299. workflow_patterns/__init__.py +33 -0
  300. workflow_patterns/behavior.py +249 -0
  301. workflow_patterns/core.py +76 -0
  302. workflow_patterns/output.py +99 -0
  303. workflow_patterns/registry.py +255 -0
  304. workflow_patterns/structural.py +288 -0
  305. workflow_scaffolding/__init__.py +11 -0
  306. workflow_scaffolding/__main__.py +12 -0
  307. workflow_scaffolding/cli.py +206 -0
  308. workflow_scaffolding/generator.py +265 -0
  309. agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
  310. agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
  311. agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
  312. agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
  313. agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
  314. agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
  315. agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
  316. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
  317. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
  318. agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
  319. agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
  320. agents/compliance_anticipation_agent.py +0 -1427
  321. agents/epic_integration_wizard.py +0 -541
  322. agents/trust_building_behaviors.py +0 -891
  323. empathy_framework-3.2.3.dist-info/RECORD +0 -104
  324. empathy_framework-3.2.3.dist-info/entry_points.txt +0 -7
  325. empathy_llm_toolkit/htmlcov/status.json +0 -1
  326. empathy_llm_toolkit/security/htmlcov/status.json +0 -1
  327. {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/WHEEL +0 -0
  328. {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,671 @@
1
+ """Fallback and Resilience Policies for Multi-Model Workflows
2
+
3
+ Provides abstractions for handling LLM failures gracefully:
4
+ - FallbackPolicy: Define fallback chains for providers/tiers
5
+ - CircuitBreaker: Temporarily disable failing providers
6
+ - RetryPolicy: Configure retry behavior
7
+
8
+ Copyright 2025 Smart-AI-Memory
9
+ Licensed under Fair Source License 0.9
10
+ """
11
+
12
+ import time
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timedelta
16
+ from enum import Enum
17
+ from typing import Any, cast
18
+
19
+ from .registry import get_model
20
+
21
+
22
+ class FallbackStrategy(Enum):
23
+ """Strategies for selecting fallback models."""
24
+
25
+ # Try same tier with different provider
26
+ SAME_TIER_DIFFERENT_PROVIDER = "same_tier_different_provider"
27
+
28
+ # Try cheaper tier with same provider
29
+ CHEAPER_TIER_SAME_PROVIDER = "cheaper_tier_same_provider"
30
+
31
+ # Try different provider, any tier
32
+ DIFFERENT_PROVIDER_ANY_TIER = "different_provider_any_tier"
33
+
34
+ # Custom fallback chain
35
+ CUSTOM = "custom"
36
+
37
+
38
+ @dataclass
39
+ class FallbackStep:
40
+ """A single step in a fallback chain."""
41
+
42
+ provider: str
43
+ tier: str
44
+ description: str = ""
45
+
46
+ @property
47
+ def model_id(self) -> str:
48
+ """Get the model ID for this step."""
49
+ model = get_model(self.provider, self.tier)
50
+ return model.id if model else ""
51
+
52
+
53
+ @dataclass
54
+ class FallbackPolicy:
55
+ """Policy for handling LLM failures with fallback chains.
56
+
57
+ Example:
58
+ >>> policy = FallbackPolicy(
59
+ ... primary_provider="anthropic",
60
+ ... primary_tier="capable",
61
+ ... strategy=FallbackStrategy.SAME_TIER_DIFFERENT_PROVIDER,
62
+ ... )
63
+ >>> chain = policy.get_fallback_chain()
64
+ >>> # Returns: [("openai", "capable"), ("ollama", "capable")]
65
+
66
+ """
67
+
68
+ # Primary configuration
69
+ primary_provider: str = "anthropic"
70
+ primary_tier: str = "capable"
71
+
72
+ # Fallback configuration
73
+ strategy: FallbackStrategy = FallbackStrategy.SAME_TIER_DIFFERENT_PROVIDER
74
+ custom_chain: list[FallbackStep] = field(default_factory=list)
75
+
76
+ # Retry configuration
77
+ max_retries: int = 2
78
+ retry_delay_ms: int = 1000
79
+ exponential_backoff: bool = True
80
+
81
+ # Timeout configuration
82
+ timeout_ms: int = 30000
83
+
84
+ def get_fallback_chain(self) -> list[FallbackStep]:
85
+ """Get the fallback chain based on strategy.
86
+
87
+ Returns:
88
+ List of FallbackStep in order of preference
89
+
90
+ """
91
+ if self.strategy == FallbackStrategy.CUSTOM:
92
+ return self.custom_chain
93
+
94
+ chain: list[FallbackStep] = []
95
+ all_providers = ["anthropic", "openai", "ollama"]
96
+ all_tiers = ["premium", "capable", "cheap"]
97
+
98
+ if self.strategy == FallbackStrategy.SAME_TIER_DIFFERENT_PROVIDER:
99
+ # Try same tier with other providers
100
+ for provider in all_providers:
101
+ if provider != self.primary_provider:
102
+ chain.append(
103
+ FallbackStep(
104
+ provider=provider,
105
+ tier=self.primary_tier,
106
+ description=f"Same tier ({self.primary_tier}) on {provider}",
107
+ ),
108
+ )
109
+
110
+ elif self.strategy == FallbackStrategy.CHEAPER_TIER_SAME_PROVIDER:
111
+ # Try cheaper tiers with same provider
112
+ tier_index = all_tiers.index(self.primary_tier) if self.primary_tier in all_tiers else 1
113
+ for tier in all_tiers[tier_index + 1 :]:
114
+ chain.append(
115
+ FallbackStep(
116
+ provider=self.primary_provider,
117
+ tier=tier,
118
+ description=f"Cheaper tier ({tier}) on {self.primary_provider}",
119
+ ),
120
+ )
121
+
122
+ elif self.strategy == FallbackStrategy.DIFFERENT_PROVIDER_ANY_TIER:
123
+ # Try other providers, preferring same tier then cheaper
124
+ for provider in all_providers:
125
+ if provider != self.primary_provider:
126
+ # Try same tier first
127
+ chain.append(
128
+ FallbackStep(
129
+ provider=provider,
130
+ tier=self.primary_tier,
131
+ description=f"{self.primary_tier} on {provider}",
132
+ ),
133
+ )
134
+ # Then cheaper tiers
135
+ tier_index = (
136
+ all_tiers.index(self.primary_tier) if self.primary_tier in all_tiers else 1
137
+ )
138
+ for tier in all_tiers[tier_index + 1 :]:
139
+ chain.append(
140
+ FallbackStep(
141
+ provider=provider,
142
+ tier=tier,
143
+ description=f"{tier} on {provider}",
144
+ ),
145
+ )
146
+
147
+ return chain
148
+
149
+
150
+ @dataclass
151
+ class CircuitBreakerState:
152
+ """State of a circuit breaker for a provider."""
153
+
154
+ failure_count: int = 0
155
+ last_failure: datetime | None = None
156
+ is_open: bool = False
157
+ opened_at: datetime | None = None
158
+
159
+
160
+ class CircuitBreaker:
161
+ """Circuit breaker to temporarily disable failing providers.
162
+
163
+ Prevents cascading failures by stopping calls to providers that
164
+ are experiencing issues. Tracks state per provider:tier combination
165
+ for fine-grained control (e.g., Opus rate-limited shouldn't block Haiku).
166
+
167
+ Example:
168
+ >>> breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=60)
169
+ >>> if breaker.is_available("anthropic", "capable"):
170
+ ... try:
171
+ ... response = call_llm(...)
172
+ ... breaker.record_success("anthropic", "capable")
173
+ ... except Exception as e:
174
+ ... breaker.record_failure("anthropic", "capable")
175
+
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ failure_threshold: int = 5,
181
+ recovery_timeout_seconds: int = 60,
182
+ half_open_calls: int = 1,
183
+ ):
184
+ """Initialize circuit breaker.
185
+
186
+ Args:
187
+ failure_threshold: Failures before opening circuit
188
+ recovery_timeout_seconds: Time before trying again
189
+ half_open_calls: Calls to allow in half-open state
190
+
191
+ """
192
+ self.failure_threshold = failure_threshold
193
+ self.recovery_timeout = timedelta(seconds=recovery_timeout_seconds)
194
+ self.half_open_calls = half_open_calls
195
+ self._states: dict[str, CircuitBreakerState] = {}
196
+
197
+ def _get_key(self, provider: str, tier: str | None = None) -> str:
198
+ """Get the state key for a provider:tier combination."""
199
+ if tier:
200
+ return f"{provider}:{tier}"
201
+ return provider
202
+
203
+ def _get_state(self, provider: str, tier: str | None = None) -> CircuitBreakerState:
204
+ """Get or create state for a provider:tier combination."""
205
+ key = self._get_key(provider, tier)
206
+ if key not in self._states:
207
+ self._states[key] = CircuitBreakerState()
208
+ return self._states[key]
209
+
210
+ def is_available(self, provider: str, tier: str | None = None) -> bool:
211
+ """Check if a provider:tier is available.
212
+
213
+ Args:
214
+ provider: Provider to check
215
+ tier: Optional tier (if None, checks provider-level)
216
+
217
+ Returns:
218
+ True if provider:tier can be called
219
+
220
+ """
221
+ state = self._get_state(provider, tier)
222
+
223
+ if not state.is_open:
224
+ return True
225
+
226
+ # Check if recovery timeout has passed
227
+ if state.opened_at:
228
+ time_since_open = datetime.now() - state.opened_at
229
+ if time_since_open >= self.recovery_timeout:
230
+ # Half-open: allow limited calls
231
+ return True
232
+
233
+ return False
234
+
235
+ def record_success(self, provider: str, tier: str | None = None) -> None:
236
+ """Record a successful call.
237
+
238
+ Args:
239
+ provider: Provider that succeeded
240
+ tier: Optional tier
241
+
242
+ """
243
+ state = self._get_state(provider, tier)
244
+
245
+ # Reset on success
246
+ state.failure_count = 0
247
+ state.is_open = False
248
+ state.opened_at = None
249
+
250
+ def record_failure(self, provider: str, tier: str | None = None) -> None:
251
+ """Record a failed call.
252
+
253
+ Args:
254
+ provider: Provider that failed
255
+ tier: Optional tier
256
+
257
+ """
258
+ state = self._get_state(provider, tier)
259
+
260
+ state.failure_count += 1
261
+ state.last_failure = datetime.now()
262
+
263
+ if state.failure_count >= self.failure_threshold:
264
+ state.is_open = True
265
+ state.opened_at = datetime.now()
266
+
267
+ def get_status(self) -> dict[str, dict[str, Any]]:
268
+ """Get status of all tracked providers."""
269
+ return {
270
+ provider: {
271
+ "failure_count": state.failure_count,
272
+ "is_open": state.is_open,
273
+ "last_failure": state.last_failure.isoformat() if state.last_failure else None,
274
+ "opened_at": state.opened_at.isoformat() if state.opened_at else None,
275
+ }
276
+ for provider, state in self._states.items()
277
+ }
278
+
279
+ def reset(self, provider: str | None = None, tier: str | None = None) -> None:
280
+ """Reset circuit breaker state.
281
+
282
+ Args:
283
+ provider: Provider to reset (all if None)
284
+ tier: Tier to reset (provider-level if None)
285
+
286
+ """
287
+ if provider:
288
+ key = self._get_key(provider, tier)
289
+ if key in self._states:
290
+ self._states[key] = CircuitBreakerState()
291
+ else:
292
+ self._states.clear()
293
+
294
+
295
+ @dataclass
296
+ class RetryPolicy:
297
+ """Policy for retrying failed LLM calls.
298
+
299
+ Configures how many times to retry and with what delays.
300
+ """
301
+
302
+ max_retries: int = 3
303
+ initial_delay_ms: int = 1000
304
+ max_delay_ms: int = 30000
305
+ exponential_backoff: bool = True
306
+ backoff_multiplier: float = 2.0
307
+ retry_on_errors: list[str] = field(
308
+ default_factory=lambda: [
309
+ "rate_limit",
310
+ "timeout",
311
+ "server_error",
312
+ "connection_error",
313
+ ],
314
+ )
315
+
316
+ def get_delay_ms(self, attempt: int) -> int:
317
+ """Get delay before retry attempt.
318
+
319
+ Args:
320
+ attempt: Current attempt number (1-indexed)
321
+
322
+ Returns:
323
+ Delay in milliseconds
324
+
325
+ """
326
+ if not self.exponential_backoff:
327
+ return self.initial_delay_ms
328
+
329
+ delay = self.initial_delay_ms * (self.backoff_multiplier ** (attempt - 1))
330
+ return min(int(delay), self.max_delay_ms)
331
+
332
+ def should_retry(self, error_type: str, attempt: int) -> bool:
333
+ """Check if should retry for this error.
334
+
335
+ Args:
336
+ error_type: Type of error encountered
337
+ attempt: Current attempt number
338
+
339
+ Returns:
340
+ True if should retry
341
+
342
+ """
343
+ if attempt >= self.max_retries:
344
+ return False
345
+
346
+ return error_type in self.retry_on_errors
347
+
348
+
349
+ class AllProvidersFailedError(Exception):
350
+ """Raised when all fallback providers have failed."""
351
+
352
+ def __init__(self, message: str, attempts: list[dict[str, Any]]):
353
+ super().__init__(message)
354
+ self.attempts = attempts
355
+
356
+
357
+ class ResilientExecutor:
358
+ """Wrapper that adds resilience to LLM execution.
359
+
360
+ Combines fallback policies, circuit breakers, and retry logic.
361
+ Implements the LLMExecutor protocol by wrapping another executor.
362
+
363
+ Example:
364
+ >>> from empathy_os.models.empathy_executor import EmpathyLLMExecutor
365
+ >>> base_executor = EmpathyLLMExecutor(provider="anthropic")
366
+ >>> resilient = ResilientExecutor(executor=base_executor)
367
+ >>> response = await resilient.run("summarize", "Summarize this...")
368
+
369
+ """
370
+
371
+ def __init__(
372
+ self,
373
+ executor: Any | None = None,
374
+ fallback_policy: FallbackPolicy | None = None,
375
+ circuit_breaker: CircuitBreaker | None = None,
376
+ retry_policy: RetryPolicy | None = None,
377
+ ):
378
+ """Initialize resilient executor.
379
+
380
+ Args:
381
+ executor: Inner LLMExecutor to wrap
382
+ fallback_policy: Fallback configuration
383
+ circuit_breaker: Circuit breaker instance
384
+ retry_policy: Retry configuration
385
+
386
+ """
387
+ self._executor = executor
388
+ self.fallback_policy = fallback_policy or FallbackPolicy()
389
+ self.circuit_breaker = circuit_breaker or CircuitBreaker()
390
+ self.retry_policy = retry_policy or RetryPolicy()
391
+
392
+ async def run(
393
+ self,
394
+ task_type: str,
395
+ prompt: str,
396
+ system: str | None = None,
397
+ context: Any | None = None,
398
+ **kwargs: Any,
399
+ ) -> Any:
400
+ """Execute LLM call with retry and fallback support.
401
+
402
+ Implements the LLMExecutor protocol. Uses per-call policies from
403
+ context.metadata if provided.
404
+
405
+ Args:
406
+ task_type: Type of task for routing
407
+ prompt: The user prompt
408
+ system: Optional system prompt
409
+ context: Optional ExecutionContext (can contain retry_policy, fallback_policy)
410
+ **kwargs: Additional arguments
411
+
412
+ Returns:
413
+ LLMResponse from the wrapped executor
414
+
415
+ """
416
+ if self._executor is None:
417
+ raise RuntimeError("ResilientExecutor requires an inner executor")
418
+
419
+ # Allow per-call policy overrides via context.metadata
420
+ retry_policy = self.retry_policy
421
+ fallback_policy = self.fallback_policy
422
+
423
+ if context and hasattr(context, "metadata"):
424
+ if "retry_policy" in context.metadata:
425
+ retry_policy = context.metadata["retry_policy"]
426
+ if "fallback_policy" in context.metadata:
427
+ fallback_policy = context.metadata["fallback_policy"]
428
+
429
+ # Build execution chain: primary + fallbacks
430
+ chain = [
431
+ FallbackStep(
432
+ provider=fallback_policy.primary_provider,
433
+ tier=fallback_policy.primary_tier,
434
+ description="Primary",
435
+ ),
436
+ ] + fallback_policy.get_fallback_chain()
437
+
438
+ attempts: list[dict[str, Any]] = []
439
+ last_error: Exception | None = None
440
+ total_retries = 0 # Track total retry count across all attempts
441
+
442
+ for step in chain:
443
+ # Check circuit breaker (per provider:tier)
444
+ if not self.circuit_breaker.is_available(step.provider, step.tier):
445
+ attempts.append(
446
+ {
447
+ "provider": step.provider,
448
+ "tier": step.tier,
449
+ "skipped": True,
450
+ "reason": "circuit_breaker_open",
451
+ "circuit_breaker_state": "open",
452
+ },
453
+ )
454
+ continue
455
+
456
+ # Try with retries
457
+ for attempt_num in range(1, retry_policy.max_retries + 1):
458
+ try:
459
+ # Update context with current provider/tier hints
460
+ if context and hasattr(context, "provider_hint"):
461
+ context.provider_hint = step.provider
462
+ if context and hasattr(context, "tier_hint"):
463
+ context.tier_hint = step.tier
464
+
465
+ response = await self._executor.run(
466
+ task_type=task_type,
467
+ prompt=prompt,
468
+ system=system,
469
+ context=context,
470
+ **kwargs,
471
+ )
472
+
473
+ # Success - record and return
474
+ self.circuit_breaker.record_success(step.provider, step.tier)
475
+
476
+ # Add resilience metadata to response
477
+ if hasattr(response, "metadata"):
478
+ response.metadata["fallback_used"] = step.description != "Primary"
479
+ response.metadata["attempts"] = attempts
480
+ response.metadata["retry_count"] = total_retries
481
+ response.metadata["circuit_breaker_state"] = "closed"
482
+ response.metadata["original_provider"] = fallback_policy.primary_provider
483
+ response.metadata["original_tier"] = fallback_policy.primary_tier
484
+ if step.description != "Primary":
485
+ response.metadata["fallback_chain"] = [
486
+ f"{a['provider']}:{a['tier']}" for a in attempts
487
+ ]
488
+
489
+ return response
490
+
491
+ except Exception as e:
492
+ last_error = e
493
+ error_type = self._classify_error(e)
494
+ total_retries += 1 # Increment retry counter
495
+
496
+ if retry_policy.should_retry(error_type, attempt_num):
497
+ delay = retry_policy.get_delay_ms(attempt_num)
498
+ time.sleep(delay / 1000)
499
+ continue
500
+
501
+ # Record failure and move to next fallback
502
+ self.circuit_breaker.record_failure(step.provider, step.tier)
503
+ attempts.append(
504
+ {
505
+ "provider": step.provider,
506
+ "tier": step.tier,
507
+ "skipped": False,
508
+ "error": str(e),
509
+ "error_type": error_type,
510
+ "attempt": attempt_num,
511
+ },
512
+ )
513
+ break
514
+
515
+ # All fallbacks exhausted
516
+ raise AllProvidersFailedError(
517
+ f"All fallback options exhausted. Last error: {last_error}",
518
+ attempts=attempts,
519
+ ) from last_error
520
+
521
+ def get_model_for_task(self, task_type: str) -> str:
522
+ """Delegate to inner executor."""
523
+ if self._executor and hasattr(self._executor, "get_model_for_task"):
524
+ result: str = cast("str", self._executor.get_model_for_task(task_type))
525
+ return result
526
+ return ""
527
+
528
+ def estimate_cost(
529
+ self,
530
+ task_type: str,
531
+ input_tokens: int,
532
+ output_tokens: int,
533
+ ) -> float:
534
+ """Delegate to inner executor."""
535
+ if self._executor and hasattr(self._executor, "estimate_cost"):
536
+ result: float = cast(
537
+ "float",
538
+ self._executor.estimate_cost(task_type, input_tokens, output_tokens),
539
+ )
540
+ return result
541
+ return 0.0
542
+
543
+ async def execute_with_fallback(
544
+ self,
545
+ call_fn: Callable,
546
+ *args: Any,
547
+ **kwargs: Any,
548
+ ) -> tuple[Any, dict[str, Any]]:
549
+ """Execute LLM call with fallback support (legacy API).
550
+
551
+ Args:
552
+ call_fn: Async function to call (takes provider, model as kwargs)
553
+ *args: Positional arguments for call_fn
554
+ **kwargs: Keyword arguments for call_fn
555
+
556
+ Returns:
557
+ Tuple of (result, metadata) where metadata includes fallback info
558
+
559
+ """
560
+ metadata: dict[str, Any] = {
561
+ "fallback_used": False,
562
+ "fallback_chain": [],
563
+ "attempts": 0,
564
+ "original_provider": self.fallback_policy.primary_provider,
565
+ "original_model": None,
566
+ }
567
+
568
+ # Build execution chain: primary + fallbacks
569
+ chain = [
570
+ FallbackStep(
571
+ provider=self.fallback_policy.primary_provider,
572
+ tier=self.fallback_policy.primary_tier,
573
+ description="Primary",
574
+ ),
575
+ ] + self.fallback_policy.get_fallback_chain()
576
+
577
+ last_error: Exception | None = None
578
+
579
+ for step in chain:
580
+ # Check circuit breaker (per provider:tier)
581
+ if not self.circuit_breaker.is_available(step.provider, step.tier):
582
+ metadata["fallback_chain"].append(
583
+ {
584
+ "provider": step.provider,
585
+ "tier": step.tier,
586
+ "skipped": True,
587
+ "reason": "circuit_breaker_open",
588
+ },
589
+ )
590
+ continue
591
+
592
+ # Try with retries
593
+ for attempt in range(1, self.retry_policy.max_retries + 1):
594
+ metadata["attempts"] += 1
595
+
596
+ try:
597
+ result = await call_fn(
598
+ *args,
599
+ provider=step.provider,
600
+ model=step.model_id,
601
+ **kwargs,
602
+ )
603
+
604
+ # Success
605
+ self.circuit_breaker.record_success(step.provider, step.tier)
606
+
607
+ if step.description != "Primary":
608
+ metadata["fallback_used"] = True
609
+
610
+ metadata["final_provider"] = step.provider
611
+ metadata["final_tier"] = step.tier
612
+ metadata["final_model"] = step.model_id
613
+
614
+ return result, metadata
615
+
616
+ except Exception as e:
617
+ last_error = e
618
+ error_type = self._classify_error(e)
619
+
620
+ if self.retry_policy.should_retry(error_type, attempt):
621
+ delay = self.retry_policy.get_delay_ms(attempt)
622
+ time.sleep(delay / 1000)
623
+ continue
624
+
625
+ # Record failure and move to next fallback
626
+ self.circuit_breaker.record_failure(step.provider, step.tier)
627
+ metadata["fallback_chain"].append(
628
+ {
629
+ "provider": step.provider,
630
+ "tier": step.tier,
631
+ "skipped": False,
632
+ "error": str(e),
633
+ "error_type": error_type,
634
+ },
635
+ )
636
+ break
637
+
638
+ # All fallbacks exhausted
639
+ raise AllProvidersFailedError(
640
+ f"All fallback options exhausted. Last error: {last_error}",
641
+ attempts=metadata["fallback_chain"],
642
+ ) from last_error
643
+
644
+ def _classify_error(self, error: Exception) -> str:
645
+ """Classify an error for retry decisions."""
646
+ error_str = str(error).lower()
647
+
648
+ if "rate" in error_str or "limit" in error_str:
649
+ return "rate_limit"
650
+ if "timeout" in error_str:
651
+ return "timeout"
652
+ if "connection" in error_str:
653
+ return "connection_error"
654
+ if "500" in error_str or "502" in error_str or "503" in error_str:
655
+ return "server_error"
656
+ return "unknown"
657
+
658
+
659
+ # Default policies
660
+ DEFAULT_FALLBACK_POLICY = FallbackPolicy(
661
+ primary_provider="anthropic",
662
+ primary_tier="capable",
663
+ strategy=FallbackStrategy.SAME_TIER_DIFFERENT_PROVIDER,
664
+ max_retries=2,
665
+ )
666
+
667
+ DEFAULT_RETRY_POLICY = RetryPolicy(
668
+ max_retries=3,
669
+ initial_delay_ms=1000,
670
+ exponential_backoff=True,
671
+ )