praisonai-code 0.0.1__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 (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,736 @@
1
+ """
2
+ CSV-driven Interactive Test Runner for PraisonAI.
3
+
4
+ Provides a CSV-driven test runner that:
5
+ - Loads test cases from CSV with defined schema
6
+ - Runs each test in isolated temp workspace
7
+ - Executes prompts via headless interactive core
8
+ - Validates tool calls, files, and responses
9
+ - Integrates LLM-as-judge for response quality
10
+ - Emits artifacts per-test
11
+ """
12
+
13
+ import csv
14
+ import json
15
+ import logging
16
+ import os
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # CSV Schema definition
26
+ CSV_SCHEMA = {
27
+ # Required columns
28
+ "id": {"required": True, "type": str, "description": "Unique test identifier"},
29
+ "name": {"required": True, "type": str, "description": "Test name"},
30
+ "prompts": {"required": True, "type": str, "description": "Single prompt or JSON array"},
31
+ # Optional columns
32
+ "description": {"required": False, "type": str, "default": ""},
33
+ "mode": {"required": False, "type": str, "default": "headless"},
34
+ "workspace_fixture": {"required": False, "type": str, "default": "empty"},
35
+ "expected_tools": {"required": False, "type": str, "default": ""},
36
+ "forbidden_tools": {"required": False, "type": str, "default": ""},
37
+ "expected_files": {"required": False, "type": str, "default": "{}"},
38
+ "expected_response": {"required": False, "type": str, "default": ""},
39
+ "judge_rubric": {"required": False, "type": str, "default": ""},
40
+ "judge_threshold": {"required": False, "type": float, "default": 7.0},
41
+ "judge_model": {"required": False, "type": str, "default": "gpt-4o-mini"},
42
+ "timeout": {"required": False, "type": int, "default": 60},
43
+ "retries": {"required": False, "type": int, "default": 0},
44
+ "skip_if": {"required": False, "type": str, "default": ""},
45
+ "agents": {"required": False, "type": str, "default": "[]"},
46
+ "workflow": {"required": False, "type": str, "default": "{}"},
47
+ }
48
+
49
+
50
+ @dataclass
51
+ class TestCase:
52
+ """A single test case from CSV."""
53
+ id: str
54
+ name: str
55
+ prompts: List[str]
56
+ description: str = ""
57
+ mode: str = "headless"
58
+ workspace_fixture: str = "empty"
59
+ expected_tools: List[str] = field(default_factory=list)
60
+ forbidden_tools: List[str] = field(default_factory=list)
61
+ expected_files: Dict[str, str] = field(default_factory=dict)
62
+ expected_response: str = ""
63
+ judge_rubric: str = ""
64
+ judge_threshold: float = 7.0
65
+ judge_model: str = "gpt-4o-mini"
66
+ timeout: int = 60
67
+ retries: int = 0
68
+ skip_if: str = ""
69
+ agents: List[Dict[str, Any]] = field(default_factory=list)
70
+ workflow: Dict[str, Any] = field(default_factory=dict)
71
+
72
+ def should_skip(self) -> Optional[str]:
73
+ """Check if test should be skipped."""
74
+ if not self.skip_if:
75
+ return None
76
+
77
+ conditions = [c.strip() for c in self.skip_if.split(",")]
78
+
79
+ for condition in conditions:
80
+ if condition == "no_openai_key":
81
+ if not os.environ.get("OPENAI_API_KEY"):
82
+ return "OPENAI_API_KEY not set"
83
+ elif condition == "no_anthropic_key":
84
+ if not os.environ.get("ANTHROPIC_API_KEY"):
85
+ return "ANTHROPIC_API_KEY not set"
86
+ elif condition == "no_lsp":
87
+ # Could check if LSP is available
88
+ pass
89
+ elif condition == "no_network":
90
+ if os.environ.get("PRAISONAI_NO_NETWORK"):
91
+ return "Network disabled"
92
+
93
+ return None
94
+
95
+
96
+ @dataclass
97
+ class TestResult:
98
+ """Result of a single test."""
99
+ test_id: str
100
+ test_name: str
101
+ status: str # passed, failed, skipped, error
102
+ duration: float
103
+ tool_calls: List[str] = field(default_factory=list)
104
+ response: str = ""
105
+ judge_score: Optional[float] = None
106
+ judge_passed: Optional[bool] = None
107
+ judge_reasoning: str = ""
108
+ artifacts_path: Optional[str] = None
109
+ error: Optional[str] = None
110
+ skip_reason: Optional[str] = None
111
+ assertions: Dict[str, bool] = field(default_factory=dict)
112
+
113
+ def to_dict(self) -> Dict[str, Any]:
114
+ return {
115
+ "test_id": self.test_id,
116
+ "test_name": self.test_name,
117
+ "status": self.status,
118
+ "duration": self.duration,
119
+ "tool_calls": self.tool_calls,
120
+ "response": self.response[:500] if self.response else "",
121
+ "judge_score": self.judge_score,
122
+ "judge_passed": self.judge_passed,
123
+ "judge_reasoning": self.judge_reasoning,
124
+ "artifacts_path": self.artifacts_path,
125
+ "error": self.error,
126
+ "skip_reason": self.skip_reason,
127
+ "assertions": self.assertions,
128
+ }
129
+
130
+
131
+ @dataclass
132
+ class TestSummary:
133
+ """Summary of all test results."""
134
+ total: int = 0
135
+ passed: int = 0
136
+ failed: int = 0
137
+ skipped: int = 0
138
+ errors: int = 0
139
+ duration: float = 0.0
140
+ judge_avg: Optional[float] = None
141
+ results: List[TestResult] = field(default_factory=list)
142
+
143
+ def to_dict(self) -> Dict[str, Any]:
144
+ return {
145
+ "total": self.total,
146
+ "passed": self.passed,
147
+ "failed": self.failed,
148
+ "skipped": self.skipped,
149
+ "errors": self.errors,
150
+ "duration": self.duration,
151
+ "judge_avg": self.judge_avg,
152
+ "pass_rate": self.passed / self.total if self.total > 0 else 0,
153
+ "results": [r.to_dict() for r in self.results],
154
+ }
155
+
156
+ def print_summary(self) -> None:
157
+ """Print summary to console."""
158
+ try:
159
+ from rich.console import Console
160
+ from rich.table import Table
161
+
162
+ console = Console()
163
+
164
+ table = Table(title="Interactive Test Results")
165
+ table.add_column("ID", style="cyan")
166
+ table.add_column("Name", style="white")
167
+ table.add_column("Status")
168
+ table.add_column("Duration", justify="right")
169
+ table.add_column("Judge", justify="right")
170
+
171
+ for r in self.results:
172
+ status_style = {
173
+ "passed": "[green]✓ passed[/green]",
174
+ "failed": "[red]✗ failed[/red]",
175
+ "skipped": "[yellow]○ skipped[/yellow]",
176
+ "error": "[red]! error[/red]",
177
+ }.get(r.status, r.status)
178
+
179
+ judge_str = f"{r.judge_score:.1f}" if r.judge_score is not None else "-"
180
+
181
+ table.add_row(
182
+ r.test_id,
183
+ r.test_name[:30],
184
+ status_style,
185
+ f"{r.duration:.2f}s",
186
+ judge_str,
187
+ )
188
+
189
+ console.print(table)
190
+ console.print()
191
+ console.print(f"[bold]Total:[/bold] {self.total} | "
192
+ f"[green]Passed:[/green] {self.passed} | "
193
+ f"[red]Failed:[/red] {self.failed} | "
194
+ f"[yellow]Skipped:[/yellow] {self.skipped}")
195
+ if self.judge_avg is not None:
196
+ console.print(f"[bold]Average Judge Score:[/bold] {self.judge_avg:.2f}")
197
+ console.print(f"[bold]Total Duration:[/bold] {self.duration:.2f}s")
198
+
199
+ except ImportError:
200
+ # Fallback to plain print
201
+ print("\n=== Interactive Test Results ===")
202
+ for r in self.results:
203
+ status = {"passed": "✓", "failed": "✗", "skipped": "○", "error": "!"}.get(r.status, "?")
204
+ print(f" {status} {r.test_id}: {r.test_name} ({r.duration:.2f}s)")
205
+ print(f"\nTotal: {self.total} | Passed: {self.passed} | Failed: {self.failed} | Skipped: {self.skipped}")
206
+
207
+
208
+ def parse_csv(csv_path: Path) -> List[TestCase]:
209
+ """
210
+ Parse CSV file into TestCase objects.
211
+
212
+ Args:
213
+ csv_path: Path to CSV file
214
+
215
+ Returns:
216
+ List of TestCase objects
217
+ """
218
+ test_cases = []
219
+
220
+ with open(csv_path, "r", encoding="utf-8") as f:
221
+ reader = csv.DictReader(f)
222
+
223
+ for row_num, row in enumerate(reader, start=2): # Start at 2 (header is 1)
224
+ try:
225
+ # Parse prompts (single string or JSON array)
226
+ prompts_raw = row.get("prompts", "").strip()
227
+ if prompts_raw.startswith("["):
228
+ prompts = json.loads(prompts_raw)
229
+ else:
230
+ prompts = [prompts_raw] if prompts_raw else []
231
+
232
+ # Parse expected_tools
233
+ expected_tools = []
234
+ if row.get("expected_tools"):
235
+ expected_tools = [t.strip() for t in row["expected_tools"].split(",") if t.strip()]
236
+
237
+ # Parse forbidden_tools
238
+ forbidden_tools = []
239
+ if row.get("forbidden_tools"):
240
+ forbidden_tools = [t.strip() for t in row["forbidden_tools"].split(",") if t.strip()]
241
+
242
+ # Parse expected_files JSON
243
+ expected_files = {}
244
+ if row.get("expected_files"):
245
+ try:
246
+ expected_files = json.loads(row["expected_files"])
247
+ except json.JSONDecodeError:
248
+ logger.warning(f"Row {row_num}: Invalid JSON in expected_files")
249
+
250
+ # Parse agents JSON
251
+ agents = []
252
+ if row.get("agents"):
253
+ try:
254
+ agents = json.loads(row["agents"])
255
+ except json.JSONDecodeError:
256
+ logger.warning(f"Row {row_num}: Invalid JSON in agents")
257
+
258
+ # Parse workflow JSON
259
+ workflow = {}
260
+ if row.get("workflow"):
261
+ try:
262
+ workflow = json.loads(row["workflow"])
263
+ except json.JSONDecodeError:
264
+ logger.warning(f"Row {row_num}: Invalid JSON in workflow")
265
+
266
+ # Parse numeric fields
267
+ judge_threshold = float(row.get("judge_threshold", 7.0) or 7.0)
268
+ timeout = int(row.get("timeout", 60) or 60)
269
+ retries = int(row.get("retries", 0) or 0)
270
+
271
+ test_case = TestCase(
272
+ id=row.get("id", f"test_{row_num}"),
273
+ name=row.get("name", f"Test {row_num}"),
274
+ prompts=prompts,
275
+ description=row.get("description", ""),
276
+ mode=row.get("mode", "headless"),
277
+ workspace_fixture=row.get("workspace_fixture", "empty"),
278
+ expected_tools=expected_tools,
279
+ forbidden_tools=forbidden_tools,
280
+ expected_files=expected_files,
281
+ expected_response=row.get("expected_response", ""),
282
+ judge_rubric=row.get("judge_rubric", ""),
283
+ judge_threshold=judge_threshold,
284
+ judge_model=row.get("judge_model", "gpt-4o-mini"),
285
+ timeout=timeout,
286
+ retries=retries,
287
+ skip_if=row.get("skip_if", ""),
288
+ agents=agents,
289
+ workflow=workflow,
290
+ )
291
+
292
+ test_cases.append(test_case)
293
+
294
+ except Exception as e:
295
+ logger.error(f"Error parsing row {row_num}: {e}")
296
+ continue
297
+
298
+ return test_cases
299
+
300
+
301
+ class CSVTestRunner:
302
+ """
303
+ CSV-driven test runner for interactive mode.
304
+
305
+ Usage:
306
+ runner = CSVTestRunner(csv_path="tests.csv")
307
+ summary = runner.run()
308
+ summary.print_summary()
309
+ """
310
+
311
+ def __init__(
312
+ self,
313
+ csv_path: Path,
314
+ model: str = "gpt-4o-mini",
315
+ judge_model: str = "gpt-4o-mini",
316
+ workspace: Optional[Path] = None,
317
+ artifacts_dir: Optional[Path] = None,
318
+ fail_fast: bool = False,
319
+ keep_artifacts: bool = False,
320
+ no_judge: bool = False,
321
+ verbose: bool = False,
322
+ ):
323
+ """
324
+ Initialize CSV test runner.
325
+
326
+ Args:
327
+ csv_path: Path to CSV file with test cases
328
+ model: LLM model for agent
329
+ judge_model: LLM model for judge
330
+ workspace: Base workspace directory
331
+ artifacts_dir: Directory for artifacts
332
+ fail_fast: Stop on first failure
333
+ keep_artifacts: Keep artifacts after run
334
+ no_judge: Skip judge even if rubric present
335
+ verbose: Verbose output
336
+ """
337
+ self.csv_path = Path(csv_path)
338
+ self.model = model
339
+ self.judge_model = judge_model
340
+ self.workspace = workspace
341
+ self.artifacts_dir = artifacts_dir
342
+ self.fail_fast = fail_fast
343
+ self.keep_artifacts = keep_artifacts
344
+ self.no_judge = no_judge
345
+ self.verbose = verbose
346
+ self.results: List[TestResult] = []
347
+
348
+ def run(self) -> TestSummary:
349
+ """
350
+ Run all tests from CSV.
351
+
352
+ Returns:
353
+ TestSummary with all results
354
+ """
355
+ start_time = time.time()
356
+
357
+ # Parse CSV
358
+ test_cases = parse_csv(self.csv_path)
359
+ logger.info(f"Loaded {len(test_cases)} test cases from {self.csv_path}")
360
+
361
+ if self.verbose:
362
+ print(f"Running {len(test_cases)} tests from {self.csv_path}")
363
+
364
+ # Run each test
365
+ for i, test_case in enumerate(test_cases):
366
+ if self.verbose:
367
+ print(f"\n[{i+1}/{len(test_cases)}] Running: {test_case.name}")
368
+
369
+ # Check skip conditions
370
+ skip_reason = test_case.should_skip()
371
+ if skip_reason:
372
+ result = TestResult(
373
+ test_id=test_case.id,
374
+ test_name=test_case.name,
375
+ status="skipped",
376
+ duration=0.0,
377
+ skip_reason=skip_reason,
378
+ )
379
+ self.results.append(result)
380
+ if self.verbose:
381
+ print(f" Skipped: {skip_reason}")
382
+ continue
383
+
384
+ # Run test
385
+ result = self._run_single_test(test_case)
386
+ self.results.append(result)
387
+
388
+ if self.verbose:
389
+ status_icon = {"passed": "✓", "failed": "✗", "error": "!"}.get(result.status, "?")
390
+ print(f" {status_icon} {result.status} ({result.duration:.2f}s)")
391
+ if result.error:
392
+ print(f" Error: {result.error}")
393
+
394
+ # Fail fast
395
+ if self.fail_fast and result.status in ("failed", "error"):
396
+ logger.info(f"Stopping due to fail-fast: {test_case.id}")
397
+ break
398
+
399
+ # Create summary
400
+ summary = self._create_summary(time.time() - start_time)
401
+
402
+ return summary
403
+
404
+ def _run_single_test(self, test_case: TestCase) -> TestResult:
405
+ """Run a single test case."""
406
+ import tempfile
407
+
408
+ start_time = time.time()
409
+
410
+ # Create workspace
411
+ if self.workspace:
412
+ test_workspace = self.workspace / test_case.id
413
+ else:
414
+ test_workspace = Path(tempfile.mkdtemp(prefix=f"praison_test_{test_case.id}_"))
415
+
416
+ # Create artifacts dir
417
+ if self.artifacts_dir:
418
+ test_artifacts = self.artifacts_dir / test_case.id
419
+ else:
420
+ test_artifacts = test_workspace / "artifacts"
421
+
422
+ try:
423
+ # Import harness (lazy)
424
+ from .interactive_test_harness import InteractiveTestHarness
425
+
426
+ # Create harness
427
+ harness = InteractiveTestHarness(
428
+ workspace=test_workspace,
429
+ artifacts_dir=test_artifacts,
430
+ keep_workspace=self.keep_artifacts,
431
+ )
432
+
433
+ # Setup workspace
434
+ harness.setup_workspace(test_case.workspace_fixture)
435
+
436
+ # Run prompts
437
+ exec_result = harness.run(
438
+ prompts=test_case.prompts,
439
+ model=self.model,
440
+ approval_mode=os.environ.get("PRAISONAI_APPROVAL_MODE", "prompt"),
441
+ agents=test_case.agents if test_case.agents else None,
442
+ workflow=test_case.workflow if test_case.workflow else None,
443
+ )
444
+
445
+ duration = time.time() - start_time
446
+
447
+ # Get response
448
+ response = "\n".join(exec_result.responses) if exec_result.responses else ""
449
+
450
+ # Verify assertions
451
+ assertions = {}
452
+
453
+ # Tool call assertions
454
+ tool_result = harness.verify_tool_calls(
455
+ expected_tools=test_case.expected_tools,
456
+ forbidden_tools=test_case.forbidden_tools,
457
+ )
458
+ assertions["tools"] = tool_result["passed"]
459
+
460
+ # File assertions
461
+ if test_case.expected_files:
462
+ file_results = harness.verify_files(test_case.expected_files)
463
+ assertions["files"] = all(file_results.values())
464
+ else:
465
+ assertions["files"] = True
466
+
467
+ # Response assertion
468
+ if test_case.expected_response:
469
+ assertions["response"] = harness.verify_response(response, test_case.expected_response)
470
+ else:
471
+ assertions["response"] = True
472
+
473
+ # Judge evaluation
474
+ judge_score = None
475
+ judge_passed = None
476
+ judge_reasoning = ""
477
+
478
+ if test_case.judge_rubric and not self.no_judge:
479
+ judge_result = self._run_judge(
480
+ response=response,
481
+ rubric=test_case.judge_rubric,
482
+ threshold=test_case.judge_threshold,
483
+ model=test_case.judge_model or self.judge_model,
484
+ )
485
+ judge_score = judge_result.get("score")
486
+ judge_passed = judge_result.get("passed")
487
+ judge_reasoning = judge_result.get("reasoning", "")
488
+ assertions["judge"] = judge_passed if judge_passed is not None else True
489
+ harness.artifacts.judge_result = judge_result
490
+
491
+ # Determine overall status
492
+ all_passed = all(assertions.values()) and exec_result.success
493
+ status = "passed" if all_passed else "failed"
494
+
495
+ # Snapshot workspace if keeping artifacts
496
+ if self.keep_artifacts:
497
+ harness.snapshot_workspace()
498
+
499
+ # Save artifacts
500
+ artifacts_path = str(harness.save_artifacts(test_case.id))
501
+
502
+ return TestResult(
503
+ test_id=test_case.id,
504
+ test_name=test_case.name,
505
+ status=status,
506
+ duration=duration,
507
+ tool_calls=harness._executor.get_tools_called() if harness._executor else [],
508
+ response=response,
509
+ judge_score=judge_score,
510
+ judge_passed=judge_passed,
511
+ judge_reasoning=judge_reasoning,
512
+ artifacts_path=artifacts_path,
513
+ assertions=assertions,
514
+ )
515
+
516
+ except Exception as e:
517
+ logger.error(f"Test {test_case.id} error: {e}", exc_info=True)
518
+ return TestResult(
519
+ test_id=test_case.id,
520
+ test_name=test_case.name,
521
+ status="error",
522
+ duration=time.time() - start_time,
523
+ error=str(e),
524
+ )
525
+ finally:
526
+ if not self.keep_artifacts:
527
+ try:
528
+ import shutil
529
+ if test_workspace.exists() and not self.workspace:
530
+ shutil.rmtree(test_workspace)
531
+ except Exception:
532
+ pass
533
+
534
+ def _run_judge(
535
+ self,
536
+ response: str,
537
+ rubric: str,
538
+ threshold: float,
539
+ model: str,
540
+ ) -> Dict[str, Any]:
541
+ """
542
+ Run LLM judge on response using Agent class.
543
+
544
+ Args:
545
+ response: Response to evaluate
546
+ rubric: Evaluation rubric
547
+ threshold: Pass threshold (1-10)
548
+ model: Judge model
549
+
550
+ Returns:
551
+ Dict with score, passed, reasoning
552
+ """
553
+ try:
554
+ from praisonaiagents import Agent
555
+
556
+ # Create judge agent
557
+ judge_agent = Agent(
558
+ name="Judge",
559
+ role="Evaluator",
560
+ goal="Evaluate response quality based on rubric",
561
+ instructions=f"""You are an expert evaluator. Your task is to evaluate a response based on the given rubric.
562
+
563
+ RUBRIC:
564
+ {rubric}
565
+
566
+ RESPONSE TO EVALUATE:
567
+ {response}
568
+
569
+ Provide your evaluation in the following format:
570
+ SCORE: [1-10]
571
+ REASONING: [Your detailed reasoning]
572
+
573
+ Be strict but fair. A score of 7+ means the response adequately meets the rubric criteria.""",
574
+ llm=model,
575
+ verbose=False,
576
+ )
577
+
578
+ # Get evaluation
579
+ eval_response = judge_agent.chat("Please evaluate the response based on the rubric.")
580
+
581
+ # Parse score from response
582
+ score = self._parse_judge_score(eval_response)
583
+ passed = score >= threshold if score is not None else None
584
+
585
+ return {
586
+ "score": score,
587
+ "passed": passed,
588
+ "reasoning": eval_response,
589
+ "threshold": threshold,
590
+ "model": model,
591
+ }
592
+
593
+ except Exception as e:
594
+ logger.warning(f"Judge evaluation failed: {e}")
595
+ return {
596
+ "score": None,
597
+ "passed": None,
598
+ "reasoning": f"Judge error: {e}",
599
+ "error": str(e),
600
+ }
601
+
602
+ def _parse_judge_score(self, response: str) -> Optional[float]:
603
+ """Parse score from judge response."""
604
+ import re
605
+
606
+ # Try to find "SCORE: X" pattern
607
+ match = re.search(r"SCORE:\s*(\d+(?:\.\d+)?)", response, re.IGNORECASE)
608
+ if match:
609
+ try:
610
+ return float(match.group(1))
611
+ except ValueError:
612
+ pass
613
+
614
+ # Try to find standalone number at start
615
+ match = re.search(r"^(\d+(?:\.\d+)?)", response.strip())
616
+ if match:
617
+ try:
618
+ score = float(match.group(1))
619
+ if 1 <= score <= 10:
620
+ return score
621
+ except ValueError:
622
+ pass
623
+
624
+ return None
625
+
626
+ def _create_summary(self, total_duration: float) -> TestSummary:
627
+ """Create test summary."""
628
+ summary = TestSummary(
629
+ total=len(self.results),
630
+ passed=sum(1 for r in self.results if r.status == "passed"),
631
+ failed=sum(1 for r in self.results if r.status == "failed"),
632
+ skipped=sum(1 for r in self.results if r.status == "skipped"),
633
+ errors=sum(1 for r in self.results if r.status == "error"),
634
+ duration=total_duration,
635
+ results=self.results,
636
+ )
637
+
638
+ # Calculate average judge score
639
+ judge_scores = [r.judge_score for r in self.results if r.judge_score is not None]
640
+ if judge_scores:
641
+ summary.judge_avg = sum(judge_scores) / len(judge_scores)
642
+
643
+ return summary
644
+
645
+
646
+ def generate_csv_template(output_path: Path = None) -> str:
647
+ """
648
+ Generate a CSV template with all columns.
649
+
650
+ Args:
651
+ output_path: Path to write template (optional)
652
+
653
+ Returns:
654
+ CSV template string
655
+ """
656
+ headers = list(CSV_SCHEMA.keys())
657
+
658
+ # Example rows
659
+ examples = [
660
+ {
661
+ "id": "smoke_01",
662
+ "name": "Basic Chat",
663
+ "prompts": "Hello, what is 2+2?",
664
+ "description": "Test basic chat response",
665
+ "mode": "headless",
666
+ "workspace_fixture": "empty",
667
+ "expected_tools": "",
668
+ "forbidden_tools": "",
669
+ "expected_files": "{}",
670
+ "expected_response": "4",
671
+ "judge_rubric": "Response contains the number 4",
672
+ "judge_threshold": "7.0",
673
+ "judge_model": "gpt-4o-mini",
674
+ "timeout": "60",
675
+ "retries": "0",
676
+ "skip_if": "",
677
+ "agents": "[]",
678
+ "workflow": "{}",
679
+ },
680
+ {
681
+ "id": "tools_01",
682
+ "name": "Create File",
683
+ "prompts": "Create a file called hello.py with print('hello')",
684
+ "description": "Test file creation",
685
+ "mode": "headless",
686
+ "workspace_fixture": "empty",
687
+ "expected_tools": "acp_create_file",
688
+ "forbidden_tools": "",
689
+ "expected_files": '{"hello.py": "print.*hello"}',
690
+ "expected_response": "",
691
+ "judge_rubric": "File was created successfully",
692
+ "judge_threshold": "7.0",
693
+ "judge_model": "gpt-4o-mini",
694
+ "timeout": "60",
695
+ "retries": "0",
696
+ "skip_if": "no_openai_key",
697
+ "agents": "[]",
698
+ "workflow": "{}",
699
+ },
700
+ {
701
+ "id": "multi_01",
702
+ "name": "Multi-step Edit",
703
+ "prompts": '["Create test.py with x=1", "Edit test.py to change x=1 to x=2"]',
704
+ "description": "Test multi-step editing",
705
+ "mode": "headless",
706
+ "workspace_fixture": "empty",
707
+ "expected_tools": "acp_create_file,acp_edit_file",
708
+ "forbidden_tools": "",
709
+ "expected_files": '{"test.py": "x.*=.*2"}',
710
+ "expected_response": "",
711
+ "judge_rubric": "",
712
+ "judge_threshold": "7.0",
713
+ "judge_model": "gpt-4o-mini",
714
+ "timeout": "120",
715
+ "retries": "0",
716
+ "skip_if": "no_openai_key",
717
+ "agents": "[]",
718
+ "workflow": "{}",
719
+ },
720
+ ]
721
+
722
+ # Build CSV string
723
+ import io
724
+ output = io.StringIO()
725
+ writer = csv.DictWriter(output, fieldnames=headers)
726
+ writer.writeheader()
727
+ for example in examples:
728
+ writer.writerow(example)
729
+
730
+ csv_content = output.getvalue()
731
+
732
+ if output_path:
733
+ Path(output_path).write_text(csv_content)
734
+ logger.info(f"Generated CSV template at {output_path}")
735
+
736
+ return csv_content