attune-ai 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. attune/__init__.py +358 -0
  2. attune/adaptive/__init__.py +13 -0
  3. attune/adaptive/task_complexity.py +127 -0
  4. attune/agent_monitoring.py +414 -0
  5. attune/cache/__init__.py +117 -0
  6. attune/cache/base.py +166 -0
  7. attune/cache/dependency_manager.py +256 -0
  8. attune/cache/hash_only.py +251 -0
  9. attune/cache/hybrid.py +457 -0
  10. attune/cache/storage.py +285 -0
  11. attune/cache_monitor.py +356 -0
  12. attune/cache_stats.py +298 -0
  13. attune/cli/__init__.py +152 -0
  14. attune/cli/__main__.py +12 -0
  15. attune/cli/commands/__init__.py +1 -0
  16. attune/cli/commands/batch.py +264 -0
  17. attune/cli/commands/cache.py +248 -0
  18. attune/cli/commands/help.py +331 -0
  19. attune/cli/commands/info.py +140 -0
  20. attune/cli/commands/inspect.py +436 -0
  21. attune/cli/commands/inspection.py +57 -0
  22. attune/cli/commands/memory.py +48 -0
  23. attune/cli/commands/metrics.py +92 -0
  24. attune/cli/commands/orchestrate.py +184 -0
  25. attune/cli/commands/patterns.py +207 -0
  26. attune/cli/commands/profiling.py +202 -0
  27. attune/cli/commands/provider.py +98 -0
  28. attune/cli/commands/routing.py +285 -0
  29. attune/cli/commands/setup.py +96 -0
  30. attune/cli/commands/status.py +235 -0
  31. attune/cli/commands/sync.py +166 -0
  32. attune/cli/commands/tier.py +121 -0
  33. attune/cli/commands/utilities.py +114 -0
  34. attune/cli/commands/workflow.py +579 -0
  35. attune/cli/core.py +32 -0
  36. attune/cli/parsers/__init__.py +68 -0
  37. attune/cli/parsers/batch.py +118 -0
  38. attune/cli/parsers/cache.py +65 -0
  39. attune/cli/parsers/help.py +41 -0
  40. attune/cli/parsers/info.py +26 -0
  41. attune/cli/parsers/inspect.py +66 -0
  42. attune/cli/parsers/metrics.py +42 -0
  43. attune/cli/parsers/orchestrate.py +61 -0
  44. attune/cli/parsers/patterns.py +54 -0
  45. attune/cli/parsers/provider.py +40 -0
  46. attune/cli/parsers/routing.py +110 -0
  47. attune/cli/parsers/setup.py +42 -0
  48. attune/cli/parsers/status.py +47 -0
  49. attune/cli/parsers/sync.py +31 -0
  50. attune/cli/parsers/tier.py +33 -0
  51. attune/cli/parsers/workflow.py +77 -0
  52. attune/cli/utils/__init__.py +1 -0
  53. attune/cli/utils/data.py +242 -0
  54. attune/cli/utils/helpers.py +68 -0
  55. attune/cli_legacy.py +3957 -0
  56. attune/cli_minimal.py +1159 -0
  57. attune/cli_router.py +437 -0
  58. attune/cli_unified.py +814 -0
  59. attune/config/__init__.py +66 -0
  60. attune/config/xml_config.py +286 -0
  61. attune/config.py +545 -0
  62. attune/coordination.py +870 -0
  63. attune/core.py +1511 -0
  64. attune/core_modules/__init__.py +15 -0
  65. attune/cost_tracker.py +626 -0
  66. attune/dashboard/__init__.py +41 -0
  67. attune/dashboard/app.py +512 -0
  68. attune/dashboard/simple_server.py +435 -0
  69. attune/dashboard/standalone_server.py +547 -0
  70. attune/discovery.py +306 -0
  71. attune/emergence.py +306 -0
  72. attune/exceptions.py +123 -0
  73. attune/feedback_loops.py +373 -0
  74. attune/hot_reload/README.md +473 -0
  75. attune/hot_reload/__init__.py +62 -0
  76. attune/hot_reload/config.py +83 -0
  77. attune/hot_reload/integration.py +229 -0
  78. attune/hot_reload/reloader.py +298 -0
  79. attune/hot_reload/watcher.py +183 -0
  80. attune/hot_reload/websocket.py +177 -0
  81. attune/levels.py +577 -0
  82. attune/leverage_points.py +441 -0
  83. attune/logging_config.py +261 -0
  84. attune/mcp/__init__.py +10 -0
  85. attune/mcp/server.py +506 -0
  86. attune/memory/__init__.py +237 -0
  87. attune/memory/claude_memory.py +469 -0
  88. attune/memory/config.py +224 -0
  89. attune/memory/control_panel.py +1290 -0
  90. attune/memory/control_panel_support.py +145 -0
  91. attune/memory/cross_session.py +845 -0
  92. attune/memory/edges.py +179 -0
  93. attune/memory/encryption.py +159 -0
  94. attune/memory/file_session.py +770 -0
  95. attune/memory/graph.py +570 -0
  96. attune/memory/long_term.py +913 -0
  97. attune/memory/long_term_types.py +99 -0
  98. attune/memory/mixins/__init__.py +25 -0
  99. attune/memory/mixins/backend_init_mixin.py +249 -0
  100. attune/memory/mixins/capabilities_mixin.py +208 -0
  101. attune/memory/mixins/handoff_mixin.py +208 -0
  102. attune/memory/mixins/lifecycle_mixin.py +49 -0
  103. attune/memory/mixins/long_term_mixin.py +352 -0
  104. attune/memory/mixins/promotion_mixin.py +109 -0
  105. attune/memory/mixins/short_term_mixin.py +182 -0
  106. attune/memory/nodes.py +179 -0
  107. attune/memory/redis_bootstrap.py +540 -0
  108. attune/memory/security/__init__.py +31 -0
  109. attune/memory/security/audit_logger.py +932 -0
  110. attune/memory/security/pii_scrubber.py +640 -0
  111. attune/memory/security/secrets_detector.py +678 -0
  112. attune/memory/short_term.py +2192 -0
  113. attune/memory/simple_storage.py +302 -0
  114. attune/memory/storage/__init__.py +15 -0
  115. attune/memory/storage_backend.py +167 -0
  116. attune/memory/summary_index.py +583 -0
  117. attune/memory/types.py +446 -0
  118. attune/memory/unified.py +182 -0
  119. attune/meta_workflows/__init__.py +74 -0
  120. attune/meta_workflows/agent_creator.py +248 -0
  121. attune/meta_workflows/builtin_templates.py +567 -0
  122. attune/meta_workflows/cli_commands/__init__.py +56 -0
  123. attune/meta_workflows/cli_commands/agent_commands.py +321 -0
  124. attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
  125. attune/meta_workflows/cli_commands/config_commands.py +232 -0
  126. attune/meta_workflows/cli_commands/memory_commands.py +182 -0
  127. attune/meta_workflows/cli_commands/template_commands.py +354 -0
  128. attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
  129. attune/meta_workflows/cli_meta_workflows.py +59 -0
  130. attune/meta_workflows/form_engine.py +292 -0
  131. attune/meta_workflows/intent_detector.py +409 -0
  132. attune/meta_workflows/models.py +569 -0
  133. attune/meta_workflows/pattern_learner.py +738 -0
  134. attune/meta_workflows/plan_generator.py +384 -0
  135. attune/meta_workflows/session_context.py +397 -0
  136. attune/meta_workflows/template_registry.py +229 -0
  137. attune/meta_workflows/workflow.py +984 -0
  138. attune/metrics/__init__.py +12 -0
  139. attune/metrics/collector.py +31 -0
  140. attune/metrics/prompt_metrics.py +194 -0
  141. attune/models/__init__.py +172 -0
  142. attune/models/__main__.py +13 -0
  143. attune/models/adaptive_routing.py +437 -0
  144. attune/models/auth_cli.py +444 -0
  145. attune/models/auth_strategy.py +450 -0
  146. attune/models/cli.py +655 -0
  147. attune/models/empathy_executor.py +354 -0
  148. attune/models/executor.py +257 -0
  149. attune/models/fallback.py +762 -0
  150. attune/models/provider_config.py +282 -0
  151. attune/models/registry.py +472 -0
  152. attune/models/tasks.py +359 -0
  153. attune/models/telemetry/__init__.py +71 -0
  154. attune/models/telemetry/analytics.py +594 -0
  155. attune/models/telemetry/backend.py +196 -0
  156. attune/models/telemetry/data_models.py +431 -0
  157. attune/models/telemetry/storage.py +489 -0
  158. attune/models/token_estimator.py +420 -0
  159. attune/models/validation.py +280 -0
  160. attune/monitoring/__init__.py +52 -0
  161. attune/monitoring/alerts.py +946 -0
  162. attune/monitoring/alerts_cli.py +448 -0
  163. attune/monitoring/multi_backend.py +271 -0
  164. attune/monitoring/otel_backend.py +362 -0
  165. attune/optimization/__init__.py +19 -0
  166. attune/optimization/context_optimizer.py +272 -0
  167. attune/orchestration/__init__.py +67 -0
  168. attune/orchestration/agent_templates.py +707 -0
  169. attune/orchestration/config_store.py +499 -0
  170. attune/orchestration/execution_strategies.py +2111 -0
  171. attune/orchestration/meta_orchestrator.py +1168 -0
  172. attune/orchestration/pattern_learner.py +696 -0
  173. attune/orchestration/real_tools.py +931 -0
  174. attune/pattern_cache.py +187 -0
  175. attune/pattern_library.py +542 -0
  176. attune/patterns/debugging/all_patterns.json +81 -0
  177. attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
  178. attune/patterns/refactoring_memory.json +89 -0
  179. attune/persistence.py +564 -0
  180. attune/platform_utils.py +265 -0
  181. attune/plugins/__init__.py +28 -0
  182. attune/plugins/base.py +361 -0
  183. attune/plugins/registry.py +268 -0
  184. attune/project_index/__init__.py +32 -0
  185. attune/project_index/cli.py +335 -0
  186. attune/project_index/index.py +667 -0
  187. attune/project_index/models.py +504 -0
  188. attune/project_index/reports.py +474 -0
  189. attune/project_index/scanner.py +777 -0
  190. attune/project_index/scanner_parallel.py +291 -0
  191. attune/prompts/__init__.py +61 -0
  192. attune/prompts/config.py +77 -0
  193. attune/prompts/context.py +177 -0
  194. attune/prompts/parser.py +285 -0
  195. attune/prompts/registry.py +313 -0
  196. attune/prompts/templates.py +208 -0
  197. attune/redis_config.py +302 -0
  198. attune/redis_memory.py +799 -0
  199. attune/resilience/__init__.py +56 -0
  200. attune/resilience/circuit_breaker.py +256 -0
  201. attune/resilience/fallback.py +179 -0
  202. attune/resilience/health.py +300 -0
  203. attune/resilience/retry.py +209 -0
  204. attune/resilience/timeout.py +135 -0
  205. attune/routing/__init__.py +43 -0
  206. attune/routing/chain_executor.py +433 -0
  207. attune/routing/classifier.py +217 -0
  208. attune/routing/smart_router.py +234 -0
  209. attune/routing/workflow_registry.py +343 -0
  210. attune/scaffolding/README.md +589 -0
  211. attune/scaffolding/__init__.py +35 -0
  212. attune/scaffolding/__main__.py +14 -0
  213. attune/scaffolding/cli.py +240 -0
  214. attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
  215. attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
  216. attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
  217. attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
  218. attune/socratic/__init__.py +256 -0
  219. attune/socratic/ab_testing.py +958 -0
  220. attune/socratic/blueprint.py +533 -0
  221. attune/socratic/cli.py +703 -0
  222. attune/socratic/collaboration.py +1114 -0
  223. attune/socratic/domain_templates.py +924 -0
  224. attune/socratic/embeddings.py +738 -0
  225. attune/socratic/engine.py +794 -0
  226. attune/socratic/explainer.py +682 -0
  227. attune/socratic/feedback.py +772 -0
  228. attune/socratic/forms.py +629 -0
  229. attune/socratic/generator.py +732 -0
  230. attune/socratic/llm_analyzer.py +637 -0
  231. attune/socratic/mcp_server.py +702 -0
  232. attune/socratic/session.py +312 -0
  233. attune/socratic/storage.py +667 -0
  234. attune/socratic/success.py +730 -0
  235. attune/socratic/visual_editor.py +860 -0
  236. attune/socratic/web_ui.py +958 -0
  237. attune/telemetry/__init__.py +39 -0
  238. attune/telemetry/agent_coordination.py +475 -0
  239. attune/telemetry/agent_tracking.py +367 -0
  240. attune/telemetry/approval_gates.py +545 -0
  241. attune/telemetry/cli.py +1231 -0
  242. attune/telemetry/commands/__init__.py +14 -0
  243. attune/telemetry/commands/dashboard_commands.py +696 -0
  244. attune/telemetry/event_streaming.py +409 -0
  245. attune/telemetry/feedback_loop.py +567 -0
  246. attune/telemetry/usage_tracker.py +591 -0
  247. attune/templates.py +754 -0
  248. attune/test_generator/__init__.py +38 -0
  249. attune/test_generator/__main__.py +14 -0
  250. attune/test_generator/cli.py +234 -0
  251. attune/test_generator/generator.py +355 -0
  252. attune/test_generator/risk_analyzer.py +216 -0
  253. attune/test_generator/templates/unit_test.py.jinja2 +272 -0
  254. attune/tier_recommender.py +384 -0
  255. attune/tools.py +183 -0
  256. attune/trust/__init__.py +28 -0
  257. attune/trust/circuit_breaker.py +579 -0
  258. attune/trust_building.py +527 -0
  259. attune/validation/__init__.py +19 -0
  260. attune/validation/xml_validator.py +281 -0
  261. attune/vscode_bridge.py +173 -0
  262. attune/workflow_commands.py +780 -0
  263. attune/workflow_patterns/__init__.py +33 -0
  264. attune/workflow_patterns/behavior.py +249 -0
  265. attune/workflow_patterns/core.py +76 -0
  266. attune/workflow_patterns/output.py +99 -0
  267. attune/workflow_patterns/registry.py +255 -0
  268. attune/workflow_patterns/structural.py +288 -0
  269. attune/workflows/__init__.py +539 -0
  270. attune/workflows/autonomous_test_gen.py +1268 -0
  271. attune/workflows/base.py +2667 -0
  272. attune/workflows/batch_processing.py +342 -0
  273. attune/workflows/bug_predict.py +1084 -0
  274. attune/workflows/builder.py +273 -0
  275. attune/workflows/caching.py +253 -0
  276. attune/workflows/code_review.py +1048 -0
  277. attune/workflows/code_review_adapters.py +312 -0
  278. attune/workflows/code_review_pipeline.py +722 -0
  279. attune/workflows/config.py +645 -0
  280. attune/workflows/dependency_check.py +644 -0
  281. attune/workflows/document_gen/__init__.py +25 -0
  282. attune/workflows/document_gen/config.py +30 -0
  283. attune/workflows/document_gen/report_formatter.py +162 -0
  284. attune/workflows/document_gen/workflow.py +1426 -0
  285. attune/workflows/document_manager.py +216 -0
  286. attune/workflows/document_manager_README.md +134 -0
  287. attune/workflows/documentation_orchestrator.py +1205 -0
  288. attune/workflows/history.py +510 -0
  289. attune/workflows/keyboard_shortcuts/__init__.py +39 -0
  290. attune/workflows/keyboard_shortcuts/generators.py +391 -0
  291. attune/workflows/keyboard_shortcuts/parsers.py +416 -0
  292. attune/workflows/keyboard_shortcuts/prompts.py +295 -0
  293. attune/workflows/keyboard_shortcuts/schema.py +193 -0
  294. attune/workflows/keyboard_shortcuts/workflow.py +509 -0
  295. attune/workflows/llm_base.py +363 -0
  296. attune/workflows/manage_docs.py +87 -0
  297. attune/workflows/manage_docs_README.md +134 -0
  298. attune/workflows/manage_documentation.py +821 -0
  299. attune/workflows/new_sample_workflow1.py +149 -0
  300. attune/workflows/new_sample_workflow1_README.md +150 -0
  301. attune/workflows/orchestrated_health_check.py +849 -0
  302. attune/workflows/orchestrated_release_prep.py +600 -0
  303. attune/workflows/output.py +413 -0
  304. attune/workflows/perf_audit.py +863 -0
  305. attune/workflows/pr_review.py +762 -0
  306. attune/workflows/progress.py +785 -0
  307. attune/workflows/progress_server.py +322 -0
  308. attune/workflows/progressive/README 2.md +454 -0
  309. attune/workflows/progressive/README.md +454 -0
  310. attune/workflows/progressive/__init__.py +82 -0
  311. attune/workflows/progressive/cli.py +219 -0
  312. attune/workflows/progressive/core.py +488 -0
  313. attune/workflows/progressive/orchestrator.py +723 -0
  314. attune/workflows/progressive/reports.py +520 -0
  315. attune/workflows/progressive/telemetry.py +274 -0
  316. attune/workflows/progressive/test_gen.py +495 -0
  317. attune/workflows/progressive/workflow.py +589 -0
  318. attune/workflows/refactor_plan.py +694 -0
  319. attune/workflows/release_prep.py +895 -0
  320. attune/workflows/release_prep_crew.py +969 -0
  321. attune/workflows/research_synthesis.py +404 -0
  322. attune/workflows/routing.py +168 -0
  323. attune/workflows/secure_release.py +593 -0
  324. attune/workflows/security_adapters.py +297 -0
  325. attune/workflows/security_audit.py +1329 -0
  326. attune/workflows/security_audit_phase3.py +355 -0
  327. attune/workflows/seo_optimization.py +633 -0
  328. attune/workflows/step_config.py +234 -0
  329. attune/workflows/telemetry_mixin.py +269 -0
  330. attune/workflows/test5.py +125 -0
  331. attune/workflows/test5_README.md +158 -0
  332. attune/workflows/test_coverage_boost_crew.py +849 -0
  333. attune/workflows/test_gen/__init__.py +52 -0
  334. attune/workflows/test_gen/ast_analyzer.py +249 -0
  335. attune/workflows/test_gen/config.py +88 -0
  336. attune/workflows/test_gen/data_models.py +38 -0
  337. attune/workflows/test_gen/report_formatter.py +289 -0
  338. attune/workflows/test_gen/test_templates.py +381 -0
  339. attune/workflows/test_gen/workflow.py +655 -0
  340. attune/workflows/test_gen.py +54 -0
  341. attune/workflows/test_gen_behavioral.py +477 -0
  342. attune/workflows/test_gen_parallel.py +341 -0
  343. attune/workflows/test_lifecycle.py +526 -0
  344. attune/workflows/test_maintenance.py +627 -0
  345. attune/workflows/test_maintenance_cli.py +590 -0
  346. attune/workflows/test_maintenance_crew.py +840 -0
  347. attune/workflows/test_runner.py +622 -0
  348. attune/workflows/tier_tracking.py +531 -0
  349. attune/workflows/xml_enhanced_crew.py +285 -0
  350. attune_ai-2.0.0.dist-info/METADATA +1026 -0
  351. attune_ai-2.0.0.dist-info/RECORD +457 -0
  352. attune_ai-2.0.0.dist-info/WHEEL +5 -0
  353. attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
  354. attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
  355. attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
  356. attune_ai-2.0.0.dist-info/top_level.txt +5 -0
  357. attune_healthcare/__init__.py +13 -0
  358. attune_healthcare/monitors/__init__.py +9 -0
  359. attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
  360. attune_healthcare/monitors/monitoring/__init__.py +44 -0
  361. attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
  362. attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
  363. attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
  364. attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
  365. attune_llm/README.md +553 -0
  366. attune_llm/__init__.py +28 -0
  367. attune_llm/agent_factory/__init__.py +53 -0
  368. attune_llm/agent_factory/adapters/__init__.py +85 -0
  369. attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
  370. attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
  371. attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
  372. attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
  373. attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
  374. attune_llm/agent_factory/adapters/native.py +228 -0
  375. attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
  376. attune_llm/agent_factory/base.py +305 -0
  377. attune_llm/agent_factory/crews/__init__.py +67 -0
  378. attune_llm/agent_factory/crews/code_review.py +1113 -0
  379. attune_llm/agent_factory/crews/health_check.py +1262 -0
  380. attune_llm/agent_factory/crews/refactoring.py +1128 -0
  381. attune_llm/agent_factory/crews/security_audit.py +1018 -0
  382. attune_llm/agent_factory/decorators.py +287 -0
  383. attune_llm/agent_factory/factory.py +558 -0
  384. attune_llm/agent_factory/framework.py +193 -0
  385. attune_llm/agent_factory/memory_integration.py +328 -0
  386. attune_llm/agent_factory/resilient.py +320 -0
  387. attune_llm/agents_md/__init__.py +22 -0
  388. attune_llm/agents_md/loader.py +218 -0
  389. attune_llm/agents_md/parser.py +271 -0
  390. attune_llm/agents_md/registry.py +307 -0
  391. attune_llm/claude_memory.py +466 -0
  392. attune_llm/cli/__init__.py +8 -0
  393. attune_llm/cli/sync_claude.py +487 -0
  394. attune_llm/code_health.py +1313 -0
  395. attune_llm/commands/__init__.py +51 -0
  396. attune_llm/commands/context.py +375 -0
  397. attune_llm/commands/loader.py +301 -0
  398. attune_llm/commands/models.py +231 -0
  399. attune_llm/commands/parser.py +371 -0
  400. attune_llm/commands/registry.py +429 -0
  401. attune_llm/config/__init__.py +29 -0
  402. attune_llm/config/unified.py +291 -0
  403. attune_llm/context/__init__.py +22 -0
  404. attune_llm/context/compaction.py +455 -0
  405. attune_llm/context/manager.py +434 -0
  406. attune_llm/contextual_patterns.py +361 -0
  407. attune_llm/core.py +907 -0
  408. attune_llm/git_pattern_extractor.py +435 -0
  409. attune_llm/hooks/__init__.py +24 -0
  410. attune_llm/hooks/config.py +306 -0
  411. attune_llm/hooks/executor.py +289 -0
  412. attune_llm/hooks/registry.py +302 -0
  413. attune_llm/hooks/scripts/__init__.py +39 -0
  414. attune_llm/hooks/scripts/evaluate_session.py +201 -0
  415. attune_llm/hooks/scripts/first_time_init.py +285 -0
  416. attune_llm/hooks/scripts/pre_compact.py +207 -0
  417. attune_llm/hooks/scripts/session_end.py +183 -0
  418. attune_llm/hooks/scripts/session_start.py +163 -0
  419. attune_llm/hooks/scripts/suggest_compact.py +225 -0
  420. attune_llm/learning/__init__.py +30 -0
  421. attune_llm/learning/evaluator.py +438 -0
  422. attune_llm/learning/extractor.py +514 -0
  423. attune_llm/learning/storage.py +560 -0
  424. attune_llm/levels.py +227 -0
  425. attune_llm/pattern_confidence.py +414 -0
  426. attune_llm/pattern_resolver.py +272 -0
  427. attune_llm/pattern_summary.py +350 -0
  428. attune_llm/providers.py +967 -0
  429. attune_llm/routing/__init__.py +32 -0
  430. attune_llm/routing/model_router.py +362 -0
  431. attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
  432. attune_llm/security/PHASE2_COMPLETE.md +384 -0
  433. attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  434. attune_llm/security/QUICK_REFERENCE.md +316 -0
  435. attune_llm/security/README.md +262 -0
  436. attune_llm/security/__init__.py +62 -0
  437. attune_llm/security/audit_logger.py +929 -0
  438. attune_llm/security/audit_logger_example.py +152 -0
  439. attune_llm/security/pii_scrubber.py +640 -0
  440. attune_llm/security/secrets_detector.py +678 -0
  441. attune_llm/security/secrets_detector_example.py +304 -0
  442. attune_llm/security/secure_memdocs.py +1192 -0
  443. attune_llm/security/secure_memdocs_example.py +278 -0
  444. attune_llm/session_status.py +745 -0
  445. attune_llm/state.py +246 -0
  446. attune_llm/utils/__init__.py +5 -0
  447. attune_llm/utils/tokens.py +349 -0
  448. attune_software/SOFTWARE_PLUGIN_README.md +57 -0
  449. attune_software/__init__.py +13 -0
  450. attune_software/cli/__init__.py +120 -0
  451. attune_software/cli/inspect.py +362 -0
  452. attune_software/cli.py +574 -0
  453. attune_software/plugin.py +188 -0
  454. workflow_scaffolding/__init__.py +11 -0
  455. workflow_scaffolding/__main__.py +12 -0
  456. workflow_scaffolding/cli.py +206 -0
  457. workflow_scaffolding/generator.py +265 -0
@@ -0,0 +1,696 @@
1
+ """Pattern Learning System - Grammar that evolves from experience.
2
+
3
+ This module implements the learning grammar that tracks pattern success
4
+ and recommends optimal compositions based on historical data.
5
+
6
+ Features:
7
+ - Track success metrics for each pattern execution
8
+ - Memory + file storage for fast access and persistence
9
+ - Hybrid recommendation: similarity matching → statistical fallback
10
+
11
+ Security:
12
+ - No eval() or exec() usage
13
+ - File paths validated before writing
14
+ - JSON serialization only (no pickle)
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ from collections import defaultdict
20
+ from collections.abc import Iterator
21
+ from dataclasses import asdict, dataclass, field
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from attune.config import _validate_file_path
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ # =============================================================================
32
+ # Data Models
33
+ # =============================================================================
34
+
35
+
36
+ @dataclass
37
+ class ExecutionRecord:
38
+ """Record of a single pattern execution.
39
+
40
+ Captures the essential metrics for learning.
41
+
42
+ Attributes:
43
+ pattern: Pattern/strategy name used
44
+ success: Whether execution succeeded
45
+ duration_seconds: Execution time
46
+ cost: Estimated cost (tokens * rate)
47
+ confidence: Aggregate confidence score
48
+ context_features: Key features of the execution context
49
+ timestamp: When the execution occurred
50
+ """
51
+
52
+ pattern: str
53
+ success: bool
54
+ duration_seconds: float
55
+ cost: float = 0.0
56
+ confidence: float = 0.0
57
+ context_features: dict[str, Any] = field(default_factory=dict)
58
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
59
+
60
+ def to_dict(self) -> dict[str, Any]:
61
+ """Convert to dictionary for serialization."""
62
+ return asdict(self)
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict[str, Any]) -> "ExecutionRecord":
66
+ """Create from dictionary."""
67
+ return cls(**data)
68
+
69
+
70
+ @dataclass
71
+ class PatternStats:
72
+ """Aggregated statistics for a pattern.
73
+
74
+ Attributes:
75
+ pattern: Pattern/strategy name
76
+ total_executions: Number of times executed
77
+ success_count: Number of successful executions
78
+ total_duration: Sum of all execution durations
79
+ total_cost: Sum of all execution costs
80
+ avg_confidence: Average confidence across executions
81
+ """
82
+
83
+ pattern: str
84
+ total_executions: int = 0
85
+ success_count: int = 0
86
+ total_duration: float = 0.0
87
+ total_cost: float = 0.0
88
+ avg_confidence: float = 0.0
89
+
90
+ @property
91
+ def success_rate(self) -> float:
92
+ """Calculate success rate (0.0 - 1.0)."""
93
+ if self.total_executions == 0:
94
+ return 0.0
95
+ return self.success_count / self.total_executions
96
+
97
+ @property
98
+ def avg_duration(self) -> float:
99
+ """Calculate average execution duration."""
100
+ if self.total_executions == 0:
101
+ return 0.0
102
+ return self.total_duration / self.total_executions
103
+
104
+ @property
105
+ def avg_cost(self) -> float:
106
+ """Calculate average execution cost."""
107
+ if self.total_executions == 0:
108
+ return 0.0
109
+ return self.total_cost / self.total_executions
110
+
111
+ def update(self, record: ExecutionRecord) -> None:
112
+ """Update stats with a new execution record.
113
+
114
+ Args:
115
+ record: Execution record to incorporate
116
+ """
117
+ self.total_executions += 1
118
+ if record.success:
119
+ self.success_count += 1
120
+ self.total_duration += record.duration_seconds
121
+ self.total_cost += record.cost
122
+
123
+ # Running average for confidence
124
+ n = self.total_executions
125
+ self.avg_confidence = (self.avg_confidence * (n - 1) + record.confidence) / n
126
+
127
+ def to_dict(self) -> dict[str, Any]:
128
+ """Convert to dictionary for serialization."""
129
+ return {
130
+ "pattern": self.pattern,
131
+ "total_executions": self.total_executions,
132
+ "success_count": self.success_count,
133
+ "total_duration": self.total_duration,
134
+ "total_cost": self.total_cost,
135
+ "avg_confidence": self.avg_confidence,
136
+ # Computed properties
137
+ "success_rate": self.success_rate,
138
+ "avg_duration": self.avg_duration,
139
+ "avg_cost": self.avg_cost,
140
+ }
141
+
142
+ @classmethod
143
+ def from_dict(cls, data: dict[str, Any]) -> "PatternStats":
144
+ """Create from dictionary."""
145
+ return cls(
146
+ pattern=data["pattern"],
147
+ total_executions=data.get("total_executions", 0),
148
+ success_count=data.get("success_count", 0),
149
+ total_duration=data.get("total_duration", 0.0),
150
+ total_cost=data.get("total_cost", 0.0),
151
+ avg_confidence=data.get("avg_confidence", 0.0),
152
+ )
153
+
154
+
155
+ @dataclass
156
+ class ContextSignature:
157
+ """Signature of a context for similarity matching.
158
+
159
+ Extracts key features from execution context for comparison.
160
+
161
+ Attributes:
162
+ task_type: Type of task (e.g., "code_review", "test_gen")
163
+ agent_count: Number of agents involved
164
+ has_conditions: Whether conditionals were used
165
+ has_nesting: Whether nested workflows were used
166
+ priority: Task priority level
167
+ """
168
+
169
+ task_type: str = ""
170
+ agent_count: int = 0
171
+ has_conditions: bool = False
172
+ has_nesting: bool = False
173
+ priority: str = "normal"
174
+
175
+ @classmethod
176
+ def from_context(cls, context: dict[str, Any]) -> "ContextSignature":
177
+ """Extract signature from execution context.
178
+
179
+ Args:
180
+ context: Execution context dictionary
181
+
182
+ Returns:
183
+ ContextSignature with extracted features
184
+ """
185
+ return cls(
186
+ task_type=context.get("task_type", context.get("_task_type", "")),
187
+ agent_count=len(context.get("agents", [])),
188
+ has_conditions="_conditional" in context,
189
+ has_nesting="_nesting" in context,
190
+ priority=context.get("priority", "normal"),
191
+ )
192
+
193
+ def similarity(self, other: "ContextSignature") -> float:
194
+ """Calculate similarity score with another signature.
195
+
196
+ Args:
197
+ other: Signature to compare with
198
+
199
+ Returns:
200
+ Similarity score (0.0 - 1.0)
201
+ """
202
+ score = 0.0
203
+ max_score = 0.0
204
+
205
+ # Task type match (highest weight)
206
+ max_score += 3.0
207
+ if self.task_type and other.task_type:
208
+ if self.task_type == other.task_type:
209
+ score += 3.0
210
+ elif self.task_type.split("_")[0] == other.task_type.split("_")[0]:
211
+ score += 1.5 # Partial match
212
+
213
+ # Agent count similarity
214
+ max_score += 1.0
215
+ if self.agent_count > 0 and other.agent_count > 0:
216
+ ratio = min(self.agent_count, other.agent_count) / max(
217
+ self.agent_count, other.agent_count
218
+ )
219
+ score += ratio
220
+
221
+ # Boolean features
222
+ max_score += 2.0
223
+ if self.has_conditions == other.has_conditions:
224
+ score += 1.0
225
+ if self.has_nesting == other.has_nesting:
226
+ score += 1.0
227
+
228
+ # Priority match
229
+ max_score += 1.0
230
+ if self.priority == other.priority:
231
+ score += 1.0
232
+
233
+ return score / max_score if max_score > 0 else 0.0
234
+
235
+ def to_dict(self) -> dict[str, Any]:
236
+ """Convert to dictionary."""
237
+ return asdict(self)
238
+
239
+ @classmethod
240
+ def from_dict(cls, data: dict[str, Any]) -> "ContextSignature":
241
+ """Create from dictionary."""
242
+ return cls(**data)
243
+
244
+
245
+ # =============================================================================
246
+ # Storage Layer
247
+ # =============================================================================
248
+
249
+
250
+ class LearningStore:
251
+ """Memory + file storage for learning data.
252
+
253
+ Maintains an in-memory cache for fast access with
254
+ periodic persistence to a JSON file.
255
+
256
+ Attributes:
257
+ file_path: Path to persistence file
258
+ _records: In-memory execution records
259
+ _stats: In-memory pattern statistics
260
+ _dirty: Whether in-memory data needs saving
261
+ """
262
+
263
+ DEFAULT_FILE = "patterns/learning_memory.json"
264
+
265
+ def __init__(self, file_path: str | None = None):
266
+ """Initialize learning store.
267
+
268
+ Args:
269
+ file_path: Path to persistence file (default: patterns/learning_memory.json)
270
+ """
271
+ self.file_path = Path(file_path or self.DEFAULT_FILE)
272
+ self._records: list[ExecutionRecord] = []
273
+ self._stats: dict[str, PatternStats] = {}
274
+ self._context_index: dict[str, list[int]] = defaultdict(list)
275
+ self._dirty = False
276
+
277
+ # Load existing data if available
278
+ self._load()
279
+
280
+ def _load(self) -> None:
281
+ """Load data from file if it exists."""
282
+ if not self.file_path.exists():
283
+ logger.info(f"No existing learning data at {self.file_path}")
284
+ return
285
+
286
+ try:
287
+ with self.file_path.open("r") as f:
288
+ data = json.load(f)
289
+
290
+ # Load records
291
+ self._records = [ExecutionRecord.from_dict(r) for r in data.get("records", [])]
292
+
293
+ # Load stats
294
+ self._stats = {s["pattern"]: PatternStats.from_dict(s) for s in data.get("stats", [])}
295
+
296
+ # Rebuild context index
297
+ for i, record in enumerate(self._records):
298
+ sig = ContextSignature(task_type=record.context_features.get("task_type", ""))
299
+ self._context_index[sig.task_type].append(i)
300
+
301
+ logger.info(
302
+ f"Loaded {len(self._records)} records, "
303
+ f"{len(self._stats)} pattern stats from {self.file_path}"
304
+ )
305
+ except json.JSONDecodeError as e:
306
+ logger.error(f"Failed to parse learning data: {e}")
307
+ except Exception as e:
308
+ logger.exception(f"Failed to load learning data: {e}")
309
+
310
+ def save(self) -> None:
311
+ """Save data to file."""
312
+ if not self._dirty:
313
+ return
314
+
315
+ # Ensure directory exists
316
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
317
+
318
+ data = {
319
+ "records": [r.to_dict() for r in self._records],
320
+ "stats": [s.to_dict() for s in self._stats.values()],
321
+ "metadata": {
322
+ "saved_at": datetime.now().isoformat(),
323
+ "total_records": len(self._records),
324
+ "patterns_tracked": len(self._stats),
325
+ },
326
+ }
327
+
328
+ try:
329
+ validated_path = _validate_file_path(str(self.file_path))
330
+ with validated_path.open("w") as f:
331
+ json.dump(data, f, indent=2)
332
+ self._dirty = False
333
+ logger.info(f"Saved learning data to {validated_path}")
334
+ except (OSError, ValueError) as e:
335
+ logger.exception(f"Failed to save learning data: {e}")
336
+
337
+ def add_record(self, record: ExecutionRecord) -> None:
338
+ """Add an execution record.
339
+
340
+ Args:
341
+ record: Record to add
342
+ """
343
+ self._records.append(record)
344
+
345
+ # Update stats
346
+ if record.pattern not in self._stats:
347
+ self._stats[record.pattern] = PatternStats(pattern=record.pattern)
348
+ self._stats[record.pattern].update(record)
349
+
350
+ # Update context index
351
+ task_type = record.context_features.get("task_type", "")
352
+ self._context_index[task_type].append(len(self._records) - 1)
353
+
354
+ self._dirty = True
355
+
356
+ # Auto-save periodically
357
+ if len(self._records) % 10 == 0:
358
+ self.save()
359
+
360
+ def get_stats(self, pattern: str) -> PatternStats | None:
361
+ """Get statistics for a pattern.
362
+
363
+ Args:
364
+ pattern: Pattern name
365
+
366
+ Returns:
367
+ PatternStats or None if not tracked
368
+ """
369
+ return self._stats.get(pattern)
370
+
371
+ def iter_all_stats(self) -> Iterator[PatternStats]:
372
+ """Iterate over all pattern statistics (memory-efficient).
373
+
374
+ Yields patterns in arbitrary order. For sorted results,
375
+ use get_all_stats().
376
+ """
377
+ yield from self._stats.values()
378
+
379
+ def get_all_stats(self) -> list[PatternStats]:
380
+ """Get all pattern statistics sorted by success rate.
381
+
382
+ Note: For large pattern sets, prefer iter_all_stats() when
383
+ you don't need sorted results.
384
+ """
385
+ return sorted(
386
+ self.iter_all_stats(),
387
+ key=lambda s: s.success_rate,
388
+ reverse=True,
389
+ )
390
+
391
+ def find_similar_records(
392
+ self, signature: ContextSignature, limit: int = 10
393
+ ) -> list[tuple[ExecutionRecord, float]]:
394
+ """Find records with similar context.
395
+
396
+ Args:
397
+ signature: Context signature to match
398
+ limit: Maximum records to return
399
+
400
+ Returns:
401
+ List of (record, similarity_score) tuples
402
+ """
403
+ scored: list[tuple[ExecutionRecord, float]] = []
404
+
405
+ for record in self._records:
406
+ record_sig = ContextSignature(
407
+ task_type=record.context_features.get("task_type", ""),
408
+ agent_count=record.context_features.get("agent_count", 0),
409
+ has_conditions=record.context_features.get("has_conditions", False),
410
+ has_nesting=record.context_features.get("has_nesting", False),
411
+ priority=record.context_features.get("priority", "normal"),
412
+ )
413
+ score = signature.similarity(record_sig)
414
+ if score > 0.3: # Minimum threshold
415
+ scored.append((record, score))
416
+
417
+ # Sort by similarity and return top results
418
+ scored.sort(key=lambda x: x[1], reverse=True)
419
+ return scored[:limit]
420
+
421
+
422
+ # =============================================================================
423
+ # Recommendation Engine
424
+ # =============================================================================
425
+
426
+
427
+ @dataclass
428
+ class PatternRecommendation:
429
+ """A pattern recommendation.
430
+
431
+ Attributes:
432
+ pattern: Recommended pattern name
433
+ confidence: Confidence in recommendation (0.0 - 1.0)
434
+ reason: Why this pattern was recommended
435
+ expected_success_rate: Predicted success rate
436
+ expected_duration: Predicted duration
437
+ """
438
+
439
+ pattern: str
440
+ confidence: float
441
+ reason: str
442
+ expected_success_rate: float = 0.0
443
+ expected_duration: float = 0.0
444
+
445
+
446
+ class PatternRecommender:
447
+ """Hybrid recommendation engine for patterns.
448
+
449
+ Uses similarity matching first, falls back to statistical ranking.
450
+ """
451
+
452
+ def __init__(self, store: LearningStore):
453
+ """Initialize recommender.
454
+
455
+ Args:
456
+ store: Learning store with historical data
457
+ """
458
+ self.store = store
459
+
460
+ def recommend(self, context: dict[str, Any], top_k: int = 3) -> list[PatternRecommendation]:
461
+ """Recommend patterns for a context.
462
+
463
+ Uses hybrid approach:
464
+ 1. Find similar past contexts
465
+ 2. Recommend patterns that worked for them
466
+ 3. Fall back to overall statistics if no matches
467
+
468
+ Args:
469
+ context: Current execution context
470
+ top_k: Number of recommendations to return
471
+
472
+ Returns:
473
+ List of PatternRecommendation
474
+ """
475
+ signature = ContextSignature.from_context(context)
476
+ recommendations: list[PatternRecommendation] = []
477
+
478
+ # Phase 1: Similarity matching
479
+ similar = self.store.find_similar_records(signature, limit=20)
480
+ if similar:
481
+ recommendations = self._recommend_from_similar(similar, top_k)
482
+
483
+ # Phase 2: Statistical fallback
484
+ if len(recommendations) < top_k:
485
+ statistical = self._recommend_statistical(top_k - len(recommendations))
486
+ recommendations.extend(statistical)
487
+
488
+ return recommendations[:top_k]
489
+
490
+ def _recommend_from_similar(
491
+ self, similar: list[tuple[ExecutionRecord, float]], top_k: int
492
+ ) -> list[PatternRecommendation]:
493
+ """Generate recommendations from similar records.
494
+
495
+ Args:
496
+ similar: List of (record, similarity) tuples
497
+ top_k: Number of recommendations
498
+
499
+ Returns:
500
+ List of recommendations
501
+ """
502
+ # Aggregate by pattern
503
+ pattern_scores: dict[str, dict[str, Any]] = defaultdict(
504
+ lambda: {"total_similarity": 0, "success_similarity": 0, "count": 0}
505
+ )
506
+
507
+ for record, similarity in similar:
508
+ pattern_scores[record.pattern]["count"] += 1
509
+ pattern_scores[record.pattern]["total_similarity"] += similarity
510
+ if record.success:
511
+ pattern_scores[record.pattern]["success_similarity"] += similarity
512
+
513
+ # Calculate weighted success rate
514
+ recommendations = []
515
+ for pattern, scores in pattern_scores.items():
516
+ if scores["total_similarity"] > 0:
517
+ weighted_success = scores["success_similarity"] / scores["total_similarity"]
518
+ stats = self.store.get_stats(pattern)
519
+
520
+ recommendations.append(
521
+ PatternRecommendation(
522
+ pattern=pattern,
523
+ confidence=min(weighted_success, 0.95),
524
+ reason=f"Worked in {scores['count']} similar contexts",
525
+ expected_success_rate=stats.success_rate if stats else 0,
526
+ expected_duration=stats.avg_duration if stats else 0,
527
+ )
528
+ )
529
+
530
+ # Sort by confidence
531
+ recommendations.sort(key=lambda r: r.confidence, reverse=True)
532
+ return recommendations[:top_k]
533
+
534
+ def _recommend_statistical(self, top_k: int) -> list[PatternRecommendation]:
535
+ """Generate recommendations from overall statistics.
536
+
537
+ Args:
538
+ top_k: Number of recommendations
539
+
540
+ Returns:
541
+ List of recommendations based on global stats
542
+ """
543
+ all_stats = self.store.get_all_stats()
544
+ recommendations = []
545
+
546
+ for stats in all_stats[:top_k]:
547
+ if stats.total_executions >= 3: # Minimum sample size
548
+ recommendations.append(
549
+ PatternRecommendation(
550
+ pattern=stats.pattern,
551
+ confidence=stats.success_rate * 0.8, # Slight penalty
552
+ reason=f"High overall success rate ({stats.success_rate:.0%})",
553
+ expected_success_rate=stats.success_rate,
554
+ expected_duration=stats.avg_duration,
555
+ )
556
+ )
557
+
558
+ return recommendations
559
+
560
+
561
+ # =============================================================================
562
+ # Main Interface
563
+ # =============================================================================
564
+
565
+
566
+ class PatternLearner:
567
+ """Main interface for the learning grammar system.
568
+
569
+ Provides a simple API for recording executions and getting recommendations.
570
+
571
+ Example:
572
+ >>> learner = PatternLearner()
573
+ >>> # Record an execution
574
+ >>> learner.record(
575
+ ... pattern="sequential",
576
+ ... success=True,
577
+ ... duration=2.5,
578
+ ... cost=0.05,
579
+ ... context={"task_type": "code_review"}
580
+ ... )
581
+ >>> # Get recommendations
582
+ >>> recs = learner.recommend({"task_type": "code_review"})
583
+ >>> print(recs[0].pattern, recs[0].confidence)
584
+ """
585
+
586
+ def __init__(self, storage_path: str | None = None):
587
+ """Initialize pattern learner.
588
+
589
+ Args:
590
+ storage_path: Path for persistence (default: patterns/learning_memory.json)
591
+ """
592
+ self.store = LearningStore(storage_path)
593
+ self.recommender = PatternRecommender(self.store)
594
+
595
+ def record(
596
+ self,
597
+ pattern: str,
598
+ success: bool,
599
+ duration: float,
600
+ cost: float = 0.0,
601
+ confidence: float = 0.0,
602
+ context: dict[str, Any] | None = None,
603
+ ) -> None:
604
+ """Record a pattern execution.
605
+
606
+ Args:
607
+ pattern: Pattern/strategy name
608
+ success: Whether execution succeeded
609
+ duration: Execution duration in seconds
610
+ cost: Estimated cost
611
+ confidence: Aggregate confidence score
612
+ context: Execution context (for similarity matching)
613
+ """
614
+ record = ExecutionRecord(
615
+ pattern=pattern,
616
+ success=success,
617
+ duration_seconds=duration,
618
+ cost=cost,
619
+ confidence=confidence,
620
+ context_features=context or {},
621
+ )
622
+ self.store.add_record(record)
623
+ logger.debug(f"Recorded {pattern} execution: success={success}")
624
+
625
+ def recommend(self, context: dict[str, Any], top_k: int = 3) -> list[PatternRecommendation]:
626
+ """Get pattern recommendations for a context.
627
+
628
+ Args:
629
+ context: Execution context
630
+ top_k: Number of recommendations
631
+
632
+ Returns:
633
+ List of PatternRecommendation
634
+ """
635
+ return self.recommender.recommend(context, top_k)
636
+
637
+ def get_stats(self, pattern: str) -> PatternStats | None:
638
+ """Get statistics for a specific pattern.
639
+
640
+ Args:
641
+ pattern: Pattern name
642
+
643
+ Returns:
644
+ PatternStats or None
645
+ """
646
+ return self.store.get_stats(pattern)
647
+
648
+ def get_all_stats(self) -> list[PatternStats]:
649
+ """Get statistics for all patterns.
650
+
651
+ Returns:
652
+ List of PatternStats sorted by success rate
653
+ """
654
+ return self.store.get_all_stats()
655
+
656
+ def save(self) -> None:
657
+ """Force save to disk."""
658
+ self.store.save()
659
+
660
+ def report(self) -> str:
661
+ """Generate a human-readable report of learning data.
662
+
663
+ Returns:
664
+ Formatted report string
665
+ """
666
+ stats = self.get_all_stats()
667
+ if not stats:
668
+ return "No learning data recorded yet."
669
+
670
+ lines = ["Pattern Learning Report", "=" * 50, ""]
671
+
672
+ for s in stats:
673
+ lines.append(f"Pattern: {s.pattern}")
674
+ lines.append(f" Executions: {s.total_executions}")
675
+ lines.append(f" Success Rate: {s.success_rate:.1%}")
676
+ lines.append(f" Avg Duration: {s.avg_duration:.2f}s")
677
+ lines.append(f" Avg Cost: ${s.avg_cost:.4f}")
678
+ lines.append("")
679
+
680
+ return "\n".join(lines)
681
+
682
+
683
+ # Module-level singleton for convenience
684
+ _default_learner: PatternLearner | None = None
685
+
686
+
687
+ def get_learner() -> PatternLearner:
688
+ """Get the default pattern learner instance.
689
+
690
+ Returns:
691
+ PatternLearner singleton
692
+ """
693
+ global _default_learner
694
+ if _default_learner is None:
695
+ _default_learner = PatternLearner()
696
+ return _default_learner