-
Notifications
You must be signed in to change notification settings - Fork 69
fix(gooddata-eval): delete metrics created during agentic eval runs #1685
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: master
Are you sure you want to change the base?
Changes from all commits
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -127,6 +127,43 @@ def _extract_metric_result(tool_call_events: list[ToolCallEvent]) -> dict | None | |||||
| return None | ||||||
|
|
||||||
|
|
||||||
| def _extract_created_metric_ids(tool_call_events: list[ToolCallEvent]) -> list[str]: | ||||||
| """Ids of every metric created by ``create_metric`` calls (a turn may create more than one). | ||||||
|
|
||||||
| Used for cleanup so no created metric leaks — unlike ``_extract_metric_result``, which | ||||||
| returns only the first result for MAQL evaluation. Shared with conversation evaluation. | ||||||
| """ | ||||||
| metric_ids: list[str] = [] | ||||||
| for tc in tool_call_events: | ||||||
| if tc.function_name != "create_metric" or not tc.result: | ||||||
| continue | ||||||
| result_data = tc.parsed_result() | ||||||
| if not result_data: | ||||||
| continue | ||||||
| data = result_data.get("data", result_data) | ||||||
| metric_id = data.get("metric_id") if isinstance(data, dict) else None | ||||||
| if metric_id and metric_id not in metric_ids: | ||||||
| metric_ids.append(metric_id) | ||||||
| return metric_ids | ||||||
|
|
||||||
|
|
||||||
| def _delete_metric(client: ChatClient, workspace_id: str, metric_id: str) -> None: | ||||||
| """Delete a metric created during evaluation. | ||||||
|
|
||||||
| Eval runs share a persistent workspace, so a metric left behind is picked up by | ||||||
| a later test — the agent reuses it (returning ``SELECT {id}`` instead of full | ||||||
| MAQL) and the assertion fails. Deleting the created metric on the way out keeps | ||||||
| the workspace clean for the next run. Mirrors ``alert_skill._delete_alert``. | ||||||
| """ | ||||||
| host = str(client._base).split("/api/")[0] | ||||||
| url = f"{host}/api/v1/entities/workspaces/{workspace_id}/metrics/{metric_id}" | ||||||
| try: | ||||||
| response = client._client.delete(url, headers=client._auth) | ||||||
| response.raise_for_status() | ||||||
| except Exception as exc: | ||||||
|
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Silence the Ruff BLE001 warning on the intentional best-effort catch. The blind 🧹 Proposed fix- except Exception as exc:
+ except Exception as exc: # noqa: BLE001 - best-effort cleanup, log and continue
print(f"[CLEANUP] Failed to delete metric {metric_id}: {exc}")📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.20)[warning] 143-143: Do not catch blind exception: (BLE001) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
| print(f"[CLEANUP] Failed to delete metric {metric_id}: {exc}") | ||||||
|
|
||||||
|
|
||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||
| def _is_asking_clarification(text: str) -> bool: | ||||||
| if not text: | ||||||
| return False | ||||||
|
|
@@ -136,41 +173,53 @@ def _is_asking_clarification(text: str) -> bool: | |||||
|
|
||||||
| def _execute_single_metric_run( | ||||||
| client: ChatClient, | ||||||
| workspace_id: str, | ||||||
| conversation_id: str, | ||||||
| question: str, | ||||||
| expected_outputs: list[dict], | ||||||
| max_iterations: int, | ||||||
| ) -> MetricRunResult: | ||||||
| """Drive one full multi-turn metric-skill conversation and evaluate the result.""" | ||||||
| """Drive one full multi-turn metric-skill conversation and evaluate the result. | ||||||
|
|
||||||
| Any metric the agent creates during this run is deleted on the way out (see | ||||||
| ``_delete_metric``) so it cannot leak into — and be reused by — a later test | ||||||
| sharing the workspace. | ||||||
|
Comment on lines
+182
to
+186
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Track every successful Only the first result’s ID is retained. If one response contains multiple successful creation calls, additional metrics remain in the shared workspace. Collect all returned IDs while preserving the first result for evaluation. Proposed fix metric_result: dict | None = None
- metric_id_to_delete: str | None = None
+ metric_ids_to_delete: list[str] = []
turns = 0
@@
turns += 1
chat_result = client.send_message(conversation_id, current_question)
- candidate = _extract_metric_result(chat_result.tool_call_events or [])
+ tool_call_events = chat_result.tool_call_events or []
+ candidate = _extract_metric_result(tool_call_events)
+ for tc in tool_call_events:
+ if tc.function_name != "create_metric" or not tc.result:
+ continue
+ result_data = tc.parsed_result()
+ data = result_data.get("data", result_data) if isinstance(result_data, dict) else None
+ metric_id = data.get("metric_id") if isinstance(data, dict) else None
+ if metric_id and metric_id not in metric_ids_to_delete:
+ metric_ids_to_delete.append(metric_id)
+
if candidate is not None:
metric_result = candidate
- metric_id_to_delete = candidate.get("metric_id")
break
@@
finally:
- if metric_id_to_delete:
- _delete_metric(client, workspace_id, metric_id_to_delete)
+ for metric_id in metric_ids_to_delete:
+ _delete_metric(client, workspace_id, metric_id)Also applies to: 170-182, 200-202 🤖 Prompt for AI Agents |
||||||
| """ | ||||||
| primary_expected = expected_outputs[0] if expected_outputs else {} | ||||||
| metric_result: dict | None = None | ||||||
| metric_id_to_delete: str | None = None | ||||||
| turns = 0 | ||||||
| current_question = question | ||||||
|
|
||||||
| for _iteration in range(max_iterations): | ||||||
| turns += 1 | ||||||
| chat_result = client.send_message(conversation_id, current_question) | ||||||
| candidate = _extract_metric_result(chat_result.tool_call_events or []) | ||||||
| if candidate is not None: | ||||||
| metric_result = candidate | ||||||
| break | ||||||
| response_text = (chat_result.text_response or "").strip() | ||||||
| if _is_asking_clarification(response_text): | ||||||
| current_question = generate_simulated_response(response_text, primary_expected) | ||||||
| else: | ||||||
| break | ||||||
|
|
||||||
| actual_maql = (metric_result or {}).get("maql", "") | ||||||
| metric_created = metric_result is not None | ||||||
| maql_correct, _ = _best_maql_match(actual_maql, expected_outputs) if metric_created else (False, "") | ||||||
| return MetricRunResult( | ||||||
| conversation_id=conversation_id, | ||||||
| metric_result=metric_result, | ||||||
| metric_created=metric_created, | ||||||
| actual_maql=actual_maql, | ||||||
| maql_correct=maql_correct, | ||||||
| total_turns=float(turns), | ||||||
| ) | ||||||
| try: | ||||||
| for _iteration in range(max_iterations): | ||||||
| turns += 1 | ||||||
| chat_result = client.send_message(conversation_id, current_question) | ||||||
| candidate = _extract_metric_result(chat_result.tool_call_events or []) | ||||||
| if candidate is not None: | ||||||
| metric_result = candidate | ||||||
| metric_id_to_delete = candidate.get("metric_id") | ||||||
| break | ||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||
| response_text = (chat_result.text_response or "").strip() | ||||||
| if _is_asking_clarification(response_text): | ||||||
| current_question = generate_simulated_response(response_text, primary_expected) | ||||||
| else: | ||||||
| break | ||||||
|
|
||||||
| actual_maql = (metric_result or {}).get("maql", "") | ||||||
| metric_created = metric_result is not None | ||||||
| maql_correct, _ = _best_maql_match(actual_maql, expected_outputs) if metric_created else (False, "") | ||||||
| return MetricRunResult( | ||||||
| conversation_id=conversation_id, | ||||||
| metric_result=metric_result, | ||||||
| metric_created=metric_created, | ||||||
| actual_maql=actual_maql, | ||||||
| maql_correct=maql_correct, | ||||||
| total_turns=float(turns), | ||||||
| ) | ||||||
| finally: | ||||||
| if metric_id_to_delete: | ||||||
| _delete_metric(client, workspace_id, metric_id_to_delete) | ||||||
|
|
||||||
|
|
||||||
| def run_agentic_metric_skill( | ||||||
|
|
@@ -196,7 +245,7 @@ def run_agentic_metric_skill( | |||||
| conv_id_0 = initial_conversation_id if initial_conversation_id is not None else client.create_conversation() | ||||||
| try: | ||||||
| run_results.append( | ||||||
| _execute_single_metric_run(client, conv_id_0, question, expected_outputs, max_iterations) | ||||||
| _execute_single_metric_run(client, workspace_id, conv_id_0, question, expected_outputs, max_iterations) | ||||||
| ) | ||||||
| finally: | ||||||
| if initial_conversation_id is None: # only delete conversations we created | ||||||
|
|
@@ -206,7 +255,9 @@ def run_agentic_metric_skill( | |||||
| conv_id = client.create_conversation() | ||||||
| try: | ||||||
| run_results.append( | ||||||
| _execute_single_metric_run(client, conv_id, question, expected_outputs, max_iterations) | ||||||
| _execute_single_metric_run( | ||||||
| client, workspace_id, conv_id, question, expected_outputs, max_iterations | ||||||
| ) | ||||||
| ) | ||||||
| finally: | ||||||
| client.delete_conversation(conv_id) | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.