Skip to content

Commit 676aa2c

Browse files
committed
feat: separate model/judge API keys, fix f-string syntax, rename jobs
- Add judge_api_key input (defaults to llm_api_key when not set) - Fix Python SyntaxError: backslash in f-string (use constants instead) - Rename workflow job to "VerifyWise LLM Evaluation (model_name)" - Update README to document two-key architecture and when each is needed - Update reusable workflow header with cross-provider example Made-with: Cursor
1 parent 9c7ddb3 commit 676aa2c

4 files changed

Lines changed: 44 additions & 47 deletions

File tree

.github/workflows/e2e-test.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
name: E2E Evaluation Test
1+
name: VerifyWise LLM Evaluation
22

33
on:
44
workflow_dispatch:
55

66
jobs:
7-
eval-chatbot:
8-
name: Chatbot Eval (answer_relevancy + bias)
7+
evaluate:
8+
name: VerifyWise LLM Evaluation (gpt-4o-mini)
99
runs-on: ubuntu-latest
1010
steps:
1111
- uses: actions/checkout@v4
@@ -14,7 +14,7 @@ jobs:
1414
id: eval
1515
uses: ./
1616
with:
17-
api_url: https://a5d433af817b90.lhr.life
17+
api_url: https://29ae92ae4bd295.lhr.life
1818
project_id: project_20260403_144244_140917
1919
dataset_id: "52"
2020
metrics: answer_relevancy,bias
@@ -27,6 +27,7 @@ jobs:
2727
fail_on_threshold: "false"
2828
vw_api_token: ${{ secrets.VW_API_TOKEN }}
2929
llm_api_key: ${{ secrets.LLM_API_KEY }}
30+
# judge_api_key not set — defaults to llm_api_key
3031

3132
- name: Show results
3233
if: always()
@@ -37,7 +38,3 @@ jobs:
3738
echo "--- Results ---"
3839
cat "${{ steps.eval.outputs.results_path }}"
3940
fi
40-
if [ -f "${{ steps.eval.outputs.summary_path }}" ]; then
41-
echo "--- Summary ---"
42-
cat "${{ steps.eval.outputs.summary_path }}"
43-
fi

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,13 @@ jobs:
128128

129129
**Required secrets** — add these in your repo's Settings > Secrets and variables > Actions:
130130

131-
| Secret | Where to get it |
132-
|--------|----------------|
133-
| `VW_API_TOKEN` | VerifyWise dashboard > Settings > API Tokens |
134-
| `LLM_API_KEY` | Your LLM provider (OpenAI, Anthropic, etc.) |
131+
| Secret | Required | Where to get it |
132+
|--------|----------|----------------|
133+
| `VW_API_TOKEN` | yes | VerifyWise dashboard > Settings > API Tokens |
134+
| `LLM_API_KEY` | yes | API key for the model being evaluated (e.g. OpenAI, Anthropic) |
135+
| `JUDGE_API_KEY` | no | API key for the judge LLM. Defaults to `LLM_API_KEY` if not set. Only needed when the model and judge use different providers. |
136+
137+
> **How it works:** The evaluation uses two LLMs — the **model** generates responses to your prompts, and the **judge** scores those responses against the selected metrics. If both use the same provider (e.g. both OpenAI), a single `LLM_API_KEY` is enough. If they use different providers (e.g. evaluating a Claude model with GPT-4o as judge), set `JUDGE_API_KEY` separately.
135138
136139
---
137140

@@ -146,7 +149,8 @@ jobs:
146149
| `model_name` | **yes** || Model to evaluate (e.g. `gpt-4o-mini`, `claude-3-5-sonnet`) |
147150
| `model_provider` | **yes** || `openai`, `anthropic`, `google`, `mistral`, `xai`, or `self-hosted` |
148151
| `vw_api_token` | **yes** || VerifyWise API token (store as a repository secret) |
149-
| `llm_api_key` | **yes** || API key for the LLM provider (store as a repository secret) |
152+
| `llm_api_key` | **yes** || API key for the model being evaluated |
153+
| `judge_api_key` | no | *(same as llm_api_key)* | API key for the judge LLM (only needed when model and judge use different providers) |
150154
| `judge_model` | no | `gpt-4o` | LLM used to judge responses |
151155
| `judge_provider` | no | `openai` | Provider for the judge LLM |
152156
| `threshold` | no | `0.7` | Pass/fail threshold (0.0–1.0) |

action.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,12 @@ inputs:
7373
description: 'VerifyWise API token'
7474
required: true
7575
llm_api_key:
76-
description: 'LLM provider API key'
76+
description: 'API key for the model being evaluated'
7777
required: true
78+
judge_api_key:
79+
description: 'API key for the judge LLM (defaults to llm_api_key if not set)'
80+
required: false
81+
default: ''
7882

7983
outputs:
8084
passed:
@@ -120,6 +124,7 @@ runs:
120124
VW_POLL_INTERVAL: ${{ inputs.poll_interval_seconds }}
121125
VW_EXPERIMENT_NAME: ${{ inputs.experiment_name }}
122126
LLM_API_KEY: ${{ inputs.llm_api_key }}
127+
JUDGE_API_KEY: ${{ inputs.judge_api_key }}
123128
run: |
124129
RESULTS="${{ runner.temp }}/vw-results.json"
125130
SUMMARY="${{ runner.temp }}/vw-summary.md"
@@ -176,12 +181,10 @@ runs:
176181
177182
passed = data.get("passed", False)
178183
metrics = data.get("metrics", [])
179-
samples = data.get("samples", [])
180184
name = data.get("name", "Evaluation")
181185
model = data.get("model", "unknown")
182186
183187
failing = [m for m in metrics if not m.get("passed")]
184-
passing = [m for m in metrics if m.get("passed")]
185188
186189
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
187190
out.write(f"passed={'true' if passed else 'false'}\n")

ci_eval_runner.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
sys.exit(2)
2828

2929
INVERTED_KEYWORDS = ("bias", "toxicity", "hallucination", "conversationsafety")
30+
EM_DASH = "\u2014"
31+
CHECK = "\u2705"
32+
CROSS = "\u274C"
3033

3134

3235
def parse_args() -> argparse.Namespace:
@@ -84,7 +87,8 @@ def create_experiment(
8487
name: str,
8588
) -> Dict[str, Any]:
8689
url = f"{base_url}/api/deepeval/experiments"
87-
llm_api_key = os.getenv("LLM_API_KEY", "")
90+
model_api_key = os.getenv("LLM_API_KEY", "")
91+
judge_api_key = os.getenv("JUDGE_API_KEY", "") or model_api_key
8892

8993
metric_configs = []
9094
for m in metrics:
@@ -110,7 +114,7 @@ def create_experiment(
110114
"name": model_name,
111115
"model_name": model_name,
112116
"provider": model_provider,
113-
"apiKey": llm_api_key if model_provider != "self-hosted" else "",
117+
"apiKey": model_api_key if model_provider != "self-hosted" else "",
114118
},
115119
"dataset": {
116120
"id": dataset_id,
@@ -122,7 +126,7 @@ def create_experiment(
122126
"judgeLlm": {
123127
"provider": judge_provider,
124128
"model": judge_model,
125-
"apiKey": llm_api_key,
129+
"apiKey": judge_api_key,
126130
},
127131
},
128132
}
@@ -235,7 +239,6 @@ def parse_results(
235239
})
236240

237241
# Build samples from detailed_results, enriched with log data
238-
# Logs are ordered newest-first; reverse to match sample order
239242
sorted_logs = list(reversed(logs))
240243

241244
samples = []
@@ -269,7 +272,6 @@ def parse_results(
269272
}
270273
samples.append(sample_entry)
271274

272-
# If we have logs but no detailed_results, build samples from logs alone
273275
if not detailed_results and sorted_logs:
274276
for i, log in enumerate(sorted_logs):
275277
samples.append({
@@ -294,38 +296,40 @@ def parse_results(
294296

295297

296298
def _escape_md(text: str) -> str:
297-
"""Prepare text for display in markdown without truncation."""
299+
"""Prepare text for display in markdown."""
298300
if not text:
299301
return ""
300302
return text.replace("\n", " ").replace("|", "\\|").strip()
301303

302304

303305
def generate_markdown(results: Dict[str, Any]) -> str:
306+
model = results["model"]
307+
overall_icon = CHECK if results["passed"] else CROSS
308+
overall_word = "PASS" if results["passed"] else "FAIL"
309+
304310
lines = [
305311
"## VerifyWise LLM Evaluation Results",
306312
"",
307313
f"**Experiment:** {results['name']} ",
308-
f"**Model:** {results['model']} ",
314+
f"**Model:** {model} ",
309315
f"**Status:** {results['status']} ",
310316
f"**Samples:** {results['total_prompts']} ",
311317
]
312318

313319
if results.get("duration_ms"):
314320
lines.append(f"**Duration:** {results['duration_ms'] / 1000:.1f}s ")
315321

316-
icon = "\u2705" if results["passed"] else "\u274C"
317-
overall = "PASS" if results["passed"] else "FAIL"
318322
lines.extend([
319323
"",
320-
f"### Overall: {icon} {overall}",
324+
f"### Overall: {overall_icon} {overall_word}",
321325
"",
322326
"| Metric | Score | Threshold | Result |",
323327
"|--------|------:|----------:|--------|",
324328
])
325329

326330
for m in results["metrics"]:
327331
inv = " *(inverted)*" if m["inverted"] else ""
328-
mi = "\u2705" if m["passed"] else "\u274C"
332+
mi = CHECK if m["passed"] else CROSS
329333
lines.append(
330334
f"| {m['name']}{inv} | {m['score']*100:.1f}% | {m['threshold']*100:.0f}% | {mi} |"
331335
)
@@ -342,23 +346,20 @@ def generate_markdown(results: Dict[str, Any]) -> str:
342346

343347
for sample in samples:
344348
sample_scores = sample.get("metric_scores", {})
345-
346-
# Show all samples that have metric scores (not just failing ones)
347-
# so the user can compare passing vs failing on the same prompt
348349
if not sample_scores:
349350
continue
350351

351-
input_text = _escape_md(sample["input"])
352-
output_text = _escape_md(sample["output"])
352+
input_text = _escape_md(sample["input"]) or "*(empty)*"
353+
output_text = _escape_md(sample["output"]) or "*(not captured)*"
353354
expected_text = _escape_md(sample.get("expected", ""))
354355

355356
lines.extend([
356357
"",
357358
f"#### Sample {sample['index']}",
358359
"",
359-
f"**Input:** {input_text}" if input_text else "**Input:** *(empty)*",
360+
f"**Input:** {input_text}",
360361
"",
361-
f"**Response:** {output_text}" if output_text else "**Response:** *(not captured)*",
362+
f"**Response:** {output_text}",
362363
])
363364

364365
if expected_text:
@@ -380,13 +381,14 @@ def generate_markdown(results: Dict[str, Any]) -> str:
380381
score_str = "N/A"
381382

382383
if passed is True:
383-
result_str = "\u2705"
384+
result_str = CHECK
384385
elif passed is False:
385-
result_str = "\u274C"
386+
result_str = CROSS
386387
else:
387-
result_str = "\u2014"
388+
result_str = EM_DASH
388389

389-
lines.append(f"| {metric_name} | {score_str} | {result_str} | {explanation or '\u2014'} |")
390+
explanation_str = explanation if explanation else EM_DASH
391+
lines.append(f"| {metric_name} | {score_str} | {result_str} | {explanation_str} |")
390392

391393
lines.extend([
392394
"",
@@ -397,15 +399,6 @@ def generate_markdown(results: Dict[str, Any]) -> str:
397399
return "\n".join(lines)
398400

399401

400-
def _metric_name_matches(name: str, targets: set) -> bool:
401-
"""Check if a metric name matches any target, case-insensitively."""
402-
lower = name.lower()
403-
for t in targets:
404-
if t.lower() == lower or t.lower().replace("_", "") == lower.replace("_", ""):
405-
return True
406-
return False
407-
408-
409402
def main():
410403
args = parse_args()
411404

0 commit comments

Comments
 (0)