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.
- development/__init__.py +1 -0
- development/normalize_sandbox_fusion.py +628 -0
- development/utils/__init__.py +1 -0
- development/utils/generate_api_key.py +31 -0
- development/utils/subprocess_manager.py +481 -0
- eval_protocol/__init__.py +86 -0
- eval_protocol/__main__.py +10 -0
- eval_protocol/_version.py +21 -0
- eval_protocol/adapters/__init__.py +1 -0
- eval_protocol/adapters/braintrust.py +8 -0
- eval_protocol/adapters/trl.py +8 -0
- eval_protocol/agent/__init__.py +29 -0
- eval_protocol/agent/models.py +69 -0
- eval_protocol/agent/orchestrator.py +893 -0
- eval_protocol/agent/resource_abc.py +89 -0
- eval_protocol/agent/resource_pool.py +184 -0
- eval_protocol/agent/resources/__init__.py +44 -0
- eval_protocol/agent/resources/bfcl_envs/__init__.py +1 -0
- eval_protocol/agent/resources/bfcl_envs/gorilla_file_system.py +342 -0
- eval_protocol/agent/resources/bfcl_envs/math_api.py +40 -0
- eval_protocol/agent/resources/bfcl_envs/posting_api.py +157 -0
- eval_protocol/agent/resources/bfcl_sim_api_resource.py +314 -0
- eval_protocol/agent/resources/docker_resource.py +479 -0
- eval_protocol/agent/resources/filesystem_resource.py +371 -0
- eval_protocol/agent/resources/http_rollout_protocol.py +85 -0
- eval_protocol/agent/resources/http_rollout_resource.py +325 -0
- eval_protocol/agent/resources/python_state_resource.py +170 -0
- eval_protocol/agent/resources/sql_resource.py +271 -0
- eval_protocol/agent/task_manager.py +1064 -0
- eval_protocol/agent/tool_registry.py +111 -0
- eval_protocol/auth.py +156 -0
- eval_protocol/cli.py +425 -0
- eval_protocol/cli_commands/__init__.py +1 -0
- eval_protocol/cli_commands/agent_eval_cmd.py +264 -0
- eval_protocol/cli_commands/common.py +242 -0
- eval_protocol/cli_commands/deploy.py +486 -0
- eval_protocol/cli_commands/deploy_mcp.py +287 -0
- eval_protocol/cli_commands/preview.py +186 -0
- eval_protocol/cli_commands/run_eval_cmd.py +202 -0
- eval_protocol/common_utils.py +36 -0
- eval_protocol/config.py +180 -0
- eval_protocol/datasets/__init__.py +1 -0
- eval_protocol/datasets/loader.py +521 -0
- eval_protocol/evaluation.py +1045 -0
- eval_protocol/execution/__init__.py +1 -0
- eval_protocol/execution/pipeline.py +920 -0
- eval_protocol/gcp_tools.py +484 -0
- eval_protocol/generation/cache.py +141 -0
- eval_protocol/generation/clients/base.py +67 -0
- eval_protocol/generation/clients.py +248 -0
- eval_protocol/generic_server.py +165 -0
- eval_protocol/integrations/__init__.py +12 -0
- eval_protocol/integrations/braintrust.py +51 -0
- eval_protocol/integrations/deepeval.py +106 -0
- eval_protocol/integrations/openeval.py +40 -0
- eval_protocol/integrations/trl.py +187 -0
- eval_protocol/mcp/__init__.py +48 -0
- eval_protocol/mcp/adapter.py +131 -0
- eval_protocol/mcp/client/__init__.py +12 -0
- eval_protocol/mcp/client/connection.py +499 -0
- eval_protocol/mcp/clients.py +195 -0
- eval_protocol/mcp/execution/__init__.py +23 -0
- eval_protocol/mcp/execution/base_policy.py +227 -0
- eval_protocol/mcp/execution/fireworks_policy.py +209 -0
- eval_protocol/mcp/execution/manager.py +506 -0
- eval_protocol/mcp/execution/policy.py +421 -0
- eval_protocol/mcp/grid_renderer.py +54 -0
- eval_protocol/mcp/mcpgym.py +637 -0
- eval_protocol/mcp/process_manager.py +177 -0
- eval_protocol/mcp/session/__init__.py +11 -0
- eval_protocol/mcp/session/manager.py +228 -0
- eval_protocol/mcp/simple_process_manager.py +291 -0
- eval_protocol/mcp/simulation_server.py +458 -0
- eval_protocol/mcp/types.py +80 -0
- eval_protocol/mcp_agent/__init__.py +1 -0
- eval_protocol/mcp_agent/config.py +147 -0
- eval_protocol/mcp_agent/intermediary_server.py +542 -0
- eval_protocol/mcp_agent/main.py +210 -0
- eval_protocol/mcp_agent/orchestration/__init__.py +1 -0
- eval_protocol/mcp_agent/orchestration/base_client.py +132 -0
- eval_protocol/mcp_agent/orchestration/local_docker_client.py +702 -0
- eval_protocol/mcp_agent/orchestration/remote_http_client.py +304 -0
- eval_protocol/mcp_agent/orchestration/stdio_mcp_client_helper.py +3 -0
- eval_protocol/mcp_agent/session.py +79 -0
- eval_protocol/mcp_env.py +304 -0
- eval_protocol/models.py +366 -0
- eval_protocol/packaging.py +219 -0
- eval_protocol/platform_api.py +360 -0
- eval_protocol/playback_policy.py +396 -0
- eval_protocol/resources.py +128 -0
- eval_protocol/reward_function.py +410 -0
- eval_protocol/rewards/__init__.py +94 -0
- eval_protocol/rewards/accuracy.py +454 -0
- eval_protocol/rewards/accuracy_length.py +173 -0
- eval_protocol/rewards/apps_coding_reward.py +331 -0
- eval_protocol/rewards/apps_execution_utils.py +149 -0
- eval_protocol/rewards/apps_testing_util.py +559 -0
- eval_protocol/rewards/bfcl_reward.py +313 -0
- eval_protocol/rewards/code_execution.py +1620 -0
- eval_protocol/rewards/code_execution_utils.py +72 -0
- eval_protocol/rewards/cpp_code.py +861 -0
- eval_protocol/rewards/deepcoder_reward.py +161 -0
- eval_protocol/rewards/format.py +129 -0
- eval_protocol/rewards/function_calling.py +541 -0
- eval_protocol/rewards/json_schema.py +422 -0
- eval_protocol/rewards/language_consistency.py +700 -0
- eval_protocol/rewards/lean_prover.py +479 -0
- eval_protocol/rewards/length.py +375 -0
- eval_protocol/rewards/list_comparison_math_reward.py +221 -0
- eval_protocol/rewards/math.py +762 -0
- eval_protocol/rewards/multiple_choice_math_reward.py +232 -0
- eval_protocol/rewards/reasoning_steps.py +249 -0
- eval_protocol/rewards/repetition.py +342 -0
- eval_protocol/rewards/tag_count.py +162 -0
- eval_protocol/rl_processing.py +82 -0
- eval_protocol/server.py +271 -0
- eval_protocol/typed_interface.py +260 -0
- eval_protocol/utils/__init__.py +8 -0
- eval_protocol/utils/batch_evaluation.py +217 -0
- eval_protocol/utils/batch_transformation.py +205 -0
- eval_protocol/utils/dataset_helpers.py +112 -0
- eval_protocol/utils/module_loader.py +56 -0
- eval_protocol/utils/packaging_utils.py +108 -0
- eval_protocol/utils/static_policy.py +305 -0
- eval_protocol-0.0.3.dist-info/METADATA +635 -0
- eval_protocol-0.0.3.dist-info/RECORD +130 -0
- eval_protocol-0.0.3.dist-info/WHEEL +5 -0
- eval_protocol-0.0.3.dist-info/entry_points.txt +4 -0
- eval_protocol-0.0.3.dist-info/licenses/LICENSE +201 -0
- 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()
|