gooddata-eval 1.79.2.dev1__tar.gz → 1.80.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/AGENTS.md +1 -1
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/PKG-INFO +4 -3
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/README.md +2 -1
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/pyproject.toml +2 -2
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/cli/agentic_runner.py +54 -0
- gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/_editor_takeaways.py +274 -0
- gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/_outcome.py +123 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/alert_skill.py +59 -26
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/anomaly_detection.py +56 -31
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/conversation.py +36 -28
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/dashboard_skill.py +386 -28
- gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/dashboard_summary.py +581 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/document_skill.py +622 -43
- gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/forecasting.py +641 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/general_question.py +57 -18
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/guardrail.py +31 -14
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/kda_skill.py +37 -19
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/metric_skill.py +34 -25
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/search_tool.py +66 -22
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/visualization.py +30 -20
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/what_if.py +57 -32
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/chat/render.py +2 -2
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/from_insights.py +9 -2
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_llm_judge.py +1 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/interaction.py +2 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/models.py +7 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/takeaway/evaluate.py +2 -9
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/takeaway/http_client.py +22 -3
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/timing.py +12 -1
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_alert_skill.py +61 -6
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_anomaly_detection.py +10 -2
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_dashboard_skill.py +451 -1
- gooddata_eval-1.80.0/tests/test_agentic_dashboard_summary.py +368 -0
- gooddata_eval-1.80.0/tests/test_agentic_document_editor_takeaways.py +385 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_document_skill.py +987 -3
- gooddata_eval-1.80.0/tests/test_agentic_forecasting.py +528 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_general_question.py +72 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_kda_skill.py +2 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_metric_skill.py +37 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_runner.py +112 -7
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_search_tool.py +56 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_what_if.py +12 -2
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_chat_render.py +33 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_from_insights.py +50 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_takeaway_client.py +15 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_timing.py +8 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_trace_linker.py +2 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/.gitignore +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/CLAUDE.md +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/LICENSE.txt +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/Makefile +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/scripts/calibrate_takeaway_judge.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/scripts/verify_guardrail_refusal_criteria.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/cli/main.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/_output.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/_retry.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_clarification.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_gate.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/chat/sse_client.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/config.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_rubric.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/takeaway.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/granularity.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/_env.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/client.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/observations.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/number_format.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/html_report.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/json_report.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/report_template.html +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/runner.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/takeaway/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/__init__.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/_fake_langfuse.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/conftest.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/takeaway_calibration.json +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_conversation.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_gate.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_guardrail.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_join_trace.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_langfuse_trace.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_observe_experiment.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_run_context.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_visualization.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_cli.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_connection.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_fake_langfuse.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_guardrail_criteria.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_html_report.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_interaction.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_client.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_e2e_fake_server.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_env.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_experiment.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_item_scope.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_observations.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_otlp.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_source.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_maql_normalize.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_models.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_number_format.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_runner.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_sse_client.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_takeaway_evaluate.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_takeaway_evaluator.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_workspace.py +0 -0
- {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tox.ini +0 -0
|
@@ -27,7 +27,7 @@ experiment. The newest and most actively developed package in the repo.
|
|
|
27
27
|
| `core/agentic/` | multi-turn agentic evaluation per capability, **plus** all Langfuse trace polling and linking (`_langfuse.py`, `_trace_linker.py`) |
|
|
28
28
|
| `core/chat/` | SSE client for the agent's streaming chat endpoint |
|
|
29
29
|
| `core/summary/` | HTTP client for the dedicated dashboard-summary endpoint — a single-shot chat backend, not reporting |
|
|
30
|
-
| `core/takeaway/` | HTTP client for the AI Publisher takeaway endpoint — a single-shot backend like `core/summary/`, keeping the whole response on `ChatResult.raw_response` — plus `evaluate_document_takeaway`, the K-run, gate and Langfuse entry point the Tavern shim calls |
|
|
30
|
+
| `core/takeaway/` | HTTP client for the AI Publisher takeaway endpoint — a single-shot backend like `core/summary/`, keeping the whole response on `ChatResult.raw_response` — plus `evaluate_document_takeaway`, the K-run, gate and Langfuse entry point the Tavern shim calls. Also used by the agentic document skill to write the takeaways the copilot leaves to the editor, before the narrative is graded |
|
|
31
31
|
| `core/dataset/` | dataset format, loading, and `from_insights.py` — dataset generation from a workspace's real insights |
|
|
32
32
|
| `core/evaluators/` | single-shot evaluators and their registry |
|
|
33
33
|
| `core/langfuse/` | the whole Langfuse v4 client: `_env` (base URL + credentials), `otlp` (OTLP/JSON encoding), `experiment` (root-span construction, score targets), `item_scope` (an item's experiment scope sent to gen-ai as baggage, and which conversations joined), `observations` (trace reads), `client` (httpx calls), `sink` (single-shot results as experiments) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.80.0
|
|
4
4
|
Summary: Evaluate the GoodData AI agent against your own questions and models.
|
|
5
5
|
Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
|
|
6
6
|
Author-email: GoodData <support@gooddata.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
17
17
|
Classifier: Topic :: Software Development
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: gooddata-sdk~=1.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.80.0
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
22
|
Requires-Dist: jsonpatch<2.0,>=1.33
|
|
23
23
|
Requires-Dist: orjson<4.0.0,>=3.9.15
|
|
@@ -306,7 +306,8 @@ Each item additionally carries a per-phase breakdown:
|
|
|
306
306
|
```
|
|
307
307
|
|
|
308
308
|
Every item reports `runs_ungraded` beside `runs_passed`: runs the agent answered but the LLM judge returned
|
|
309
|
-
nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail
|
|
309
|
+
nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail` (a document item whose takeaway-endpoint request failed
|
|
310
|
+
lists that failure under `request_errors` instead), and a
|
|
310
311
|
`dashboard_summary` item carries `ungraded_criteria`. Such a run — or, for `dashboard_summary`, such a
|
|
311
312
|
criterion — is excluded from pass@K and from the quality score rather than counted as a failure: scoring it 0
|
|
312
313
|
would be indistinguishable from the judge genuinely failing the answer, which is the confusion
|
|
@@ -277,7 +277,8 @@ Each item additionally carries a per-phase breakdown:
|
|
|
277
277
|
```
|
|
278
278
|
|
|
279
279
|
Every item reports `runs_ungraded` beside `runs_passed`: runs the agent answered but the LLM judge returned
|
|
280
|
-
nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail
|
|
280
|
+
nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail` (a document item whose takeaway-endpoint request failed
|
|
281
|
+
lists that failure under `request_errors` instead), and a
|
|
281
282
|
`dashboard_summary` item carries `ungraded_criteria`. Such a run — or, for `dashboard_summary`, such a
|
|
282
283
|
criterion — is excluded from pass@K and from the quality score rather than counted as a failure: scoring it 0
|
|
283
284
|
would be indistinguishable from the judge genuinely failing the answer, which is the confusion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-eval"
|
|
4
|
-
version = "1.
|
|
4
|
+
version = "1.80.0"
|
|
5
5
|
description = "Evaluate the GoodData AI agent against your own questions and models."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
keywords = ["gooddata", "ai", "evaluation", "llm", "analytics", "cli"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"gooddata-sdk~=1.
|
|
14
|
+
"gooddata-sdk~=1.80.0",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"jsonpatch>=1.33,<2.0",
|
|
17
17
|
"orjson>=3.9.15,<4.0.0",
|
|
@@ -16,7 +16,9 @@ from gooddata_eval.core.agentic.alert_skill import evaluate_agentic_alert_skill
|
|
|
16
16
|
from gooddata_eval.core.agentic.anomaly_detection import evaluate_agentic_anomaly_detection
|
|
17
17
|
from gooddata_eval.core.agentic.conversation import ConversationFixture, evaluate_agentic_conversation
|
|
18
18
|
from gooddata_eval.core.agentic.dashboard_skill import evaluate_agentic_dashboard_skill
|
|
19
|
+
from gooddata_eval.core.agentic.dashboard_summary import evaluate_agentic_dashboard_summary
|
|
19
20
|
from gooddata_eval.core.agentic.document_skill import evaluate_agentic_document_skill
|
|
21
|
+
from gooddata_eval.core.agentic.forecasting import evaluate_agentic_forecasting
|
|
20
22
|
from gooddata_eval.core.agentic.general_question import evaluate_agentic_general_question
|
|
21
23
|
from gooddata_eval.core.agentic.guardrail import evaluate_agentic_guardrail
|
|
22
24
|
from gooddata_eval.core.agentic.kda_skill import evaluate_agentic_kda_skill
|
|
@@ -56,6 +58,8 @@ AGENTIC_TEST_KINDS = frozenset(
|
|
|
56
58
|
"agentic_guardrail",
|
|
57
59
|
"agentic_conversation",
|
|
58
60
|
"agentic_kda_skill",
|
|
61
|
+
"agentic_forecasting",
|
|
62
|
+
"agentic_dashboard_summary",
|
|
59
63
|
"agentic_anomaly_detection",
|
|
60
64
|
"agentic_what_if",
|
|
61
65
|
}
|
|
@@ -303,6 +307,41 @@ def _dispatch_agentic(
|
|
|
303
307
|
user_context=item.user_context,
|
|
304
308
|
**lf_kw,
|
|
305
309
|
)
|
|
310
|
+
elif kind == "agentic_forecasting":
|
|
311
|
+
return evaluate_agentic_forecasting(
|
|
312
|
+
host=host,
|
|
313
|
+
token=token,
|
|
314
|
+
workspace_id=workspace_id,
|
|
315
|
+
question=item.question,
|
|
316
|
+
expected_output=eo if isinstance(eo, dict) else {},
|
|
317
|
+
k=k,
|
|
318
|
+
gate=gate,
|
|
319
|
+
agent_id=agent_id,
|
|
320
|
+
user_context=item.user_context,
|
|
321
|
+
**lf_kw,
|
|
322
|
+
)
|
|
323
|
+
elif kind == "agentic_dashboard_summary":
|
|
324
|
+
summary_input = item.summary_input
|
|
325
|
+
if summary_input is None:
|
|
326
|
+
raise ValueError(f"agentic_dashboard_summary item '{item.id}' is missing required 'summary_input'.")
|
|
327
|
+
return evaluate_agentic_dashboard_summary(
|
|
328
|
+
host=host,
|
|
329
|
+
token=token,
|
|
330
|
+
workspace_id=workspace_id,
|
|
331
|
+
dashboard_id=summary_input.dashboard_id,
|
|
332
|
+
expected_output=eo,
|
|
333
|
+
# The fixture's own wording is the prompt under test -- a localized fixture is
|
|
334
|
+
# only meaningful if its own phrasing is what reaches the agent.
|
|
335
|
+
question=item.question,
|
|
336
|
+
# Each widget costs an execution, so a fixture asserting on a handful of charts
|
|
337
|
+
# can name them instead of paying for the whole dashboard. Same field and meaning
|
|
338
|
+
# the headless /summary endpoint gives it; None summarizes everything.
|
|
339
|
+
only_visualizations=summary_input.visualizations,
|
|
340
|
+
k=k,
|
|
341
|
+
gate=gate,
|
|
342
|
+
agent_id=agent_id,
|
|
343
|
+
**lf_kw,
|
|
344
|
+
)
|
|
306
345
|
elif kind == "agentic_what_if":
|
|
307
346
|
return evaluate_agentic_what_if(
|
|
308
347
|
host=host,
|
|
@@ -404,6 +443,18 @@ def _apply_timings(item_report: ItemReport, timings: Any) -> None:
|
|
|
404
443
|
item_report.simulated_user_latency_s = timings.simulated_user_s
|
|
405
444
|
|
|
406
445
|
|
|
446
|
+
def _apply_best_run_latency(item_report: ItemReport, source: Any) -> None:
|
|
447
|
+
"""Copy the rank-selected run's own wall time, mirroring the single-shot path's
|
|
448
|
+
``ItemReport.best_run_latency_s`` (see ``core/runner.py``'s ``_run_one_item``).
|
|
449
|
+
|
|
450
|
+
Kinds not yet wired to measure it pass None and keep the field at its own None
|
|
451
|
+
default rather than reporting an invented number.
|
|
452
|
+
"""
|
|
453
|
+
best_run_latency_s = getattr(source, "best_run_latency_s", None)
|
|
454
|
+
if best_run_latency_s is not None:
|
|
455
|
+
item_report.best_run_latency_s = best_run_latency_s
|
|
456
|
+
|
|
457
|
+
|
|
407
458
|
def run_agentic_items(
|
|
408
459
|
items: list[DatasetItem],
|
|
409
460
|
host: str,
|
|
@@ -501,6 +552,7 @@ def run_agentic_items(
|
|
|
501
552
|
item_report.response_id = response_id
|
|
502
553
|
item_report.best_detail = detail or {}
|
|
503
554
|
_apply_timings(item_report, getattr(outcome, "timings", None))
|
|
555
|
+
_apply_best_run_latency(item_report, outcome)
|
|
504
556
|
_apply_run_counts(item_report, outcome)
|
|
505
557
|
_apply_failed_runs(item_report, outcome)
|
|
506
558
|
_apply_run_latencies(item_report, outcome)
|
|
@@ -512,6 +564,7 @@ def run_agentic_items(
|
|
|
512
564
|
item_report.response_id = getattr(exc, "response_id", None)
|
|
513
565
|
item_report.best_detail = getattr(exc, "detail", None) or {}
|
|
514
566
|
_apply_timings(item_report, getattr(exc, "timings", None))
|
|
567
|
+
_apply_best_run_latency(item_report, exc)
|
|
515
568
|
_apply_run_counts(item_report, exc)
|
|
516
569
|
_apply_failed_runs(item_report, exc)
|
|
517
570
|
_apply_run_latencies(item_report, exc)
|
|
@@ -555,6 +608,7 @@ def run_agentic_items(
|
|
|
555
608
|
# runs and then raised would otherwise show up in the per-response table and not in
|
|
556
609
|
# the per-conversation one.
|
|
557
610
|
traces.discard()
|
|
611
|
+
_apply_best_run_latency(item_report, exc)
|
|
558
612
|
finally:
|
|
559
613
|
item_report.latency_s = time.perf_counter() - t0
|
|
560
614
|
item_report.trace_latencies_s = traces.stop()
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation. All rights reserved.
|
|
2
|
+
"""The takeaway request the Publisher editor sends for an unwritten takeaway of a v2 document.
|
|
3
|
+
|
|
4
|
+
Every visualization on the page, in layout order, each under the filters it is drawn with, a
|
|
5
|
+
visualization drawn twice the same way sent once; ``filterContext`` empty; the takeaway's and the
|
|
6
|
+
document's instructions. A visualization is drawn with the period on its own date dataset unless it
|
|
7
|
+
ignores the period, a document date filter on that dataset with both ends set in the period's place
|
|
8
|
+
(keeping its ``emptyValueHandling`` and, for a relative one, its ``boundedFilter``),
|
|
9
|
+
and every document attribute and text filter that narrows the data. An attribute filter excluding
|
|
10
|
+
nothing and a text filter with no value narrow nothing and are left out; measure value filters
|
|
11
|
+
and any other kind are left out too, since the renderer draws no chart differently for them. Widget
|
|
12
|
+
and page filters, and ``ignoreDocumentFilters``, are stored but not applied, so they are not applied
|
|
13
|
+
here either.
|
|
14
|
+
|
|
15
|
+
Two parts of the editor's request are left out: ``maxCharacters``, which the editor derives from the
|
|
16
|
+
layout geometry and the theme, so the endpoint's default length applies; and resolving text variables
|
|
17
|
+
in the instructions, which are sent as written, as the editor does while it has no values for them.
|
|
18
|
+
|
|
19
|
+
Mirrors gdc-ui at commit 7e0f3719ca2 (``documentTakeawayRequest`` and ``copilotTakeaways`` of the
|
|
20
|
+
publisher's v2 editor); check them again when the editor's request changes.
|
|
21
|
+
|
|
22
|
+
Pure: no network.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from gooddata_eval.core.models import TakeawayInput, TakeawayInstructions, TakeawayVisualization
|
|
31
|
+
|
|
32
|
+
# Publisher periods and dashboard date filters name granularities as GDC.time.*; AFM date filters
|
|
33
|
+
# take the platform enum.
|
|
34
|
+
_AFM_GRANULARITY = {
|
|
35
|
+
"GDC.time.year": "YEAR",
|
|
36
|
+
"GDC.time.quarter": "QUARTER",
|
|
37
|
+
"GDC.time.month": "MONTH",
|
|
38
|
+
"GDC.time.week_us": "WEEK",
|
|
39
|
+
"GDC.time.week": "WEEK",
|
|
40
|
+
"GDC.time.date": "DAY",
|
|
41
|
+
"GDC.time.hour": "HOUR",
|
|
42
|
+
"GDC.time.minute": "MINUTE",
|
|
43
|
+
"GDC.time.second": "SECOND",
|
|
44
|
+
"GDC.time.quarter_in_year": "QUARTER_OF_YEAR",
|
|
45
|
+
"GDC.time.month_in_year": "MONTH_OF_YEAR",
|
|
46
|
+
"GDC.time.week_in_year": "WEEK_OF_YEAR",
|
|
47
|
+
"GDC.time.day_in_year": "DAY_OF_YEAR",
|
|
48
|
+
"GDC.time.day_in_month": "DAY_OF_MONTH",
|
|
49
|
+
"GDC.time.day_in_week": "DAY_OF_WEEK",
|
|
50
|
+
"GDC.time.hour_in_day": "HOUR_OF_DAY",
|
|
51
|
+
"GDC.time.minute_in_hour": "MINUTE_OF_HOUR",
|
|
52
|
+
"GDC.time.minute_in_day": "MINUTE_OF_DAY",
|
|
53
|
+
"GDC.time.second_in_day": "SECOND_OF_DAY",
|
|
54
|
+
"GDC.time.second_in_minute": "SECOND_OF_MINUTE",
|
|
55
|
+
"GDC.time.fiscal_month": "FISCAL_MONTH",
|
|
56
|
+
"GDC.time.fiscal_quarter": "FISCAL_QUARTER",
|
|
57
|
+
"GDC.time.fiscal_year": "FISCAL_YEAR",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Unbuildable(Exception):
|
|
62
|
+
"""The editor's request has a part this module cannot reproduce."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _identifier(reference: Any) -> str | None:
|
|
66
|
+
"""The id of a reference written as ``{"identifier": "..."}``, ``{"identifier": {"id": ...}}`` or a bare string."""
|
|
67
|
+
if isinstance(reference, dict):
|
|
68
|
+
reference = reference.get("identifier")
|
|
69
|
+
if isinstance(reference, dict):
|
|
70
|
+
reference = reference.get("id")
|
|
71
|
+
return reference if isinstance(reference, str) else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _date_filter(
|
|
75
|
+
dataset: str, kind: Any, granularity: Any, start: Any, end: Any, options: dict[str, Any] | None = None
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""The AFM date filter; ``options`` are the document filter's ``emptyValueHandling`` and ``boundedFilter``."""
|
|
78
|
+
dataset_ref = {"identifier": {"id": dataset, "type": "dataset"}}
|
|
79
|
+
options = options or {}
|
|
80
|
+
if kind == "absolute":
|
|
81
|
+
return {
|
|
82
|
+
"absoluteDateFilter": {
|
|
83
|
+
"dataset": dataset_ref,
|
|
84
|
+
"from": start,
|
|
85
|
+
"to": end,
|
|
86
|
+
**{key: value for key, value in options.items() if key == "emptyValueHandling"},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if kind == "relative":
|
|
90
|
+
afm_granularity = _AFM_GRANULARITY.get(granularity) if isinstance(granularity, str) else None
|
|
91
|
+
if afm_granularity is None:
|
|
92
|
+
raise _Unbuildable(f"relative date granularity {granularity!r} has no AFM form")
|
|
93
|
+
return {
|
|
94
|
+
"relativeDateFilter": {
|
|
95
|
+
"dataset": dataset_ref,
|
|
96
|
+
"granularity": afm_granularity,
|
|
97
|
+
"from": start,
|
|
98
|
+
"to": end,
|
|
99
|
+
**options,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
raise _Unbuildable(f"date filter type {kind!r} has no AFM form")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _narrows_the_date(date_filter: dict) -> bool:
|
|
106
|
+
return date_filter.get("from") is not None and date_filter.get("to") is not None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _date_filters(widget: dict, period: Any, filters: list[Any]) -> list[dict]:
|
|
110
|
+
dataset = _identifier(widget.get("dateDataSet"))
|
|
111
|
+
if dataset is None:
|
|
112
|
+
return []
|
|
113
|
+
for item in filters:
|
|
114
|
+
date_filter = item.get("dateFilter") if isinstance(item, dict) else None
|
|
115
|
+
if (
|
|
116
|
+
isinstance(date_filter, dict)
|
|
117
|
+
and _narrows_the_date(date_filter)
|
|
118
|
+
and _identifier(date_filter.get("dataSet")) == dataset
|
|
119
|
+
):
|
|
120
|
+
return [
|
|
121
|
+
_date_filter(
|
|
122
|
+
dataset,
|
|
123
|
+
date_filter.get("type"),
|
|
124
|
+
date_filter.get("granularity"),
|
|
125
|
+
date_filter.get("from"),
|
|
126
|
+
date_filter.get("to"),
|
|
127
|
+
{
|
|
128
|
+
key: date_filter[key]
|
|
129
|
+
for key in ("emptyValueHandling", "boundedFilter")
|
|
130
|
+
if date_filter.get(key) is not None
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
if not isinstance(period, dict) or widget.get("ignoreDocumentPeriod") is True:
|
|
135
|
+
return []
|
|
136
|
+
return [_date_filter(dataset, period.get("type"), period.get("granularity"), period.get("from"), period.get("to"))]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_MATCH_TYPES = {"contains": "CONTAINS", "startsWith": "STARTS_WITH", "endsWith": "ENDS_WITH"}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _label_ref(body: dict) -> dict:
|
|
143
|
+
label = _identifier(body.get("displayForm"))
|
|
144
|
+
if label is None:
|
|
145
|
+
raise _Unbuildable(f"filter {body!r} names no label, so it has no AFM form")
|
|
146
|
+
return {"identifier": {"id": label, "type": "label"}}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _element_filter(label_ref: dict, values: list[Any], negative: bool) -> dict | None:
|
|
150
|
+
"""Selected elements as AFM; None for an exclusion of nothing, which keeps every row."""
|
|
151
|
+
if negative:
|
|
152
|
+
return {"negativeAttributeFilter": {"label": label_ref, "notIn": {"values": values}}} if values else None
|
|
153
|
+
return {"positiveAttributeFilter": {"label": label_ref, "in": {"values": values}}}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _attribute_filter(body: dict) -> dict | None:
|
|
157
|
+
elements = body.get("attributeElements")
|
|
158
|
+
elements = elements if isinstance(elements, dict) else {}
|
|
159
|
+
values = elements.get("uris") or elements.get("values") or []
|
|
160
|
+
if not isinstance(values, list):
|
|
161
|
+
raise _Unbuildable(f"attribute filter {body!r} has no element list, so it has no AFM form")
|
|
162
|
+
negative = body.get("negativeSelection") is True
|
|
163
|
+
if not values and not negative:
|
|
164
|
+
raise _Unbuildable(f"attribute filter {body!r} selects no element, which has no AFM form")
|
|
165
|
+
return _element_filter(_label_ref(body), values, negative)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _arbitrary_attribute_filter(body: dict) -> dict | None:
|
|
169
|
+
values = body.get("values")
|
|
170
|
+
if not isinstance(values, list):
|
|
171
|
+
raise _Unbuildable(f"text filter {body!r} has no value list, so it has no AFM form")
|
|
172
|
+
negative = body.get("negativeSelection") is True
|
|
173
|
+
# Tiger drops a text filter without values, so the editor's request carries none.
|
|
174
|
+
return _element_filter(_label_ref(body), values, negative) if values else None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _match_attribute_filter(body: dict) -> dict:
|
|
178
|
+
operator = body.get("operator")
|
|
179
|
+
match_type = _MATCH_TYPES.get(operator) if isinstance(operator, str) else None
|
|
180
|
+
literal = body.get("literal")
|
|
181
|
+
if match_type is None or not isinstance(literal, str):
|
|
182
|
+
raise _Unbuildable(f"text filter {body!r} has no AFM form")
|
|
183
|
+
return {
|
|
184
|
+
"matchAttributeFilter": {
|
|
185
|
+
"label": _label_ref(body),
|
|
186
|
+
"literal": literal,
|
|
187
|
+
"matchType": match_type,
|
|
188
|
+
"caseSensitive": body.get("caseSensitive") is True,
|
|
189
|
+
"negate": body.get("negativeSelection") is True,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
_ATTRIBUTE_KINDS = {
|
|
195
|
+
"attributeFilter": _attribute_filter,
|
|
196
|
+
"arbitraryAttributeFilter": _arbitrary_attribute_filter,
|
|
197
|
+
"matchAttributeFilter": _match_attribute_filter,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _attribute_filters(filters: list[Any]) -> list[dict]:
|
|
202
|
+
"""The AFM form of every document filter other than a date filter that narrows the data."""
|
|
203
|
+
afm: list[dict] = []
|
|
204
|
+
for item in filters:
|
|
205
|
+
if not isinstance(item, dict):
|
|
206
|
+
raise _Unbuildable(f"document filter {item!r} is not an object")
|
|
207
|
+
kind = next((kind for kind in _ATTRIBUTE_KINDS if kind in item), None)
|
|
208
|
+
if kind is None:
|
|
209
|
+
continue
|
|
210
|
+
body = item[kind]
|
|
211
|
+
if not isinstance(body, dict):
|
|
212
|
+
raise _Unbuildable(f"document filter {item!r} has no AFM form")
|
|
213
|
+
converted = _ATTRIBUTE_KINDS[kind](body)
|
|
214
|
+
if converted is not None:
|
|
215
|
+
afm.append(converted)
|
|
216
|
+
return afm
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _visualization_widgets(node: Any) -> list[dict]:
|
|
220
|
+
"""Every visualization widget with a visualization under ``node``, in layout order."""
|
|
221
|
+
if not isinstance(node, dict):
|
|
222
|
+
return []
|
|
223
|
+
children = node.get("children")
|
|
224
|
+
if isinstance(children, list):
|
|
225
|
+
return [widget for child in children for widget in _visualization_widgets(child)]
|
|
226
|
+
if node.get("type") == "visualization" and _identifier(node.get("visualization")) is not None:
|
|
227
|
+
return [node]
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _instruction(text: Any) -> str | None:
|
|
232
|
+
written = text.strip() if isinstance(text, str) else ""
|
|
233
|
+
return written or None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _generation_instructions(holder: Any) -> Any:
|
|
237
|
+
generation = holder.get("generation") if isinstance(holder, dict) else None
|
|
238
|
+
return generation.get("instructions") if isinstance(generation, dict) else None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def editor_takeaway_input(document: dict, page: dict, takeaway: dict) -> TakeawayInput | str:
|
|
242
|
+
"""The request the editor sends to write ``takeaway`` on ``page``, or why it cannot be built.
|
|
243
|
+
|
|
244
|
+
``document`` is a v2 document in its declarative form, the one the editor reads, with its content
|
|
245
|
+
keys under ``content`` or at the top level.
|
|
246
|
+
"""
|
|
247
|
+
content = document.get("content")
|
|
248
|
+
content = content if isinstance(content, dict) else document
|
|
249
|
+
filters = content.get("filters") or []
|
|
250
|
+
if not isinstance(filters, list):
|
|
251
|
+
return f"document filters {filters!r} are not a list"
|
|
252
|
+
try:
|
|
253
|
+
attribute_filters = _attribute_filters(filters)
|
|
254
|
+
placements: dict[str, TakeawayVisualization] = {}
|
|
255
|
+
for widget in _visualization_widgets(page.get("layout")):
|
|
256
|
+
placement = TakeawayVisualization(
|
|
257
|
+
visualization_id=_identifier(widget.get("visualization")) or "",
|
|
258
|
+
filters=[*_date_filters(widget, document.get("period"), filters), *attribute_filters],
|
|
259
|
+
)
|
|
260
|
+
placements.setdefault(json.dumps(placement.model_dump(), sort_keys=True), placement)
|
|
261
|
+
except _Unbuildable as exc:
|
|
262
|
+
return str(exc)
|
|
263
|
+
if not placements:
|
|
264
|
+
return f"page {page.get('id')!r} has no visualization to summarize"
|
|
265
|
+
defaults = content.get("defaults")
|
|
266
|
+
takeaway_defaults = defaults.get("takeaway") if isinstance(defaults, dict) else None
|
|
267
|
+
return TakeawayInput(
|
|
268
|
+
visualizations=list(placements.values()),
|
|
269
|
+
filter_context=[],
|
|
270
|
+
instructions=TakeawayInstructions(
|
|
271
|
+
slot=_instruction(_generation_instructions(takeaway)),
|
|
272
|
+
document=_instruction(_generation_instructions(takeaway_defaults)),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation
|
|
2
|
+
"""The common tail of every ``evaluate_agentic_*`` function.
|
|
3
|
+
|
|
4
|
+
Before this module, each of the agentic evaluators hand-wrote the same block: copy
|
|
5
|
+
``reasoning_steps``/``conversation_id``/``response_id``/``detail``/``runs_passed``/
|
|
6
|
+
``runs_effective``/``best_run_latency_s`` (and sometimes ``timings``) either onto a
|
|
7
|
+
raised exception or into the returned ``AgenticEvalOutcome``. Eleven independent copies
|
|
8
|
+
of the same ~7 lines meant a new universal field needed eleven edits, not one, and
|
|
9
|
+
nothing failed loudly when a copy was missed -- exactly what happened to ``timings``
|
|
10
|
+
(see ``AgenticAssertionError``'s docstring: "while it lived in eight copies, two
|
|
11
|
+
declared timings and six did not") and again to ``best_run_latency_s`` (every agentic
|
|
12
|
+
kind silently reported it as ``null`` until this was noticed and fixed kind-by-kind).
|
|
13
|
+
|
|
14
|
+
``agentic_detail`` has the same motivation for the ``detail`` dict's timeline fields:
|
|
15
|
+
``timeline_detail`` builds both ``latency_breakdown`` and ``tool_calls`` from the same
|
|
16
|
+
events so they stay index-aligned, but roughly half the evaluators called
|
|
17
|
+
``build_latency_breakdown`` directly and silently never got a ``tool_calls`` key.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, NoReturn
|
|
23
|
+
|
|
24
|
+
from gooddata_eval.core.models import (
|
|
25
|
+
AgenticAssertionError,
|
|
26
|
+
AgenticEvalOutcome,
|
|
27
|
+
ReasoningStepEvent,
|
|
28
|
+
ToolCallEvent,
|
|
29
|
+
timeline_detail,
|
|
30
|
+
)
|
|
31
|
+
from gooddata_eval.core.timing import PhaseTimings
|
|
32
|
+
|
|
33
|
+
__all__ = ["agentic_detail", "agentic_success", "raise_agentic_failure"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def agentic_detail(
|
|
37
|
+
tool_call_events: list[ToolCallEvent],
|
|
38
|
+
reasoning_step_events: list[ReasoningStepEvent] | None,
|
|
39
|
+
**kind_specific: Any,
|
|
40
|
+
) -> dict:
|
|
41
|
+
"""A kind's full ``detail`` dict: its own fields plus the universal timeline ones.
|
|
42
|
+
|
|
43
|
+
``kind_specific`` comes first in the merge so a kind can never accidentally shadow
|
|
44
|
+
``latency_breakdown``/``tool_calls`` with a same-named field of its own.
|
|
45
|
+
"""
|
|
46
|
+
return {**kind_specific, **timeline_detail(tool_call_events, reasoning_step_events)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def raise_agentic_failure(
|
|
50
|
+
exception_cls: type[AgenticAssertionError],
|
|
51
|
+
message: str,
|
|
52
|
+
*,
|
|
53
|
+
reasoning_steps: list[str],
|
|
54
|
+
conversation_id: str,
|
|
55
|
+
response_id: str | None,
|
|
56
|
+
detail: dict,
|
|
57
|
+
runs_passed: int,
|
|
58
|
+
runs_effective: int,
|
|
59
|
+
best_run_latency_s: float | None,
|
|
60
|
+
timings: PhaseTimings | None = None,
|
|
61
|
+
failed_runs: list[dict] | None = None,
|
|
62
|
+
run_latencies_s: list[float] | None = None,
|
|
63
|
+
) -> NoReturn:
|
|
64
|
+
"""Build ``exception_cls(message)`` with every common field attached, and raise it.
|
|
65
|
+
|
|
66
|
+
``exception_cls`` must be an ``AgenticAssertionError`` subclass -- that base class is
|
|
67
|
+
what declares these fields as legal targets (see its docstring). A kind whose own
|
|
68
|
+
"no verdict at all" branch raises a bare ``JudgeResponseError`` instead (not a subclass)
|
|
69
|
+
does not go through this helper for that branch.
|
|
70
|
+
|
|
71
|
+
``timings`` stays optional: only the three kinds that track per-run ``PhaseTimings``
|
|
72
|
+
(general_question, metric_skill, dashboard_skill) pass one -- the rest keep their
|
|
73
|
+
own ``PhaseTimings()`` zero default, same as before this helper existed. ``failed_runs``
|
|
74
|
+
and ``run_latencies_s`` are likewise optional: not every kind has built them by the
|
|
75
|
+
time it raises.
|
|
76
|
+
"""
|
|
77
|
+
exc = exception_cls(message)
|
|
78
|
+
exc.reasoning_steps = reasoning_steps
|
|
79
|
+
exc.conversation_id = conversation_id
|
|
80
|
+
exc.response_id = response_id
|
|
81
|
+
exc.detail = detail
|
|
82
|
+
exc.runs_passed = runs_passed
|
|
83
|
+
exc.runs_effective = runs_effective
|
|
84
|
+
exc.best_run_latency_s = best_run_latency_s
|
|
85
|
+
if timings is not None:
|
|
86
|
+
exc.timings = timings
|
|
87
|
+
if failed_runs is not None:
|
|
88
|
+
exc.failed_runs = failed_runs
|
|
89
|
+
if run_latencies_s is not None:
|
|
90
|
+
exc.run_latencies_s = run_latencies_s
|
|
91
|
+
raise exc
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def agentic_success(
|
|
95
|
+
*,
|
|
96
|
+
reasoning_steps: list[str],
|
|
97
|
+
conversation_id: str | None,
|
|
98
|
+
response_id: str | None,
|
|
99
|
+
detail: dict,
|
|
100
|
+
runs_passed: int,
|
|
101
|
+
runs_effective: int,
|
|
102
|
+
best_run_latency_s: float | None,
|
|
103
|
+
timings: PhaseTimings | None = None,
|
|
104
|
+
failed_runs: list[dict] | None = None,
|
|
105
|
+
run_latencies_s: list[float] | None = None,
|
|
106
|
+
) -> AgenticEvalOutcome:
|
|
107
|
+
"""The success-path mirror of ``raise_agentic_failure`` -- same fields, same shape."""
|
|
108
|
+
kwargs: dict[str, Any] = {
|
|
109
|
+
"reasoning_steps": reasoning_steps,
|
|
110
|
+
"conversation_id": conversation_id,
|
|
111
|
+
"response_id": response_id,
|
|
112
|
+
"detail": detail,
|
|
113
|
+
"runs_passed": runs_passed,
|
|
114
|
+
"runs_effective": runs_effective,
|
|
115
|
+
"best_run_latency_s": best_run_latency_s,
|
|
116
|
+
}
|
|
117
|
+
if timings is not None:
|
|
118
|
+
kwargs["timings"] = timings
|
|
119
|
+
if failed_runs is not None:
|
|
120
|
+
kwargs["failed_runs"] = failed_runs
|
|
121
|
+
if run_latencies_s is not None:
|
|
122
|
+
kwargs["run_latencies_s"] = run_latencies_s
|
|
123
|
+
return AgenticEvalOutcome(**kwargs)
|