kgdrathan commited on
Commit
8fa7af1
·
verified ·
1 Parent(s): 43f41de

Upload folder using huggingface_hub

Browse files
__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\">&quot;/var/folders/44/xrn2r2n539lcfpfq4mb99p48hlq2mb/T/marimo_98690/__marimo__cell_vblA_.py&quot;</span>, line <span class=\"m\">9</span>, in <span class=\"n\">&lt;module&gt;</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\">&amp;</span> <span class=\"mi\">1</span> \\\\ <span class=\"mi\">6</span> <span class=\"o\">&amp;</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 &#39;amatrix&#39; 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='&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAYAAACadoJwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAA9IJJREFUeJzsnQd8HMX5v797p2I1Sy6SC+4VY4yNAWNjuumEktB7BwMxJSQk+acQfikkhN7BYDqh99BMCwZTTDOmuvcmN/V6t//Pu6eVV6cru3fb7vR9+AjJp9PN7ux7czM7z7yjqKqqghBCCCGEEEJcIOBGIYQQQgghhBDCAQghhBBCCCHEVTgDQgghhBBCCHENDkAIIYQQQgghrsEBCCGEEEIIIcQ1OAAhhBBCCCGEuAYHIIQQQgghhBDX4ACEEEIIIYQQ4hocgBBCCCGEEEJcgwMQQgghhBBCiGtwAEIIIYQQQghxDQ5ACCGEEEIIIa7BAQghhBBCCCHENTgAIYQQQgghhLgGByCEEEIIIYQQ1+AAhBBCCCGEEOIaHIAQQgghhBBCXIMDEEIIIYQQQohrcABCCCGEEEIIcQ0OQAghhBBCCCGuwQEIIYQQQgghxDU4ACGEEEIIIYS4BgcghBBCCCGEENfgAIQQQgghhBDiGhyAEEIIIYQQQlyDAxBCCCGEEEKIa3AAQgghhBBCCHENDkAIIYQQQgghrsEBCCGEEEIIIcQ1OAAhhBBCCCGEuAYHIIQQQgghhBDX4ACEEEIIIYQQ4hocgBBCCCGEEEJcgwMQQgghhBBCiGtwAEIIIYQQQghxDQ5ACCGEEEIIIa7BAQghhBBCCCHENTgAIYQQQgghhLgGByCEEEIIIYQQ1+AAhBBCCCGEEOIaHIAQQgghhBBCXIMDEEIIIYQQQohrcABCCCGEEEIIcQ0OQAghhBBCCCGuwQEIIYQQQgghxDU4ACGEEEIIIYS4BgcghBBCCCGEENfgAIQQQgghhBDiGhyAEEIIIYQQQlyDAxDSJaitrcX555+Pvn37QlEUXHHFFdrjGzZswPHHH49evXppj99yyy3I9HPKJOS4//KXv6CrsmjRIhxyyCEoLS3V6uLFF19EtrD//vtrX8QcZ599NoYMGZL0ecuXL9di5aGHHsraay1tgpwjISR74QCEZCzyASwfUvG+Pvnkk/bn/uMf/9Cef/HFF+PRRx/FGWecoT1+5ZVX4s0338Tvf/977fHDDjvM9uOUsp3oWMY7p1hIx0bq5KCDDor5+5kzZ7bX2+eff275WObOnat1GrZt2wYv0M8v2ZefOm3CWWedhQULFuDvf/+7dg133313ZBLff/+9dt2lU9xVWbZsGX75y19i1KhRKCws1L522mknXHrppfjmm2+8Pjxfkeh96kTbm8l43aYS4jSKqqqq46UQ4gDSmTznnHPwf//3fxg6dGin38sHWu/evbWfJ0+ejJycHHz44YcdniOzB9Ipf+yxxxy7RsXFxdosi92d33jnFO+DX2Z7mpubsWbNGu28jcjdy08//RSNjY2YN2+e5Y7wDTfcgN/85jdaZ8zMXVwdKU/OQb7SQQZ4MiOk89prr+E///kPbr755vYYEPbaay8MGzYMfqChoUHrrP7hD3/A3/72N2Qizz77LE444QS89957ne6AS6wJeXl5yFZeffVVnHTSSVr8nnbaaRg/fjwCgQB+/PFHPP/881ixYoX2nhg8eHDS12ppaUE4HEZ+fn7C58lgT9q7Bx98UJs18QP6tX///fcTPk/ahh49euCqq67q9Lv+/fvjwAMP1H5ubW3Vvrp164auSqptKiGZQnqf+oT4gMMPPzxph3njxo3aXclYj5eVlSETiXdO8Zg6dao2uHjqqadw+eWXtz++evVqzJkzBz//+c/x3HPPwWmkkyWdU+lc2NXBOPbYYzv8e/369doARB5P9OFdV1eHoqIieEFlZaX23c748/J8osnmgYewZMkSnHzyydrg4p133kG/fv06/P5f//oX7rrrLm1AYuaa5ebmoiuwww474PTTT0/4HDtuSpDOyP1muelTUFDA6iGeQwWLZDVyR06m9+Uu0n//+98OKo58lwb5zjvvbH9cR6a9ZU3FwIEDtTuSI0aM0DoU0nk2Iv++9dZbMW7cOK0zXV5ers286BqTvKZ0MB5++OH2MpLdtZSBxXnnnYc+ffporyl3VeXvk51TMg1GXusXv/gFnnjiiQ6PS0dd7koeeuihnf5GFBI5Xpk1kL+XmZNzzz0Xmzdvbn+OaAJyp06QO7PRxyM/i6Ly+OOPY+zYsVp9vvHGG53WgMiMwI477qh9yc86W7Zs0Tp3MnsRCoWQKnIeMhslHccjjjgCJSUl2l1rQQZgcid/0KBB2vHJdRc9z3gcxteQWSQZ3MjPcs1//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&quot;' 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='&quot;linear&quot;' data-y-scale='&quot;linear&quot;'></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 = 3
4
- MAX_REPAIR_STEPS = 1
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=3, description="Remaining explore steps before forced generate"
88
  )
89
  repair_attempts_left: int = Field(
90
- default=1, description="Remaining repair attempts after failed generation"
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: numpy
14
- Requires-Dist: matplotlib
15
- Requires-Dist: pandas
16
- Requires-Dist: scipy
17
- Requires-Dist: sympy
18
- Requires-Dist: scikit-learn
19
- Requires-Dist: networkx
20
- Requires-Dist: plotly
21
- Requires-Dist: trafilatura
 
 
 
 
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
- numpy
9
- matplotlib
10
- pandas
11
- scipy
12
- sympy
13
- scikit-learn
14
- networkx
15
- plotly
16
- trafilatura
 
 
 
 
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
- "numpy",
19
- "matplotlib",
20
- "pandas",
21
- "scipy",
22
- "sympy",
23
- "scikit-learn",
24
- "networkx",
25
- "plotly",
26
- "trafilatura",
 
 
 
 
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, BM25, chunking, optional embeddings."""
2
 
3
  from __future__ import annotations
4
 
5
  import math
6
- import os
7
  import re
8
- from collections import Counter
9
 
10
  from .types import ResearchChunk
11
 
12
  SECTION_MAX_CHARS = 900
13
  MAX_RETURNED_CHUNKS = 5
14
- BM25_CANDIDATES_FOR_EMBEDDINGS = 12
15
-
16
- _BM25_K1 = 1.5
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 bm25_rank(query: str, chunks: list[ResearchChunk], top_k: int) -> list[ResearchChunk]:
99
- """Rank chunks against query using BM25."""
 
 
 
 
 
 
 
 
 
 
100
  if not chunks:
101
  return []
102
 
103
- query_terms = tokenize(query)
104
- if not query_terms:
105
- ranked = chunks[:top_k]
106
- for idx, chunk in enumerate(ranked, start=1):
107
- chunk.rank = idx
108
- return ranked
109
 
110
- doc_tokens = [tokenize(f"{chunk.title} {chunk.text}") for chunk in chunks]
111
- doc_lengths = [len(tokens) for tokens in doc_tokens]
112
- avgdl = sum(doc_lengths) / max(len(doc_lengths), 1)
113
- n_docs = len(chunks)
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 idx, chunk in enumerate(chunks):
122
- tf_counts = Counter(doc_tokens[idx])
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
- ranked = _maybe_embedding_rerank(query, scored[:BM25_CANDIDATES_FOR_EMBEDDINGS])
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 _maybe_embedding_rerank(query: str, chunks: list[ResearchChunk]) -> list[ResearchChunk]:
145
- """Optionally rerank a small BM25 shortlist with a tiny local embedding model."""
146
- if os.getenv("EMBEDDINGS_ENABLED", "").lower() not in {"1", "true", "yes"}:
147
- return chunks
 
148
 
149
- try:
 
 
 
150
  from fastembed import TextEmbedding
151
- except Exception:
152
- return chunks
153
 
154
- try:
155
- model_name = os.getenv("EMBEDDING_MODEL", "BAAI/bge-small-en-v1.5")
156
- model = TextEmbedding(model_name=model_name)
157
- vectors = list(model.embed([query] + [chunk.text for chunk in chunks]))
158
- except Exception:
159
- return chunks
160
 
161
- if len(vectors) != len(chunks) + 1:
162
- return chunks
163
 
164
- query_vec = vectors[0]
165
- rescored: list[ResearchChunk] = []
166
- for chunk, vec in zip(chunks, vectors[1:]):
167
- chunk.score = _cosine(query_vec, vec)
168
- rescored.append(chunk)
169
- rescored.sort(key=lambda chunk: chunk.score, reverse=True)
170
- return rescored
 
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 bm25_rank, chunk_markdown, trim_text
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 = bm25_rank(f"{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,7 +117,7 @@ async def search_hf_papers(query: str, intent: str = "") -> ResearchResult:
117
  )
118
  )
119
 
120
- ranked = bm25_rank(f"{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,7 +173,7 @@ async def search_arxiv(query: str, intent: str = "") -> ResearchResult:
173
  )
174
  )
175
 
176
- ranked = bm25_rank(f"{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,7 +215,7 @@ async def search_scholar(query: str, intent: str = "") -> ResearchResult:
215
  )
216
  )
217
 
218
- ranked = bm25_rank(f"{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,7 +249,7 @@ async def fetch_docs(query: str, intent: str = "") -> ResearchResult:
249
  )
250
  )
251
 
252
- ranked = bm25_rank(f"{query} {intent}", chunks, MAX_RETURNED_CHUNKS)
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 = bm25_rank(f"{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,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 explainable: each component maps to a visible skill.
13
- W_TOOL_CHOICE = 0.15
14
- W_QUERY = 0.20
15
- W_SOURCE_QUALITY = 0.20
16
- W_COVERAGE_DELTA = 0.20
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
- src_quality = source_quality(result)
 
 
 
 
 
206
  delta = coverage_delta(keywords_csv, task_content, previous_context, result_text)
207
- novelty = result_novelty(result_text, previous_context)
208
- diversity = diversity_score(tool, used_tools, result)
209
- sufficiency = content_sufficiency(task_content, keywords_csv, accumulated_context)
 
 
 
 
 
210
 
211
- info_need = max(0.0, 1.0 - sufficiency)
212
  raw = (
213
- W_TOOL_CHOICE * t_choice
214
- + W_QUERY * q_rel
215
- + W_SOURCE_QUALITY * src_quality
216
- + W_COVERAGE_DELTA * delta
217
- + W_NOVELTY * novelty
218
- + W_DIVERSITY * diversity
219
  )
220
- gate = _exploration_gate(sufficiency)
221
- total = raw * gate + 0.10 * info_need - STEP_COST
222
- total = max(0.0, total)
223
 
224
  components = {
225
- "tool_choice": round(t_choice, 3),
226
- "query_relevance": round(q_rel, 3),
227
- "source_quality": round(src_quality, 3),
228
- "coverage_delta": round(delta, 3),
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
- code quality, execution success, keyword coverage, format match, structural
5
- quality, narration (manim only), and context usage.
6
 
7
  Scoring model:
8
- quality = weighted sum of (coverage, format, structure, narration, context)
9
  total = quality × gate
10
 
11
  Gates (multiplicative):
12
  - code doesn't parse → total = 0
13
- - code doesn't run → total = quality × 0.4
 
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
- "coverage": 0.20,
35
- "format": 0.10,
36
- "structure": 0.20,
37
- "narration": 0.15,
38
- "context": 0.35,
39
  }
40
 
41
- GATE_RUNS_FAIL = 0.4 # quality multiplier when code doesn't execute
42
-
43
 
44
- def _get_weights(fmt: str) -> dict[str, float]:
45
- """Return component weights for the given format.
46
 
47
- Marimo has no narration — its weight is redistributed to structure.
48
- """
49
- w = dict(_WEIGHTS)
50
- if fmt == "marimo":
51
- w["structure"] += w.pop("narration")
52
- return w
 
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(code: str, task: Task) -> float:
 
 
 
 
 
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 = ["mo.ui.", "mo.md(", "mo.Html", "mo.accordion", "mo.callout"]
101
- score += min(0.2, sum(0.05 for p in ui_patterns if p in code))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- viz_patterns = ["plt.", "px.", "altair", "matplotlib", "plotly", "mo.ui.slider"]
104
- if any(p in code for p in viz_patterns):
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
- passed, _, violations = check_marimo(code)
 
 
 
 
 
113
  if passed:
114
  score += 0.1
115
  else:
116
- penalty = {"MB002": 0.35, "MB003": 0.4, "MB005": 0.25, "MB001": 0.3, "MB004": 0.2}
 
 
 
 
 
 
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.5
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.5
186
 
187
  code_words = set(_tokens(code))
188
  overlap = code_words & context_words
189
- return min(1.0, len(overlap) / max(len(context_words), 1) * 5)
 
 
 
 
 
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
- ``code_valid`` and ``code_runs`` act as gates: broken code gets the
208
- quality score heavily discounted rather than losing a single 0.15 component.
 
209
  """
210
- c_valid = 1.0 if ast_parses(code) else 0.0
 
 
 
 
 
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
- c_struct = (marimo_structure if fmt == "marimo" else manim_structure)(code, task)
215
- c_narr = narration_score(narration, fmt)
 
 
 
216
  c_ctx = context_usage(code, accumulated_context)
 
 
217
 
218
- w = _get_weights(fmt)
219
  quality = (
220
- w["coverage"] * c_coverage
221
- + w["format"] * c_format
222
- + w["structure"] * c_struct
223
- + w.get("narration", 0.0) * c_narr
224
- + w["context"] * c_ctx
225
  )
226
 
227
  # Apply gates
228
- if c_valid == 0.0:
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
- "code_valid": round(c_valid, 3),
237
- "code_runs": round(c_runs, 3),
238
- "coverage": round(c_coverage, 3),
239
- "format_match": round(c_format, 3),
240
  "structure": round(c_struct, 3),
241
- "narration": round(c_narr, 3),
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
- repeated = _fingerprint(previous_code) == _fingerprint(repaired_code)
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.8 + (0.1 if fixed_prior else 0.0)
 
 
265
  else:
266
- reward = base_reward * 0.3
 
267
 
268
- if repeated:
269
  reward -= 0.15
270
 
271
- reward = max(0.0, min(1.0, reward))
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
- "repeated_code": 1.0 if repeated else 0.0,
276
  "repair_total": round(reward, 4),
277
  }
278
 
279
 
280
  def _tokens(text: str) -> list[str]:
281
- return [w for w in re.findall(r"\w+", text.lower()) if len(w) > 3]
 
 
 
 
282
 
283
 
284
  def _fingerprint(code: str) -> str:
285
- normalized = re.sub(r"\s+", "", code)
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 = list({i["code"] for i in issues})
100
- first_msg = issues[0].get("message", "unknown error")
101
- fix_hint = issues[0].get("fix", "")
102
- msg = f"{first_msg}\n{fix_hint}".strip() if fix_hint else first_msg
 
 
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
- return False, "marimo check output unparseable", ["MARIMO_CHECK_PARSE"]
 
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.stderr[:500]
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.stderr[:500]
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
- self._last_errors = sandbox.render_errors()
 
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: {sandbox.render_errors()}")
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("Repair phase: submit one revised artifact using the error feedback.")
 
 
 
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 sandbox.render_errors(),
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
- self._last_errors = sandbox.render_errors()
 
457
  self._last_error_codes = sandbox.error_codes
458
- self._phase = "done"
459
- self._done = True
 
 
 
 
460
 
461
  status = "REPAIR OK" if sandbox.exec_success else "REPAIR FAILED"
462
- feedback = (
463
- f"{status}: {sandbox.message if sandbox.exec_success else sandbox.render_errors()}\n"
464
- f"Reward: {', '.join(f'{k}={v}' for k, v in components.items())}"
465
- )
 
 
 
 
 
 
466
  return self._make_obs(
467
  task,
468
- phase="done",
469
  feedback=feedback,
470
  reward=repair_reward,
471
- done=True,
472
- last_errors="" if sandbox.exec_success else sandbox.render_errors(),
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 == 3
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 == 2
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(3):
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 ("code_valid", "code_runs", "coverage", "format_match", "structure"):
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 test_repair_ends_episode():
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.metadata["phase"] == "repair"
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
- test_repair_ends_episode,
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 == 3
65
- assert obs.repair_attempts_left == 1
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 "tool_choice" in comp
124
- assert "query_relevance" in comp
125
- assert "source_quality" in comp
126
- assert "coverage_delta" in comp
127
- assert "content_sufficiency" in comp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
 
130
  # --- Generation rewards ---
@@ -158,26 +247,67 @@ def test_narration_manim():
158
 
159
 
160
  def test_structure_marimo():
161
- good = """import marimo as mo
162
- app = mo.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.5 # no context
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["code_valid"] == 0.0
198
 
199
 
200
  def test_generate_reward_good():
201
- code = """import marimo as mo
202
- app = mo.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 _(X, y):
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["code_valid"] == 1.0
230
- assert comp["code_runs"] == 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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