Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- __marimo__/session/a.py.json +138 -0
- a.py +0 -0
- constants.py +9 -3
- layouts/a.grid.json +41 -0
- layouts/a.slides.json +18 -0
- models.py +12 -3
- openenv_explainer_env.egg-info/PKG-INFO +13 -10
- openenv_explainer_env.egg-info/SOURCES.txt +5 -0
- openenv_explainer_env.egg-info/requires.txt +13 -10
- pyproject.toml +17 -11
- research/retrieval.py +52 -65
- research/router.py +9 -7
- rewards/exploration.py +56 -31
- rewards/generation.py +163 -59
- rewards/sandbox.py +76 -11
- server/app.py +23 -0
- server/explainer_env_environment.py +77 -14
- tests/test_environment.py +21 -7
- tests/test_models.py +3 -2
- tests/test_retrieval.py +107 -0
- tests/test_rewards.py +258 -27
- uv.lock +0 -0
__marimo__/session/a.py.json
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1",
|
| 3 |
+
"metadata": {
|
| 4 |
+
"marimo_version": "0.23.3",
|
| 5 |
+
"script_metadata_hash": null
|
| 6 |
+
},
|
| 7 |
+
"cells": [
|
| 8 |
+
{
|
| 9 |
+
"id": "Hbol",
|
| 10 |
+
"code_hash": "e68d0f9bfeb37dc8982263a48f46efd6",
|
| 11 |
+
"outputs": [
|
| 12 |
+
{
|
| 13 |
+
"type": "data",
|
| 14 |
+
"data": {
|
| 15 |
+
"text/plain": ""
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
],
|
| 19 |
+
"console": []
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "MJUe",
|
| 23 |
+
"code_hash": "f7841b59ed8b1d4e950e92591fd64cf4",
|
| 24 |
+
"outputs": [
|
| 25 |
+
{
|
| 26 |
+
"type": "data",
|
| 27 |
+
"data": {
|
| 28 |
+
"text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><h1 id=\"eigenvalues-and-eigenvectors-an-interactive-guide\">Eigenvalues and Eigenvectors: An Interactive Guide</h1>\n<span class=\"paragraph\">In this interactive guide, we'll explore eigenvalues and eigenvectors, fundamental concepts in linear algebra with wide-ranging applications.</span>\n<h3 id=\"what-are-eigenvalues-and-eigenvectors\">What Are Eigenvalues and Eigenvectors?</h3>\n<span class=\"paragraph\">For a square matrix $ A $, an <strong>eigenvector</strong> $ v $ is a non-zero vector that, when multiplied by $ A $, results in a scalar multiple of itself:</span>\n<marimo-tex class=\"arithmatex\">||[ A v = \\lambda v ||]</marimo-tex><span class=\"paragraph\">where $ \\lambda $ is the corresponding <strong>eigenvalue</strong>. This means that the direction of the eigenvector remains unchanged under the transformation defined by $ A $, only its magnitude is scaled by $ \\lambda $.</span></span>"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
],
|
| 32 |
+
"console": []
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "vblA",
|
| 36 |
+
"code_hash": "185a12a80c0ed6792c68ae3a1cf8ddda",
|
| 37 |
+
"outputs": [
|
| 38 |
+
{
|
| 39 |
+
"type": "error",
|
| 40 |
+
"ename": "exception",
|
| 41 |
+
"evalue": "name 'amatrix' is not defined",
|
| 42 |
+
"traceback": null
|
| 43 |
+
}
|
| 44 |
+
],
|
| 45 |
+
"console": [
|
| 46 |
+
{
|
| 47 |
+
"type": "stream",
|
| 48 |
+
"name": "stderr",
|
| 49 |
+
"text": "<span class=\"codehilite\"><div class=\"highlight\"><pre><span></span><span class=\"gt\">Traceback (most recent call last):</span>\n File <span class=\"nb\">"/var/folders/44/xrn2r2n539lcfpfq4mb99p48hlq2mb/T/marimo_98690/__marimo__cell_vblA_.py"</span>, line <span class=\"m\">9</span>, in <span class=\"n\"><module></span>\n<span class=\"w\"> </span><span class=\"err\">$$</span> <span class=\"n\">A</span> <span class=\"o\">=</span> \\<span class=\"n\">begin</span><span class=\"p\">{</span><span class=\"n\">amatrix</span><span class=\"p\">}</span> <span class=\"mi\">4</span> <span class=\"o\">&</span> <span class=\"mi\">1</span> \\\\ <span class=\"mi\">6</span> <span class=\"o\">&</span> <span class=\"mi\">3</span> \\<span class=\"n\">end</span><span class=\"p\">{</span><span class=\"n\">bmatrix</span><span class=\"p\">}</span> <span class=\"err\">$$</span>\n<span class=\"w\"> </span><span class=\"pm\">^^^^^^^</span>\n<span class=\"gr\">NameError</span>: <span class=\"n\">name 'amatrix' is not defined</span>\n</pre></div>\n</span>",
|
| 50 |
+
"mimetype": "application/vnd.marimo+traceback"
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"id": "bkHC",
|
| 56 |
+
"code_hash": "36a4f0c65a9521a0e4a12ab23e18bcb4",
|
| 57 |
+
"outputs": [
|
| 58 |
+
{
|
| 59 |
+
"type": "data",
|
| 60 |
+
"data": {
|
| 61 |
+
"text/html": "<marimo-ui-element object-id='bkHC-0' random-id='ab65d3ba-4670-0915-4d98-8e312ee44073'><marimo-matplotlib data-initial-value='{}' data-label='null' data-chart-base64='"data:image/png;base64,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3MjPcs1//etfdzq2J598ErvttptWTvfu3bUYkVjR60y/Ky51J/VgHCh99dVX2sBa/k7KmDZtWgelUNBj+H//+x8uueQSVFRUYMCAAe13pHfeeWft+u23337aTIvEsMxYCPI3e+65p9YRGT16NN5+++0Ory137uU15XfyHFkjJfVjjDEpXx4TDjjggPbrrt8Fj7UuIFlsG9c4yB3g++67D8OHD9euyR577KENoM2wdOlS7dh69uypnbvMFsp7xYj+Pnr66ac1BU7qTo5J6nrx4sVJy7j++uu197bMREQPPgTpQF922WVaLJmJwVhrQKQdksdljZAMVEXZM6vkyPtG4lLiTsqUWJKYmj9/flr1oF8TiYtJkyZp7x27ibUGRN6LUp8yoyn1dvTRR2vvw1jryORxaackziR2pN2ZNWtWSuctbZfUX319fafjPOWUU7Q20fjef/3117HPPvtog0o5ziOPPBLfffddp7+VWbITTzxRaz/096HMhpppU2V26K9//Wv7e0Pi5v/9v/+HpqamDmXI4z/72c80zVhu0kk59957r/a72bNnY++999biSs5PypfXIMQteIuBZDxVVVXYtGlTh8eksZZO05gxYzS3XjqT8gGjT/3vuuuu7esmDj74YJx55pntfysfNNJpkw+xiy66SOuUio8r60TWrVvXYaG6dKakIyYf7LIgXD4Y5ANZOovS4EsZ8rh8UF944YXa38iHRjzkQ1Y6bfIBKB988uHzzDPPaJ0Q6XjIzEW8c5IPsmSceuqp2qJn6QDpxyEDElHEYt2BlQ8p6cyJ6iYftPJBKh0Q+S7nKPUsg5qFCxd2Up6Mx/Puu+9qH/RyTvL7WLMS8uEonVGZqZEP4ptuukl7XFx6ucZSz8FgEOkg10cGWvLBKx1c6ZwKUsdy3WU9jcTNZ599httvv12bHZLfGZHOhryGdODlNaTzfuONN2r1KX+v15t0TqQzIwNX4YcffsBHH32kXUOpM/ngl2soz5POqHQCBKlb6cBIh/Hqq6/Wrot0GiQu9IGDERkoSF3/+c9/1jrEOlu3btU6H3KXXjrjd999t/azDARlcD19+nQtHv79739r13/VqlVah0mQjr7EvDxfYkw6PvL3cgyy7kPqbd9999U6hLfddpvWcZG4FPTvqcS2EYnLmpoa7T0ocSYdfqk3icdEswWiGspgVa6nHJ9c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02To0KFyyimnyPPPPx/1W0qsH/3oR/KBD3xA2traZNy4cXLuuefKX//616jfVt348Y9/7Byzv/71r0f9VoCawAQEiJmrr75aJk2aFPXbSLy//OUvkslk5Oabb5bXXntNrr/+ernpppvku9/9btRvLRHuvfde+eY3v+lM6l588UWZPXu2nHXWWfL2229H/dYS6fe//71cdtll8txzz8njjz8uvb29cuaZZ0pnZ2fUby3xVq5c6RxHTjzxxKjfClAzaMMLxMijjz7qnLTdf//98t73vldWrVrl3A2BGT/96U9lyZIlsm7dOkpeJb3joVfkFy9e7PxdJ3tTpkyRK664Qr7zne9Q35Dt3LnTuROiE5PTTz+deoeko6NDTjrpJLnxxhvlBz/4gXO8/vnPf069gTK4AwLExI4dO2ThwoVy5513yrBhw6J+O3Vp3759MmrUqKjfRs3TGNsLL7wgH//4xw99LZVKOX9/9tlnI31v9TSWFeM5XHrX6eyzzx401gGU1+DhewCEzLZtueiii2TRokUyd+5c5/kEmPXmm2/KL3/5S7nuuusofZV27dol/f39Mn78+EFf179r9A3h0rtN+izCBz/4QTn++OMpd0juueceJ16oESwA/nAHBAiRRk30wcRSf/SETE98Dxw4INdccw37w1DNc23ZskU++clPyrx585y7UECtX5V/9dVXnRNkhGPTpk1y1VVXybJly5wmCwD84RkQIOQc9u7du0t+z4wZM2T+/Pny8MMPOyfHLr2CnE6nZcGCBXL77beznwKueWNjo/P51q1b5cMf/rCceuqpcttttzlRIVQfwdIY4X333ed0Y3J9+ctflvb2dnnooYcocUguv/xyp75PPfWU0+EN4XjwwQflvPPOc47RucdsPYbrMUS7Geb+G4DBmIAAMfDWW2/J/v37D/1dT4q1Y5CewOnDvJMnT470/SWV3vn4yEc+Iu9///vlrrvu4oQhQDpuTz75ZOfunhsLmjp1qnOCzEPo4cQ49QH/Bx54QJ588kmZNWtWCK8Cl96x3rhx46CCXHzxxXLcccfJt7/9baJvQBk8AwLEgJ6Y5WptbXU+6toUTD7Cm3zonY+jjjrKee5D75y4JkyYENKr1g/t5qZ3PPSZJp2IaGcgbQmrJ2kIJ3Z19913O3c/dC2Q7du3O18fMWKEs84NgqU1zn++pqWlRUaPHs3kA/CACQiAuqRrJeiD5/onf5KnV5NRnfPPP9+Z1H3ve99zToa1PemKFSsOezAdwdD20Uon1bmWLl3qNLgAgDghggUAAADAGJ62BAAAAGAMExAAAAAAxjABAQAAAGAMExAAAAAAxjABAQAAAGAMExAAAAAAxjABAQAAAMAEBAAAAEDycAcEAAAAgDFMQAAAAAAYwwQEABConTt3yoQJE+SHP/zhoa8988wz0tjYKL/97W+pNgDUOcu2bTvqNwEASJZHHnlEzj33XGficeyxx8qcOXPknHPOkZ/97GdRvzUAQMSYgAAAQnHZZZfJE088IXPnzpVXXnlFVq5cKU1NTVQbAOocExAAQCgOHjwoxx9/vGzatEleeOEFOeGEE6g0AIBnQAAA4Vi7dq1s3bpVMpmMbNiwgTIDABzcAQEABK6np0dOPvlk59kPfQbk5z//uRPDGjduHNUGgDrHBAQAELhvfetbct9998nq1aultbVVzjjjDBkxYoT8+te/ptoAUOdowwsACNSTTz7p3PG48847Zfjw4ZJKpZzP//CHP8iSJUuoNgDUOe6AAAAAADCGOyAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAjGECAgAAAMAYJiAAAAAAxJT/D4wFLduhmSJNAAAAAElFTkSuQmCC"' data-x-bounds='[-5.0,5.0]' data-y-bounds='[-5.0,5.0]' data-axes-pixel-bounds='[179.0,72.0,641.0,534.0]' data-width='800.0' data-height='600.0' data-debounce='false' data-x-scale='"linear"' data-y-scale='"linear"'></marimo-matplotlib></marimo-ui-element>"
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
],
|
| 65 |
+
"console": []
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"id": "lEQa",
|
| 69 |
+
"code_hash": "3146055003496935c5fc2f89c7047d0f",
|
| 70 |
+
"outputs": [
|
| 71 |
+
{
|
| 72 |
+
"type": "data",
|
| 73 |
+
"data": {
|
| 74 |
+
"text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><h3 id=\"application-principal-component-analysis-pca\">Application: Principal Component Analysis (PCA)</h3>\n<span class=\"paragraph\">PCA relies heavily on eigenvectors and eigenvalues of the covariance matrix to identify the principal components \u2014 directions of maximum variance in the data.</span>\n<span class=\"paragraph\">In data analysis:</span>\n<ul>\n<li><strong>Covariance Matrix</strong>: Measures how much variables change together</li>\n<li><strong>Eigenvectors</strong>: Principal components (directions)</li>\n<li><strong>Eigenvalues</strong>: Amount of variance explained by each component</li>\n</ul>\n<span class=\"paragraph\">By projecting data onto the principal components, we can perform dimensionality reduction while retaining most of the information.</span></span>"
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
],
|
| 78 |
+
"console": []
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"id": "PKri",
|
| 82 |
+
"code_hash": "8ed5701904eccbda4e41171b790d6840",
|
| 83 |
+
"outputs": [
|
| 84 |
+
{
|
| 85 |
+
"type": "error",
|
| 86 |
+
"ename": "multiple-defs",
|
| 87 |
+
"evalue": "The variable 'x' was defined by another cell",
|
| 88 |
+
"traceback": []
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"type": "error",
|
| 92 |
+
"ename": "multiple-defs",
|
| 93 |
+
"evalue": "The variable 'y' was defined by another cell",
|
| 94 |
+
"traceback": []
|
| 95 |
+
}
|
| 96 |
+
],
|
| 97 |
+
"console": []
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"id": "Xref",
|
| 101 |
+
"code_hash": "1a6201498b29d7ec58ea500ec3f6927a",
|
| 102 |
+
"outputs": [
|
| 103 |
+
{
|
| 104 |
+
"type": "error",
|
| 105 |
+
"ename": "multiple-defs",
|
| 106 |
+
"evalue": "The variable 'i' was defined by another cell",
|
| 107 |
+
"traceback": []
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"type": "error",
|
| 111 |
+
"ename": "multiple-defs",
|
| 112 |
+
"evalue": "The variable 'val' was defined by another cell",
|
| 113 |
+
"traceback": []
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"type": "error",
|
| 117 |
+
"ename": "multiple-defs",
|
| 118 |
+
"evalue": "The variable 'vec' was defined by another cell",
|
| 119 |
+
"traceback": []
|
| 120 |
+
}
|
| 121 |
+
],
|
| 122 |
+
"console": []
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"id": "SFPL",
|
| 126 |
+
"code_hash": "fa5f4c36613e916c64edc94072bbe1bb",
|
| 127 |
+
"outputs": [
|
| 128 |
+
{
|
| 129 |
+
"type": "data",
|
| 130 |
+
"data": {
|
| 131 |
+
"text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><h3 id=\"key-takeaways\">Key Takeaways</h3>\n<ol>\n<li><strong>Eigenvalues and Eigenvectors</strong> describe linear transformations that act by scaling vectors without changing their direction.</li>\n<li><strong>PCA</strong> uses eigen-decomposition of the covariance matrix to identify the directions of maximum variance.</li>\n<li><strong>Applications</strong> include data compression, dimensionality reduction, facial recognition (eigenfaces), and understanding complex systems.</li>\n<li><strong>Practical Computation</strong> requires numerical methods like QR algorithm for large matrices, as analytical solutions are often intractable.</li>\n</ol></span>"
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
],
|
| 135 |
+
"console": []
|
| 136 |
+
}
|
| 137 |
+
]
|
| 138 |
+
}
|
a.py
ADDED
|
File without changes
|
constants.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Shared limits and scoring helpers for explainer episodes."""
|
| 2 |
|
| 3 |
-
MAX_EXPLORE_STEPS =
|
| 4 |
-
MAX_REPAIR_STEPS =
|
| 5 |
|
| 6 |
AVAILABLE_TOOLS = (
|
| 7 |
"search_wikipedia",
|
|
@@ -14,11 +14,17 @@ AVAILABLE_TOOLS = (
|
|
| 14 |
|
| 15 |
MAX_EXPLORE_REWARD = 0.8
|
| 16 |
MAX_GENERATE_REWARD = 1.0
|
|
|
|
| 17 |
SUCCESS_SCORE_THRESHOLD = 0.3
|
| 18 |
|
| 19 |
|
| 20 |
def normalized_episode_score(total_reward: float) -> float:
|
| 21 |
-
"""Normalize an episode's accumulated reward to the required [0, 1] range.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
max_possible = MAX_EXPLORE_STEPS * MAX_EXPLORE_REWARD + MAX_GENERATE_REWARD
|
| 23 |
score = total_reward / max_possible if max_possible > 0 else 0.0
|
| 24 |
return min(max(score, 0.0), 1.0)
|
|
|
|
| 1 |
"""Shared limits and scoring helpers for explainer episodes."""
|
| 2 |
|
| 3 |
+
MAX_EXPLORE_STEPS = 6
|
| 4 |
+
MAX_REPAIR_STEPS = 3
|
| 5 |
|
| 6 |
AVAILABLE_TOOLS = (
|
| 7 |
"search_wikipedia",
|
|
|
|
| 14 |
|
| 15 |
MAX_EXPLORE_REWARD = 0.8
|
| 16 |
MAX_GENERATE_REWARD = 1.0
|
| 17 |
+
MAX_REPAIR_REWARD = 0.7
|
| 18 |
SUCCESS_SCORE_THRESHOLD = 0.3
|
| 19 |
|
| 20 |
|
| 21 |
def normalized_episode_score(total_reward: float) -> float:
|
| 22 |
+
"""Normalize an episode's accumulated reward to the required [0, 1] range.
|
| 23 |
+
|
| 24 |
+
Repair is intentionally not added to the denominator: repair rewards are
|
| 25 |
+
discounted so a failed generate + successful repair should not beat a clean
|
| 26 |
+
first-pass generation.
|
| 27 |
+
"""
|
| 28 |
max_possible = MAX_EXPLORE_STEPS * MAX_EXPLORE_REWARD + MAX_GENERATE_REWARD
|
| 29 |
score = total_reward / max_possible if max_possible > 0 else 0.0
|
| 30 |
return min(max(score, 0.0), 1.0)
|
layouts/a.grid.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "grid",
|
| 3 |
+
"data": {
|
| 4 |
+
"columns": 24,
|
| 5 |
+
"rowHeight": 20,
|
| 6 |
+
"maxWidth": 1400,
|
| 7 |
+
"bordered": true,
|
| 8 |
+
"cells": [
|
| 9 |
+
{
|
| 10 |
+
"position": null
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"position": null
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"position": null
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"position": null
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"position": null
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"position": null
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"position": null
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"position": null
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"position": null
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"position": null
|
| 38 |
+
}
|
| 39 |
+
]
|
| 40 |
+
}
|
| 41 |
+
}
|
layouts/a.slides.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "slides",
|
| 3 |
+
"data": {
|
| 4 |
+
"cells": [
|
| 5 |
+
{},
|
| 6 |
+
{},
|
| 7 |
+
{},
|
| 8 |
+
{},
|
| 9 |
+
{},
|
| 10 |
+
{},
|
| 11 |
+
{},
|
| 12 |
+
{},
|
| 13 |
+
{},
|
| 14 |
+
{}
|
| 15 |
+
],
|
| 16 |
+
"deck": {}
|
| 17 |
+
}
|
| 18 |
+
}
|
models.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
"""Data models for the Research -> Interactive Explainer environment."""
|
| 2 |
|
| 3 |
-
from typing import Literal
|
| 4 |
|
| 5 |
from openenv.core.env_server.types import Action, Observation
|
| 6 |
from pydantic import Field
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
ResearchTool = Literal[
|
| 10 |
"search_wikipedia",
|
|
@@ -79,15 +84,19 @@ class ExplainerObservation(Observation):
|
|
| 79 |
search_results: str = Field(
|
| 80 |
default="", description="Papers/snippets returned from an explore step"
|
| 81 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
explored_context: str = Field(
|
| 83 |
default="",
|
| 84 |
description="Accumulated research context from all explore steps so far",
|
| 85 |
)
|
| 86 |
explore_steps_left: int = Field(
|
| 87 |
-
default=
|
| 88 |
)
|
| 89 |
repair_attempts_left: int = Field(
|
| 90 |
-
default=
|
| 91 |
)
|
| 92 |
last_errors: str = Field(
|
| 93 |
default="", description="Latest lint/build errors available for repair"
|
|
|
|
| 1 |
"""Data models for the Research -> Interactive Explainer environment."""
|
| 2 |
|
| 3 |
+
from typing import Any, Literal
|
| 4 |
|
| 5 |
from openenv.core.env_server.types import Action, Observation
|
| 6 |
from pydantic import Field
|
| 7 |
|
| 8 |
+
try:
|
| 9 |
+
from .constants import MAX_EXPLORE_STEPS, MAX_REPAIR_STEPS
|
| 10 |
+
except ImportError: # pragma: no cover - supports direct test execution
|
| 11 |
+
from constants import MAX_EXPLORE_STEPS, MAX_REPAIR_STEPS
|
| 12 |
+
|
| 13 |
|
| 14 |
ResearchTool = Literal[
|
| 15 |
"search_wikipedia",
|
|
|
|
| 84 |
search_results: str = Field(
|
| 85 |
default="", description="Papers/snippets returned from an explore step"
|
| 86 |
)
|
| 87 |
+
top_chunks: list[dict[str, Any]] = Field(
|
| 88 |
+
default_factory=list,
|
| 89 |
+
description="Ranked top chunks returned from the last explore step",
|
| 90 |
+
)
|
| 91 |
explored_context: str = Field(
|
| 92 |
default="",
|
| 93 |
description="Accumulated research context from all explore steps so far",
|
| 94 |
)
|
| 95 |
explore_steps_left: int = Field(
|
| 96 |
+
default=MAX_EXPLORE_STEPS, description="Remaining explore steps before forced generate"
|
| 97 |
)
|
| 98 |
repair_attempts_left: int = Field(
|
| 99 |
+
default=MAX_REPAIR_STEPS, description="Remaining repair attempts after failed generation"
|
| 100 |
)
|
| 101 |
last_errors: str = Field(
|
| 102 |
default="", description="Latest lint/build errors available for repair"
|
openenv_explainer_env.egg-info/PKG-INFO
CHANGED
|
@@ -4,21 +4,24 @@ Version: 0.1.0
|
|
| 4 |
Summary: Interactive Explainer OpenEnv
|
| 5 |
Requires-Python: >=3.10
|
| 6 |
Requires-Dist: openenv-core[core]>=0.2.3
|
| 7 |
-
Requires-Dist: marimo>=0.10.0
|
| 8 |
Requires-Dist: manim>=0.18.0
|
| 9 |
Requires-Dist: wikipedia-api>=0.14.1
|
| 10 |
Requires-Dist: huggingface-hub>=1.12.0
|
| 11 |
Requires-Dist: httpx>=0.28.1
|
| 12 |
Requires-Dist: nbformat>=5.10.4
|
| 13 |
-
Requires-Dist:
|
| 14 |
-
Requires-Dist:
|
| 15 |
-
Requires-Dist:
|
| 16 |
-
Requires-Dist:
|
| 17 |
-
Requires-Dist:
|
| 18 |
-
Requires-Dist:
|
| 19 |
-
Requires-Dist:
|
| 20 |
-
Requires-Dist:
|
| 21 |
-
Requires-Dist:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
Provides-Extra: dev
|
| 23 |
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 24 |
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
|
| 4 |
Summary: Interactive Explainer OpenEnv
|
| 5 |
Requires-Python: >=3.10
|
| 6 |
Requires-Dist: openenv-core[core]>=0.2.3
|
|
|
|
| 7 |
Requires-Dist: manim>=0.18.0
|
| 8 |
Requires-Dist: wikipedia-api>=0.14.1
|
| 9 |
Requires-Dist: huggingface-hub>=1.12.0
|
| 10 |
Requires-Dist: httpx>=0.28.1
|
| 11 |
Requires-Dist: nbformat>=5.10.4
|
| 12 |
+
Requires-Dist: fastembed>=0.8.0
|
| 13 |
+
Requires-Dist: altair>=6.1.0
|
| 14 |
+
Requires-Dist: seaborn>=0.13.2
|
| 15 |
+
Requires-Dist: numpy>=2.2.6
|
| 16 |
+
Requires-Dist: matplotlib>=3.10.9
|
| 17 |
+
Requires-Dist: pandas>=2.3.3
|
| 18 |
+
Requires-Dist: scipy>=1.15.3
|
| 19 |
+
Requires-Dist: sympy>=1.14.0
|
| 20 |
+
Requires-Dist: scikit-learn>=1.7.2
|
| 21 |
+
Requires-Dist: networkx>=3.4.2
|
| 22 |
+
Requires-Dist: plotly>=6.7.0
|
| 23 |
+
Requires-Dist: trafilatura>=2.0.0
|
| 24 |
+
Requires-Dist: marimo>=0.23.3
|
| 25 |
Provides-Extra: dev
|
| 26 |
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 27 |
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
openenv_explainer_env.egg-info/SOURCES.txt
CHANGED
|
@@ -1,11 +1,15 @@
|
|
| 1 |
README.md
|
| 2 |
__init__.py
|
|
|
|
| 3 |
client.py
|
|
|
|
| 4 |
models.py
|
| 5 |
pyproject.toml
|
| 6 |
task_bank.py
|
| 7 |
./__init__.py
|
|
|
|
| 8 |
./client.py
|
|
|
|
| 9 |
./models.py
|
| 10 |
./task_bank.py
|
| 11 |
openenv_explainer_env.egg-info/PKG-INFO
|
|
@@ -31,5 +35,6 @@ tests/test_client_server.py
|
|
| 31 |
tests/test_docker.py
|
| 32 |
tests/test_environment.py
|
| 33 |
tests/test_models.py
|
|
|
|
| 34 |
tests/test_rewards.py
|
| 35 |
tests/test_task_bank.py
|
|
|
|
| 1 |
README.md
|
| 2 |
__init__.py
|
| 3 |
+
a.py
|
| 4 |
client.py
|
| 5 |
+
constants.py
|
| 6 |
models.py
|
| 7 |
pyproject.toml
|
| 8 |
task_bank.py
|
| 9 |
./__init__.py
|
| 10 |
+
./a.py
|
| 11 |
./client.py
|
| 12 |
+
./constants.py
|
| 13 |
./models.py
|
| 14 |
./task_bank.py
|
| 15 |
openenv_explainer_env.egg-info/PKG-INFO
|
|
|
|
| 35 |
tests/test_docker.py
|
| 36 |
tests/test_environment.py
|
| 37 |
tests/test_models.py
|
| 38 |
+
tests/test_retrieval.py
|
| 39 |
tests/test_rewards.py
|
| 40 |
tests/test_task_bank.py
|
openenv_explainer_env.egg-info/requires.txt
CHANGED
|
@@ -1,19 +1,22 @@
|
|
| 1 |
openenv-core[core]>=0.2.3
|
| 2 |
-
marimo>=0.10.0
|
| 3 |
manim>=0.18.0
|
| 4 |
wikipedia-api>=0.14.1
|
| 5 |
huggingface-hub>=1.12.0
|
| 6 |
httpx>=0.28.1
|
| 7 |
nbformat>=5.10.4
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
[dev]
|
| 19 |
pytest>=8.0.0
|
|
|
|
| 1 |
openenv-core[core]>=0.2.3
|
|
|
|
| 2 |
manim>=0.18.0
|
| 3 |
wikipedia-api>=0.14.1
|
| 4 |
huggingface-hub>=1.12.0
|
| 5 |
httpx>=0.28.1
|
| 6 |
nbformat>=5.10.4
|
| 7 |
+
fastembed>=0.8.0
|
| 8 |
+
altair>=6.1.0
|
| 9 |
+
seaborn>=0.13.2
|
| 10 |
+
numpy>=2.2.6
|
| 11 |
+
matplotlib>=3.10.9
|
| 12 |
+
pandas>=2.3.3
|
| 13 |
+
scipy>=1.15.3
|
| 14 |
+
sympy>=1.14.0
|
| 15 |
+
scikit-learn>=1.7.2
|
| 16 |
+
networkx>=3.4.2
|
| 17 |
+
plotly>=6.7.0
|
| 18 |
+
trafilatura>=2.0.0
|
| 19 |
+
marimo>=0.23.3
|
| 20 |
|
| 21 |
[dev]
|
| 22 |
pytest>=8.0.0
|
pyproject.toml
CHANGED
|
@@ -9,21 +9,24 @@ description = "Interactive Explainer OpenEnv"
|
|
| 9 |
requires-python = ">=3.10"
|
| 10 |
dependencies = [
|
| 11 |
"openenv-core[core]>=0.2.3",
|
| 12 |
-
"marimo>=0.10.0",
|
| 13 |
"manim>=0.18.0",
|
| 14 |
"wikipedia-api>=0.14.1",
|
| 15 |
"huggingface-hub>=1.12.0",
|
| 16 |
"httpx>=0.28.1",
|
| 17 |
"nbformat>=5.10.4",
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
]
|
| 28 |
|
| 29 |
[project.optional-dependencies]
|
|
@@ -41,4 +44,7 @@ packages = ["explainer_env", "explainer_env.server", "explainer_env.rewards", "e
|
|
| 41 |
package-dir = { "explainer_env" = ".", "explainer_env.server" = "server", "explainer_env.rewards" = "rewards", "explainer_env.research" = "research" }
|
| 42 |
|
| 43 |
[dependency-groups]
|
| 44 |
-
dev = [
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
requires-python = ">=3.10"
|
| 10 |
dependencies = [
|
| 11 |
"openenv-core[core]>=0.2.3",
|
|
|
|
| 12 |
"manim>=0.18.0",
|
| 13 |
"wikipedia-api>=0.14.1",
|
| 14 |
"huggingface-hub>=1.12.0",
|
| 15 |
"httpx>=0.28.1",
|
| 16 |
"nbformat>=5.10.4",
|
| 17 |
+
"fastembed>=0.8.0",
|
| 18 |
+
"altair>=6.1.0",
|
| 19 |
+
"seaborn>=0.13.2",
|
| 20 |
+
"numpy>=2.2.6",
|
| 21 |
+
"matplotlib>=3.10.9",
|
| 22 |
+
"pandas>=2.3.3",
|
| 23 |
+
"scipy>=1.15.3",
|
| 24 |
+
"sympy>=1.14.0",
|
| 25 |
+
"scikit-learn>=1.7.2",
|
| 26 |
+
"networkx>=3.4.2",
|
| 27 |
+
"plotly>=6.7.0",
|
| 28 |
+
"trafilatura>=2.0.0",
|
| 29 |
+
"marimo>=0.23.3",
|
| 30 |
]
|
| 31 |
|
| 32 |
[project.optional-dependencies]
|
|
|
|
| 44 |
package-dir = { "explainer_env" = ".", "explainer_env.server" = "server", "explainer_env.rewards" = "rewards", "explainer_env.research" = "research" }
|
| 45 |
|
| 46 |
[dependency-groups]
|
| 47 |
+
dev = [
|
| 48 |
+
"pytest>=9.0.3",
|
| 49 |
+
"pytest-cov>=7.1.0",
|
| 50 |
+
]
|
research/retrieval.py
CHANGED
|
@@ -1,20 +1,18 @@
|
|
| 1 |
-
"""Small retrieval helpers: tokenization,
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
| 6 |
-
import os
|
| 7 |
import re
|
| 8 |
-
from
|
| 9 |
|
| 10 |
from .types import ResearchChunk
|
| 11 |
|
| 12 |
SECTION_MAX_CHARS = 900
|
| 13 |
MAX_RETURNED_CHUNKS = 5
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
_BM25_B = 0.75
|
| 18 |
|
| 19 |
_STOP_WORDS = frozenset({
|
| 20 |
"the",
|
|
@@ -95,79 +93,68 @@ def chunk_markdown(text: str, fallback_title: str) -> list[tuple[str, str]]:
|
|
| 95 |
return chunks
|
| 96 |
|
| 97 |
|
| 98 |
-
def
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if not chunks:
|
| 101 |
return []
|
| 102 |
|
| 103 |
-
|
| 104 |
-
if not
|
| 105 |
-
|
| 106 |
-
for idx, chunk in enumerate(ranked, start=1):
|
| 107 |
-
chunk.rank = idx
|
| 108 |
-
return ranked
|
| 109 |
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
df = {
|
| 116 |
-
term: sum(1 for tokens in doc_tokens if term in tokens)
|
| 117 |
-
for term in set(query_terms)
|
| 118 |
-
}
|
| 119 |
|
|
|
|
| 120 |
scored: list[ResearchChunk] = []
|
| 121 |
-
for
|
| 122 |
-
|
| 123 |
-
dl = doc_lengths[idx]
|
| 124 |
-
score = 0.0
|
| 125 |
-
for term in query_terms:
|
| 126 |
-
if df.get(term, 0) == 0:
|
| 127 |
-
continue
|
| 128 |
-
idf = math.log((n_docs - df[term] + 0.5) / (df[term] + 0.5) + 1.0)
|
| 129 |
-
tf = tf_counts.get(term, 0)
|
| 130 |
-
score += idf * tf * (_BM25_K1 + 1) / (
|
| 131 |
-
tf + _BM25_K1 * (1 - _BM25_B + _BM25_B * dl / max(avgdl, 1))
|
| 132 |
-
)
|
| 133 |
-
chunk.score = score
|
| 134 |
scored.append(chunk)
|
| 135 |
-
|
| 136 |
scored.sort(key=lambda chunk: chunk.score, reverse=True)
|
| 137 |
-
|
| 138 |
-
ranked = ranked[:top_k]
|
| 139 |
-
for idx, chunk in enumerate(ranked, start=1):
|
| 140 |
-
chunk.rank = idx
|
| 141 |
-
return ranked
|
| 142 |
|
| 143 |
|
| 144 |
-
def
|
| 145 |
-
"""
|
| 146 |
-
|
| 147 |
-
|
|
|
|
| 148 |
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
| 150 |
from fastembed import TextEmbedding
|
| 151 |
-
except Exception:
|
| 152 |
-
return chunks
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
|
| 161 |
-
if len(vectors) != len(chunks) + 1:
|
| 162 |
-
return chunks
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
| 171 |
|
| 172 |
|
| 173 |
def _cosine(a, b) -> float:
|
|
|
|
| 1 |
+
"""Small retrieval helpers: tokenization, chunking, and embedding ranking."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
|
|
|
| 6 |
import re
|
| 7 |
+
from pathlib import Path
|
| 8 |
|
| 9 |
from .types import ResearchChunk
|
| 10 |
|
| 11 |
SECTION_MAX_CHARS = 900
|
| 12 |
MAX_RETURNED_CHUNKS = 5
|
| 13 |
+
EMBEDDING_MODEL_NAME = "BAAI/bge-small-en-v1.5"
|
| 14 |
+
EMBEDDING_CACHE_DIR = Path(__file__).resolve().parents[1] / ".cache" / "fastembed"
|
| 15 |
+
_EMBEDDING_MODEL = None
|
|
|
|
| 16 |
|
| 17 |
_STOP_WORDS = frozenset({
|
| 18 |
"the",
|
|
|
|
| 93 |
return chunks
|
| 94 |
|
| 95 |
|
| 96 |
+
def rank_chunks_for_query(
|
| 97 |
+
query: str,
|
| 98 |
+
intent: str,
|
| 99 |
+
chunks: list[ResearchChunk],
|
| 100 |
+
top_k: int = MAX_RETURNED_CHUNKS,
|
| 101 |
+
embedding_model=None,
|
| 102 |
+
) -> list[ResearchChunk]:
|
| 103 |
+
"""Return the final top chunks for query+intent.
|
| 104 |
+
|
| 105 |
+
The pipeline is: source results -> text chunks -> embedding similarity
|
| 106 |
+
against query+intent -> final top-k chunks.
|
| 107 |
+
"""
|
| 108 |
if not chunks:
|
| 109 |
return []
|
| 110 |
|
| 111 |
+
query_text = f"{query} {intent}".strip()
|
| 112 |
+
if not query_text:
|
| 113 |
+
return _assign_ranks(chunks[:top_k])
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
model = embedding_model or _get_embedding_model()
|
| 116 |
+
texts = [query_text] + [_chunk_embedding_text(chunk) for chunk in chunks]
|
| 117 |
+
vectors = list(model.embed(texts))
|
| 118 |
+
if len(vectors) != len(texts):
|
| 119 |
+
raise RuntimeError("Embedding model returned an unexpected number of vectors")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
query_vec = vectors[0]
|
| 122 |
scored: list[ResearchChunk] = []
|
| 123 |
+
for chunk, vec in zip(chunks, vectors[1:]):
|
| 124 |
+
chunk.score = _cosine(query_vec, vec)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
scored.append(chunk)
|
|
|
|
| 126 |
scored.sort(key=lambda chunk: chunk.score, reverse=True)
|
| 127 |
+
return _assign_ranks(scored[:top_k])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
|
| 130 |
+
def preload_embedding_model() -> None:
|
| 131 |
+
"""Download/cache and initialize the embedding model before serving traffic."""
|
| 132 |
+
model = _get_embedding_model()
|
| 133 |
+
# Force model files and runtime session to be ready, not just configured.
|
| 134 |
+
list(model.embed(["startup warmup"]))
|
| 135 |
|
| 136 |
+
|
| 137 |
+
def _get_embedding_model():
|
| 138 |
+
global _EMBEDDING_MODEL
|
| 139 |
+
if _EMBEDDING_MODEL is None:
|
| 140 |
from fastembed import TextEmbedding
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
EMBEDDING_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
| 143 |
+
_EMBEDDING_MODEL = TextEmbedding(
|
| 144 |
+
model_name=EMBEDDING_MODEL_NAME,
|
| 145 |
+
cache_dir=str(EMBEDDING_CACHE_DIR),
|
| 146 |
+
)
|
| 147 |
+
return _EMBEDDING_MODEL
|
| 148 |
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
def _chunk_embedding_text(chunk: ResearchChunk) -> str:
|
| 151 |
+
return f"{chunk.title}\n{chunk.text}".strip()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _assign_ranks(chunks: list[ResearchChunk]) -> list[ResearchChunk]:
|
| 155 |
+
for idx, chunk in enumerate(chunks, start=1):
|
| 156 |
+
chunk.rank = idx
|
| 157 |
+
return chunks
|
| 158 |
|
| 159 |
|
| 160 |
def _cosine(a, b) -> float:
|
research/router.py
CHANGED
|
@@ -16,7 +16,7 @@ try:
|
|
| 16 |
except ImportError: # pragma: no cover - supports direct test execution
|
| 17 |
import constants as _constants
|
| 18 |
|
| 19 |
-
from .retrieval import
|
| 20 |
from .types import ResearchChunk, ResearchResult
|
| 21 |
|
| 22 |
AVAILABLE_TOOLS = _constants.AVAILABLE_TOOLS
|
|
@@ -71,7 +71,7 @@ async def search_wikipedia(query: str, intent: str = "") -> ResearchResult:
|
|
| 71 |
)
|
| 72 |
)
|
| 73 |
|
| 74 |
-
ranked =
|
| 75 |
return ResearchResult("search_wikipedia", query, ranked, raw_count=len(chunks))
|
| 76 |
except Exception as exc:
|
| 77 |
return ResearchResult("search_wikipedia", query, error=str(exc))
|
|
@@ -117,7 +117,7 @@ async def search_hf_papers(query: str, intent: str = "") -> ResearchResult:
|
|
| 117 |
)
|
| 118 |
)
|
| 119 |
|
| 120 |
-
ranked =
|
| 121 |
return ResearchResult("search_hf_papers", query, ranked, raw_count=len(chunks))
|
| 122 |
except Exception as exc:
|
| 123 |
return ResearchResult("search_hf_papers", query, error=str(exc))
|
|
@@ -173,7 +173,7 @@ async def search_arxiv(query: str, intent: str = "") -> ResearchResult:
|
|
| 173 |
)
|
| 174 |
)
|
| 175 |
|
| 176 |
-
ranked =
|
| 177 |
return ResearchResult("search_arxiv", query, ranked, raw_count=len(chunks))
|
| 178 |
except Exception as exc:
|
| 179 |
return ResearchResult("search_arxiv", query, error=str(exc))
|
|
@@ -215,7 +215,7 @@ async def search_scholar(query: str, intent: str = "") -> ResearchResult:
|
|
| 215 |
)
|
| 216 |
)
|
| 217 |
|
| 218 |
-
ranked =
|
| 219 |
return ResearchResult("search_scholar", query, ranked, raw_count=len(chunks))
|
| 220 |
except Exception as exc:
|
| 221 |
return ResearchResult("search_scholar", query, error=str(exc))
|
|
@@ -249,7 +249,7 @@ async def fetch_docs(query: str, intent: str = "") -> ResearchResult:
|
|
| 249 |
)
|
| 250 |
)
|
| 251 |
|
| 252 |
-
ranked =
|
| 253 |
return ResearchResult("fetch_docs", query, ranked, raw_count=len(chunks))
|
| 254 |
|
| 255 |
|
|
@@ -293,7 +293,7 @@ async def search_hf_hub(query: str, intent: str = "") -> ResearchResult:
|
|
| 293 |
|
| 294 |
try:
|
| 295 |
chunks = await asyncio.to_thread(_load)
|
| 296 |
-
ranked =
|
| 297 |
return ResearchResult("search_hf_hub", query, ranked, raw_count=len(chunks))
|
| 298 |
except Exception as exc:
|
| 299 |
return ResearchResult("search_hf_hub", query, error=str(exc))
|
|
@@ -323,6 +323,8 @@ _DOC_URLS = {
|
|
| 323 |
"marimo": [
|
| 324 |
("marimo CLI", "https://docs.marimo.io/cli/"),
|
| 325 |
("marimo lint rules", "https://docs.marimo.io/guides/lint_rules/"),
|
|
|
|
|
|
|
| 326 |
],
|
| 327 |
"manim": [
|
| 328 |
("Manim quickstart", "https://docs.manim.community/en/stable/tutorials/quickstart.html"),
|
|
|
|
| 16 |
except ImportError: # pragma: no cover - supports direct test execution
|
| 17 |
import constants as _constants
|
| 18 |
|
| 19 |
+
from .retrieval import chunk_markdown, rank_chunks_for_query, trim_text
|
| 20 |
from .types import ResearchChunk, ResearchResult
|
| 21 |
|
| 22 |
AVAILABLE_TOOLS = _constants.AVAILABLE_TOOLS
|
|
|
|
| 71 |
)
|
| 72 |
)
|
| 73 |
|
| 74 |
+
ranked = rank_chunks_for_query(query, intent, chunks, MAX_RETURNED_CHUNKS)
|
| 75 |
return ResearchResult("search_wikipedia", query, ranked, raw_count=len(chunks))
|
| 76 |
except Exception as exc:
|
| 77 |
return ResearchResult("search_wikipedia", query, error=str(exc))
|
|
|
|
| 117 |
)
|
| 118 |
)
|
| 119 |
|
| 120 |
+
ranked = rank_chunks_for_query(query, intent, chunks, MAX_RETURNED_CHUNKS)
|
| 121 |
return ResearchResult("search_hf_papers", query, ranked, raw_count=len(chunks))
|
| 122 |
except Exception as exc:
|
| 123 |
return ResearchResult("search_hf_papers", query, error=str(exc))
|
|
|
|
| 173 |
)
|
| 174 |
)
|
| 175 |
|
| 176 |
+
ranked = rank_chunks_for_query(query, intent, chunks, MAX_RETURNED_CHUNKS)
|
| 177 |
return ResearchResult("search_arxiv", query, ranked, raw_count=len(chunks))
|
| 178 |
except Exception as exc:
|
| 179 |
return ResearchResult("search_arxiv", query, error=str(exc))
|
|
|
|
| 215 |
)
|
| 216 |
)
|
| 217 |
|
| 218 |
+
ranked = rank_chunks_for_query(query, intent, chunks, MAX_RETURNED_CHUNKS)
|
| 219 |
return ResearchResult("search_scholar", query, ranked, raw_count=len(chunks))
|
| 220 |
except Exception as exc:
|
| 221 |
return ResearchResult("search_scholar", query, error=str(exc))
|
|
|
|
| 249 |
)
|
| 250 |
)
|
| 251 |
|
| 252 |
+
ranked = rank_chunks_for_query(query, intent, chunks, MAX_RETURNED_CHUNKS)
|
| 253 |
return ResearchResult("fetch_docs", query, ranked, raw_count=len(chunks))
|
| 254 |
|
| 255 |
|
|
|
|
| 293 |
|
| 294 |
try:
|
| 295 |
chunks = await asyncio.to_thread(_load)
|
| 296 |
+
ranked = rank_chunks_for_query(query, intent, chunks, MAX_RETURNED_CHUNKS)
|
| 297 |
return ResearchResult("search_hf_hub", query, ranked, raw_count=len(chunks))
|
| 298 |
except Exception as exc:
|
| 299 |
return ResearchResult("search_hf_hub", query, error=str(exc))
|
|
|
|
| 323 |
"marimo": [
|
| 324 |
("marimo CLI", "https://docs.marimo.io/cli/"),
|
| 325 |
("marimo lint rules", "https://docs.marimo.io/guides/lint_rules/"),
|
| 326 |
+
("marimo duplicate definitions", "https://docs.marimo.io/guides/understanding_errors/multiple_definitions/"),
|
| 327 |
+
("marimo plotting", "https://docs.marimo.io/guides/working_with_data/plotting/"),
|
| 328 |
],
|
| 329 |
"manim": [
|
| 330 |
("Manim quickstart", "https://docs.manim.community/en/stable/tutorials/quickstart.html"),
|
rewards/exploration.py
CHANGED
|
@@ -3,19 +3,19 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
try:
|
|
|
|
| 6 |
from ..research.retrieval import tokenize
|
| 7 |
from ..research.types import ResearchResult
|
| 8 |
except ImportError: # pragma: no cover - supports direct test execution
|
|
|
|
| 9 |
from research.retrieval import tokenize
|
| 10 |
from research.types import ResearchResult
|
| 11 |
|
| 12 |
-
# Weights. Keep the reward
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
W_NOVELTY = 0.15
|
| 18 |
-
W_DIVERSITY = 0.10
|
| 19 |
|
| 20 |
# Flat per-step penalty — the agent must expect enough gain to justify each search
|
| 21 |
STEP_COST = 0.05
|
|
@@ -162,6 +162,15 @@ def diversity_score(tool: str, used_tools: set[str], result: ResearchResult) ->
|
|
| 162 |
return 0.5 if len(unique_urls) > 1 else 0.25
|
| 163 |
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
# ---------------------------------------------------------------------------
|
| 166 |
# Gating
|
| 167 |
# ---------------------------------------------------------------------------
|
|
@@ -195,43 +204,47 @@ def compute_explore_reward(
|
|
| 195 |
previous_context: list[str],
|
| 196 |
accumulated_context: list[str],
|
| 197 |
used_tools: set[str] | None = None,
|
|
|
|
| 198 |
) -> tuple[float, dict]:
|
| 199 |
"""Compute per-step exploration reward. Returns (total, components)."""
|
| 200 |
used_tools = used_tools or set()
|
|
|
|
| 201 |
result_text = result.text
|
|
|
|
| 202 |
|
| 203 |
t_choice = tool_choice_score(tool, difficulty, query, intent)
|
| 204 |
q_rel = query_relevance(query, topic, keywords_csv, intent)
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
delta = coverage_delta(keywords_csv, task_content, previous_context, result_text)
|
| 207 |
-
novelty = result_novelty(result_text, previous_context)
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
info_need = max(0.0, 1.0 - sufficiency)
|
| 212 |
raw = (
|
| 213 |
-
|
| 214 |
-
+
|
| 215 |
-
+
|
| 216 |
-
+
|
| 217 |
-
+ W_NOVELTY * novelty
|
| 218 |
-
+ W_DIVERSITY * diversity
|
| 219 |
)
|
| 220 |
-
gate = _exploration_gate(
|
| 221 |
-
total = raw * gate + 0.
|
| 222 |
-
total = max(0.0, total)
|
| 223 |
|
| 224 |
components = {
|
| 225 |
-
"
|
| 226 |
-
"
|
| 227 |
-
"
|
| 228 |
-
"
|
| 229 |
-
"result_novelty": round(novelty, 3),
|
| 230 |
-
"diversity": round(diversity, 3),
|
| 231 |
-
"research_breadth": round(research_breadth(accumulated_context), 3),
|
| 232 |
-
"content_sufficiency": round(sufficiency, 3),
|
| 233 |
-
"info_need": round(info_need, 3),
|
| 234 |
-
"step_cost": STEP_COST,
|
| 235 |
"explore_total": round(total, 4),
|
| 236 |
}
|
| 237 |
return total, components
|
|
@@ -239,3 +252,15 @@ def compute_explore_reward(
|
|
| 239 |
|
| 240 |
def _normalized_text(text: str) -> str:
|
| 241 |
return " ".join(tokenize(text))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
try:
|
| 6 |
+
from ..constants import MAX_EXPLORE_REWARD
|
| 7 |
from ..research.retrieval import tokenize
|
| 8 |
from ..research.types import ResearchResult
|
| 9 |
except ImportError: # pragma: no cover - supports direct test execution
|
| 10 |
+
from constants import MAX_EXPLORE_REWARD
|
| 11 |
from research.retrieval import tokenize
|
| 12 |
from research.types import ResearchResult
|
| 13 |
|
| 14 |
+
# Weights. Keep the visible reward compact: each component maps to a skill.
|
| 15 |
+
W_QUERY_QUALITY = 0.20
|
| 16 |
+
W_EVIDENCE_QUALITY = 0.25
|
| 17 |
+
W_INFORMATION_GAIN = 0.40
|
| 18 |
+
W_EFFICIENCY = 0.15
|
|
|
|
|
|
|
| 19 |
|
| 20 |
# Flat per-step penalty — the agent must expect enough gain to justify each search
|
| 21 |
STEP_COST = 0.05
|
|
|
|
| 162 |
return 0.5 if len(unique_urls) > 1 else 0.25
|
| 163 |
|
| 164 |
|
| 165 |
+
def action_novelty(tool: str, query: str, intent: str, previous_actions: list[str]) -> float:
|
| 166 |
+
"""Score whether this explore action asks for genuinely new information."""
|
| 167 |
+
if not previous_actions:
|
| 168 |
+
return 1.0
|
| 169 |
+
current = _action_text(tool, query, intent)
|
| 170 |
+
max_similarity = max(_jaccard(current, previous) for previous in previous_actions)
|
| 171 |
+
return max(0.0, 1.0 - max_similarity)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
# ---------------------------------------------------------------------------
|
| 175 |
# Gating
|
| 176 |
# ---------------------------------------------------------------------------
|
|
|
|
| 204 |
previous_context: list[str],
|
| 205 |
accumulated_context: list[str],
|
| 206 |
used_tools: set[str] | None = None,
|
| 207 |
+
previous_actions: list[str] | None = None,
|
| 208 |
) -> tuple[float, dict]:
|
| 209 |
"""Compute per-step exploration reward. Returns (total, components)."""
|
| 210 |
used_tools = used_tools or set()
|
| 211 |
+
previous_actions = previous_actions or []
|
| 212 |
result_text = result.text
|
| 213 |
+
result_ok = result.ok
|
| 214 |
|
| 215 |
t_choice = tool_choice_score(tool, difficulty, query, intent)
|
| 216 |
q_rel = query_relevance(query, topic, keywords_csv, intent)
|
| 217 |
+
query_quality = 0.65 * q_rel + 0.35 * t_choice
|
| 218 |
+
|
| 219 |
+
src_quality = source_quality(result) if result_ok else 0.0
|
| 220 |
+
diversity = diversity_score(tool, used_tools, result) if result_ok else 0.0
|
| 221 |
+
evidence_quality = 0.75 * src_quality + 0.25 * diversity
|
| 222 |
+
|
| 223 |
delta = coverage_delta(keywords_csv, task_content, previous_context, result_text)
|
| 224 |
+
novelty = result_novelty(result_text, previous_context) if result_ok else 0.0
|
| 225 |
+
information_gain = 0.70 * delta + 0.30 * novelty if result_ok else 0.0
|
| 226 |
+
|
| 227 |
+
act_novelty = action_novelty(tool, query, intent, previous_actions)
|
| 228 |
+
sufficiency_before = content_sufficiency(task_content, keywords_csv, previous_context)
|
| 229 |
+
sufficiency_after = content_sufficiency(task_content, keywords_csv, accumulated_context)
|
| 230 |
+
info_need = max(0.0, 1.0 - sufficiency_before)
|
| 231 |
+
efficiency = act_novelty * (0.35 + 0.65 * info_need) if result_ok else 0.0
|
| 232 |
|
|
|
|
| 233 |
raw = (
|
| 234 |
+
W_QUERY_QUALITY * query_quality
|
| 235 |
+
+ W_EVIDENCE_QUALITY * evidence_quality
|
| 236 |
+
+ W_INFORMATION_GAIN * information_gain
|
| 237 |
+
+ W_EFFICIENCY * efficiency
|
|
|
|
|
|
|
| 238 |
)
|
| 239 |
+
gate = _exploration_gate(sufficiency_after) if result_ok else 0.0
|
| 240 |
+
total = raw * gate + 0.08 * info_need - STEP_COST
|
| 241 |
+
total = max(0.0, min(MAX_EXPLORE_REWARD, total))
|
| 242 |
|
| 243 |
components = {
|
| 244 |
+
"query_quality": round(query_quality, 3),
|
| 245 |
+
"evidence_quality": round(evidence_quality, 3),
|
| 246 |
+
"information_gain": round(information_gain, 3),
|
| 247 |
+
"efficiency": round(efficiency, 3),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
"explore_total": round(total, 4),
|
| 249 |
}
|
| 250 |
return total, components
|
|
|
|
| 252 |
|
| 253 |
def _normalized_text(text: str) -> str:
|
| 254 |
return " ".join(tokenize(text))
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def _action_text(tool: str, query: str, intent: str) -> str:
|
| 258 |
+
return " ".join(tokenize(f"{tool} {query} {intent}"))
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _jaccard(left: str, right: str) -> float:
|
| 262 |
+
left_tokens = set(left.split())
|
| 263 |
+
right_tokens = set(right.split())
|
| 264 |
+
if not left_tokens or not right_tokens:
|
| 265 |
+
return 0.0
|
| 266 |
+
return len(left_tokens & right_tokens) / len(left_tokens | right_tokens)
|
rewards/generation.py
CHANGED
|
@@ -1,26 +1,30 @@
|
|
| 1 |
"""Reward components for the generation phase.
|
| 2 |
|
| 3 |
After exploration, the agent generates marimo/manim code. Rewards measure
|
| 4 |
-
|
| 5 |
-
quality, narration (manim only), and context usage.
|
| 6 |
|
| 7 |
Scoring model:
|
| 8 |
-
quality = weighted sum of (
|
| 9 |
total = quality × gate
|
| 10 |
|
| 11 |
Gates (multiplicative):
|
| 12 |
- code doesn't parse → total = 0
|
| 13 |
-
-
|
|
|
|
| 14 |
- code runs → total = quality × 1.0
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
-
import hashlib
|
| 20 |
import re
|
| 21 |
from typing import TYPE_CHECKING
|
| 22 |
|
| 23 |
-
from .sandbox import ast_parses, check_marimo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
if TYPE_CHECKING:
|
| 26 |
from ..task_bank import Task
|
|
@@ -31,25 +35,23 @@ if TYPE_CHECKING:
|
|
| 31 |
# ---------------------------------------------------------------------------
|
| 32 |
|
| 33 |
_WEIGHTS = {
|
| 34 |
-
"
|
| 35 |
-
"
|
| 36 |
-
"structure": 0.
|
| 37 |
-
"
|
| 38 |
-
"context": 0.35,
|
| 39 |
}
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
|
| 44 |
-
def _get_weights(fmt: str) -> dict[str, float]:
|
| 45 |
-
"""Return component weights for the given format.
|
| 46 |
|
| 47 |
-
|
| 48 |
-
"""
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
# ---------------------------------------------------------------------------
|
|
@@ -78,7 +80,12 @@ def format_match(chosen_format: str, task: Task) -> float:
|
|
| 78 |
return 1.0 if chosen_format == task.preferred_format else 0.3
|
| 79 |
|
| 80 |
|
| 81 |
-
def marimo_structure(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
"""Score structural quality of a marimo notebook (0-1).
|
| 83 |
|
| 84 |
Additive scoring for good patterns, penalties from ``marimo check``
|
|
@@ -97,23 +104,67 @@ def marimo_structure(code: str, task: Task) -> float:
|
|
| 97 |
elif cell_count >= 1:
|
| 98 |
score += 0.1
|
| 99 |
|
| 100 |
-
ui_patterns = [
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
score += 0.2 if task.data_available else 0.1
|
| 106 |
|
| 107 |
tier_thresholds = {"advanced": 6, "intermediate": 4, "beginner": 2}
|
| 108 |
if cell_count >= tier_thresholds.get(task.tier, 2):
|
| 109 |
score += 0.1
|
| 110 |
|
| 111 |
# Marimo check: penalize breaking violations, bonus for clean code
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
if passed:
|
| 114 |
score += 0.1
|
| 115 |
else:
|
| 116 |
-
penalty = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
for v in violations:
|
| 118 |
score -= penalty.get(v, 0.15)
|
| 119 |
|
|
@@ -175,18 +226,23 @@ def narration_score(narration: str, fmt: str) -> float:
|
|
| 175 |
def context_usage(code: str, accumulated_context: list[str]) -> float:
|
| 176 |
"""Score whether the generated code incorporates research findings (0-1)."""
|
| 177 |
if not accumulated_context:
|
| 178 |
-
return 0.
|
| 179 |
|
| 180 |
context_words: set[str] = set()
|
| 181 |
for ctx in accumulated_context:
|
| 182 |
context_words.update(_tokens(ctx))
|
| 183 |
|
| 184 |
if not context_words:
|
| 185 |
-
return 0.
|
| 186 |
|
| 187 |
code_words = set(_tokens(code))
|
| 188 |
overlap = code_words & context_words
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
|
| 192 |
# ---------------------------------------------------------------------------
|
|
@@ -201,50 +257,92 @@ def compute_generate_reward(
|
|
| 201 |
task: Task,
|
| 202 |
exec_success: bool,
|
| 203 |
accumulated_context: list[str],
|
|
|
|
|
|
|
| 204 |
) -> tuple[float, dict]:
|
| 205 |
"""Compute the generation-phase reward. Returns (total, components).
|
| 206 |
|
| 207 |
-
``
|
| 208 |
-
|
|
|
|
| 209 |
"""
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
c_runs = 1.0 if exec_success else 0.0
|
| 212 |
c_coverage = keyword_coverage(code, task.keywords)
|
| 213 |
c_format = format_match(fmt, task)
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
| 216 |
c_ctx = context_usage(code, accumulated_context)
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
w = _get_weights(fmt)
|
| 219 |
quality = (
|
| 220 |
-
|
| 221 |
-
+
|
| 222 |
-
+
|
| 223 |
-
+
|
| 224 |
-
+ w["context"] * c_ctx
|
| 225 |
)
|
| 226 |
|
| 227 |
# Apply gates
|
| 228 |
-
if
|
| 229 |
total = 0.0
|
|
|
|
|
|
|
| 230 |
elif c_runs == 0.0:
|
| 231 |
total = quality * GATE_RUNS_FAIL
|
| 232 |
else:
|
| 233 |
total = quality
|
| 234 |
|
| 235 |
components = {
|
| 236 |
-
"
|
| 237 |
-
"
|
| 238 |
-
"coverage": round(c_coverage, 3),
|
| 239 |
-
"format_match": round(c_format, 3),
|
| 240 |
"structure": round(c_struct, 3),
|
| 241 |
-
"
|
| 242 |
-
"context_usage": round(c_ctx, 3),
|
| 243 |
"generate_total": round(total, 4),
|
| 244 |
}
|
| 245 |
return total, components
|
| 246 |
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
def adjust_repair_reward(
|
| 249 |
base_reward: float,
|
| 250 |
*,
|
|
@@ -255,32 +353,38 @@ def adjust_repair_reward(
|
|
| 255 |
repaired_code: str,
|
| 256 |
) -> tuple[float, dict]:
|
| 257 |
"""Discount repaired code but reward fixing the specific prior failure."""
|
| 258 |
-
|
| 259 |
fixed_prior = bool(previous_error_codes) and not (
|
| 260 |
set(previous_error_codes) & set(new_error_codes)
|
| 261 |
)
|
| 262 |
|
| 263 |
if repair_success:
|
| 264 |
-
reward = base_reward * 0.
|
|
|
|
|
|
|
| 265 |
else:
|
| 266 |
-
reward = base_reward * 0.
|
|
|
|
| 267 |
|
| 268 |
-
if
|
| 269 |
reward -= 0.15
|
| 270 |
|
| 271 |
-
reward = max(0.0, min(
|
| 272 |
return reward, {
|
| 273 |
"repair_success": 1.0 if repair_success else 0.0,
|
| 274 |
"fixed_prior_errors": 1.0 if fixed_prior else 0.0,
|
| 275 |
-
"
|
| 276 |
"repair_total": round(reward, 4),
|
| 277 |
}
|
| 278 |
|
| 279 |
|
| 280 |
def _tokens(text: str) -> list[str]:
|
| 281 |
-
return [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
|
| 284 |
def _fingerprint(code: str) -> str:
|
| 285 |
-
|
| 286 |
-
return hashlib.sha256(normalized.encode()).hexdigest()
|
|
|
|
| 1 |
"""Reward components for the generation phase.
|
| 2 |
|
| 3 |
After exploration, the agent generates marimo/manim code. Rewards measure
|
| 4 |
+
validity, task alignment, artifact structure, and research usage.
|
|
|
|
| 5 |
|
| 6 |
Scoring model:
|
| 7 |
+
quality = weighted sum of (validity, task alignment, structure, research usage)
|
| 8 |
total = quality × gate
|
| 9 |
|
| 10 |
Gates (multiplicative):
|
| 11 |
- code doesn't parse → total = 0
|
| 12 |
+
- static check fails → total = quality × small static-fail multiplier
|
| 13 |
+
- code doesn't run → total = quality × execution-fail multiplier
|
| 14 |
- code runs → total = quality × 1.0
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
|
|
|
| 19 |
import re
|
| 20 |
from typing import TYPE_CHECKING
|
| 21 |
|
| 22 |
+
from .sandbox import ast_parses, check_marimo, extract_scene_class
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from ..constants import MAX_REPAIR_REWARD
|
| 26 |
+
except ImportError: # pragma: no cover - supports direct test execution
|
| 27 |
+
from constants import MAX_REPAIR_REWARD
|
| 28 |
|
| 29 |
if TYPE_CHECKING:
|
| 30 |
from ..task_bank import Task
|
|
|
|
| 35 |
# ---------------------------------------------------------------------------
|
| 36 |
|
| 37 |
_WEIGHTS = {
|
| 38 |
+
"validity": 0.15,
|
| 39 |
+
"task_alignment": 0.30,
|
| 40 |
+
"structure": 0.30,
|
| 41 |
+
"research_usage": 0.25,
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
+
GATE_STATIC_FAIL = 0.12
|
| 45 |
+
GATE_RUNS_FAIL = 0.30 # quality multiplier when static checks pass but execution fails
|
| 46 |
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
_STOPWORDS = {
|
| 49 |
+
"about", "after", "again", "against", "also", "because", "before", "being",
|
| 50 |
+
"between", "class", "code", "construct", "could", "from", "have", "into",
|
| 51 |
+
"like", "make", "more", "most", "only", "self", "show", "step", "than",
|
| 52 |
+
"that", "their", "then", "there", "these", "this", "through", "using",
|
| 53 |
+
"value", "where", "with", "would",
|
| 54 |
+
}
|
| 55 |
|
| 56 |
|
| 57 |
# ---------------------------------------------------------------------------
|
|
|
|
| 80 |
return 1.0 if chosen_format == task.preferred_format else 0.3
|
| 81 |
|
| 82 |
|
| 83 |
+
def marimo_structure(
|
| 84 |
+
code: str,
|
| 85 |
+
task: Task,
|
| 86 |
+
static_check_passed: bool | None = None,
|
| 87 |
+
error_codes: list[str] | None = None,
|
| 88 |
+
) -> float:
|
| 89 |
"""Score structural quality of a marimo notebook (0-1).
|
| 90 |
|
| 91 |
Additive scoring for good patterns, penalties from ``marimo check``
|
|
|
|
| 104 |
elif cell_count >= 1:
|
| 105 |
score += 0.1
|
| 106 |
|
| 107 |
+
ui_patterns = [
|
| 108 |
+
"mo.md(",
|
| 109 |
+
"mo.Html",
|
| 110 |
+
"mo.accordion",
|
| 111 |
+
"mo.callout",
|
| 112 |
+
"mo.hstack(",
|
| 113 |
+
"mo.vstack(",
|
| 114 |
+
"mo.ui.slider",
|
| 115 |
+
"mo.ui.dropdown",
|
| 116 |
+
"mo.ui.table",
|
| 117 |
+
"mo.ui.dataframe",
|
| 118 |
+
]
|
| 119 |
+
score += min(0.22, sum(0.06 for p in ui_patterns if p in code))
|
| 120 |
+
|
| 121 |
+
reactive_plot_patterns = [
|
| 122 |
+
"mo.ui.matplotlib(",
|
| 123 |
+
"mo.ui.plotly(",
|
| 124 |
+
"mo.ui.altair_chart(",
|
| 125 |
+
]
|
| 126 |
+
raw_plot_patterns = [
|
| 127 |
+
"plt.",
|
| 128 |
+
"matplotlib.pyplot",
|
| 129 |
+
"px.",
|
| 130 |
+
"plotly.",
|
| 131 |
+
"alt.Chart",
|
| 132 |
+
]
|
| 133 |
+
if "mo.ui.matplotlib(plt.gca())" in code:
|
| 134 |
+
score += 0.24 if task.data_available else 0.16
|
| 135 |
+
elif any(p in code for p in reactive_plot_patterns):
|
| 136 |
+
score += 0.18 if task.data_available else 0.10
|
| 137 |
+
elif any(p in code for p in raw_plot_patterns):
|
| 138 |
+
score += 0.08 if task.data_available else 0.03
|
| 139 |
+
score -= 0.08
|
| 140 |
+
|
| 141 |
+
if "plt.tight_layout(" in code:
|
| 142 |
+
score -= 0.12
|
| 143 |
|
| 144 |
+
if "np.math." in code:
|
| 145 |
+
score -= 0.15
|
|
|
|
| 146 |
|
| 147 |
tier_thresholds = {"advanced": 6, "intermediate": 4, "beginner": 2}
|
| 148 |
if cell_count >= tier_thresholds.get(task.tier, 2):
|
| 149 |
score += 0.1
|
| 150 |
|
| 151 |
# Marimo check: penalize breaking violations, bonus for clean code
|
| 152 |
+
if static_check_passed is None:
|
| 153 |
+
passed, _, violations = check_marimo(code)
|
| 154 |
+
else:
|
| 155 |
+
passed = static_check_passed
|
| 156 |
+
violations = error_codes or []
|
| 157 |
+
|
| 158 |
if passed:
|
| 159 |
score += 0.1
|
| 160 |
else:
|
| 161 |
+
penalty = {
|
| 162 |
+
"MB002": 0.35,
|
| 163 |
+
"MB003": 0.4,
|
| 164 |
+
"MB005": 0.25,
|
| 165 |
+
"MB001": 0.3,
|
| 166 |
+
"MB004": 0.2,
|
| 167 |
+
}
|
| 168 |
for v in violations:
|
| 169 |
score -= penalty.get(v, 0.15)
|
| 170 |
|
|
|
|
| 226 |
def context_usage(code: str, accumulated_context: list[str]) -> float:
|
| 227 |
"""Score whether the generated code incorporates research findings (0-1)."""
|
| 228 |
if not accumulated_context:
|
| 229 |
+
return 0.0
|
| 230 |
|
| 231 |
context_words: set[str] = set()
|
| 232 |
for ctx in accumulated_context:
|
| 233 |
context_words.update(_tokens(ctx))
|
| 234 |
|
| 235 |
if not context_words:
|
| 236 |
+
return 0.0
|
| 237 |
|
| 238 |
code_words = set(_tokens(code))
|
| 239 |
overlap = code_words & context_words
|
| 240 |
+
if not overlap:
|
| 241 |
+
return 0.0
|
| 242 |
+
# Do not reward broad generic overlap too heavily; a few meaningful terms
|
| 243 |
+
# should help, but strong usage needs a substantial slice of the context.
|
| 244 |
+
target = min(max(len(context_words), 1), 24)
|
| 245 |
+
return min(1.0, len(overlap) / target * 2.5)
|
| 246 |
|
| 247 |
|
| 248 |
# ---------------------------------------------------------------------------
|
|
|
|
| 257 |
task: Task,
|
| 258 |
exec_success: bool,
|
| 259 |
accumulated_context: list[str],
|
| 260 |
+
static_check_passed: bool | None = None,
|
| 261 |
+
error_codes: list[str] | None = None,
|
| 262 |
) -> tuple[float, dict]:
|
| 263 |
"""Compute the generation-phase reward. Returns (total, components).
|
| 264 |
|
| 265 |
+
``python_parse_valid``, ``static_check_passed``, and ``code_runs`` act as
|
| 266 |
+
gates. ``code_valid`` means the artifact is valid for its target format,
|
| 267 |
+
not merely that the Python AST parses.
|
| 268 |
"""
|
| 269 |
+
parse_valid = ast_parses(code)
|
| 270 |
+
c_parse = 1.0 if parse_valid else 0.0
|
| 271 |
+
if static_check_passed is None:
|
| 272 |
+
static_check_passed = _infer_static_check(code, fmt, parse_valid)
|
| 273 |
+
|
| 274 |
+
c_static = 1.0 if parse_valid and static_check_passed else 0.0
|
| 275 |
c_runs = 1.0 if exec_success else 0.0
|
| 276 |
c_coverage = keyword_coverage(code, task.keywords)
|
| 277 |
c_format = format_match(fmt, task)
|
| 278 |
+
if fmt == "marimo":
|
| 279 |
+
c_struct = marimo_structure(code, task, static_check_passed, error_codes)
|
| 280 |
+
else:
|
| 281 |
+
scene_structure = manim_structure(code, task)
|
| 282 |
+
c_struct = 0.75 * scene_structure + 0.25 * narration_score(narration, fmt)
|
| 283 |
c_ctx = context_usage(code, accumulated_context)
|
| 284 |
+
c_validity = _validity_score(c_parse, c_static, c_runs)
|
| 285 |
+
c_alignment = 0.75 * c_coverage + 0.25 * c_format
|
| 286 |
|
|
|
|
| 287 |
quality = (
|
| 288 |
+
_WEIGHTS["validity"] * c_validity
|
| 289 |
+
+ _WEIGHTS["task_alignment"] * c_alignment
|
| 290 |
+
+ _WEIGHTS["structure"] * c_struct
|
| 291 |
+
+ _WEIGHTS["research_usage"] * c_ctx
|
|
|
|
| 292 |
)
|
| 293 |
|
| 294 |
# Apply gates
|
| 295 |
+
if c_parse == 0.0:
|
| 296 |
total = 0.0
|
| 297 |
+
elif c_static == 0.0:
|
| 298 |
+
total = quality * _static_fail_multiplier(error_codes or [])
|
| 299 |
elif c_runs == 0.0:
|
| 300 |
total = quality * GATE_RUNS_FAIL
|
| 301 |
else:
|
| 302 |
total = quality
|
| 303 |
|
| 304 |
components = {
|
| 305 |
+
"validity": round(c_validity, 3),
|
| 306 |
+
"task_alignment": round(c_alignment, 3),
|
|
|
|
|
|
|
| 307 |
"structure": round(c_struct, 3),
|
| 308 |
+
"research_usage": round(c_ctx, 3),
|
|
|
|
| 309 |
"generate_total": round(total, 4),
|
| 310 |
}
|
| 311 |
return total, components
|
| 312 |
|
| 313 |
|
| 314 |
+
def _infer_static_check(code: str, fmt: str, parse_valid: bool) -> bool:
|
| 315 |
+
if not parse_valid:
|
| 316 |
+
return False
|
| 317 |
+
if fmt == "marimo":
|
| 318 |
+
passed, _, _ = check_marimo(code)
|
| 319 |
+
return passed
|
| 320 |
+
if fmt == "manim":
|
| 321 |
+
return extract_scene_class(code) is not None
|
| 322 |
+
return False
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _static_fail_multiplier(error_codes: list[str]) -> float:
|
| 326 |
+
"""Keep parseable but structurally invalid artifacts from scoring high."""
|
| 327 |
+
if any(code.startswith("MB") for code in error_codes):
|
| 328 |
+
return GATE_STATIC_FAIL
|
| 329 |
+
return min(GATE_RUNS_FAIL, GATE_STATIC_FAIL * 1.5)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _validity_score(
|
| 333 |
+
parse_valid: float,
|
| 334 |
+
static_check_passed: float,
|
| 335 |
+
code_runs: float,
|
| 336 |
+
) -> float:
|
| 337 |
+
if parse_valid == 0.0:
|
| 338 |
+
return 0.0
|
| 339 |
+
if static_check_passed == 0.0:
|
| 340 |
+
return 0.35
|
| 341 |
+
if code_runs == 0.0:
|
| 342 |
+
return 0.70
|
| 343 |
+
return 1.0
|
| 344 |
+
|
| 345 |
+
|
| 346 |
def adjust_repair_reward(
|
| 347 |
base_reward: float,
|
| 348 |
*,
|
|
|
|
| 353 |
repaired_code: str,
|
| 354 |
) -> tuple[float, dict]:
|
| 355 |
"""Discount repaired code but reward fixing the specific prior failure."""
|
| 356 |
+
changed = _fingerprint(previous_code) != _fingerprint(repaired_code)
|
| 357 |
fixed_prior = bool(previous_error_codes) and not (
|
| 358 |
set(previous_error_codes) & set(new_error_codes)
|
| 359 |
)
|
| 360 |
|
| 361 |
if repair_success:
|
| 362 |
+
reward = base_reward * 0.60
|
| 363 |
+
reward += 0.08 if fixed_prior else 0.0
|
| 364 |
+
reward += 0.04 if changed else 0.0
|
| 365 |
else:
|
| 366 |
+
reward = base_reward * 0.25
|
| 367 |
+
reward += 0.04 if fixed_prior else 0.0
|
| 368 |
|
| 369 |
+
if not changed:
|
| 370 |
reward -= 0.15
|
| 371 |
|
| 372 |
+
reward = max(0.0, min(MAX_REPAIR_REWARD, reward))
|
| 373 |
return reward, {
|
| 374 |
"repair_success": 1.0 if repair_success else 0.0,
|
| 375 |
"fixed_prior_errors": 1.0 if fixed_prior else 0.0,
|
| 376 |
+
"changed_code": 1.0 if changed else 0.0,
|
| 377 |
"repair_total": round(reward, 4),
|
| 378 |
}
|
| 379 |
|
| 380 |
|
| 381 |
def _tokens(text: str) -> list[str]:
|
| 382 |
+
return [
|
| 383 |
+
w
|
| 384 |
+
for w in re.findall(r"\w+", text.lower())
|
| 385 |
+
if len(w) > 3 and w not in _STOPWORDS
|
| 386 |
+
]
|
| 387 |
|
| 388 |
|
| 389 |
def _fingerprint(code: str) -> str:
|
| 390 |
+
return re.sub(r"\s+", "", code)
|
|
|
rewards/sandbox.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
import ast
|
| 4 |
import json
|
| 5 |
import subprocess
|
|
|
|
| 6 |
import tempfile
|
| 7 |
from dataclasses import dataclass, field
|
| 8 |
from typing import Any
|
|
@@ -49,6 +50,26 @@ def ast_parses(code: str) -> bool:
|
|
| 49 |
return False
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
def extract_scene_class(code: str) -> str | None:
|
| 53 |
"""Return the first Scene subclass name found in manim code."""
|
| 54 |
try:
|
|
@@ -86,20 +107,26 @@ def check_marimo(code: str, timeout: int = 8) -> tuple[bool, str, list[str]]:
|
|
| 86 |
tmp = f.name
|
| 87 |
try:
|
| 88 |
result = subprocess.run(
|
| 89 |
-
["marimo", "check", "--format", "json", "--select", "MB", tmp],
|
| 90 |
capture_output=True,
|
| 91 |
text=True,
|
| 92 |
timeout=timeout,
|
| 93 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
data = json.loads(result.stdout)
|
| 95 |
issues = data.get("issues", [])
|
| 96 |
if not issues:
|
| 97 |
return True, "marimo check passed", []
|
| 98 |
|
| 99 |
-
codes =
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
return False, msg, codes
|
| 104 |
|
| 105 |
except FileNotFoundError:
|
|
@@ -107,7 +134,8 @@ def check_marimo(code: str, timeout: int = 8) -> tuple[bool, str, list[str]]:
|
|
| 107 |
except subprocess.TimeoutExpired:
|
| 108 |
return False, "marimo check timed out", ["MARIMO_TIMEOUT"]
|
| 109 |
except (json.JSONDecodeError, KeyError):
|
| 110 |
-
|
|
|
|
| 111 |
finally:
|
| 112 |
Path(tmp).unlink(missing_ok=True)
|
| 113 |
|
|
@@ -135,14 +163,14 @@ def run_marimo(code: str, timeout: int = 15, *, skip_check: bool = False) -> tup
|
|
| 135 |
tmp = f.name
|
| 136 |
try:
|
| 137 |
result = subprocess.run(
|
| 138 |
-
["marimo", "export", "html", tmp],
|
| 139 |
capture_output=True,
|
| 140 |
text=True,
|
| 141 |
timeout=max(1, timeout - check_timeout),
|
| 142 |
)
|
| 143 |
if result.returncode == 0:
|
| 144 |
return True, "marimo export succeeded"
|
| 145 |
-
return False, result
|
| 146 |
except FileNotFoundError:
|
| 147 |
return False, "marimo not installed"
|
| 148 |
except subprocess.TimeoutExpired:
|
|
@@ -162,14 +190,14 @@ def run_manim(code: str, timeout: int = 30) -> tuple[bool, str]:
|
|
| 162 |
src.write_text(code)
|
| 163 |
try:
|
| 164 |
result = subprocess.run(
|
| 165 |
-
["manim", "render", "-ql", "--media_dir", tmpdir, str(src), scene],
|
| 166 |
capture_output=True,
|
| 167 |
text=True,
|
| 168 |
timeout=timeout,
|
| 169 |
)
|
| 170 |
if result.returncode == 0:
|
| 171 |
return True, "manim render succeeded"
|
| 172 |
-
return False, result
|
| 173 |
except FileNotFoundError:
|
| 174 |
return False, "manim not installed"
|
| 175 |
except subprocess.TimeoutExpired:
|
|
@@ -179,13 +207,14 @@ def run_manim(code: str, timeout: int = 30) -> tuple[bool, str]:
|
|
| 179 |
def validate_code(fmt: str, code: str) -> SandboxResult:
|
| 180 |
"""Validate code and return parseable feedback for generation/repair."""
|
| 181 |
if not ast_parses(code):
|
|
|
|
| 182 |
return SandboxResult(
|
| 183 |
fmt=fmt,
|
| 184 |
parses=False,
|
| 185 |
check_passed=False,
|
| 186 |
exec_success=False,
|
| 187 |
message="Code has syntax errors and cannot be parsed.",
|
| 188 |
-
errors=[{"code": "PY_SYNTAX", "message": "Code has syntax errors."}],
|
| 189 |
)
|
| 190 |
|
| 191 |
if fmt == "marimo":
|
|
@@ -239,3 +268,39 @@ def validate_code(fmt: str, code: str) -> SandboxResult:
|
|
| 239 |
message=f"Unknown format: {fmt}",
|
| 240 |
errors=[{"code": "UNKNOWN_FORMAT", "message": f"Unknown format: {fmt}"}],
|
| 241 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import ast
|
| 4 |
import json
|
| 5 |
import subprocess
|
| 6 |
+
import sys
|
| 7 |
import tempfile
|
| 8 |
from dataclasses import dataclass, field
|
| 9 |
from typing import Any
|
|
|
|
| 50 |
return False
|
| 51 |
|
| 52 |
|
| 53 |
+
def syntax_error_message(code: str) -> str:
|
| 54 |
+
"""Return a line-level Python syntax error message for repair prompts."""
|
| 55 |
+
try:
|
| 56 |
+
ast.parse(code)
|
| 57 |
+
except SyntaxError as exc:
|
| 58 |
+
location = []
|
| 59 |
+
if exc.lineno is not None:
|
| 60 |
+
location.append(f"line {exc.lineno}")
|
| 61 |
+
if exc.offset is not None:
|
| 62 |
+
location.append(f"column {exc.offset}")
|
| 63 |
+
prefix = f" at {', '.join(location)}" if location else ""
|
| 64 |
+
details = f"{exc.msg}{prefix}"
|
| 65 |
+
if exc.text:
|
| 66 |
+
details += f"\n {exc.text.strip()}"
|
| 67 |
+
if exc.offset:
|
| 68 |
+
details += f"\n {' ' * max(exc.offset - 1, 0)}^"
|
| 69 |
+
return details
|
| 70 |
+
return ""
|
| 71 |
+
|
| 72 |
+
|
| 73 |
def extract_scene_class(code: str) -> str | None:
|
| 74 |
"""Return the first Scene subclass name found in manim code."""
|
| 75 |
try:
|
|
|
|
| 107 |
tmp = f.name
|
| 108 |
try:
|
| 109 |
result = subprocess.run(
|
| 110 |
+
[sys.executable, "-m", "marimo", "check", "--format", "json", "--select", "MB", tmp],
|
| 111 |
capture_output=True,
|
| 112 |
text=True,
|
| 113 |
timeout=timeout,
|
| 114 |
)
|
| 115 |
+
if not result.stdout.strip():
|
| 116 |
+
message = _subprocess_error_message(result, "marimo check produced no output")
|
| 117 |
+
code = "MARIMO_MISSING" if _missing_module(result, "marimo") else "MARIMO_CHECK"
|
| 118 |
+
return False, message, [code]
|
| 119 |
data = json.loads(result.stdout)
|
| 120 |
issues = data.get("issues", [])
|
| 121 |
if not issues:
|
| 122 |
return True, "marimo check passed", []
|
| 123 |
|
| 124 |
+
codes = []
|
| 125 |
+
for issue in issues:
|
| 126 |
+
code = issue.get("code")
|
| 127 |
+
if code and code not in codes:
|
| 128 |
+
codes.append(code)
|
| 129 |
+
msg = "\n\n".join(_format_marimo_issue(issue) for issue in issues[:3])
|
| 130 |
return False, msg, codes
|
| 131 |
|
| 132 |
except FileNotFoundError:
|
|
|
|
| 134 |
except subprocess.TimeoutExpired:
|
| 135 |
return False, "marimo check timed out", ["MARIMO_TIMEOUT"]
|
| 136 |
except (json.JSONDecodeError, KeyError):
|
| 137 |
+
message = _subprocess_error_message(result, "marimo check output unparseable")
|
| 138 |
+
return False, message, ["MARIMO_CHECK_PARSE"]
|
| 139 |
finally:
|
| 140 |
Path(tmp).unlink(missing_ok=True)
|
| 141 |
|
|
|
|
| 163 |
tmp = f.name
|
| 164 |
try:
|
| 165 |
result = subprocess.run(
|
| 166 |
+
[sys.executable, "-m", "marimo", "export", "html", tmp],
|
| 167 |
capture_output=True,
|
| 168 |
text=True,
|
| 169 |
timeout=max(1, timeout - check_timeout),
|
| 170 |
)
|
| 171 |
if result.returncode == 0:
|
| 172 |
return True, "marimo export succeeded"
|
| 173 |
+
return False, _subprocess_error_message(result, "marimo export failed")[:500]
|
| 174 |
except FileNotFoundError:
|
| 175 |
return False, "marimo not installed"
|
| 176 |
except subprocess.TimeoutExpired:
|
|
|
|
| 190 |
src.write_text(code)
|
| 191 |
try:
|
| 192 |
result = subprocess.run(
|
| 193 |
+
[sys.executable, "-m", "manim", "render", "-ql", "--media_dir", tmpdir, str(src), scene],
|
| 194 |
capture_output=True,
|
| 195 |
text=True,
|
| 196 |
timeout=timeout,
|
| 197 |
)
|
| 198 |
if result.returncode == 0:
|
| 199 |
return True, "manim render succeeded"
|
| 200 |
+
return False, _subprocess_error_message(result, "manim render failed")[:500]
|
| 201 |
except FileNotFoundError:
|
| 202 |
return False, "manim not installed"
|
| 203 |
except subprocess.TimeoutExpired:
|
|
|
|
| 207 |
def validate_code(fmt: str, code: str) -> SandboxResult:
|
| 208 |
"""Validate code and return parseable feedback for generation/repair."""
|
| 209 |
if not ast_parses(code):
|
| 210 |
+
details = syntax_error_message(code)
|
| 211 |
return SandboxResult(
|
| 212 |
fmt=fmt,
|
| 213 |
parses=False,
|
| 214 |
check_passed=False,
|
| 215 |
exec_success=False,
|
| 216 |
message="Code has syntax errors and cannot be parsed.",
|
| 217 |
+
errors=[{"code": "PY_SYNTAX", "message": details or "Code has syntax errors."}],
|
| 218 |
)
|
| 219 |
|
| 220 |
if fmt == "marimo":
|
|
|
|
| 268 |
message=f"Unknown format: {fmt}",
|
| 269 |
errors=[{"code": "UNKNOWN_FORMAT", "message": f"Unknown format: {fmt}"}],
|
| 270 |
)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def _format_marimo_issue(issue: dict[str, Any]) -> str:
|
| 274 |
+
code = issue.get("code", "MB")
|
| 275 |
+
message = issue.get("message", "unknown error")
|
| 276 |
+
fix_hint = issue.get("fix", "")
|
| 277 |
+
location = _format_issue_location(issue)
|
| 278 |
+
rendered = f"{code}{location}: {message}"
|
| 279 |
+
if fix_hint:
|
| 280 |
+
rendered += f"\nFix hint: {fix_hint}"
|
| 281 |
+
return rendered
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _missing_module(result: subprocess.CompletedProcess[str], module: str) -> bool:
|
| 285 |
+
output = f"{result.stderr}\n{result.stdout}"
|
| 286 |
+
return f"No module named {module}" in output
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _subprocess_error_message(
|
| 290 |
+
result: subprocess.CompletedProcess[str],
|
| 291 |
+
fallback: str,
|
| 292 |
+
) -> str:
|
| 293 |
+
details = (result.stderr or result.stdout or "").strip()
|
| 294 |
+
if details:
|
| 295 |
+
return details
|
| 296 |
+
return fallback
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _format_issue_location(issue: dict[str, Any]) -> str:
|
| 300 |
+
line = issue.get("line") or issue.get("lineno") or issue.get("start_line")
|
| 301 |
+
column = issue.get("column") or issue.get("col") or issue.get("start_column")
|
| 302 |
+
if line and column:
|
| 303 |
+
return f" at line {line}, column {column}"
|
| 304 |
+
if line:
|
| 305 |
+
return f" at line {line}"
|
| 306 |
+
return ""
|
server/app.py
CHANGED
|
@@ -28,6 +28,8 @@ Usage:
|
|
| 28 |
python -m server.app
|
| 29 |
"""
|
| 30 |
|
|
|
|
|
|
|
| 31 |
try:
|
| 32 |
from openenv.core.env_server.http_server import create_app
|
| 33 |
except Exception as e: # pragma: no cover
|
|
@@ -35,9 +37,11 @@ except Exception as e: # pragma: no cover
|
|
| 35 |
|
| 36 |
try:
|
| 37 |
from ..models import ExplainerAction, ExplainerObservation
|
|
|
|
| 38 |
from .explainer_env_environment import ExplainerEnvironment
|
| 39 |
except ImportError:
|
| 40 |
from models import ExplainerAction, ExplainerObservation
|
|
|
|
| 41 |
from server.explainer_env_environment import ExplainerEnvironment
|
| 42 |
|
| 43 |
|
|
@@ -51,6 +55,25 @@ app = create_app(
|
|
| 51 |
)
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 55 |
"""
|
| 56 |
Entry point for direct execution via uv run or python -m.
|
|
|
|
| 28 |
python -m server.app
|
| 29 |
"""
|
| 30 |
|
| 31 |
+
from contextlib import asynccontextmanager
|
| 32 |
+
|
| 33 |
try:
|
| 34 |
from openenv.core.env_server.http_server import create_app
|
| 35 |
except Exception as e: # pragma: no cover
|
|
|
|
| 37 |
|
| 38 |
try:
|
| 39 |
from ..models import ExplainerAction, ExplainerObservation
|
| 40 |
+
from ..research.retrieval import EMBEDDING_CACHE_DIR, EMBEDDING_MODEL_NAME, preload_embedding_model
|
| 41 |
from .explainer_env_environment import ExplainerEnvironment
|
| 42 |
except ImportError:
|
| 43 |
from models import ExplainerAction, ExplainerObservation
|
| 44 |
+
from research.retrieval import EMBEDDING_CACHE_DIR, EMBEDDING_MODEL_NAME, preload_embedding_model
|
| 45 |
from server.explainer_env_environment import ExplainerEnvironment
|
| 46 |
|
| 47 |
|
|
|
|
| 55 |
)
|
| 56 |
|
| 57 |
|
| 58 |
+
_base_lifespan = app.router.lifespan_context
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@asynccontextmanager
|
| 62 |
+
async def _lifespan(app_instance):
|
| 63 |
+
"""Block startup until the embedding model is downloaded and initialized."""
|
| 64 |
+
preload_embedding_model()
|
| 65 |
+
print(
|
| 66 |
+
f"Embedding model ready: {EMBEDDING_MODEL_NAME} "
|
| 67 |
+
f"(cache={EMBEDDING_CACHE_DIR})",
|
| 68 |
+
flush=True,
|
| 69 |
+
)
|
| 70 |
+
async with _base_lifespan(app_instance):
|
| 71 |
+
yield
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
app.router.lifespan_context = _lifespan
|
| 75 |
+
|
| 76 |
+
|
| 77 |
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 78 |
"""
|
| 79 |
Entry point for direct execution via uv run or python -m.
|
server/explainer_env_environment.py
CHANGED
|
@@ -38,6 +38,23 @@ except ImportError:
|
|
| 38 |
from task_bank import ALL_TASKS, EASY_TASKS, HARD_TASKS, MEDIUM_TASKS, Task
|
| 39 |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
class ExplainerEnvironment(Environment):
|
| 42 |
"""
|
| 43 |
Multi-step Research → Interactive Explainer environment.
|
|
@@ -57,6 +74,7 @@ class ExplainerEnvironment(Environment):
|
|
| 57 |
self._current_task: Task | None = None
|
| 58 |
self._difficulty_pool: list[Task] = EASY_TASKS
|
| 59 |
self._accumulated_context: list[str] = []
|
|
|
|
| 60 |
self._used_tools: set[str] = set()
|
| 61 |
self._explore_steps: int = 0
|
| 62 |
self._repair_steps: int = 0
|
|
@@ -184,6 +202,7 @@ class ExplainerEnvironment(Environment):
|
|
| 184 |
episode_id=episode_id or str(uuid4()), step_count=0
|
| 185 |
)
|
| 186 |
self._accumulated_context = []
|
|
|
|
| 187 |
self._used_tools = set()
|
| 188 |
self._explore_steps = 0
|
| 189 |
self._repair_steps = 0
|
|
@@ -273,10 +292,12 @@ class ExplainerEnvironment(Environment):
|
|
| 273 |
)
|
| 274 |
|
| 275 |
previous_context = list(self._accumulated_context)
|
|
|
|
| 276 |
used_tools = set(self._used_tools)
|
| 277 |
|
| 278 |
result = await run_research_tool(tool, query, intent)
|
| 279 |
results_text = result.render()
|
|
|
|
| 280 |
if result.ok:
|
| 281 |
self._accumulated_context.append(result.text)
|
| 282 |
self._used_tools.add(tool)
|
|
@@ -294,6 +315,7 @@ class ExplainerEnvironment(Environment):
|
|
| 294 |
previous_context=previous_context,
|
| 295 |
accumulated_context=self._accumulated_context,
|
| 296 |
used_tools=used_tools,
|
|
|
|
| 297 |
)
|
| 298 |
|
| 299 |
steps_left = MAX_EXPLORE_STEPS - self._explore_steps
|
|
@@ -313,12 +335,14 @@ class ExplainerEnvironment(Environment):
|
|
| 313 |
phase=phase,
|
| 314 |
feedback=f"{hint}\nTool: {tool}\nReward: {components}",
|
| 315 |
search_results=results_text,
|
|
|
|
| 316 |
reward=reward,
|
| 317 |
metadata={
|
| 318 |
"step": self._state.step_count,
|
| 319 |
"phase": "explore",
|
| 320 |
"tool": tool,
|
| 321 |
"source_count": len(result.chunks),
|
|
|
|
| 322 |
"error": result.error,
|
| 323 |
**components,
|
| 324 |
},
|
|
@@ -356,13 +380,16 @@ class ExplainerEnvironment(Environment):
|
|
| 356 |
task=task,
|
| 357 |
exec_success=sandbox.exec_success,
|
| 358 |
accumulated_context=self._accumulated_context,
|
|
|
|
|
|
|
| 359 |
)
|
| 360 |
reward = max(0.0, reward + skip_penalty)
|
| 361 |
|
| 362 |
self._last_code = code
|
| 363 |
self._last_format = fmt
|
| 364 |
self._last_narration = narration
|
| 365 |
-
|
|
|
|
| 366 |
self._last_error_codes = sandbox.error_codes
|
| 367 |
|
| 368 |
# Feedback
|
|
@@ -372,7 +399,7 @@ class ExplainerEnvironment(Environment):
|
|
| 372 |
if not sandbox.parses:
|
| 373 |
parts.append("SYNTAX ERROR: code does not parse.")
|
| 374 |
elif not sandbox.exec_success:
|
| 375 |
-
parts.append(f"EXECUTION FAILED: {
|
| 376 |
else:
|
| 377 |
parts.append(f"EXECUTION OK: {sandbox.message}")
|
| 378 |
parts.append(
|
|
@@ -384,7 +411,10 @@ class ExplainerEnvironment(Environment):
|
|
| 384 |
self._phase = phase
|
| 385 |
self._done = done
|
| 386 |
if not done:
|
| 387 |
-
parts.append(
|
|
|
|
|
|
|
|
|
|
| 388 |
|
| 389 |
return self._make_obs(
|
| 390 |
task,
|
|
@@ -392,7 +422,7 @@ class ExplainerEnvironment(Environment):
|
|
| 392 |
feedback="\n".join(parts),
|
| 393 |
reward=reward,
|
| 394 |
done=done,
|
| 395 |
-
last_errors="" if sandbox.exec_success else
|
| 396 |
metadata={
|
| 397 |
"step": self._state.step_count,
|
| 398 |
"phase": "generate",
|
|
@@ -439,6 +469,8 @@ class ExplainerEnvironment(Environment):
|
|
| 439 |
task=task,
|
| 440 |
exec_success=sandbox.exec_success,
|
| 441 |
accumulated_context=self._accumulated_context,
|
|
|
|
|
|
|
| 442 |
)
|
| 443 |
repair_reward, repair_components = adjust_repair_reward(
|
| 444 |
base_reward,
|
|
@@ -453,23 +485,34 @@ class ExplainerEnvironment(Environment):
|
|
| 453 |
self._last_code = code
|
| 454 |
self._last_format = fmt
|
| 455 |
self._last_narration = narration
|
| 456 |
-
|
|
|
|
| 457 |
self._last_error_codes = sandbox.error_codes
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
|
| 461 |
status = "REPAIR OK" if sandbox.exec_success else "REPAIR FAILED"
|
| 462 |
-
|
| 463 |
-
f"{status}: {sandbox.message if sandbox.exec_success else
|
| 464 |
-
f"Reward: {', '.join(f'{k}={v}' for k, v in components.items())}"
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
return self._make_obs(
|
| 467 |
task,
|
| 468 |
-
phase=
|
| 469 |
feedback=feedback,
|
| 470 |
reward=repair_reward,
|
| 471 |
-
done=
|
| 472 |
-
last_errors="" if sandbox.exec_success else
|
| 473 |
metadata={
|
| 474 |
"step": self._state.step_count,
|
| 475 |
"phase": "repair",
|
|
@@ -490,6 +533,7 @@ class ExplainerEnvironment(Environment):
|
|
| 490 |
reward: float = 0.0,
|
| 491 |
done: bool = False,
|
| 492 |
search_results: str = "",
|
|
|
|
| 493 |
last_errors: str | None = None,
|
| 494 |
metadata: dict | None = None,
|
| 495 |
) -> ExplainerObservation:
|
|
@@ -503,6 +547,7 @@ class ExplainerEnvironment(Environment):
|
|
| 503 |
phase=phase,
|
| 504 |
feedback=feedback,
|
| 505 |
search_results=search_results,
|
|
|
|
| 506 |
explored_context="\n---\n".join(self._accumulated_context),
|
| 507 |
explore_steps_left=MAX_EXPLORE_STEPS - self._explore_steps,
|
| 508 |
repair_attempts_left=MAX_REPAIR_STEPS - self._repair_steps,
|
|
@@ -516,3 +561,21 @@ class ExplainerEnvironment(Environment):
|
|
| 516 |
@property
|
| 517 |
def state(self) -> State:
|
| 518 |
return self._state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
from task_bank import ALL_TASKS, EASY_TASKS, HARD_TASKS, MEDIUM_TASKS, Task
|
| 39 |
|
| 40 |
|
| 41 |
+
MB002_REPAIR_HINT = (
|
| 42 |
+
"MB002 repair checklist: Marimo treats every non-underscore assignment as a "
|
| 43 |
+
"global notebook variable, including `for` loop variables. Audit the whole "
|
| 44 |
+
"file and rename cell-local names to private names everywhere: `arr` -> "
|
| 45 |
+
"`_arr`, `target` -> `_target`, `i` -> `_i`, `t` -> `_t`, `freqs` -> "
|
| 46 |
+
"`_freqs`, `fig` -> `_fig`, `ax` -> `_ax`. Public names should only be used "
|
| 47 |
+
"for values intentionally passed to later cells, and each public name may be "
|
| 48 |
+
"defined once globally."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _render_errors_with_hints(errors: str, error_codes: list[str]) -> str:
|
| 53 |
+
if "MB002" not in error_codes:
|
| 54 |
+
return errors
|
| 55 |
+
return f"{errors}\n\n{MB002_REPAIR_HINT}"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
class ExplainerEnvironment(Environment):
|
| 59 |
"""
|
| 60 |
Multi-step Research → Interactive Explainer environment.
|
|
|
|
| 74 |
self._current_task: Task | None = None
|
| 75 |
self._difficulty_pool: list[Task] = EASY_TASKS
|
| 76 |
self._accumulated_context: list[str] = []
|
| 77 |
+
self._explore_actions: list[str] = []
|
| 78 |
self._used_tools: set[str] = set()
|
| 79 |
self._explore_steps: int = 0
|
| 80 |
self._repair_steps: int = 0
|
|
|
|
| 202 |
episode_id=episode_id or str(uuid4()), step_count=0
|
| 203 |
)
|
| 204 |
self._accumulated_context = []
|
| 205 |
+
self._explore_actions = []
|
| 206 |
self._used_tools = set()
|
| 207 |
self._explore_steps = 0
|
| 208 |
self._repair_steps = 0
|
|
|
|
| 292 |
)
|
| 293 |
|
| 294 |
previous_context = list(self._accumulated_context)
|
| 295 |
+
previous_actions = list(self._explore_actions)
|
| 296 |
used_tools = set(self._used_tools)
|
| 297 |
|
| 298 |
result = await run_research_tool(tool, query, intent)
|
| 299 |
results_text = result.render()
|
| 300 |
+
self._explore_actions.append(_explore_action_text(tool, query, intent))
|
| 301 |
if result.ok:
|
| 302 |
self._accumulated_context.append(result.text)
|
| 303 |
self._used_tools.add(tool)
|
|
|
|
| 315 |
previous_context=previous_context,
|
| 316 |
accumulated_context=self._accumulated_context,
|
| 317 |
used_tools=used_tools,
|
| 318 |
+
previous_actions=previous_actions,
|
| 319 |
)
|
| 320 |
|
| 321 |
steps_left = MAX_EXPLORE_STEPS - self._explore_steps
|
|
|
|
| 335 |
phase=phase,
|
| 336 |
feedback=f"{hint}\nTool: {tool}\nReward: {components}",
|
| 337 |
search_results=results_text,
|
| 338 |
+
top_chunks=_top_chunks_payload(result.chunks),
|
| 339 |
reward=reward,
|
| 340 |
metadata={
|
| 341 |
"step": self._state.step_count,
|
| 342 |
"phase": "explore",
|
| 343 |
"tool": tool,
|
| 344 |
"source_count": len(result.chunks),
|
| 345 |
+
"top_chunks": _top_chunks_payload(result.chunks),
|
| 346 |
"error": result.error,
|
| 347 |
**components,
|
| 348 |
},
|
|
|
|
| 380 |
task=task,
|
| 381 |
exec_success=sandbox.exec_success,
|
| 382 |
accumulated_context=self._accumulated_context,
|
| 383 |
+
static_check_passed=sandbox.check_passed,
|
| 384 |
+
error_codes=sandbox.error_codes,
|
| 385 |
)
|
| 386 |
reward = max(0.0, reward + skip_penalty)
|
| 387 |
|
| 388 |
self._last_code = code
|
| 389 |
self._last_format = fmt
|
| 390 |
self._last_narration = narration
|
| 391 |
+
rendered_errors = _render_errors_with_hints(sandbox.render_errors(), sandbox.error_codes)
|
| 392 |
+
self._last_errors = rendered_errors
|
| 393 |
self._last_error_codes = sandbox.error_codes
|
| 394 |
|
| 395 |
# Feedback
|
|
|
|
| 399 |
if not sandbox.parses:
|
| 400 |
parts.append("SYNTAX ERROR: code does not parse.")
|
| 401 |
elif not sandbox.exec_success:
|
| 402 |
+
parts.append(f"EXECUTION FAILED: {rendered_errors}")
|
| 403 |
else:
|
| 404 |
parts.append(f"EXECUTION OK: {sandbox.message}")
|
| 405 |
parts.append(
|
|
|
|
| 411 |
self._phase = phase
|
| 412 |
self._done = done
|
| 413 |
if not done:
|
| 414 |
+
parts.append(
|
| 415 |
+
f"Repair phase: {MAX_REPAIR_STEPS} attempts available. "
|
| 416 |
+
"Submit a revised artifact using the error feedback."
|
| 417 |
+
)
|
| 418 |
|
| 419 |
return self._make_obs(
|
| 420 |
task,
|
|
|
|
| 422 |
feedback="\n".join(parts),
|
| 423 |
reward=reward,
|
| 424 |
done=done,
|
| 425 |
+
last_errors="" if sandbox.exec_success else rendered_errors,
|
| 426 |
metadata={
|
| 427 |
"step": self._state.step_count,
|
| 428 |
"phase": "generate",
|
|
|
|
| 469 |
task=task,
|
| 470 |
exec_success=sandbox.exec_success,
|
| 471 |
accumulated_context=self._accumulated_context,
|
| 472 |
+
static_check_passed=sandbox.check_passed,
|
| 473 |
+
error_codes=sandbox.error_codes,
|
| 474 |
)
|
| 475 |
repair_reward, repair_components = adjust_repair_reward(
|
| 476 |
base_reward,
|
|
|
|
| 485 |
self._last_code = code
|
| 486 |
self._last_format = fmt
|
| 487 |
self._last_narration = narration
|
| 488 |
+
rendered_errors = _render_errors_with_hints(sandbox.render_errors(), sandbox.error_codes)
|
| 489 |
+
self._last_errors = rendered_errors
|
| 490 |
self._last_error_codes = sandbox.error_codes
|
| 491 |
+
|
| 492 |
+
attempts_left = MAX_REPAIR_STEPS - self._repair_steps
|
| 493 |
+
done = sandbox.exec_success or attempts_left <= 0
|
| 494 |
+
phase = "done" if done else "repair"
|
| 495 |
+
self._phase = phase
|
| 496 |
+
self._done = done
|
| 497 |
|
| 498 |
status = "REPAIR OK" if sandbox.exec_success else "REPAIR FAILED"
|
| 499 |
+
feedback_parts = [
|
| 500 |
+
f"{status}: {sandbox.message if sandbox.exec_success else rendered_errors}",
|
| 501 |
+
f"Reward: {', '.join(f'{k}={v}' for k, v in components.items())}",
|
| 502 |
+
]
|
| 503 |
+
if not done:
|
| 504 |
+
feedback_parts.append(
|
| 505 |
+
f"Repair phase continues: {attempts_left} repair attempts left. "
|
| 506 |
+
"Submit another corrected artifact using the latest error feedback."
|
| 507 |
+
)
|
| 508 |
+
feedback = "\n".join(feedback_parts)
|
| 509 |
return self._make_obs(
|
| 510 |
task,
|
| 511 |
+
phase=phase,
|
| 512 |
feedback=feedback,
|
| 513 |
reward=repair_reward,
|
| 514 |
+
done=done,
|
| 515 |
+
last_errors="" if sandbox.exec_success else rendered_errors,
|
| 516 |
metadata={
|
| 517 |
"step": self._state.step_count,
|
| 518 |
"phase": "repair",
|
|
|
|
| 533 |
reward: float = 0.0,
|
| 534 |
done: bool = False,
|
| 535 |
search_results: str = "",
|
| 536 |
+
top_chunks: list[dict] | None = None,
|
| 537 |
last_errors: str | None = None,
|
| 538 |
metadata: dict | None = None,
|
| 539 |
) -> ExplainerObservation:
|
|
|
|
| 547 |
phase=phase,
|
| 548 |
feedback=feedback,
|
| 549 |
search_results=search_results,
|
| 550 |
+
top_chunks=top_chunks or [],
|
| 551 |
explored_context="\n---\n".join(self._accumulated_context),
|
| 552 |
explore_steps_left=MAX_EXPLORE_STEPS - self._explore_steps,
|
| 553 |
repair_attempts_left=MAX_REPAIR_STEPS - self._repair_steps,
|
|
|
|
| 561 |
@property
|
| 562 |
def state(self) -> State:
|
| 563 |
return self._state
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def _explore_action_text(tool: str, query: str, intent: str) -> str:
|
| 567 |
+
return f"{tool} {query.strip()} {intent.strip()}".strip()
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def _top_chunks_payload(chunks) -> list[dict]:
|
| 571 |
+
return [
|
| 572 |
+
{
|
| 573 |
+
"rank": chunk.rank,
|
| 574 |
+
"source": chunk.source,
|
| 575 |
+
"title": chunk.title,
|
| 576 |
+
"url": chunk.url,
|
| 577 |
+
"score": round(chunk.score, 4),
|
| 578 |
+
"snippet": chunk.text,
|
| 579 |
+
}
|
| 580 |
+
for chunk in chunks[:5]
|
| 581 |
+
]
|
tests/test_environment.py
CHANGED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
|
|
|
| 8 |
from models import ExplainerAction, ExplainerObservation
|
| 9 |
from server.explainer_env_environment import ExplainerEnvironment
|
| 10 |
|
|
@@ -15,7 +16,7 @@ def test_reset_returns_observation():
|
|
| 15 |
assert isinstance(obs, ExplainerObservation)
|
| 16 |
assert obs.topic != ""
|
| 17 |
assert obs.phase == "explore"
|
| 18 |
-
assert obs.explore_steps_left ==
|
| 19 |
assert obs.done is False
|
| 20 |
|
| 21 |
|
|
@@ -37,9 +38,10 @@ def test_explore_step():
|
|
| 37 |
)
|
| 38 |
obs = env.step(action)
|
| 39 |
assert obs.done is False
|
| 40 |
-
assert obs.explore_steps_left ==
|
| 41 |
assert isinstance(obs.reward, (int, float))
|
| 42 |
assert obs.reward >= 0.0
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
def test_explore_empty_query():
|
|
@@ -54,7 +56,7 @@ def test_explore_empty_query():
|
|
| 54 |
def test_explore_max_steps():
|
| 55 |
env = ExplainerEnvironment()
|
| 56 |
env.reset(seed=1)
|
| 57 |
-
for i in range(
|
| 58 |
obs = env.step(ExplainerAction(
|
| 59 |
action_type="explore",
|
| 60 |
tool="search_wikipedia",
|
|
@@ -119,7 +121,7 @@ def test_generate_reward_in_metadata():
|
|
| 119 |
format="marimo",
|
| 120 |
code="x = 1",
|
| 121 |
))
|
| 122 |
-
for key in ("
|
| 123 |
assert key in obs.metadata, f"missing {key} in metadata"
|
| 124 |
assert "explore_steps_used" in obs.metadata
|
| 125 |
|
|
@@ -160,7 +162,7 @@ def test_bad_code_does_not_crash():
|
|
| 160 |
assert "SYNTAX ERROR" in obs.feedback
|
| 161 |
|
| 162 |
|
| 163 |
-
def
|
| 164 |
env = ExplainerEnvironment()
|
| 165 |
env.reset(seed=1)
|
| 166 |
env.step(ExplainerAction(
|
|
@@ -174,9 +176,21 @@ def test_repair_ends_episode():
|
|
| 174 |
code="x = 2",
|
| 175 |
repair_notes="attempted fix",
|
| 176 |
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
assert obs.done is True
|
| 178 |
assert obs.phase == "done"
|
| 179 |
-
assert obs.
|
| 180 |
|
| 181 |
|
| 182 |
if __name__ == "__main__":
|
|
@@ -193,7 +207,7 @@ if __name__ == "__main__":
|
|
| 193 |
test_state_episode_id_changes,
|
| 194 |
test_step_increments_count,
|
| 195 |
test_bad_code_does_not_crash,
|
| 196 |
-
|
| 197 |
]
|
| 198 |
passed = 0
|
| 199 |
for t in tests:
|
|
|
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
| 8 |
+
from constants import MAX_EXPLORE_STEPS, MAX_REPAIR_STEPS
|
| 9 |
from models import ExplainerAction, ExplainerObservation
|
| 10 |
from server.explainer_env_environment import ExplainerEnvironment
|
| 11 |
|
|
|
|
| 16 |
assert isinstance(obs, ExplainerObservation)
|
| 17 |
assert obs.topic != ""
|
| 18 |
assert obs.phase == "explore"
|
| 19 |
+
assert obs.explore_steps_left == MAX_EXPLORE_STEPS
|
| 20 |
assert obs.done is False
|
| 21 |
|
| 22 |
|
|
|
|
| 38 |
)
|
| 39 |
obs = env.step(action)
|
| 40 |
assert obs.done is False
|
| 41 |
+
assert obs.explore_steps_left == MAX_EXPLORE_STEPS - 1
|
| 42 |
assert isinstance(obs.reward, (int, float))
|
| 43 |
assert obs.reward >= 0.0
|
| 44 |
+
assert isinstance(obs.top_chunks, list)
|
| 45 |
|
| 46 |
|
| 47 |
def test_explore_empty_query():
|
|
|
|
| 56 |
def test_explore_max_steps():
|
| 57 |
env = ExplainerEnvironment()
|
| 58 |
env.reset(seed=1)
|
| 59 |
+
for i in range(MAX_EXPLORE_STEPS):
|
| 60 |
obs = env.step(ExplainerAction(
|
| 61 |
action_type="explore",
|
| 62 |
tool="search_wikipedia",
|
|
|
|
| 121 |
format="marimo",
|
| 122 |
code="x = 1",
|
| 123 |
))
|
| 124 |
+
for key in ("validity", "task_alignment", "structure", "research_usage"):
|
| 125 |
assert key in obs.metadata, f"missing {key} in metadata"
|
| 126 |
assert "explore_steps_used" in obs.metadata
|
| 127 |
|
|
|
|
| 162 |
assert "SYNTAX ERROR" in obs.feedback
|
| 163 |
|
| 164 |
|
| 165 |
+
def test_failed_repair_can_continue_until_limit():
|
| 166 |
env = ExplainerEnvironment()
|
| 167 |
env.reset(seed=1)
|
| 168 |
env.step(ExplainerAction(
|
|
|
|
| 176 |
code="x = 2",
|
| 177 |
repair_notes="attempted fix",
|
| 178 |
))
|
| 179 |
+
assert obs.done is False
|
| 180 |
+
assert obs.phase == "repair"
|
| 181 |
+
assert obs.repair_attempts_left == MAX_REPAIR_STEPS - 1
|
| 182 |
+
assert obs.metadata["phase"] == "repair"
|
| 183 |
+
|
| 184 |
+
for attempt in range(MAX_REPAIR_STEPS - 1):
|
| 185 |
+
obs = env.step(ExplainerAction(
|
| 186 |
+
action_type="repair",
|
| 187 |
+
format="marimo",
|
| 188 |
+
code=f"x = {attempt + 3}",
|
| 189 |
+
repair_notes="still invalid",
|
| 190 |
+
))
|
| 191 |
assert obs.done is True
|
| 192 |
assert obs.phase == "done"
|
| 193 |
+
assert obs.repair_attempts_left == 0
|
| 194 |
|
| 195 |
|
| 196 |
if __name__ == "__main__":
|
|
|
|
| 207 |
test_state_episode_id_changes,
|
| 208 |
test_step_increments_count,
|
| 209 |
test_bad_code_does_not_crash,
|
| 210 |
+
test_failed_repair_can_continue_until_limit,
|
| 211 |
]
|
| 212 |
passed = 0
|
| 213 |
for t in tests:
|
tests/test_models.py
CHANGED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
|
|
|
| 8 |
from models import ExplainerAction, ExplainerObservation
|
| 9 |
|
| 10 |
|
|
@@ -61,8 +62,8 @@ def test_observation_defaults():
|
|
| 61 |
assert obs.topic == ""
|
| 62 |
assert obs.tier == "beginner"
|
| 63 |
assert obs.phase == "explore"
|
| 64 |
-
assert obs.explore_steps_left ==
|
| 65 |
-
assert obs.repair_attempts_left ==
|
| 66 |
assert obs.done is False
|
| 67 |
|
| 68 |
|
|
|
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
| 8 |
+
from constants import MAX_EXPLORE_STEPS, MAX_REPAIR_STEPS
|
| 9 |
from models import ExplainerAction, ExplainerObservation
|
| 10 |
|
| 11 |
|
|
|
|
| 62 |
assert obs.topic == ""
|
| 63 |
assert obs.tier == "beginner"
|
| 64 |
assert obs.phase == "explore"
|
| 65 |
+
assert obs.explore_steps_left == MAX_EXPLORE_STEPS
|
| 66 |
+
assert obs.repair_attempts_left == MAX_REPAIR_STEPS
|
| 67 |
assert obs.done is False
|
| 68 |
|
| 69 |
|
tests/test_retrieval.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the source-result -> chunk -> top-k retrieval pipeline."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
|
| 8 |
+
import research.retrieval as retrieval
|
| 9 |
+
from research.retrieval import chunk_markdown, rank_chunks_for_query
|
| 10 |
+
from research.types import ResearchChunk
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class FakeTextEmbedding:
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self.seen_texts = []
|
| 16 |
+
|
| 17 |
+
def embed(self, texts):
|
| 18 |
+
self.seen_texts.extend(texts)
|
| 19 |
+
for text in texts:
|
| 20 |
+
lower = text.lower()
|
| 21 |
+
if "backpropagation" in lower or "chain rule" in lower or "target" in lower:
|
| 22 |
+
yield [1.0, 0.0]
|
| 23 |
+
else:
|
| 24 |
+
yield [0.0, 1.0]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _chunk(title: str, text: str) -> ResearchChunk:
|
| 28 |
+
return ResearchChunk(
|
| 29 |
+
source="test",
|
| 30 |
+
tool="fetch_docs",
|
| 31 |
+
title=title,
|
| 32 |
+
url="https://example.test",
|
| 33 |
+
text=text,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_chunking_then_top5_ranking():
|
| 38 |
+
docs = chunk_markdown(
|
| 39 |
+
"""
|
| 40 |
+
# Intro
|
| 41 |
+
general overview
|
| 42 |
+
# Chain Rule
|
| 43 |
+
backpropagation chain rule gradients neural network
|
| 44 |
+
# History
|
| 45 |
+
unrelated history
|
| 46 |
+
""",
|
| 47 |
+
"Fallback",
|
| 48 |
+
)
|
| 49 |
+
chunks = [_chunk(title, text) for title, text in docs]
|
| 50 |
+
chunks.extend(_chunk(f"Filler {i}", f"unrelated filler {i}") for i in range(8))
|
| 51 |
+
|
| 52 |
+
ranked = rank_chunks_for_query(
|
| 53 |
+
"backpropagation",
|
| 54 |
+
"chain rule gradients",
|
| 55 |
+
chunks,
|
| 56 |
+
embedding_model=FakeTextEmbedding(),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
assert len(ranked) == 5
|
| 60 |
+
assert [chunk.rank for chunk in ranked] == [1, 2, 3, 4, 5]
|
| 61 |
+
assert ranked[0].title == "Chain Rule"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_embedding_ranking_is_not_bm25():
|
| 65 |
+
chunks = [
|
| 66 |
+
_chunk("Lexical Match", "query repeated query repeated lexical only"),
|
| 67 |
+
_chunk("Embedding Match", "target concept with less lexical overlap"),
|
| 68 |
+
_chunk("Other", "unrelated content"),
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
ranked = rank_chunks_for_query(
|
| 72 |
+
"query",
|
| 73 |
+
"intent target",
|
| 74 |
+
chunks,
|
| 75 |
+
top_k=2,
|
| 76 |
+
embedding_model=FakeTextEmbedding(),
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
assert len(ranked) == 2
|
| 80 |
+
assert ranked[0].title == "Embedding Match"
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def test_preload_embedding_model_warms_runtime():
|
| 84 |
+
previous_model = retrieval._EMBEDDING_MODEL
|
| 85 |
+
fake_model = FakeTextEmbedding()
|
| 86 |
+
retrieval._EMBEDDING_MODEL = fake_model
|
| 87 |
+
try:
|
| 88 |
+
retrieval.preload_embedding_model()
|
| 89 |
+
assert fake_model.seen_texts == ["startup warmup"]
|
| 90 |
+
finally:
|
| 91 |
+
retrieval._EMBEDDING_MODEL = previous_model
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
tests = [
|
| 96 |
+
test_chunking_then_top5_ranking,
|
| 97 |
+
test_embedding_ranking_is_not_bm25,
|
| 98 |
+
test_preload_embedding_model_warms_runtime,
|
| 99 |
+
]
|
| 100 |
+
passed = 0
|
| 101 |
+
for test in tests:
|
| 102 |
+
try:
|
| 103 |
+
test()
|
| 104 |
+
passed += 1
|
| 105 |
+
except Exception as exc:
|
| 106 |
+
print(f"FAIL: {test.__name__}: {exc}")
|
| 107 |
+
print(f"PASS: test_retrieval ({passed}/{len(tests)})")
|
tests/test_rewards.py
CHANGED
|
@@ -5,7 +5,9 @@ from pathlib import Path
|
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
|
|
|
| 8 |
from rewards.exploration import (
|
|
|
|
| 9 |
coverage_delta,
|
| 10 |
compute_explore_reward,
|
| 11 |
query_relevance,
|
|
@@ -15,6 +17,7 @@ from rewards.exploration import (
|
|
| 15 |
tool_choice_score,
|
| 16 |
)
|
| 17 |
from rewards.generation import (
|
|
|
|
| 18 |
compute_generate_reward,
|
| 19 |
context_usage,
|
| 20 |
format_match,
|
|
@@ -22,7 +25,7 @@ from rewards.generation import (
|
|
| 22 |
marimo_structure,
|
| 23 |
narration_score,
|
| 24 |
)
|
| 25 |
-
from rewards.sandbox import ast_parses
|
| 26 |
from research.types import ResearchChunk, ResearchResult
|
| 27 |
from task_bank import ALL_TASKS
|
| 28 |
|
|
@@ -37,6 +40,49 @@ def test_ast_parses():
|
|
| 37 |
assert ast_parses("not python!!!") is False
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# --- Exploration rewards ---
|
| 41 |
|
| 42 |
def test_query_relevance():
|
|
@@ -51,6 +97,22 @@ def test_result_novelty():
|
|
| 51 |
assert result_novelty("", []) == 0.0
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def test_research_breadth():
|
| 55 |
assert research_breadth([], min_sources=2) == 0.0
|
| 56 |
assert research_breadth(["a"], min_sources=2) == 0.5
|
|
@@ -120,11 +182,38 @@ def test_explore_reward_integration():
|
|
| 120 |
used_tools=set(),
|
| 121 |
)
|
| 122 |
assert reward > 0.1
|
| 123 |
-
assert
|
| 124 |
-
assert
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
|
| 130 |
# --- Generation rewards ---
|
|
@@ -158,26 +247,67 @@ def test_narration_manim():
|
|
| 158 |
|
| 159 |
|
| 160 |
def test_structure_marimo():
|
| 161 |
-
good = """import marimo
|
| 162 |
-
app =
|
| 163 |
@app.cell
|
| 164 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
mo.md("# Regression")
|
| 166 |
-
return
|
| 167 |
@app.cell
|
| 168 |
-
def
|
| 169 |
import matplotlib.pyplot as plt
|
| 170 |
-
return
|
| 171 |
@app.cell
|
| 172 |
-
def
|
| 173 |
slider = mo.ui.slider(0, 5)
|
| 174 |
-
return
|
| 175 |
"""
|
| 176 |
assert marimo_structure(good, MARIMO_TASK) > 0.5
|
| 177 |
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
def test_context_usage():
|
| 180 |
-
assert context_usage("x = 1", []) == 0.
|
| 181 |
assert context_usage(
|
| 182 |
"linear regression least squares gradient descent optimization",
|
| 183 |
["linear regression least squares optimization methods"],
|
|
@@ -194,28 +324,32 @@ def test_generate_reward_garbage():
|
|
| 194 |
accumulated_context=[],
|
| 195 |
)
|
| 196 |
assert reward < 0.4
|
| 197 |
-
assert comp["
|
| 198 |
|
| 199 |
|
| 200 |
def test_generate_reward_good():
|
| 201 |
-
code = """import marimo
|
| 202 |
-
app =
|
| 203 |
-
@app.cell
|
| 204 |
-
def _():
|
| 205 |
-
mo.md("# Linear Regression")
|
| 206 |
-
return
|
| 207 |
@app.cell
|
| 208 |
-
def
|
|
|
|
| 209 |
import numpy as np
|
| 210 |
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
# linear regression least squares MSE gradient descent weights bias
|
| 212 |
X = np.linspace(0, 10, 50)
|
| 213 |
y = 2 * X + 1
|
| 214 |
return X, y
|
| 215 |
@app.cell
|
| 216 |
-
def
|
| 217 |
slider = mo.ui.slider(0, 5, value=2, label="Slope")
|
| 218 |
-
return
|
| 219 |
"""
|
| 220 |
reward, comp = compute_generate_reward(
|
| 221 |
code=code,
|
|
@@ -224,10 +358,41 @@ def _(X, y):
|
|
| 224 |
task=MARIMO_TASK,
|
| 225 |
exec_success=True,
|
| 226 |
accumulated_context=["linear regression least squares"],
|
|
|
|
| 227 |
)
|
| 228 |
assert reward > 0.6
|
| 229 |
-
assert comp["
|
| 230 |
-
assert comp["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
|
| 233 |
def test_generate_reward_wrong_format():
|
|
@@ -247,26 +412,92 @@ def test_reward_spread():
|
|
| 247 |
assert len(unique) >= 3
|
| 248 |
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
if __name__ == "__main__":
|
| 251 |
tests = [
|
| 252 |
test_ast_parses,
|
|
|
|
|
|
|
|
|
|
| 253 |
test_query_relevance,
|
| 254 |
test_result_novelty,
|
|
|
|
| 255 |
test_research_breadth,
|
| 256 |
test_tool_choice_score,
|
| 257 |
test_source_quality,
|
| 258 |
test_coverage_delta,
|
| 259 |
test_explore_reward_integration,
|
|
|
|
| 260 |
test_keyword_coverage,
|
| 261 |
test_format_match,
|
| 262 |
test_narration_marimo,
|
| 263 |
test_narration_manim,
|
| 264 |
test_structure_marimo,
|
|
|
|
| 265 |
test_context_usage,
|
| 266 |
test_generate_reward_garbage,
|
| 267 |
test_generate_reward_good,
|
|
|
|
| 268 |
test_generate_reward_wrong_format,
|
| 269 |
test_reward_spread,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
]
|
| 271 |
passed = 0
|
| 272 |
for t in tests:
|
|
|
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
| 8 |
+
from constants import MAX_EXPLORE_REWARD, MAX_REPAIR_REWARD, normalized_episode_score
|
| 9 |
from rewards.exploration import (
|
| 10 |
+
action_novelty,
|
| 11 |
coverage_delta,
|
| 12 |
compute_explore_reward,
|
| 13 |
query_relevance,
|
|
|
|
| 17 |
tool_choice_score,
|
| 18 |
)
|
| 19 |
from rewards.generation import (
|
| 20 |
+
adjust_repair_reward,
|
| 21 |
compute_generate_reward,
|
| 22 |
context_usage,
|
| 23 |
format_match,
|
|
|
|
| 25 |
marimo_structure,
|
| 26 |
narration_score,
|
| 27 |
)
|
| 28 |
+
from rewards.sandbox import ast_parses, validate_code
|
| 29 |
from research.types import ResearchChunk, ResearchResult
|
| 30 |
from task_bank import ALL_TASKS
|
| 31 |
|
|
|
|
| 40 |
assert ast_parses("not python!!!") is False
|
| 41 |
|
| 42 |
|
| 43 |
+
def test_syntax_errors_are_verbose():
|
| 44 |
+
result = validate_code("marimo", "x = (1 +\n")
|
| 45 |
+
rendered = result.render_errors()
|
| 46 |
+
assert "PY_SYNTAX" in rendered
|
| 47 |
+
assert "line" in rendered
|
| 48 |
+
assert "^" in rendered
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_marimo_duplicate_definitions_fail_static_check():
|
| 52 |
+
code = """import marimo
|
| 53 |
+
app = marimo.App()
|
| 54 |
+
@app.cell
|
| 55 |
+
def __():
|
| 56 |
+
x = 1
|
| 57 |
+
return x,
|
| 58 |
+
@app.cell
|
| 59 |
+
def __():
|
| 60 |
+
x = 2
|
| 61 |
+
return x,
|
| 62 |
+
"""
|
| 63 |
+
result = validate_code("marimo", code)
|
| 64 |
+
assert result.parses is True
|
| 65 |
+
assert result.check_passed is False
|
| 66 |
+
assert "MB002" in result.error_codes
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_marimo_runtime_rejects_numpy_math_namespace():
|
| 70 |
+
code = """import marimo
|
| 71 |
+
app = marimo.App()
|
| 72 |
+
@app.cell
|
| 73 |
+
def __():
|
| 74 |
+
import numpy as np
|
| 75 |
+
value = np.math.factorial(3)
|
| 76 |
+
return value,
|
| 77 |
+
"""
|
| 78 |
+
result = validate_code("marimo", code)
|
| 79 |
+
assert result.parses is True
|
| 80 |
+
assert result.check_passed is True
|
| 81 |
+
assert result.exec_success is False
|
| 82 |
+
assert "MARIMO_EXPORT" in result.error_codes
|
| 83 |
+
assert "np.math" in result.message or "module 'numpy'" in result.message
|
| 84 |
+
|
| 85 |
+
|
| 86 |
# --- Exploration rewards ---
|
| 87 |
|
| 88 |
def test_query_relevance():
|
|
|
|
| 97 |
assert result_novelty("", []) == 0.0
|
| 98 |
|
| 99 |
|
| 100 |
+
def test_action_novelty_penalizes_repeated_intent():
|
| 101 |
+
previous = ["search_wikipedia backpropagation algorithm neural network fundamentals"]
|
| 102 |
+
assert action_novelty(
|
| 103 |
+
"search_wikipedia",
|
| 104 |
+
"backpropagation algorithm neural network",
|
| 105 |
+
"fundamentals",
|
| 106 |
+
previous,
|
| 107 |
+
) < 0.3
|
| 108 |
+
assert action_novelty(
|
| 109 |
+
"fetch_docs",
|
| 110 |
+
"marimo slider plotting examples",
|
| 111 |
+
"interactive code patterns",
|
| 112 |
+
previous,
|
| 113 |
+
) > 0.7
|
| 114 |
+
|
| 115 |
+
|
| 116 |
def test_research_breadth():
|
| 117 |
assert research_breadth([], min_sources=2) == 0.0
|
| 118 |
assert research_breadth(["a"], min_sources=2) == 0.5
|
|
|
|
| 182 |
used_tools=set(),
|
| 183 |
)
|
| 184 |
assert reward > 0.1
|
| 185 |
+
assert reward <= MAX_EXPLORE_REWARD
|
| 186 |
+
assert set(comp) == {
|
| 187 |
+
"query_quality",
|
| 188 |
+
"evidence_quality",
|
| 189 |
+
"information_gain",
|
| 190 |
+
"efficiency",
|
| 191 |
+
"explore_total",
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_explore_reward_empty_result_is_gated():
|
| 196 |
+
result = ResearchResult(
|
| 197 |
+
tool="search_wikipedia",
|
| 198 |
+
query="linear regression least squares MSE",
|
| 199 |
+
chunks=[],
|
| 200 |
+
)
|
| 201 |
+
reward, comp = compute_explore_reward(
|
| 202 |
+
query="linear regression least squares MSE",
|
| 203 |
+
tool="search_wikipedia",
|
| 204 |
+
intent="beginner explanation",
|
| 205 |
+
result=result,
|
| 206 |
+
topic="Linear Regression",
|
| 207 |
+
keywords_csv="linear regression,least squares,MSE",
|
| 208 |
+
task_content="",
|
| 209 |
+
difficulty="easy",
|
| 210 |
+
previous_context=[],
|
| 211 |
+
accumulated_context=[],
|
| 212 |
+
used_tools=set(),
|
| 213 |
+
)
|
| 214 |
+
assert reward < 0.05
|
| 215 |
+
assert comp["evidence_quality"] == 0.0
|
| 216 |
+
assert comp["information_gain"] == 0.0
|
| 217 |
|
| 218 |
|
| 219 |
# --- Generation rewards ---
|
|
|
|
| 247 |
|
| 248 |
|
| 249 |
def test_structure_marimo():
|
| 250 |
+
good = """import marimo
|
| 251 |
+
app = marimo.App()
|
| 252 |
@app.cell
|
| 253 |
+
def __():
|
| 254 |
+
import marimo as mo
|
| 255 |
+
return mo,
|
| 256 |
+
@app.cell
|
| 257 |
+
def __(mo):
|
| 258 |
mo.md("# Regression")
|
| 259 |
+
return ()
|
| 260 |
@app.cell
|
| 261 |
+
def __():
|
| 262 |
import matplotlib.pyplot as plt
|
| 263 |
+
return plt,
|
| 264 |
@app.cell
|
| 265 |
+
def __(mo):
|
| 266 |
slider = mo.ui.slider(0, 5)
|
| 267 |
+
return slider,
|
| 268 |
"""
|
| 269 |
assert marimo_structure(good, MARIMO_TASK) > 0.5
|
| 270 |
|
| 271 |
|
| 272 |
+
def test_marimo_structure_prefers_reactive_plot_wrappers():
|
| 273 |
+
raw = """import marimo
|
| 274 |
+
app = marimo.App()
|
| 275 |
+
@app.cell
|
| 276 |
+
def __():
|
| 277 |
+
import numpy as np
|
| 278 |
+
import matplotlib.pyplot as plt
|
| 279 |
+
return np, plt
|
| 280 |
+
@app.cell
|
| 281 |
+
def __(np, plt):
|
| 282 |
+
_x = np.linspace(0, 1, 10)
|
| 283 |
+
_fig, _ax = plt.subplots()
|
| 284 |
+
_ax.plot(_x, _x)
|
| 285 |
+
_fig
|
| 286 |
+
return ()
|
| 287 |
+
"""
|
| 288 |
+
reactive = """import marimo
|
| 289 |
+
app = marimo.App()
|
| 290 |
+
@app.cell
|
| 291 |
+
def __():
|
| 292 |
+
import marimo as mo
|
| 293 |
+
import numpy as np
|
| 294 |
+
import matplotlib.pyplot as plt
|
| 295 |
+
return mo, np, plt
|
| 296 |
+
@app.cell
|
| 297 |
+
def __(mo, np, plt):
|
| 298 |
+
_x = np.linspace(0, 1, 10)
|
| 299 |
+
_fig, _ax = plt.subplots()
|
| 300 |
+
_ax.plot(_x, _x)
|
| 301 |
+
mo.ui.matplotlib(plt.gca())
|
| 302 |
+
return ()
|
| 303 |
+
"""
|
| 304 |
+
raw_score = marimo_structure(raw, MARIMO_TASK, static_check_passed=True)
|
| 305 |
+
reactive_score = marimo_structure(reactive, MARIMO_TASK, static_check_passed=True)
|
| 306 |
+
assert reactive_score > raw_score
|
| 307 |
+
|
| 308 |
+
|
| 309 |
def test_context_usage():
|
| 310 |
+
assert context_usage("x = 1", []) == 0.0
|
| 311 |
assert context_usage(
|
| 312 |
"linear regression least squares gradient descent optimization",
|
| 313 |
["linear regression least squares optimization methods"],
|
|
|
|
| 324 |
accumulated_context=[],
|
| 325 |
)
|
| 326 |
assert reward < 0.4
|
| 327 |
+
assert comp["validity"] == 0.0
|
| 328 |
|
| 329 |
|
| 330 |
def test_generate_reward_good():
|
| 331 |
+
code = """import marimo
|
| 332 |
+
app = marimo.App()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
@app.cell
|
| 334 |
+
def __():
|
| 335 |
+
import marimo as mo
|
| 336 |
import numpy as np
|
| 337 |
import matplotlib.pyplot as plt
|
| 338 |
+
return mo, np, plt
|
| 339 |
+
@app.cell
|
| 340 |
+
def __(mo):
|
| 341 |
+
mo.md("# Linear Regression")
|
| 342 |
+
return ()
|
| 343 |
+
@app.cell
|
| 344 |
+
def __(np):
|
| 345 |
# linear regression least squares MSE gradient descent weights bias
|
| 346 |
X = np.linspace(0, 10, 50)
|
| 347 |
y = 2 * X + 1
|
| 348 |
return X, y
|
| 349 |
@app.cell
|
| 350 |
+
def __(mo):
|
| 351 |
slider = mo.ui.slider(0, 5, value=2, label="Slope")
|
| 352 |
+
return slider,
|
| 353 |
"""
|
| 354 |
reward, comp = compute_generate_reward(
|
| 355 |
code=code,
|
|
|
|
| 358 |
task=MARIMO_TASK,
|
| 359 |
exec_success=True,
|
| 360 |
accumulated_context=["linear regression least squares"],
|
| 361 |
+
static_check_passed=True,
|
| 362 |
)
|
| 363 |
assert reward > 0.6
|
| 364 |
+
assert comp["validity"] == 1.0
|
| 365 |
+
assert comp["task_alignment"] == 1.0
|
| 366 |
+
assert comp["structure"] > 0.8
|
| 367 |
+
assert comp["research_usage"] > 0.5
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def test_marimo_static_failure_is_not_code_valid():
|
| 371 |
+
code = """import marimo
|
| 372 |
+
app = marimo.App()
|
| 373 |
+
@app.cell
|
| 374 |
+
def __():
|
| 375 |
+
import matplotlib.pyplot as plt
|
| 376 |
+
fig, ax = plt.subplots()
|
| 377 |
+
return fig, ax
|
| 378 |
+
@app.cell
|
| 379 |
+
def __():
|
| 380 |
+
import matplotlib.pyplot as plt
|
| 381 |
+
fig, ax = plt.subplots()
|
| 382 |
+
return fig, ax
|
| 383 |
+
"""
|
| 384 |
+
reward, comp = compute_generate_reward(
|
| 385 |
+
code=code,
|
| 386 |
+
fmt="marimo",
|
| 387 |
+
narration="",
|
| 388 |
+
task=MARIMO_TASK,
|
| 389 |
+
exec_success=False,
|
| 390 |
+
accumulated_context=["linear regression least squares"],
|
| 391 |
+
static_check_passed=False,
|
| 392 |
+
error_codes=["MB002"],
|
| 393 |
+
)
|
| 394 |
+
assert 0.0 < comp["validity"] < 1.0
|
| 395 |
+
assert reward < 0.15
|
| 396 |
|
| 397 |
|
| 398 |
def test_generate_reward_wrong_format():
|
|
|
|
| 412 |
assert len(unique) >= 3
|
| 413 |
|
| 414 |
|
| 415 |
+
def test_repair_reward_success_is_capped_and_changed():
|
| 416 |
+
reward, comp = adjust_repair_reward(
|
| 417 |
+
1.0,
|
| 418 |
+
repair_success=True,
|
| 419 |
+
previous_error_codes=["PY_SYNTAX"],
|
| 420 |
+
new_error_codes=[],
|
| 421 |
+
previous_code="x =",
|
| 422 |
+
repaired_code="x = 1",
|
| 423 |
+
)
|
| 424 |
+
assert reward == MAX_REPAIR_REWARD
|
| 425 |
+
assert comp["repair_success"] == 1.0
|
| 426 |
+
assert comp["fixed_prior_errors"] == 1.0
|
| 427 |
+
assert comp["changed_code"] == 1.0
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def test_repair_reward_penalizes_repeated_code():
|
| 431 |
+
changed_reward, _ = adjust_repair_reward(
|
| 432 |
+
1.0,
|
| 433 |
+
repair_success=True,
|
| 434 |
+
previous_error_codes=["PY_SYNTAX"],
|
| 435 |
+
new_error_codes=[],
|
| 436 |
+
previous_code="x =",
|
| 437 |
+
repaired_code="x = 1",
|
| 438 |
+
)
|
| 439 |
+
repeated_reward, comp = adjust_repair_reward(
|
| 440 |
+
1.0,
|
| 441 |
+
repair_success=True,
|
| 442 |
+
previous_error_codes=["PY_SYNTAX"],
|
| 443 |
+
new_error_codes=[],
|
| 444 |
+
previous_code="x =",
|
| 445 |
+
repaired_code="x =",
|
| 446 |
+
)
|
| 447 |
+
assert repeated_reward < changed_reward
|
| 448 |
+
assert comp["changed_code"] == 0.0
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def test_repair_reward_failed_fix_stays_discounted():
|
| 452 |
+
reward, comp = adjust_repair_reward(
|
| 453 |
+
0.8,
|
| 454 |
+
repair_success=False,
|
| 455 |
+
previous_error_codes=["MB002"],
|
| 456 |
+
new_error_codes=["MB002"],
|
| 457 |
+
previous_code="x = 1",
|
| 458 |
+
repaired_code="x = 2",
|
| 459 |
+
)
|
| 460 |
+
assert 0.0 < reward < MAX_REPAIR_REWARD
|
| 461 |
+
assert comp["repair_success"] == 0.0
|
| 462 |
+
assert comp["fixed_prior_errors"] == 0.0
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def test_normalized_episode_score_bounds():
|
| 466 |
+
assert normalized_episode_score(-1.0) == 0.0
|
| 467 |
+
assert normalized_episode_score(999.0) == 1.0
|
| 468 |
+
|
| 469 |
+
|
| 470 |
if __name__ == "__main__":
|
| 471 |
tests = [
|
| 472 |
test_ast_parses,
|
| 473 |
+
test_syntax_errors_are_verbose,
|
| 474 |
+
test_marimo_duplicate_definitions_fail_static_check,
|
| 475 |
+
test_marimo_runtime_rejects_numpy_math_namespace,
|
| 476 |
test_query_relevance,
|
| 477 |
test_result_novelty,
|
| 478 |
+
test_action_novelty_penalizes_repeated_intent,
|
| 479 |
test_research_breadth,
|
| 480 |
test_tool_choice_score,
|
| 481 |
test_source_quality,
|
| 482 |
test_coverage_delta,
|
| 483 |
test_explore_reward_integration,
|
| 484 |
+
test_explore_reward_empty_result_is_gated,
|
| 485 |
test_keyword_coverage,
|
| 486 |
test_format_match,
|
| 487 |
test_narration_marimo,
|
| 488 |
test_narration_manim,
|
| 489 |
test_structure_marimo,
|
| 490 |
+
test_marimo_structure_prefers_reactive_plot_wrappers,
|
| 491 |
test_context_usage,
|
| 492 |
test_generate_reward_garbage,
|
| 493 |
test_generate_reward_good,
|
| 494 |
+
test_marimo_static_failure_is_not_code_valid,
|
| 495 |
test_generate_reward_wrong_format,
|
| 496 |
test_reward_spread,
|
| 497 |
+
test_repair_reward_success_is_capped_and_changed,
|
| 498 |
+
test_repair_reward_penalizes_repeated_code,
|
| 499 |
+
test_repair_reward_failed_fix_stays_discounted,
|
| 500 |
+
test_normalized_episode_score_bounds,
|
| 501 |
]
|
| 502 |
passed = 0
|
| 503 |
for t in tests:
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|