gooddata-eval 1.78.0__tar.gz → 1.78.1.dev1__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.78.0 → gooddata_eval-1.78.1.dev1}/AGENTS.md +14 -9
- gooddata_eval-1.78.1.dev1/Makefile +6 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/PKG-INFO +66 -6
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/README.md +64 -4
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/pyproject.toml +2 -2
- gooddata_eval-1.78.1.dev1/scripts/calibrate_takeaway_judge.py +75 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/cli/main.py +17 -11
- gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/_retry.py +71 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_conversation_context.py +59 -10
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/chat/sse_client.py +13 -59
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/langfuse_source.py +29 -2
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/__init__.py +5 -2
- gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/evaluators/_rubric.py +94 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/summary.py +12 -64
- gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/evaluators/takeaway.py +245 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/models.py +41 -0
- gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/takeaway/evaluate.py +275 -0
- gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/takeaway/http_client.py +104 -0
- gooddata_eval-1.78.1.dev1/tests/__init__.py +1 -0
- gooddata_eval-1.78.1.dev1/tests/fixtures/takeaway_calibration.json +68 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_conversation.py +115 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_cli.py +51 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_source.py +96 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_models.py +50 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_runner.py +1 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_sse_client.py +6 -0
- gooddata_eval-1.78.1.dev1/tests/test_takeaway_client.py +257 -0
- gooddata_eval-1.78.1.dev1/tests/test_takeaway_evaluate.py +349 -0
- gooddata_eval-1.78.1.dev1/tests/test_takeaway_evaluator.py +425 -0
- gooddata_eval-1.78.0/Makefile +0 -2
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/.gitignore +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/CLAUDE.md +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/LICENSE.txt +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/scripts/verify_guardrail_refusal_criteria.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/cli/agentic_runner.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/_output.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_gate.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/alert_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/anomaly_detection.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/conversation.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/dashboard_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/document_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/general_question.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/guardrail.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/kda_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/metric_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/search_tool.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/visualization.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/what_if.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/chat/render.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/config.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/granularity.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/interaction.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/_env.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/client.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/observations.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/html_report.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/json_report.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/report_template.html +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/runner.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.78.0/tests → gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/takeaway}/__init__.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/timing.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/_fake_langfuse.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/conftest.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_alert_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_anomaly_detection.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_dashboard_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_document_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_gate.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_general_question.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_guardrail.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_join_trace.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_kda_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_langfuse_trace.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_metric_skill.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_observe_experiment.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_run_context.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_runner.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_search_tool.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_visualization.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_what_if.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_chat_render.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_connection.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_fake_langfuse.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_from_insights.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_guardrail_criteria.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_html_report.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_interaction.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_client.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_e2e_fake_server.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_env.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_experiment.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_item_scope.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_observations.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_otlp.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_maql_normalize.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_timing.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_trace_linker.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tests/test_workspace.py +0 -0
- {gooddata_eval-1.78.0 → gooddata_eval-1.78.1.dev1}/tox.ini +0 -0
|
@@ -27,6 +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
31
|
| `core/dataset/` | dataset format, loading, and `from_insights.py` — dataset generation from a workspace's real insights |
|
|
31
32
|
| `core/evaluators/` | single-shot evaluators and their registry |
|
|
32
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) |
|
|
@@ -59,7 +60,8 @@ implementation, sometimes under different `test_kind` strings (`search_tool` vs
|
|
|
59
60
|
`expected_output: Any`. `expected_output` is deliberately untyped — each evaluator parses
|
|
60
61
|
its own shape. `test_kind` on the item is what labels the result, not the evaluator class,
|
|
61
62
|
which is why `knowledge_question` can reuse `GeneralQuestionEvaluator` verbatim.
|
|
62
|
-
`dashboard_summary` items additionally need `summary_input
|
|
63
|
+
`dashboard_summary` items additionally need `summary_input`, `document_takeaway` items
|
|
64
|
+
`takeaway_input`.
|
|
63
65
|
|
|
64
66
|
## Running the pipeline
|
|
65
67
|
|
|
@@ -155,8 +157,8 @@ Run this before guessing a `--model` string.
|
|
|
155
157
|
| `GD_EVAL_JUDGE_MODEL` | judge model, same as `--judge-model` |
|
|
156
158
|
| `GD_EVAL_AGENT_ID` | which agent to drive, same as `--agent-id` |
|
|
157
159
|
| `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` | `--langfuse`, `--langfuse-dataset` |
|
|
158
|
-
| `GOODDATA_EVAL_CHAT_*` |
|
|
159
|
-
| `GOODDATA_EVAL_TRACE_LABELS` | `key=value,...` labels stamped on every observation of a chat conversation via W3C baggage; `model_version` also sets the observation `version`, so set it only for a one-model run: every `--model` in one process gets the same labels. Keys must match `[A-Za-z0-9_-]+`; others are skipped with a warning. Dashboard-summary runs are not labelled |
|
|
160
|
+
| `GOODDATA_EVAL_CHAT_*` | retry and backoff knobs shared by the chat and takeaway clients (`core/_retry.py`), and the SSE turn and item timeouts |
|
|
161
|
+
| `GOODDATA_EVAL_TRACE_LABELS` | `key=value,...` labels stamped on every observation of a chat conversation via W3C baggage; `model_version` also sets the observation `version`, so set it only for a one-model run: every `--model` in one process gets the same labels. Keys must match `[A-Za-z0-9_-]+`; others are skipped with a warning. Dashboard-summary and document-takeaway runs are not labelled |
|
|
160
162
|
| `GOODDATA_EVAL_JOIN_GENAI_TRACE` | on unless set to `0`, `false`, `no`, `off` or empty. An agentic item's chat turns ask gen-ai to open them under gd-eval's experiment root, so each item is one Langfuse trace with its scores written once. A gen-ai that drops caller baggage keeps today's separate traces; see the gotcha below |
|
|
161
163
|
| `GD_EVAL_TIMERS` | same as `--timers` |
|
|
162
164
|
|
|
@@ -181,12 +183,15 @@ That default is correct: agent tool calls create real server-side objects (metri
|
|
|
181
183
|
Adding a kind to `PARALLEL_SAFE_TEST_KINDS` is a deliberate assertion that it is read-only,
|
|
182
184
|
which nothing in the package can prove for you.
|
|
183
185
|
|
|
184
|
-
**The SSE client's retry predicate is load-bearing.**
|
|
185
|
-
429/502/503/504
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
**The SSE client's retry predicate is load-bearing.** The backoff loop and the retryable
|
|
187
|
+
status set (429/500/502/503/504) live in `core/_retry.py`, shared by the chat and takeaway
|
|
188
|
+
clients and tuned by the same `GOODDATA_EVAL_CHAT_*` knobs. `core/chat/sse_client.py`
|
|
189
|
+
retries that status set and `httpx.RemoteProtocolError` (a mid-stream disconnect), and
|
|
190
|
+
treats a `METADATA_SYNC_IN_PROGRESS` payload as transient. The `RemoteProtocolError` case
|
|
191
|
+
was added after it was confirmed live to contaminate a small percentage of visualization
|
|
192
|
+
runs with a hard fail and no retry. Narrowing that predicate reintroduces the problem. The
|
|
193
|
+
takeaway client retries the same status set plus connection failures, but not a read
|
|
194
|
+
timeout: a slow page would spend the whole timeout again on every attempt.
|
|
190
195
|
|
|
191
196
|
**Langfuse trace linking is deliberately off the item critical path.** Polling for trace
|
|
192
197
|
ingestion has no pass/fail signal and inflates or misattributes per-item latency, so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.78.
|
|
3
|
+
Version: 1.78.1.dev1
|
|
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.78.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.78.1.dev1
|
|
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
|
|
@@ -161,8 +161,8 @@ gd-eval run \
|
|
|
161
161
|
| `--runs K` | `2` | Independent runs per item. |
|
|
162
162
|
| `--gate` | `any` | Which verdict decides an item: `any` = pass@K (a run passing is enough), `power` = pass^K (every run must pass, so the verdict measures stability). Identical at `--runs 1`. Kinds that repeat K runs only — `power` is refused when the dataset also has non-agentic items or `agentic_conversation`, which are always decided on pass@K. |
|
|
163
163
|
| `--concurrency K` | `1` | Number of items evaluated concurrently. `1` = sequential (default). Increase to load-test the agent under simultaneous requests — see *Concurrency and workspace safety* below. |
|
|
164
|
-
| `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail` and `
|
|
165
|
-
| `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` items go through
|
|
164
|
+
| `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail`, `dashboard_summary` and `document_takeaway`. Also settable via `GD_EVAL_JUDGE_MODEL`. Two things to weigh before changing it: the gpt-5 family rejects `temperature=0`, so verdicts stop being reproducible (the run warns when this happens); and choosing the same model the agent runs means the judge grades its own family's output. |
|
|
165
|
+
| `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` and `document_takeaway` items go through their own endpoints, which have no such option. |
|
|
166
166
|
|
|
167
167
|
**Concurrency and workspace safety.** Agentic kinds that create workspace objects
|
|
168
168
|
(`agentic_metric_skill`, `agentic_alert_skill`, `agentic_conversation`, `agentic_kda_skill`) always run one at a
|
|
@@ -736,7 +736,7 @@ A dataset is a folder of `.json` files, one per question:
|
|
|
736
736
|
```
|
|
737
737
|
|
|
738
738
|
Supported `test_kind` values: `visualization`, `metric_skill`, `alert_skill`,
|
|
739
|
-
`search_tool`, `general_question`, `guardrail`, `dashboard_summary`.
|
|
739
|
+
`search_tool`, `general_question`, `guardrail`, `dashboard_summary`, `document_takeaway`.
|
|
740
740
|
|
|
741
741
|
### `dashboard_summary` items
|
|
742
742
|
|
|
@@ -775,6 +775,64 @@ The `expected_output` rubric:
|
|
|
775
775
|
Each criterion is scored independently by the LLM judge, so `quality_score`
|
|
776
776
|
is the fraction of satisfied criteria.
|
|
777
777
|
|
|
778
|
+
### `document_takeaway` items
|
|
779
|
+
|
|
780
|
+
Takeaway items call the AI Publisher takeaway endpoint
|
|
781
|
+
(`POST /api/v1/ai/workspaces/{ws}/takeaway`) with a `takeaway_input` block, authored in
|
|
782
|
+
snake_case and sent as the endpoint's camelCase body. In a Langfuse dataset the block goes
|
|
783
|
+
in the item's `metadata` (or its `input` object), and `input` can stay a plain case label;
|
|
784
|
+
an item carrying `takeaway_input` is a `document_takeaway` item unless it declares another
|
|
785
|
+
`test_kind`. The endpoint needs the `enableGenAiReportTakeaways` feature flag.
|
|
786
|
+
|
|
787
|
+
```json
|
|
788
|
+
{
|
|
789
|
+
"id": "takeaway-001",
|
|
790
|
+
"dataset_name": "agent_document_takeaway",
|
|
791
|
+
"test_kind": "document_takeaway",
|
|
792
|
+
"question": "P01 default format, Sales page, no instructions",
|
|
793
|
+
"takeaway_input": {
|
|
794
|
+
"visualizations": ["<visualization id>", {"visualization_id": "<id>", "filters": []}],
|
|
795
|
+
"filter_context": [],
|
|
796
|
+
"instructions": {"document": null, "slot": null, "format_hint": null},
|
|
797
|
+
"max_characters": 300
|
|
798
|
+
},
|
|
799
|
+
"expected_output": {
|
|
800
|
+
"no_data": false,
|
|
801
|
+
"format": {"no_lists": false},
|
|
802
|
+
"must_include": ["States one finding that connects at least two of the charts"],
|
|
803
|
+
"must_not_include": [],
|
|
804
|
+
"rubric": []
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
`takeaway_input` requires `visualizations`; unknown keys are rejected. Every
|
|
810
|
+
`expected_output` key is optional:
|
|
811
|
+
|
|
812
|
+
- `no_data` — whether the endpoint must answer `noData`; when either side says no data,
|
|
813
|
+
nothing else is graded.
|
|
814
|
+
- `format` — overrides of the deterministic checks, which gate pass/fail: `bold_headline`,
|
|
815
|
+
`max_headline_words` (10), `max_sentences` (2), `max_words` (50), `single_paragraph`,
|
|
816
|
+
`no_lists`, `no_headings`. `false` or `null` switches a check off; unknown keys are
|
|
817
|
+
rejected. `length` (`max_characters`), `no_ids` and `no_meta` always apply.
|
|
818
|
+
- `default_rules` — `false` drops the default judge criteria (one finding; no executive
|
|
819
|
+
summary, chart walk-through, background opener, figures, dates, causes the data cannot
|
|
820
|
+
show, chart description).
|
|
821
|
+
- `must_include`, `must_not_include`, `rubric` — the item's own judge criteria, graded like
|
|
822
|
+
`dashboard_summary`.
|
|
823
|
+
|
|
824
|
+
`word_count` and `headline_word_count` are reported in the item detail. The default judge
|
|
825
|
+
criteria are calibrated with `make calibrate-takeaway` (needs `OPENAI_API_KEY`); re-run it
|
|
826
|
+
after changing their wording.
|
|
827
|
+
|
|
828
|
+
The client retries 429/5xx answers and connection failures. A request error that survives
|
|
829
|
+
the retries is handled differently by the two entry points: `gd-eval run` aborts the item
|
|
830
|
+
with that error, whatever `--runs` is, while the CI entry point
|
|
831
|
+
(`evaluate_document_takeaway`) records it as an errored run, keeps the item's other runs,
|
|
832
|
+
and raises only when no run completed. A read timeout is not retried, and the CI entry
|
|
833
|
+
point records it as an errored run too. Any other 4xx and an unsupported URL scheme are
|
|
834
|
+
never retried and fail the item in both.
|
|
835
|
+
|
|
778
836
|
### Items asked in a dashboard context (`user_context`)
|
|
779
837
|
|
|
780
838
|
An item can be asked the way a user asks from an open dashboard or an attached widget.
|
|
@@ -782,7 +840,8 @@ Its `user_context` is sent verbatim as `userContext` on every chat message of th
|
|
|
782
840
|
follow-up and clarification messages included — the server treats a message without one
|
|
783
841
|
as a cleared context. Requires the `enableAiContextSetup` feature flag on the target
|
|
784
842
|
organization — without it the server ignores the dashboard view. Applies to chat items
|
|
785
|
-
only; `dashboard_summary` items carry their scope in
|
|
843
|
+
only; `dashboard_summary` and `document_takeaway` items carry their scope in
|
|
844
|
+
`summary_input` / `takeaway_input` instead. No
|
|
786
845
|
`userContext` key is sent at all when neither the item nor, for `agentic_conversation`,
|
|
787
846
|
the current turn sets one.
|
|
788
847
|
|
|
@@ -835,6 +894,7 @@ from that turn on until another turn changes it, as the attached context does in
|
|
|
835
894
|
| `general_question` | Text answer judged by LLM | `[llm-judge]` |
|
|
836
895
|
| `guardrail` | Refusal/redirect (visualization response auto-fails) | `[llm-judge]` |
|
|
837
896
|
| `dashboard_summary` | Dashboard summary (via `/summary` endpoint) scored against a rubric by LLM | `[llm-judge]` |
|
|
897
|
+
| `document_takeaway` | One short takeaway for a page (via `/takeaway` endpoint): deterministic format checks plus a rubric scored by LLM | `[llm-judge]` |
|
|
838
898
|
|
|
839
899
|
## Optional extras
|
|
840
900
|
|
|
@@ -132,8 +132,8 @@ gd-eval run \
|
|
|
132
132
|
| `--runs K` | `2` | Independent runs per item. |
|
|
133
133
|
| `--gate` | `any` | Which verdict decides an item: `any` = pass@K (a run passing is enough), `power` = pass^K (every run must pass, so the verdict measures stability). Identical at `--runs 1`. Kinds that repeat K runs only — `power` is refused when the dataset also has non-agentic items or `agentic_conversation`, which are always decided on pass@K. |
|
|
134
134
|
| `--concurrency K` | `1` | Number of items evaluated concurrently. `1` = sequential (default). Increase to load-test the agent under simultaneous requests — see *Concurrency and workspace safety* below. |
|
|
135
|
-
| `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail` and `
|
|
136
|
-
| `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` items go through
|
|
135
|
+
| `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail`, `dashboard_summary` and `document_takeaway`. Also settable via `GD_EVAL_JUDGE_MODEL`. Two things to weigh before changing it: the gpt-5 family rejects `temperature=0`, so verdicts stop being reproducible (the run warns when this happens); and choosing the same model the agent runs means the judge grades its own family's output. |
|
|
136
|
+
| `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` and `document_takeaway` items go through their own endpoints, which have no such option. |
|
|
137
137
|
|
|
138
138
|
**Concurrency and workspace safety.** Agentic kinds that create workspace objects
|
|
139
139
|
(`agentic_metric_skill`, `agentic_alert_skill`, `agentic_conversation`, `agentic_kda_skill`) always run one at a
|
|
@@ -707,7 +707,7 @@ A dataset is a folder of `.json` files, one per question:
|
|
|
707
707
|
```
|
|
708
708
|
|
|
709
709
|
Supported `test_kind` values: `visualization`, `metric_skill`, `alert_skill`,
|
|
710
|
-
`search_tool`, `general_question`, `guardrail`, `dashboard_summary`.
|
|
710
|
+
`search_tool`, `general_question`, `guardrail`, `dashboard_summary`, `document_takeaway`.
|
|
711
711
|
|
|
712
712
|
### `dashboard_summary` items
|
|
713
713
|
|
|
@@ -746,6 +746,64 @@ The `expected_output` rubric:
|
|
|
746
746
|
Each criterion is scored independently by the LLM judge, so `quality_score`
|
|
747
747
|
is the fraction of satisfied criteria.
|
|
748
748
|
|
|
749
|
+
### `document_takeaway` items
|
|
750
|
+
|
|
751
|
+
Takeaway items call the AI Publisher takeaway endpoint
|
|
752
|
+
(`POST /api/v1/ai/workspaces/{ws}/takeaway`) with a `takeaway_input` block, authored in
|
|
753
|
+
snake_case and sent as the endpoint's camelCase body. In a Langfuse dataset the block goes
|
|
754
|
+
in the item's `metadata` (or its `input` object), and `input` can stay a plain case label;
|
|
755
|
+
an item carrying `takeaway_input` is a `document_takeaway` item unless it declares another
|
|
756
|
+
`test_kind`. The endpoint needs the `enableGenAiReportTakeaways` feature flag.
|
|
757
|
+
|
|
758
|
+
```json
|
|
759
|
+
{
|
|
760
|
+
"id": "takeaway-001",
|
|
761
|
+
"dataset_name": "agent_document_takeaway",
|
|
762
|
+
"test_kind": "document_takeaway",
|
|
763
|
+
"question": "P01 default format, Sales page, no instructions",
|
|
764
|
+
"takeaway_input": {
|
|
765
|
+
"visualizations": ["<visualization id>", {"visualization_id": "<id>", "filters": []}],
|
|
766
|
+
"filter_context": [],
|
|
767
|
+
"instructions": {"document": null, "slot": null, "format_hint": null},
|
|
768
|
+
"max_characters": 300
|
|
769
|
+
},
|
|
770
|
+
"expected_output": {
|
|
771
|
+
"no_data": false,
|
|
772
|
+
"format": {"no_lists": false},
|
|
773
|
+
"must_include": ["States one finding that connects at least two of the charts"],
|
|
774
|
+
"must_not_include": [],
|
|
775
|
+
"rubric": []
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
`takeaway_input` requires `visualizations`; unknown keys are rejected. Every
|
|
781
|
+
`expected_output` key is optional:
|
|
782
|
+
|
|
783
|
+
- `no_data` — whether the endpoint must answer `noData`; when either side says no data,
|
|
784
|
+
nothing else is graded.
|
|
785
|
+
- `format` — overrides of the deterministic checks, which gate pass/fail: `bold_headline`,
|
|
786
|
+
`max_headline_words` (10), `max_sentences` (2), `max_words` (50), `single_paragraph`,
|
|
787
|
+
`no_lists`, `no_headings`. `false` or `null` switches a check off; unknown keys are
|
|
788
|
+
rejected. `length` (`max_characters`), `no_ids` and `no_meta` always apply.
|
|
789
|
+
- `default_rules` — `false` drops the default judge criteria (one finding; no executive
|
|
790
|
+
summary, chart walk-through, background opener, figures, dates, causes the data cannot
|
|
791
|
+
show, chart description).
|
|
792
|
+
- `must_include`, `must_not_include`, `rubric` — the item's own judge criteria, graded like
|
|
793
|
+
`dashboard_summary`.
|
|
794
|
+
|
|
795
|
+
`word_count` and `headline_word_count` are reported in the item detail. The default judge
|
|
796
|
+
criteria are calibrated with `make calibrate-takeaway` (needs `OPENAI_API_KEY`); re-run it
|
|
797
|
+
after changing their wording.
|
|
798
|
+
|
|
799
|
+
The client retries 429/5xx answers and connection failures. A request error that survives
|
|
800
|
+
the retries is handled differently by the two entry points: `gd-eval run` aborts the item
|
|
801
|
+
with that error, whatever `--runs` is, while the CI entry point
|
|
802
|
+
(`evaluate_document_takeaway`) records it as an errored run, keeps the item's other runs,
|
|
803
|
+
and raises only when no run completed. A read timeout is not retried, and the CI entry
|
|
804
|
+
point records it as an errored run too. Any other 4xx and an unsupported URL scheme are
|
|
805
|
+
never retried and fail the item in both.
|
|
806
|
+
|
|
749
807
|
### Items asked in a dashboard context (`user_context`)
|
|
750
808
|
|
|
751
809
|
An item can be asked the way a user asks from an open dashboard or an attached widget.
|
|
@@ -753,7 +811,8 @@ Its `user_context` is sent verbatim as `userContext` on every chat message of th
|
|
|
753
811
|
follow-up and clarification messages included — the server treats a message without one
|
|
754
812
|
as a cleared context. Requires the `enableAiContextSetup` feature flag on the target
|
|
755
813
|
organization — without it the server ignores the dashboard view. Applies to chat items
|
|
756
|
-
only; `dashboard_summary` items carry their scope in
|
|
814
|
+
only; `dashboard_summary` and `document_takeaway` items carry their scope in
|
|
815
|
+
`summary_input` / `takeaway_input` instead. No
|
|
757
816
|
`userContext` key is sent at all when neither the item nor, for `agentic_conversation`,
|
|
758
817
|
the current turn sets one.
|
|
759
818
|
|
|
@@ -806,6 +865,7 @@ from that turn on until another turn changes it, as the attached context does in
|
|
|
806
865
|
| `general_question` | Text answer judged by LLM | `[llm-judge]` |
|
|
807
866
|
| `guardrail` | Refusal/redirect (visualization response auto-fails) | `[llm-judge]` |
|
|
808
867
|
| `dashboard_summary` | Dashboard summary (via `/summary` endpoint) scored against a rubric by LLM | `[llm-judge]` |
|
|
868
|
+
| `document_takeaway` | One short takeaway for a page (via `/takeaway` endpoint): deterministic format checks plus a rubric scored by LLM | `[llm-judge]` |
|
|
809
869
|
|
|
810
870
|
## Optional extras
|
|
811
871
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-eval"
|
|
4
|
-
version = "1.78.
|
|
4
|
+
version = "1.78.1.dev1"
|
|
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.78.
|
|
14
|
+
"gooddata-sdk~=1.78.1.dev1",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"jsonpatch>=1.33,<2.0",
|
|
17
17
|
"orjson>=3.9.15,<4.0.0",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation
|
|
2
|
+
"""Does the real judge put each calibration text on the right side of every default criterion?
|
|
3
|
+
|
|
4
|
+
tests/test_takeaway_evaluator.py pins the wiring and the criterion wording; it cannot pin
|
|
5
|
+
the verdict, because the verdict is an LLM's. This does, against the texts in
|
|
6
|
+
tests/fixtures/takeaway_calibration.json:
|
|
7
|
+
|
|
8
|
+
- a "pass" text must satisfy every default judge criterion on every run;
|
|
9
|
+
- a "fail" text must violate each criterion it names on every run.
|
|
10
|
+
|
|
11
|
+
Run it after changing any DEFAULT_MUST_INCLUDE / DEFAULT_MUST_NOT_INCLUDE wording:
|
|
12
|
+
|
|
13
|
+
OPENAI_API_KEY=... make calibrate-takeaway
|
|
14
|
+
|
|
15
|
+
It is a script rather than a test because CI has no OPENAI_API_KEY. Exits 1 on any mismatch.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from gooddata_eval.core.evaluators.takeaway import (
|
|
24
|
+
DEFAULT_MUST_INCLUDE,
|
|
25
|
+
DEFAULT_MUST_NOT_INCLUDE,
|
|
26
|
+
DocumentTakeawayEvaluator,
|
|
27
|
+
)
|
|
28
|
+
from gooddata_eval.core.models import ChatResult, DatasetItem, TakeawayInput
|
|
29
|
+
|
|
30
|
+
RUNS = 3
|
|
31
|
+
FIXTURE = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "takeaway_calibration.json"
|
|
32
|
+
|
|
33
|
+
CRITERIA = {f"default_include_{k}": k for k in DEFAULT_MUST_INCLUDE} | {
|
|
34
|
+
f"default_exclude_{k}": k for k in DEFAULT_MUST_NOT_INCLUDE
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> int:
|
|
39
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
40
|
+
raise SystemExit("OPENAI_API_KEY required")
|
|
41
|
+
fixture = json.loads(FIXTURE.read_text())
|
|
42
|
+
evaluator = DocumentTakeawayEvaluator()
|
|
43
|
+
print(f"judge model: {evaluator._positive_judge.model}, runs: {RUNS}")
|
|
44
|
+
mismatches = 0
|
|
45
|
+
for case in fixture["texts"]:
|
|
46
|
+
item = DatasetItem(
|
|
47
|
+
id=case["label"],
|
|
48
|
+
dataset_name="takeaway_calibration",
|
|
49
|
+
test_kind="document_takeaway",
|
|
50
|
+
question=fixture["question"],
|
|
51
|
+
expected_output={},
|
|
52
|
+
takeaway_input=TakeawayInput(visualizations=["calibration"]),
|
|
53
|
+
)
|
|
54
|
+
chat = ChatResult(text_response=case["text"], raw_response={"takeaway": case["text"], "noData": False})
|
|
55
|
+
satisfied = dict.fromkeys(CRITERIA.values(), 0)
|
|
56
|
+
for _ in range(RUNS):
|
|
57
|
+
detail = evaluator.evaluate(item, chat).detail
|
|
58
|
+
for key, name in CRITERIA.items():
|
|
59
|
+
satisfied[name] += detail.get(key) is True
|
|
60
|
+
want_pass = case["want"] == "pass"
|
|
61
|
+
must = dict.fromkeys(satisfied, RUNS) if want_pass else dict.fromkeys(case["criteria"], 0)
|
|
62
|
+
print(f"\n{case['label']} (want {case['want']})")
|
|
63
|
+
for name, count in satisfied.items():
|
|
64
|
+
flag = ""
|
|
65
|
+
if name in must:
|
|
66
|
+
ok = count == must[name]
|
|
67
|
+
mismatches += not ok
|
|
68
|
+
flag = "OK" if ok else "MISMATCH"
|
|
69
|
+
print(f" {name:20} satisfied {count}/{RUNS} {flag}")
|
|
70
|
+
print(f"\n{'all calibration texts on the right side' if not mismatches else f'{mismatches} mismatch(es)'}")
|
|
71
|
+
return 1 if mismatches else 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
sys.exit(main())
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import get_args
|
|
10
|
+
from typing import Any, get_args
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
13
|
from gooddata_api_client.exceptions import ApiException, ApiTypeError
|
|
@@ -36,32 +36,31 @@ from gooddata_eval.core.reporting.html_report import load_report_files, write_ht
|
|
|
36
36
|
from gooddata_eval.core.reporting.json_report import build_multi_model_report, write_multi_model_report
|
|
37
37
|
from gooddata_eval.core.runner import ItemReport, run_items
|
|
38
38
|
from gooddata_eval.core.summary.http_client import SummaryClient
|
|
39
|
+
from gooddata_eval.core.takeaway.http_client import TakeawayClient
|
|
39
40
|
from gooddata_eval.core.timing import TIMERS_ENV_VAR
|
|
40
41
|
from gooddata_eval.core.workspace import ModelResolutionError, WorkspaceModelController
|
|
41
42
|
|
|
42
43
|
_EXIT_OK = 0
|
|
43
44
|
_EXIT_OPERATIONAL_ERROR = 2
|
|
44
|
-
_SUMMARY_TEST_KIND = "dashboard_summary"
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class _RoutingBackend:
|
|
48
48
|
"""Dispatch each item to the right backend by test_kind.
|
|
49
49
|
|
|
50
|
-
`
|
|
51
|
-
|
|
50
|
+
Kinds in `by_kind` go to their dedicated endpoint client (`dashboard_summary` to the
|
|
51
|
+
summary endpoint, `document_takeaway` to the takeaway endpoint); everything else uses
|
|
52
|
+
the conversational chat endpoint.
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
|
-
def __init__(self, chat: ChatClient,
|
|
55
|
+
def __init__(self, chat: ChatClient, by_kind: dict[str, Any]):
|
|
55
56
|
self._chat = chat
|
|
56
|
-
self.
|
|
57
|
+
self._by_kind = by_kind
|
|
57
58
|
|
|
58
59
|
def ask(self, item: DatasetItem) -> ChatResult:
|
|
59
|
-
|
|
60
|
-
return self._summary.ask(item)
|
|
61
|
-
return self._chat.ask(item)
|
|
60
|
+
return self._by_kind.get(item.test_kind, self._chat).ask(item)
|
|
62
61
|
|
|
63
62
|
def close(self) -> None:
|
|
64
|
-
for backend in (self._chat, self.
|
|
63
|
+
for backend in (self._chat, *self._by_kind.values()):
|
|
65
64
|
if hasattr(backend, "close"):
|
|
66
65
|
backend.close()
|
|
67
66
|
|
|
@@ -614,7 +613,14 @@ def _run(config: RunConfig) -> int:
|
|
|
614
613
|
turn_timeout_s=config.turn_timeout_s,
|
|
615
614
|
item_timeout_s=config.item_timeout_s,
|
|
616
615
|
),
|
|
617
|
-
|
|
616
|
+
{
|
|
617
|
+
"dashboard_summary": SummaryClient(
|
|
618
|
+
host=config.host, token=config.token, workspace_id=config.workspace_id
|
|
619
|
+
),
|
|
620
|
+
"document_takeaway": TakeawayClient(
|
|
621
|
+
host=config.host, token=config.token, workspace_id=config.workspace_id
|
|
622
|
+
),
|
|
623
|
+
},
|
|
618
624
|
)
|
|
619
625
|
try:
|
|
620
626
|
single_report = run_items(
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation
|
|
2
|
+
"""Bounded exponential-backoff retry shared by the gen-ai HTTP clients.
|
|
3
|
+
|
|
4
|
+
The budget is read from the ``GOODDATA_EVAL_CHAT_*`` environment on every call, so the chat
|
|
5
|
+
and takeaway clients retune together.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
# 500 is here on evidence, not on principle: in one visualization eval batch it hard-failed
|
|
21
|
+
# 10 of 56 runs with zero retry attempts, and every affected question scored normally when the
|
|
22
|
+
# same question/model ran again the next day -- i.e. transient gen-ai faults, not deterministic
|
|
23
|
+
# server bugs. A genuinely deterministic 500 still terminates, just after the bounded backoff.
|
|
24
|
+
RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({429, 500, 502, 503, 504})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def int_env(name: str, default: int) -> int:
|
|
28
|
+
"""Read an int from the environment, falling back to ``default`` when unset or blank."""
|
|
29
|
+
raw = os.getenv(name)
|
|
30
|
+
return int(raw) if raw else default
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def float_env(name: str, default: float) -> float:
|
|
34
|
+
"""Read a float from the environment, falling back to ``default`` when unset or blank."""
|
|
35
|
+
raw = os.getenv(name)
|
|
36
|
+
return float(raw) if raw else default
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Retry budget defaults, giving a ~2 min worst-case cap per send (5/10/20/40/60s).
|
|
40
|
+
# Each is overridable via env so CI can retune without cutting a new release --
|
|
41
|
+
# read per call rather than at import, so an exported value cannot silently
|
|
42
|
+
# rewrite what a test that patches these attributes expects.
|
|
43
|
+
_MAX_RETRIES_DEFAULT = 5
|
|
44
|
+
_INITIAL_BACKOFF_S_DEFAULT = 5.0
|
|
45
|
+
_BACKOFF_FACTOR_DEFAULT = 2.0
|
|
46
|
+
_MAX_BACKOFF_S_DEFAULT = 60.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def retry_transient(operation: Callable[[], T], *, is_retryable: Callable[[Exception], bool]) -> T:
|
|
50
|
+
"""Run ``operation``; retry retryable failures with bounded exponential backoff."""
|
|
51
|
+
max_retries = int_env("GOODDATA_EVAL_CHAT_MAX_RETRIES", _MAX_RETRIES_DEFAULT)
|
|
52
|
+
delay = float_env("GOODDATA_EVAL_CHAT_INITIAL_BACKOFF_S", _INITIAL_BACKOFF_S_DEFAULT)
|
|
53
|
+
factor = float_env("GOODDATA_EVAL_CHAT_BACKOFF_FACTOR", _BACKOFF_FACTOR_DEFAULT)
|
|
54
|
+
max_backoff = float_env("GOODDATA_EVAL_CHAT_MAX_BACKOFF_S", _MAX_BACKOFF_S_DEFAULT)
|
|
55
|
+
for attempt in range(max_retries + 1): # 0..N => N retries + 1 initial attempt
|
|
56
|
+
try:
|
|
57
|
+
return operation()
|
|
58
|
+
except Exception as exc: # noqa: PERF203 — retry loop: per-attempt try/except is intentional
|
|
59
|
+
if attempt == max_retries or not is_retryable(exc):
|
|
60
|
+
raise
|
|
61
|
+
sleep_s = min(delay, max_backoff)
|
|
62
|
+
_log.warning(
|
|
63
|
+
"Transient gen-ai error (attempt %d/%d): %s; retrying in %.0fs",
|
|
64
|
+
attempt + 1,
|
|
65
|
+
max_retries + 1,
|
|
66
|
+
exc,
|
|
67
|
+
sleep_s,
|
|
68
|
+
)
|
|
69
|
+
time.sleep(sleep_s)
|
|
70
|
+
delay *= factor
|
|
71
|
+
raise AssertionError("unreachable") # loop either returns or raises
|
|
@@ -37,14 +37,36 @@ CONFIRMATION_REPLY = "Yes, go ahead."
|
|
|
37
37
|
# Asked to pick from a list (which dashboard to bind an alert to), a real user picks one.
|
|
38
38
|
NO_ANSWER_REPLY = "Use your best judgement and go ahead. If you need me to pick from a list, take the first option."
|
|
39
39
|
|
|
40
|
-
# Part of the metric and alert skills' designed flow, not a clarification
|
|
40
|
+
# Part of the metric and alert skills' designed flow, not a clarification; so is a retry of a
|
|
41
|
+
# corrected proposal. Keep `use` and `run` out: "Which one should I use?" is a choice question.
|
|
42
|
+
_RETRY_VERBS = r"retry|try\s+(?:(?:it|this|that)\s+)?(?:again|once\s+more)|resubmit"
|
|
43
|
+
_APPROVAL_VERBS = rf"go\s+ahead|proceed|create|save|update|apply|{_RETRY_VERBS}"
|
|
44
|
+
_SHOULD_I = r"\b(?:should|shall|may|can)\s+i\s+"
|
|
45
|
+
_WANT_ME_TO = r"\b(?:do\s+you\s+want|would\s+you\s+like)\s+me\s+to\s+"
|
|
41
46
|
_CONFIRMATION_RE = re.compile(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
r"|\bwould\s+you\s+like\s+me\s+to\s+(?:go\s+ahead|proceed|create|save|update|apply)"
|
|
47
|
+
rf"{_SHOULD_I}(?:{_APPROVAL_VERBS}|add|set\s+(?:it|this)\s+up)\b"
|
|
48
|
+
rf"|{_WANT_ME_TO}(?:{_APPROVAL_VERBS})\b"
|
|
45
49
|
r"|\bplease\s+confirm\b|\bconfirm\s+(?:if|whether|that)\b",
|
|
46
50
|
re.IGNORECASE,
|
|
47
51
|
)
|
|
52
|
+
# "Should I retry with gross or net revenue?" offers a choice, so it stays a question, as does
|
|
53
|
+
# a second option asked right after ("Should I retry? Or adjust the formula first?"). The
|
|
54
|
+
# clause runs to the sentence end: a dot inside a token ("2.5") does not end it. Only a
|
|
55
|
+
# prose "or"/"Or" counts: MAQL writes its OR in uppercase. An "or" that hands the turn back
|
|
56
|
+
# to the user ("..., or would you like to adjust it?", "or not?") is not a second option.
|
|
57
|
+
_NOT_AN_OPTION = (
|
|
58
|
+
r"not\b|(?:would|do)\s+you\s+(?:like|want|prefer)\s+to\b|(?:is|are)\s+there\b"
|
|
59
|
+
r"|anything\b|something\b|if\s+you\b|let\s+me\s+know\b"
|
|
60
|
+
)
|
|
61
|
+
_RETRY_CHOICE_RE = re.compile(
|
|
62
|
+
rf"(?:{_SHOULD_I}|{_WANT_ME_TO})(?:{_RETRY_VERBS})\b(?:[^?.!]|\.(?=\S))*"
|
|
63
|
+
rf"(?:[?.!]\s+)?(?:(?-i:\b[Oo]r\b)|\balternatively\b)(?!\s+(?:{_NOT_AN_OPTION}))",
|
|
64
|
+
re.IGNORECASE,
|
|
65
|
+
)
|
|
66
|
+
# Inline code, {type/id} references and quoted literals are the proposal itself; an OR
|
|
67
|
+
# inside a formula is not a choice offered to the user. A single quote counts only outside a
|
|
68
|
+
# word, so the apostrophes in "don't ... it's" do not mask the text between them.
|
|
69
|
+
_FORMULA_SPAN_RE = re.compile(r"`+[^`]*`+|\{[^{}]*\}|\"[^\"\n]*\"|(?<!\w)'[^'\n]*'(?!\w)")
|
|
48
70
|
|
|
49
71
|
# Checked after _CONFIRMATION_RE, so "please confirm" stays a confirmation.
|
|
50
72
|
_REQUEST_RE = re.compile(
|
|
@@ -77,7 +99,7 @@ def classify_reply(chat_result: ChatResult, rendered_text: str) -> ReplyKind:
|
|
|
77
99
|
text = (rendered_text or "").strip()
|
|
78
100
|
if not text:
|
|
79
101
|
return "no_action"
|
|
80
|
-
if _CONFIRMATION_RE.search(text):
|
|
102
|
+
if _CONFIRMATION_RE.search(text) and not _RETRY_CHOICE_RE.search(_FORMULA_SPAN_RE.sub(" ", text)):
|
|
81
103
|
return "confirmation"
|
|
82
104
|
if "?" in text or _REQUEST_RE.search(text):
|
|
83
105
|
return "question"
|
|
@@ -238,6 +260,33 @@ class ClarificationJudge:
|
|
|
238
260
|
|
|
239
261
|
_SIM_USER_MODEL = "gpt-4o"
|
|
240
262
|
|
|
263
|
+
# The simulated user plays a business user: choosing the formula is the assistant's job.
|
|
264
|
+
_NO_DEFINITIONS = (
|
|
265
|
+
"Never write MAQL, SQL, formulas or object definitions. If asked to approve one, reply that it is fine to go ahead."
|
|
266
|
+
)
|
|
267
|
+
# An uppercase SELECT counts once a reference, `(`, `*`, DISTINCT or a call follows it, so
|
|
268
|
+
# "SELECT the second option" and "Select the first one" are left alone; in any case it counts
|
|
269
|
+
# once an aggregation call follows, as in "select sum(revenue)". An
|
|
270
|
+
# aggregation call is a formula even without SELECT: in uppercase ("SUM (units)") or in any
|
|
271
|
+
# case with the parenthesis attached ("sum(units) / count(orders)"), so prose such as
|
|
272
|
+
# "Count (all) is fine" is left alone. Lowercase SQL counts by its shape (`*`, a column list,
|
|
273
|
+
# or a table followed by a clause), so "Select the first option from the list" is left alone.
|
|
274
|
+
_MAQL_RE = re.compile(
|
|
275
|
+
r"\bSELECT\s+(?:[{<(*]|DISTINCT\b|\w+\s*\()|(?i:\bselect\s+(?:sum|count|avg|min|max|median|distinct)\s*\()"
|
|
276
|
+
r"|(?i:\bselect\s+(?:\*|[\w.]+(?:\s*,\s*[\w.]+)+)\s+from\b)"
|
|
277
|
+
r"|(?i:\bselect\b[^?!]*?\bfrom\s+[\w.]+\s+(?:where|group\s+by|order\s+by|join)\b)"
|
|
278
|
+
r"|\b(?:SUM|COUNT|AVG|MIN|MAX|MEDIAN)\s*\(|(?i:\b(?:sum|count|avg|min|max|median)\()"
|
|
279
|
+
r"|[{<](?:metric|fact|label|attribute|dataset)/"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _without_maql(reply: str | None) -> str | None:
|
|
284
|
+
"""A reply that slipped MAQL in past the prompt leaves the choice to the assistant.
|
|
285
|
+
|
|
286
|
+
Both callers answer a question, so the fallback must answer one too, not just approve.
|
|
287
|
+
"""
|
|
288
|
+
return NO_ANSWER_REPLY if reply and _MAQL_RE.search(reply) else reply
|
|
289
|
+
|
|
241
290
|
|
|
242
291
|
def _chat(prompt_system: str, prompt_user: str) -> str | None:
|
|
243
292
|
try:
|
|
@@ -271,12 +320,12 @@ def reply_from_facts(transcript: str, question: str, facts: list[str]) -> str:
|
|
|
271
320
|
return NO_ANSWER_REPLY
|
|
272
321
|
reply = _chat(
|
|
273
322
|
"You play a business user talking to an analytics assistant. Answer its question briefly, "
|
|
274
|
-
"using ONLY the facts listed below. Never invent requirements. If the facts
|
|
275
|
-
f"the question, reply exactly: {NO_ANSWER_REPLY}",
|
|
323
|
+
f"using ONLY the facts listed below. Never invent requirements. {_NO_DEFINITIONS} If the facts "
|
|
324
|
+
f"do not answer the question, reply exactly: {NO_ANSWER_REPLY}",
|
|
276
325
|
f"Conversation so far:\n{transcript}\n\nThe assistant now asks:\n{question}\n\n"
|
|
277
326
|
"Facts you know:\n" + "\n".join(f"- {f}" for f in facts),
|
|
278
327
|
)
|
|
279
|
-
return reply or NO_ANSWER_REPLY
|
|
328
|
+
return _without_maql(reply) or NO_ANSWER_REPLY
|
|
280
329
|
|
|
281
330
|
|
|
282
331
|
def reply_restating(transcript: str, question: str) -> str:
|
|
@@ -289,7 +338,7 @@ def reply_restating(transcript: str, question: str) -> str:
|
|
|
289
338
|
"You play a business user talking to an analytics assistant. The assistant just asked for "
|
|
290
339
|
"something that was already said or found earlier in the conversation. Reply briefly, "
|
|
291
340
|
"starting with 'As I said earlier,' and repeat only that information, taken from the "
|
|
292
|
-
"conversation. Do not add anything new.",
|
|
341
|
+
f"conversation. Do not add anything new. {_NO_DEFINITIONS}",
|
|
293
342
|
f"Conversation so far:\n{transcript}\n\nThe assistant now asks:\n{question}",
|
|
294
343
|
)
|
|
295
|
-
return reply or "As I said earlier in this conversation — please use what we already established."
|
|
344
|
+
return _without_maql(reply) or "As I said earlier in this conversation — please use what we already established."
|