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,655 @@
1
+ """Test Generation Workflow.
2
+
3
+ Main workflow orchestration for test generation.
4
+
5
+ Copyright 2025 Smart-AI-Memory
6
+ Licensed under Fair Source License 0.9
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ..base import BaseWorkflow, ModelTier
14
+ from .ast_analyzer import ASTFunctionAnalyzer
15
+ from .config import DEFAULT_SKIP_PATTERNS
16
+ from .test_templates import (
17
+ generate_test_for_class,
18
+ generate_test_for_function,
19
+ )
20
+
21
+
22
+ class TestGenerationWorkflow(BaseWorkflow):
23
+ """Generate tests targeting areas with historical bugs.
24
+
25
+ Prioritizes test generation for files that have historically
26
+ been bug-prone and have low test coverage.
27
+ """
28
+
29
+ name = "test-gen"
30
+ description = "Generate tests targeting areas with historical bugs"
31
+ stages = ["identify", "analyze", "generate", "review"]
32
+ tier_map = {
33
+ "identify": ModelTier.CHEAP,
34
+ "analyze": ModelTier.CAPABLE,
35
+ "generate": ModelTier.CAPABLE,
36
+ "review": ModelTier.PREMIUM,
37
+ }
38
+
39
+ def __init__(
40
+ self,
41
+ patterns_dir: str = "./patterns",
42
+ min_tests_for_review: int = 10,
43
+ write_tests: bool = False,
44
+ output_dir: str = "tests/generated",
45
+ enable_auth_strategy: bool = True,
46
+ **kwargs: Any,
47
+ ):
48
+ """Initialize test generation workflow.
49
+
50
+ Args:
51
+ patterns_dir: Directory containing learned patterns
52
+ min_tests_for_review: Minimum tests generated to trigger premium review
53
+ write_tests: If True, write generated tests to output_dir
54
+ output_dir: Directory to write generated test files
55
+ enable_auth_strategy: Enable intelligent auth routing (default: True)
56
+ **kwargs: Additional arguments passed to BaseWorkflow
57
+
58
+ """
59
+ super().__init__(**kwargs)
60
+ self.patterns_dir = patterns_dir
61
+ self.min_tests_for_review = min_tests_for_review
62
+ self.write_tests = write_tests
63
+ self.output_dir = output_dir
64
+ self.enable_auth_strategy = enable_auth_strategy
65
+ self._test_count: int = 0
66
+ self._bug_hotspots: list[str] = []
67
+ self._auth_mode_used: str | None = None
68
+ self._load_bug_hotspots()
69
+
70
+ def _load_bug_hotspots(self) -> None:
71
+ """Load files with historical bugs from pattern library."""
72
+ debugging_file = Path(self.patterns_dir) / "debugging.json"
73
+ if debugging_file.exists():
74
+ try:
75
+ with open(debugging_file) as fh:
76
+ data = json.load(fh)
77
+ patterns = data.get("patterns", [])
78
+ # Extract files from bug patterns
79
+ files = set()
80
+ for p in patterns:
81
+ for file_entry in p.get("files_affected", []):
82
+ if file_entry is None:
83
+ continue
84
+ files.add(str(file_entry))
85
+ self._bug_hotspots = list(files)
86
+ except (json.JSONDecodeError, OSError):
87
+ pass
88
+
89
+ def should_skip_stage(self, stage_name: str, input_data: Any) -> tuple[bool, str | None]:
90
+ """Downgrade review stage if few tests generated.
91
+
92
+ Args:
93
+ stage_name: Name of the stage to check
94
+ input_data: Current workflow data
95
+
96
+ Returns:
97
+ Tuple of (should_skip, reason)
98
+
99
+ """
100
+ if stage_name == "review":
101
+ if self._test_count < self.min_tests_for_review:
102
+ # Downgrade to CAPABLE
103
+ self.tier_map["review"] = ModelTier.CAPABLE
104
+ return False, None
105
+ return False, None
106
+
107
+ async def run_stage(
108
+ self,
109
+ stage_name: str,
110
+ tier: ModelTier,
111
+ input_data: Any,
112
+ ) -> tuple[Any, int, int]:
113
+ """Route to specific stage implementation."""
114
+ if stage_name == "identify":
115
+ return await self._identify(input_data, tier)
116
+ if stage_name == "analyze":
117
+ return await self._analyze(input_data, tier)
118
+ if stage_name == "generate":
119
+ return await self._generate(input_data, tier)
120
+ if stage_name == "review":
121
+ return await self._review(input_data, tier)
122
+ raise ValueError(f"Unknown stage: {stage_name}")
123
+
124
+ async def _identify(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
125
+ """Identify files needing tests.
126
+
127
+ Finds files with low coverage, historical bugs, or
128
+ no existing tests.
129
+
130
+ Configurable options via input_data:
131
+ max_files_to_scan: Maximum files to scan before stopping (default: 1000)
132
+ max_file_size_kb: Skip files larger than this (default: 200)
133
+ max_candidates: Maximum candidates to return (default: 50)
134
+ skip_patterns: List of directory patterns to skip (default: DEFAULT_SKIP_PATTERNS)
135
+ include_all_files: Include files with priority=0 (default: False)
136
+ """
137
+ target_path = input_data.get("path", ".")
138
+ file_types = input_data.get("file_types", [".py"])
139
+
140
+ # === AUTH STRATEGY INTEGRATION ===
141
+ if self.enable_auth_strategy:
142
+ try:
143
+ import logging
144
+ from pathlib import Path
145
+
146
+ from attune.models import (
147
+ count_lines_of_code,
148
+ get_auth_strategy,
149
+ get_module_size_category,
150
+ )
151
+
152
+ logger = logging.getLogger(__name__)
153
+
154
+ # Calculate total LOC for the project/path
155
+ target = Path(target_path)
156
+ total_lines = 0
157
+ if target.is_file():
158
+ total_lines = count_lines_of_code(target)
159
+ elif target.is_dir():
160
+ # Estimate total lines for directory
161
+ for py_file in target.rglob("*.py"):
162
+ try:
163
+ total_lines += count_lines_of_code(py_file)
164
+ except Exception:
165
+ pass
166
+
167
+ if total_lines > 0:
168
+ strategy = get_auth_strategy()
169
+ recommended_mode = strategy.get_recommended_mode(total_lines)
170
+ self._auth_mode_used = recommended_mode.value
171
+
172
+ size_category = get_module_size_category(total_lines)
173
+ logger.info(
174
+ f"Test generation target: {target_path} "
175
+ f"({total_lines:,} LOC, {size_category})"
176
+ )
177
+ logger.info(f"Recommended auth mode: {recommended_mode.value}")
178
+
179
+ cost_estimate = strategy.estimate_cost(total_lines, recommended_mode)
180
+ if recommended_mode.value == "subscription":
181
+ logger.info(f"Cost: {cost_estimate['quota_cost']}")
182
+ else:
183
+ logger.info(f"Cost: ~${cost_estimate['monetary_cost']:.4f}")
184
+
185
+ except Exception as e:
186
+ import logging
187
+
188
+ logger = logging.getLogger(__name__)
189
+ logger.warning(f"Auth strategy detection failed: {e}")
190
+
191
+ # Parse configurable limits with sensible defaults
192
+ max_files_to_scan = input_data.get("max_files_to_scan", 1000)
193
+ max_file_size_kb = input_data.get("max_file_size_kb", 200)
194
+ max_candidates = input_data.get("max_candidates", 50)
195
+ skip_patterns = input_data.get("skip_patterns", DEFAULT_SKIP_PATTERNS)
196
+ include_all_files = input_data.get("include_all_files", False)
197
+
198
+ target = Path(target_path)
199
+ candidates: list[dict] = []
200
+
201
+ # Track project scope for enterprise reporting
202
+ total_source_files = 0
203
+ existing_test_files = 0
204
+
205
+ # Track scan summary for debugging/visibility
206
+ # Use separate counters for type safety
207
+ scan_counts = {
208
+ "files_scanned": 0,
209
+ "files_too_large": 0,
210
+ "files_read_error": 0,
211
+ "files_excluded_by_pattern": 0,
212
+ }
213
+ early_exit_reason: str | None = None
214
+
215
+ max_file_size_bytes = max_file_size_kb * 1024
216
+ scan_limit_reached = False
217
+
218
+ if target.exists():
219
+ for ext in file_types:
220
+ if scan_limit_reached:
221
+ break
222
+
223
+ for file_path in target.rglob(f"*{ext}"):
224
+ # Check if we've hit the scan limit
225
+ if scan_counts["files_scanned"] >= max_files_to_scan:
226
+ early_exit_reason = f"max_files_to_scan ({max_files_to_scan}) reached"
227
+ scan_limit_reached = True
228
+ break
229
+
230
+ # Skip non-code directories using configurable patterns
231
+ file_str = str(file_path)
232
+ if any(skip in file_str for skip in skip_patterns):
233
+ scan_counts["files_excluded_by_pattern"] += 1
234
+ continue
235
+
236
+ # Count test files separately for scope awareness
237
+ if "test_" in file_str or "_test." in file_str or "/tests/" in file_str:
238
+ existing_test_files += 1
239
+ continue
240
+
241
+ # Check file size before reading
242
+ try:
243
+ file_size = file_path.stat().st_size
244
+ if file_size > max_file_size_bytes:
245
+ scan_counts["files_too_large"] += 1
246
+ continue
247
+ except OSError:
248
+ scan_counts["files_read_error"] += 1
249
+ continue
250
+
251
+ # Count source files and increment scan counter
252
+ total_source_files += 1
253
+ scan_counts["files_scanned"] += 1
254
+
255
+ try:
256
+ content = file_path.read_text(errors="ignore")
257
+ lines = len(content.splitlines())
258
+
259
+ # Check if in bug hotspots
260
+ is_hotspot = any(hotspot in file_str for hotspot in self._bug_hotspots)
261
+
262
+ # Check for existing tests
263
+ test_file = self._find_test_file(file_path)
264
+ has_tests = test_file.exists() if test_file else False
265
+
266
+ # Calculate priority
267
+ priority = 0
268
+ if is_hotspot:
269
+ priority += 50
270
+ if not has_tests:
271
+ priority += 30
272
+ if lines > 100:
273
+ priority += 10
274
+ if lines > 300:
275
+ priority += 10
276
+
277
+ # Include if priority > 0 OR include_all_files is set
278
+ if priority > 0 or include_all_files:
279
+ candidates.append(
280
+ {
281
+ "file": file_str,
282
+ "lines": lines,
283
+ "is_hotspot": is_hotspot,
284
+ "has_tests": has_tests,
285
+ "priority": priority,
286
+ },
287
+ )
288
+ except OSError:
289
+ scan_counts["files_read_error"] += 1
290
+ continue
291
+
292
+ # Sort by priority
293
+ candidates.sort(key=lambda x: -x["priority"])
294
+
295
+ input_tokens = len(str(input_data)) // 4
296
+ output_tokens = len(str(candidates)) // 4
297
+
298
+ # Calculate scope metrics for enterprise reporting
299
+ analyzed_count = min(max_candidates, len(candidates))
300
+ coverage_pct = (analyzed_count / len(candidates) * 100) if candidates else 100
301
+
302
+ return (
303
+ {
304
+ "candidates": candidates[:max_candidates],
305
+ "total_candidates": len(candidates),
306
+ "hotspot_count": sum(1 for c in candidates if c["is_hotspot"]),
307
+ "untested_count": sum(1 for c in candidates if not c["has_tests"]),
308
+ # Scope awareness fields for enterprise reporting
309
+ "total_source_files": total_source_files,
310
+ "existing_test_files": existing_test_files,
311
+ "large_project_warning": len(candidates) > 100,
312
+ "analysis_coverage_percent": coverage_pct,
313
+ # Scan summary for debugging/visibility
314
+ "scan_summary": {**scan_counts, "early_exit_reason": early_exit_reason},
315
+ # Pass through config for subsequent stages
316
+ "config": {
317
+ "max_files_to_analyze": input_data.get("max_files_to_analyze", 20),
318
+ "max_functions_per_file": input_data.get("max_functions_per_file", 30),
319
+ "max_classes_per_file": input_data.get("max_classes_per_file", 15),
320
+ "max_files_to_generate": input_data.get("max_files_to_generate", 15),
321
+ "max_functions_to_generate": input_data.get("max_functions_to_generate", 8),
322
+ "max_classes_to_generate": input_data.get("max_classes_to_generate", 4),
323
+ },
324
+ **input_data,
325
+ },
326
+ input_tokens,
327
+ output_tokens,
328
+ )
329
+
330
+ def _find_test_file(self, source_file: Path) -> Path | None:
331
+ """Find corresponding test file for a source file."""
332
+ name = source_file.stem
333
+ parent = source_file.parent
334
+
335
+ # Check common test locations
336
+ possible = [
337
+ parent / f"test_{name}.py",
338
+ parent / "tests" / f"test_{name}.py",
339
+ parent.parent / "tests" / f"test_{name}.py",
340
+ ]
341
+
342
+ for p in possible:
343
+ if p.exists():
344
+ return p
345
+
346
+ return possible[0] # Return expected location even if doesn't exist
347
+
348
+ async def _analyze(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
349
+ """Analyze code structure for test generation.
350
+
351
+ Examines functions, classes, and patterns to determine
352
+ what tests should be generated.
353
+
354
+ Uses config from _identify stage for limits:
355
+ max_files_to_analyze: Maximum files to analyze (default: 20)
356
+ max_functions_per_file: Maximum functions per file (default: 30)
357
+ max_classes_per_file: Maximum classes per file (default: 15)
358
+ """
359
+ # Get config from previous stage or use defaults
360
+ config = input_data.get("config", {})
361
+ max_files_to_analyze = config.get("max_files_to_analyze", 20)
362
+ max_functions_per_file = config.get("max_functions_per_file", 30)
363
+ max_classes_per_file = config.get("max_classes_per_file", 15)
364
+
365
+ candidates = input_data.get("candidates", [])[:max_files_to_analyze]
366
+ analysis: list[dict] = []
367
+ parse_errors: list[str] = [] # Track files that failed to parse
368
+
369
+ for candidate in candidates:
370
+ file_path = Path(candidate["file"])
371
+ if not file_path.exists():
372
+ continue
373
+
374
+ try:
375
+ content = file_path.read_text(errors="ignore")
376
+
377
+ # Extract testable items with configurable limits and error tracking
378
+ functions, func_error = self._extract_functions(
379
+ content,
380
+ candidate["file"],
381
+ max_functions_per_file,
382
+ )
383
+ classes, class_error = self._extract_classes(
384
+ content,
385
+ candidate["file"],
386
+ max_classes_per_file,
387
+ )
388
+
389
+ # Track parse errors for visibility
390
+ if func_error:
391
+ parse_errors.append(func_error)
392
+ if class_error and class_error != func_error:
393
+ parse_errors.append(class_error)
394
+
395
+ analysis.append(
396
+ {
397
+ "file": candidate["file"],
398
+ "priority": candidate["priority"],
399
+ "functions": functions,
400
+ "classes": classes,
401
+ "function_count": len(functions),
402
+ "class_count": len(classes),
403
+ "test_suggestions": self._generate_suggestions(functions, classes),
404
+ },
405
+ )
406
+ except OSError:
407
+ continue
408
+
409
+ input_tokens = len(str(input_data)) // 4
410
+ output_tokens = len(str(analysis)) // 4
411
+
412
+ return (
413
+ {
414
+ "analysis": analysis,
415
+ "total_functions": sum(a["function_count"] for a in analysis),
416
+ "total_classes": sum(a["class_count"] for a in analysis),
417
+ "parse_errors": parse_errors, # Expose errors for debugging
418
+ **input_data,
419
+ },
420
+ input_tokens,
421
+ output_tokens,
422
+ )
423
+
424
+ def _extract_functions(
425
+ self,
426
+ content: str,
427
+ file_path: str = "",
428
+ max_functions: int = 30,
429
+ ) -> tuple[list[dict], str | None]:
430
+ """Extract function definitions from Python code using AST analysis.
431
+
432
+ Args:
433
+ content: Python source code
434
+ file_path: File path for error reporting
435
+ max_functions: Maximum functions to extract (configurable)
436
+
437
+ Returns:
438
+ Tuple of (functions list, error message or None)
439
+
440
+ """
441
+ analyzer = ASTFunctionAnalyzer()
442
+ functions, _ = analyzer.analyze(content, file_path)
443
+
444
+ result = []
445
+ for sig in functions[:max_functions]:
446
+ if not sig.name.startswith("_") or sig.name.startswith("__"):
447
+ result.append(
448
+ {
449
+ "name": sig.name,
450
+ "params": [(p[0], p[1], p[2]) for p in sig.params],
451
+ "param_names": [p[0] for p in sig.params],
452
+ "is_async": sig.is_async,
453
+ "return_type": sig.return_type,
454
+ "raises": list(sig.raises),
455
+ "has_side_effects": sig.has_side_effects,
456
+ "complexity": sig.complexity,
457
+ "docstring": sig.docstring,
458
+ },
459
+ )
460
+ return result, analyzer.last_error
461
+
462
+ def _extract_classes(
463
+ self,
464
+ content: str,
465
+ file_path: str = "",
466
+ max_classes: int = 15,
467
+ ) -> tuple[list[dict], str | None]:
468
+ """Extract class definitions from Python code using AST analysis.
469
+
470
+ Args:
471
+ content: Python source code
472
+ file_path: File path for error reporting
473
+ max_classes: Maximum classes to extract (configurable)
474
+
475
+ Returns:
476
+ Tuple of (classes list, error message or None)
477
+
478
+ """
479
+ analyzer = ASTFunctionAnalyzer()
480
+ _, classes = analyzer.analyze(content, file_path)
481
+
482
+ result = []
483
+ for sig in classes[:max_classes]:
484
+ # Skip enums - they don't need traditional class tests
485
+ if sig.is_enum:
486
+ continue
487
+
488
+ methods = [
489
+ {
490
+ "name": m.name,
491
+ "params": [(p[0], p[1], p[2]) for p in m.params],
492
+ "is_async": m.is_async,
493
+ "raises": list(m.raises),
494
+ }
495
+ for m in sig.methods
496
+ if not m.name.startswith("_") or m.name == "__init__"
497
+ ]
498
+ result.append(
499
+ {
500
+ "name": sig.name,
501
+ "init_params": [(p[0], p[1], p[2]) for p in sig.init_params],
502
+ "methods": methods,
503
+ "base_classes": sig.base_classes,
504
+ "docstring": sig.docstring,
505
+ "is_dataclass": sig.is_dataclass,
506
+ "required_init_params": sig.required_init_params,
507
+ },
508
+ )
509
+ return result, analyzer.last_error
510
+
511
+ def _generate_suggestions(self, functions: list[dict], classes: list[dict]) -> list[str]:
512
+ """Generate test suggestions based on code structure."""
513
+ suggestions = []
514
+
515
+ for func in functions[:5]:
516
+ if func["params"]:
517
+ suggestions.append(f"Test {func['name']} with valid inputs")
518
+ suggestions.append(f"Test {func['name']} with edge cases")
519
+ if func["is_async"]:
520
+ suggestions.append(f"Test {func['name']} async behavior")
521
+
522
+ for cls in classes[:3]:
523
+ suggestions.append(f"Test {cls['name']} initialization")
524
+ suggestions.append(f"Test {cls['name']} methods")
525
+
526
+ return suggestions
527
+
528
+ async def _generate(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
529
+ """Generate test cases.
530
+
531
+ Creates test code targeting identified functions
532
+ and classes, focusing on edge cases.
533
+
534
+ Uses config from _identify stage for limits:
535
+ max_files_to_generate: Maximum files to generate tests for (default: 15)
536
+ max_functions_to_generate: Maximum functions per file (default: 8)
537
+ max_classes_to_generate: Maximum classes per file (default: 4)
538
+ """
539
+ # Get config from previous stages or use defaults
540
+ config = input_data.get("config", {})
541
+ max_files_to_generate = config.get("max_files_to_generate", 15)
542
+ max_functions_to_generate = config.get("max_functions_to_generate", 8)
543
+ max_classes_to_generate = config.get("max_classes_to_generate", 4)
544
+
545
+ analysis = input_data.get("analysis", [])
546
+ generated_tests: list[dict] = []
547
+
548
+ for item in analysis[:max_files_to_generate]:
549
+ file_path = item["file"]
550
+ module_name = Path(file_path).stem
551
+
552
+ tests = []
553
+ for func in item.get("functions", [])[:max_functions_to_generate]:
554
+ test_code = generate_test_for_function(module_name, func)
555
+ tests.append(
556
+ {
557
+ "target": func["name"],
558
+ "type": "function",
559
+ "code": test_code,
560
+ },
561
+ )
562
+
563
+ for cls in item.get("classes", [])[:max_classes_to_generate]:
564
+ test_code = generate_test_for_class(module_name, cls)
565
+ tests.append(
566
+ {
567
+ "target": cls["name"],
568
+ "type": "class",
569
+ "code": test_code,
570
+ },
571
+ )
572
+
573
+ if tests:
574
+ generated_tests.append(
575
+ {
576
+ "source_file": file_path,
577
+ "test_file": f"test_{module_name}.py",
578
+ "tests": tests,
579
+ "test_count": len(tests),
580
+ },
581
+ )
582
+
583
+ self._test_count = sum(t["test_count"] for t in generated_tests)
584
+
585
+ # Write tests to files if enabled (via input_data or instance config)
586
+ write_tests = input_data.get("write_tests", self.write_tests)
587
+ output_dir = input_data.get("output_dir", self.output_dir)
588
+ written_files: list[str] = []
589
+
590
+ if write_tests and generated_tests:
591
+ output_path = Path(output_dir)
592
+ output_path.mkdir(parents=True, exist_ok=True)
593
+
594
+ for test_item in generated_tests:
595
+ test_filename = test_item["test_file"]
596
+ test_file_path = output_path / test_filename
597
+
598
+ # Combine all test code for this file
599
+ combined_code = []
600
+ imports_added = set()
601
+
602
+ for test in test_item["tests"]:
603
+ code = test["code"]
604
+ # Extract and dedupe imports
605
+ for line in code.split("\n"):
606
+ if line.startswith("import ") or line.startswith("from "):
607
+ if line not in imports_added:
608
+ imports_added.add(line)
609
+ elif line.strip():
610
+ combined_code.append(line)
611
+
612
+ # Write the combined test file
613
+ final_code = "\n".join(sorted(imports_added)) + "\n\n" + "\n".join(combined_code)
614
+ test_file_path.write_text(final_code)
615
+ written_files.append(str(test_file_path))
616
+ test_item["written_to"] = str(test_file_path)
617
+
618
+ input_tokens = len(str(input_data)) // 4
619
+ output_tokens = sum(len(str(t)) for t in generated_tests) // 4
620
+
621
+ return (
622
+ {
623
+ "generated_tests": generated_tests,
624
+ "total_tests_generated": self._test_count,
625
+ "written_files": written_files,
626
+ "tests_written": len(written_files) > 0,
627
+ **input_data,
628
+ },
629
+ input_tokens,
630
+ output_tokens,
631
+ )
632
+
633
+
634
+
635
+
636
+ def main():
637
+ """CLI entry point for test generation workflow."""
638
+ import asyncio
639
+
640
+ async def run():
641
+ workflow = TestGenerationWorkflow()
642
+ result = await workflow.execute(path=".", file_types=[".py"])
643
+
644
+ print("\nTest Generation Results")
645
+ print("=" * 50)
646
+ print(f"Provider: {result.provider}")
647
+ print(f"Success: {result.success}")
648
+ print(f"Tests Generated: {result.final_output.get('total_tests', 0)}")
649
+ print("\nCost Report:")
650
+ print(f" Total Cost: ${result.cost_report.total_cost:.4f}")
651
+ savings = result.cost_report.savings
652
+ pct = result.cost_report.savings_percent
653
+ print(f" Savings: ${savings:.4f} ({pct:.1f}%)")
654
+
655
+ asyncio.run(run())