-
Notifications
You must be signed in to change notification settings - Fork 9
Add score trace URL to evaluation run #558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
87c8e40
01ce874
8736311
62d0673
0ac775e
6e3d12e
afc0d70
788f3ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Add score_trace_url to evaluation_run | ||
|
|
||
| Revision ID: 043 | ||
| Revises: 042 | ||
| Create Date: 2026-01-24 19:34:46.763908 | ||
|
|
||
| """ | ||
| from alembic import op | ||
| import sqlalchemy as sa | ||
| import sqlmodel.sql.sqltypes | ||
|
|
||
|
|
||
| revision = "043" | ||
| down_revision = "042" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade(): | ||
| op.add_column( | ||
| "evaluation_run", | ||
| sa.Column( | ||
| "score_trace_url", | ||
| sqlmodel.sql.sqltypes.AutoString(), | ||
| nullable=True, | ||
| comment="S3 URL where per-trace evaluation scores are stored", | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def downgrade(): | ||
| op.drop_column("evaluation_run", "score_trace_url") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,10 +84,7 @@ def __init__(self, content: bytes): | |
|
|
||
|
|
||
| def upload_jsonl_to_object_store( | ||
| storage: CloudStorage, | ||
| results: list[dict], | ||
| filename: str, | ||
| subdirectory: str, | ||
| storage: CloudStorage, results: list[dict], filename: str, subdirectory: str | ||
| ) -> str | None: | ||
| """ | ||
| Upload JSONL (JSON Lines) content to object store. | ||
|
|
@@ -114,8 +111,7 @@ def upload_jsonl_to_object_store( | |
| # Create file path | ||
| file_path = Path(subdirectory) / filename | ||
|
|
||
| # Convert results to JSONL | ||
| jsonl_content = "\n".join([json.dumps(result) for result in results]) | ||
| jsonl_content = json.dumps(results, ensure_ascii=False) | ||
| content_bytes = jsonl_content.encode("utf-8") | ||
|
|
||
| # Create UploadFile-like object | ||
|
|
@@ -152,6 +148,37 @@ def upload_jsonl_to_object_store( | |
| return None | ||
|
|
||
|
|
||
| def load_json_from_object_store(storage: CloudStorage, url: str) -> list | None: | ||
| logger.info(f"[load_json_from_object_store] Loading JSON from '{url}") | ||
|
Comment on lines
+151
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix log message formatting. The opening quote isn’t closed, which makes the log line confusing. ✍️ Suggested update- logger.info(f"[load_json_from_object_store] Loading JSON from '{url}")
+ logger.info(f"[load_json_from_object_store] Loading JSON from '{url}'")🤖 Prompt for AI Agents |
||
| try: | ||
| body = storage.stream(url) | ||
| content = body.read() | ||
|
|
||
| data = json.loads(content.decode("utf-8")) | ||
|
|
||
| logger.info( | ||
| f"[load_json_from_object_store] Download successful | " | ||
| f"url='{url}', size={len(content)} bytes" | ||
| ) | ||
| return data | ||
| except CloudStorageError as e: | ||
| logger.warning( | ||
| f"[load_json_from_object_store] failed to load JSON from '{url}': {e}", | ||
| ) | ||
| return None | ||
| except json.JSONDecodeError as e: | ||
| logger.warning( | ||
| f"[load_json_from_object_store] JSON decode error loading JSON from '{url}': {e}", | ||
| ) | ||
| return None | ||
| except Exception as e: | ||
| logger.warning( | ||
| f"[load_json_from_object_store] unexpected error loading JSON from '{url}': {e}", | ||
| exc_info=True, | ||
| ) | ||
| return None | ||
|
|
||
|
|
||
| def generate_timestamped_filename(base_name: str, extension: str = "csv") -> str: | ||
| """ | ||
| Generate a filename with timestamp. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| """Tests for storage_utils.py - upload and load functions for object store.""" | ||
|
|
||
| import json | ||
| from io import BytesIO | ||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest | ||
|
|
||
| from app.core.cloud.storage import CloudStorageError | ||
| from app.core.storage_utils import ( | ||
| load_json_from_object_store, | ||
| upload_jsonl_to_object_store, | ||
| ) | ||
|
|
||
|
|
||
| class TestUploadJsonlToObjectStore: | ||
| """Test uploading JSON content to object store.""" | ||
|
|
||
| def test_upload_success(self) -> None: | ||
| """Verify successful upload returns URL and content is correct.""" | ||
| mock_storage = MagicMock() | ||
| mock_storage.put.return_value = "s3://bucket/path/traces.json" | ||
|
|
||
| results = [{"trace_id": "t1", "score": 0.9}] | ||
|
|
||
| url = upload_jsonl_to_object_store( | ||
| storage=mock_storage, | ||
| results=results, | ||
| filename="traces.json", | ||
| subdirectory="evaluations/score/70", | ||
| ) | ||
|
|
||
| assert url == "s3://bucket/path/traces.json" | ||
| mock_storage.put.assert_called_once() | ||
|
|
||
| # Verify content is valid JSON | ||
| call_args = mock_storage.put.call_args | ||
| upload_file = call_args.kwargs.get("source") | ||
| content = upload_file.file.read().decode("utf-8") | ||
| assert json.loads(content) == results | ||
|
|
||
| def test_upload_returns_none_on_error(self) -> None: | ||
| """Verify returns None on CloudStorageError.""" | ||
| mock_storage = MagicMock() | ||
| mock_storage.put.side_effect = CloudStorageError("Upload failed") | ||
|
|
||
| url = upload_jsonl_to_object_store( | ||
| storage=mock_storage, | ||
| results=[{"id": 1}], | ||
| filename="test.json", | ||
| subdirectory="test", | ||
| ) | ||
|
|
||
| assert url is None | ||
|
|
||
|
Comment on lines
+16
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add parameter type hints to test methods. Annotate ✍️ Suggested update- def test_upload_success(self) -> None:
+ def test_upload_success(self: "TestUploadJsonlToObjectStore") -> None:
"""Verify successful upload returns URL and content is correct."""
mock_storage = MagicMock()
mock_storage.put.return_value = "s3://bucket/path/traces.json"
@@
- def test_upload_returns_none_on_error(self) -> None:
+ def test_upload_returns_none_on_error(
+ self: "TestUploadJsonlToObjectStore",
+ ) -> None:
"""Verify returns None on CloudStorageError."""
mock_storage = MagicMock()
mock_storage.put.side_effect = CloudStorageError("Upload failed")As per coding guidelines, **/*.py: Always add type hints to all function parameters and return values in Python code. 🤖 Prompt for AI Agents |
||
|
|
||
| class TestLoadJsonFromObjectStore: | ||
| """Test loading JSON from object store.""" | ||
|
|
||
| def test_load_success(self) -> None: | ||
| """Verify successful load returns parsed JSON.""" | ||
| mock_storage = MagicMock() | ||
| test_data = [{"id": 1, "value": "test"}] | ||
| mock_storage.stream.return_value = BytesIO( | ||
| json.dumps(test_data).encode("utf-8") | ||
| ) | ||
|
|
||
| result = load_json_from_object_store( | ||
| storage=mock_storage, | ||
| url="s3://bucket/path/file.json", | ||
| ) | ||
|
|
||
| assert result == test_data | ||
| mock_storage.stream.assert_called_once_with("s3://bucket/path/file.json") | ||
|
|
||
| def test_load_returns_none_on_error(self) -> None: | ||
| """Verify returns None on CloudStorageError.""" | ||
| mock_storage = MagicMock() | ||
| mock_storage.stream.side_effect = CloudStorageError("Download failed") | ||
|
|
||
| result = load_json_from_object_store( | ||
| storage=mock_storage, | ||
| url="s3://bucket/file.json", | ||
| ) | ||
|
|
||
| assert result is None | ||
|
|
||
| def test_load_returns_none_on_invalid_json(self) -> None: | ||
| """Verify returns None on invalid JSON content.""" | ||
| mock_storage = MagicMock() | ||
| mock_storage.stream.return_value = BytesIO(b"not valid json") | ||
|
|
||
| result = load_json_from_object_store( | ||
| storage=mock_storage, | ||
| url="s3://bucket/file.json", | ||
| ) | ||
|
|
||
| assert result is None | ||
|
Comment on lines
+57
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add parameter type hints to test methods. Same type-hint requirement applies to this class’ methods. ✍️ Suggested update- def test_load_success(self) -> None:
+ def test_load_success(self: "TestLoadJsonFromObjectStore") -> None:
"""Verify successful load returns parsed JSON."""
mock_storage = MagicMock()
test_data = [{"id": 1, "value": "test"}]
@@
- def test_load_returns_none_on_error(self) -> None:
+ def test_load_returns_none_on_error(
+ self: "TestLoadJsonFromObjectStore",
+ ) -> None:
"""Verify returns None on CloudStorageError."""
mock_storage = MagicMock()
mock_storage.stream.side_effect = CloudStorageError("Download failed")
@@
- def test_load_returns_none_on_invalid_json(self) -> None:
+ def test_load_returns_none_on_invalid_json(
+ self: "TestLoadJsonFromObjectStore",
+ ) -> None:
"""Verify returns None on invalid JSON content."""
mock_storage = MagicMock()
mock_storage.stream.return_value = BytesIO(b"not valid json")As per coding guidelines, **/*.py: Always add type hints to all function parameters and return values in Python code. 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add return type hints to migration functions.
Both
upgrade()anddowngrade()should declare-> Noneto comply with the type-hinting rule.✍️ Suggested update
As per coding guidelines, **/*.py: Always add type hints to all function parameters and return values in Python code.
📝 Committable suggestion
🤖 Prompt for AI Agents