-
Notifications
You must be signed in to change notification settings - Fork 77
fix: persist feedback model and ruleset versions #47
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
Changes from all commits
7e147ac
2afbcc9
1556dbd
bf8597b
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 |
|---|---|---|
|
|
@@ -984,6 +984,65 @@ def validate_feedback(self) -> "FeedbackRequest": | |
| return self | ||
|
|
||
|
|
||
| def _coerce_json_dict(value: Any) -> Dict[str, Any]: | ||
| """Convert JSON-ish values to a dict for metadata lookups.""" | ||
| if isinstance(value, dict): | ||
| return value | ||
| if isinstance(value, str): | ||
| try: | ||
| parsed = json.loads(value) | ||
| except (TypeError, ValueError): | ||
| return {} | ||
| return parsed if isinstance(parsed, dict) else {} | ||
| return {} | ||
|
|
||
|
|
||
| def _extract_feedback_versions(payload: Optional[Dict[str, Any]]) -> tuple[Optional[str], Optional[str]]: | ||
| """Extract model and ruleset versions from a scan payload when available.""" | ||
| if not isinstance(payload, dict): | ||
| return None, None | ||
|
|
||
| metadata = _coerce_json_dict(payload.get("metadata")) | ||
| summary = _coerce_json_dict(payload.get("summary")) | ||
| report_view_model_source = payload.get("report_view_model") | ||
| if not report_view_model_source: | ||
| report_view_model_source = summary.get("report_view_model") | ||
| report_view_model = _coerce_json_dict(report_view_model_source) | ||
| report_meta = _coerce_json_dict(report_view_model.get("meta")) | ||
|
|
||
| scoring_v2 = _coerce_json_dict(payload.get("scoring_v2")) | ||
| if not scoring_v2: | ||
| scoring_v2 = _coerce_json_dict(summary.get("scoring_v2")) | ||
|
|
||
| model_version = ( | ||
| summary.get("model_version") | ||
| or metadata.get("model_version") | ||
| or report_meta.get("model_version") | ||
| or summary.get("llm_model") | ||
| or metadata.get("llm_model") | ||
| ) | ||
|
|
||
| ruleset_version = ( | ||
| summary.get("ruleset_version") | ||
| or metadata.get("ruleset_version") | ||
| or report_meta.get("ruleset_version") | ||
| or scoring_v2.get("weights_version") | ||
| or scoring_v2.get("scoring_version") | ||
| ) | ||
|
|
||
| return model_version, ruleset_version | ||
|
|
||
|
|
||
| def _resolve_feedback_versions(scan_id: str) -> tuple[Optional[str], Optional[str]]: | ||
| """Look up the related scan payload and extract version metadata safely.""" | ||
| payload = scan_results.get(scan_id) | ||
| if isinstance(payload, dict): | ||
| return _extract_feedback_versions(payload) | ||
|
|
||
| db_payload = db.get_scan_result(scan_id) | ||
| return _extract_feedback_versions(db_payload) | ||
|
|
||
|
|
||
| class ReviewQueueClaimRequest(BaseModel): | ||
| """Request body for claiming a review queue item.""" | ||
| queue_item_id: str | ||
|
|
@@ -1504,6 +1563,16 @@ async def run_analysis_workflow(url: str, extension_id: str): | |
| if scoring_result.coverage_cap_reason is not None: | ||
| scoring_v2_payload["coverage_cap_reason"] = scoring_result.coverage_cap_reason | ||
|
|
||
| executive_summary = final_state.get("executive_summary") or {} | ||
| if isinstance(executive_summary, dict): | ||
| executive_summary = dict(executive_summary) | ||
| else: | ||
| executive_summary = {} | ||
| executive_summary.setdefault( | ||
| "ruleset_version", | ||
| scoring_v2_payload.get("weights_version") or scoring_v2_payload.get("scoring_version"), | ||
| ) | ||
|
Comment on lines
+1566
to
+1574
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.
If upstream already includes Patch sketch- executive_summary.setdefault(
- "ruleset_version",
- scoring_v2_payload.get("weights_version") or scoring_v2_payload.get("scoring_version"),
- )
+ resolved_ruleset_version = (
+ scoring_v2_payload.get("weights_version")
+ or scoring_v2_payload.get("scoring_version")
+ )
+ if resolved_ruleset_version and not executive_summary.get("ruleset_version"):
+ executive_summary["ruleset_version"] = resolved_ruleset_version🤖 Prompt for AI Agents |
||
|
|
||
| # Build scan results - sanitize complex objects to prevent circular references | ||
| raw_results = { | ||
| "extension_id": extension_id, | ||
|
|
@@ -1518,7 +1587,7 @@ async def run_analysis_workflow(url: str, extension_id: str): | |
| "webstore_analysis": analysis_results.get("webstore_analysis") or {}, | ||
| "virustotal_analysis": analysis_results.get("virustotal_analysis") or {}, | ||
| "entropy_analysis": analysis_results.get("entropy_analysis") or {}, | ||
| "summary": final_state.get("executive_summary") or {}, | ||
| "summary": executive_summary, | ||
| "impact_analysis": analysis_results.get("impact_analysis") or {}, | ||
| "privacy_compliance": analysis_results.get("privacy_compliance") or {}, | ||
| "extracted_path": _storage_relative_extracted_path(final_state.get("extension_dir")), | ||
|
|
@@ -2199,11 +2268,14 @@ async def submit_feedback(feedback: FeedbackRequest, http_request: Request): | |
| provide details about why it wasn't (false positive, score issues, etc.). | ||
| """ | ||
| user_id = _get_user_id(http_request) | ||
| if user_id == "anon": | ||
| user_id = None | ||
|
|
||
| # If helpful=true, ignore reason/suggested_score/comment | ||
| reason = None if feedback.helpful else (feedback.reason.value if feedback.reason else None) | ||
| suggested_score = None if feedback.helpful else feedback.suggested_score | ||
| comment = None if feedback.helpful else feedback.comment | ||
| model_version, ruleset_version = _resolve_feedback_versions(feedback.scan_id) | ||
|
|
||
| # Save feedback to database (SQLite or Supabase) | ||
| db.save_feedback( | ||
|
|
@@ -2213,8 +2285,8 @@ async def submit_feedback(feedback: FeedbackRequest, http_request: Request): | |
| suggested_score=suggested_score, | ||
| comment=comment, | ||
| user_id=user_id, | ||
| model_version=None, # TODO: Extract from scan result metadata | ||
| ruleset_version=None, # TODO: Extract from scan result metadata | ||
| model_version=model_version, | ||
| ruleset_version=ruleset_version, | ||
| ) | ||
|
|
||
| return {"ok": True} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,162 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from unittest.mock import MagicMock, patch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi.testclient import TestClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from extension_shield.api.main import app, scan_results | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.fixture | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def client() -> TestClient: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return TestClient(app) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_feedback_persists_versions_from_scan_payload(client: TestClient) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scan_id = "abcdefghijklmnopabcdefghijklmnop" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scan_results.pop(scan_id, None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scan_payload = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "summary": {"model_version": "gpt-4o"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "scoring_v2": {"scoring_version": "2.0.0", "weights_version": "v1"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with patch("extension_shield.api.main.db") as mock_db: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mock_db.get_scan_result = MagicMock(return_value=scan_payload) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mock_db.save_feedback = MagicMock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = client.post( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/api/feedback", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "scan_id": scan_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "helpful": False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "reason": "score_off", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "suggested_score": 72, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "comment": "Score feels too strict", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert response.status_code == 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mock_db.save_feedback.assert_called_once_with( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scan_id=scan_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| helpful=False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason="score_off", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| suggested_score=72, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| comment="Score feels too strict", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id=None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_version="gpt-4o", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ruleset_version="v1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+47
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. These assertions expect the wrong anonymous
Suggested fix- user_id=None,
+ user_id="anon",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_feedback_persists_versions_from_nested_summary_payload( | |
| client: TestClient, | |
| ) -> None: | |
| scan_id = "nestedabcdefghijnestedabcdefghij" | |
| scan_results.pop(scan_id, None) | |
| # Simulate a DB-style payload where scoring_v2 is embedded under summary | |
| scan_payload = { | |
| "summary": { | |
| "model_version": "gpt-4.1-mini", | |
| "scoring_v2": { | |
| "scoring_version": "2.1.0", | |
| "weights_version": "rules-v3", | |
| }, | |
| "report_view_model": { | |
| "meta": { | |
| "model_version": "legacy-model", | |
| "ruleset_version": "legacy-rules", | |
| } | |
| }, | |
| } | |
| } | |
| with patch("extension_shield.api.main.db") as mock_db: | |
| mock_db.get_scan_result = MagicMock(return_value=scan_payload) | |
| mock_db.save_feedback = MagicMock() | |
| response = client.post( | |
| "/api/feedback", | |
| json={ | |
| "scan_id": scan_id, | |
| "helpful": False, | |
| "reason": "score_off", | |
| "suggested_score": 85, | |
| "comment": "Nested summary payload", | |
| }, | |
| ) | |
| assert response.status_code == 200 | |
| mock_db.save_feedback.assert_called_once_with( | |
| scan_id=scan_id, | |
| helpful=False, | |
| reason="score_off", | |
| suggested_score=85, | |
| comment="Nested summary payload", | |
| user_id=None, | |
| model_version="gpt-4.1-mini", | |
| ruleset_version="rules-v3", | |
| ) |
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.
Include the existing file-backed scan fallback here too.
get_scan_results()can still load<scan_id>_results.jsonwhen the DB row is missing, but_resolve_feedback_versions()stops after memory/DB. That means feedback for file-only scans regresses toNoneagain even though the metadata is still available.Suggested fix
def _resolve_feedback_versions(scan_id: str) -> tuple[Optional[str], Optional[str]]: """Look up the related scan payload and extract version metadata safely.""" payload = scan_results.get(scan_id) if isinstance(payload, dict): return _extract_feedback_versions(payload) - db_payload = db.get_scan_result(scan_id) - return _extract_feedback_versions(db_payload) + try: + db_payload = db.get_scan_result(scan_id) + if isinstance(db_payload, dict): + return _extract_feedback_versions(db_payload) + except Exception as exc: + logger.debug("[FEEDBACK] Failed DB lookup for %s: %s", scan_id, exc) + + result_file = RESULTS_DIR / f"{scan_id}_results.json" + if result_file.exists(): + try: + with open(result_file, "r", encoding="utf-8") as f: + return _extract_feedback_versions(json.load(f)) + except (OSError, json.JSONDecodeError) as exc: + logger.debug("[FEEDBACK] Failed file lookup for %s: %s", scan_id, exc) + + return None, None🤖 Prompt for AI Agents