Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, path: Path) -> None:
async def _write(self, payload: dict) -> None:
async with self._lock:
self._path.parent.mkdir(parents=True, exist_ok=True)
with self._path.open("a") as f: # noqa: ASYNC230
with self._path.open("a", encoding="utf-8") as f: # noqa: ASYNC230
f.write(json.dumps(payload) + "\n")

async def emit_partial(self, run_id: UUID, results: list[Result], attempt: int) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/api/debug_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _collect_temp_files(session_ids: set[str] | None = None) -> dict[str, str]:
if sid not in session_ids:
continue
try:
with open(path) as f:
with open(path, encoding="utf-8") as f:
files[basename] = f.read()
except OSError:
logger.debug("Could not read temp file %s", path)
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _load_eval_set_dict(path: str | None) -> dict | None:
if not path:
return None
try:
with open(path) as f:
with open(path, encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
logger.warning("could not re-read eval_set file at %s for persistence", path)
Expand Down
6 changes: 3 additions & 3 deletions src/agentevals/api/streaming_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ async def evaluate_sessions(

import tempfile

eval_set_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
eval_set_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8")
json.dump(eval_set_response.data.eval_set, eval_set_file)
eval_set_file.close()

Expand Down Expand Up @@ -334,7 +334,7 @@ async def prepare_evaluation(
temp_dir = tempfile.gettempdir()

eval_set_file = os.path.join(temp_dir, f"eval_set_{request.golden_session_id}.json")
with open(eval_set_file, "w") as f: # noqa: ASYNC230
with open(eval_set_file, "w", encoding="utf-8") as f: # noqa: ASYNC230
json.dump(eval_set_response.data.eval_set, f)

trace_files = []
Expand Down Expand Up @@ -415,7 +415,7 @@ async def get_trace(
# evaluate-sessions path. Serializing spans here independently would
# drop the resource attribute and lose agent identity on the run.
trace_file = await manager._save_spans_to_temp_file(session)
with open(trace_file) as f: # noqa: ASYNC230
with open(trace_file, encoding="utf-8") as f: # noqa: ASYNC230
trace_content = f.read()
num_spans = sum(1 for line in trace_content.splitlines() if line.strip())

Expand Down
4 changes: 2 additions & 2 deletions src/agentevals/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,8 @@ def migrate_create(name: str, output_dir: str | None) -> None:
f"-- Migration {next_version:06d}: {name}\n"
"-- Once tagged in a release this file is immutable. Fix bugs by adding a NEW migration.\n\n"
)
up_path.write_text(header)
down_path.write_text(header)
up_path.write_text(header, encoding="utf-8")
down_path.write_text(header, encoding="utf-8")
click.echo(f"Created {up_path}")
click.echo(f"Created {down_path}")

Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/eval_config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def load_eval_config(path: str | Path) -> EvalRunConfig:
if not path.exists():
raise FileNotFoundError(f"Eval config file not found: {path}")

with open(path) as f:
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f)

if not isinstance(data, dict):
Expand Down
5 changes: 3 additions & 2 deletions src/agentevals/evaluator/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _read_cache(key: str, ttl: int = _CACHE_TTL_SECONDS) -> list[EvaluatorInfo]
if not cache_file.exists():
return None
try:
data = json.loads(cache_file.read_text())
data = json.loads(cache_file.read_text(encoding="utf-8"))
if time.time() - data.get("ts", 0) > ttl:
return None
return [EvaluatorInfo(**item) for item in data["evaluators"]]
Expand All @@ -83,7 +83,8 @@ def _write_cache(key: str, evaluators: list[EvaluatorInfo]) -> None:
"ts": time.time(),
"evaluators": [asdict(g) for g in evaluators],
}
)
),
encoding="utf-8",
)
except Exception:
pass
Expand Down
8 changes: 6 additions & 2 deletions src/agentevals/evaluator/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ def _venv_key(evaluator_path: Path) -> str:

def _is_venv_valid(venv_dir: Path, req_hash: str) -> bool:
hash_file = venv_dir / _HASH_FILE
return _venv_python(venv_dir).exists() and hash_file.exists() and hash_file.read_text().strip() == req_hash
return (
_venv_python(venv_dir).exists()
and hash_file.exists()
and hash_file.read_text(encoding="utf-8").strip() == req_hash
)


def _create_venv(venv_dir: Path, uv: str | None) -> None:
Expand Down Expand Up @@ -104,7 +108,7 @@ def ensure_venv(evaluator_path: Path) -> Path | None:
stderr = exc.stderr.decode() if isinstance(exc.stderr, bytes) else (exc.stderr or "")
raise RuntimeError(f"Failed to set up environment for evaluator '{evaluator_path.stem}': {stderr}") from exc

(venv_dir / _HASH_FILE).write_text(req_hash)
(venv_dir / _HASH_FILE).write_text(req_hash, encoding="utf-8")
logger.info("Environment ready for '%s'", evaluator_path.stem)
return _venv_python(venv_dir)

Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/loader/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def detect_format(path: str) -> str | None:
load can fall back to a default.
"""
try:
with open(path) as f:
with open(path, encoding="utf-8") as f:
if path.lower().endswith(".jsonl"):
return OTLP_JSON
data = json.load(f)
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/loader/jaeger.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def format_name(self) -> str:
return "jaeger-json"

def load(self, source: str) -> list[Trace]:
with open(source) as f:
with open(source, encoding="utf-8") as f:
raw = json.load(f)

if not isinstance(raw, dict) or "data" not in raw:
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/loader/otlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def format_name(self) -> str:

def load(self, source: str) -> list[Trace]:
"""Load OTLP JSON file or JSONL (one span per line)."""
with open(source) as f:
with open(source, encoding="utf-8") as f:
content = f.read().strip()

if not content:
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ async def summarize_session(session_id: str) -> SummarizeSessionResponse:

raw = await _post("/api/streaming/get-trace", {"session_id": session_id})

with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8") as f:
f.write(raw["traceContent"])
tmp_path = f.name

Expand Down
4 changes: 2 additions & 2 deletions src/agentevals/run/fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class InlineTraceFetcher:
async def fetch(self, target: TraceTarget, context: dict) -> list[Trace]:
if not target.inline:
raise ValueError("InlineTraceFetcher requires target.inline to be set")
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as f:
json.dump(target.inline, f)
path = Path(f.name)
try:
Expand All @@ -61,7 +61,7 @@ async def fetch(self, target: TraceTarget, context: dict) -> list[Trace]:
resp = await client.get(url, headers=headers)
resp.raise_for_status()
payload = resp.json()
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as f:
json.dump(payload, f)
path = Path(f.name)
try:
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/run/sinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def __init__(self, path: str | Path) -> None:
async def _write(self, payload: dict) -> None:
async with self._lock:
self._path.parent.mkdir(parents=True, exist_ok=True)
with self._path.open("a") as f: # noqa: ASYNC230
with self._path.open("a", encoding="utf-8") as f: # noqa: ASYNC230
f.write(json.dumps(payload) + "\n")

async def emit_partial(self, run_id: UUID, results: list[Result], attempt: int) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class RunResult(BaseModel):


def load_eval_set(path: str) -> EvalSet:
with open(path) as f:
with open(path, encoding="utf-8") as f:
data = json.load(f)
return EvalSet.model_validate(data)

Expand Down
2 changes: 1 addition & 1 deletion src/agentevals/storage/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def _read_dsn_from_env() -> str | None:
file_path = os.environ.get("AGENTEVALS_DATABASE_URL_FILE")
if file_path:
try:
with open(file_path) as f:
with open(file_path, encoding="utf-8") as f:
return f.read().strip() or None
except OSError as exc:
raise ValueError(f"AGENTEVALS_DATABASE_URL_FILE={file_path!r} is unreadable: {exc}") from exc
Expand Down
4 changes: 2 additions & 2 deletions src/agentevals/streaming/ws_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ async def _save_spans_to_temp_file(self, session: TraceSession) -> Path:
# span here; otherwise it's lost on reload and runs can't group by agent.
service_name = (session.metadata or {}).get(OTEL_SERVICE_NAME)

with open(temp_file, "w") as f: # noqa: ASYNC230
with open(temp_file, "w", encoding="utf-8") as f: # noqa: ASYNC230
for span in enriched_spans:
span_copy = span.copy()
span_copy["traceId"] = session.trace_id
Expand Down Expand Up @@ -695,7 +695,7 @@ async def _extract_invocations(self, session: TraceSession) -> list[dict]:
- modelInfo: Model metadata (model name, tokens, etc.)
"""
try:
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False)
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8")

has_genai_spans = any(
span.get("attributes", [])
Expand Down
12 changes: 6 additions & 6 deletions tests/run/test_sinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ async def test_emits_partial_and_final(self, tmp_path):
await sink.emit_partial(run_id, [_result(run_id)], attempt=1)
await sink.emit_final(run_id, {"trace_count": 1}, attempt=1)
await sink.emit_error(run_id, "boom", attempt=1)
lines = path.read_text().strip().splitlines()
lines = path.read_text(encoding="utf-8").strip().splitlines()
assert len(lines) == 3
partial = json.loads(lines[0])
assert partial["phase"] == "partial"
Expand Down Expand Up @@ -249,7 +249,7 @@ def _factory(spec: dict) -> FileSink:
fanout = build_sinks([{"kind": "plugin_file", "path": str(path)}])
run_id = uuid4()
await fanout.emit_final(run_id, {"ok": True}, attempt=1)
assert json.loads(path.read_text().strip())["summary"] == {"ok": True}
assert json.loads(path.read_text(encoding="utf-8").strip())["summary"] == {"ok": True}

async def test_programmatic_registration_overrides_builtin(self, isolated_sink_plugins):
finals: list[dict] = []
Expand Down Expand Up @@ -299,7 +299,7 @@ def file_factory(spec: dict) -> FileSink:
ep.load.assert_called_once_with()
run_id = uuid4()
await fanout.emit_final(run_id, {"via": "ep"}, attempt=1)
assert json.loads(path.read_text().strip())["summary"] == {"via": "ep"}
assert json.loads(path.read_text(encoding="utf-8").strip())["summary"] == {"via": "ep"}

async def test_builtin_kind_does_not_load_colliding_entry_point(self, isolated_sink_plugins, capsys):
ep = MagicMock()
Expand Down Expand Up @@ -350,7 +350,7 @@ async def test_missing_plugin_sink_does_not_break_fanout(self, isolated_sink_plu
assert any("unknown sink kind 'demo_ndjson'" in r.getMessage() for r in caplog.records)
run_id = uuid4()
await fanout.emit_final(run_id, {"ok": True}, attempt=1)
assert json.loads(path.read_text().strip())["summary"] == {"ok": True}
assert json.loads(path.read_text(encoding="utf-8").strip())["summary"] == {"ok": True}


def _demo_example_sink_installed() -> bool:
Expand All @@ -372,7 +372,7 @@ async def test_path_dot_writes_default_ndjson(self, tmp_path, monkeypatch):
sink = create_demo_sink({"path": "."})
await sink.emit_final(uuid4(), {"x": 1}, attempt=1)
out = tmp_path / "agentevals-demo-sink.ndjson"
assert json.loads(out.read_text().strip())["summary"] == {"x": 1}
assert json.loads(out.read_text(encoding="utf-8").strip())["summary"] == {"x": 1}

async def test_existing_directory_appends_default_filename(self, tmp_path):
d = tmp_path / "outdir"
Expand All @@ -390,7 +390,7 @@ async def test_directory_with_custom_filename(self, tmp_path):

sink = create_demo_sink({"path": str(d), "filename": "runs.jsonl"})
await sink.emit_final(uuid4(), {"n": 2}, attempt=1)
assert json.loads((d / "runs.jsonl").read_text().strip())["summary"] == {"n": 2}
assert json.loads((d / "runs.jsonl").read_text(encoding="utf-8").strip())["summary"] == {"n": 2}


class TestSinkFanoutErrorIsolation:
Expand Down
2 changes: 1 addition & 1 deletion tests/storage/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_from_env_reads_postgres(self, monkeypatch):

def test_from_env_url_file_takes_precedence(self, tmp_path, monkeypatch):
dsn_file = tmp_path / "dsn"
dsn_file.write_text("postgresql://from-file/db\n")
dsn_file.write_text("postgresql://from-file/db\n", encoding="utf-8")
monkeypatch.setenv("AGENTEVALS_STORAGE_BACKEND", "postgres")
monkeypatch.setenv("AGENTEVALS_DATABASE_URL", "postgresql://from-env/db")
monkeypatch.setenv("AGENTEVALS_DATABASE_URL_FILE", str(dsn_file))
Expand Down
4 changes: 3 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,9 @@ def test_download_missing(self):
assert resp.status_code == 404

def test_download_success(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, dir=tempfile.gettempdir()) as f:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, dir=tempfile.gettempdir(), encoding="utf-8"
) as f:
f.write('{"test": true}')
fname = os.path.basename(f.name)

Expand Down
10 changes: 6 additions & 4 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def fake_server_factory(config):

def test_run_preserves_config_file_settings_when_flags_omitted(monkeypatch, tmp_path):
trace_file = tmp_path / "trace.json"
trace_file.write_text("{}")
trace_file.write_text("{}", encoding="utf-8")
config_file = tmp_path / "eval.yaml"
config_file.write_text(
textwrap.dedent(
Expand All @@ -105,7 +105,8 @@ def test_run_preserves_config_file_settings_when_flags_omitted(monkeypatch, tmp_
trace_format: otlp-json
output: json
"""
)
),
encoding="utf-8",
)

captured = {}
Expand All @@ -129,7 +130,7 @@ async def fake_run_evaluation(config):

def test_run_merges_explicit_metrics_with_config_file(monkeypatch, tmp_path):
trace_file = tmp_path / "trace.json"
trace_file.write_text("{}")
trace_file.write_text("{}", encoding="utf-8")
config_file = tmp_path / "eval.yaml"
config_file.write_text(
textwrap.dedent(
Expand All @@ -141,7 +142,8 @@ def test_run_merges_explicit_metrics_with_config_file(monkeypatch, tmp_path):
type: code
path: ./examples/custom_evaluators/tool_call_checker.py
"""
)
),
encoding="utf-8",
)

captured = {}
Expand Down
3 changes: 2 additions & 1 deletion tests/test_eval_config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def test_load_eval_config_rejects_legacy_keys(tmp_path):
trace_format: otlp-json
output: json
"""
)
),
encoding="utf-8",
)

with pytest.raises(ValueError, match="Legacy eval config keys are no longer supported"):
Expand Down
14 changes: 7 additions & 7 deletions tests/test_jaeger_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_format_name(self, loader):

def test_load_minimal(self, loader, minimal_jaeger_json, tmp_path):
path = tmp_path / "test.json"
path.write_text(json.dumps(minimal_jaeger_json))
path.write_text(json.dumps(minimal_jaeger_json), encoding="utf-8")

traces = loader.load(str(path))
assert len(traces) == 1
Expand All @@ -63,7 +63,7 @@ def test_load_minimal(self, loader, minimal_jaeger_json, tmp_path):

def test_span_tree_structure(self, loader, minimal_jaeger_json, tmp_path):
path = tmp_path / "test.json"
path.write_text(json.dumps(minimal_jaeger_json))
path.write_text(json.dumps(minimal_jaeger_json), encoding="utf-8")

trace = loader.load(str(path))[0]

Expand All @@ -79,7 +79,7 @@ def test_span_tree_structure(self, loader, minimal_jaeger_json, tmp_path):

def test_tags_parsed(self, loader, minimal_jaeger_json, tmp_path):
path = tmp_path / "test.json"
path.write_text(json.dumps(minimal_jaeger_json))
path.write_text(json.dumps(minimal_jaeger_json), encoding="utf-8")

trace = loader.load(str(path))[0]
root = trace.root_spans[0]
Expand All @@ -89,22 +89,22 @@ def test_tags_parsed(self, loader, minimal_jaeger_json, tmp_path):

def test_invalid_format_raises(self, loader, tmp_path):
path = tmp_path / "bad.json"
path.write_text(json.dumps({"not_data": []}))
path.write_text(json.dumps({"not_data": []}), encoding="utf-8")

with pytest.raises(ValueError, match="Invalid Jaeger JSON format"):
loader.load(str(path))

def test_empty_spans_skipped(self, loader, tmp_path):
data = {"data": [{"traceID": "empty", "spans": []}]}
path = tmp_path / "empty.json"
path.write_text(json.dumps(data))
path.write_text(json.dumps(data), encoding="utf-8")

traces = loader.load(str(path))
assert len(traces) == 0

def test_find_spans_by_operation(self, loader, minimal_jaeger_json, tmp_path):
path = tmp_path / "test.json"
path.write_text(json.dumps(minimal_jaeger_json))
path.write_text(json.dumps(minimal_jaeger_json), encoding="utf-8")

trace = loader.load(str(path))[0]
found = trace.find_spans_by_operation("child")
Expand All @@ -113,7 +113,7 @@ def test_find_spans_by_operation(self, loader, minimal_jaeger_json, tmp_path):

def test_find_spans_by_tag(self, loader, minimal_jaeger_json, tmp_path):
path = tmp_path / "test.json"
path.write_text(json.dumps(minimal_jaeger_json))
path.write_text(json.dumps(minimal_jaeger_json), encoding="utf-8")

trace = loader.load(str(path))[0]
found = trace.find_spans_by_tag("key1", "val1")
Expand Down
Loading