Skip to content

Commit ad1017b

Browse files
authored
Merge branch 'eval-protocol:main' into main
2 parents 1041579 + 92cd591 commit ad1017b

17 files changed

Lines changed: 443 additions & 73 deletions

eval_protocol/adapters/fireworks_tracing.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,12 @@ def search_logs(self, tags: List[str], limit: int = 100, hours_back: int = 24) -
273273
if not tags:
274274
raise ValueError("At least one tag is required to fetch logs")
275275

276-
headers = {"Authorization": f"Bearer {os.environ.get('FIREWORKS_API_KEY')}"}
276+
from ..common_utils import get_user_agent
277+
278+
headers = {
279+
"Authorization": f"Bearer {os.environ.get('FIREWORKS_API_KEY')}",
280+
"User-Agent": get_user_agent(),
281+
}
277282
params: Dict[str, Any] = {"tags": tags, "limit": limit, "hours_back": hours_back, "program": "eval_protocol"}
278283

279284
# Try /logs first, fall back to /v1/logs if not found
@@ -398,7 +403,12 @@ def get_evaluation_rows(
398403
else:
399404
url = f"{self.base_url}/v1/traces/pointwise"
400405

401-
headers = {"Authorization": f"Bearer {os.environ.get('FIREWORKS_API_KEY')}"}
406+
from ..common_utils import get_user_agent
407+
408+
headers = {
409+
"Authorization": f"Bearer {os.environ.get('FIREWORKS_API_KEY')}",
410+
"User-Agent": get_user_agent(),
411+
}
402412

403413
result = None
404414
try:

eval_protocol/auth.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,16 @@ def verify_api_key_and_get_account_id(
242242
if not resolved_key:
243243
return None
244244
resolved_base = api_base or get_fireworks_api_base()
245+
246+
from .common_utils import get_user_agent
247+
245248
url = f"{resolved_base.rstrip('/')}/verifyApiKey"
246-
headers = {"Authorization": f"Bearer {resolved_key}"}
249+
headers = {
250+
"Authorization": f"Bearer {resolved_key}",
251+
"User-Agent": get_user_agent(),
252+
}
247253
resp = requests.get(url, headers=headers, timeout=10)
254+
248255
if resp.status_code != 200:
249256
logger.debug("verifyApiKey returned status %s", resp.status_code)
250257
return None

eval_protocol/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,10 @@ def parse_args(args=None):
355355
action="store_true",
356356
help="Non-interactive: upload all discovered evaluation tests",
357357
)
358+
upload_parser.add_argument(
359+
"--env-file",
360+
help="Path to .env file containing secrets to upload (default: .env in current directory)",
361+
)
358362

359363
# Create command group
360364
create_parser = subparsers.add_parser(
@@ -421,6 +425,7 @@ def parse_args(args=None):
421425
rft_parser.add_argument("--rft-job-id", help="Specify an explicit RFT job id")
422426
rft_parser.add_argument("--yes", "-y", action="store_true", help="Non-interactive mode")
423427
rft_parser.add_argument("--dry-run", action="store_true", help="Print planned REST calls without sending")
428+
rft_parser.add_argument("--force", action="store_true", help="Overwrite existing evaluator with the same ID")
424429

425430
# Run command (for Hydra-based evaluations)
426431
# This subparser intentionally defines no arguments itself.

eval_protocol/cli_commands/create_rft.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
import argparse
66
from typing import Any, Dict, Optional
77

8+
import requests
9+
810
from ..auth import (
911
get_fireworks_account_id,
1012
get_fireworks_api_base,
1113
get_fireworks_api_key,
1214
verify_api_key_and_get_account_id,
1315
)
16+
from ..common_utils import get_user_agent
1417
from ..fireworks_rft import (
1518
_map_api_host_to_app_host,
1619
build_default_output_model,
@@ -263,10 +266,72 @@ def _auto_select_evaluator_id(cwd: str) -> Optional[str]:
263266
return None
264267

265268

269+
def _poll_evaluator_status(
270+
evaluator_resource_name: str, api_key: str, api_base: str, timeout_minutes: int = 10
271+
) -> bool:
272+
"""
273+
Poll evaluator status until it becomes ACTIVE or times out.
274+
275+
Args:
276+
evaluator_resource_name: Full evaluator resource name (e.g., accounts/xxx/evaluators/yyy)
277+
api_key: Fireworks API key
278+
api_base: Fireworks API base URL
279+
timeout_minutes: Maximum time to wait in minutes
280+
281+
Returns:
282+
True if evaluator becomes ACTIVE, False if timeout or BUILD_FAILED
283+
"""
284+
headers = {
285+
"Authorization": f"Bearer {api_key}",
286+
"Content-Type": "application/json",
287+
"User-Agent": get_user_agent(),
288+
}
289+
290+
check_url = f"{api_base}/v1/{evaluator_resource_name}"
291+
timeout_seconds = timeout_minutes * 60
292+
poll_interval = 10 # seconds
293+
start_time = time.time()
294+
295+
print(f"Polling evaluator status (timeout: {timeout_minutes}m, interval: {poll_interval}s)...")
296+
297+
while time.time() - start_time < timeout_seconds:
298+
try:
299+
response = requests.get(check_url, headers=headers, timeout=30)
300+
response.raise_for_status()
301+
302+
evaluator_data = response.json()
303+
state = evaluator_data.get("state", "STATE_UNSPECIFIED")
304+
status = evaluator_data.get("status", "")
305+
306+
if state == "ACTIVE":
307+
print("✅ Evaluator is ACTIVE and ready!")
308+
return True
309+
elif state == "BUILD_FAILED":
310+
print(f"❌ Evaluator build failed. Status: {status}")
311+
return False
312+
elif state == "BUILDING":
313+
elapsed_minutes = (time.time() - start_time) / 60
314+
print(f"⏳ Evaluator is still building... ({elapsed_minutes:.1f}m elapsed)")
315+
else:
316+
print(f"⏳ Evaluator state: {state}, status: {status}")
317+
318+
except requests.exceptions.RequestException as e:
319+
print(f"Warning: Failed to check evaluator status: {e}")
320+
321+
# Wait before next poll
322+
time.sleep(poll_interval)
323+
324+
# Timeout reached
325+
elapsed_minutes = (time.time() - start_time) / 60
326+
print(f"⏰ Timeout after {elapsed_minutes:.1f}m - evaluator is not yet ACTIVE")
327+
return False
328+
329+
266330
def create_rft_command(args) -> int:
267331
evaluator_id: Optional[str] = getattr(args, "evaluator_id", None)
268332
non_interactive: bool = bool(getattr(args, "yes", False))
269333
dry_run: bool = bool(getattr(args, "dry_run", False))
334+
force: bool = bool(getattr(args, "force", False))
270335

271336
api_key = get_fireworks_api_key()
272337
if not api_key:
@@ -326,12 +391,34 @@ def create_rft_command(args) -> int:
326391
id=evaluator_id,
327392
display_name=None,
328393
description=None,
329-
force=False,
394+
force=force, # Pass through the --force flag
330395
yes=True,
396+
env_file=None, # Add the new env_file parameter
331397
)
398+
399+
if force:
400+
print(f"🔄 Force flag enabled - will overwrite existing evaluator '{evaluator_id}'")
401+
332402
rc = upload_command(upload_args)
333403
if rc == 0:
334404
print(f"✓ Uploaded/ensured evaluator: {evaluator_id}")
405+
406+
# Poll for evaluator status
407+
print(f"Waiting for evaluator '{evaluator_id}' to become ACTIVE...")
408+
is_active = _poll_evaluator_status(
409+
evaluator_resource_name=evaluator_resource_name, api_key=api_key, api_base=api_base, timeout_minutes=10
410+
)
411+
412+
if not is_active:
413+
# Print helpful message with dashboard link
414+
app_base = _map_api_host_to_app_host(api_base)
415+
evaluator_slug = _extract_terminal_segment(evaluator_id)
416+
dashboard_url = f"{app_base}/dashboard/evaluators/{evaluator_slug}"
417+
418+
print("\n❌ Evaluator is not ready within the timeout period.")
419+
print(f"📊 Please check the evaluator status at: {dashboard_url}")
420+
print(" Wait for it to become ACTIVE, then run 'eval-protocol create rft' again.")
421+
return 1
335422
else:
336423
print("Warning: Evaluator upload did not complete successfully; proceeding to RFT creation.")
337424
except Exception as e:

eval_protocol/cli_commands/upload.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
from dataclasses import dataclass
1111
from pathlib import Path
12-
from typing import Any, Callable, Iterable, Optional
12+
from typing import Any, Dict, Iterable
1313

1414
import pytest
1515
from eval_protocol.auth import (
@@ -551,6 +551,35 @@ def _prompt_select(tests: list[DiscoveredTest], non_interactive: bool) -> list[D
551551
return _prompt_select_interactive(tests)
552552

553553

554+
def _load_secrets_from_env_file(env_file_path: str) -> Dict[str, str]:
555+
"""
556+
Load secrets from a .env file that should be uploaded to Fireworks.
557+
558+
Returns a dictionary of secret key-value pairs that contain 'API_KEY' in the name.
559+
"""
560+
if not os.path.exists(env_file_path):
561+
return {}
562+
563+
# Load the .env file into a temporary environment
564+
env_vars = {}
565+
with open(env_file_path, "r") as f:
566+
for line in f:
567+
line = line.strip()
568+
if line and not line.startswith("#") and "=" in line:
569+
key, value = line.split("=", 1)
570+
key = key.strip()
571+
value = value.strip().strip('"').strip("'") # Remove quotes
572+
env_vars[key] = value
573+
574+
# Filter for secrets that look like API keys
575+
secrets = {}
576+
for key, value in env_vars.items():
577+
if "API_KEY" in key.upper() and value:
578+
secrets[key] = value
579+
580+
return secrets
581+
582+
554583
def upload_command(args: argparse.Namespace) -> int:
555584
root = os.path.abspath(getattr(args, "path", "."))
556585
entries_arg = getattr(args, "entry", None)
@@ -585,11 +614,27 @@ def upload_command(args: argparse.Namespace) -> int:
585614
display_name = getattr(args, "display_name", None)
586615
description = getattr(args, "description", None)
587616
force = bool(getattr(args, "force", False))
617+
env_file = getattr(args, "env_file", None)
588618

589-
# Ensure FIREWORKS_API_KEY is available to the remote by storing it as a Fireworks secret
619+
# Load secrets from .env file and ensure they're available on Fireworks
590620
try:
591621
fw_account_id = get_fireworks_account_id()
622+
623+
# Determine .env file path
624+
if env_file:
625+
env_file_path = env_file
626+
else:
627+
env_file_path = os.path.join(root, ".env")
628+
629+
# Load secrets from .env file
630+
secrets_from_file = _load_secrets_from_env_file(env_file_path)
631+
secrets_from_env_file = secrets_from_file.copy() # Track what came from .env file
632+
633+
# Also ensure FIREWORKS_API_KEY from environment is included
592634
fw_api_key_value = get_fireworks_api_key()
635+
if fw_api_key_value:
636+
secrets_from_file["FIREWORKS_API_KEY"] = fw_api_key_value
637+
593638
if not fw_account_id and fw_api_key_value:
594639
# Attempt to verify and resolve account id from server headers
595640
resolved = verify_api_key_and_get_account_id(api_key=fw_api_key_value, api_base=get_fireworks_api_base())
@@ -598,21 +643,27 @@ def upload_command(args: argparse.Namespace) -> int:
598643
# Propagate to environment so downstream calls use it if needed
599644
os.environ["FIREWORKS_ACCOUNT_ID"] = fw_account_id
600645
print(f"Resolved FIREWORKS_ACCOUNT_ID via API verification: {fw_account_id}")
601-
if fw_account_id and fw_api_key_value:
602-
print("Ensuring FIREWORKS_API_KEY is registered as a secret on Fireworks for rollout...")
603-
if create_or_update_fireworks_secret(
604-
account_id=fw_account_id,
605-
key_name="FIREWORKS_API_KEY",
606-
secret_value=fw_api_key_value,
607-
):
608-
print("✓ FIREWORKS_API_KEY secret created/updated on Fireworks.")
609-
else:
610-
print("Warning: Failed to create/update FIREWORKS_API_KEY secret on Fireworks.")
646+
647+
if fw_account_id and secrets_from_file:
648+
print(f"Found {len(secrets_from_file)} API keys to upload as Fireworks secrets...")
649+
if secrets_from_env_file and os.path.exists(env_file_path):
650+
print(f"Loading secrets from: {env_file_path}")
651+
652+
for secret_name, secret_value in secrets_from_file.items():
653+
print(f"Ensuring {secret_name} is registered as a secret on Fireworks for rollout...")
654+
if create_or_update_fireworks_secret(
655+
account_id=fw_account_id,
656+
key_name=secret_name,
657+
secret_value=secret_value,
658+
):
659+
print(f"✓ {secret_name} secret created/updated on Fireworks.")
660+
else:
661+
print(f"Warning: Failed to create/update {secret_name} secret on Fireworks.")
611662
else:
612663
if not fw_account_id:
613-
print("Warning: FIREWORKS_ACCOUNT_ID not found; cannot register FIREWORKS_API_KEY secret.")
614-
if not fw_api_key_value:
615-
print("Warning: FIREWORKS_API_KEY not found locally; cannot register secret.")
664+
print("Warning: FIREWORKS_ACCOUNT_ID not found; cannot register secrets.")
665+
if not secrets_from_file:
666+
print("Warning: No API keys found in environment or .env file; no secrets to register.")
616667
except Exception as e:
617668
print(f"Warning: Skipped Fireworks secret registration due to error: {e}")
618669

eval_protocol/common_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@
55
import requests
66

77

8+
def get_user_agent() -> str:
9+
"""
10+
Returns the user-agent string for eval-protocol CLI requests.
11+
12+
Format: eval-protocol-cli/{version}
13+
14+
Returns:
15+
User-agent string identifying the eval-protocol CLI and version.
16+
"""
17+
try:
18+
from . import __version__
19+
20+
return f"eval-protocol/{__version__}"
21+
except Exception:
22+
return "eval-protocol/unknown"
23+
24+
825
def load_jsonl(file_path: str) -> List[Dict[str, Any]]:
926
"""
1027
Reads a JSONL file where each line is a valid JSON object and returns a list of these objects.

eval_protocol/evaluation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
get_fireworks_api_key,
2121
verify_api_key_and_get_account_id,
2222
)
23+
from eval_protocol.common_utils import get_user_agent
2324
from eval_protocol.typed_interface import EvaluationMode
2425

2526
from eval_protocol.get_pep440_version import get_pep440_version
@@ -405,6 +406,7 @@ def preview(self, sample_file, max_samples=5):
405406
headers = {
406407
"Authorization": f"Bearer {auth_token}",
407408
"Content-Type": "application/json",
409+
"User-Agent": get_user_agent(),
408410
}
409411
logger.info(f"Previewing evaluator using API endpoint: {url} with account: {account_id}")
410412
logger.debug(f"Preview API Request URL: {url}")
@@ -748,6 +750,7 @@ def create(self, evaluator_id, display_name=None, description=None, force=False)
748750
headers = {
749751
"Authorization": f"Bearer {auth_token}",
750752
"Content-Type": "application/json",
753+
"User-Agent": get_user_agent(),
751754
}
752755

753756
self._ensure_requirements_present(os.getcwd())

0 commit comments

Comments
 (0)