empathy-framework 2.4.0__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 (329) hide show
  1. coach_wizards/__init__.py +13 -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 +661 -0
  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.8.2.dist-info/METADATA +1176 -0
  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-2.4.0.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 +186 -28
  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 +168 -53
  64. empathy_llm_toolkit/git_pattern_extractor.py +17 -13
  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 +16 -14
  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 +20 -22
  86. empathy_llm_toolkit/state.py +28 -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 +125 -84
  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} +28 -28
  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 +1516 -70
  105. empathy_os/cli_unified.py +597 -0
  106. empathy_os/config/__init__.py +63 -0
  107. empathy_os/config/xml_config.py +239 -0
  108. empathy_os/config.py +95 -37
  109. empathy_os/coordination.py +72 -68
  110. empathy_os/core.py +94 -107
  111. empathy_os/cost_tracker.py +74 -55
  112. empathy_os/dashboard/__init__.py +15 -0
  113. empathy_os/dashboard/server.py +743 -0
  114. empathy_os/discovery.py +17 -14
  115. empathy_os/emergence.py +21 -22
  116. empathy_os/exceptions.py +18 -30
  117. empathy_os/feedback_loops.py +30 -33
  118. empathy_os/levels.py +32 -35
  119. empathy_os/leverage_points.py +31 -32
  120. empathy_os/logging_config.py +19 -16
  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 +30 -29
  160. empathy_os/persistence.py +35 -37
  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 +79 -77
  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 +19 -14
  192. empathy_os/trust/__init__.py +28 -0
  193. empathy_os/trust/circuit_breaker.py +579 -0
  194. empathy_os/trust_building.py +67 -58
  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} +131 -37
  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 +49 -27
  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-2.4.0.dist-info/METADATA +0 -485
  324. empathy_framework-2.4.0.dist-info/RECORD +0 -102
  325. empathy_framework-2.4.0.dist-info/entry_points.txt +0 -6
  326. empathy_llm_toolkit/htmlcov/status.json +0 -1
  327. empathy_llm_toolkit/security/htmlcov/status.json +0 -1
  328. {empathy_framework-2.4.0.dist-info → empathy_framework-3.8.2.dist-info}/WHEEL +0 -0
  329. {empathy_framework-2.4.0.dist-info → empathy_framework-3.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,563 @@
1
+ """Provider Configuration System
2
+
3
+ Handles user provider selection during install/update and runtime configuration.
4
+ Supports single-provider mode (default) and hybrid mode (multi-provider).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from .registry import MODEL_REGISTRY, ModelInfo, ModelTier
17
+
18
+
19
+ class ProviderMode(str, Enum):
20
+ """How the system selects models across providers."""
21
+
22
+ SINGLE = "single" # Use one provider for all tiers
23
+ HYBRID = "hybrid" # Best-of across providers (requires multiple API keys)
24
+ CUSTOM = "custom" # User-defined per-tier mapping
25
+
26
+
27
+ @dataclass
28
+ class ProviderConfig:
29
+ """User's provider configuration."""
30
+
31
+ # Primary mode
32
+ mode: ProviderMode = ProviderMode.SINGLE
33
+
34
+ # For SINGLE mode: which provider to use
35
+ primary_provider: str = "anthropic"
36
+
37
+ # For CUSTOM mode: per-tier provider overrides
38
+ tier_providers: dict[str, str] = field(default_factory=dict)
39
+
40
+ # API key availability (detected at runtime)
41
+ available_providers: list[str] = field(default_factory=list)
42
+
43
+ # User preferences
44
+ prefer_local: bool = False # Prefer Ollama when available
45
+ cost_optimization: bool = True # Use cheaper tiers when appropriate
46
+
47
+ @classmethod
48
+ def detect_available_providers(cls) -> list[str]:
49
+ """Detect which providers have API keys configured."""
50
+ available = []
51
+
52
+ # Load .env files if they exist (project root and home)
53
+ env_keys = cls._load_env_files()
54
+
55
+ # Check environment variables for API keys
56
+ provider_env_vars = {
57
+ "anthropic": ["ANTHROPIC_API_KEY"],
58
+ "openai": ["OPENAI_API_KEY"],
59
+ "google": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
60
+ "ollama": [], # Ollama is local, check if running
61
+ }
62
+
63
+ for provider, env_vars in provider_env_vars.items():
64
+ if provider == "ollama":
65
+ # Check if Ollama is available (local)
66
+ if cls._check_ollama_available():
67
+ available.append(provider)
68
+ elif any(os.getenv(var) or env_keys.get(var) for var in env_vars):
69
+ available.append(provider)
70
+
71
+ return available
72
+
73
+ @staticmethod
74
+ def _load_env_files() -> dict[str, str]:
75
+ """Load API keys from .env files without modifying os.environ."""
76
+ env_keys: dict[str, str] = {}
77
+
78
+ # Possible .env file locations
79
+ env_paths = [
80
+ Path.cwd() / ".env",
81
+ Path.home() / ".env",
82
+ Path.home() / ".empathy" / ".env",
83
+ ]
84
+
85
+ for env_path in env_paths:
86
+ if env_path.exists():
87
+ try:
88
+ with open(env_path) as f:
89
+ for line in f:
90
+ line = line.strip()
91
+ if line and not line.startswith("#") and "=" in line:
92
+ key, _, value = line.partition("=")
93
+ key = key.strip()
94
+ value = value.strip().strip("'\"")
95
+ if key and value:
96
+ env_keys[key] = value
97
+ except Exception:
98
+ pass
99
+
100
+ return env_keys
101
+
102
+ @staticmethod
103
+ def _check_ollama_available() -> bool:
104
+ """Check if Ollama is running locally."""
105
+ try:
106
+ import socket
107
+
108
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109
+ sock.settimeout(1)
110
+ result = sock.connect_ex(("localhost", 11434))
111
+ sock.close()
112
+ return result == 0
113
+ except Exception:
114
+ return False
115
+
116
+ @classmethod
117
+ def auto_detect(cls) -> ProviderConfig:
118
+ """Auto-detect the best configuration based on available API keys.
119
+
120
+ Logic:
121
+ - If only one provider available → SINGLE mode with that provider
122
+ - If multiple providers available → SINGLE mode with first cloud provider
123
+ - If no providers available → SINGLE mode with anthropic (will prompt for key)
124
+ """
125
+ available = cls.detect_available_providers()
126
+
127
+ if len(available) == 0:
128
+ # No providers detected, default to anthropic
129
+ return cls(
130
+ mode=ProviderMode.SINGLE,
131
+ primary_provider="anthropic",
132
+ available_providers=[],
133
+ )
134
+ if len(available) == 1:
135
+ # Single provider available, use it
136
+ return cls(
137
+ mode=ProviderMode.SINGLE,
138
+ primary_provider=available[0],
139
+ available_providers=available,
140
+ )
141
+ # Multiple providers available
142
+ # Default to first cloud provider (prefer anthropic > openai > google > ollama)
143
+ priority = ["anthropic", "openai", "google", "ollama"]
144
+ primary = next((p for p in priority if p in available), available[0])
145
+ return cls(
146
+ mode=ProviderMode.SINGLE,
147
+ primary_provider=primary,
148
+ available_providers=available,
149
+ )
150
+
151
+ def get_model_for_tier(self, tier: str | ModelTier) -> ModelInfo | None:
152
+ """Get the model to use for a given tier based on current config."""
153
+ tier_str = tier.value if isinstance(tier, ModelTier) else tier
154
+
155
+ if self.mode == ProviderMode.HYBRID:
156
+ # Use hybrid provider from registry
157
+ return MODEL_REGISTRY.get("hybrid", {}).get(tier_str)
158
+ if self.mode == ProviderMode.CUSTOM:
159
+ # Use per-tier provider mapping
160
+ provider = self.tier_providers.get(tier_str, self.primary_provider)
161
+ return MODEL_REGISTRY.get(provider, {}).get(tier_str)
162
+ # SINGLE mode: use primary provider for all tiers
163
+ return MODEL_REGISTRY.get(self.primary_provider, {}).get(tier_str)
164
+
165
+ def get_effective_registry(self) -> dict[str, ModelInfo]:
166
+ """Get the effective model registry based on current config."""
167
+ result = {}
168
+ for tier in ["cheap", "capable", "premium"]:
169
+ model = self.get_model_for_tier(tier)
170
+ if model:
171
+ result[tier] = model
172
+ return result
173
+
174
+ def to_dict(self) -> dict[str, Any]:
175
+ """Serialize to dictionary."""
176
+ return {
177
+ "mode": self.mode.value,
178
+ "primary_provider": self.primary_provider,
179
+ "tier_providers": self.tier_providers,
180
+ "prefer_local": self.prefer_local,
181
+ "cost_optimization": self.cost_optimization,
182
+ }
183
+
184
+ @classmethod
185
+ def from_dict(cls, data: dict[str, Any]) -> ProviderConfig:
186
+ """Deserialize from dictionary."""
187
+ return cls(
188
+ mode=ProviderMode(data.get("mode", "single")),
189
+ primary_provider=data.get("primary_provider", "anthropic"),
190
+ tier_providers=data.get("tier_providers", {}),
191
+ prefer_local=data.get("prefer_local", False),
192
+ cost_optimization=data.get("cost_optimization", True),
193
+ available_providers=cls.detect_available_providers(),
194
+ )
195
+
196
+ def save(self, path: Path | None = None) -> None:
197
+ """Save configuration to file."""
198
+ if path is None:
199
+ path = Path.home() / ".empathy" / "provider_config.json"
200
+ path.parent.mkdir(parents=True, exist_ok=True)
201
+ with open(path, "w") as f:
202
+ json.dump(self.to_dict(), f, indent=2)
203
+
204
+ @classmethod
205
+ def load(cls, path: Path | None = None) -> ProviderConfig:
206
+ """Load configuration from file, or auto-detect if not found."""
207
+ if path is None:
208
+ path = Path.home() / ".empathy" / "provider_config.json"
209
+
210
+ if path.exists():
211
+ try:
212
+ with open(path) as f:
213
+ data = json.load(f)
214
+ return cls.from_dict(data)
215
+ except Exception:
216
+ pass
217
+
218
+ # Auto-detect if no config exists
219
+ return cls.auto_detect()
220
+
221
+
222
+ # Interactive configuration for install/update
223
+ def configure_provider_interactive() -> ProviderConfig:
224
+ """Interactive provider configuration for install/update.
225
+
226
+ Returns configured ProviderConfig after user selection.
227
+ """
228
+ print("\n" + "=" * 60)
229
+ print("Empathy Framework - Provider Configuration")
230
+ print("=" * 60)
231
+
232
+ # Detect available providers
233
+ config = ProviderConfig.auto_detect()
234
+ available = config.available_providers
235
+
236
+ print(f"\nDetected API keys for: {', '.join(available) if available else 'None'}")
237
+
238
+ if not available:
239
+ print("\n⚠️ No API keys detected. Please set one of:")
240
+ print(" - ANTHROPIC_API_KEY (recommended)")
241
+ print(" - OPENAI_API_KEY")
242
+ print(" - GOOGLE_API_KEY or GEMINI_API_KEY (2M context window)")
243
+ print(" - Or run Ollama locally")
244
+ print("\nDefaulting to Anthropic. You'll need to set ANTHROPIC_API_KEY.")
245
+ return ProviderConfig(
246
+ mode=ProviderMode.SINGLE,
247
+ primary_provider="anthropic",
248
+ available_providers=[],
249
+ )
250
+
251
+ # Show options
252
+ print("\nSelect your provider configuration:")
253
+ print("-" * 40)
254
+
255
+ options = []
256
+
257
+ # Option 1: Single provider (for each available)
258
+ for i, provider in enumerate(available, 1):
259
+ provider_name = provider.capitalize()
260
+ if provider == "anthropic":
261
+ desc = "Claude models (Haiku/Sonnet/Opus)"
262
+ elif provider == "openai":
263
+ desc = "GPT models (GPT-4o-mini/GPT-4o/o1)"
264
+ elif provider == "google":
265
+ desc = "Gemini models (Flash/Pro - 2M context window)"
266
+ elif provider == "ollama":
267
+ desc = "Local models (Llama 3.2)"
268
+ else:
269
+ desc = "Unknown provider"
270
+ options.append((provider, ProviderMode.SINGLE))
271
+ print(f" [{i}] {provider_name} only - {desc}")
272
+
273
+ # Option: Hybrid (if multiple providers available)
274
+ if len(available) > 1:
275
+ options.append(("hybrid", ProviderMode.HYBRID))
276
+ print(f" [{len(options)}] Hybrid - Best model from each provider per tier")
277
+ print(" (Recommended if you have multiple API keys)")
278
+
279
+ # Default selection
280
+ default_idx = 0
281
+ if len(available) == 1:
282
+ default_idx = 0
283
+ elif "anthropic" in available:
284
+ default_idx = available.index("anthropic")
285
+
286
+ print(f"\nDefault: [{default_idx + 1}]")
287
+
288
+ # Get user input
289
+ try:
290
+ choice = input(f"\nYour choice [1-{len(options)}]: ").strip()
291
+ if not choice:
292
+ choice = str(default_idx + 1)
293
+ idx = int(choice) - 1
294
+ if idx < 0 or idx >= len(options):
295
+ idx = default_idx
296
+ except (ValueError, EOFError):
297
+ idx = default_idx
298
+
299
+ selected_provider, selected_mode = options[idx]
300
+
301
+ if selected_mode == ProviderMode.HYBRID:
302
+ config = ProviderConfig(
303
+ mode=ProviderMode.HYBRID,
304
+ primary_provider="hybrid",
305
+ available_providers=available,
306
+ )
307
+ print("\n✓ Configured: Hybrid mode (best-of across providers)")
308
+ else:
309
+ config = ProviderConfig(
310
+ mode=ProviderMode.SINGLE,
311
+ primary_provider=selected_provider,
312
+ available_providers=available,
313
+ )
314
+ print(f"\n✓ Configured: {selected_provider.capitalize()} as primary provider")
315
+
316
+ # Show effective models
317
+ print("\nEffective model mapping:")
318
+ effective = config.get_effective_registry()
319
+ for tier, model in effective.items():
320
+ if model:
321
+ print(f" {tier:8} → {model.id} ({model.provider})")
322
+
323
+ # Save configuration
324
+ config.save()
325
+ print("\nConfiguration saved to ~/.empathy/provider_config.json")
326
+
327
+ return config
328
+
329
+
330
+ def configure_provider_cli(
331
+ provider: str | None = None,
332
+ mode: str | None = None,
333
+ ) -> ProviderConfig:
334
+ """CLI-based provider configuration (non-interactive).
335
+
336
+ Args:
337
+ provider: Provider name (anthropic, openai, google, ollama, hybrid)
338
+ mode: Mode (single, hybrid, custom)
339
+
340
+ Returns:
341
+ Configured ProviderConfig
342
+
343
+ """
344
+ available = ProviderConfig.detect_available_providers()
345
+
346
+ if provider == "hybrid" or mode == "hybrid":
347
+ return ProviderConfig(
348
+ mode=ProviderMode.HYBRID,
349
+ primary_provider="hybrid",
350
+ available_providers=available,
351
+ )
352
+
353
+ if provider:
354
+ return ProviderConfig(
355
+ mode=ProviderMode.SINGLE,
356
+ primary_provider=provider,
357
+ available_providers=available,
358
+ )
359
+
360
+ # Auto-detect
361
+ return ProviderConfig.auto_detect()
362
+
363
+
364
+ # Global config instance (lazy-loaded)
365
+ _global_config: ProviderConfig | None = None
366
+
367
+
368
+ def get_provider_config() -> ProviderConfig:
369
+ """Get the global provider configuration."""
370
+ global _global_config
371
+ if _global_config is None:
372
+ _global_config = ProviderConfig.load()
373
+ return _global_config
374
+
375
+
376
+ def set_provider_config(config: ProviderConfig) -> None:
377
+ """Set the global provider configuration."""
378
+ global _global_config
379
+ _global_config = config
380
+
381
+
382
+ def reset_provider_config() -> None:
383
+ """Reset the global provider configuration (forces reload)."""
384
+ global _global_config
385
+ _global_config = None
386
+
387
+
388
+ def configure_hybrid_interactive() -> ProviderConfig:
389
+ """Interactive hybrid configuration - let users pick models for each tier.
390
+
391
+ Shows available models from all providers with detected API keys,
392
+ allowing users to mix and match the best models for their workflow.
393
+
394
+ Returns:
395
+ ProviderConfig with custom tier mappings
396
+
397
+ """
398
+ print("\n" + "=" * 60)
399
+ print("🔀 Hybrid Model Configuration")
400
+ print("=" * 60)
401
+ print("\nSelect the best model for each tier from available providers.")
402
+ print("This creates a custom mix optimized for your workflow.\n")
403
+
404
+ # Detect available providers
405
+ available = ProviderConfig.detect_available_providers()
406
+
407
+ if not available:
408
+ print("⚠️ No API keys detected. Please set at least one of:")
409
+ print(" - ANTHROPIC_API_KEY")
410
+ print(" - OPENAI_API_KEY")
411
+ print(" - GOOGLE_API_KEY")
412
+ print(" - Or run Ollama locally")
413
+ return ProviderConfig.auto_detect()
414
+
415
+ print(f"✓ Available providers: {', '.join(available)}\n")
416
+
417
+ # Collect models for each tier from available providers
418
+ tier_selections: dict[str, str] = {}
419
+
420
+ for tier in ["cheap", "capable", "premium"]:
421
+ tier_upper = tier.upper()
422
+ print("-" * 60)
423
+ print(f" {tier_upper} TIER - Select a model:")
424
+ print("-" * 60)
425
+
426
+ # Build options from available providers
427
+ options: list[tuple[str, ModelInfo]] = []
428
+ for provider in available:
429
+ model_info = MODEL_REGISTRY.get(provider, {}).get(tier)
430
+ if model_info:
431
+ options.append((provider, model_info))
432
+
433
+ if not options:
434
+ print(f" No models available for {tier} tier")
435
+ continue
436
+
437
+ # Display options with pricing info
438
+ for i, (provider, info) in enumerate(options, 1):
439
+ provider_label = provider.capitalize()
440
+ cost_info = f"${info.input_cost_per_million:.2f}/${info.output_cost_per_million:.2f} per M tokens"
441
+ if provider == "ollama":
442
+ cost_info = "FREE (local)"
443
+
444
+ # Add feature badges
445
+ features = []
446
+ if info.supports_vision:
447
+ features.append("👁 vision")
448
+ if info.supports_tools:
449
+ features.append("🔧 tools")
450
+ if provider == "google":
451
+ features.append("📚 2M context")
452
+
453
+ features_str = f" [{', '.join(features)}]" if features else ""
454
+
455
+ print(f" [{i}] {info.id}")
456
+ print(f" Provider: {provider_label} | {cost_info}{features_str}")
457
+
458
+ # Get user choice
459
+ default_idx = 0
460
+ # Set smart defaults based on tier
461
+ if tier == "cheap":
462
+ # Prefer cheapest: ollama > google > openai > anthropic
463
+ for pref in ["ollama", "google", "openai", "anthropic"]:
464
+ for i, (p, _) in enumerate(options):
465
+ if p == pref:
466
+ default_idx = i
467
+ break
468
+ else:
469
+ continue
470
+ break
471
+ elif tier == "capable":
472
+ # Prefer best reasoning: anthropic > openai > google > ollama
473
+ for pref in ["anthropic", "openai", "google", "ollama"]:
474
+ for i, (p, _) in enumerate(options):
475
+ if p == pref:
476
+ default_idx = i
477
+ break
478
+ else:
479
+ continue
480
+ break
481
+ elif tier == "premium":
482
+ # Prefer most capable: anthropic > openai > google > ollama
483
+ for pref in ["anthropic", "openai", "google", "ollama"]:
484
+ for i, (p, _) in enumerate(options):
485
+ if p == pref:
486
+ default_idx = i
487
+ break
488
+ else:
489
+ continue
490
+ break
491
+
492
+ print(f"\n Recommended: [{default_idx + 1}] {options[default_idx][1].id}")
493
+
494
+ try:
495
+ choice = input(f" Your choice [1-{len(options)}]: ").strip()
496
+ if not choice:
497
+ idx = default_idx
498
+ else:
499
+ idx = int(choice) - 1
500
+ if idx < 0 or idx >= len(options):
501
+ idx = default_idx
502
+ except (ValueError, EOFError):
503
+ idx = default_idx
504
+
505
+ selected_provider, selected_model = options[idx]
506
+ tier_selections[tier] = selected_model.id
507
+ print(f" ✓ Selected: {selected_model.id} ({selected_provider})\n")
508
+
509
+ # Create custom config
510
+ config = ProviderConfig(
511
+ mode=ProviderMode.CUSTOM,
512
+ primary_provider="custom",
513
+ tier_providers={}, # Not used in CUSTOM mode
514
+ available_providers=available,
515
+ )
516
+
517
+ # Store the custom tier->model mapping
518
+ # We'll save this to workflows.yaml custom_models section
519
+ print("\n" + "=" * 60)
520
+ print("✅ Hybrid Configuration Complete!")
521
+ print("=" * 60)
522
+ print("\nYour custom model mapping:")
523
+ for tier, model_id in tier_selections.items():
524
+ print(f" {tier:8} → {model_id}")
525
+
526
+ # Save to workflows.yaml
527
+ _save_hybrid_to_workflows_yaml(tier_selections)
528
+
529
+ print("\n✓ Configuration saved to .empathy/workflows.yaml")
530
+ print(" Run workflows with: python -m empathy_os.cli workflow run <name>")
531
+
532
+ return config
533
+
534
+
535
+ def _save_hybrid_to_workflows_yaml(tier_selections: dict[str, str]) -> None:
536
+ """Save hybrid tier selections to workflows.yaml."""
537
+ from pathlib import Path
538
+
539
+ import yaml
540
+
541
+ workflows_path = Path(".empathy/workflows.yaml")
542
+
543
+ # Load existing config or create new
544
+ if workflows_path.exists():
545
+ with open(workflows_path) as f:
546
+ config = yaml.safe_load(f) or {}
547
+ else:
548
+ config = {}
549
+ workflows_path.parent.mkdir(parents=True, exist_ok=True)
550
+
551
+ # Update config
552
+ config["default_provider"] = "hybrid"
553
+
554
+ # Ensure custom_models exists
555
+ if "custom_models" not in config or config["custom_models"] is None:
556
+ config["custom_models"] = {}
557
+
558
+ # Set hybrid model mapping
559
+ config["custom_models"]["hybrid"] = tier_selections
560
+
561
+ # Write back
562
+ with open(workflows_path, "w") as f:
563
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)