vis-agent 0.3.4.dev8048__tar.gz → 0.3.4.dev8055__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 (156) hide show
  1. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/PKG-INFO +1 -1
  2. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/evals.json +24 -2
  3. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/pyproject.toml +1 -1
  4. vis_agent-0.3.4.dev8055/release-tag +1 -0
  5. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/_gateway.py +10 -0
  6. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/compare.py +99 -25
  7. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/stats.py +7 -5
  8. vis_agent-0.3.4.dev8055/tests/test_evals_compare.py +196 -0
  9. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_gateway.py +5 -0
  10. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_stats.py +121 -1
  11. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_tasks.py +2 -1
  12. vis_agent-0.3.4.dev8048/release-tag +0 -1
  13. vis_agent-0.3.4.dev8048/tests/test_evals_compare.py +0 -53
  14. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/.gitignore +0 -0
  15. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/LICENSE +0 -0
  16. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/README.md +0 -0
  17. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-arguments.json +0 -0
  18. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-cases.json +0 -0
  19. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-copy.json +0 -0
  20. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-execution.json +0 -0
  21. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-groups.json +0 -0
  22. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-read-session.json +0 -0
  23. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-reads.json +0 -0
  24. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-results.json +0 -0
  25. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-stream.json +0 -0
  26. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity-tables.json +0 -0
  27. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/activity.json +0 -0
  28. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/diff.json +0 -0
  29. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/live-primitives.json +0 -0
  30. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/session-health.json +0 -0
  31. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/fixtures/view.json +0 -0
  32. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/activity.json +0 -0
  33. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/agents.json +0 -0
  34. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/automations.json +0 -0
  35. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/common.json +0 -0
  36. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/config.json +0 -0
  37. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/content.json +0 -0
  38. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/council.json +0 -0
  39. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/diff.json +0 -0
  40. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/gateway.json +0 -0
  41. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/provider.json +0 -0
  42. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/rooms.json +0 -0
  43. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/symbol.json +0 -0
  44. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/toggle.json +0 -0
  45. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/contracts/schema/view.json +0 -0
  46. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/examples/greeter/tests/test_greeter.py +0 -0
  47. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/hatch_build.py +0 -0
  48. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/__init__.py +0 -0
  49. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/_contracts.py +0 -0
  50. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/_outside.py +0 -0
  51. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/_wire.py +0 -0
  52. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/activity.py +0 -0
  53. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/__init__.py +0 -0
  54. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_agent.py +0 -0
  55. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_agents.py +0 -0
  56. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_client.py +0 -0
  57. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_council.py +0 -0
  58. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_extensions.py +0 -0
  59. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_host.py +0 -0
  60. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_install.py +0 -0
  61. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_local.py +0 -0
  62. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/engine/_models.py +0 -0
  63. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/__init__.py +0 -0
  64. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/agent.py +0 -0
  65. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/bench.py +0 -0
  66. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/check.py +0 -0
  67. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/environment.py +0 -0
  68. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/grade.py +0 -0
  69. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/harbor.py +0 -0
  70. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/judge-rubric.toml +0 -0
  71. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/judge.py +0 -0
  72. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/labels.py +0 -0
  73. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/leaderboard.py +0 -0
  74. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/lic.py +0 -0
  75. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/longmemeval.py +0 -0
  76. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/mrcr.py +0 -0
  77. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/repeats.py +0 -0
  78. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/report.py +0 -0
  79. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/reports.py +0 -0
  80. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/rounds.py +0 -0
  81. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/stages.py +0 -0
  82. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/evals/trajectory.py +0 -0
  83. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/extension.py +0 -0
  84. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/extension_package.py +0 -0
  85. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/py.typed +0 -0
  86. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/rooms.py +0 -0
  87. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/src/blockether/vis/views.py +0 -0
  88. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/conftest.py +0 -0
  89. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/evals_atif.py +0 -0
  90. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_activity.py +0 -0
  91. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_agent.py +0 -0
  92. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_agent_extension_lifecycle.py +0 -0
  93. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_agent_extensions.py +0 -0
  94. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_agents.py +0 -0
  95. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_api_guides.py +0 -0
  96. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_attachments.py +0 -0
  97. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_authoring.py +0 -0
  98. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_catalog.py +0 -0
  99. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_catalog_recipe.py +0 -0
  100. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_ci_report_recipe.py +0 -0
  101. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_client.py +0 -0
  102. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_client_extensions_engine.py +0 -0
  103. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_code_quality_recipe.py +0 -0
  104. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_contracts.py +0 -0
  105. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_council.py +0 -0
  106. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_declarations.py +0 -0
  107. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_engine.py +0 -0
  108. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_engine_docstrings.py +0 -0
  109. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_agent.py +0 -0
  110. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_bench.py +0 -0
  111. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_check.py +0 -0
  112. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_environment.py +0 -0
  113. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_grade.py +0 -0
  114. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_guards.py +0 -0
  115. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_harbor.py +0 -0
  116. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_judge.py +0 -0
  117. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_labels.py +0 -0
  118. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_lic.py +0 -0
  119. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_longmemeval.py +0 -0
  120. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_mrcr.py +0 -0
  121. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_repeats.py +0 -0
  122. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_report.py +0 -0
  123. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_rounds.py +0 -0
  124. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_stages.py +0 -0
  125. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_evals_trajectory.py +0 -0
  126. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_extension_package.py +0 -0
  127. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_extension_reference.py +0 -0
  128. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_gateway_performance.py +0 -0
  129. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_goals.py +0 -0
  130. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_install.py +0 -0
  131. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_live_primitives.py +0 -0
  132. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_live_wait.py +0 -0
  133. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_local.py +0 -0
  134. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_method_typing.py +0 -0
  135. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_models.py +0 -0
  136. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_monitor_recipe.py +0 -0
  137. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_namespace.py +0 -0
  138. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_outside.py +0 -0
  139. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_providers.py +0 -0
  140. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_read_session.py +0 -0
  141. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_record_arguments.py +0 -0
  142. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_registration.py +0 -0
  143. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_repair_hooks.py +0 -0
  144. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_result_sequence.py +0 -0
  145. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_rooms.py +0 -0
  146. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_scoped_settings.py +0 -0
  147. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_sdk_bootstrap.py +0 -0
  148. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_sdk_guide.py +0 -0
  149. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_sdk_typing.py +0 -0
  150. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_shortcuts_live.py +0 -0
  151. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_structured_live.py +0 -0
  152. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_symbol_contract.py +0 -0
  153. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_testing.py +0 -0
  154. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_text_list_arguments.py +0 -0
  155. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_tools.py +0 -0
  156. {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8055}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vis-agent
3
- Version: 0.3.4.dev8048
3
+ Version: 0.3.4.dev8055
4
4
  Summary: The Vis Python SDK — author extensions and control local or remote engines.
5
5
  Project-URL: Homepage, https://vis.blockether.com
6
6
  Project-URL: Documentation, https://vis.blockether.com
@@ -149,6 +149,12 @@
149
149
  },
150
150
  "candidate_name": {
151
151
  "type": "string"
152
+ },
153
+ "margin_points": {
154
+ "description": "The smallest change in points that matters. It splits a result without a clear change into equivalent, not worse, not better or inconclusive.",
155
+ "type": "number",
156
+ "exclusiveMinimum": 0,
157
+ "maximum": 100
152
158
  }
153
159
  },
154
160
  "required": [
@@ -649,7 +655,16 @@
649
655
  "maxItems": 2
650
656
  },
651
657
  "verdict": {
652
- "type": "string"
658
+ "type": "string",
659
+ "enum": [
660
+ "better",
661
+ "worse",
662
+ "no measurable difference",
663
+ "equivalent",
664
+ "not worse",
665
+ "not better",
666
+ "inconclusive"
667
+ ]
653
668
  },
654
669
  "detectable_change_points": {
655
670
  "type": "number"
@@ -683,6 +698,12 @@
683
698
  "items": {
684
699
  "type": "string"
685
700
  }
701
+ },
702
+ "margin_points": {
703
+ "type": [
704
+ "number",
705
+ "null"
706
+ ]
686
707
  }
687
708
  },
688
709
  "required": [
@@ -698,7 +719,8 @@
698
719
  "worse",
699
720
  "better",
700
721
  "unpaired",
701
- "caveats"
722
+ "caveats",
723
+ "margin_points"
702
724
  ],
703
725
  "additionalProperties": false
704
726
  },
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "vis-agent"
11
- version = "0.3.4.dev8048"
11
+ version = "0.3.4.dev8055"
12
12
  description = "The Vis Python SDK — author extensions and control local or remote engines."
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.12"
@@ -0,0 +1 @@
1
+ beta-6e5ce3880c1ae076ac0f06479a3186f14709b26e
@@ -68,6 +68,15 @@ def _required_text(body: Mapping, key: str) -> str:
68
68
  return value
69
69
 
70
70
 
71
+ def _number(body: Mapping, key: str) -> float | None:
72
+ value = body.get(key)
73
+ if value is None:
74
+ return None
75
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
76
+ raise ValueError(f"`{key}` must be a number.")
77
+ return float(value)
78
+
79
+
71
80
  def _cases(body: Mapping, key: str) -> dict[tuple[str, ...], tuple[int, int]]:
72
81
  rows = body.get(key)
73
82
  if not isinstance(rows, list):
@@ -114,6 +123,7 @@ def _compare(body: Mapping):
114
123
  _cases(body, "candidate"),
115
124
  baseline_name=_word(body, "baseline_name", "baseline"),
116
125
  candidate_name=_word(body, "candidate_name", "candidate"),
126
+ margin_points=_number(body, "margin_points"),
117
127
  )
118
128
 
119
129
 
@@ -3,7 +3,9 @@
3
3
  A case is a key such as `(task, agent, model)` from `case_results`, with its
4
4
  `(passed, runs)` count. A route is the agent with the model of a case. Call
5
5
  `compare_cases` with the cases of a baseline and a candidate. It pairs the cases,
6
- gives the paired difference with its 95% interval and tests each route.
6
+ gives the paired difference with its 95% interval and tests each route. Give
7
+ `margin_points`, the smallest change that matters to you, to tell an equivalent
8
+ result from a result that needs more cases.
7
9
  """
8
10
 
9
11
  from __future__ import annotations
@@ -51,13 +53,25 @@ class CaseComparison:
51
53
 
52
54
  The rates are the mean pass rates of the paired cases, in percent.
53
55
  `difference_ci95` is the Agresti-Min 95% interval of the paired difference, in
54
- points. `verdict` is `better` or `worse` only when that interval excludes zero,
55
- else `no measurable difference`. At this case count, a true change of
56
- `detectable_change_points` or more shows with 80% power. `p_value` is the two-sided
57
- exact McNemar p-value of the changed cases. With repeats, it is the exact sign test
58
- of the case changes. `routes` holds this test for each route. `worse` and `better`
59
- list the changed cases, largest change first. `unpaired` counts cases on one side
60
- only; the comparison leaves them out.
56
+ points. `verdict` is `better` or `worse` only when that interval excludes zero.
57
+ Without `margin_points`, every other result is `no measurable difference`. With
58
+ it, the position of the interval against the margin gives the verdict:
59
+
60
+ - `equivalent`: the interval is inside the margin on both sides.
61
+ - `not worse`: the interval is above the negative margin.
62
+ - `not better`: the interval is below the margin, but reaches the negative margin.
63
+ - `inconclusive`: the interval reaches both sides of the margin.
64
+
65
+ The verdict uses the 95% interval before rounding. With a margin, a `better` or
66
+ `worse` verdict gets a caveat when the change is, or can be, smaller than the margin.
67
+
68
+ At this case count, a true change of `detectable_change_points` or more shows with
69
+ about 80% power, by a normal approximation. `p_value` is the two-sided exact McNemar
70
+ p-value of the changed cases. With repeats, it is the exact sign test of the case
71
+ changes. `routes` holds this test for each route. `worse` and `better` list the
72
+ changed cases, largest change first. `unpaired` counts cases on one side only; the
73
+ comparison leaves them out. Caveats name unpaired cases, different routes, a change
74
+ smaller than the margin and a verdict that the exact test does not confirm.
61
75
  """
62
76
 
63
77
  paired_cases: int
@@ -73,6 +87,7 @@ class CaseComparison:
73
87
  better: tuple[CaseChange, ...]
74
88
  unpaired: int
75
89
  caveats: tuple[str, ...]
90
+ margin_points: float | None = None
76
91
 
77
92
 
78
93
  def _route(key: tuple[str, ...]) -> str:
@@ -87,6 +102,59 @@ def _points(change: float) -> float:
87
102
  return round(change * 100, 1)
88
103
 
89
104
 
105
+ def _crossed_caveat(base: tuple[str, ...], other: tuple[str, ...]) -> str:
106
+ if base[1] != other[1] and base[2] != other[2]:
107
+ return (
108
+ f"The sides used different agents and models, {_route(base)} and "
109
+ f"{_route(other)}. The change can come from either one."
110
+ )
111
+ if base[1] != other[1]:
112
+ return (
113
+ f"The sides used different agents, {base[1]} and {other[1]}, "
114
+ f"with the same model {base[2]}."
115
+ )
116
+ return (
117
+ f"The sides used different models, {base[2]} and {other[2]}, "
118
+ f"with the same agent {base[1]}."
119
+ )
120
+
121
+
122
+ def _margin_caveat(
123
+ verdict: str, interval: tuple[float, float], margin: float | None
124
+ ) -> str | None:
125
+ if not margin or verdict not in ("better", "worse"):
126
+ return None
127
+ low, high = interval if verdict == "better" else (-interval[1], -interval[0])
128
+ change = "gain" if verdict == "better" else "loss"
129
+ if high < margin:
130
+ return (
131
+ f"The {change} is real, but smaller than the margin of {margin:g} points."
132
+ )
133
+ if low < margin:
134
+ return (
135
+ f"The {change} is real, but it can be smaller than the margin of "
136
+ f"{margin:g} points."
137
+ )
138
+ return None
139
+
140
+
141
+ def _verdict(interval: tuple[float, float], margin: float | None) -> str:
142
+ low, high = interval
143
+ if low > 0:
144
+ return "better"
145
+ if high < 0:
146
+ return "worse"
147
+ if not margin:
148
+ return "no measurable difference"
149
+ if -margin < low and high < margin:
150
+ return "equivalent"
151
+ if low > -margin:
152
+ return "not worse"
153
+ if high < margin:
154
+ return "not better"
155
+ return "inconclusive"
156
+
157
+
90
158
  def _route_tests(
91
159
  changes: Mapping[tuple[str, ...], float], crossed: str | None
92
160
  ) -> tuple[RouteTest, ...]:
@@ -117,13 +185,18 @@ def compare_cases(
117
185
  *,
118
186
  baseline_name: str = "baseline",
119
187
  candidate_name: str = "candidate",
188
+ margin_points: float | None = None,
120
189
  ) -> CaseComparison:
121
190
  """Compare the `(passed, runs)` cases of a baseline and a candidate.
122
191
 
123
192
  Each key is `(task, agent, model)`. When the sides share no key and each side has
124
193
  one route, the comparison pairs the cases by task and adds a caveat. The names
125
- appear in error messages. Raise ValueError when no case pairs.
194
+ appear in error messages. `margin_points` is the smallest change in points that
195
+ matters, above 0 and at most 100. Raise ValueError when no case pairs, or for a
196
+ margin outside that range.
126
197
  """
198
+ if margin_points is not None and not 0 < margin_points <= 100:
199
+ raise ValueError("margin_points must be above 0 and at most 100")
127
200
  caveats = []
128
201
  crossed = None
129
202
  if not baseline.keys() & candidate.keys():
@@ -135,12 +208,10 @@ def compare_cases(
135
208
  "has more than one route. Compare results with the same agent and model."
136
209
  )
137
210
  crossed = f"{min(base_routes)} against {min(other_routes)}"
211
+ if base_routes != other_routes:
212
+ caveats.append(_crossed_caveat(next(iter(baseline)), next(iter(candidate))))
138
213
  baseline = {key[:1]: value for key, value in baseline.items()}
139
214
  candidate = {key[:1]: value for key, value in candidate.items()}
140
- caveats.append(
141
- f"The sides used different routes, {min(base_routes)} and "
142
- f"{min(other_routes)}. Model and harness differ together."
143
- )
144
215
  paired = sorted(baseline.keys() & candidate.keys())
145
216
  if not paired:
146
217
  raise ValueError(f"{baseline_name} and {candidate_name} share no case")
@@ -156,20 +227,24 @@ def compare_cases(
156
227
  )
157
228
  changed.append((change, row))
158
229
  (low, high), floor = paired_change(list(changes.values()))
159
- interval = (_points(low), _points(high))
160
- verdict = (
161
- "better"
162
- if interval[0] > 0
163
- else "worse"
164
- if interval[1] < 0
165
- else "no measurable difference"
166
- )
230
+ verdict = _verdict((low * 100, high * 100), margin_points)
167
231
  unpaired = len(baseline.keys() ^ candidate.keys())
168
232
  if unpaired:
169
233
  cases = "1 case" if unpaired == 1 else f"{unpaired} cases"
170
234
  caveats.append(f"{cases} ran on one side only. The comparison leaves them out.")
171
235
  count = len(paired)
172
236
  values = list(changes.values())
237
+ p_value = mcnemar_exact(
238
+ sum(value < 0 for value in values), sum(value > 0 for value in values)
239
+ )
240
+ if verdict in ("better", "worse") and p_value >= 0.05:
241
+ caveats.append(
242
+ f"The interval says {verdict}, but the exact test gives p = "
243
+ f"{p_value:.3f}. Confirm the change with more cases."
244
+ )
245
+ margin_caveat = _margin_caveat(verdict, (low * 100, high * 100), margin_points)
246
+ if margin_caveat:
247
+ caveats.append(margin_caveat)
173
248
 
174
249
  def rate(cases: Cases) -> float:
175
250
  return round(
@@ -181,12 +256,10 @@ def compare_cases(
181
256
  baseline_rate_percent=rate(baseline),
182
257
  candidate_rate_percent=rate(candidate),
183
258
  difference_points=round(100 * sum(values) / count, 1),
184
- difference_ci95=interval,
259
+ difference_ci95=(_points(low), _points(high)),
185
260
  verdict=verdict,
186
261
  detectable_change_points=_points(floor),
187
- p_value=mcnemar_exact(
188
- sum(value < 0 for value in values), sum(value > 0 for value in values)
189
- ),
262
+ p_value=p_value,
190
263
  routes=_route_tests(changes, crossed),
191
264
  worse=tuple(
192
265
  row for change, row in sorted(changed, key=lambda i: i[0]) if change < 0
@@ -196,4 +269,5 @@ def compare_cases(
196
269
  ),
197
270
  unpaired=unpaired,
198
271
  caveats=tuple(caveats),
272
+ margin_points=margin_points,
199
273
  )
@@ -94,8 +94,10 @@ def detectable_change(units: Sequence[tuple[int, int]]) -> float | None:
94
94
  """Return the smallest change of the pass rate that a second run of this size detects.
95
95
 
96
96
  The test compares two independent runs of the same size, two-sided at the 5% level,
97
- with 80% power. The change has steps of 0.001. None means that there are no runs, or
98
- that no change from 0 to 1 is detectable.
97
+ with 80% power. The change holds for a rise and for a fall, so the larger of the two
98
+ applies. A direction without a detectable change before 0 or 1 does not count. The
99
+ change has steps of 0.001. None means that there are no runs, or that no change
100
+ from 0 to 1 is detectable.
99
101
  """
100
102
  total = sum(runs for _, runs in units)
101
103
  if not total:
@@ -109,16 +111,16 @@ def detectable_change(units: Sequence[tuple[int, int]]) -> float | None:
109
111
  power = Z_POWER * math.sqrt((rate * (1 - rate) + other * (1 - other)) / size)
110
112
  return abs(other - rate) >= noise + power
111
113
 
112
- smallest = None
114
+ steps = []
113
115
  for sign in (1, -1):
114
116
  for step in range(1, 1001):
115
117
  other = rate + sign * step / 1000
116
118
  if not 0 <= other <= 1:
117
119
  break
118
120
  if detects(other):
119
- smallest = step if smallest is None else min(smallest, step)
121
+ steps.append(step)
120
122
  break
121
- return smallest / 1000 if smallest is not None else None
123
+ return max(steps) / 1000 if steps else None
122
124
 
123
125
 
124
126
  def paired_change(changes: Sequence[float]) -> tuple[tuple[float, float], float]:
@@ -0,0 +1,196 @@
1
+ """Tests for the paired comparison of `blockether.vis.evals`.
2
+
3
+ The calibration tests run seeded simulations. They prove the claims of the
4
+ evaluation guide on false verdicts and power.
5
+ """
6
+
7
+ import random
8
+
9
+ import pytest
10
+ from blockether.vis.evals import CaseChange, RouteTest, compare_cases
11
+
12
+ ROUTE = ("vis", "zai/glm")
13
+
14
+
15
+ def _cases(passes):
16
+ return {
17
+ (f"case-{index:02}", *ROUTE): (passed, 2) for index, passed in enumerate(passes)
18
+ }
19
+
20
+
21
+ def test_compare_cases_reports_a_measurable_drop():
22
+ found = compare_cases(_cases([2] * 12), _cases([0] * 8 + [2] * 4))
23
+ assert found.verdict == "worse"
24
+ assert (found.paired_cases, found.baseline_rate_percent) == (12, 100.0)
25
+ assert found.candidate_rate_percent == 33.3
26
+ assert found.worse[0] == CaseChange("case-00 (vis with zai/glm)", "2/2", "0/2")
27
+ assert found.better == ()
28
+ assert found.routes == (
29
+ RouteTest("vis with zai/glm", 12, 8, 0, 2 / 2**8, 2 / 2**8),
30
+ )
31
+
32
+
33
+ def test_compare_cases_without_change_has_no_verdict():
34
+ found = compare_cases(_cases([1, 2]), _cases([1, 2]))
35
+ assert found.verdict == "no measurable difference"
36
+ assert found.difference_points == 0.0
37
+ assert found.p_value == 1.0
38
+
39
+
40
+ def test_compare_cases_leaves_out_unpaired_cases():
41
+ found = compare_cases(_cases([2, 2]), _cases([2]))
42
+ assert (found.paired_cases, found.unpaired) == (1, 1)
43
+ assert found.caveats == (
44
+ "1 case ran on one side only. The comparison leaves them out.",
45
+ )
46
+
47
+
48
+ def test_compare_cases_pairs_two_single_routes_by_task():
49
+ other = {("case-00", "vis", "zai/glm-next"): (0, 1)}
50
+ found = compare_cases({("case-00", *ROUTE): (1, 1)}, other)
51
+ assert found.worse == (CaseChange("case-00", "1/1", "0/1"),)
52
+ assert found.routes[0].route == "vis with zai/glm against vis with zai/glm-next"
53
+ assert found.caveats == (
54
+ "The sides used different models, zai/glm and zai/glm-next, "
55
+ "with the same agent vis.",
56
+ )
57
+
58
+
59
+ @pytest.mark.parametrize(
60
+ ("other", "caveat"),
61
+ [
62
+ (
63
+ ("codex", "zai/glm"),
64
+ "The sides used different agents, vis and codex, with the same model "
65
+ "zai/glm.",
66
+ ),
67
+ (
68
+ ("codex", "openai/gpt"),
69
+ "The sides used different agents and models, vis with zai/glm and codex "
70
+ "with openai/gpt. The change can come from either one.",
71
+ ),
72
+ ],
73
+ )
74
+ def test_the_route_caveat_names_only_the_parts_that_differ(other, caveat):
75
+ found = compare_cases({("case-00", *ROUTE): (1, 1)}, {("case-00", *other): (1, 1)})
76
+ assert found.caveats == (caveat,)
77
+
78
+
79
+ def test_compare_cases_needs_a_shared_case():
80
+ two_routes = {("a", *ROUTE): (1, 1), ("a", "vis", "other"): (1, 1)}
81
+ with pytest.raises(ValueError, match="more than one route"):
82
+ compare_cases(two_routes, {("b", "x", "y"): (1, 1)}, candidate_name="run-2")
83
+
84
+
85
+ def _changes(changes):
86
+ baseline, candidate = {}, {}
87
+ for index, change in enumerate(changes):
88
+ key = (f"case-{index:03}", *ROUTE)
89
+ baseline[key] = (1 if change < 0 else 0, 1)
90
+ candidate[key] = (1 if change > 0 else 0, 1)
91
+ return baseline, candidate
92
+
93
+
94
+ @pytest.mark.parametrize(
95
+ ("changes", "verdict"),
96
+ [
97
+ ([0] * 100, "equivalent"),
98
+ ([0] * 4, "inconclusive"),
99
+ ([1] * 3 + [0] * 57, "not worse"),
100
+ ([-1] * 3 + [0] * 57, "not better"),
101
+ ],
102
+ )
103
+ def test_a_margin_tells_an_equivalent_result_from_too_few_cases(changes, verdict):
104
+ found = compare_cases(*_changes(changes), margin_points=10)
105
+ assert (found.verdict, found.margin_points) == (verdict, 10)
106
+ assert compare_cases(*_changes(changes)).verdict == "no measurable difference"
107
+
108
+
109
+ @pytest.mark.parametrize("margin", [-1, 0, 101])
110
+ def test_compare_cases_refuses_a_margin_outside_0_to_100(margin):
111
+ with pytest.raises(ValueError, match="margin_points"):
112
+ compare_cases(*_changes([0]), margin_points=margin)
113
+
114
+
115
+ @pytest.mark.parametrize(
116
+ ("change", "margin", "caveat"),
117
+ [
118
+ (1, 15, "The gain is real, but smaller than the margin of 15 points."),
119
+ (
120
+ 1,
121
+ 10,
122
+ "The gain is real, but it can be smaller than the margin of 10 points.",
123
+ ),
124
+ (-1, 15, "The loss is real, but smaller than the margin of 15 points."),
125
+ ],
126
+ )
127
+ def test_a_verdict_inside_the_margin_gets_a_caveat(change, margin, caveat):
128
+ # The 95% interval of 40 changed cases in 400 is 7.0 to 12.9 points.
129
+ found = compare_cases(*_changes([change] * 40 + [0] * 360), margin_points=margin)
130
+ verdict = "better" if change > 0 else "worse"
131
+ assert (found.verdict, found.caveats) == (verdict, (caveat,))
132
+
133
+
134
+ def test_a_change_beyond_the_margin_gets_no_margin_caveat():
135
+ found = compare_cases(*_changes([1] * 120 + [0] * 280), margin_points=10)
136
+ assert (found.verdict, found.caveats) == ("better", ())
137
+
138
+
139
+ def test_compare_cases_names_a_verdict_that_the_exact_test_does_not_confirm():
140
+ # 12 tasks: 6 tasks became better and 1 worse, as in a small agent comparison.
141
+ found = compare_cases(*_changes([1] * 6 + [-1] + [0] * 5))
142
+ assert (found.verdict, found.p_value) == ("better", 0.125)
143
+ assert found.caveats == (
144
+ "The interval says better, but the exact test gives p = 0.125. "
145
+ "Confirm the change with more cases.",
146
+ )
147
+
148
+
149
+ def _simulate(rng, tasks, change, margin=None, runs=1):
150
+ rates = [rng.uniform(0.3, 0.7) for _ in range(tasks)]
151
+
152
+ def side(shift):
153
+ return {
154
+ (f"t{index}", *ROUTE): (
155
+ sum(rng.random() < rate + shift for _ in range(runs)),
156
+ runs,
157
+ )
158
+ for index, rate in enumerate(rates)
159
+ }
160
+
161
+ return compare_cases(side(0.0), side(change), margin_points=margin)
162
+
163
+
164
+ @pytest.mark.parametrize(
165
+ ("tasks", "runs"), [(12, 1), (16, 1), (30, 1), (30, 3), (80, 1)]
166
+ )
167
+ def test_without_a_change_a_verdict_is_rare(tasks, runs):
168
+ rng = random.Random(tasks * runs)
169
+ trials = 1000
170
+ verdicts = sum(
171
+ _simulate(rng, tasks, 0.0, runs=runs).verdict != "no measurable difference"
172
+ for _ in range(trials)
173
+ )
174
+ assert verdicts / trials <= 0.06
175
+
176
+
177
+ def test_the_detectable_change_shows_with_80_percent_power():
178
+ rng = random.Random(3)
179
+ floor = _simulate(random.Random(1), 40, 0.0).detectable_change_points
180
+ trials = 600
181
+ better = sum(
182
+ _simulate(rng, 40, floor / 100).verdict == "better" for _ in range(trials)
183
+ )
184
+ assert better / trials >= 0.80
185
+
186
+
187
+ @pytest.mark.parametrize("tasks", [60, 200])
188
+ def test_a_candidate_worse_by_the_margin_is_rarely_called_not_worse(tasks):
189
+ rng = random.Random(5)
190
+ trials = 600
191
+ wrong = sum(
192
+ _simulate(rng, tasks, -0.10, margin=10).verdict
193
+ in ("better", "equivalent", "not worse")
194
+ for _ in range(trials)
195
+ )
196
+ assert wrong / trials <= 0.04
@@ -50,6 +50,10 @@ def test_compare_pairs_the_cases():
50
50
  )["result"]
51
51
  assert result["paired_cases"] == 20
52
52
  assert result["verdict"] == "better"
53
+ same = side([1] * 60)
54
+ body = {"baseline": same, "candidate": same, "margin_points": 5}
55
+ result = roundtrip("compare", body)["result"]
56
+ assert (result["verdict"], result["margin_points"]) == ("equivalent", 5.0)
53
57
 
54
58
 
55
59
  def test_agreement_gives_the_kappa_advice():
@@ -83,6 +87,7 @@ def test_leaderboard_reads_every_page(monkeypatch):
83
87
  ("report", [], "must be a JSON object"),
84
88
  ("report", {}, "`summary` must be an object"),
85
89
  ("compare", {"baseline": [{}], "candidate": []}, "needs task, agent"),
90
+ ("compare", {"baseline": [], "candidate": [], "margin_points": "5"}, "number"),
86
91
  ("agreement", {"items": [{"name": "x"}]}, "needs name, automatic"),
87
92
  ("leaderboard", {"name": "1-0-0"}, "`package` is required"),
88
93
  ("leaderboard", {"package": "org/bench"}, "`name` is required"),