clear-eval 2.0.3__tar.gz → 2.0.4__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 (125) hide show
  1. {clear_eval-2.0.3 → clear_eval-2.0.4}/PKG-INFO +1 -1
  2. {clear_eval-2.0.3 → clear_eval-2.0.4}/pyproject.toml +1 -1
  3. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/dashboard/generate_static_dashboard.py +121 -7
  4. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/build_json_results.py +131 -0
  5. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/run_trajectory_evaluation_pipeline.py +3 -0
  6. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/trace_evaluation/base_evaluator.py +11 -2
  7. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/run_clear_agentic_eval.py +3 -0
  8. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/constants.py +11 -1
  9. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/eval_utils.py +58 -15
  10. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/full_pipeline.py +11 -3
  11. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/inference_utils/llm_client.py +153 -64
  12. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/setup/default_config.yaml +1 -1
  13. clear_eval-2.0.4/src/clear_eval/pipeline/use_cases/tool_call_use_case.py +375 -0
  14. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval.egg-info/PKG-INFO +1 -1
  15. clear_eval-2.0.3/src/clear_eval/pipeline/use_cases/tool_call_use_case.py +0 -482
  16. {clear_eval-2.0.3 → clear_eval-2.0.4}/LICENSE +0 -0
  17. {clear_eval-2.0.3 → clear_eval-2.0.4}/README.md +0 -0
  18. {clear_eval-2.0.3 → clear_eval-2.0.4}/setup.cfg +0 -0
  19. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/__init__.py +0 -0
  20. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/dashboard/agentic_workflow_dashboard.py +0 -0
  21. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/dashboard/launch_dashboard.py +0 -0
  22. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/dashboard/path_analysis.py +0 -0
  23. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/argument_definitions.py +0 -0
  24. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/create_ui_input.py +0 -0
  25. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/clear_analysis/base_clear_runner.py +0 -0
  26. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/clear_analysis/issues_clear_runner.py +0 -0
  27. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/clear_analysis/root_cause_clear_runner.py +0 -0
  28. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/clear_analysis/run_clear_analysis.py +0 -0
  29. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/trace_evaluation/full_trajectory_evaluator.py +0 -0
  30. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/trace_evaluation/full_trajectory_prompts.py +0 -0
  31. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/trace_evaluation/rubric_evaluator.py +0 -0
  32. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/trace_evaluation/rubric_generator.py +0 -0
  33. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/full_traces_evaluation/trace_evaluation/task_success_evaluator.py +0 -0
  34. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/preprocess_traces/compact_trace_formatter.py +0 -0
  35. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/preprocess_traces/preprocess_traces.py +0 -0
  36. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/preprocess_traces/process_langfuse_traces.py +0 -0
  37. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/preprocess_traces/process_mlflow_traces.py +0 -0
  38. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/preprocess_traces/trace_utils.py +0 -0
  39. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/run_clear_step_analysis.py +0 -0
  40. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/setup/__init__.py +0 -0
  41. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/setup/config_example.json +0 -0
  42. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/setup/config_example.yaml +0 -0
  43. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/setup/default_agentic_config.yaml +0 -0
  44. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/setup/rh_config_example.yaml +0 -0
  45. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/setup/test_tools_config_example.yaml +0 -0
  46. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/agentic/pipeline/utils.py +0 -0
  47. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/analysis_runner.py +0 -0
  48. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/args.py +0 -0
  49. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/cli.py +0 -0
  50. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/dashboard/__init__.py +0 -0
  51. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/dashboard/show_analysis_dashboard.py +0 -0
  52. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/load_ui.py +0 -0
  53. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/logging_config.py +0 -0
  54. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/__init__.py +0 -0
  55. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/caching_utils.py +0 -0
  56. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/config_loader.py +0 -0
  57. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/evaluation_criteria.py +0 -0
  58. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/external_judge.py +0 -0
  59. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/inference_utils/__init__.py +0 -0
  60. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/inference_utils/endpoint_backends.py +0 -0
  61. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/inference_utils/langchain_chat_models.py +0 -0
  62. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/propmts.py +0 -0
  63. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/setup/__init__.py +0 -0
  64. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/threading_utils.py +0 -0
  65. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/use_cases/__init__.py +0 -0
  66. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/use_cases/eval_use_case.py +0 -0
  67. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/use_cases/external_judge_use_case.py +0 -0
  68. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/pipeline/use_cases/use_case_utils.py +0 -0
  69. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/__init__.py +0 -0
  70. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/pipeline_summary.json +0 -0
  71. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-006eee07eed4484592661b1bdba32a75.csv +0 -0
  72. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-0e0148ed21eeebc046f03f63e4418723.csv +0 -0
  73. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-0e1ef041647642c958a8aaa1892fdb88.csv +0 -0
  74. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-4ae9a4305b7a0319ec516ef4eef4ae17.csv +0 -0
  75. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-4eead8c00e900cc84f217a9a10724ecf.csv +0 -0
  76. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-50adf266e9c1522dae57a6290a61596f.csv +0 -0
  77. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-554015d661e4f69cfc629b6d46898421.csv +0 -0
  78. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-55a6003784e34390f47c5c929d8f4db0.csv +0 -0
  79. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-5818239708258c81fff3a4bb0b144802.csv +0 -0
  80. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-5e63ae3886b225ae89a07b7fa4aafe0d.csv +0 -0
  81. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-640f5750b635929a88547548f0cbb4da.csv +0 -0
  82. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-6b2d02f30d4f0d3f4ca7d8f1469913ae.csv +0 -0
  83. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-6c852575331f2e0a02a331d1ddaae59f.csv +0 -0
  84. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-8e68becd37326e6fec6a7d3d3b47dbbb.csv +0 -0
  85. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-b35ed52266fd023dd808950c6848f062.csv +0 -0
  86. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-bccc245d6d47cdf730fd5db3028b52a3.csv +0 -0
  87. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-d6010cdc5e36a50046fc10f33f0bc427.csv +0 -0
  88. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-edadad8b3a67a24a43d145ae07c0c12a.csv +0 -0
  89. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-f082801e1021022e0756e7d089f1558c.csv +0 -0
  90. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/traces_data/tr-f34a9851b31fafffe527c5b2d6262de8.csv +0 -0
  91. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_results/mlflow/my_experiment/unified_ui_results.zip +0 -0
  92. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/langfuse/434f3f96ac470e805428c4be45b4f2a3.json +0 -0
  93. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/langfuse/4c354cfb5b17f61af073df6796f2b236.json +0 -0
  94. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/langfuse/4deb34a4521fcedf92083d0a93fe11d4.json +0 -0
  95. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/langfuse/c685b7eb7826a87149bd2c42a50e47c5.json +0 -0
  96. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/langfuse/ebf1d6897941adb63bcc62aa7c50e8f8.json +0 -0
  97. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-006eee07eed4484592661b1bdba32a75.json +0 -0
  98. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-0e0148ed21eeebc046f03f63e4418723.json +0 -0
  99. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-0e1ef041647642c958a8aaa1892fdb88.json +0 -0
  100. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-4ae9a4305b7a0319ec516ef4eef4ae17.json +0 -0
  101. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-4eead8c00e900cc84f217a9a10724ecf.json +0 -0
  102. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-50adf266e9c1522dae57a6290a61596f.json +0 -0
  103. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-554015d661e4f69cfc629b6d46898421.json +0 -0
  104. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-55a6003784e34390f47c5c929d8f4db0.json +0 -0
  105. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-5818239708258c81fff3a4bb0b144802.json +0 -0
  106. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-5e63ae3886b225ae89a07b7fa4aafe0d.json +0 -0
  107. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-640f5750b635929a88547548f0cbb4da.json +0 -0
  108. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-6b2d02f30d4f0d3f4ca7d8f1469913ae.json +0 -0
  109. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-6c852575331f2e0a02a331d1ddaae59f.json +0 -0
  110. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-8e68becd37326e6fec6a7d3d3b47dbbb.json +0 -0
  111. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-b35ed52266fd023dd808950c6848f062.json +0 -0
  112. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-bccc245d6d47cdf730fd5db3028b52a3.json +0 -0
  113. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-d6010cdc5e36a50046fc10f33f0bc427.json +0 -0
  114. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-edadad8b3a67a24a43d145ae07c0c12a.json +0 -0
  115. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-f082801e1021022e0756e7d089f1558c.json +0 -0
  116. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/agentic/research_agent_traces/mlflow/tr-f34a9851b31fafffe527c5b2d6262de8.json +0 -0
  117. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/gsm8k/__init__.py +0 -0
  118. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/gsm8k/analysis_results_gsm8k_default.zip +0 -0
  119. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/gsm8k/gsm8k_default_predictions.csv +0 -0
  120. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval/sample_data/tool_calls/tool_calls_sample_data.csv +0 -0
  121. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval.egg-info/SOURCES.txt +0 -0
  122. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval.egg-info/dependency_links.txt +0 -0
  123. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval.egg-info/entry_points.txt +0 -0
  124. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval.egg-info/requires.txt +0 -0
  125. {clear_eval-2.0.3 → clear_eval-2.0.4}/src/clear_eval.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clear_eval
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: A python API sdk facilitating Error Analysis via LLM-as-a-Judge
5
5
  Author-email: Lilach Eden <lilache@il.ibm.com>, Asaf Yehudai <asaf.yehudai@ibm.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clear_eval"
7
- version = "2.0.3"
7
+ version = "2.0.4"
8
8
  description = "A python API sdk facilitating Error Analysis via LLM-as-a-Judge"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Lilach Eden", email = "lilache@il.ibm.com"},
@@ -42,7 +42,7 @@ def _maybe_parse_json_text(value):
42
42
  return value
43
43
 
44
44
 
45
- def load_json_data(json_path, include_examples=True):
45
+ def load_json_data(json_path, num_examples=3):
46
46
  """Load all dashboard data from clear_results.json and map it to the HTML data shape."""
47
47
  json_path = Path(json_path)
48
48
 
@@ -79,8 +79,8 @@ def load_json_data(json_path, include_examples=True):
79
79
  for issue in issues:
80
80
  issue_text = issue.get("issue_text") or issue.get("issue_id") or ""
81
81
  examples = []
82
- if include_examples:
83
- for occurrence in issue.get("occurrences", [])[:3]:
82
+ if num_examples:
83
+ for occurrence in issue.get("occurrences", [])[:num_examples]:
84
84
  tasks_list.add(occurrence.get("trace_id"))
85
85
  span_reference = occurrence.get("span_reference", {})
86
86
  input_output_pair = occurrence.get("input_output_pair", {})
@@ -121,13 +121,22 @@ def load_json_data(json_path, include_examples=True):
121
121
  key=lambda row: -row["count"],
122
122
  ) + [row for row in issues_table if row["is_no_issue"]]
123
123
 
124
- agents_data[agent_label] = {
124
+ entry = {
125
125
  "total_evals": int(summary.get("total_interactions", 0)),
126
126
  "avg_score": round(float(summary["avg_score"]), 2) if summary.get("avg_score") is not None else None,
127
127
  "unique_issues": len(issues),
128
128
  "unique_tasks": node_stats.get(agent_name, {}).get("unique_tasks", 0),
129
129
  "issues_table": issues_table,
130
+ "recommendations": None,
130
131
  }
132
+ if agent_type == "tools":
133
+ # Recommendations now travel inside the per-agent payload
134
+ # produced by build_json_results._aggregate_recommendations,
135
+ # so the dashboard just reads the key off the JSON.
136
+ recs = agent_type_payload.get("recommendations")
137
+ if recs and (recs.get("by_tool") or recs.get("by_system_prompt")):
138
+ entry["recommendations"] = recs
139
+ agents_data[agent_label] = entry
131
140
 
132
141
  return {
133
142
  "unique_tasks": int(stats.get("total_traces", 0)),
@@ -234,6 +243,26 @@ body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:
234
243
  .chat-message-content{font-size:14px;color:#1E293B;white-space:pre-wrap;word-break:break-word;line-height:1.6}
235
244
  .chat-message-tools{margin-top:10px;padding-top:10px;border-top:1px dashed #CBD5E1}
236
245
  .expand-indicator{display:block;margin-top:8px;color:#6366F1;font-weight:800;font-size:13px}
246
+ .recs-block{margin-top:24px}
247
+ .recs-subblock{margin-top:14px}
248
+ .recs-subblock-title{font-size:14px;font-weight:800;color:#4338CA;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
249
+ .rec-card{background:#fff;border:1px solid #D8E1EE;border-radius:12px;padding:14px 16px;margin-bottom:12px;box-shadow:0 1px 3px rgba(15,23,42,.04)}
250
+ .rec-card-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}
251
+ .rec-target-pill{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:2px 8px;border-radius:999px;background:#EEF2FF;color:#4338CA}
252
+ .rec-target-pill.system{background:#DBEAFE;color:#1D4ED8}
253
+ .rec-target-pill.tool{background:#DCFCE7;color:#166534}
254
+ .rec-target-pill.param{background:#FEF3C7;color:#92400E}
255
+ .rec-scope{font-size:13px;color:#334155;font-weight:600}
256
+ .rec-importance{margin-left:auto;font-size:12px;font-weight:700;color:#6366F1}
257
+ .rec-count{font-size:12px;color:#64748B}
258
+ .rec-rationale{font-size:13px;color:#475569;line-height:1.6;margin-bottom:10px}
259
+ .rec-diff{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12.5px;background:#0F172A;color:#E2E8F0;padding:12px 14px;border-radius:10px;overflow:auto;max-height:260px;white-space:pre;line-height:1.5}
260
+ .rec-diff .rec-line-add{color:#86EFAC}
261
+ .rec-diff .rec-line-del{color:#FCA5A5}
262
+ .rec-diff .rec-line-hunk{color:#93C5FD}
263
+ .rec-tool-header{display:flex;align-items:baseline;gap:12px;margin:14px 0 6px}
264
+ .rec-tool-name{font-size:15px;font-weight:800;color:#0F172A}
265
+ .rec-tool-count{font-size:12px;color:#64748B}
237
266
  @media(max-width:768px){.container{padding:16px}.dashboard-header{padding:24px}.agent-metrics-grid{grid-template-columns:repeat(2,1fr)}}
238
267
  .info-tooltip{position:relative;display:inline-block}
239
268
  .info-tooltip-trigger{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-left:6px;border-radius:50%;background:#E2E8F0;color:#475569;font-size:11px;font-weight:700;cursor:help;vertical-align:middle}
@@ -567,7 +596,8 @@ function heatFg(val, min, max) {
567
596
  <div class="agent-metric"><div class="agent-metric-label">#Traces</div><div class="agent-metric-value">${ad.unique_tasks}</div></div>
568
597
  </div></div>
569
598
  <div class="section-header" style="margin-top:0"><div class="section-title">Discovered Issues</div><div class="section-subtitle">Recurring problems identified in this agent's outputs <span class="info-tooltip"><span class="info-tooltip-trigger" tabindex="0" aria-label="Issue matching explanation">i</span><span class="info-tooltip-bubble">Each llm call can be matched to any number of discovered issues, or to no issues.</span></span></div></div>
570
- ${tableHtml}`;
599
+ ${tableHtml}
600
+ ${renderRecommendations(ad.recommendations)}`;
571
601
  container.appendChild(sec);
572
602
  }
573
603
  })();
@@ -579,14 +609,98 @@ function toggleIssueExamples(id, rowEl){
579
609
  const indicator=rowEl.querySelector('.expand-indicator');
580
610
  if(indicator) indicator.textContent=isHidden?'▾ Examples':'▸ Examples';
581
611
  }
612
+
613
+ // ─── SPARC recommendations renderer ─────────────────────────────────────
614
+ function escHtml(s){return esc(s||'');}
615
+ function renderDiff(raw){
616
+ if(!raw) return '';
617
+ const lines=String(raw).split('\n');
618
+ const out=lines.map(line=>{
619
+ const safe=escHtml(line);
620
+ if(line.startsWith('+++')||line.startsWith('---')) return `<span class="rec-line-hunk">${safe}</span>`;
621
+ if(line.startsWith('@@')) return `<span class="rec-line-hunk">${safe}</span>`;
622
+ if(line.startsWith('+')) return `<span class="rec-line-add">${safe}</span>`;
623
+ if(line.startsWith('-')) return `<span class="rec-line-del">${safe}</span>`;
624
+ return safe;
625
+ });
626
+ return `<div class="rec-diff">${out.join('\n')}</div>`;
627
+ }
628
+ function targetPillClass(target){
629
+ if(target==='system_prompt') return 'system';
630
+ if(target==='tool_description') return 'tool';
631
+ if(target==='parameter_description'||target==='parameter_examples') return 'param';
632
+ return '';
633
+ }
634
+ function renderRecCard(rec, scope){
635
+ const imp=(typeof rec.importance_mean==='number')?rec.importance_mean:(rec.importance||0);
636
+ const count=rec.count||1;
637
+ const cls=targetPillClass(rec.target||'system_prompt');
638
+ const rationale=rec.rationale?`<div class="rec-rationale">${escHtml(rec.rationale)}</div>`:'';
639
+ const scopeLabel=scope?`<span class="rec-scope">${escHtml(scope)}</span>`:'';
640
+ return `<div class="rec-card">
641
+ <div class="rec-card-head">
642
+ <span class="rec-target-pill ${cls}">${escHtml(rec.target||'')}</span>
643
+ ${scopeLabel}
644
+ <span class="rec-count">×${count}</span>
645
+ <span class="rec-importance">importance ${imp.toFixed(2)}</span>
646
+ </div>
647
+ ${rationale}
648
+ ${renderDiff(rec.diff)}
649
+ </div>`;
650
+ }
651
+ function renderRecommendations(recs){
652
+ if(!recs) return '';
653
+ const byTool=recs.by_tool||{};
654
+ const bySystem=recs.by_system_prompt||[];
655
+ const toolEntries=Object.entries(byTool);
656
+ if(!toolEntries.length && !bySystem.length) return '';
657
+
658
+ const header=`<div class="section-header" style="margin-top:28px">
659
+ <div class="section-title">SPARC Recommendations</div>
660
+ <div class="section-subtitle">Unified-diff suggestions derived from the call judgments (${recs.total||0} recommendation occurrence(s))</div>
661
+ </div>`;
662
+
663
+ let body='<div class="recs-block">';
664
+
665
+ if(bySystem.length){
666
+ body+='<div class="recs-subblock"><div class="recs-subblock-title">System Prompt</div>';
667
+ for(const r of bySystem){ body+=renderRecCard(r, null); }
668
+ body+='</div>';
669
+ }
670
+
671
+ if(toolEntries.length){
672
+ body+='<div class="recs-subblock"><div class="recs-subblock-title">Per-Tool Suggestions</div>';
673
+ // Sort tools by total recommendations descending
674
+ toolEntries.sort((a,b)=>{
675
+ const ca=a[1].reduce((s,x)=>s+(x.count||1),0);
676
+ const cb=b[1].reduce((s,x)=>s+(x.count||1),0);
677
+ return cb-ca || a[0].localeCompare(b[0]);
678
+ });
679
+ for(const [toolName, items] of toolEntries){
680
+ const total=items.reduce((s,x)=>s+(x.count||1),0);
681
+ body+=`<div class="rec-tool-header">
682
+ <span class="rec-tool-name">${escHtml(toolName)}</span>
683
+ <span class="rec-tool-count">${items.length} distinct suggestion(s), ${total} occurrence(s)</span>
684
+ </div>`;
685
+ for(const r of items){
686
+ const scope=r.parameter_name?`${toolName} · ${r.parameter_name}`:toolName;
687
+ body+=renderRecCard(r, scope);
688
+ }
689
+ }
690
+ body+='</div>';
691
+ }
692
+
693
+ body+='</div>';
694
+ return header+body;
695
+ }
582
696
  </script>
583
697
  </body>
584
698
  </html>"""
585
699
 
586
700
 
587
- def generate_html(json_path, output_path=None, include_examples=False):
701
+ def generate_html(json_path, output_path=None, num_examples=3):
588
702
  """Generate static HTML dashboard from a clear_results.json file."""
589
- data = load_json_data(json_path, include_examples=include_examples)
703
+ data = load_json_data(json_path, num_examples=num_examples)
590
704
  logging.info(f"Generating Static HTML report from {json_path}")
591
705
 
592
706
  input_path = Path(json_path)
@@ -9,10 +9,12 @@ Creates a structured JSON with all issues mapped to spans, including:
9
9
  """
10
10
 
11
11
  import ast
12
+ import hashlib
12
13
  import json
13
14
  import logging
14
15
  import math
15
16
  import os
17
+ import re
16
18
  from collections import defaultdict
17
19
  from datetime import datetime
18
20
  from pathlib import Path
@@ -28,6 +30,9 @@ logger = logging.getLogger(__name__)
28
30
  # Must match the subdir name used in run_clear_step_analysis.run_analysis_for_agent.
29
31
  TOOL_CALLS_SUBDIR = "tool_calls"
30
32
 
33
+ # SPARC recommendation targets that scope to a specific tool (vs. system prompt)
34
+ _TOOL_SCOPED_TARGETS = ("tool_description", "parameter_description", "parameter_examples")
35
+
31
36
 
32
37
  def build_workflow_graph(traj_df):
33
38
  """Build workflow graph data from trajectory dataframe."""
@@ -142,6 +147,126 @@ def _parse_meta_data(meta_data_str):
142
147
  return {}
143
148
 
144
149
 
150
+ _WHITESPACE_RE = re.compile(r"\s+")
151
+
152
+
153
+ def _normalize_diff(diff: str) -> str:
154
+ """Whitespace-normalize a unified diff for stable hashing/dedup."""
155
+ if not isinstance(diff, str):
156
+ return ""
157
+ return _WHITESPACE_RE.sub(" ", diff).strip()
158
+
159
+
160
+ def _diff_hash(diff: str) -> str:
161
+ """Stable short hash of the normalized diff text for grouping."""
162
+ return hashlib.sha1(_normalize_diff(diff).encode("utf-8")).hexdigest()[:16]
163
+
164
+
165
+ def _parse_recs(cell: Any) -> List[Dict[str, Any]]:
166
+ """Parse a ``sparc_recommendations`` cell into a list of rec dicts.
167
+
168
+ Accepts JSON arrays (the canonical form), already-parsed lists, or
169
+ ``"[]"``/empty/NaN. Anything else returns an empty list."""
170
+ if cell is None or (isinstance(cell, float) and pd.isna(cell)):
171
+ return []
172
+ if isinstance(cell, list):
173
+ return [r for r in cell if isinstance(r, dict)]
174
+ if isinstance(cell, str) and cell.strip():
175
+ try:
176
+ parsed = json.loads(cell)
177
+ except (json.JSONDecodeError, TypeError):
178
+ return []
179
+ if isinstance(parsed, list):
180
+ return [r for r in parsed if isinstance(r, dict)]
181
+ return []
182
+
183
+
184
+ def _aggregate_recommendations(results_df: pd.DataFrame) -> Optional[Dict[str, Any]]:
185
+ """Group per-row SPARC recommendations into ``by_tool`` and
186
+ ``by_system_prompt`` buckets.
187
+
188
+ Returns ``None`` if the DataFrame has no ``sparc_recommendations`` column
189
+ or if no row produced any recommendation (runtime mode / static-only)."""
190
+ if "sparc_recommendations" not in results_df.columns:
191
+ return None
192
+
193
+ # by_tool: {tool_name: {hash: accumulator}}
194
+ by_tool: Dict[str, Dict[str, Dict[str, Any]]] = defaultdict(dict)
195
+ # by_system_prompt: {hash: accumulator}
196
+ by_system: Dict[str, Dict[str, Any]] = {}
197
+ total = 0
198
+
199
+ for _, row in results_df.iterrows():
200
+ for rec in _parse_recs(row.get("sparc_recommendations")):
201
+ target = rec.get("target")
202
+ diff = rec.get("diff") or ""
203
+ if not target or not diff:
204
+ continue
205
+ try:
206
+ importance = max(0.0, min(1.0, float(rec.get("importance", 0.0))))
207
+ except (TypeError, ValueError):
208
+ importance = 0.0
209
+ h = _diff_hash(diff)
210
+ entry_template = {
211
+ "hash": h,
212
+ "target": target,
213
+ "diff": diff,
214
+ "rationale": rec.get("rationale", ""),
215
+ "tool_name": rec.get("tool_name"),
216
+ "parameter_name": rec.get("parameter_name"),
217
+ "count": 0,
218
+ "importance_sum": 0.0,
219
+ "importance_max": 0.0,
220
+ }
221
+
222
+ if target == "system_prompt":
223
+ bucket = by_system.setdefault(h, dict(entry_template))
224
+ elif target in _TOOL_SCOPED_TARGETS:
225
+ tool_name = rec.get("tool_name") or "<unknown>"
226
+ bucket = by_tool[tool_name].setdefault(h, dict(entry_template))
227
+ else:
228
+ continue
229
+
230
+ bucket["count"] += 1
231
+ bucket["importance_sum"] += importance
232
+ if importance > bucket["importance_max"]:
233
+ bucket["importance_max"] = importance
234
+ total += 1
235
+
236
+ if total == 0:
237
+ return None
238
+
239
+ def _finalize(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
240
+ out = []
241
+ for e in entries:
242
+ mean = e["importance_sum"] / e["count"] if e["count"] else 0.0
243
+ out.append({
244
+ "hash": e["hash"],
245
+ "target": e["target"],
246
+ "diff": e["diff"],
247
+ "rationale": e["rationale"],
248
+ "tool_name": e["tool_name"],
249
+ "parameter_name": e["parameter_name"],
250
+ "count": e["count"],
251
+ "importance_mean": round(mean, 4),
252
+ "importance_max": round(e["importance_max"], 4),
253
+ })
254
+ out.sort(key=lambda r: (-r["importance_mean"], -r["count"], r["target"]))
255
+ return out
256
+
257
+ by_tool_out: Dict[str, List[Dict[str, Any]]] = {
258
+ tool: _finalize(list(entries.values()))
259
+ for tool, entries in by_tool.items()
260
+ }
261
+ by_system_out = _finalize(list(by_system.values()))
262
+
263
+ return {
264
+ "by_tool": by_tool_out,
265
+ "by_system_prompt": by_system_out,
266
+ "total": total,
267
+ }
268
+
269
+
145
270
  def _parse_issues_list(recurring_issues_str):
146
271
  """Parse recurring issues string into a list."""
147
272
  if pd.isna(recurring_issues_str) or not recurring_issues_str:
@@ -315,6 +440,12 @@ def _process_results_dir(
315
440
 
316
441
  result["no_issues"] = no_issue_spans
317
442
 
443
+ # Aggregate SPARC actionable recommendations (tool-call results only carry
444
+ # the column; reasoning_eval CSVs don't, so this is a no-op there).
445
+ recs = _aggregate_recommendations(results_df)
446
+ if recs is not None:
447
+ result["recommendations"] = recs
448
+
318
449
  # Attach scores and weighted severity for pass/fail calculation
319
450
  result["_scores"] = [_safe_float(row.get('score', 0)) for _, row in results_df.iterrows()]
320
451
  result["_weighted_severity"] = sum(
@@ -721,6 +721,9 @@ def main():
721
721
  config.get('run_name')
722
722
  )
723
723
 
724
+ # Update config with resolved run_name so downstream components use the same name
725
+ config['run_name'] = run_name
726
+
724
727
  # Convert paths
725
728
  traj_input_dir = Path(config['data_dir'])
726
729
  rubric_dir = Path(config['rubric_dir']) if config.get('rubric_dir') else None
@@ -95,6 +95,7 @@ class TrajectoryEvaluator(ABC):
95
95
  overwrite: bool = False,
96
96
  max_workers: int = 7,
97
97
  max_files: int | None = None,
98
+ checkpoint_every: int = 0,
98
99
  full_trace_evaluation_criteria: dict | None = None,
99
100
  ):
100
101
  """
@@ -110,6 +111,7 @@ class TrajectoryEvaluator(ABC):
110
111
  max_files: Maximum number of files to process (for testing)
111
112
  full_trace_evaluation_criteria: Custom flat evaluation criteria dict
112
113
  that replaces all default dimensions when set
114
+ checkpoint_every: Save intermediate results every N items (0 = disabled)
113
115
  """
114
116
  self.inference_config = inference_config
115
117
  self.traj_input_dir = Path(traj_input_dir)
@@ -118,6 +120,7 @@ class TrajectoryEvaluator(ABC):
118
120
  self.overwrite = overwrite
119
121
  self.max_workers = max_workers
120
122
  self.max_files = max_files
123
+ self.checkpoint_every = checkpoint_every
121
124
  self.full_trace_evaluation_criteria = full_trace_evaluation_criteria
122
125
 
123
126
  # Create results directory: output_dir/evaluation_type[/model_subdir]
@@ -543,14 +546,20 @@ class TrajectoryEvaluator(ABC):
543
546
  for entry in entries
544
547
  ]
545
548
 
549
+ # Caching params
550
+ item_ids = [entry["traj_name"] for entry in entries] if self.checkpoint_every else None
551
+ cache_path = str(self.results_dir / "run_cache.jsonl") if self.checkpoint_every else None
552
+
546
553
  # Use pipeline's parallel execution with progress bar (always async)
547
554
  start = time.time()
548
555
  parallel_results = run_parallel(
549
556
  func=self.evaluate_single,
550
557
  inputs=inputs,
551
- use_async=True,
552
558
  max_workers=self.max_workers,
553
- progress_desc=f"Evaluating trajectories ({self.__class__.__name__})"
559
+ progress_desc=f"Evaluating trajectories ({self.__class__.__name__})",
560
+ checkpoint_every=self.checkpoint_every,
561
+ checkpoint_path=cache_path,
562
+ item_ids=item_ids,
554
563
  )
555
564
  elapsed = time.time() - start
556
565
 
@@ -337,6 +337,9 @@ def main():
337
337
  config.get('run_name')
338
338
  )
339
339
 
340
+ # Update config with resolved run_name so downstream components use the same name
341
+ config['run_name'] = run_name
342
+
340
343
  # Extract parameters
341
344
  data_dir = Path(config['data_dir'])
342
345
  from_raw_traces = config.get('from_raw_traces')
@@ -11,4 +11,14 @@ SHORTCOMING_LIST_FILE_PREFIX = "shortcoming_list"
11
11
  MAPPING_FILE_PREFIX = "mapping_results"
12
12
  MAPPING_NO_ISSUES = "NO_ISSUES"
13
13
  ANALYSIS_SKIPPED = "Analysis Skipped"
14
- DEFAULT_ISSUES_FORMAT_MODE = "shortcomings"
14
+ DEFAULT_ISSUES_FORMAT_MODE = "shortcomings"
15
+
16
+ # SPARC-specific per-row columns produced by the tool-call use case. They are
17
+ # carried through analysis CSVs unchanged so downstream aggregation
18
+ # (build_json_results) and dashboards can read per-row judgments without
19
+ # re-running SPARC.
20
+ _SPARC_COLUMNS_PASSTHROUGH = (
21
+ "sparc_decision",
22
+ "sparc_score_1_to_5",
23
+ "sparc_recommendations",
24
+ )
@@ -5,13 +5,15 @@ import numpy as np
5
5
  from clear_eval.pipeline.caching_utils import save_dataframe_to_cache
6
6
  import pandas as pd
7
7
  from clear_eval.pipeline.constants import IDENTIFIED_SHORTCOMING_COL, EVALUATION_TEXT_COL, EVALUATION_SUMMARY_COL, \
8
- SHORTCOMING_PREFIX, SCORE_COL, ERROR_COL, MAPPING_NO_ISSUES, ANALYSIS_SKIPPED, DEFAULT_ISSUES_FORMAT_MODE
8
+ SHORTCOMING_PREFIX, SCORE_COL, ERROR_COL, MAPPING_NO_ISSUES, ANALYSIS_SKIPPED, DEFAULT_ISSUES_FORMAT_MODE, \
9
+ _SPARC_COLUMNS_PASSTHROUGH
9
10
  from clear_eval.pipeline.propmts import get_summarization_prompt, \
10
11
  get_shortcomings_clustering_prompt, get_issues_mapping_system_prompt, get_issues_mapping_human_prompt, get_synthesis_prompt, get_synthesis_prompt_cont
11
12
  import re
12
13
  from clear_eval.pipeline.inference_utils.llm_client import run_parallel, run_async, get_llm_client
13
14
  logger = logging.getLogger(__name__)
14
15
 
16
+
15
17
  def is_missing_or_error(eval_text):
16
18
  if pd.isna(eval_text) or not eval_text or not eval_text.strip() or \
17
19
  eval_text.startswith(ANALYSIS_SKIPPED) or eval_text.startswith("Error:"):
@@ -32,6 +34,16 @@ async def evaluate_row(row, config, llm, generate_evaluation_model_prompt_func):
32
34
  return parse_evaluation_response(content)
33
35
 
34
36
 
37
+ def _get_checkpoint_cache_path(config, stage_suffix):
38
+ """Build a cache file path for run_parallel checkpointing."""
39
+ checkpoint_path = config.get('checkpoint_path', '')
40
+ if checkpoint_path:
41
+ # Derive cache path from the main checkpoint CSV path
42
+ return checkpoint_path.replace('.csv', f'_cache_{stage_suffix}.jsonl')
43
+ output_dir = config.get('output_dir', '.')
44
+ return os.path.join(output_dir, f"cache_{stage_suffix}.jsonl")
45
+
46
+
35
47
  def evaluate_single_records(df, llm, config, get_evaluation_prompt_func, score_col=SCORE_COL):
36
48
  """Evaluates predictions and adds scores."""
37
49
  logger.info(f"\n--- Evaluating Predictions ---")
@@ -52,13 +64,19 @@ def evaluate_single_records(df, llm, config, get_evaluation_prompt_func, score_c
52
64
  for idx, row in df.iterrows():
53
65
  inputs.append((row, config, llm, get_evaluation_prompt_func))
54
66
 
67
+ checkpoint_every = config.get('checkpoint_every', 0)
68
+ item_ids = [str(row[config['qid_column']]) for _, row in df.iterrows()] if checkpoint_every else None
69
+ cache_path = _get_checkpoint_cache_path(config, "eval") if checkpoint_every else None
70
+
55
71
  results = run_parallel(
56
72
  func=evaluate_row,
57
73
  inputs=inputs,
58
- use_async=True,
59
74
  max_workers=config['max_workers'],
60
75
  error_prefix="Evaluation: ",
61
- progress_desc="Evaluating predictions"
76
+ progress_desc="Evaluating predictions",
77
+ checkpoint_every=checkpoint_every,
78
+ checkpoint_path=cache_path,
79
+ item_ids=item_ids,
62
80
  )
63
81
 
64
82
  for i, result in enumerate(results):
@@ -83,19 +101,25 @@ def produce_summaries_per_record(df, llm, config):
83
101
 
84
102
  logger.info(f"Generating evaluation summaries for {len(inputs)} items ...")
85
103
 
104
+ df[EVALUATION_SUMMARY_COL] = ""
105
+ if ERROR_COL not in df.columns:
106
+ df[ERROR_COL] = [[] for _ in range(len(df))]
107
+
108
+ checkpoint_every = config.get('checkpoint_every', 0)
109
+ item_ids = [str(row[config['qid_column']]) for _, row in df.iterrows()] if checkpoint_every else None
110
+ cache_path = _get_checkpoint_cache_path(config, "summary") if checkpoint_every else None
111
+
86
112
  results = run_parallel(
87
113
  func=generate_evaluation_summary,
88
114
  inputs=inputs,
89
- use_async=True,
90
115
  max_workers=config['max_workers'],
91
116
  error_prefix="Summary: ",
92
- progress_desc="Generating evaluation summaries"
117
+ progress_desc="Generating evaluation summaries",
118
+ checkpoint_every=checkpoint_every,
119
+ checkpoint_path=cache_path,
120
+ item_ids=item_ids,
93
121
  )
94
122
 
95
- df[EVALUATION_SUMMARY_COL] = ""
96
- if ERROR_COL not in df.columns:
97
- df[ERROR_COL] = [[] for _ in range(len(df))]
98
-
99
123
  for i, result in enumerate(results):
100
124
  if result.is_success:
101
125
  df.at[df.index[i], EVALUATION_SUMMARY_COL] = result.result
@@ -383,7 +407,9 @@ async def analyze_shortcoming_row(eval_text, question_id, shortcomings_list, llm
383
407
 
384
408
 
385
409
  def map_shortcomings_to_records(df, llm, shortcomings_list,
386
- use_full_text, qid_col, max_workers, high_score_threshold, score_col = SCORE_COL, format_mode=DEFAULT_ISSUES_FORMAT_MODE):
410
+ use_full_text, qid_col, max_workers, high_score_threshold,
411
+ score_col = SCORE_COL, format_mode=DEFAULT_ISSUES_FORMAT_MODE,
412
+ checkpoint_every = 0, cache_path = None):
387
413
  """Analyzes evaluation text for the dynamically generated shortcomings."""
388
414
  logger.info(f"\n--- Analyzing Shortcomings based on Synthesized List ---")
389
415
  df[IDENTIFIED_SHORTCOMING_COL] = ""
@@ -420,13 +446,17 @@ def map_shortcomings_to_records(df, llm, shortcomings_list,
420
446
  inputs.append((str(row[evaluation_text_col]), row.get(qid_col, f"row_{idx}"), shortcomings_list, llm, system_prompt, format_mode))
421
447
  logger.info(f"Mapping {n_records_to_map}/{len(df)} records to {len(shortcomings_list)} discovered shortcomings.")
422
448
 
449
+ item_ids = [row.get(qid_col, f"row_{idx}") for idx, row in df.iterrows()] if checkpoint_every else None
450
+
423
451
  results = run_parallel(
424
452
  func=analyze_shortcoming_row,
425
453
  inputs=inputs,
426
- use_async=True,
427
454
  max_workers=max_workers,
428
455
  error_prefix="Mapping: ",
429
- progress_desc="Analyzing shortcomings"
456
+ progress_desc="Analyzing shortcomings",
457
+ checkpoint_every = checkpoint_every,
458
+ checkpoint_path = cache_path,
459
+ item_ids = item_ids,
430
460
  )
431
461
 
432
462
  for i, result in enumerate(results):
@@ -509,13 +539,19 @@ def generate_model_predictions(df, llm, config):
509
539
  for i, row in df.iterrows():
510
540
  inputs.append((llm, row[config['model_input_column']], row[config['qid_column']]))
511
541
 
542
+ checkpoint_every = config.get('checkpoint_every', 0)
543
+ item_ids = [str(row[config['qid_column']]) for _, row in df.iterrows()] if checkpoint_every else None
544
+ cache_path = _get_checkpoint_cache_path(config, "gen") if checkpoint_every else None
545
+
512
546
  results = run_parallel(
513
547
  func=predict_row,
514
548
  inputs=inputs,
515
- use_async=True,
516
549
  max_workers=config["max_workers"],
517
550
  error_prefix="Prediction: ",
518
- progress_desc="Generating predictions"
551
+ progress_desc="Generating predictions",
552
+ checkpoint_every=checkpoint_every,
553
+ checkpoint_path=cache_path,
554
+ item_ids=item_ids,
519
555
  )
520
556
 
521
557
  for i, result in enumerate(results):
@@ -606,10 +642,17 @@ def convert_results_to_ui_input(df, config, task_data):
606
642
  df.loc[:, "recurring_issues_str"] = df.apply(lambda r: get_recurring_issues_list(r), axis=1)
607
643
  custom_output_df["recurring_issues_str"] = df["recurring_issues_str"]
608
644
 
645
+ # Carry SPARC per-row columns through unchanged. Aggregation happens
646
+ # in build_json_results._process_results_dir, which reads these.
647
+ for col in _SPARC_COLUMNS_PASSTHROUGH:
648
+ if col in df.columns:
649
+ custom_output_df[col] = df[col]
650
+
609
651
  required_cols =[config.get(r, r) for r in required_input_fields] + config.get("input_columns", []) + \
610
652
  ["question_id", 'model_input', 'response',
611
653
  'score', 'evaluation_text', 'evaluation_summary',
612
- 'recurring_issues', 'recurring_issues_str', 'ground_truth', 'error']
654
+ 'recurring_issues', 'recurring_issues_str', 'ground_truth', 'error'] + \
655
+ [c for c in _SPARC_COLUMNS_PASSTHROUGH if c in df.columns]
613
656
  required_cols = list(dict.fromkeys(required_cols))
614
657
 
615
658
  for col in required_cols:
@@ -16,10 +16,11 @@ from clear_eval.pipeline.constants import (GENERATION_FILE_PREFIX, SHORTCOMING_L
16
16
  from clear_eval.pipeline.caching_utils import load_dataframe_from_cache, save_dataframe_to_cache, save_json_to_cache, \
17
17
  ensure_dir, \
18
18
  load_json_from_cache, resolve_data_path
19
- from clear_eval.pipeline.eval_utils import map_shortcomings_to_records, get_model_name_for_file, convert_results_to_ui_input, \
19
+ from clear_eval.pipeline.eval_utils import map_shortcomings_to_records, get_model_name_for_file, \
20
+ convert_results_to_ui_input, \
20
21
  load_inputs, synthesize_shortcomings_from_df, \
21
22
  remove_duplicates_shortcomings, run_predictions_generation_save_results, produce_summaries_per_record, \
22
- generate_model_predictions
23
+ generate_model_predictions, _get_checkpoint_cache_path
23
24
  from clear_eval.pipeline.inference_utils.llm_client import get_llm_client
24
25
  from clear_eval.pipeline.config_loader import load_yaml
25
26
 
@@ -101,6 +102,7 @@ def convert_to_ui_format(mapped_data_df, output_dir, config, file_name_info):
101
102
  logger.info(f"Custom formatted analysis results saved to {output_path}")
102
103
  save_ui_input_results(output_df, output_path, config)
103
104
 
105
+
104
106
  def save_ui_input_results(output_df, output_path, config):
105
107
  parquet_bytes = get_parquet_bytes(output_df)
106
108
  json_bytes = json.dumps(config, indent=2).encode()
@@ -223,9 +225,13 @@ def resolve_issues_and_map(df, config, eval_llm, resume_enabled, checkpoint_path
223
225
  use_full_text = config['use_full_text_for_analysis']
224
226
  qid_col = config['qid_column']
225
227
  max_workers = config['max_workers']
228
+ checkpoint_every = config.get("checkpoint_every", 0)
229
+ cache_path =_get_checkpoint_cache_path(config, "map") if checkpoint_every else None
230
+
226
231
  high_score_threshold = config.get("high_score_threshold", 1)
227
232
  df = map_shortcomings_to_records(df, eval_llm, shortcoming_list, use_full_text,
228
- qid_col, max_workers, high_score_threshold, format_mode=format_mode)
233
+ qid_col, max_workers, high_score_threshold, format_mode=format_mode,
234
+ checkpoint_every = checkpoint_every, cache_path=cache_path)
229
235
  save_dataframe_to_cache(df, checkpoint_path)
230
236
  return df
231
237
 
@@ -240,6 +246,7 @@ def run_aggregation_from_df(config, df, file_name_info, eval_llm=None):
240
246
  resume_enabled = config['resume_enabled']
241
247
  format_mode = get_issues_format(config)
242
248
  checkpoint_path = f"{output_dir}/{CHECKPOINT_FILE_PREFIX}_{file_name_info}.csv"
249
+ config['checkpoint_path'] = checkpoint_path
243
250
  shortcoming_list_output_path = f"{output_dir}/{SHORTCOMING_LIST_FILE_PREFIX}_{file_name_info}.json"
244
251
  deduplicated_shortcomings_list_output_path = f"{output_dir}/{SHORTCOMING_LIST_FILE_PREFIX}_{file_name_info}_dedup.json"
245
252
  zip_path = f"{output_dir}/analysis_results_{file_name_info}.zip"
@@ -270,6 +277,7 @@ def run_eval_pipeline(config):
270
277
 
271
278
  generate_issues = config.get("generate_issues", True)
272
279
  checkpoint_path = f"{output_dir}/{CHECKPOINT_FILE_PREFIX}_{run_info}.csv"
280
+ config['checkpoint_path'] = checkpoint_path
273
281
  zip_path = f"{output_dir}/analysis_results_{run_info}.zip"
274
282
 
275
283
  # If final output exists, nothing to do