eval-protocol 0.0.3__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 (130) hide show
  1. development/__init__.py +1 -0
  2. development/normalize_sandbox_fusion.py +628 -0
  3. development/utils/__init__.py +1 -0
  4. development/utils/generate_api_key.py +31 -0
  5. development/utils/subprocess_manager.py +481 -0
  6. eval_protocol/__init__.py +86 -0
  7. eval_protocol/__main__.py +10 -0
  8. eval_protocol/_version.py +21 -0
  9. eval_protocol/adapters/__init__.py +1 -0
  10. eval_protocol/adapters/braintrust.py +8 -0
  11. eval_protocol/adapters/trl.py +8 -0
  12. eval_protocol/agent/__init__.py +29 -0
  13. eval_protocol/agent/models.py +69 -0
  14. eval_protocol/agent/orchestrator.py +893 -0
  15. eval_protocol/agent/resource_abc.py +89 -0
  16. eval_protocol/agent/resource_pool.py +184 -0
  17. eval_protocol/agent/resources/__init__.py +44 -0
  18. eval_protocol/agent/resources/bfcl_envs/__init__.py +1 -0
  19. eval_protocol/agent/resources/bfcl_envs/gorilla_file_system.py +342 -0
  20. eval_protocol/agent/resources/bfcl_envs/math_api.py +40 -0
  21. eval_protocol/agent/resources/bfcl_envs/posting_api.py +157 -0
  22. eval_protocol/agent/resources/bfcl_sim_api_resource.py +314 -0
  23. eval_protocol/agent/resources/docker_resource.py +479 -0
  24. eval_protocol/agent/resources/filesystem_resource.py +371 -0
  25. eval_protocol/agent/resources/http_rollout_protocol.py +85 -0
  26. eval_protocol/agent/resources/http_rollout_resource.py +325 -0
  27. eval_protocol/agent/resources/python_state_resource.py +170 -0
  28. eval_protocol/agent/resources/sql_resource.py +271 -0
  29. eval_protocol/agent/task_manager.py +1064 -0
  30. eval_protocol/agent/tool_registry.py +111 -0
  31. eval_protocol/auth.py +156 -0
  32. eval_protocol/cli.py +425 -0
  33. eval_protocol/cli_commands/__init__.py +1 -0
  34. eval_protocol/cli_commands/agent_eval_cmd.py +264 -0
  35. eval_protocol/cli_commands/common.py +242 -0
  36. eval_protocol/cli_commands/deploy.py +486 -0
  37. eval_protocol/cli_commands/deploy_mcp.py +287 -0
  38. eval_protocol/cli_commands/preview.py +186 -0
  39. eval_protocol/cli_commands/run_eval_cmd.py +202 -0
  40. eval_protocol/common_utils.py +36 -0
  41. eval_protocol/config.py +180 -0
  42. eval_protocol/datasets/__init__.py +1 -0
  43. eval_protocol/datasets/loader.py +521 -0
  44. eval_protocol/evaluation.py +1045 -0
  45. eval_protocol/execution/__init__.py +1 -0
  46. eval_protocol/execution/pipeline.py +920 -0
  47. eval_protocol/gcp_tools.py +484 -0
  48. eval_protocol/generation/cache.py +141 -0
  49. eval_protocol/generation/clients/base.py +67 -0
  50. eval_protocol/generation/clients.py +248 -0
  51. eval_protocol/generic_server.py +165 -0
  52. eval_protocol/integrations/__init__.py +12 -0
  53. eval_protocol/integrations/braintrust.py +51 -0
  54. eval_protocol/integrations/deepeval.py +106 -0
  55. eval_protocol/integrations/openeval.py +40 -0
  56. eval_protocol/integrations/trl.py +187 -0
  57. eval_protocol/mcp/__init__.py +48 -0
  58. eval_protocol/mcp/adapter.py +131 -0
  59. eval_protocol/mcp/client/__init__.py +12 -0
  60. eval_protocol/mcp/client/connection.py +499 -0
  61. eval_protocol/mcp/clients.py +195 -0
  62. eval_protocol/mcp/execution/__init__.py +23 -0
  63. eval_protocol/mcp/execution/base_policy.py +227 -0
  64. eval_protocol/mcp/execution/fireworks_policy.py +209 -0
  65. eval_protocol/mcp/execution/manager.py +506 -0
  66. eval_protocol/mcp/execution/policy.py +421 -0
  67. eval_protocol/mcp/grid_renderer.py +54 -0
  68. eval_protocol/mcp/mcpgym.py +637 -0
  69. eval_protocol/mcp/process_manager.py +177 -0
  70. eval_protocol/mcp/session/__init__.py +11 -0
  71. eval_protocol/mcp/session/manager.py +228 -0
  72. eval_protocol/mcp/simple_process_manager.py +291 -0
  73. eval_protocol/mcp/simulation_server.py +458 -0
  74. eval_protocol/mcp/types.py +80 -0
  75. eval_protocol/mcp_agent/__init__.py +1 -0
  76. eval_protocol/mcp_agent/config.py +147 -0
  77. eval_protocol/mcp_agent/intermediary_server.py +542 -0
  78. eval_protocol/mcp_agent/main.py +210 -0
  79. eval_protocol/mcp_agent/orchestration/__init__.py +1 -0
  80. eval_protocol/mcp_agent/orchestration/base_client.py +132 -0
  81. eval_protocol/mcp_agent/orchestration/local_docker_client.py +702 -0
  82. eval_protocol/mcp_agent/orchestration/remote_http_client.py +304 -0
  83. eval_protocol/mcp_agent/orchestration/stdio_mcp_client_helper.py +3 -0
  84. eval_protocol/mcp_agent/session.py +79 -0
  85. eval_protocol/mcp_env.py +304 -0
  86. eval_protocol/models.py +366 -0
  87. eval_protocol/packaging.py +219 -0
  88. eval_protocol/platform_api.py +360 -0
  89. eval_protocol/playback_policy.py +396 -0
  90. eval_protocol/resources.py +128 -0
  91. eval_protocol/reward_function.py +410 -0
  92. eval_protocol/rewards/__init__.py +94 -0
  93. eval_protocol/rewards/accuracy.py +454 -0
  94. eval_protocol/rewards/accuracy_length.py +173 -0
  95. eval_protocol/rewards/apps_coding_reward.py +331 -0
  96. eval_protocol/rewards/apps_execution_utils.py +149 -0
  97. eval_protocol/rewards/apps_testing_util.py +559 -0
  98. eval_protocol/rewards/bfcl_reward.py +313 -0
  99. eval_protocol/rewards/code_execution.py +1620 -0
  100. eval_protocol/rewards/code_execution_utils.py +72 -0
  101. eval_protocol/rewards/cpp_code.py +861 -0
  102. eval_protocol/rewards/deepcoder_reward.py +161 -0
  103. eval_protocol/rewards/format.py +129 -0
  104. eval_protocol/rewards/function_calling.py +541 -0
  105. eval_protocol/rewards/json_schema.py +422 -0
  106. eval_protocol/rewards/language_consistency.py +700 -0
  107. eval_protocol/rewards/lean_prover.py +479 -0
  108. eval_protocol/rewards/length.py +375 -0
  109. eval_protocol/rewards/list_comparison_math_reward.py +221 -0
  110. eval_protocol/rewards/math.py +762 -0
  111. eval_protocol/rewards/multiple_choice_math_reward.py +232 -0
  112. eval_protocol/rewards/reasoning_steps.py +249 -0
  113. eval_protocol/rewards/repetition.py +342 -0
  114. eval_protocol/rewards/tag_count.py +162 -0
  115. eval_protocol/rl_processing.py +82 -0
  116. eval_protocol/server.py +271 -0
  117. eval_protocol/typed_interface.py +260 -0
  118. eval_protocol/utils/__init__.py +8 -0
  119. eval_protocol/utils/batch_evaluation.py +217 -0
  120. eval_protocol/utils/batch_transformation.py +205 -0
  121. eval_protocol/utils/dataset_helpers.py +112 -0
  122. eval_protocol/utils/module_loader.py +56 -0
  123. eval_protocol/utils/packaging_utils.py +108 -0
  124. eval_protocol/utils/static_policy.py +305 -0
  125. eval_protocol-0.0.3.dist-info/METADATA +635 -0
  126. eval_protocol-0.0.3.dist-info/RECORD +130 -0
  127. eval_protocol-0.0.3.dist-info/WHEEL +5 -0
  128. eval_protocol-0.0.3.dist-info/entry_points.txt +4 -0
  129. eval_protocol-0.0.3.dist-info/licenses/LICENSE +201 -0
  130. eval_protocol-0.0.3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,287 @@
1
+ """
2
+ CLI command for deploying MCP servers to Google Cloud Run.
3
+ """
4
+
5
+ import importlib
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Dict, Optional
11
+
12
+ from eval_protocol.config import GCPCloudRunConfig, RewardKitConfig
13
+ from eval_protocol.config import _config_file_path as global_loaded_config_path
14
+ from eval_protocol.config import get_config
15
+ from eval_protocol.gcp_tools import (
16
+ build_and_push_docker_image,
17
+ deploy_to_cloud_run,
18
+ ensure_artifact_registry_repo_exists,
19
+ )
20
+
21
+ from .common import check_environment
22
+
23
+
24
+ def _generate_mcp_dockerfile_content(
25
+ mcp_server_module: str,
26
+ python_version: str = "3.11",
27
+ service_port: int = 8000,
28
+ additional_requirements: Optional[str] = None,
29
+ ) -> str:
30
+ """
31
+ Generate Dockerfile content for MCP server deployment.
32
+
33
+ Args:
34
+ mcp_server_module: The Python module containing the MCP server (e.g., 'frozen_lake_mcp_server')
35
+ python_version: Python version to use in the container
36
+ service_port: Port the MCP server will listen on
37
+ additional_requirements: Additional pip requirements
38
+
39
+ Returns:
40
+ Dockerfile content as string
41
+ """
42
+
43
+ # Base requirements for MCP servers - matching setup.py dependencies
44
+ base_requirements = [
45
+ "fastmcp>=0.1.0",
46
+ # Core reward-kit dependencies from setup.py
47
+ "requests>=2.25.0",
48
+ "pydantic>=2.0.0",
49
+ "dataclasses-json>=0.5.7",
50
+ "fastapi>=0.68.0",
51
+ "uvicorn>=0.15.0",
52
+ "python-dotenv>=0.19.0",
53
+ "openai==1.78.1",
54
+ "aiosqlite",
55
+ "aiohttp",
56
+ "mcp>=1.9.2",
57
+ "PyYAML>=5.0",
58
+ "datasets==3.6.0",
59
+ "fsspec==2025.3.0",
60
+ "hydra-core>=1.3.2",
61
+ "omegaconf>=2.3.0",
62
+ "gymnasium>=0.29.0",
63
+ "httpx>=0.24.0",
64
+ "fireworks-ai>=0.17.19",
65
+ ]
66
+
67
+ if additional_requirements:
68
+ requirements = base_requirements + [req.strip() for req in additional_requirements.split("\n") if req.strip()]
69
+ else:
70
+ requirements = base_requirements
71
+
72
+ # Generate pip install lines with proper escaping
73
+ pip_install_lines = []
74
+ for req in requirements[:-1]:
75
+ pip_install_lines.append(f" {req} \\")
76
+ pip_install_lines.append(f" {requirements[-1]}")
77
+ pip_install_section = "\n".join(pip_install_lines)
78
+
79
+ dockerfile_content = f"""# Multi-stage build for MCP server deployment
80
+ FROM python:{python_version}-slim as builder
81
+
82
+ WORKDIR /app
83
+
84
+ # Install system dependencies
85
+ RUN apt-get update && apt-get install -y \\
86
+ build-essential \\
87
+ && rm -rf /var/lib/apt/lists/*
88
+
89
+ # Copy requirements and install Python dependencies
90
+ RUN pip install --no-cache-dir --upgrade pip
91
+
92
+ # Install MCP server requirements
93
+ RUN pip install --no-cache-dir \\
94
+ {pip_install_section}
95
+
96
+ # Production stage
97
+ FROM python:{python_version}-slim
98
+
99
+ WORKDIR /app
100
+
101
+ # Install runtime dependencies
102
+ RUN apt-get update && apt-get install -y \\
103
+ && rm -rf /var/lib/apt/lists/*
104
+
105
+ # Copy Python packages from builder
106
+ COPY --from=builder /usr/local/lib/python{python_version}/site-packages /usr/local/lib/python{python_version}/site-packages
107
+ COPY --from=builder /usr/local/bin /usr/local/bin
108
+
109
+ # Copy the MCP server code
110
+ COPY . .
111
+
112
+ # Set environment variables for Cloud Run
113
+ # FastMCP uses HOST and PORT environment variables for streamable-http transport
114
+ ENV HOST=0.0.0.0
115
+ ENV PORT={service_port}
116
+ ENV PYTHONPATH=/app
117
+ ENV PYTHONUNBUFFERED=1
118
+
119
+ # Expose the port
120
+ EXPOSE {service_port}
121
+
122
+ # Run the MCP server with proper host and port for Cloud Run
123
+ CMD ["python", "-m", "{mcp_server_module}", "--host", "0.0.0.0", "--port", "{service_port}"]
124
+ """
125
+
126
+ return dockerfile_content
127
+
128
+
129
+ def _deploy_mcp_to_gcp_cloud_run(args, current_config, gcp_config_from_yaml):
130
+ """Deploy MCP server to GCP Cloud Run."""
131
+ print(f"Starting MCP server deployment to GCP Cloud Run for '{args.id}'...")
132
+
133
+ # Validate required arguments - either dockerfile or mcp-server-module must be provided
134
+ if not args.dockerfile and not args.mcp_server_module:
135
+ print("Error: Either --dockerfile or --mcp-server-module is required for MCP server deployment.")
136
+ return None
137
+
138
+ # Resolve GCP configuration
139
+ gcp_project_id = args.gcp_project
140
+ if not gcp_project_id and gcp_config_from_yaml:
141
+ gcp_project_id = gcp_config_from_yaml.project_id
142
+ if not gcp_project_id:
143
+ print("Error: GCP Project ID must be provided via --gcp-project or rewardkit.yaml.")
144
+ return None
145
+
146
+ gcp_region = args.gcp_region
147
+ if not gcp_region and gcp_config_from_yaml:
148
+ gcp_region = gcp_config_from_yaml.region
149
+ if not gcp_region:
150
+ print("Error: GCP Region must be provided via --gcp-region or rewardkit.yaml.")
151
+ return None
152
+
153
+ gcp_ar_repo_name = args.gcp_ar_repo
154
+ if not gcp_ar_repo_name and gcp_config_from_yaml:
155
+ gcp_ar_repo_name = gcp_config_from_yaml.artifact_registry_repository
156
+ if not gcp_ar_repo_name:
157
+ gcp_ar_repo_name = "reward-kit-mcp-servers"
158
+
159
+ print(f"Using GCP Project: {gcp_project_id}, Region: {gcp_region}, AR Repo: {gcp_ar_repo_name}")
160
+
161
+ # Ensure Artifact Registry repository exists
162
+ if not ensure_artifact_registry_repo_exists(
163
+ project_id=gcp_project_id, region=gcp_region, repo_name=gcp_ar_repo_name
164
+ ):
165
+ print(f"Failed to ensure Artifact Registry repository '{gcp_ar_repo_name}' exists. Aborting.")
166
+ return None
167
+
168
+ # Determine Dockerfile content - use provided file or generate
169
+ dockerfile_content = None
170
+ if hasattr(args, "dockerfile") and args.dockerfile:
171
+ # Use provided Dockerfile
172
+ dockerfile_path = Path(args.dockerfile)
173
+ if not dockerfile_path.exists():
174
+ print(f"Error: Dockerfile not found at {dockerfile_path}")
175
+ return None
176
+ print(f"Using provided Dockerfile: {dockerfile_path}")
177
+ try:
178
+ with open(dockerfile_path, "r") as f:
179
+ dockerfile_content = f.read()
180
+ except Exception as e:
181
+ print(f"Error reading Dockerfile at {dockerfile_path}: {e}")
182
+ return None
183
+ else:
184
+ # Generate Dockerfile content (legacy approach)
185
+ print("Generating Dockerfile content from mcp-server-module...")
186
+ dockerfile_content = _generate_mcp_dockerfile_content(
187
+ mcp_server_module=args.mcp_server_module,
188
+ python_version=getattr(args, "python_version", "3.11"),
189
+ service_port=getattr(args, "port", 8000),
190
+ additional_requirements=getattr(args, "requirements", None),
191
+ )
192
+
193
+ if not dockerfile_content:
194
+ print("Failed to obtain Dockerfile content. Aborting.")
195
+ return None
196
+
197
+ # Build and push Docker image
198
+ image_tag = "latest"
199
+ image_name_tag = f"{gcp_region}-docker.pkg.dev/{gcp_project_id}/{gcp_ar_repo_name}/{args.id}:{image_tag}"
200
+ build_context_dir = os.getcwd()
201
+
202
+ if not build_and_push_docker_image(
203
+ image_name_tag=image_name_tag,
204
+ dockerfile_content=dockerfile_content,
205
+ build_context_dir=build_context_dir,
206
+ gcp_project_id=gcp_project_id,
207
+ ):
208
+ print(f"Failed to build and push Docker image {image_name_tag}. Aborting.")
209
+ return None
210
+
211
+ print(f"Successfully built and pushed Docker image: {image_name_tag}")
212
+
213
+ # Deploy to Cloud Run
214
+ service_port = getattr(args, "port", 8000)
215
+ env_vars = {}
216
+
217
+ # Add any custom environment variables
218
+ if hasattr(args, "env_vars") and args.env_vars:
219
+ for env_pair in args.env_vars:
220
+ if "=" in env_pair:
221
+ key, value = env_pair.split("=", 1)
222
+ env_vars[key] = value
223
+
224
+ cloud_run_service_url = deploy_to_cloud_run(
225
+ service_name=args.id,
226
+ image_name_tag=image_name_tag,
227
+ gcp_project_id=gcp_project_id,
228
+ gcp_region=gcp_region,
229
+ allow_unauthenticated=True, # MCP servers typically need to be publicly accessible
230
+ env_vars=env_vars if env_vars else None,
231
+ service_port=service_port,
232
+ )
233
+
234
+ if not cloud_run_service_url:
235
+ print("Failed to deploy to Cloud Run or retrieve service URL. Aborting.")
236
+ return None
237
+
238
+ print(f"🚀 Successfully deployed MCP server to Cloud Run!")
239
+ print(f"📍 Service URL: {cloud_run_service_url}")
240
+ print(f"🔗 MCP Connection URL: {cloud_run_service_url}")
241
+ print(f"📋 Service Name: {args.id}")
242
+ deployment_method = (
243
+ "local Dockerfile" if (hasattr(args, "dockerfile") and args.dockerfile) else "auto-generated Dockerfile"
244
+ )
245
+ print(f"🐳 Deployment Method: {deployment_method}")
246
+ print()
247
+ print("🎯 Next steps:")
248
+ print(f" 1. Test your MCP server: curl {cloud_run_service_url}/health")
249
+ print(f" 2. Connect MCP clients to: {cloud_run_service_url}")
250
+ print(
251
+ f" 3. Monitor logs: gcloud logging read 'resource.type=cloud_run_revision AND resource.labels.service_name={args.id}' --project {gcp_project_id}"
252
+ )
253
+
254
+ return cloud_run_service_url
255
+
256
+
257
+ def deploy_mcp_command(args):
258
+ """Main entry point for MCP server deployment command."""
259
+
260
+ # Check environment (similar to existing deploy command)
261
+ if not check_environment():
262
+ print("Environment check failed. Please resolve the issues above before deploying.")
263
+ return False
264
+
265
+ try:
266
+ # Load configuration
267
+ current_config = get_config()
268
+ gcp_config_from_yaml: Optional[GCPCloudRunConfig] = None
269
+ if current_config and current_config.gcp_cloud_run:
270
+ gcp_config_from_yaml = current_config.gcp_cloud_run
271
+
272
+ # Deploy to GCP Cloud Run
273
+ service_url = _deploy_mcp_to_gcp_cloud_run(args, current_config, gcp_config_from_yaml)
274
+
275
+ if service_url:
276
+ print(f"✅ MCP server '{args.id}' successfully deployed!")
277
+ return True
278
+ else:
279
+ print(f"❌ Failed to deploy MCP server '{args.id}'")
280
+ return False
281
+
282
+ except Exception as e:
283
+ print(f"Error during MCP server deployment: {e}")
284
+ import traceback
285
+
286
+ traceback.print_exc()
287
+ return False
@@ -0,0 +1,186 @@
1
+ """
2
+ CLI command for previewing an evaluator.
3
+ """
4
+
5
+ import json
6
+ import sys # For sys.exit
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Iterator, List, Optional, Union
9
+
10
+ import requests # For making HTTP requests
11
+
12
+ from eval_protocol.evaluation import preview_evaluation
13
+
14
+ # Assuming EvaluationRequest is defined in generic_server.
15
+ # For loose coupling, it might be better in models.py or a shared types module.
16
+ from eval_protocol.generic_server import EvaluationRequest
17
+ from eval_protocol.models import EvaluateResult, Message
18
+
19
+ # Assuming these helper functions exist or will be created in .common
20
+ # If not, their logic for loading samples would need to be integrated here or called differently.
21
+ from .common import (
22
+ check_environment,
23
+ load_samples_from_file,
24
+ load_samples_from_huggingface,
25
+ )
26
+
27
+
28
+ def preview_command(args):
29
+ """Preview an evaluator with sample data"""
30
+
31
+ # Check environment variables
32
+ if not check_environment():
33
+ return 1
34
+
35
+ # Validate --remote-url and --metrics-folders usage
36
+ if args.remote_url and args.metrics_folders:
37
+ print("Info: --metrics-folders are ignored when --remote-url is specified.")
38
+
39
+ if not args.remote_url and not args.metrics_folders:
40
+ print("Error: Either --remote-url or --metrics-folders must be specified.")
41
+ return 1
42
+
43
+ # Ensure either samples or huggingface_dataset is provided (still needed for remote_url)
44
+ if not args.samples and not args.huggingface_dataset:
45
+ print("Error: Either sample file (--samples) or HuggingFace dataset (--huggingface-dataset) is required.")
46
+ return 1
47
+
48
+ # If using samples file, verify it exists
49
+ if args.samples and not Path(args.samples).exists():
50
+ print(f"Error: Sample file '{args.samples}' not found")
51
+ return 1
52
+
53
+ # Process HuggingFace key mapping if provided
54
+ huggingface_message_key_map = None
55
+ if args.huggingface_key_map:
56
+ try:
57
+ huggingface_message_key_map = json.loads(args.huggingface_key_map)
58
+ except json.JSONDecodeError:
59
+ print("Error: Invalid JSON format for --huggingface-key-map")
60
+ return 1
61
+
62
+ if args.remote_url:
63
+ # Handle previewing against a remote URL
64
+ print(f"Previewing against remote URL: {args.remote_url}")
65
+
66
+ # Ensure the remote URL is a valid base URL
67
+ if not (args.remote_url.startswith("http://") or args.remote_url.startswith("https://")):
68
+ print(f"Error: Invalid --remote-url '{args.remote_url}'. Must start with http:// or https://")
69
+ return 1
70
+
71
+ evaluate_endpoint = f"{args.remote_url.rstrip('/')}/evaluate"
72
+
73
+ samples_iterator: Union[List[Any], Iterator[Dict[str, Any]]] = []
74
+ try:
75
+ if args.samples:
76
+ # Assuming load_samples_from_file yields dicts with "messages" and optional "ground_truth"
77
+ samples_iterator = load_samples_from_file(args.samples, args.max_samples)
78
+ elif args.huggingface_dataset:
79
+ # Assuming load_samples_from_huggingface yields dicts with "messages" and optional "ground_truth"
80
+ samples_iterator = load_samples_from_huggingface(
81
+ dataset_name=args.huggingface_dataset,
82
+ split=args.huggingface_split,
83
+ prompt_key=args.huggingface_prompt_key,
84
+ response_key=args.huggingface_response_key,
85
+ key_map=huggingface_message_key_map,
86
+ max_samples=args.max_samples,
87
+ )
88
+ except Exception as e:
89
+ print(f"Error loading samples: {e}")
90
+ return 1
91
+
92
+ results_count = 0
93
+ for i, sample_data in enumerate(samples_iterator):
94
+ # The load_samples_from_* helpers should ideally cap at max_samples,
95
+ # but we double-check here.
96
+ if i >= args.max_samples:
97
+ break
98
+ results_count += 1
99
+
100
+ messages_payload = sample_data.get("messages", [])
101
+ ground_truth_payload = sample_data.get("ground_truth")
102
+ # Allow passing other sample fields as kwargs to the reward function
103
+ sample_kwargs = {k: v for k, v in sample_data.items() if k not in ["messages", "ground_truth"]}
104
+
105
+ processed_messages = []
106
+ for msg_item in messages_payload:
107
+ if isinstance(msg_item, Message): # If helpers return Message objects
108
+ processed_messages.append(msg_item.model_dump(exclude_none=True))
109
+ elif isinstance(msg_item, dict): # If helpers return dicts
110
+ processed_messages.append(msg_item)
111
+ else:
112
+ print(
113
+ f"Warning: Sample {i+1} has unexpected message item type: {type(msg_item)}. Skipping this message item."
114
+ )
115
+
116
+ try:
117
+ request_obj = EvaluationRequest(
118
+ messages=processed_messages,
119
+ ground_truth=ground_truth_payload,
120
+ kwargs=sample_kwargs,
121
+ )
122
+ except Exception as e: # Pydantic validation for EvaluationRequest
123
+ print(f"\n--- Sample {i+1} ---")
124
+ print(f" Error creating request payload for sample: {e}")
125
+ print(f" Sample data: {sample_data}")
126
+ print("--- End Sample ---")
127
+ continue # Skip to next sample
128
+
129
+ print(f"\n--- Sample {i+1} ---")
130
+
131
+ try:
132
+ response = requests.post(
133
+ evaluate_endpoint,
134
+ json=request_obj.model_dump(), # Use model_dump() for Pydantic v2, or .dict() for v1
135
+ timeout=30,
136
+ )
137
+ response.raise_for_status()
138
+
139
+ result_json = response.json()
140
+ evaluate_result = EvaluateResult(**result_json)
141
+
142
+ print(f" Score: {evaluate_result.score}")
143
+ print(f" Reason: {evaluate_result.reason if evaluate_result.reason else 'N/A'}")
144
+ print(f" Is Valid: {evaluate_result.is_score_valid}")
145
+ if evaluate_result.metrics:
146
+ for k, v_metric in evaluate_result.metrics.items():
147
+ print(
148
+ f" Metric '{k}': Score={v_metric.score}, Valid={v_metric.is_score_valid}, Reason={v_metric.reason}"
149
+ )
150
+
151
+ except requests.exceptions.RequestException as e:
152
+ print(f" Error calling remote URL '{evaluate_endpoint}': {e}")
153
+ except json.JSONDecodeError:
154
+ print(
155
+ f" Error: Invalid JSON response from remote URL. Status: {response.status_code}. Response text: {response.text[:200]}..."
156
+ )
157
+ except Exception as e:
158
+ print(f" Error processing response from remote URL: {e}")
159
+ print("--- End Sample ---")
160
+
161
+ if results_count == 0:
162
+ print("No samples were processed. Check sample source or loading functions.")
163
+ return 0
164
+
165
+ else:
166
+ # Original behavior: preview using local metrics_folders
167
+ # This path is taken if args.remote_url is None (or empty string)
168
+ # We already checked above that if not remote_url, then metrics_folders must be present.
169
+
170
+ try:
171
+ preview_result = preview_evaluation(
172
+ metric_folders=args.metrics_folders,
173
+ sample_file=args.samples if args.samples else None,
174
+ max_samples=args.max_samples,
175
+ huggingface_dataset=args.huggingface_dataset,
176
+ huggingface_split=args.huggingface_split,
177
+ huggingface_prompt_key=args.huggingface_prompt_key,
178
+ huggingface_response_key=args.huggingface_response_key,
179
+ huggingface_message_key_map=huggingface_message_key_map,
180
+ )
181
+
182
+ preview_result.display()
183
+ return 0
184
+ except Exception as e:
185
+ print(f"Error previewing evaluator (local mode): {str(e)}")
186
+ return 1
@@ -0,0 +1,202 @@
1
+ """
2
+ CLI command for running the full evaluation pipeline (generation + evaluation).
3
+ This script is intended to be a Hydra application.
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ import sys
9
+
10
+ import hydra
11
+
12
+ # Ensure hydra.core.hydra_config is available if used for output_dir
13
+ from hydra.core.hydra_config import HydraConfig
14
+ from omegaconf import ( # Ensure MISSING is imported if used in configs
15
+ MISSING,
16
+ DictConfig,
17
+ OmegaConf,
18
+ )
19
+
20
+ from eval_protocol.execution.pipeline import EvaluationPipeline
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def run_evaluation_command_logic(cfg: DictConfig) -> None:
26
+ """
27
+ Main logic for the 'run-evaluation' command.
28
+ """
29
+ logger.info("Starting 'run-evaluation' command with resolved Hydra config.")
30
+
31
+ # Make Hydra's runtime output directory available to the pipeline if needed
32
+ # This assumes 'hydra_output_dir' is a valid field in the pipeline's config if it uses it.
33
+ # A cleaner way is for the pipeline to be Hydra-aware or for this function to pass it explicitly.
34
+ # For now, let's add it to the cfg object that pipeline receives.
35
+ # Ensure the config is not frozen if we add keys, then restore its original struct state.
36
+ was_struct = OmegaConf.is_struct(cfg)
37
+ if was_struct:
38
+ OmegaConf.set_struct(cfg, False)
39
+ cfg.hydra_output_dir = HydraConfig.get().runtime.output_dir
40
+ if was_struct:
41
+ OmegaConf.set_struct(cfg, True)
42
+
43
+ logger.debug(f"Full configuration for pipeline:\n{OmegaConf.to_yaml(cfg)}")
44
+
45
+ try:
46
+ pipeline = EvaluationPipeline(pipeline_cfg=cfg)
47
+ all_results = asyncio.run(pipeline.run()) # Store the results
48
+ logger.info("'run-evaluation' command finished successfully.")
49
+
50
+ # --- Add Summary Report ---
51
+ if all_results:
52
+ total_samples = len(all_results)
53
+ errors = [r for r in all_results if "error" in r and r["error"]]
54
+ # Consider a result successful for summary if it has a score and no critical error string
55
+ successful_evals = [
56
+ r for r in all_results if r.get("evaluation_score") is not None and not ("error" in r and r["error"])
57
+ ]
58
+
59
+ num_errors = len(errors)
60
+ num_successful = len(successful_evals)
61
+
62
+ summary_lines = [
63
+ "\n--- Evaluation Summary ---",
64
+ f"Total samples processed: {total_samples}",
65
+ f"Successful evaluations: {num_successful}",
66
+ f"Evaluation errors: {num_errors}",
67
+ ]
68
+
69
+ if num_successful > 0:
70
+ scores = [
71
+ r["evaluation_score"]
72
+ for r in successful_evals
73
+ if isinstance(r.get("evaluation_score"), (int, float))
74
+ ]
75
+ if scores:
76
+ avg_score = sum(scores) / len(scores)
77
+ min_score = min(scores)
78
+ max_score = max(scores)
79
+ summary_lines.append(f"Average score: {avg_score:.2f}")
80
+ summary_lines.append(f"Min score: {min_score:.2f}")
81
+ summary_lines.append(f"Max score: {max_score:.2f}")
82
+
83
+ # Score distribution (example: 5 bins)
84
+ bins = [
85
+ 0.0,
86
+ 0.2,
87
+ 0.4,
88
+ 0.6,
89
+ 0.8,
90
+ 1.01,
91
+ ] # 1.01 to include 1.0 in last bin
92
+ score_counts = [0] * (len(bins) - 1)
93
+ for score in scores:
94
+ for i in range(len(bins) - 1):
95
+ if bins[i] <= score < bins[i + 1]:
96
+ score_counts[i] += 1
97
+ break
98
+ summary_lines.append("Score distribution:")
99
+ for i in range(len(bins) - 1):
100
+ # Ensure bin upper bound is displayed correctly as 1.0 for the last bin
101
+ upper_bin_display = 1.0 if bins[i + 1] > 1.0 else bins[i + 1]
102
+ summary_lines.append(f" [{bins[i]:.1f} - {upper_bin_display:.1f}): {score_counts[i]}")
103
+
104
+ if num_errors > 0:
105
+ summary_lines.append("\nError details (first 5):")
106
+ for i, err_item in enumerate(errors[:5]):
107
+ err_id = err_item.get("id", "N/A")
108
+ err_msg = err_item.get("error", "Unknown error")
109
+ # Truncate long error messages for summary
110
+ if len(err_msg) > 100:
111
+ err_msg = err_msg[:100] + "..."
112
+ summary_lines.append(f" Sample ID {err_id}: {err_msg}")
113
+
114
+ summary_lines.append("--- End of Summary ---")
115
+
116
+ # Use logger.info for summary to respect overall logging settings
117
+ for line in summary_lines:
118
+ logger.info(line)
119
+ else:
120
+ logger.info("No results to summarize.")
121
+
122
+ except ValueError as ve:
123
+ error_msg = str(ve)
124
+ logger.error(f"Configuration or Value error in pipeline: {ve}")
125
+
126
+ # Provide helpful suggestions based on common errors
127
+ if "final_columns" in error_msg:
128
+ logger.error(
129
+ "HINT: This error suggests your dataset config has 'final_columns' which conflicts with the datasets library."
130
+ )
131
+ logger.error("SOLUTION: Remove 'final_columns' from your dataset config or use the simplified config.")
132
+ elif "user_query" in error_msg and "missing" in error_msg.lower():
133
+ logger.error("HINT: Your data is missing the 'user_query' column.")
134
+ logger.error("SOLUTION: Run 'reward-kit validate-data --file your_data.jsonl' to check data schema.")
135
+ elif "import" in error_msg.lower() or "module" in error_msg.lower():
136
+ logger.error("HINT: Cannot import your reward function module.")
137
+ logger.error("SOLUTION: Ensure your reward function file is in the current directory.")
138
+ elif "config" in error_msg.lower() and "not found" in error_msg.lower():
139
+ logger.error("HINT: Configuration file not found.")
140
+ logger.error("SOLUTION: Ensure your config file is in ./conf/ directory or use --config-path.")
141
+
142
+ sys.exit(1) # Exit with error code for critical failures
143
+ except Exception as e:
144
+ error_msg = str(e)
145
+ logger.error(f"An unexpected error occurred during the evaluation pipeline: {e}")
146
+
147
+ # Provide helpful suggestions for common issues
148
+ if "unexpected keyword argument" in error_msg:
149
+ logger.error("HINT: This suggests a configuration parameter is being passed incorrectly.")
150
+ logger.error("SOLUTION: Check your dataset config for extra parameters like 'final_columns'.")
151
+ elif "No module named" in error_msg:
152
+ logger.error("HINT: Cannot find Python module for reward function.")
153
+ logger.error("SOLUTION: Ensure your reward function file is in the current directory.")
154
+ elif "not enough values to unpack" in error_msg:
155
+ logger.error("HINT: Data format mismatch.")
156
+ logger.error("SOLUTION: Run 'reward-kit validate-data --file your_data.jsonl' to check data format.")
157
+
158
+ logger.error("For more help, try:")
159
+ logger.error("1. Run 'reward-kit validate-data --file your_data.jsonl' to check data")
160
+ logger.error("2. Use the simplified config: --config-name simple_uipath_eval")
161
+ logger.error("3. Check that all files are in the correct locations")
162
+
163
+ sys.exit(1) # Exit with error code
164
+
165
+
166
+ # This is the Hydra entry point for this command.
167
+ # It needs a config_path relative to where this script is, or an absolute one.
168
+ # If reward-kit is installed, conf might not be easily found via relative paths.
169
+ # Using `pkg://` provider is more robust for installed packages.
170
+ # For now, assume a `conf` dir at project root, and this script is in `eval_protocol/cli_commands`.
171
+ import os # Ensure os is imported for path manipulation
172
+
173
+ # So, `config_path` would be `../../conf`.
174
+ # The `config_name` will be the primary config for this `run` command.
175
+ # Let's point directly to the example's config for now to simplify debugging Hydra pathing.
176
+ # Path from eval_protocol/cli_commands/ to examples/math_example/conf/
177
+ # Construct an absolute path or a file:// URL to make it more robust.
178
+ _RUN_EVAL_CMD_DIR = os.path.dirname(os.path.abspath(__file__))
179
+ # Default config_path for @hydra.main, relative to this file.
180
+ # Points to the project's top-level 'conf' directory.
181
+ _DEFAULT_HYDRA_CONFIG_PATH = os.path.abspath(os.path.join(_RUN_EVAL_CMD_DIR, "..", "..", "conf"))
182
+
183
+
184
+ @hydra.main(config_path=_DEFAULT_HYDRA_CONFIG_PATH, config_name=None, version_base="1.3")
185
+ def hydra_cli_entry_point(cfg: DictConfig) -> None:
186
+ # config_path and config_name from CLI will override the defaults in the decorator.
187
+ # If --config-name is not provided via CLI, Hydra would look for a default config
188
+ # (e.g., config.yaml) in the _DEFAULT_HYDRA_CONFIG_PATH.
189
+ # However, our reward-kit run command will always pass --config-path and --config-name.
190
+ # passed to `reward-kit run` (e.g., --config-path, --config-name)
191
+ # or by Hydra's default search behavior if not provided via CLI.
192
+ run_evaluation_command_logic(cfg)
193
+
194
+
195
+ # This allows running `python -m eval_protocol.cli_commands.run_eval_cmd` (if __main__.py in folder)
196
+ # or if this file itself is made executable.
197
+ if __name__ == "__main__":
198
+ # This will parse sys.argv for Hydra overrides.
199
+ # Example: python eval_protocol/cli_commands/run_eval_cmd.py dataset=gsm8k_local_prompts generation.enabled=false
200
+ import sys # Required for sys.exit
201
+
202
+ hydra_cli_entry_point()