gooddata-eval 1.76.1.dev2__tar.gz → 1.76.1.dev3__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.
Files changed (135) hide show
  1. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/AGENTS.md +1 -0
  2. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/PKG-INFO +2 -2
  3. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/pyproject.toml +2 -2
  4. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/chat/sse_client.py +43 -1
  5. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_sse_client.py +77 -0
  6. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/.gitignore +0 -0
  7. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/CLAUDE.md +0 -0
  8. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/LICENSE.txt +0 -0
  9. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/Makefile +0 -0
  10. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/README.md +0 -0
  11. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/scripts/verify_guardrail_refusal_criteria.py +0 -0
  12. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/__init__.py +0 -0
  13. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/_version.py +0 -0
  14. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/cli/__init__.py +0 -0
  15. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/cli/agentic_runner.py +0 -0
  16. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/cli/main.py +0 -0
  17. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/__init__.py +0 -0
  18. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/_output.py +0 -0
  19. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  20. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  21. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
  22. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/_gate.py +0 -0
  23. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  24. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
  25. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/alert_skill.py +0 -0
  26. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/anomaly_detection.py +0 -0
  27. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/conversation.py +0 -0
  28. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/dashboard_skill.py +0 -0
  29. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/general_question.py +0 -0
  30. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/guardrail.py +0 -0
  31. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/kda_skill.py +0 -0
  32. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/metric_skill.py +0 -0
  33. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
  34. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/search_tool.py +0 -0
  35. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/visualization.py +0 -0
  36. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/agentic/what_if.py +0 -0
  37. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/chat/__init__.py +0 -0
  38. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/chat/render.py +0 -0
  39. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/config.py +0 -0
  40. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/connection.py +0 -0
  41. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  42. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
  43. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
  44. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/dataset/local.py +0 -0
  45. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  46. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  47. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
  48. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
  49. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
  50. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  51. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  52. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/base.py +0 -0
  53. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  54. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  55. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  56. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  57. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  58. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  59. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/granularity.py +0 -0
  60. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  61. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/_env.py +0 -0
  62. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/client.py +0 -0
  63. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
  64. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/observations.py +0 -0
  65. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
  66. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  67. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/models.py +0 -0
  68. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  69. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/reporting/console.py +0 -0
  70. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/reporting/html_report.py +0 -0
  71. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/reporting/json_report.py +0 -0
  72. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/reporting/report_template.html +0 -0
  73. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/runner.py +0 -0
  74. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/scoring.py +0 -0
  75. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/summary/__init__.py +0 -0
  76. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/summary/http_client.py +0 -0
  77. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/timing.py +0 -0
  78. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/src/gooddata_eval/core/workspace.py +0 -0
  79. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/__init__.py +0 -0
  80. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/_fake_langfuse.py +0 -0
  81. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/conftest.py +0 -0
  82. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  83. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  84. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/fixtures/sse_visualization_stream.txt +0 -0
  85. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_alert_skill.py +0 -0
  86. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_anomaly_detection.py +0 -0
  87. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_conversation.py +0 -0
  88. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_dashboard_skill.py +0 -0
  89. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_gate.py +0 -0
  90. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_general_question.py +0 -0
  91. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_guardrail.py +0 -0
  92. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_kda_skill.py +0 -0
  93. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_langfuse_trace.py +0 -0
  94. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_metric_skill.py +0 -0
  95. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_observe_experiment.py +0 -0
  96. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_report_skill.py +0 -0
  97. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_run_context.py +0 -0
  98. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_runner.py +0 -0
  99. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_search_tool.py +0 -0
  100. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_visualization.py +0 -0
  101. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_agentic_what_if.py +0 -0
  102. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_alert_skill_evaluator.py +0 -0
  103. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_chat_render.py +0 -0
  104. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_cli.py +0 -0
  105. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_connection.py +0 -0
  106. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_deep_subset.py +0 -0
  107. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_fake_langfuse.py +0 -0
  108. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_from_insights.py +0 -0
  109. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_guardrail_criteria.py +0 -0
  110. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_html_report.py +0 -0
  111. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_client.py +0 -0
  112. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_e2e_fake_server.py +0 -0
  113. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_env.py +0 -0
  114. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_experiment.py +0 -0
  115. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_observations.py +0 -0
  116. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_otlp.py +0 -0
  117. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_sink.py +0 -0
  118. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_langfuse_source.py +0 -0
  119. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_llm_judge.py +0 -0
  120. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_local_loader.py +0 -0
  121. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_maql_normalize.py +0 -0
  122. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_metric_skill_evaluator.py +0 -0
  123. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_models.py +0 -0
  124. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_reporting.py +0 -0
  125. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_runner.py +0 -0
  126. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_scoring.py +0 -0
  127. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_search_tool_evaluator.py +0 -0
  128. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_summary_client.py +0 -0
  129. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_summary_evaluator.py +0 -0
  130. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_text_evaluators.py +0 -0
  131. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_timing.py +0 -0
  132. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_trace_linker.py +0 -0
  133. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_visualization_evaluator.py +0 -0
  134. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tests/test_workspace.py +0 -0
  135. {gooddata_eval-1.76.1.dev2 → gooddata_eval-1.76.1.dev3}/tox.ini +0 -0
@@ -156,6 +156,7 @@ Run this before guessing a `--model` string.
156
156
  | `GD_EVAL_AGENT_ID` | which agent to drive, same as `--agent-id` |
157
157
  | `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` | `--langfuse`, `--langfuse-dataset` |
158
158
  | `GOODDATA_EVAL_CHAT_*` | SSE retry, backoff and timeout knobs |
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 |
159
160
  | `GD_EVAL_TIMERS` | same as `--timers` |
160
161
 
161
162
  A gitignored `.env` at the repo root is the normal place for these; load it with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gooddata-eval
3
- Version: 1.76.1.dev2
3
+ Version: 1.76.1.dev3
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.76.1.dev2
20
+ Requires-Dist: gooddata-sdk~=1.76.1.dev3
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
@@ -1,7 +1,7 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-eval"
4
- version = "1.76.1.dev2"
4
+ version = "1.76.1.dev3"
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.76.1.dev2",
14
+ "gooddata-sdk~=1.76.1.dev3",
15
15
  "httpx>=0.27,<1.0",
16
16
  "jsonpatch>=1.33,<2.0",
17
17
  "orjson>=3.9.15,<4.0.0",
@@ -13,12 +13,15 @@ module is the single place to swap — the runner only depends on the ChatBacken
13
13
  protocol, not on this class.
14
14
  """
15
15
 
16
+ import functools
16
17
  import json
17
18
  import logging
18
19
  import os
20
+ import re
19
21
  import time
20
22
  from dataclasses import dataclass, field
21
23
  from typing import Any, Callable, Iterable, TypeVar
24
+ from urllib.parse import quote
22
25
 
23
26
  import httpx
24
27
 
@@ -135,6 +138,45 @@ _TURN_TIMEOUT_S = _float_env("GOODDATA_EVAL_CHAT_TURN_TIMEOUT_S", 0.0)
135
138
  # 4-turn conversation run to 4x the budget, which is not what a user would sit through.
136
139
  _ITEM_TIMEOUT_S = _float_env("GOODDATA_EVAL_CHAT_ITEM_TIMEOUT_S", 0.0)
137
140
 
141
+ # Comma-separated `key=value` labels, e.g. `model_version=gpt-5.2,github_run_id=42`, sent as
142
+ # W3C baggage on every chat request. gen-ai's Langfuse span processor copies `langfuse_*`
143
+ # baggage onto each span it starts, so every observation of the conversation -- the cost-bearing
144
+ # generations, retries and the title trace included -- carries them from creation. Langfuse v4
145
+ # cannot add them to an observation afterwards. A value cannot contain a comma, and a key must be
146
+ # a plain token (letters, digits, `_`, `-`); any other key is skipped with a warning, because httpx
147
+ # refuses a non-ASCII header and gen-ai's OTel propagator drops or rewrites the rest.
148
+ _TRACE_LABELS_ENV = "GOODDATA_EVAL_TRACE_LABELS"
149
+ _TRACE_LABEL_KEY = re.compile(r"[A-Za-z0-9_-]+")
150
+
151
+
152
+ @functools.cache
153
+ def _warn_skipped_trace_label(key: str) -> None:
154
+ """Warn once per key per process: every ChatClient re-reads the labels."""
155
+ _log.warning("%s: skipping label with key %r, keys must match [A-Za-z0-9_-]+", _TRACE_LABELS_ENV, key)
156
+
157
+
158
+ def _trace_baggage() -> dict[str, str]:
159
+ """The `baggage` header for the labels in ``GOODDATA_EVAL_TRACE_LABELS``, or none.
160
+
161
+ Each label becomes trace metadata, and ``model_version`` -- the combo the report groups
162
+ on -- also becomes the observation ``version``, the dimension Langfuse groups cost by.
163
+ Values are percent-encoded: a space or comma would otherwise end the baggage entry.
164
+ """
165
+ labels: dict[str, str] = {}
166
+ for pair in os.getenv(_TRACE_LABELS_ENV, "").split(","):
167
+ key, sep, value = pair.partition("=")
168
+ key = key.strip()
169
+ if not sep or not key:
170
+ continue
171
+ if _TRACE_LABEL_KEY.fullmatch(key):
172
+ labels[key] = value.strip()
173
+ else:
174
+ _warn_skipped_trace_label(key)
175
+ entries = [f"langfuse_metadata_{k}={quote(v, safe='')}" for k, v in labels.items()]
176
+ if labels.get("model_version"):
177
+ entries.insert(0, f"langfuse_version={quote(labels['model_version'], safe='')}")
178
+ return {"baggage": ",".join(entries)} if entries else {}
179
+
138
180
 
139
181
  def set_default_turn_timeout(seconds: float | None) -> None:
140
182
  """Set the per-turn budget every ChatClient built afterwards inherits.
@@ -470,7 +512,7 @@ class ChatClient:
470
512
  the ``enableAiContextSetup`` feature flag is on for the organization.
471
513
  """
472
514
  self._base = f"{host.rstrip('/')}/api/v1/ai/workspaces/{workspace_id}/chat/conversations"
473
- self._auth = {"Authorization": f"Bearer {token}"}
515
+ self._auth = {"Authorization": f"Bearer {token}", **_trace_baggage()}
474
516
  # 0/None disables the cap. Also lowered onto the read timeout: the wall-clock check
475
517
  # fires between events, so a turn that goes silent needs the transport to give up too.
476
518
  budget = _TURN_TIMEOUT_S if turn_timeout_s is None else turn_timeout_s
@@ -1,5 +1,8 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  import json
3
+ import logging
4
+ from collections.abc import Callable
5
+ from urllib.parse import unquote
3
6
 
4
7
  import httpx
5
8
  import pytest
@@ -1053,3 +1056,77 @@ def test_ask_puts_the_item_attachment_on_the_wire():
1053
1056
  )
1054
1057
  client.ask(item)
1055
1058
  assert captured["body"]["userContext"] == _ATTACHMENT
1059
+
1060
+
1061
+ def _baggage_of(requests: list[httpx.Request]) -> list[dict[str, str] | None]:
1062
+ """Each request's `baggage` header as {key: decoded value}, or None when absent."""
1063
+ out: list[dict[str, str] | None] = []
1064
+ for r in requests:
1065
+ raw = r.headers.get("baggage")
1066
+ out.append(None if raw is None else {k: unquote(v) for k, v in (e.split("=", 1) for e in raw.split(","))})
1067
+ return out
1068
+
1069
+
1070
+ def _record_requests(requests: list[httpx.Request]) -> Callable[[httpx.Request], httpx.Response]:
1071
+ def handler(request: httpx.Request) -> httpx.Response:
1072
+ requests.append(request)
1073
+ if request.url.path.endswith("/conversations"):
1074
+ return httpx.Response(200, json={"conversationId": "c1"})
1075
+ return httpx.Response(200, content=_OK_SSE)
1076
+
1077
+ return handler
1078
+
1079
+
1080
+ def test_trace_labels_ride_every_request_as_langfuse_baggage(monkeypatch: pytest.MonkeyPatch) -> None:
1081
+ """gen-ai's Langfuse span processor copies `langfuse_*` baggage onto every span it starts, so
1082
+ the labels reach the root generation and its cost-bearing children at creation."""
1083
+ monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", "model_version=gpt-5.2 openai,github_run_id=42")
1084
+ requests: list[httpx.Request] = []
1085
+ client = _client_with_handler(_record_requests(requests))
1086
+
1087
+ client.send_message(client.create_conversation(), "q")
1088
+
1089
+ expected = {
1090
+ "langfuse_version": "gpt-5.2 openai",
1091
+ "langfuse_metadata_model_version": "gpt-5.2 openai",
1092
+ "langfuse_metadata_github_run_id": "42",
1093
+ }
1094
+ assert _baggage_of(requests) == [expected, expected]
1095
+ # Encoded, not raw: a space or comma in a value would otherwise split or end the entry.
1096
+ assert " " not in requests[0].headers["baggage"]
1097
+
1098
+
1099
+ def test_trace_labels_without_a_model_version_set_no_langfuse_version(monkeypatch: pytest.MonkeyPatch) -> None:
1100
+ monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", "github_run_id=42")
1101
+ requests: list[httpx.Request] = []
1102
+ _client_with_handler(_record_requests(requests)).create_conversation()
1103
+
1104
+ assert _baggage_of(requests) == [{"langfuse_metadata_github_run_id": "42"}]
1105
+
1106
+
1107
+ @pytest.mark.parametrize("key", ["modèle", "run id", "c++"])
1108
+ def test_trace_label_with_a_non_token_key_is_skipped_with_one_warning(
1109
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, key: str
1110
+ ) -> None:
1111
+ """httpx refuses a non-ASCII header, gen-ai's OTel propagator drops a key with a space, and
1112
+ it reads `+` as a space; each would otherwise fail the run or mislabel the trace."""
1113
+ monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", f"github_run_id=42,{key}=1")
1114
+ requests: list[httpx.Request] = []
1115
+
1116
+ for _ in range(2):
1117
+ _client_with_handler(_record_requests(requests)).create_conversation()
1118
+
1119
+ assert _baggage_of(requests) == [{"langfuse_metadata_github_run_id": "42"}] * 2
1120
+ assert [r.levelno for r in caplog.records if repr(key) in r.getMessage()] == [logging.WARNING]
1121
+
1122
+
1123
+ @pytest.mark.parametrize("raw", [None, "", "no-equals-sign", " , "])
1124
+ def test_no_trace_labels_send_no_baggage(monkeypatch: pytest.MonkeyPatch, raw: str | None) -> None:
1125
+ if raw is None:
1126
+ monkeypatch.delenv("GOODDATA_EVAL_TRACE_LABELS", raising=False)
1127
+ else:
1128
+ monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", raw)
1129
+ requests: list[httpx.Request] = []
1130
+ _client_with_handler(_record_requests(requests)).create_conversation()
1131
+
1132
+ assert _baggage_of(requests) == [None]