Skip to content

Commit 9b642d9

Browse files
authored
fix(deps): pin patched LiteLLM and repair integration CI (#460)
* fix(deps): admit patched litellm and preserve mcp v1 APIs * fix(ci): authenticate E2B sandboxes and isolate integration prerequisites * fix(deps): require tested MCP 1.30.0 minimum * fix(ci): remove unrelated Klavis credential wiring
1 parent 1a619c9 commit 9b642d9

10 files changed

Lines changed: 1857 additions & 460 deletions

‎eval_protocol/rewards/code_execution.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,10 @@ def execute_code_with_e2b(
833833
"error": "API key is required for E2B execution. Set it using the api_key parameter or E2B_API_KEY environment variable.",
834834
}
835835

836-
with Sandbox(api_key=api_key) as sandbox:
836+
# E2B v2's factory carries the sandbox access token required by envd.
837+
# Retain constructor support for older optional code-interpreter SDKs.
838+
sandbox_factory = getattr(Sandbox, "create", Sandbox)
839+
with sandbox_factory(api_key=api_key) as sandbox:
837840
stdout = []
838841
stderr = []
839842

@@ -849,8 +852,6 @@ def capture_stderr(output):
849852
else:
850853
stderr.append(str(output))
851854

852-
sandbox.on_exit = lambda *args: None # type: ignore[method-assign, assignment]
853-
854855
if language.lower() in ["python", "py"]:
855856
file_path = "/code/script.py"
856857
cmd = "python3 /code/script.py"
@@ -866,7 +867,9 @@ def capture_stderr(output):
866867

867868
try:
868869
fs_handler = None
869-
if _E2B_SOURCE == "e2b_code_interpreter":
870+
if hasattr(sandbox, "files"):
871+
fs_handler = sandbox.files
872+
elif _E2B_SOURCE == "e2b_code_interpreter":
870873
if hasattr(sandbox, "filesystem"):
871874
fs_handler = sandbox.filesystem
872875
elif _E2B_SOURCE == "e2b":

‎pyproject.toml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ dependencies = [
2525
"openai>=1.78.1",
2626
"aiosqlite",
2727
"aiohttp",
28-
"mcp>=1.9.2",
28+
"mcp>=1.30.0,<2",
2929
"PyYAML>=5.0",
3030
"hydra-core>=1.3.2",
3131
"omegaconf>=2.3.0",
3232
"httpx>=0.24.0",
3333
"anthropic>=0.59.0",
34-
"litellm==1.84.3",
34+
"litellm==1.105.0",
3535
"pytest>=6.0.0",
3636
"pytest-asyncio>=0.21.0",
3737
"peewee>=3.18.2",
@@ -70,7 +70,8 @@ dev = [
7070
"versioneer>=0.20",
7171
"openai>=1.78.1",
7272
"pre-commit",
73-
"e2b",
73+
# Reproducible v2 sandbox tests without changing the optional tracing stack.
74+
"e2b==2.34.0",
7475
"pytest-cov",
7576
"pytest-xdist",
7677
"docker==7.1.0",

‎scripts/run_sharded_tests.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ if [ "$DRY_RUN" = "--dry-run" ]; then
7171
fi
7272

7373
# Run tests for this shard
74+
# Keep evaluation tests and their shared-processor verification in the same worker.
7475
# shellcheck disable=SC2086
7576
exec pytest \
7677
-n auto \
78+
--dist=loadfile \
7779
--ignore=eval_protocol/benchmarks/ \
7880
--ignore=eval_protocol/quickstart/ \
7981
-v --durations=10 \

‎tests/klavis_helpers.py‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Helpers shared by live Klavis evaluations and offline tests."""
2+
3+
import json
4+
import logging
5+
import os
6+
7+
from openai import AsyncOpenAI
8+
from pydantic import BaseModel
9+
10+
from eval_protocol.models import EvaluateResult, EvaluationRow, Message
11+
12+
logger = logging.getLogger(__name__)
13+
MODEL = os.environ.get("EP_KLAVIS_MODEL", "accounts/fireworks/models/gpt-oss-120b")
14+
15+
16+
class ResponseFormat(BaseModel):
17+
score: float
18+
19+
20+
async def judge_klavis_response(row: EvaluationRow) -> EvaluationRow:
21+
"""Judge a completed Klavis rollout; service errors must fail the live test."""
22+
ground_truth = row.ground_truth
23+
24+
async with AsyncOpenAI(
25+
api_key=os.environ["FIREWORKS_API_KEY"], base_url="https://api.fireworks.ai/inference/v1"
26+
) as client:
27+
response = await client.chat.completions.create(
28+
model=MODEL,
29+
messages=[
30+
{
31+
"role": "system",
32+
"content": "You are judging the output of the model versus the ground truth. Return score = 1 if the output contains the ground truth, 0 otherwise.",
33+
},
34+
{
35+
"role": "user",
36+
"content": f"Final model output: {row.messages[-1].content}\nGround truth: {ground_truth}",
37+
},
38+
],
39+
response_format={
40+
"type": "json_schema",
41+
"json_schema": {"name": "ResponseFormat", "schema": ResponseFormat.model_json_schema()},
42+
},
43+
)
44+
response_text = response.choices[0].message.content
45+
logger.info("response_text: %s", response_text)
46+
try:
47+
parsed = json.loads(response_text or "{}")
48+
score = parsed.get("score", 0.0)
49+
except (json.JSONDecodeError, TypeError):
50+
logger.warning("Failed to parse response as JSON: %s", response_text)
51+
score = 0.0
52+
53+
row.evaluation_result = EvaluateResult(score=score, reason=response_text)
54+
return row
55+
56+
57+
def klavis_gmail_sandbox_dataset_adapter(rows: list[dict]) -> list[EvaluationRow]:
58+
"""Dataset adapter for sandbox JSONL rows.
59+
60+
Supports the new schema:
61+
- initialize_data: dict (passed to Klavis sandbox initializer)
62+
- messages: str (task instruction)
63+
- ground_truth: dict (expected final sandbox state)
64+
65+
"""
66+
adapted: list[EvaluationRow] = []
67+
system_prompt = (
68+
"You are a helpful assistant with access to Gmail. "
69+
"You can send emails, draft emails, and manage messages, etc."
70+
)
71+
72+
for r in rows:
73+
if isinstance(r.get("messages"), str) and "initialize_data" in r:
74+
init_data = r.get("initialize_data") or {}
75+
task = r.get("messages") or ""
76+
ground_truth = r.get("ground_truth")
77+
78+
row = EvaluationRow(
79+
messages=[
80+
Message(role="system", content=system_prompt),
81+
Message(role="user", content=task),
82+
],
83+
ground_truth=ground_truth,
84+
)
85+
row.input_metadata.session_data = {
86+
"initialize_data": init_data,
87+
"task": task,
88+
}
89+
adapted.append(row)
90+
else:
91+
adapted.append(EvaluationRow(**r))
92+
93+
return adapted
Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,25 @@
1-
from eval_protocol.models import EvaluateResult, EvaluationRow, Message
2-
from eval_protocol.pytest import AgentRolloutProcessor, evaluation_test
3-
from openai import AsyncOpenAI
4-
import json
5-
from pydantic import BaseModel
6-
import logging
7-
8-
logger = logging.getLogger(__name__)
91
import os
102

3+
import pytest
114

12-
class ResponseFormat(BaseModel):
13-
score: float
14-
15-
16-
"""
17-
You should copy https://painted-tennis-ebc.notion.site/MCPMark-Source-Hub-23181626b6d7805fb3a7d59c63033819
18-
into your Notion for the notion test.
19-
"""
5+
from eval_protocol.models import EvaluationRow
6+
from eval_protocol.pytest import AgentRolloutProcessor
7+
from eval_protocol.pytest.evaluation_test import evaluation_test
8+
from tests.klavis_helpers import MODEL, judge_klavis_response
209

2110

11+
# Live datasets require a Klavis connection to Gmail/Notion and their seeded test data.
12+
@pytest.mark.skipif(
13+
not (os.environ.get("KLAVIS_AUTH_TOKEN") and os.environ.get("FIREWORKS_API_KEY")),
14+
reason="Live Klavis MCP test requires KLAVIS_AUTH_TOKEN and FIREWORKS_API_KEY",
15+
)
2216
@evaluation_test(
2317
input_dataset=["tests/pytest/datasets/klavis_mcp_test.jsonl"],
2418
rollout_processor=AgentRolloutProcessor(),
25-
completion_params=[{"model": "fireworks_ai/accounts/fireworks/models/kimi-k2p5"}],
19+
completion_params=[{"model": f"fireworks_ai/{MODEL}"}],
2620
mode="pointwise",
2721
mcp_config_path="tests/pytest/mcp_configurations/klavis_strata_mcp.json",
2822
)
2923
async def test_pytest_klavis_mcp(row: EvaluationRow) -> EvaluationRow:
30-
ground_truth = row.ground_truth
31-
# check if the final messages contains the ground truth
32-
33-
async with AsyncOpenAI(
34-
api_key=os.environ["FIREWORKS_API_KEY"], base_url="https://api.fireworks.ai/inference/v1"
35-
) as client:
36-
response = await client.chat.completions.create(
37-
model="accounts/fireworks/models/kimi-k2p5",
38-
messages=[
39-
{
40-
"role": "system",
41-
"content": "You are judging the output of the model versus the ground truth. Return score = 1 if the output contains the ground truth, 0 otherwise.",
42-
},
43-
{
44-
"role": "user",
45-
"content": f"Final model output: {row.messages[-1].content}\nGround truth: {ground_truth}",
46-
},
47-
],
48-
response_format={
49-
"type": "json_schema",
50-
"json_schema": {"name": "ResponseFormat", "schema": ResponseFormat.model_json_schema()},
51-
},
52-
)
53-
response_text = response.choices[0].message.content
54-
logger.info("response_text: %s", response_text)
55-
try:
56-
parsed = json.loads(response_text or "{}")
57-
score = parsed.get("score", 0.0)
58-
except (json.JSONDecodeError, TypeError):
59-
logger.warning("Failed to parse response as JSON: %s", response_text)
60-
score = 0.0
61-
62-
row.evaluation_result = EvaluateResult(
63-
score=score,
64-
reason=response_text,
65-
)
66-
return row
24+
assert row.rollout_status.is_finished(), row.rollout_status
25+
return await judge_klavis_response(row)

‎tests/pytest/test_pytest_klavis_sandbox.py‎

Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,73 +2,43 @@
22
import logging
33
import os
44

5-
from eval_protocol.models import EvaluateResult, EvaluationRow, Message
6-
from eval_protocol.pytest import KlavisSandboxRolloutProcessor, evaluation_test
5+
import pytest
76
from openai import AsyncOpenAI
87
from pydantic import BaseModel
98

9+
from eval_protocol.models import EvaluateResult, EvaluationRow
10+
from eval_protocol.pytest import KlavisSandboxRolloutProcessor
11+
from eval_protocol.pytest.evaluation_test import evaluation_test
12+
from tests.klavis_helpers import MODEL, klavis_gmail_sandbox_dataset_adapter
13+
1014
logger = logging.getLogger(__name__)
1115

1216

1317
class ResponseFormat(BaseModel):
1418
score: float
15-
16-
17-
def klavis_gmail_sandbox_dataset_adapter(rows: list[dict]) -> list[EvaluationRow]:
18-
"""Dataset adapter for sandbox JSONL rows.
19-
20-
Supports the new schema:
21-
- initialize_data: dict (passed to Klavis sandbox initializer)
22-
- messages: str (task instruction)
23-
- ground_truth: dict (expected final sandbox state)
24-
25-
"""
26-
adapted: list[EvaluationRow] = []
27-
system_prompt = (
28-
"You are a helpful assistant with access to Gmail. "
29-
"You can send emails, draft emails, and manage messages, etc."
30-
)
31-
32-
for r in rows:
33-
if isinstance(r.get("messages"), str) and "initialize_data" in r:
34-
init_data = r.get("initialize_data") or {}
35-
task = r.get("messages") or ""
36-
ground_truth = r.get("ground_truth")
37-
38-
row = EvaluationRow(
39-
messages=[
40-
Message(role="system", content=system_prompt),
41-
Message(role="user", content=task),
42-
],
43-
ground_truth=ground_truth,
44-
)
45-
row.input_metadata.session_data = {
46-
"initialize_data": init_data,
47-
"task": task,
48-
}
49-
adapted.append(row)
50-
else:
51-
adapted.append(EvaluationRow(**r))
52-
53-
return adapted
5419

5520

21+
@pytest.mark.skipif(
22+
not (os.environ.get("KLAVIS_API_KEY") and os.environ.get("FIREWORKS_API_KEY")),
23+
reason="Live Klavis Gmail test requires KLAVIS_API_KEY and FIREWORKS_API_KEY",
24+
)
5625
@evaluation_test(
5726
input_dataset=["tests/pytest/datasets/klavis_gmail_sandbox_test.jsonl"],
5827
rollout_processor=KlavisSandboxRolloutProcessor(
5928
server_name="gmail",
6029
),
61-
completion_params=[{"model": "fireworks_ai/accounts/fireworks/models/kimi-k2p5"}],
30+
completion_params=[{"model": f"fireworks_ai/{MODEL}"}],
6231
mode="pointwise",
6332
dataset_adapter=klavis_gmail_sandbox_dataset_adapter,
6433
)
6534
async def test_pytest_gmail_sandbox(row: EvaluationRow) -> EvaluationRow:
6635
"""
6736
Evaluate Gmail sandbox results by comparing with ground truth using LLM judge.
68-
37+
6938
The sandbox data is exported after agent execution and compared with expected output.
7039
Sandbox data is available in row.execution_metadata.extra["sandbox_data"].
7140
"""
41+
assert row.rollout_status.is_finished(), row.rollout_status
7242
ground_truth = row.ground_truth
7343
sandbox_data = row.execution_metadata.extra.get("sandbox_data", {}) if row.execution_metadata.extra else {}
7444
final_message = row.messages[-1].content if row.messages else ""
@@ -83,11 +53,10 @@ async def test_pytest_gmail_sandbox(row: EvaluationRow) -> EvaluationRow:
8353
async with AsyncOpenAI(
8454
api_key=os.environ["FIREWORKS_API_KEY"], base_url="https://api.fireworks.ai/inference/v1"
8555
) as client:
86-
8756
evaluation_prompt = f"""You are evaluating an AI agent's performance on a Gmail sandbox task.
8857
8958
Task:
90-
{task or (row.messages[-1].content if row.messages else 'N/A')}
59+
{task or (row.messages[-1].content if row.messages else "N/A")}
9160
9261
Initial Gmail Sandbox State (initialize_data):
9362
{json.dumps(initialize_data, indent=2, default=str)}
@@ -110,7 +79,7 @@ async def test_pytest_gmail_sandbox(row: EvaluationRow) -> EvaluationRow:
11079

11180
try:
11281
response = await client.chat.completions.create(
113-
model="accounts/fireworks/models/kimi-k2p5",
82+
model=MODEL,
11483
messages=[
11584
{
11685
"role": "system",
@@ -134,9 +103,6 @@ async def test_pytest_gmail_sandbox(row: EvaluationRow) -> EvaluationRow:
134103
row.evaluation_result = EvaluateResult(score=score)
135104
except Exception as e:
136105
logger.error(f"Error during LLM evaluation: {str(e)}", exc_info=True)
137-
row.evaluation_result = EvaluateResult(
138-
score=0.0,
139-
reason=f"Evaluation error: {str(e)}",
140-
)
106+
raise
141107

142108
return row

0 commit comments

Comments
 (0)