benchmax 0.1.2.dev35__tar.gz → 0.1.2.dev36__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 (214) hide show
  1. {benchmax-0.1.2.dev35/src/benchmax.egg-info → benchmax-0.1.2.dev36}/PKG-INFO +1 -1
  2. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/pyproject.toml +4 -2
  3. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/__init__.py +2 -0
  4. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/_output.py +17 -0
  5. benchmax-0.1.2.dev36/src/benchmax/cli/_preflight.py +104 -0
  6. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/_project.py +55 -5
  7. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/corpus.py +101 -1
  8. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/data.py +124 -3
  9. benchmax-0.1.2.dev36/src/benchmax/cli/dataview.py +436 -0
  10. benchmax-0.1.2.dev36/src/benchmax/cli/doctor.py +110 -0
  11. benchmax-0.1.2.dev36/src/benchmax/cli/launch.py +445 -0
  12. benchmax-0.1.2.dev36/src/benchmax/cli/runs.py +392 -0
  13. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/CLAUDE.md +162 -0
  14. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/scaffold/STARTER.md +16 -11
  15. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/generic_eval_dataset.jsonl +2 -0
  16. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/generic_main.py +301 -0
  17. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/generic_train_dataset.jsonl +3 -0
  18. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/rag_eval_dataset.jsonl +1 -0
  19. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/rag_main.py +508 -0
  20. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/rag_train_dataset.jsonl +2 -0
  21. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/scaffold/skills/design-environment/SKILL.md +113 -60
  22. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/scaffold/skills/generate-data/SKILL.md +42 -39
  23. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/skills/launch-run/SKILL.md +130 -0
  24. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/skills/verify-environment/SKILL.md +228 -0
  25. benchmax-0.1.2.dev36/src/benchmax/cli/scaffold/skills/view-progress/SKILL.md +173 -0
  26. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/setup.py +107 -33
  27. benchmax-0.1.2.dev36/src/benchmax/cli/templates/viewer.html +485 -0
  28. benchmax-0.1.2.dev36/src/benchmax/cli/validate.py +775 -0
  29. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/base_env.py +25 -0
  30. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/postgres_search/search_env.py +226 -107
  31. benchmax-0.1.2.dev36/src/benchmax/platform/browser.py +32 -0
  32. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/client.py +123 -7
  33. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/login.py +2 -24
  34. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/validation.py +62 -6
  35. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36/src/benchmax.egg-info}/PKG-INFO +1 -1
  36. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax.egg-info/SOURCES.txt +11 -1
  37. benchmax-0.1.2.dev35/src/benchmax/cli/launch.py +0 -241
  38. benchmax-0.1.2.dev35/src/benchmax/cli/runs.py +0 -187
  39. benchmax-0.1.2.dev35/src/benchmax/cli/scaffold/CLAUDE.md +0 -132
  40. benchmax-0.1.2.dev35/src/benchmax/cli/scaffold/rag_run.py +0 -72
  41. benchmax-0.1.2.dev35/src/benchmax/cli/scaffold/skills/launch-run/SKILL.md +0 -68
  42. benchmax-0.1.2.dev35/src/benchmax/cli/scaffold/skills/verify-environment/SKILL.md +0 -199
  43. benchmax-0.1.2.dev35/src/benchmax/cli/scaffold/skills/view-progress/SKILL.md +0 -63
  44. benchmax-0.1.2.dev35/src/benchmax/cli/validate.py +0 -448
  45. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/LICENSE +0 -0
  46. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/README.md +0 -0
  47. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/setup.cfg +0 -0
  48. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/bundle.py +0 -0
  49. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/_auth.py +0 -0
  50. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/_client.py +0 -0
  51. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/_providers.py +0 -0
  52. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/control.py +0 -0
  53. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/help.py +0 -0
  54. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/cli/scaffold/__init__.py +0 -0
  55. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/config.py +0 -0
  56. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/__init__.py +0 -0
  57. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/crm/crm_env.py +0 -0
  58. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/crm/workdir/reward_fn.py +0 -0
  59. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/example_id.py +0 -0
  60. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/excel/data_utils.py +0 -0
  61. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/excel/excel_env.py +0 -0
  62. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/excel/workdir/__init__.py +0 -0
  63. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/excel/workdir/excel_code_runner_mcp.py +0 -0
  64. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/excel/workdir/excel_utils.py +0 -0
  65. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/excel/workdir/reward_fn.py +0 -0
  66. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/logging.py +0 -0
  67. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/math/math_env.py +0 -0
  68. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/math/workdir/reward_fn.py +0 -0
  69. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/__init__.py +0 -0
  70. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/example_workdir/demo_mcp_server.py +0 -0
  71. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/example_workdir/reward_fn.py +0 -0
  72. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/parallel_mcp_env.py +0 -0
  73. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/provisioners/__init__.py +0 -0
  74. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/provisioners/base_provisioner.py +0 -0
  75. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/provisioners/local_provisioner.py +0 -0
  76. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/provisioners/manual_provisioner.py +0 -0
  77. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/provisioners/skypilot_provisioner.py +0 -0
  78. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/provisioners/utils.py +0 -0
  79. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/proxy_server.py +0 -0
  80. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/server_pool.py +0 -0
  81. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/mcp/utils.py +0 -0
  82. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/postgres_search/__init__.py +0 -0
  83. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/postgres_search/linker_env.py +0 -0
  84. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/reward_helpers.py +0 -0
  85. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/telestich/example.py +0 -0
  86. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/telestich/telestich_env.py +0 -0
  87. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/types.py +0 -0
  88. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/wikipedia/utils.py +0 -0
  89. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/envs/wikipedia/wiki_env.py +0 -0
  90. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/__init__.py +0 -0
  91. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/caller.py +0 -0
  92. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/clients.py +0 -0
  93. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/example_usage.py +0 -0
  94. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/inspector.py +0 -0
  95. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/models.py +0 -0
  96. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/multi_model/pricing.py +0 -0
  97. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/__init__.py +0 -0
  98. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/credentials.py +0 -0
  99. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/device_auth.py +0 -0
  100. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/exceptions.py +0 -0
  101. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/platform/training_run.py +0 -0
  102. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/prompts/__init__.py +0 -0
  103. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/prompts/tools.py +0 -0
  104. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/chunkers/__init__.py +0 -0
  105. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/chunkers/email.py +0 -0
  106. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/chunkers/inspector.py +0 -0
  107. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/chunkers/markdown.py +0 -0
  108. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/chunkers/models.py +0 -0
  109. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/chunkers/storage.py +0 -0
  110. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/__init__.py +0 -0
  111. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/chroma/__init__.py +0 -0
  112. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/chroma/client.py +0 -0
  113. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/chroma/files.py +0 -0
  114. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/chroma/filter_mapper.py +0 -0
  115. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/chroma/search.py +0 -0
  116. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/chroma/source.py +0 -0
  117. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/embed.py +0 -0
  118. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/pinecone/__init__.py +0 -0
  119. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/pinecone/files.py +0 -0
  120. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/pinecone/filter_mapper.py +0 -0
  121. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/pinecone/index_client.py +0 -0
  122. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/pinecone/search.py +0 -0
  123. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/pinecone/source.py +0 -0
  124. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/__init__.py +0 -0
  125. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/client.py +0 -0
  126. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/exceptions.py +0 -0
  127. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/filter_mapper.py +0 -0
  128. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/models.py +0 -0
  129. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/search.py +0 -0
  130. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/postgres/source.py +0 -0
  131. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/search_client.py +0 -0
  132. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/search_schema/__init__.py +0 -0
  133. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/search_schema/builders.py +0 -0
  134. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/search_schema/dsl_parser.py +0 -0
  135. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/search_schema/search_exceptions.py +0 -0
  136. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/search_schema/search_types.py +0 -0
  137. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/source.py +0 -0
  138. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/turbopuffer/__init__.py +0 -0
  139. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/turbopuffer/files.py +0 -0
  140. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/turbopuffer/filter_mapper.py +0 -0
  141. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/turbopuffer/namespace.py +0 -0
  142. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/turbopuffer/search.py +0 -0
  143. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/corpus/turbopuffer/source.py +0 -0
  144. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/__init__.py +0 -0
  145. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/__init__.py +0 -0
  146. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/clean_bodies.py +0 -0
  147. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/dedupe.py +0 -0
  148. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/filter_automated_email_qas.py +0 -0
  149. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/filter_automated_emails.py +0 -0
  150. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/mbox.py +0 -0
  151. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/preprocess/email/schema.py +0 -0
  152. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/__init__.py +0 -0
  153. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/anchor_selector.py +0 -0
  154. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/auto_tune.py +0 -0
  155. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/batch_processor.py +0 -0
  156. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/checkpoint.py +0 -0
  157. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/corpus_capabilities.py +0 -0
  158. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/corpus_profile.py +0 -0
  159. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/__init__.py +0 -0
  160. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/deterministic_guards.py +0 -0
  161. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/env_rollout.py +0 -0
  162. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/grounding_llm.py +0 -0
  163. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/hop_count_validity.py +0 -0
  164. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/quality_gate.py +0 -0
  165. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/filters/retrieval_llm.py +0 -0
  166. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/formatters/__init__.py +0 -0
  167. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/formatters/train_eval.py +0 -0
  168. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/generated_qa.py +0 -0
  169. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/generators/__init__.py +0 -0
  170. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/generators/direct_llm.py +0 -0
  171. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/helpers.py +0 -0
  172. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/metadata_linker.py +0 -0
  173. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/metrics.py +0 -0
  174. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/models.py +0 -0
  175. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/pipeline.py +0 -0
  176. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/pipeline_config.py +0 -0
  177. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/protocols.py +0 -0
  178. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/query_rewriter.py +0 -0
  179. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/response_parsers.py +0 -0
  180. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/retrieval_query.py +0 -0
  181. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/scoring.py +0 -0
  182. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/search_agent_linker.py +0 -0
  183. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/storage.py +0 -0
  184. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/style_controls.py +0 -0
  185. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/transformers/__init__.py +0 -0
  186. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/transformers/base.py +0 -0
  187. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/transformers/dedup.py +0 -0
  188. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/wiki_builder.py +0 -0
  189. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rag/qa_generation/wiki_chunk_linker.py +0 -0
  190. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rewards/__init__.py +0 -0
  191. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rewards/diversity.py +0 -0
  192. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/__init__.py +0 -0
  193. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/_utils.py +0 -0
  194. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/adaptive.py +0 -0
  195. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/cache.py +0 -0
  196. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/prompts.py +0 -0
  197. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/reward_fns.py +0 -0
  198. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/rubrics/rubric.py +0 -0
  199. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/__init__.py +0 -0
  200. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/adapter.py +0 -0
  201. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/braintrust/__init__.py +0 -0
  202. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/braintrust/adapter.py +0 -0
  203. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/braintrust/message_extraction.py +0 -0
  204. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/http.py +0 -0
  205. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/pipeline.py +0 -0
  206. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/pivot.py +0 -0
  207. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/processing.py +0 -0
  208. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/traces/registry.py +0 -0
  209. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/utils/__init__.py +0 -0
  210. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax/utils/checkpoint.py +0 -0
  211. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax.egg-info/dependency_links.txt +0 -0
  212. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax.egg-info/entry_points.txt +0 -0
  213. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax.egg-info/requires.txt +0 -0
  214. {benchmax-0.1.2.dev35 → benchmax-0.1.2.dev36}/src/benchmax.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: benchmax
3
- Version: 0.1.2.dev35
3
+ Version: 0.1.2.dev36
4
4
  Summary: Framework-Agnostic RL Environments for LLM Fine-Tuning
5
5
  Author: castie@castform.com
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "benchmax"
3
- version = "0.1.2.dev35"
3
+ version = "0.1.2.dev36"
4
4
  description = "Framework-Agnostic RL Environments for LLM Fine-Tuning"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "castie@castform.com" }]
@@ -32,9 +32,11 @@ where = ["src"]
32
32
 
33
33
  [tool.setuptools.package-data]
34
34
  # Ship the `castform setup` scaffold templates in the wheel (read at runtime via
35
- # importlib.resources): docs (*.md), the starter run.py + datasets (*.py/*.jsonl),
35
+ # importlib.resources): docs (*.md), the starter main.py + seed datasets (*.py/*.jsonl),
36
36
  # and the per-skill SKILL.md. Skill dirs are hyphenated (data, not Python packages).
37
37
  "benchmax.cli.scaffold" = ["*.md", "*.py", "*.jsonl", "skills/*/SKILL.md"]
38
+ # Ship the `castform data view` HTML viewer (read at runtime via importlib.resources).
39
+ "benchmax.cli" = ["templates/*.html"]
38
40
 
39
41
  [project.optional-dependencies]
40
42
  mcp = [
@@ -18,6 +18,7 @@ from benchmax.cli import (
18
18
  control,
19
19
  corpus,
20
20
  data,
21
+ doctor,
21
22
  help,
22
23
  launch,
23
24
  runs,
@@ -43,6 +44,7 @@ def build_parser() -> argparse.ArgumentParser:
43
44
  data.register(sub)
44
45
  corpus.register(sub)
45
46
  setup.register(sub)
47
+ doctor.register(sub)
46
48
 
47
49
  # `guide` renders the getting-started walkthrough (the renderer lives in
48
50
  # ``benchmax.cli.help``). Named `guide`, not `quickstart`, because `setup`
@@ -132,3 +132,20 @@ def fmt_value(value: Any) -> str:
132
132
  if isinstance(value, float):
133
133
  return f"{value:.4g}"
134
134
  return str(value)
135
+
136
+
137
+ def truncate(text: object, n: int) -> str:
138
+ """Collapse whitespace and clip to ``n`` chars (…-suffixed) for one-line display.
139
+
140
+ ``None`` → ``""``, but a falsy-yet-real value is preserved (a gold answer of
141
+ ``0`` must render as ``0``, not blank)."""
142
+ s = " ".join(("" if text is None else str(text)).split())
143
+ return s if len(s) <= n else s[: n - 1] + "…"
144
+
145
+
146
+ def final_answer(messages: list | None) -> str | None:
147
+ """The last assistant-turn content in a transcript — the model's committed answer."""
148
+ for m in reversed(messages or []):
149
+ if isinstance(m, dict) and m.get("role") == "assistant" and m.get("content"):
150
+ return m["content"]
151
+ return None
@@ -0,0 +1,104 @@
1
+ """Preflight: turn a missing-env-dependency ImportError into a copy-paste fix.
2
+
3
+ ``castform validate``/``launch`` import the project's ``main.py`` in-process (see
4
+ :func:`benchmax.cli._project.load_project`). A ``main.py`` that uses a corpus-provider
5
+ backend (turbopuffer / pinecone / chroma) or the data-generation helpers imports a
6
+ package that lives behind a ``castform[...]`` extra, not in base castform — so a base
7
+ install fails with ``ModuleNotFoundError`` mid-import. Rather than surface a raw
8
+ traceback, map the missing top-level module back to the extra that ships it and print
9
+ the exact ``uv pip install`` line.
10
+
11
+ The default *postgres* rag env imports on base castform (openai + httpx); this is for
12
+ the provider backends and the ``castform data qa-gen`` / chunking path, which pull the
13
+ heavier deps.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import importlib.util
19
+ import sys
20
+
21
+ # Import-name → the ``castform[<extra>]`` extra that provides it. Import names differ
22
+ # from the pip/extra names (scikit-learn→``sklearn``; chromadb ships under the
23
+ # ``[chroma]`` extra), and ``ModuleNotFoundError.name`` reports the *import* name — so
24
+ # this is keyed on that. Provider rows mirror ``_providers.PROVIDER_PIP``'s keys; the
25
+ # rest are the ``[rag]`` extra's data-generation + chunking deps.
26
+ _MODULE_EXTRA: dict[str, str] = {
27
+ # provider search backends — benchmax.rag.corpus.{turbopuffer,pinecone,chroma}
28
+ "turbopuffer": "turbopuffer",
29
+ "pinecone": "pinecone",
30
+ "chromadb": "chroma",
31
+ "snowballstemmer": "chroma",
32
+ # data generation (qa-gen) + chunking / ingest — the [rag] extra
33
+ "sentence_transformers": "rag",
34
+ "sklearn": "rag",
35
+ "keybert": "rag",
36
+ "nest_asyncio": "rag",
37
+ "langchain_text_splitters": "rag",
38
+ "ragas": "rag",
39
+ "ruamel": "rag",
40
+ "tqdm": "rag",
41
+ }
42
+
43
+ # One sentinel import name per extra — used by ``castform doctor`` to report whether an
44
+ # extra is installed without importing the (heavy) package itself.
45
+ _EXTRA_SENTINEL: dict[str, str] = {
46
+ "rag": "keybert",
47
+ "turbopuffer": "turbopuffer",
48
+ "pinecone": "pinecone",
49
+ "chroma": "chromadb",
50
+ }
51
+
52
+
53
+ def _missing_module(exc: BaseException) -> str | None:
54
+ """Top-level name of the first ``ModuleNotFoundError`` in the exception chain."""
55
+ seen: set[int] = set()
56
+ cur: BaseException | None = exc
57
+ while cur is not None and id(cur) not in seen:
58
+ seen.add(id(cur))
59
+ if isinstance(cur, ModuleNotFoundError) and cur.name:
60
+ return cur.name.split(".")[0]
61
+ cur = cur.__cause__ or cur.__context__
62
+ return None
63
+
64
+
65
+ def install_hint_for_import_error(exc: BaseException) -> str | None:
66
+ """A copy-paste install line if ``exc`` (or its cause chain) is a missing import.
67
+
68
+ Returns ``None`` when the error isn't a ``ModuleNotFoundError`` — callers then
69
+ just print the base error and leave it at that.
70
+ """
71
+ module = _missing_module(exc)
72
+ if module is None:
73
+ return None
74
+ extra = _MODULE_EXTRA.get(module)
75
+ if extra:
76
+ return f"→ install the env deps: uv pip install 'castform[{extra}]'"
77
+ # A third-party module the env imports that isn't one of our extras — still nudge
78
+ # toward installing it rather than leaving a bare "no module named …".
79
+ return (
80
+ f"→ missing dependency {module!r}: install it into this environment "
81
+ f"(e.g. uv pip install {module})"
82
+ )
83
+
84
+
85
+ def print_project_error(exc: Exception) -> None:
86
+ """Print a project-load error to stderr, appending an install hint for a
87
+ missing env dependency (so a base-install ImportError is a one-line fix, not a
88
+ traceback). Shared by ``validate`` and ``launch``."""
89
+ print(f"Error: {exc}", file=sys.stderr)
90
+ hint = install_hint_for_import_error(exc)
91
+ if hint:
92
+ print(hint, file=sys.stderr)
93
+
94
+
95
+ def extra_is_installed(extra: str) -> bool:
96
+ """True if ``extra``'s sentinel module resolves (no import executed)."""
97
+ sentinel = _EXTRA_SENTINEL.get(extra)
98
+ if sentinel is None:
99
+ return False
100
+ try:
101
+ return importlib.util.find_spec(sentinel) is not None
102
+ except (ImportError, ValueError):
103
+ # A parent package missing (or a namespace edge case) => treat as absent.
104
+ return False
@@ -1,10 +1,10 @@
1
1
  """Load a benchmax project (env class + datasets) from a directory.
2
2
 
3
- Convention mirrors the web-app scaffold (``buildAgentContextBody``): ``run.py``
3
+ Convention mirrors the web-app scaffold (``buildAgentContextBody``): ``main.py``
4
4
  defines a single :class:`BaseEnv` subclass; ``train_dataset.jsonl`` /
5
5
  ``eval_dataset.jsonl`` hold one JSON object per line. ``validate`` and ``launch``
6
6
  share this loader. An importable module path (``--module``) is an alternative to
7
- ``run.py`` for shipped envs / fixtures.
7
+ ``main.py`` for shipped envs / fixtures.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -21,7 +21,7 @@ from typing import Any
21
21
 
22
22
 
23
23
  class ProjectError(Exception):
24
- """A project couldn't be loaded (missing run.py/dataset, or no/ambiguous env)."""
24
+ """A project couldn't be loaded (missing main.py/dataset, or no/ambiguous env)."""
25
25
 
26
26
 
27
27
  @dataclass
@@ -31,8 +31,56 @@ class LoadedProject:
31
31
  eval_dataset: list[dict[str, Any]]
32
32
  module: ModuleType
33
33
  from_file: (
34
- bool # loaded from a run.py path (pickle env by value) vs an importable module
34
+ bool # loaded from a main.py path (pickle env by value) vs an importable module
35
35
  )
36
+ # Optional module-level config dicts baked into main.py so a run is
37
+ # reproducible from the file (validate/launch read these; CLI flags override).
38
+ launch_config: dict[str, Any]
39
+ validate_config: dict[str, Any]
40
+
41
+
42
+ def row_question_and_gold(row: Any) -> tuple[object, object]:
43
+ """``(question, gold)`` from a dataset row, across the on-disk shapes.
44
+
45
+ Question: ``prompt`` (a chat-list prompt → its last user turn) else ``question``.
46
+ Gold: ``ground_truth`` else ``answer``. One definition so ``runs rollout`` and
47
+ ``validate --reward-audit`` can't drift when a dataset field is renamed (see the
48
+ ``castform-dataset-ondisk-shapes`` note)."""
49
+ if not isinstance(row, dict):
50
+ return None, None
51
+ q = row.get("prompt")
52
+ if not q:
53
+ q = row.get("question")
54
+ if isinstance(q, list): # chat-style prompt → last user turn
55
+ q = next(
56
+ (
57
+ m.get("content")
58
+ for m in reversed(q)
59
+ if isinstance(m, dict) and m.get("role") == "user" and m.get("content")
60
+ ),
61
+ None,
62
+ )
63
+ gold = row.get("ground_truth")
64
+ if gold is None:
65
+ gold = row.get("answer")
66
+ return q, gold
67
+
68
+
69
+ def _read_config(module: ModuleType, name: str) -> dict[str, Any]:
70
+ """A module-level config dict (``LAUNCH_CONFIG`` / ``VALIDATE_CONFIG``) from
71
+ main.py — the knobs the file bakes in so the run reproduces without remembering
72
+ CLI flags. Absent → ``{}`` (the block is optional); present-but-not-a-dict is a
73
+ user error we fail loudly on rather than silently drop (a dropped budget wastes
74
+ GPU with no explanation)."""
75
+ value = getattr(module, name, None)
76
+ if value is None:
77
+ return {}
78
+ if not isinstance(value, dict):
79
+ raise ProjectError(
80
+ f"{name} must be a dict (got {type(value).__name__}); it bakes the "
81
+ "validate/launch knobs into main.py. Fix or remove it."
82
+ )
83
+ return dict(value)
36
84
 
37
85
 
38
86
  def _load_module_from_file(path: Path) -> ModuleType:
@@ -97,7 +145,7 @@ def _load_jsonl(path: Path) -> list[dict[str, Any]]:
97
145
  def load_project(
98
146
  *,
99
147
  directory: str = ".",
100
- run_file: str = "run.py",
148
+ run_file: str = "main.py",
101
149
  module_path: str | None = None,
102
150
  env_class_name: str | None = None,
103
151
  train_file: str = "train_dataset.jsonl",
@@ -135,4 +183,6 @@ def load_project(
135
183
  eval_dataset=eval_dataset,
136
184
  module=module,
137
185
  from_file=from_file,
186
+ launch_config=_read_config(module, "LAUNCH_CONFIG"),
187
+ validate_config=_read_config(module, "VALIDATE_CONFIG"),
138
188
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  ``corpus ingest <folder>`` chunks a local document folder and uploads it to the
4
4
  deployed Corpora backend (BM25/lexical search), yielding a named corpus that
5
- ``data qa-gen`` and the rag ``run.py`` template resolve by name. ``corpus list`` /
5
+ ``data qa-gen`` and the rag ``main.py`` template resolve by name. ``corpus list`` /
6
6
  ``corpus delete`` manage them against the 5-corpus-per-user cap (the cap is why
7
7
  ``ingest`` can refuse — delete one to free a slot). Thin wrappers over
8
8
  ``benchmax.rag.corpus.postgres`` — the rag bits import lazily so users without the
@@ -18,7 +18,9 @@ under cap, or raises ``CorpusLimitError`` — surfaced here as a clean message.
18
18
  from __future__ import annotations
19
19
 
20
20
  import argparse
21
+ import os
21
22
  import sys
23
+ import tempfile
22
24
  from pathlib import Path
23
25
 
24
26
  from benchmax import config
@@ -186,6 +188,74 @@ def _cmd_corpus_search(args: argparse.Namespace) -> int:
186
188
  return 0
187
189
 
188
190
 
191
+ @handle_errors
192
+ def _cmd_corpus_browse(args: argparse.Namespace) -> int:
193
+ # Recover the wizard's "preview the corpus" step: pull the stored chunks and
194
+ # render them in the local HTML viewer (same one `data view` uses).
195
+ from benchmax.cli.dataview import build_view_model, write_html
196
+ from benchmax.platform.browser import maybe_open_browser
197
+
198
+ try:
199
+ client = _corpus_client()
200
+ except ImportError as exc:
201
+ print(f"Error: {exc}. {_RAG_INSTALL_HINT}", file=sys.stderr)
202
+ return 1
203
+
204
+ corpora = client.list_corpora()
205
+ target = next(
206
+ (c for c in corpora if c.id == args.corpus or c.name == args.corpus), None
207
+ )
208
+ if target is None:
209
+ have = ", ".join(c.name for c in corpora) or "(none)"
210
+ print(f"Error: no corpus matching {args.corpus!r}. Have: {have}", file=sys.stderr)
211
+ return 1
212
+
213
+ # Page through chunks up to --limit (0 = all). `more` tracks whether a
214
+ # next-page cursor remained when we stopped, so we can say so honestly.
215
+ want = args.limit
216
+ chunks: list = []
217
+ cursor: str | None = None
218
+ while True:
219
+ if want and want > 0 and len(chunks) >= want:
220
+ break
221
+ page_size = 500 if not (want and want > 0) else min(500, want - len(chunks))
222
+ page, cursor = client.list_corpus_chunks(target.id, limit=page_size, cursor=cursor)
223
+ chunks.extend(page)
224
+ if not cursor or not page:
225
+ break
226
+ more = cursor is not None
227
+
228
+ records = [
229
+ {"id": c.id, "content": c.content, "metadata": c.metadata, "score": c.score}
230
+ for c in chunks
231
+ ]
232
+ if args.json:
233
+ from benchmax.cli._output import print_json
234
+
235
+ print_json(records)
236
+ return 0
237
+
238
+ model = build_view_model(
239
+ records, source=f"{target.name} (chunks)", type_override="chunks", limit=0
240
+ )
241
+ if args.out:
242
+ out = Path(args.out)
243
+ else:
244
+ fd, tmp = tempfile.mkstemp(prefix="castform-chunks-", suffix=".html")
245
+ os.close(fd)
246
+ out = Path(tmp)
247
+ write_html(model, out, view=args.view)
248
+
249
+ n = len(records)
250
+ print(f"✓ corpus '{target.name}' — {n} chunk{'' if n == 1 else 's'}")
251
+ print(f" {out}")
252
+ if more:
253
+ print(f" (showing first {n}; corpus has more — re-run with --limit 0 for all)")
254
+ if not args.no_open:
255
+ maybe_open_browser(out.resolve().as_uri())
256
+ return 0
257
+
258
+
189
259
  def register(sub: argparse._SubParsersAction) -> None:
190
260
  """Attach the `corpus` group to the top-level subparsers."""
191
261
  corpus = sub.add_parser("corpus", help="Corpus utilities for RAG envs")
@@ -228,3 +298,33 @@ def register(sub: argparse._SubParsersAction) -> None:
228
298
  )
229
299
  p_se.add_argument("--json", action="store_true", help="Emit raw JSON")
230
300
  p_se.set_defaults(func=_cmd_corpus_search)
301
+
302
+ p_br = corpus_sub.add_parser(
303
+ "browse",
304
+ help="Browse a corpus's chunks in an HTML viewer (opens in your browser)",
305
+ description="Page a corpus's stored chunks into the local HTML viewer — recovering "
306
+ "the wizard's 'preview corpus' step. Resolves the corpus by name or id.",
307
+ epilog="Examples:\n"
308
+ " castform corpus browse my-corpus\n"
309
+ " castform corpus browse my-corpus --limit 0 # all chunks\n"
310
+ " castform corpus browse my-corpus --json > chunks.jsonl",
311
+ formatter_class=argparse.RawDescriptionHelpFormatter,
312
+ )
313
+ p_br.add_argument("corpus", help="Corpus name or id")
314
+ p_br.add_argument(
315
+ "--limit", type=int, default=200, help="Max chunks to fetch (default: 200; 0 = all)"
316
+ )
317
+ p_br.add_argument(
318
+ "--view",
319
+ choices=["auto", "table", "messages", "raw"],
320
+ default="auto",
321
+ help="Initial display mode (default: auto)",
322
+ )
323
+ p_br.add_argument("--out", help="Write the HTML here (default: a temp file)")
324
+ p_br.add_argument(
325
+ "--no-open", action="store_true", help="Write the file but don't open a browser"
326
+ )
327
+ p_br.add_argument(
328
+ "--json", action="store_true", help="Emit raw chunk JSON instead of the viewer"
329
+ )
330
+ p_br.set_defaults(func=_cmd_corpus_browse)
@@ -15,6 +15,7 @@ import argparse
15
15
  import json
16
16
  import os
17
17
  import sys
18
+ import tempfile
18
19
  from pathlib import Path
19
20
  from typing import Any
20
21
 
@@ -36,7 +37,7 @@ def _write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
36
37
  def _provider_qa_source_factory(provider: str) -> tuple[Any, str]:
37
38
  """Build a ``(source_factory, corpus_label)`` for ``qa-gen --provider``.
38
39
 
39
- Reads the same ``DATA_*`` env vars the wizard / run.py use (``DATA_api_key`` plus
40
+ Reads the same ``DATA_*`` env vars the wizard / main.py use (``DATA_api_key`` plus
40
41
  per-provider resource fields). qa-gen reads stored chunks directly, so no ``embed_fn``
41
42
  is wired here. Raises ``ValueError`` with a clean message on a missing env var or a
42
43
  missing provider SDK (the caller turns that into an ``Error:`` line).
@@ -280,6 +281,32 @@ def _cmd_data_traces(args: argparse.Namespace) -> int:
280
281
  print(f"Error: no traces fetched from project {project_id}.", file=sys.stderr)
281
282
  return 1
282
283
 
284
+ if args.preview:
285
+ # Recover the wizard's "preview imported traces" step: show the NORMALIZED
286
+ # traces before any filtering, and stop (don't build the train/eval set).
287
+ from benchmax.cli.dataview import build_view_model, write_html
288
+ from benchmax.platform.browser import maybe_open_browser
289
+
290
+ out_dir = Path(args.out)
291
+ out_dir.mkdir(parents=True, exist_ok=True)
292
+ records = [t.to_dict() for t in traces]
293
+ snapshot = out_dir / "traces_preview.jsonl"
294
+ _write_jsonl(snapshot, records)
295
+ html = out_dir / "traces_preview.html"
296
+ model = build_view_model(records, source=f"{project_id} (traces preview)", limit=0)
297
+ write_html(model, html)
298
+ n = len(records)
299
+ if args.json:
300
+ print_json(
301
+ {"preview": True, "count": n, "snapshot": str(snapshot), "html": str(html)}
302
+ )
303
+ else:
304
+ print(f"✓ preview: {n} normalized trace{'' if n == 1 else 's'} (not built into a dataset)")
305
+ print(f" snapshot: {snapshot}")
306
+ print(f" viewer: {html}")
307
+ maybe_open_browser(html.resolve().as_uri())
308
+ return 0
309
+
283
310
  pipeline = TracesPipeline(
284
311
  traces=traces,
285
312
  system_prompt=args.system_prompt,
@@ -346,15 +373,61 @@ def _cmd_data_traces(args: argparse.Namespace) -> int:
346
373
  print(f" detected system prompt ({len(system_prompt)} chars): {preview}")
347
374
  if tools:
348
375
  print(f" detected tools: {', '.join(t.get('name', '?') for t in tools)}")
349
- # The agent authors run.py's dataset_preprocess to match the pulled rows.
376
+ # The agent authors main.py's dataset_preprocess to match the pulled rows.
350
377
  print(
351
378
  " Rows are {prompt_messages, ground_truth, init_rollout_args} — author "
352
- "run.py's dataset_preprocess to match."
379
+ "main.py's dataset_preprocess to match."
353
380
  )
354
381
  print(" Next: castform setup (then castform validate)")
355
382
  return 0
356
383
 
357
384
 
385
+ @handle_errors
386
+ def _cmd_data_view(args: argparse.Namespace) -> int:
387
+ # Lazy import — `dataview` only pulls the traces models for trace files, and
388
+ # then only on demand; RAG/generic viewing stays free of the [traces] extra.
389
+ from benchmax.cli.dataview import load_view_model, write_html
390
+ from benchmax.platform.browser import maybe_open_browser
391
+
392
+ src = Path(args.file)
393
+ if not src.exists():
394
+ print(f"Error: file not found: {src}", file=sys.stderr)
395
+ return 1
396
+
397
+ try:
398
+ model = load_view_model(src, type_override=args.type, limit=args.limit)
399
+ except (OSError, UnicodeDecodeError) as exc: # binary / unreadable file
400
+ print(f"Error: could not read {src}: {exc}", file=sys.stderr)
401
+ return 1
402
+
403
+ if args.out:
404
+ out = Path(args.out)
405
+ else:
406
+ # A persistent temp file (not auto-deleted) so the browser can open it;
407
+ # printed below so headless users can grab the path.
408
+ fd, tmp = tempfile.mkstemp(prefix="castform-view-", suffix=".html")
409
+ os.close(fd)
410
+ out = Path(tmp)
411
+ try:
412
+ write_html(model, out, view=args.view)
413
+ except OSError as exc:
414
+ print(f"Error: could not write {out}: {exc}", file=sys.stderr)
415
+ return 1
416
+
417
+ noun = "row" if model["total"] == 1 else "rows"
418
+ print(
419
+ f"✓ {model['kind']}/{model['variant']} — "
420
+ f"{model['shown']} of {model['total']} {noun}"
421
+ )
422
+ print(f" {out}")
423
+ if model["capped"]:
424
+ print(f" (capped at {args.limit}; re-run with --limit 0 to embed all)")
425
+
426
+ if not args.no_open:
427
+ maybe_open_browser(out.resolve().as_uri())
428
+ return 0
429
+
430
+
358
431
  def register(sub: argparse._SubParsersAction) -> None:
359
432
  """Attach the `data` group to the top-level subparsers."""
360
433
  data = sub.add_parser("data", help="Dataset utilities")
@@ -437,5 +510,53 @@ def register(sub: argparse._SubParsersAction) -> None:
437
510
  help="Detect-only: print the full detected system prompt + tools and write "
438
511
  "nothing — confirm them before generating the dataset",
439
512
  )
513
+ p_tr.add_argument(
514
+ "--preview",
515
+ action="store_true",
516
+ help="Open the imported (normalized) traces in the browser and exit, "
517
+ "without building the train/eval dataset",
518
+ )
440
519
  p_tr.add_argument("--json", action="store_true", help="Emit raw JSON")
441
520
  p_tr.set_defaults(func=_cmd_data_traces)
521
+
522
+ p_view = data_sub.add_parser(
523
+ "view",
524
+ help="Render a local dataset/traces JSONL as an HTML viewer (opens in your browser)",
525
+ description="Render a dataset, traces, corpus-chunk, or Claude session-transcript "
526
+ "JSONL as a self-contained HTML page and open it in the browser. The shape is "
527
+ "auto-detected; anything unrecognized falls back to a generic record table (never "
528
+ "errors). The path is always printed, so it works headless (open it yourself).",
529
+ epilog="Examples:\n"
530
+ " castform data view dataset.jsonl\n"
531
+ " castform data view traces.jsonl --view raw\n"
532
+ " castform data view ~/.claude/projects/<proj>/<session-id>.jsonl # a Claude session\n"
533
+ " castform data view data.jsonl --type chunks --out chunks.html --no-open",
534
+ formatter_class=argparse.RawDescriptionHelpFormatter,
535
+ )
536
+ p_view.add_argument("file", help="Local .jsonl dataset or traces file to view")
537
+ p_view.add_argument(
538
+ "--type",
539
+ choices=["rag", "traces", "chunks", "claude", "generic"],
540
+ default=None,
541
+ help="Force the classification (default: auto-detect from the first row; "
542
+ "`claude` = a Claude Code session transcript)",
543
+ )
544
+ p_view.add_argument(
545
+ "--view",
546
+ choices=["auto", "table", "messages", "raw"],
547
+ default="auto",
548
+ help="Initial display mode (default: auto — the detected kind's view)",
549
+ )
550
+ p_view.add_argument("--out", help="Write the HTML here (default: a temp file)")
551
+ p_view.add_argument(
552
+ "--limit",
553
+ type=int,
554
+ default=1000,
555
+ help="Max rows to embed (default: 1000; 0 = all)",
556
+ )
557
+ p_view.add_argument(
558
+ "--no-open",
559
+ action="store_true",
560
+ help="Write the file but don't open a browser",
561
+ )
562
+ p_view.set_defaults(func=_cmd_data_view)