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.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- 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
|