synth-ai 0.2.16__py3-none-any.whl → 0.2.19__py3-none-any.whl

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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (299) hide show
  1. examples/analyze_semantic_words.sh +2 -2
  2. examples/baseline/banking77_baseline.py +204 -0
  3. examples/baseline/crafter_baseline.py +407 -0
  4. examples/baseline/pokemon_red_baseline.py +326 -0
  5. examples/baseline/simple_baseline.py +56 -0
  6. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  7. examples/blog_posts/gepa/README.md +355 -0
  8. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  9. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
  10. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
  11. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
  12. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
  13. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
  14. examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
  15. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
  16. examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
  17. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
  18. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
  19. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
  20. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
  21. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
  22. examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
  23. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  24. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  25. examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
  26. examples/blog_posts/gepa/task_apps.py +105 -0
  27. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  28. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  29. examples/blog_posts/pokemon_vl/README.md +98 -0
  30. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  31. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
  32. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  33. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  34. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
  35. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  36. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  37. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  38. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  39. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  40. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  41. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  42. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  43. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  44. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  45. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  46. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  47. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  48. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  49. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  50. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  51. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  52. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  53. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  54. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
  55. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  56. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  57. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  58. examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
  59. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
  60. examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
  61. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
  62. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
  63. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -1
  64. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  65. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  66. examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
  67. examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
  68. examples/qwen_coder/configs/coder_lora_small.toml +1 -3
  69. examples/qwen_vl/README.md +10 -12
  70. examples/qwen_vl/SETUP_COMPLETE.md +7 -8
  71. examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
  72. examples/qwen_vl/collect_data_via_cli.md +76 -84
  73. examples/qwen_vl/collect_vision_traces.py +4 -4
  74. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
  75. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
  76. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
  77. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
  78. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  79. examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
  80. examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
  81. examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
  82. examples/qwen_vl/run_vision_comparison.sh +6 -7
  83. examples/rl/README.md +5 -5
  84. examples/rl/configs/rl_from_base_qwen.toml +26 -1
  85. examples/rl/configs/rl_from_base_qwen17.toml +6 -2
  86. examples/rl/task_app/README.md +1 -2
  87. examples/rl/task_app/math_single_step.py +2 -2
  88. examples/run_crafter_demo.sh +2 -2
  89. examples/sft/README.md +1 -1
  90. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
  91. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
  92. examples/swe/task_app/README.md +32 -2
  93. examples/swe/task_app/grpo_swe_mini.py +4 -0
  94. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  95. examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
  96. examples/swe/task_app/hosted/inference/openai_client.py +4 -38
  97. examples/swe/task_app/hosted/policy_routes.py +17 -0
  98. examples/swe/task_app/hosted/rollout.py +4 -2
  99. examples/swe/task_app/morph_backend.py +178 -0
  100. examples/task_apps/banking77/__init__.py +6 -0
  101. examples/task_apps/banking77/banking77_task_app.py +841 -0
  102. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  103. examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
  104. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
  105. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
  106. examples/task_apps/crafter/task_app/README.md +1 -1
  107. examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
  108. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  109. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
  110. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  111. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
  112. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
  113. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
  114. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
  115. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
  116. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  117. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  118. examples/task_apps/gepa_benchmarks/common.py +260 -0
  119. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  120. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  121. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  122. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  123. examples/task_apps/math/README.md +1 -2
  124. examples/task_apps/pokemon_red/README.md +3 -4
  125. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
  126. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  127. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  128. examples/task_apps/pokemon_red/task_app.py +288 -39
  129. examples/task_apps/sokoban/README.md +2 -3
  130. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  131. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  132. examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
  133. examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
  134. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
  135. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +3 -2
  136. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  137. examples/warming_up_to_rl/task_app/README.md +1 -1
  138. examples/warming_up_to_rl/task_app/grpo_crafter.py +185 -5
  139. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
  147. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  148. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
  149. synth_ai/api/train/builders.py +99 -4
  150. synth_ai/api/train/cli.py +516 -26
  151. synth_ai/api/train/config_finder.py +13 -2
  152. synth_ai/api/train/configs/__init__.py +23 -2
  153. synth_ai/api/train/configs/prompt_learning.py +442 -0
  154. synth_ai/api/train/configs/rl.py +61 -7
  155. synth_ai/api/train/configs/sft.py +6 -2
  156. synth_ai/api/train/configs/shared.py +59 -2
  157. synth_ai/api/train/task_app.py +1 -1
  158. synth_ai/api/train/validators.py +277 -0
  159. synth_ai/auth/credentials.py +119 -0
  160. synth_ai/baseline/__init__.py +25 -0
  161. synth_ai/baseline/config.py +209 -0
  162. synth_ai/baseline/discovery.py +214 -0
  163. synth_ai/baseline/execution.py +146 -0
  164. synth_ai/cli/__init__.py +94 -18
  165. synth_ai/cli/__main__.py +0 -0
  166. synth_ai/cli/claude.py +70 -0
  167. synth_ai/cli/codex.py +84 -0
  168. synth_ai/cli/commands/__init__.py +18 -0
  169. synth_ai/cli/commands/baseline/__init__.py +12 -0
  170. synth_ai/cli/commands/baseline/core.py +637 -0
  171. synth_ai/cli/commands/baseline/list.py +93 -0
  172. synth_ai/cli/commands/demo/__init__.py +6 -0
  173. synth_ai/cli/commands/demo/core.py +163 -0
  174. synth_ai/cli/commands/eval/__init__.py +19 -0
  175. synth_ai/cli/commands/eval/core.py +1112 -0
  176. synth_ai/cli/commands/eval/errors.py +81 -0
  177. synth_ai/cli/commands/eval/validation.py +133 -0
  178. synth_ai/cli/commands/filter/__init__.py +12 -0
  179. synth_ai/cli/commands/filter/core.py +424 -0
  180. synth_ai/cli/commands/filter/errors.py +55 -0
  181. synth_ai/cli/commands/filter/validation.py +77 -0
  182. synth_ai/cli/commands/help/__init__.py +177 -0
  183. synth_ai/cli/commands/help/core.py +72 -0
  184. synth_ai/cli/commands/smoke/__init__.py +7 -0
  185. synth_ai/cli/commands/smoke/core.py +1436 -0
  186. synth_ai/cli/commands/status/__init__.py +64 -0
  187. synth_ai/cli/commands/status/client.py +192 -0
  188. synth_ai/cli/commands/status/config.py +92 -0
  189. synth_ai/cli/commands/status/errors.py +20 -0
  190. synth_ai/cli/commands/status/formatters.py +164 -0
  191. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  192. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  193. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  194. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  195. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  196. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  197. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  198. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  199. synth_ai/cli/commands/status/utils.py +114 -0
  200. synth_ai/cli/commands/train/__init__.py +53 -0
  201. synth_ai/cli/commands/train/core.py +21 -0
  202. synth_ai/cli/commands/train/errors.py +117 -0
  203. synth_ai/cli/commands/train/judge_schemas.py +200 -0
  204. synth_ai/cli/commands/train/judge_validation.py +305 -0
  205. synth_ai/cli/commands/train/validation.py +386 -0
  206. synth_ai/cli/demo.py +30 -158
  207. synth_ai/cli/deploy/__init__.py +43 -0
  208. synth_ai/cli/deploy.py +162 -0
  209. synth_ai/cli/eval/__init__.py +36 -0
  210. synth_ai/cli/eval/core.py +5 -0
  211. synth_ai/cli/eval/errors.py +31 -0
  212. synth_ai/cli/eval/validation.py +5 -0
  213. synth_ai/cli/filter/__init__.py +28 -0
  214. synth_ai/cli/filter/core.py +5 -0
  215. synth_ai/cli/filter/errors.py +23 -0
  216. synth_ai/cli/filter/validation.py +5 -0
  217. synth_ai/cli/legacy_root_backup.py +14 -8
  218. synth_ai/cli/modal_serve/__init__.py +12 -0
  219. synth_ai/cli/modal_serve/core.py +14 -0
  220. synth_ai/cli/modal_serve/errors.py +8 -0
  221. synth_ai/cli/modal_serve/validation.py +11 -0
  222. synth_ai/cli/opencode.py +107 -0
  223. synth_ai/cli/root.py +9 -5
  224. synth_ai/cli/serve/__init__.py +12 -0
  225. synth_ai/cli/serve/core.py +14 -0
  226. synth_ai/cli/serve/errors.py +8 -0
  227. synth_ai/cli/serve/validation.py +11 -0
  228. synth_ai/cli/setup.py +20 -265
  229. synth_ai/cli/status.py +7 -126
  230. synth_ai/cli/task_app_deploy.py +1 -10
  231. synth_ai/cli/task_app_modal_serve.py +4 -9
  232. synth_ai/cli/task_app_serve.py +4 -11
  233. synth_ai/cli/task_apps.py +51 -1480
  234. synth_ai/cli/train/__init__.py +12 -0
  235. synth_ai/cli/train/core.py +21 -0
  236. synth_ai/cli/train/errors.py +8 -0
  237. synth_ai/cli/train/validation.py +24 -0
  238. synth_ai/cli/train.py +1 -14
  239. synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
  240. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  241. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  242. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  243. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  244. synth_ai/environments/examples/red/engine.py +33 -12
  245. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  246. synth_ai/environments/examples/red/environment.py +26 -0
  247. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  248. synth_ai/http.py +12 -0
  249. synth_ai/judge_schemas.py +10 -10
  250. synth_ai/learning/__init__.py +10 -0
  251. synth_ai/learning/prompt_learning_client.py +276 -0
  252. synth_ai/learning/prompt_learning_types.py +184 -0
  253. synth_ai/learning/rl/client.py +3 -1
  254. synth_ai/pricing/__init__.py +2 -0
  255. synth_ai/pricing/model_pricing.py +57 -0
  256. synth_ai/streaming/__init__.py +29 -0
  257. synth_ai/streaming/config.py +94 -0
  258. synth_ai/streaming/handlers.py +518 -0
  259. synth_ai/streaming/streamer.py +320 -0
  260. synth_ai/streaming/types.py +95 -0
  261. synth_ai/task/apps/__init__.py +1 -0
  262. synth_ai/task/config.py +2 -0
  263. synth_ai/task/tracing_utils.py +25 -25
  264. synth_ai/task/validators.py +45 -9
  265. synth_ai/task_app_cfgs.py +21 -0
  266. synth_ai/tracing_v3/config.py +162 -19
  267. synth_ai/tracing_v3/constants.py +1 -1
  268. synth_ai/tracing_v3/db_config.py +24 -38
  269. synth_ai/tracing_v3/migration_helper.py +1 -2
  270. synth_ai/tracing_v3/storage/config.py +47 -13
  271. synth_ai/tracing_v3/storage/factory.py +3 -3
  272. synth_ai/tracing_v3/turso/daemon.py +113 -11
  273. synth_ai/tracing_v3/turso/native_manager.py +92 -16
  274. synth_ai/types.py +8 -0
  275. synth_ai/urls.py +11 -0
  276. synth_ai/utils/__init__.py +30 -1
  277. synth_ai/utils/agents.py +74 -0
  278. synth_ai/utils/bin.py +39 -0
  279. synth_ai/utils/cli.py +149 -5
  280. synth_ai/utils/env.py +40 -33
  281. synth_ai/utils/http.py +4 -1
  282. synth_ai/utils/json.py +72 -0
  283. synth_ai/utils/modal.py +285 -3
  284. synth_ai/utils/paths.py +48 -0
  285. synth_ai/utils/uvicorn.py +113 -0
  286. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
  287. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
  288. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
  289. synth_ai/cli/tui.py +0 -62
  290. synth_ai/tui/__init__.py +0 -5
  291. synth_ai/tui/__main__.py +0 -13
  292. synth_ai/tui/cli/__init__.py +0 -1
  293. synth_ai/tui/cli/query_experiments.py +0 -164
  294. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  295. synth_ai/tui/dashboard.py +0 -911
  296. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
  297. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
  298. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
  299. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,460 @@
1
+ """PUPA privacy-aware delegation task app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import os
7
+ import uuid
8
+ from collections.abc import Iterable, Sequence
9
+ from pathlib import Path
10
+ from typing import Any, Mapping, cast
11
+
12
+ from datasets import load_dataset
13
+ from fastapi import APIRouter, HTTPException, Request
14
+
15
+ from synth_ai.task.apps import ModalDeploymentConfig, TaskAppEntry, register_task_app
16
+ from synth_ai.task.contracts import (
17
+ RolloutMetrics,
18
+ RolloutRequest,
19
+ RolloutResponse,
20
+ RolloutStep,
21
+ RolloutTrajectory,
22
+ TaskInfo,
23
+ )
24
+ from synth_ai.task.datasets import TaskDatasetRegistry, TaskDatasetSpec
25
+ from synth_ai.task.rubrics import Rubric, load_rubric
26
+ from synth_ai.task.server import ProxyConfig, RubricBundle, TaskAppConfig
27
+ from synth_ai.task.vendors import normalize_vendor_keys
28
+
29
+ from .common import call_chat_completion, tokenize
30
+
31
+ REPO_ROOT = Path(__file__).resolve().parents[3]
32
+
33
+ DATASET_ID = "Columbia-NLP/PUPA"
34
+ DATASET_CONFIG = "pupa_new"
35
+ AVAILABLE_SPLITS: tuple[str, ...] = ("train",)
36
+ DEFAULT_SPLIT = "train"
37
+
38
+
39
+ pupa_router = APIRouter()
40
+
41
+
42
+ PUPA_DATASET_SPEC = TaskDatasetSpec(
43
+ id="pupa",
44
+ name="PUPA Privacy-Aware Delegation",
45
+ version="1.0.0",
46
+ splits=list(AVAILABLE_SPLITS),
47
+ default_split=DEFAULT_SPLIT,
48
+ description="Privacy-preserving delegation tasks requiring redaction of sensitive fields.",
49
+ )
50
+
51
+ STOPWORDS = {
52
+ "the",
53
+ "a",
54
+ "an",
55
+ "and",
56
+ "or",
57
+ "to",
58
+ "of",
59
+ "for",
60
+ "in",
61
+ "on",
62
+ "with",
63
+ "as",
64
+ "by",
65
+ "at",
66
+ "from",
67
+ "is",
68
+ "are",
69
+ "be",
70
+ "was",
71
+ "were",
72
+ "that",
73
+ "this",
74
+ "it",
75
+ "its",
76
+ "into",
77
+ "about",
78
+ "such",
79
+ "their",
80
+ "they",
81
+ "them",
82
+ "his",
83
+ "her",
84
+ }
85
+
86
+
87
+ class PUPADataset:
88
+ """Load PUPA dataset for privacy-aware evaluation."""
89
+
90
+ def __init__(self) -> None:
91
+ self._cache: dict[str, list[dict[str, Any]]] = {}
92
+
93
+ def _load_split(self, split: str) -> list[dict[str, Any]]:
94
+ if split not in AVAILABLE_SPLITS:
95
+ raise ValueError(f"Unknown split '{split}'. Available: {AVAILABLE_SPLITS}")
96
+ if split not in self._cache:
97
+ try:
98
+ dataset = load_dataset(DATASET_ID, DATASET_CONFIG, split=split)
99
+ except Exception as exc: # pragma: no cover
100
+ raise RuntimeError(
101
+ f"Failed to download PUPA split '{split}'. Ensure network access."
102
+ ) from exc
103
+ self._cache[split] = list(dataset)
104
+ return self._cache[split]
105
+
106
+ def ensure_ready(self, splits: Sequence[str]) -> None:
107
+ for split in splits:
108
+ self._load_split(split)
109
+
110
+ def size(self, split: str) -> int:
111
+ return len(self._load_split(split))
112
+
113
+ def sample(self, *, split: str, index: int) -> dict[str, Any]:
114
+ dataset = self._load_split(split)
115
+ size = len(dataset)
116
+ if size == 0:
117
+ raise RuntimeError(f"PUPA split '{split}' is empty")
118
+ idx = int(index) % size
119
+ row = dataset[int(idx)]
120
+
121
+ pii_units = str(row.get("pii_units") or "")
122
+ pii_tokens = [token.strip() for token in pii_units.split("||") if token.strip()]
123
+
124
+ return {
125
+ "index": idx,
126
+ "split": split,
127
+ "user_query": str(row.get("user_query") or ""),
128
+ "redacted_query": str(row.get("redacted_query") or ""),
129
+ "target_response": str(row.get("target_response") or ""),
130
+ "predicted_category": str(row.get("predicted_category") or ""),
131
+ "pii_units": pii_tokens,
132
+ "conversation_hash": str(row.get("conversation_hash") or ""),
133
+ }
134
+
135
+
136
+ def compute_overlap(target: str, predicted: str) -> float:
137
+ target_tokens = {token for token in tokenize(target) if token not in STOPWORDS}
138
+ if not target_tokens:
139
+ return 0.0
140
+ predicted_tokens = set(tokenize(predicted))
141
+ overlap = len(target_tokens & predicted_tokens)
142
+ return overlap / len(target_tokens)
143
+
144
+
145
+ async def rollout_executor(request: RolloutRequest, fastapi_request: Request) -> RolloutResponse:
146
+ dataset: PUPADataset = fastapi_request.app.state.pupa_dataset
147
+
148
+ split = str(((request.env.config or {}).get("split")) or DEFAULT_SPLIT)
149
+ seed = request.env.seed or 0
150
+
151
+ sample = dataset.sample(split=split, index=seed)
152
+
153
+ observation = {
154
+ "redacted_query": sample["redacted_query"],
155
+ "category": sample["predicted_category"],
156
+ "index": sample["index"],
157
+ "split": sample["split"],
158
+ }
159
+
160
+ placeholders = {
161
+ "redacted_query": sample["redacted_query"],
162
+ "category": sample["predicted_category"],
163
+ }
164
+
165
+ default_messages = [
166
+ {
167
+ "role": "system",
168
+ "pattern": (
169
+ "You must respond to the user while preserving privacy. "
170
+ "Never reconstruct redacted details; use the anonymised placeholders as-is."
171
+ ),
172
+ },
173
+ {
174
+ "role": "user",
175
+ "pattern": "Category: {category}\n\nDelegation Task:\n{redacted_query}",
176
+ },
177
+ ]
178
+
179
+ response_json: dict[str, Any] | None = None
180
+ response_text = ""
181
+ error_info: dict[str, Any] = {}
182
+
183
+ try:
184
+ response_text, response_json, _ = await call_chat_completion(
185
+ request.policy.config or {},
186
+ placeholders,
187
+ default_messages,
188
+ )
189
+ except HTTPException as http_err: # pragma: no cover
190
+ error_info = {"error": str(http_err.detail), "code": http_err.status_code}
191
+ except Exception as exc: # pragma: no cover
192
+ error_info = {"error": str(exc)}
193
+
194
+ placeholder_used = int("PRESIDIO_ANONYMIZED" in response_text or "[REDACTED]" in response_text)
195
+ content_overlap = compute_overlap(sample["target_response"], response_text)
196
+ reward = 0.6 * placeholder_used + 0.4 * content_overlap
197
+
198
+ info_payload = {
199
+ "placeholder_used": bool(placeholder_used),
200
+ "content_overlap": content_overlap,
201
+ "pii_units": sample["pii_units"],
202
+ "response_json": response_json,
203
+ **error_info,
204
+ }
205
+
206
+ with contextlib.suppress(Exception):
207
+ print(
208
+ f"[PUPA_ROLLOUT] run_id={request.run_id} index={sample['index']} "
209
+ f"placeholder_used={placeholder_used} overlap={content_overlap:.3f} reward={reward:.3f}",
210
+ flush=True,
211
+ )
212
+
213
+ step = RolloutStep(
214
+ obs=observation,
215
+ tool_calls=[],
216
+ reward=reward,
217
+ done=True,
218
+ info=info_payload,
219
+ )
220
+
221
+ inference_url = (request.policy.config or {}).get("inference_url")
222
+ trajectory = RolloutTrajectory(
223
+ env_id=f"pupa::{sample['split']}::{sample['index']}",
224
+ policy_id=request.policy.policy_id or request.policy.policy_name or "policy",
225
+ steps=[step],
226
+ final={"observation": observation, "reward": reward},
227
+ length=1,
228
+ inference_url=str(inference_url or ""),
229
+ )
230
+
231
+ metrics = RolloutMetrics(
232
+ episode_returns=[reward],
233
+ mean_return=reward,
234
+ num_steps=1,
235
+ num_episodes=1,
236
+ outcome_score=reward,
237
+ events_score=reward,
238
+ details={
239
+ "placeholder_used": bool(placeholder_used),
240
+ "content_overlap": content_overlap,
241
+ },
242
+ )
243
+
244
+ trace_payload = None
245
+ include_trace = bool(
246
+ (request.record and getattr(request.record, "return_trace", False))
247
+ or os.getenv("TASKAPP_TRACING_ENABLED")
248
+ )
249
+ if include_trace:
250
+ trace_payload = {
251
+ "session_id": str(uuid.uuid4()),
252
+ "events_count": 1,
253
+ "decision_rewards": [reward],
254
+ "metadata": {
255
+ "env": "pupa",
256
+ "split": sample["split"],
257
+ "index": sample["index"],
258
+ "placeholder_used": bool(placeholder_used),
259
+ },
260
+ }
261
+
262
+ return RolloutResponse(
263
+ run_id=request.run_id,
264
+ trajectories=[trajectory],
265
+ branches={},
266
+ metrics=metrics,
267
+ aborted=False,
268
+ ops_executed=2,
269
+ trace=trace_payload,
270
+ )
271
+
272
+
273
+ def build_dataset() -> tuple[TaskDatasetRegistry, PUPADataset]:
274
+ registry = TaskDatasetRegistry()
275
+ dataset = PUPADataset()
276
+ dataset.ensure_ready([DEFAULT_SPLIT])
277
+ registry.register(PUPA_DATASET_SPEC, lambda _spec: dataset, cache=True)
278
+ return registry, dataset
279
+
280
+
281
+ def _base_task_info() -> TaskInfo:
282
+ return TaskInfo(
283
+ task={
284
+ "id": "pupa",
285
+ "name": "PUPA Privacy-Aware Delegation",
286
+ "version": "1.0.0",
287
+ "action_space": {
288
+ "type": "free_text",
289
+ "description": "Respond using anonymised placeholders while fulfilling the task.",
290
+ },
291
+ },
292
+ environment="pupa",
293
+ dataset={
294
+ **PUPA_DATASET_SPEC.model_dump(),
295
+ "hf_dataset": DATASET_ID,
296
+ "hf_config": DATASET_CONFIG,
297
+ },
298
+ rubric={
299
+ "version": "1",
300
+ "criteria_count": 2,
301
+ "source": "inline",
302
+ },
303
+ inference={
304
+ "supports_proxy": True,
305
+ "tool": None,
306
+ },
307
+ limits={"max_turns": 1},
308
+ task_metadata={"privacy_guardrails": "Use anonymised tokens; do not fabricate PII."},
309
+ )
310
+
311
+
312
+ def describe_taskset(dataset: PUPADataset) -> Mapping[str, Any]:
313
+ return {
314
+ **PUPA_DATASET_SPEC.model_dump(),
315
+ "hf_dataset": DATASET_ID,
316
+ "hf_config": DATASET_CONFIG,
317
+ "sizes": {split: dataset.size(split) for split in AVAILABLE_SPLITS},
318
+ }
319
+
320
+
321
+ def provide_task_instances(dataset: PUPADataset, seeds: Sequence[int]) -> Iterable[TaskInfo]:
322
+ base_info = _base_task_info()
323
+ for seed in seeds:
324
+ sample = dataset.sample(split=DEFAULT_SPLIT, index=seed)
325
+ yield TaskInfo(
326
+ task=base_info.task,
327
+ environment=base_info.environment,
328
+ dataset={
329
+ **base_info.dataset,
330
+ "split": sample["split"],
331
+ "index": sample["index"],
332
+ },
333
+ rubric=base_info.rubric,
334
+ inference=base_info.inference,
335
+ limits=base_info.limits,
336
+ task_metadata={
337
+ **base_info.task_metadata,
338
+ "category": sample["predicted_category"],
339
+ },
340
+ )
341
+
342
+
343
+ OUTCOME_RUBRIC: Rubric = cast(
344
+ Rubric,
345
+ load_rubric(
346
+ {
347
+ "version": "1",
348
+ "goal_text": "Respect privacy constraints while completing the task.",
349
+ "aggregation": "weighted_sum",
350
+ "criteria": [
351
+ {
352
+ "id": "privacy_compliance",
353
+ "description": "Use anonymised placeholders rather than raw PII.",
354
+ "weight": 0.6,
355
+ },
356
+ {
357
+ "id": "task_quality",
358
+ "description": "Provide a useful response aligned with the target.",
359
+ "weight": 0.4,
360
+ },
361
+ ],
362
+ }
363
+ ),
364
+ )
365
+
366
+ EVENTS_RUBRIC: Rubric = cast(
367
+ Rubric,
368
+ load_rubric(
369
+ {
370
+ "version": "1",
371
+ "goal_text": "Reward responses that maintain privacy throughout the conversation.",
372
+ "aggregation": "weighted_sum",
373
+ "criteria": [
374
+ {
375
+ "id": "no_pii_leak",
376
+ "description": "Avoid introducing new personal data or removing anonymisation.",
377
+ "weight": 1.0,
378
+ }
379
+ ],
380
+ }
381
+ ),
382
+ )
383
+
384
+
385
+ def build_config() -> TaskAppConfig:
386
+ registry, dataset = build_dataset()
387
+ base_info = _base_task_info()
388
+
389
+ proxy_keys = normalize_vendor_keys()
390
+ proxy_config = ProxyConfig(
391
+ enable_openai=proxy_keys.get("OPENAI_API_KEY") is not None,
392
+ enable_groq=proxy_keys.get("GROQ_API_KEY") is not None,
393
+ system_hint="Never reveal redacted fields. Preserve anonymised tokens verbatim.",
394
+ )
395
+
396
+ config = TaskAppConfig(
397
+ app_id="pupa",
398
+ name="PUPA Privacy-Aware Task",
399
+ description="PUPA task app for evaluating privacy-aware delegation policies.",
400
+ base_task_info=base_info,
401
+ describe_taskset=lambda: describe_taskset(dataset),
402
+ provide_task_instances=lambda seeds: provide_task_instances(dataset, seeds),
403
+ rollout=rollout_executor,
404
+ dataset_registry=registry,
405
+ rubrics=RubricBundle(outcome=OUTCOME_RUBRIC, events=EVENTS_RUBRIC),
406
+ proxy=proxy_config,
407
+ routers=(pupa_router,),
408
+ app_state={"pupa_dataset": dataset},
409
+ cors_origins=["*"],
410
+ )
411
+ return config
412
+
413
+
414
+ register_task_app(
415
+ entry=TaskAppEntry(
416
+ app_id="pupa",
417
+ description="PUPA privacy-aware delegation task app.",
418
+ config_factory=build_config,
419
+ aliases=("pupa-privacy",),
420
+ modal=ModalDeploymentConfig(
421
+ app_name="synth-pupa",
422
+ pip_packages=(
423
+ "datasets>=2.14.0",
424
+ "fastapi>=0.115.0",
425
+ "pydantic>=2.0.0",
426
+ "httpx>=0.26.0",
427
+ ),
428
+ extra_local_dirs=((str(REPO_ROOT / "synth_ai"), "/opt/synth_ai_repo/synth_ai"),),
429
+ ),
430
+ )
431
+ )
432
+
433
+
434
+ if __name__ == "__main__": # pragma: no cover - manual helper
435
+ import argparse
436
+ from synth_ai.task.server import run_task_app
437
+
438
+ parser = argparse.ArgumentParser(description="Run the PUPA task app locally")
439
+ parser.add_argument("--host", default="0.0.0.0")
440
+ parser.add_argument("--port", type=int, default=8113)
441
+ parser.add_argument("--reload", action="store_true", help="Enable uvicorn autoreload")
442
+ parser.add_argument(
443
+ "--env-file",
444
+ action="append",
445
+ default=[],
446
+ help="Additional .env files to load before startup",
447
+ )
448
+ args = parser.parse_args()
449
+
450
+ default_env = Path(__file__).resolve().parents[2] / ".env"
451
+ env_files = [str(default_env)] if default_env.exists() else []
452
+ env_files.extend(args.env_file or [])
453
+
454
+ run_task_app(
455
+ build_config,
456
+ host=args.host,
457
+ port=args.port,
458
+ reload=args.reload,
459
+ env_files=env_files,
460
+ )
@@ -3,7 +3,7 @@
3
3
  This directory hosts the legacy entrypoint for the math single-step task app. Prefer starting the app via:
4
4
 
5
5
  ```bash
6
- uvx synth-ai serve math-single-step --env-file examples/rl/.env --port 8101
6
+ uvx synth-ai deploy --runtime uvicorn math-single-step --env-file examples/rl/.env --port 8101
7
7
  ```
8
8
 
9
9
  If you need to run it directly (e.g., for Modal `modal deploy` compatibility), use:
@@ -19,4 +19,3 @@ Environment variables:
19
19
  - `MATH_DATASET_DEFAULT_SPLIT`, `MATH_DATASET_VALIDATION_SPLIT`, `MATH_DATASET_TEST_SPLIT`
20
20
 
21
21
  The task app enforces a single `math_submit` tool call per episode, enabling RL to reward correct final answers and penalise missing or malformed submissions.
22
-
@@ -17,7 +17,7 @@ A reinforcement learning environment for Pokémon Red using PyBoy emulation with
17
17
 
18
18
  ```bash
19
19
  # From synth-ai root
20
- uv run -m synth_ai task-app serve pokemon_red --port 8913
20
+ uv run -m synth_ai task-app deploy --runtime uvicorn pokemon_red --port 8913
21
21
  ```
22
22
 
23
23
  ### 2. Run a Random Rollout
@@ -232,7 +232,7 @@ uv add pyboy
232
232
  lsof -ti :8913 | xargs -r kill -9
233
233
 
234
234
  # Or use a different port
235
- uv run -m synth_ai task-app serve pokemon_red --port 8914
235
+ uv run -m synth_ai task-app deploy --runtime uvicorn pokemon_red --port 8914
236
236
  ```
237
237
 
238
238
  ## Examples
@@ -249,7 +249,7 @@ cd /Users/joshpurtell/Documents/GitHub/synth-ai
249
249
  echo "OPENAI_API_KEY=sk-..." >> .env
250
250
 
251
251
  # 2. Start the task app server (in background)
252
- nohup sh -c 'printf "n\n" | uv run -m synth_ai task-app serve pokemon_red --port 8913 --no-reload' > nohup_pokemon.log 2>&1 &
252
+ nohup sh -c 'printf "n\n" | uv run -m synth_ai task-app deploy --runtime uvicorn pokemon_red --port 8913 --no-reload' > nohup_pokemon.log 2>&1 &
253
253
 
254
254
  # Wait for startup
255
255
  sleep 8
@@ -354,4 +354,3 @@ TOTAL REWARD: 705 points
354
354
  - **PyBoy**: Game Boy emulator - https://github.com/Baekalfen/PyBoy
355
355
  - **Pokémon Red Disassembly**: RAM map reference - https://github.com/pret/pokered
356
356
  - **Datacrystal.org**: Memory address documentation
357
-
@@ -413,3 +413,7 @@ max_llm_calls = 100
413
413
  Pokemon Red is challenging - don't be discouraged by zero rewards with image-only + 10 steps! 🎮
414
414
 
415
415
 
416
+
417
+
418
+
419
+
@@ -1,11 +1,12 @@
1
- # Evaluation config for Pokemon Red with image-only input
1
+ # Evaluation config for Pokemon Red with image-only input and NEW REWARD SYSTEM
2
2
  # This config uses GPT-4o mini with only image data (no text observations)
3
+ # Uses the comprehensive reward system with deterministic progress milestones
3
4
 
4
5
  [eval]
5
6
  app_id = "pokemon_red"
6
7
  model = "gpt-4o-mini-2024-07-18"
7
- seeds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
8
- max_turns = 10
8
+ seeds = [0, 1, 2, 3, 4] # Test with fewer seeds for quick results
9
+ max_turns = 20 # Allow more turns to see progress
9
10
  concurrency = 1 # Keep low initially to avoid issues
10
11
  env_name = "pokemon_red"
11
12
  policy_name = "pokemon_red_policy"
@@ -13,7 +14,7 @@ trace_format = "full"
13
14
  return_trace = true
14
15
 
15
16
  [eval.env_config]
16
- max_steps_per_episode = 10
17
+ max_steps_per_episode = 20
17
18
 
18
19
  [eval.policy_config]
19
20
  provider = "openai"
@@ -24,6 +25,6 @@ top_p = 0.95
24
25
  max_tokens = 512
25
26
  use_vision = true
26
27
  image_only_mode = true
27
- max_llm_calls = 10
28
+ max_llm_calls = 20
28
29
 
29
30
 
@@ -129,7 +129,7 @@ async def main():
129
129
  print("✓ Server is healthy")
130
130
  except Exception as e:
131
131
  print(f"❌ Server not responding: {e}")
132
- print(f" Start it with: uv run -m synth_ai task-app serve pokemon_red --port 8913")
132
+ print(f" Start it with: uv run -m synth_ai task-app deploy --runtime uvicorn pokemon_red --port 8913")
133
133
  return
134
134
 
135
135
  # Check API key
@@ -222,4 +222,3 @@ async def main():
222
222
 
223
223
  if __name__ == "__main__":
224
224
  asyncio.run(main())
225
-