Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +99 -36
- client.py +8 -2
- constants.py +24 -0
- models.py +42 -16
- notes.html +305 -0
- openenv_explainer_env.egg-info/PKG-INFO +11 -1
- openenv_explainer_env.egg-info/SOURCES.txt +4 -0
- openenv_explainer_env.egg-info/requires.txt +11 -1
- pyproject.toml +13 -3
- research/__init__.py +11 -0
- research/retrieval.py +179 -0
- research/router.py +381 -0
- research/types.py +62 -0
- rewards/README.md +75 -70
- rewards/__init__.py +1 -6
- rewards/exploration.py +170 -67
- rewards/generation.py +145 -77
- rewards/sandbox.py +161 -3
- rewards/sources.py +26 -313
- server/app.py +1 -1
- server/explainer_env_environment.py +235 -52
- tests/test_client_server.py +16 -3
- tests/test_docker.py +7 -0
- tests/test_environment.py +54 -12
- tests/test_models.py +22 -2
- tests/test_rewards.py +64 -3
- uv.lock +0 -0
README.md
CHANGED
|
@@ -11,26 +11,65 @@ tags:
|
|
| 11 |
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
-
# Research
|
| 15 |
|
| 16 |
-
An OpenEnv RL environment that trains small language models to create interactive educational content. Given a
|
| 17 |
|
| 18 |
-
|
| 19 |
-
2. **Generates** — produces a **Marimo** reactive notebook or **Manim** math animation explaining the topic
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
##
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
```
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
## Quick Start
|
| 36 |
|
|
@@ -49,7 +88,12 @@ with ExplainerEnv(base_url='http://localhost:8000').sync() as sc:
|
|
| 49 |
print(f'Topic: {result.observation.topic}, Tier: {result.observation.tier}')
|
| 50 |
|
| 51 |
# Explore
|
| 52 |
-
result = sc.step(ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
print(f'Explore reward: {result.reward:.3f}')
|
| 54 |
|
| 55 |
# Generate
|
|
@@ -62,30 +106,9 @@ with ExplainerEnv(base_url='http://localhost:8000').sync() as sc:
|
|
| 62 |
"
|
| 63 |
```
|
| 64 |
|
| 65 |
-
## LLM-as-Judge (Optional Eval)
|
| 66 |
-
|
| 67 |
-
For final evaluation of explanation quality, an optional LLM judge scores outputs on clarity, accuracy, engagement, completeness, and appropriateness.
|
| 68 |
-
|
| 69 |
-
**Not used during training** — too slow and non-deterministic for RL rewards. Training uses 12 fast heuristic reward components instead.
|
| 70 |
-
|
| 71 |
-
```bash
|
| 72 |
-
# Configure (any OpenAI-compatible endpoint)
|
| 73 |
-
export JUDGE_API_URL="http://localhost:11434/v1" # e.g. ollama
|
| 74 |
-
export JUDGE_MODEL="llama3"
|
| 75 |
-
|
| 76 |
-
# Usage
|
| 77 |
-
python -c "
|
| 78 |
-
from rewards.llm_judge import judge_explainability, is_available
|
| 79 |
-
if is_available():
|
| 80 |
-
score, details = judge_explainability(code='...', topic='Linear Regression', tier='beginner')
|
| 81 |
-
print(f'Score: {score:.2f}, Rationale: {details.get(\"rationale\", \"\")}')"
|
| 82 |
-
```
|
| 83 |
-
|
| 84 |
-
See [rewards/README.md](rewards/README.md) for full configuration details.
|
| 85 |
-
|
| 86 |
## Concurrent WebSocket Sessions
|
| 87 |
|
| 88 |
-
The server supports multiple concurrent WebSocket connections for parallel training rollouts:
|
| 89 |
|
| 90 |
```python
|
| 91 |
from client import ExplainerEnv
|
|
@@ -95,7 +118,11 @@ from concurrent.futures import ThreadPoolExecutor
|
|
| 95 |
def run_episode(client_id: int):
|
| 96 |
with ExplainerEnv(base_url="http://localhost:8000").sync() as sc:
|
| 97 |
result = sc.reset()
|
| 98 |
-
result = sc.step(ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
result = sc.step(ExplainerAction(
|
| 100 |
action_type="generate", format="marimo",
|
| 101 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
|
@@ -105,3 +132,39 @@ def run_episode(client_id: int):
|
|
| 105 |
with ThreadPoolExecutor(max_workers=4) as executor:
|
| 106 |
results = list(executor.map(run_episode, range(4)))
|
| 107 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Research -> Interactive Explainer Environment
|
| 15 |
|
| 16 |
+
An OpenEnv RL environment that trains small language models to create interactive educational content. Given a STEM topic, the agent explores with explicit research tools, generates a **Marimo** reactive notebook or **Manim** math animation, and gets one repair attempt if lint/build validation fails.
|
| 17 |
|
| 18 |
+
## Episode Flow
|
|
|
|
| 19 |
|
| 20 |
+
```
|
| 21 |
+
reset() --> topic + tier assigned
|
| 22 |
+
|
|
| 23 |
+
explore x 0..3 --> choose research tools + queries
|
| 24 |
+
|
|
| 25 |
+
generate x 1 --> produce marimo/manim code
|
| 26 |
+
|
|
| 27 |
+
repair x 0..1 --> fix lint/build errors if needed --> episode ends
|
| 28 |
+
```
|
| 29 |
|
| 30 |
+
## Actions
|
| 31 |
|
| 32 |
+
**Explore** -- search for information relevant to the assigned topic:
|
| 33 |
+
```python
|
| 34 |
+
ExplainerAction(
|
| 35 |
+
action_type="explore",
|
| 36 |
+
tool="search_arxiv",
|
| 37 |
+
query="merge sort divide and conquer visual explanation",
|
| 38 |
+
intent="find examples and visual intuition",
|
| 39 |
+
)
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Available tools: `search_wikipedia`, `search_hf_papers`, `search_arxiv`, `search_scholar`, `fetch_docs`, and `search_hf_hub`.
|
| 43 |
+
|
| 44 |
+
**Generate** -- produce educational code using accumulated research:
|
| 45 |
+
```python
|
| 46 |
+
ExplainerAction(
|
| 47 |
+
action_type="generate",
|
| 48 |
+
format="marimo", # or "manim"
|
| 49 |
+
code="import marimo...",
|
| 50 |
+
narration="...", # manim only
|
| 51 |
+
)
|
| 52 |
```
|
| 53 |
+
|
| 54 |
+
**Repair** -- revise generated code using lint/build feedback:
|
| 55 |
+
```python
|
| 56 |
+
ExplainerAction(
|
| 57 |
+
action_type="repair",
|
| 58 |
+
format="marimo",
|
| 59 |
+
code="import marimo...",
|
| 60 |
+
repair_notes="fixed the reported Marimo validation error",
|
| 61 |
+
)
|
| 62 |
```
|
| 63 |
|
| 64 |
+
## Reward System
|
| 65 |
+
|
| 66 |
+
Multi-component rewards across exploration, generation, and repair. See [rewards/README.md](rewards/README.md) for the full breakdown.
|
| 67 |
+
|
| 68 |
+
**Exploration** (per-step): tool choice, query quality, source quality, coverage delta, novelty, diversity, gated by information sufficiency. Step cost of -0.05 forces the agent to justify each search.
|
| 69 |
+
|
| 70 |
+
**Generation/repair**: keyword coverage, format match, structural quality (via `marimo check` CLI or manim scene analysis), narration (manim only), context usage, and repair success.
|
| 71 |
+
|
| 72 |
+
Key design: `marimo check` CLI catches 5 breaking rules (MB001-MB005) in ~100ms. Code that doesn't parse scores 0. Code that doesn't execute gets quality * 0.4.
|
| 73 |
|
| 74 |
## Quick Start
|
| 75 |
|
|
|
|
| 88 |
print(f'Topic: {result.observation.topic}, Tier: {result.observation.tier}')
|
| 89 |
|
| 90 |
# Explore
|
| 91 |
+
result = sc.step(ExplainerAction(
|
| 92 |
+
action_type='explore',
|
| 93 |
+
tool='search_wikipedia',
|
| 94 |
+
query=result.observation.topic,
|
| 95 |
+
intent='overview',
|
| 96 |
+
))
|
| 97 |
print(f'Explore reward: {result.reward:.3f}')
|
| 98 |
|
| 99 |
# Generate
|
|
|
|
| 106 |
"
|
| 107 |
```
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
## Concurrent WebSocket Sessions
|
| 110 |
|
| 111 |
+
The server supports multiple concurrent WebSocket connections for parallel GRPO training rollouts:
|
| 112 |
|
| 113 |
```python
|
| 114 |
from client import ExplainerEnv
|
|
|
|
| 118 |
def run_episode(client_id: int):
|
| 119 |
with ExplainerEnv(base_url="http://localhost:8000").sync() as sc:
|
| 120 |
result = sc.reset()
|
| 121 |
+
result = sc.step(ExplainerAction(
|
| 122 |
+
action_type="explore",
|
| 123 |
+
tool="search_wikipedia",
|
| 124 |
+
query=result.observation.topic,
|
| 125 |
+
))
|
| 126 |
result = sc.step(ExplainerAction(
|
| 127 |
action_type="generate", format="marimo",
|
| 128 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
|
|
|
| 132 |
with ThreadPoolExecutor(max_workers=4) as executor:
|
| 133 |
results = list(executor.map(run_episode, range(4)))
|
| 134 |
```
|
| 135 |
+
|
| 136 |
+
## API Endpoints
|
| 137 |
+
|
| 138 |
+
| Endpoint | Method | Description |
|
| 139 |
+
|---|---|---|
|
| 140 |
+
| `/reset` | POST | Start new episode, get topic assignment |
|
| 141 |
+
| `/step` | POST | Submit action, get observation + reward |
|
| 142 |
+
| `/state` | GET | Current episode state |
|
| 143 |
+
| `/schema` | GET | Action/Observation JSON schemas |
|
| 144 |
+
| `/ws` | WebSocket | Low-latency session for training |
|
| 145 |
+
| `/docs` | GET | Interactive API docs |
|
| 146 |
+
|
| 147 |
+
## Task Bank
|
| 148 |
+
|
| 149 |
+
26 tasks across 4 categories (ML, Math, Algorithms, Statistics), 3 difficulty levels (easy, medium, hard), and 3 audience tiers (beginner, intermediate, advanced). Each task specifies keywords for reward scoring and an optional preferred output format.
|
| 150 |
+
|
| 151 |
+
## File Structure
|
| 152 |
+
|
| 153 |
+
```
|
| 154 |
+
explainer_env/
|
| 155 |
+
├── server/
|
| 156 |
+
│ ├── explainer_env_environment.py # Environment logic (reset/step/state)
|
| 157 |
+
│ ├── app.py # FastAPI server (create_app)
|
| 158 |
+
│ └── Dockerfile # Multi-stage Docker build
|
| 159 |
+
├── rewards/
|
| 160 |
+
│ ├── exploration.py # Explore-phase reward components
|
| 161 |
+
│ ├── generation.py # Generate/repair reward components
|
| 162 |
+
│ ├── sources.py # Compatibility wrapper for research tools
|
| 163 |
+
│ ├── sandbox.py # Code validation (marimo check, AST, execution)
|
| 164 |
+
│ └── README.md # Reward system documentation
|
| 165 |
+
├── research/ # Research tools, structured results, retrieval
|
| 166 |
+
├── models.py # ExplainerAction, ExplainerObservation
|
| 167 |
+
├── task_bank.py # 26 curated STEM tasks
|
| 168 |
+
├── client.py # ExplainerEnv WebSocket client
|
| 169 |
+
└── openenv.yaml # OpenEnv manifest
|
| 170 |
+
```
|
client.py
CHANGED
|
@@ -6,7 +6,10 @@ from openenv.core import EnvClient
|
|
| 6 |
from openenv.core.client_types import StepResult
|
| 7 |
from openenv.core.env_server.types import State
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class ExplainerEnv(
|
|
@@ -20,7 +23,10 @@ class ExplainerEnv(
|
|
| 20 |
... result = sc.reset()
|
| 21 |
... # Explore phase
|
| 22 |
... result = sc.step(ExplainerAction(
|
| 23 |
-
... action_type="explore",
|
|
|
|
|
|
|
|
|
|
| 24 |
... ))
|
| 25 |
... # Generate phase
|
| 26 |
... result = sc.step(ExplainerAction(
|
|
|
|
| 6 |
from openenv.core.client_types import StepResult
|
| 7 |
from openenv.core.env_server.types import State
|
| 8 |
|
| 9 |
+
try:
|
| 10 |
+
from .models import ExplainerAction, ExplainerObservation
|
| 11 |
+
except ImportError: # pragma: no cover - supports direct test execution
|
| 12 |
+
from models import ExplainerAction, ExplainerObservation
|
| 13 |
|
| 14 |
|
| 15 |
class ExplainerEnv(
|
|
|
|
| 23 |
... result = sc.reset()
|
| 24 |
... # Explore phase
|
| 25 |
... result = sc.step(ExplainerAction(
|
| 26 |
+
... action_type="explore",
|
| 27 |
+
... tool="search_arxiv",
|
| 28 |
+
... query="attention mechanism transformers",
|
| 29 |
+
... intent="visual intuition and equations",
|
| 30 |
... ))
|
| 31 |
... # Generate phase
|
| 32 |
... result = sc.step(ExplainerAction(
|
constants.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 8 |
+
"search_hf_papers",
|
| 9 |
+
"search_arxiv",
|
| 10 |
+
"search_scholar",
|
| 11 |
+
"fetch_docs",
|
| 12 |
+
"search_hf_hub",
|
| 13 |
+
)
|
| 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)
|
models.py
CHANGED
|
@@ -1,10 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Data models for the Research → Interactive Explainer environment.
|
| 3 |
-
|
| 4 |
-
Two-phase episode:
|
| 5 |
-
1. Explore: agent searches for papers/resources (1-3 steps)
|
| 6 |
-
2. Generate: agent produces marimo/manim code (1 step, ends episode)
|
| 7 |
-
"""
|
| 8 |
|
| 9 |
from typing import Literal
|
| 10 |
|
|
@@ -12,31 +6,54 @@ from openenv.core.env_server.types import Action, Observation
|
|
| 12 |
from pydantic import Field
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
class ExplainerAction(Action):
|
| 16 |
-
"""Action: agent
|
| 17 |
|
| 18 |
-
action_type: Literal["explore", "generate"] = Field(
|
| 19 |
-
...,
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
# -- explore fields --
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
query: str = Field(
|
| 24 |
default="",
|
| 25 |
-
description="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
)
|
| 27 |
|
| 28 |
-
# -- generate fields --
|
| 29 |
format: Literal["marimo", "manim"] | None = Field(
|
| 30 |
default=None,
|
| 31 |
-
description="Output format (required
|
| 32 |
)
|
| 33 |
code: str = Field(
|
| 34 |
default="",
|
| 35 |
-
description="Complete Python source code (required
|
| 36 |
)
|
| 37 |
narration: str = Field(
|
| 38 |
default="",
|
| 39 |
-
description="Narration script (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
)
|
| 41 |
|
| 42 |
|
|
@@ -55,7 +72,7 @@ class ExplainerObservation(Observation):
|
|
| 55 |
)
|
| 56 |
|
| 57 |
# -- per-step feedback --
|
| 58 |
-
phase: Literal["explore", "generate", "done"] = Field(
|
| 59 |
default="explore", description="Current episode phase"
|
| 60 |
)
|
| 61 |
feedback: str = Field(default="", description="Feedback on the last action")
|
|
@@ -69,3 +86,12 @@ class ExplainerObservation(Observation):
|
|
| 69 |
explore_steps_left: int = Field(
|
| 70 |
default=3, description="Remaining explore steps before forced generate"
|
| 71 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data models for the Research -> Interactive Explainer environment."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from typing import Literal
|
| 4 |
|
|
|
|
| 6 |
from pydantic import Field
|
| 7 |
|
| 8 |
|
| 9 |
+
ResearchTool = Literal[
|
| 10 |
+
"search_wikipedia",
|
| 11 |
+
"search_hf_papers",
|
| 12 |
+
"search_arxiv",
|
| 13 |
+
"search_scholar",
|
| 14 |
+
"fetch_docs",
|
| 15 |
+
"search_hf_hub",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
class ExplainerAction(Action):
|
| 20 |
+
"""Action: agent explores, generates, or repairs an artifact."""
|
| 21 |
|
| 22 |
+
action_type: Literal["explore", "generate", "repair"] = Field(
|
| 23 |
+
...,
|
| 24 |
+
description="'explore' to research, 'generate' to produce code, 'repair' to fix code",
|
| 25 |
)
|
| 26 |
|
| 27 |
# -- explore fields --
|
| 28 |
+
tool: ResearchTool | None = Field(
|
| 29 |
+
default=None,
|
| 30 |
+
description="Research tool to call when action_type='explore'",
|
| 31 |
+
)
|
| 32 |
query: str = Field(
|
| 33 |
default="",
|
| 34 |
+
description="Research query used when action_type='explore'",
|
| 35 |
+
)
|
| 36 |
+
intent: str = Field(
|
| 37 |
+
default="",
|
| 38 |
+
description="Brief goal for the research call, e.g. equations or visual intuition",
|
| 39 |
)
|
| 40 |
|
| 41 |
+
# -- generate / repair fields --
|
| 42 |
format: Literal["marimo", "manim"] | None = Field(
|
| 43 |
default=None,
|
| 44 |
+
description="Output format (required for generate/repair)",
|
| 45 |
)
|
| 46 |
code: str = Field(
|
| 47 |
default="",
|
| 48 |
+
description="Complete Python source code (required for generate/repair)",
|
| 49 |
)
|
| 50 |
narration: str = Field(
|
| 51 |
default="",
|
| 52 |
+
description="Narration script (used when format='manim')",
|
| 53 |
+
)
|
| 54 |
+
repair_notes: str = Field(
|
| 55 |
+
default="",
|
| 56 |
+
description="Short explanation of what changed when action_type='repair'",
|
| 57 |
)
|
| 58 |
|
| 59 |
|
|
|
|
| 72 |
)
|
| 73 |
|
| 74 |
# -- per-step feedback --
|
| 75 |
+
phase: Literal["explore", "generate", "repair", "done"] = Field(
|
| 76 |
default="explore", description="Current episode phase"
|
| 77 |
)
|
| 78 |
feedback: str = Field(default="", description="Feedback on the last action")
|
|
|
|
| 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"
|
| 94 |
+
)
|
| 95 |
+
available_tools: list[str] = Field(
|
| 96 |
+
default_factory=list, description="Research tools available during explore"
|
| 97 |
+
)
|
notes.html
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/favicon.ico" />
|
| 6 |
+
<!-- Preload is necessary because we show these images when we disconnect from the server,
|
| 7 |
+
but at that point we cannot load these images from the server -->
|
| 8 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/gradient-yHQUC_QB.png" as="image" />
|
| 9 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/noise-60BoTA8O.png" as="image" />
|
| 10 |
+
<!-- Preload the fonts -->
|
| 11 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/Lora-VariableFont_wght-CZceb_kH.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
| 12 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/PTSans-Regular-Bam3NpBI.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
| 13 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/PTSans-Bold-C_DwAp7Z.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
| 14 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/FiraMono-Regular-CEsLFVD9.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
| 15 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/FiraMono-Medium-D5MAsWEG.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
| 16 |
+
<link rel="preload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/FiraMono-Bold-C6PDArdf.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
| 17 |
+
|
| 18 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 19 |
+
<meta name="theme-color" content="#000000" />
|
| 20 |
+
<meta name="description" content="a marimo app" />
|
| 21 |
+
<link rel="apple-touch-icon" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/apple-touch-icon.png" />
|
| 22 |
+
<link rel="manifest" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/manifest.json" />
|
| 23 |
+
|
| 24 |
+
<script data-marimo="true">
|
| 25 |
+
function __resizeIframe(obj) {
|
| 26 |
+
const scrollbarHeight = 20; // Max between windows, mac, and linux
|
| 27 |
+
|
| 28 |
+
function setHeight() {
|
| 29 |
+
// Guard against race condition where iframe isn't ready
|
| 30 |
+
if (!obj.contentWindow?.document?.documentElement) {
|
| 31 |
+
return;
|
| 32 |
+
}
|
| 33 |
+
const element = obj.contentWindow.document.documentElement;
|
| 34 |
+
// If there is no vertical scrollbar, we don't need to resize the iframe
|
| 35 |
+
if (element.scrollHeight === element.clientHeight) {
|
| 36 |
+
return;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Create a new height that includes the scrollbar height if it's visible
|
| 40 |
+
const hasHorizontalScrollbar = element.scrollWidth > element.clientWidth;
|
| 41 |
+
const newHeight = element.scrollHeight + (hasHorizontalScrollbar ? scrollbarHeight : 0);
|
| 42 |
+
|
| 43 |
+
// Only update the height if it's different from the current height
|
| 44 |
+
if (obj.style.height !== `${newHeight}px`) {
|
| 45 |
+
obj.style.height = `${newHeight}px`;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Resize the iframe to the height of the content and bottom scrollbar height
|
| 50 |
+
setHeight();
|
| 51 |
+
|
| 52 |
+
// Resize the iframe when the content changes
|
| 53 |
+
const resizeObserver = new ResizeObserver((_entries) => {
|
| 54 |
+
setHeight();
|
| 55 |
+
});
|
| 56 |
+
// Only observe if iframe content is ready
|
| 57 |
+
if (obj.contentWindow?.document?.body) {
|
| 58 |
+
resizeObserver.observe(obj.contentWindow.document.body);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
</script>
|
| 62 |
+
<marimo-filename hidden>notes.py</marimo-filename>
|
| 63 |
+
<!-- TODO(Trevor): Legacy, required by VS Code plugin. Remove when plugin is updated (see marimo/server/_templates/template.py) -->
|
| 64 |
+
<marimo-version data-version="{{ version }}" hidden></marimo-version>
|
| 65 |
+
<marimo-user-config data-config="{{ user_config }}" hidden></marimo-user-config>
|
| 66 |
+
<marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
|
| 67 |
+
<!-- /TODO -->
|
| 68 |
+
<title>notes</title>
|
| 69 |
+
<script type="module" crossorigin crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/index-BjiE1T38.js"></script>
|
| 70 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/preload-helper-D2MJg03u.js">
|
| 71 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/chunk-LvLJmgfZ.js">
|
| 72 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/react-Bj1aDYRI.js">
|
| 73 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/compiler-runtime-B3qBwwSJ.js">
|
| 74 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/jsx-runtime-BqBOg78p.js">
|
| 75 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useEventListener-BR0C1MaI.js">
|
| 76 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/defaultLocale-JieDVWC_.js">
|
| 77 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/precisionRound-CU2C3Vxx.js">
|
| 78 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/defaultLocale-BLne0bXb.js">
|
| 79 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/vega-loader.browser-DXARUlxo.js">
|
| 80 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/utils-Bq5kLQ87.js">
|
| 81 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/clsx-Dz_KRrWq.js">
|
| 82 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/cn-DCUzRj2J.js">
|
| 83 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/badge-ChrPsdTW.js">
|
| 84 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/button-D9nb17Rw.js">
|
| 85 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/react-dom-CSu739Rf.js">
|
| 86 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/fullscreen-BDxedMYP.js">
|
| 87 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/menu-items-CwUpDHG7.js">
|
| 88 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-BoOh2kN5.js">
|
| 89 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/createLucideIcon-D5guW7EU.js">
|
| 90 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/check-BH35Ndha.js">
|
| 91 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/x-C-6liIBr.js">
|
| 92 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/select-5i7URBEn.js">
|
| 93 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/tooltip-Gcwqb_SK.js">
|
| 94 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/use-toast-BDYuj3zG.js">
|
| 95 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useEvent-D91BmmQi.js">
|
| 96 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/invariant-BUdrueMv.js">
|
| 97 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/isObject-DvLSfCY5.js">
|
| 98 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/_baseFor-DKD1r8uL.js">
|
| 99 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/merge-_9WLTEyt.js">
|
| 100 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/zod-H_cgTO0M.js">
|
| 101 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/utils-DIGrmLDO.js">
|
| 102 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/Deferred-DxQeE5uh.js">
|
| 103 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/uuid-DXdzqzcr.js">
|
| 104 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/DeferredRequestRegistry-DjHgMr3S.js">
|
| 105 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/constants-DMpttj8Q.js">
|
| 106 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/session-nmTRerwF.js">
|
| 107 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/config-ChCHm539.js">
|
| 108 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/requests-DIwGYs0l.js">
|
| 109 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useLifecycle-DieWOfXE.js">
|
| 110 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useNonce-DfoVjkkH.js">
|
| 111 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useTheme-DEcgJENn.js">
|
| 112 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/arrays-DYDL-3-i.js">
|
| 113 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/strings-md4mFbOQ.js">
|
| 114 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/once-DRroIaBz.js">
|
| 115 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/capabilities-C_FLIcjP.js">
|
| 116 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/createReducer-1ePoj7v6.js">
|
| 117 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/paths-CXng2XQv.js">
|
| 118 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-BrR4M-k3.js">
|
| 119 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-HlsDto3K.js">
|
| 120 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-CHcznzB-.js">
|
| 121 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-DGxytjtv.js">
|
| 122 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-DZ2qbK2N.js">
|
| 123 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-CEysMnMQ.js">
|
| 124 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-Ds4ite_2.js">
|
| 125 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-CRPTyqAT.js">
|
| 126 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dist-C9ZB41s2.js">
|
| 127 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/stex-jWatZkll.js">
|
| 128 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/toDate-B5A0DFEz.js">
|
| 129 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/purify.es-DC4RGS9t.js">
|
| 130 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/cjs-BRGiG41H.js">
|
| 131 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/isSymbol-BT6B8Bon.js">
|
| 132 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/now-CsYTPAhW.js">
|
| 133 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/debounce-DhnxH9Rh.js">
|
| 134 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/database-zap-BUVn5HoR.js">
|
| 135 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/main-B0OX4z33.js">
|
| 136 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/cells-CJlo_hG2.js">
|
| 137 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/ErrorBoundary-DzYV_VeY.js">
|
| 138 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/kbd-CGShmG7L.js">
|
| 139 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useInstallPackage-afgh7PwB.js">
|
| 140 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/alert-dialog-BGBdrcqJ.js">
|
| 141 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dialog-EekxpBBM.js">
|
| 142 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useDebounce-BDIglWmG.js">
|
| 143 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/numbers-OHL_xBiC.js">
|
| 144 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/SSRProvider-D3zWcDme.js">
|
| 145 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/context-DGqo1TbK.js">
|
| 146 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useNumberFormatter-wQU1z0W_.js">
|
| 147 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/usePress-CtfZXGno.js">
|
| 148 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/input-DyPS_GiK.js">
|
| 149 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/ImperativeModal-BBqcKmmk.js">
|
| 150 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/cell-link-BA7Demf0.js">
|
| 151 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/multi-map-rafH3cg3.js">
|
| 152 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/alert-cDholnQq.js">
|
| 153 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/chevron-right-CG5QYXYk.js">
|
| 154 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dropdown-menu-D1A3cFC8.js">
|
| 155 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/links-C-rLiK3d.js">
|
| 156 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useRunCells-B531RIUE.js">
|
| 157 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/copy-B5qyZn5s.js">
|
| 158 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/copy-Dk_3y0H-.js">
|
| 159 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/copy-icon-C67c9EwB.js">
|
| 160 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/RenderHTML-BT9obKLc.js">
|
| 161 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/datasource-BKDU-4D5.js">
|
| 162 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/state-C5AUgyZT.js">
|
| 163 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/sparkles-CZ5WmLPA.js">
|
| 164 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/MarimoErrorOutput-BBD405a6.js">
|
| 165 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/spinner-Bhir8k53.js">
|
| 166 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/html-to-image-Cz8lDF-Y.js">
|
| 167 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/focus-BX3gXJxx.js">
|
| 168 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useAsyncData-dS4Ne6pU.js">
|
| 169 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/LazyAnyLanguageCodeMirror-0gZO1asr.js">
|
| 170 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/micromark-factory-space-WzovnJik.js">
|
| 171 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/chunk-5FQGJX7Z-B4RvNAGm.js">
|
| 172 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/markdown-renderer-CeHY2KoQ.js">
|
| 173 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/command-DbT_zkRP.js">
|
| 174 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/popover-UExmgBsf.js">
|
| 175 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/errors-CZb6hI2x.js">
|
| 176 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/download-mzsKQgiy.js">
|
| 177 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/table-23NNA1s9.js">
|
| 178 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useIframeCapabilities-CryCjoyY.js">
|
| 179 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/error-banner-CTBwdcdk.js">
|
| 180 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/formats-BvyJSIqk.js">
|
| 181 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/en-US-BqRooSzc.js">
|
| 182 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/isValid-bmSEa3HX.js">
|
| 183 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/dates-DhByPWH5.js">
|
| 184 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/maps-C48Oksn0.js">
|
| 185 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/extends-Co37_JfG.js">
|
| 186 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/emotion-is-prop-valid.esm-DYxi7n2b.js">
|
| 187 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useDateFormatter-BEz9SEXo.js">
|
| 188 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/react-icons.esm-BUYTQ32a.js">
|
| 189 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/table-6NxtGaCm.js">
|
| 190 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/ellipsis-pk06Lq82.js">
|
| 191 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/message-circle-CWm2KnSx.js">
|
| 192 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/react-resizable-panels.browser.esm-Bcm5njwd.js">
|
| 193 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/JsonOutput-DXnOS_Hk.js">
|
| 194 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/chart-no-axes-column-nqk474t8.js">
|
| 195 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/square-function-D1dlJvD8.js">
|
| 196 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/spec-Cq8FVoTf.js">
|
| 197 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/ellipsis-vertical-gxgNyR5G.js">
|
| 198 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/refresh-cw-DHwG4Mac.js">
|
| 199 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/tree-actions-oMCx6WNc.js">
|
| 200 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/components-BiBOcq1x.js">
|
| 201 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/column-preview-DzN2QumC.js">
|
| 202 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/icons-BRopQwI3.js">
|
| 203 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/floating-outline-BMB4_phA.js">
|
| 204 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useAddCell-CqBbGhrY.js">
|
| 205 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/objectWithoutPropertiesLoose-smPWkHxB.js">
|
| 206 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/esm-G1JMtvxe.js">
|
| 207 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/eye-off-BT-KOYV5.js">
|
| 208 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/plus-BgB18UzY.js">
|
| 209 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/readonly-python-code-Ccd8HM-7.js">
|
| 210 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/file-headphone-B3fuktN0.js">
|
| 211 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/file-HTLbeC2b.js">
|
| 212 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/image-DQHXdEQn.js">
|
| 213 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/file-icons-BNrh8MRG.js">
|
| 214 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/switch-DlgpDZMk.js">
|
| 215 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/events-Qeh-bHlj.js">
|
| 216 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/globals-xVhZei_S.js">
|
| 217 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/share-BXN2Sd1t.js">
|
| 218 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/blob-CTort_or.js">
|
| 219 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/memoize-Tp7rARFe.js">
|
| 220 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/get-C-qh_et5.js">
|
| 221 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/_baseSet-CxV9N1bc.js">
|
| 222 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/state-DHlRrwyY.js">
|
| 223 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/label-DTR8T0AE.js">
|
| 224 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/textarea-BXPC1-kb.js">
|
| 225 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/radio-group-BtBoRbGH.js">
|
| 226 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/refresh-ccw-C-n2VFP5.js">
|
| 227 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/trash-2-D280Xiwg.js">
|
| 228 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/form-CPDlIjdV.js">
|
| 229 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/renderShortcut-CkNNAheg.js">
|
| 230 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/field-B8kJgr2A.js">
|
| 231 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/RSPContexts-BeHIgT4C.js">
|
| 232 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useBoolean-CLFdHewv.js">
|
| 233 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/useDeepCompareMemoize-D0WTlCXt.js">
|
| 234 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/types-W8WWuumF.js">
|
| 235 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/fileToBase64-Bzn96tYq.js">
|
| 236 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/pathUtils-CSrNy17a.js">
|
| 237 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/prop-types-BFRDxSKF.js">
|
| 238 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/es-YQvzKo5h.js">
|
| 239 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/hasIn-DiS_ryrS.js">
|
| 240 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/flatten-Dl4V1ub3.js">
|
| 241 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/pick-DqWUNly1.js">
|
| 242 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/code-xml-CZN6vNxu.js">
|
| 243 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/square-Dj2Cf4ne.js">
|
| 244 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/triangle-alert-DBoAWWKA.js">
|
| 245 |
+
<link rel="modulepreload" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/bundle.esm-DU-4isVC.js">
|
| 246 |
+
<link rel="stylesheet" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/cells-jmgGt1lS.css">
|
| 247 |
+
<link rel="stylesheet" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/markdown-renderer-DdDKmWlR.css">
|
| 248 |
+
<link rel="stylesheet" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/JsonOutput-B7vuddcd.css">
|
| 249 |
+
<link rel="stylesheet" crossorigin crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@0.23.3/dist/assets/index-BYLYJcAY.css">
|
| 250 |
+
|
| 251 |
+
<script data-marimo="true">
|
| 252 |
+
Object.defineProperty(window, "__MARIMO_STATIC__", {
|
| 253 |
+
value: Object.freeze({
|
| 254 |
+
files: Object.freeze({}),
|
| 255 |
+
modelNotifications: Object.freeze([]),
|
| 256 |
+
}),
|
| 257 |
+
writable: false,
|
| 258 |
+
configurable: false,
|
| 259 |
+
});
|
| 260 |
+
</script>
|
| 261 |
+
|
| 262 |
+
<script data-marimo="true">
|
| 263 |
+
Object.defineProperty(window, "__MARIMO_EXPORT_CONTEXT__", {
|
| 264 |
+
value: Object.freeze({
|
| 265 |
+
trusted: true,
|
| 266 |
+
notebookCode: "import marimo\n\n__generated_with = \"0.23.3\"\napp = marimo.App()\n\n\n@app.cell\ndef _():\n import marimo as mo\n import numpy as np\n\n # Remove and warn if matplotlib is missing.\n try:\n import matplotlib.pyplot as plt\n except ModuleNotFoundError:\n plt = None\n mo.notification(\"Matplotlib is not installed. Plots will be skipped or limited.\", kind=\"warning\")\n\n from sklearn.datasets import make_regression\n from sklearn.linear_model import LinearRegression\n\n return LinearRegression, make_regression, mo, np, plt\n\n\n@app.cell\ndef _(mo):\n mo.md(\"# Linear Regression: A Beginner's Guide\")\n mo.md(\n \"Linear regression is a fundamental technique in machine learning used to model the relationship between a dependent variable and one or more independent variables.\"\n )\n mo.md(\n \"In this notebook, we'll explore what linear regression is, how it works mathematically, how to fit a model using scikit-learn, and how to implement it from scratch using gradient descent.\"\n )\n return\n\n\n@app.cell\ndef _(LinearRegression, make_regression, mo, np, plt):\n mo.md(\"## What is Linear Regression?\")\n mo.md(\n \"Linear regression aims to find the 'best fit' line through data points. This line is defined by weights (coefficients) and a bias term.\"\n )\n\n # Create sample data\n X, y = make_regression(n_samples=100, n_features=1, noise=10, random_state=42)\n\n # Fit sklearn linear regression\n lin_reg = LinearRegression()\n lin_reg.fit(X, y)\n\n # Calculate predictions\n y_pred = lin_reg.predict(X)\n\n if plt is not None:\n # Sort the data for a prettier line plot\n sorted_idx = np.argsort(X[:, 0])\n X_sorted = X[sorted_idx]\n y_pred_sorted = y_pred[sorted_idx]\n y_sorted = y[sorted_idx]\n\n # Plot\n fig, ax = plt.subplots(figsize=(10, 6))\n ax.scatter(X, y, alpha=0.6, label=\"Data points\")\n ax.plot(X_sorted, y_pred_sorted, color=\"red\", linewidth=2, label=\"Fitted line\")\n ax.set_xlabel(\"X\")\n ax.set_ylabel(\"y\")\n ax.legend()\n ax.grid(True, alpha=0.3)\n ax.set_title(\"Linear Regression Example\")\n\n mo.display(fig)\n else:\n mo.notification(\"Skipping plot: matplotlib is not available.\", kind=\"error\")\n return X, lin_reg, y, y_pred\n\n\n@app.cell\ndef _(lin_reg, mo):\n mo.md(\"## Mathematical Foundation\")\n mo.md(\"The linear regression model can be expressed as:\")\n mo.md(\"$$y = Xw + b$$\")\n mo.md(\"Where:\")\n mo.md(\"- $y$ is the target variable\")\n mo.md(\"- $X$ is the matrix of input features\")\n mo.md(\"- $w$ is the vector of weights\")\n mo.md(\"- $b$ is the bias term\")\n\n mo.md(\"In our example:\")\n mo.md(f\"- Weights (w): {lin_reg.coef_[0]:.2f}\")\n mo.md(f\"- Bias (b): {lin_reg.intercept_:.2f}\")\n return\n\n\n@app.cell\ndef _(mo, np, y, y_pred):\n mo.md(\"## Loss Function: Mean Squared Error (MSE)\")\n mo.md(\n \"The goal of linear regression is to minimize the error between predicted and actual values. The most common loss function is Mean Squared Error:\"\n )\n mo.md(\"$$\\\\text{MSE} = \\\\frac{1}{n} \\\\sum_{i=1}^{n} (y_i - \\\\hat{y}_i)^2$$\")\n mo.md(\"Where:\")\n mo.md(\"- $y_i$ is the actual value\")\n mo.md(\"- $\\\\hat{y}_i$ is the predicted value\")\n mo.md(\"- $n$ is the number of samples\")\n\n # Calculate MSE\n mse = np.mean((y - y_pred) ** 2)\n mo.md(f\"In our example, the MSE is approximately: {mse:.2f}\")\n return\n\n\n@app.cell\ndef _(X, mo, np, y):\n mo.md(\"## How Does It Work? Gradient Descent\")\n mo.md(\n \"To minimize the MSE, we can use an optimization algorithm called gradient descent. This algorithm iteratively adjusts the weights and bias to reduce the loss function.\"\n )\n\n # Simple implementation of gradient descent\n def gradient_descent(X, y, learning_rate=0.01, iterations=1000):\n m = len(X)\n w = np.random.randn(1)\n b = 0.0\n\n # Reshape X\n x_vec = X.ravel()\n for i in range(iterations):\n y_pred = x_vec * w + b\n\n # Compute gradients\n dw = (-2 / m) * np.sum(x_vec * (y - y_pred))\n db = (-2 / m) * np.sum(y - y_pred)\n\n w -= learning_rate * dw\n b -= learning_rate * db\n\n return w, b\n\n w_gd, b_gd = gradient_descent(X, y)\n y_pred_gd = X.ravel() * w_gd + b_gd\n\n mse_gd = np.mean((y - y_pred_gd) ** 2)\n\n mo.md(\"Using gradient descent (learning rate=0.01, 1000 iterations):\")\n mo.md(f\"- Weights (w): {w_gd[0]:.2f}\")\n mo.md(f\"- Bias (b): {b_gd:.2f}\")\n mo.md(f\"- MSE: {mse_gd:.2f}\")\n return (y_pred_gd,)\n\n\n@app.cell\ndef _(X, mo, np, plt, y, y_pred, y_pred_gd):\n mo.md(\"## Visualizing the Process\")\n if plt is not None:\n fig, ax = plt.subplots(figsize=(10, 6))\n ax.scatter(X, y, alpha=0.6, label=\"Data points\")\n\n # For clean lines, sort the X for both predictions\n sorted_idx = np.argsort(X[:, 0])\n X_sorted = X[sorted_idx]\n y_pred_sorted = y_pred[sorted_idx]\n y_pred_gd_sorted = y_pred_gd[sorted_idx]\n\n ax.plot(X_sorted, y_pred_sorted, color=\"red\", linewidth=2, label=\"Sklearn fit\")\n ax.plot(X_sorted, y_pred_gd_sorted, color=\"green\", linewidth=2, linestyle=\"--\", label=\"Gradient descent\")\n ax.set_xlabel(\"X\")\n ax.set_ylabel(\"y\")\n ax.legend()\n ax.grid(True, alpha=0.3)\n ax.set_title(\"Comparison: Sklearn vs Gradient Descent\")\n mo.display(fig)\n else:\n mo.notification(\"Skipping plot: matplotlib is not available.\", kind=\"error\")\n return\n\n\n@app.cell\ndef _(mo):\n mo.md(\"## Key Takeaways\")\n mo.md(\"1. Linear regression models the relationship between variables using a linear equation.\")\n mo.md(\"2. The parameters (weights and bias) are learned by minimizing the loss function.\")\n mo.md(\"3. Mean Squared Error (MSE) is a common loss function.\")\n mo.md(\"4. Gradient descent is commonly used to optimize parameters.\")\n mo.md(\"5. Even simple models like linear regression can be very effective!\")\n\n mo.md(\"---\")\n mo.md(\"**Try modifying the data, learning rate, or number of iterations above to see how the model changes!**\")\n return\n\n\nif __name__ == \"__main__\":\n app.run()\n",
|
| 267 |
+
}),
|
| 268 |
+
writable: false,
|
| 269 |
+
configurable: false,
|
| 270 |
+
});
|
| 271 |
+
</script>
|
| 272 |
+
</head>
|
| 273 |
+
<body>
|
| 274 |
+
<div id="root"></div>
|
| 275 |
+
<!-- This is a portal for the data editor to render in -->
|
| 276 |
+
<div id="portal" data-testid="glide-portal" style="position: fixed; left: 0; top: 0; z-index: 9999"></div>
|
| 277 |
+
<script data-marimo="true">
|
| 278 |
+
Object.defineProperty(window, "__MARIMO_MOUNT_CONFIG__", {
|
| 279 |
+
value: Object.freeze({
|
| 280 |
+
"filename": "notes.py",
|
| 281 |
+
"cwd": "",
|
| 282 |
+
"lspWorkspace": null,
|
| 283 |
+
"mode": "read",
|
| 284 |
+
"version": "0.23.3",
|
| 285 |
+
"serverToken": "static",
|
| 286 |
+
"config": {"ai": {"custom_providers": {}, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}},
|
| 287 |
+
"configOverrides": {},
|
| 288 |
+
"appConfig": {"sql_output": "auto", "width": "compact"},
|
| 289 |
+
"view": {"showAppCode": true},
|
| 290 |
+
"notebook": {"cells": [{"code": "import marimo as mo\nimport numpy as np\n\n# Remove and warn if matplotlib is missing.\ntry:\n import matplotlib.pyplot as plt\nexcept ModuleNotFoundError:\n plt = None\n mo.notification(\"Matplotlib is not installed. Plots will be skipped or limited.\", kind=\"warning\")\n\nfrom sklearn.datasets import make_regression\nfrom sklearn.linear_model import LinearRegression", "code_hash": "6311d2d636bfb7b8a25160e81c78d21d", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "Hbol", "name": "_"}, {"code": "mo.md(\"# Linear Regression: A Beginner's Guide\")\nmo.md(\n \"Linear regression is a fundamental technique in machine learning used to model the relationship between a dependent variable and one or more independent variables.\"\n)\nmo.md(\n \"In this notebook, we'll explore what linear regression is, how it works mathematically, how to fit a model using scikit-learn, and how to implement it from scratch using gradient descent.\"\n)", "code_hash": "ae6176c366881e7b334d536a6a4860a5", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "MJUe", "name": "_"}, {"code": "mo.md(\"## What is Linear Regression?\")\nmo.md(\n \"Linear regression aims to find the 'best fit' line through data points. This line is defined by weights (coefficients) and a bias term.\"\n)\n\n# Create sample data\nX, y = make_regression(n_samples=100, n_features=1, noise=10, random_state=42)\n\n# Fit sklearn linear regression\nlin_reg = LinearRegression()\nlin_reg.fit(X, y)\n\n# Calculate predictions\ny_pred = lin_reg.predict(X)\n\nif plt is not None:\n # Sort the data for a prettier line plot\n sorted_idx = np.argsort(X[:, 0])\n X_sorted = X[sorted_idx]\n y_pred_sorted = y_pred[sorted_idx]\n y_sorted = y[sorted_idx]\n\n # Plot\n fig, ax = plt.subplots(figsize=(10, 6))\n ax.scatter(X, y, alpha=0.6, label=\"Data points\")\n ax.plot(X_sorted, y_pred_sorted, color=\"red\", linewidth=2, label=\"Fitted line\")\n ax.set_xlabel(\"X\")\n ax.set_ylabel(\"y\")\n ax.legend()\n ax.grid(True, alpha=0.3)\n ax.set_title(\"Linear Regression Example\")\n\n mo.display(fig)\nelse:\n mo.notification(\"Skipping plot: matplotlib is not available.\", kind=\"error\")", "code_hash": "170ff6c75442add92b65232c51558154", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "vblA", "name": "_"}, {"code": "mo.md(\"## Mathematical Foundation\")\nmo.md(\"The linear regression model can be expressed as:\")\nmo.md(\"$$y = Xw + b$$\")\nmo.md(\"Where:\")\nmo.md(\"- $y$ is the target variable\")\nmo.md(\"- $X$ is the matrix of input features\")\nmo.md(\"- $w$ is the vector of weights\")\nmo.md(\"- $b$ is the bias term\")\n\nmo.md(\"In our example:\")\nmo.md(f\"- Weights (w): {lin_reg.coef_[0]:.2f}\")\nmo.md(f\"- Bias (b): {lin_reg.intercept_:.2f}\")", "code_hash": "59a3865c53fa126bd4cf2f338bb8a1d3", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "bkHC", "name": "_"}, {"code": "mo.md(\"## Loss Function: Mean Squared Error (MSE)\")\nmo.md(\n \"The goal of linear regression is to minimize the error between predicted and actual values. The most common loss function is Mean Squared Error:\"\n)\nmo.md(\"$$\\\\text{MSE} = \\\\frac{1}{n} \\\\sum_{i=1}^{n} (y_i - \\\\hat{y}_i)^2$$\")\nmo.md(\"Where:\")\nmo.md(\"- $y_i$ is the actual value\")\nmo.md(\"- $\\\\hat{y}_i$ is the predicted value\")\nmo.md(\"- $n$ is the number of samples\")\n\n# Calculate MSE\nmse = np.mean((y - y_pred) ** 2)\nmo.md(f\"In our example, the MSE is approximately: {mse:.2f}\")", "code_hash": "de9208afe44eff71d520ff697cad4770", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "lEQa", "name": "_"}, {"code": "mo.md(\"## How Does It Work? Gradient Descent\")\nmo.md(\n \"To minimize the MSE, we can use an optimization algorithm called gradient descent. This algorithm iteratively adjusts the weights and bias to reduce the loss function.\"\n)\n\n# Simple implementation of gradient descent\ndef gradient_descent(X, y, learning_rate=0.01, iterations=1000):\n m = len(X)\n w = np.random.randn(1)\n b = 0.0\n\n # Reshape X\n x_vec = X.ravel()\n for i in range(iterations):\n y_pred = x_vec * w + b\n\n # Compute gradients\n dw = (-2 / m) * np.sum(x_vec * (y - y_pred))\n db = (-2 / m) * np.sum(y - y_pred)\n\n w -= learning_rate * dw\n b -= learning_rate * db\n\n return w, b\n\nw_gd, b_gd = gradient_descent(X, y)\ny_pred_gd = X.ravel() * w_gd + b_gd\n\nmse_gd = np.mean((y - y_pred_gd) ** 2)\n\nmo.md(\"Using gradient descent (learning rate=0.01, 1000 iterations):\")\nmo.md(f\"- Weights (w): {w_gd[0]:.2f}\")\nmo.md(f\"- Bias (b): {b_gd:.2f}\")\nmo.md(f\"- MSE: {mse_gd:.2f}\")", "code_hash": "df993a47fd4bb8dc97b0a17617769ede", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "PKri", "name": "_"}, {"code": "mo.md(\"## Visualizing the Process\")\nif plt is not None:\n fig, ax = plt.subplots(figsize=(10, 6))\n ax.scatter(X, y, alpha=0.6, label=\"Data points\")\n\n # For clean lines, sort the X for both predictions\n sorted_idx = np.argsort(X[:, 0])\n X_sorted = X[sorted_idx]\n y_pred_sorted = y_pred[sorted_idx]\n y_pred_gd_sorted = y_pred_gd[sorted_idx]\n\n ax.plot(X_sorted, y_pred_sorted, color=\"red\", linewidth=2, label=\"Sklearn fit\")\n ax.plot(X_sorted, y_pred_gd_sorted, color=\"green\", linewidth=2, linestyle=\"--\", label=\"Gradient descent\")\n ax.set_xlabel(\"X\")\n ax.set_ylabel(\"y\")\n ax.legend()\n ax.grid(True, alpha=0.3)\n ax.set_title(\"Comparison: Sklearn vs Gradient Descent\")\n mo.display(fig)\nelse:\n mo.notification(\"Skipping plot: matplotlib is not available.\", kind=\"error\")", "code_hash": "306bc2687366587cb56a645ff49a1445", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "Xref", "name": "_"}, {"code": "mo.md(\"## Key Takeaways\")\nmo.md(\"1. Linear regression models the relationship between variables using a linear equation.\")\nmo.md(\"2. The parameters (weights and bias) are learned by minimizing the loss function.\")\nmo.md(\"3. Mean Squared Error (MSE) is a common loss function.\")\nmo.md(\"4. Gradient descent is commonly used to optimize parameters.\")\nmo.md(\"5. Even simple models like linear regression can be very effective!\")\n\nmo.md(\"---\")\nmo.md(\"**Try modifying the data, learning rate, or number of iterations above to see how the model changes!**\")", "code_hash": "87605417fa79512c613421afe7a9f671", "config": {"column": null, "disabled": false, "hide_code": false}, "id": "SFPL", "name": "_"}], "metadata": {"marimo_version": "0.23.3"}, "version": "1"},
|
| 291 |
+
"session": {"cells": [{"code_hash": "6311d2d636bfb7b8a25160e81c78d21d", "console": [{"mimetype": "application/vnd.marimo+traceback", "name": "stderr", "text": "\u003Cspan class=\"codehilite\"\u003E\u003Cdiv class=\"highlight\"\u003E\u003Cpre\u003E\u003Cspan\u003E\u003C/span\u003E\u003Cspan class=\"gt\"\u003ETraceback (most recent call last):\u003C/span\u003E\n File \u003Cspan class=\"nb\"\u003E\u0026quot;/var/folders/44/xrn2r2n539lcfpfq4mb99p48hlq2mb/T/marimo_89694/__marimo__cell_Hbol_.py\u0026quot;\u003C/span\u003E, line \u003Cspan class=\"m\"\u003E6\u003C/span\u003E, in \u003Cspan class=\"n\"\u003E\u0026lt;module\u0026gt;\u003C/span\u003E\n\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"kn\"\u003Eimport\u003C/span\u003E\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"nn\"\u003Ematplotlib.pyplot\u003C/span\u003E\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"k\"\u003Eas\u003C/span\u003E\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"nn\"\u003Eplt\u003C/span\u003E\n\u003Cspan class=\"gr\"\u003EModuleNotFoundError\u003C/span\u003E: \u003Cspan class=\"n\"\u003ENo module named \u0026#39;matplotlib\u0026#39;\u003C/span\u003E\n\n\u003Cspan class=\"gt\"\u003EDuring handling of the above exception, another exception occurred:\u003C/span\u003E\n\n\u003Cspan class=\"gt\"\u003ETraceback (most recent call last):\u003C/span\u003E\n File \u003Cspan class=\"nb\"\u003E\u0026quot;/Users/mmt10913/Personal/hackathons/openenv-hackathon/explainer_env/.venv/lib/python3.12/site-packages/marimo/_runtime/executor.py\u0026quot;\u003C/span\u003E, line \u003Cspan class=\"m\"\u003E138\u003C/span\u003E, in \u003Cspan class=\"n\"\u003Eexecute_cell\u003C/span\u003E\n\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"n\"\u003Eexec\u003C/span\u003E\u003Cspan class=\"p\"\u003E(\u003C/span\u003E\u003Cspan class=\"n\"\u003Ecell\u003C/span\u003E\u003Cspan class=\"o\"\u003E.\u003C/span\u003E\u003Cspan class=\"n\"\u003Ebody\u003C/span\u003E\u003Cspan class=\"p\"\u003E,\u003C/span\u003E \u003Cspan class=\"n\"\u003Eglbls\u003C/span\u003E\u003Cspan class=\"p\"\u003E)\u003C/span\u003E\n File \u003Cspan class=\"nb\"\u003E\u0026quot;/var/folders/44/xrn2r2n539lcfpfq4mb99p48hlq2mb/T/marimo_89694/__marimo__cell_Hbol_.py\u0026quot;\u003C/span\u003E, line \u003Cspan class=\"m\"\u003E9\u003C/span\u003E, in \u003Cspan class=\"n\"\u003E\u0026lt;module\u0026gt;\u003C/span\u003E\n\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"n\"\u003Emo\u003C/span\u003E\u003Cspan class=\"o\"\u003E.\u003C/span\u003E\u003Cspan class=\"n\"\u003Enotification\u003C/span\u003E\u003Cspan class=\"p\"\u003E(\u003C/span\u003E\u003Cspan class=\"s2\"\u003E\u0026quot;Matplotlib is not installed. Plots will be skipped or limited.\u0026quot;\u003C/span\u003E\u003Cspan class=\"p\"\u003E,\u003C/span\u003E \u003Cspan class=\"n\"\u003Ekind\u003C/span\u003E\u003Cspan class=\"o\"\u003E=\u003C/span\u003E\u003Cspan class=\"s2\"\u003E\u0026quot;warning\u0026quot;\u003C/span\u003E\u003Cspan class=\"p\"\u003E)\u003C/span\u003E\n\u003Cspan class=\"w\"\u003E \u003C/span\u003E\u003Cspan class=\"pm\"\u003E^^^^^^^^^^^^^^^\u003C/span\u003E\n\u003Cspan class=\"gr\"\u003EAttributeError\u003C/span\u003E: \u003Cspan class=\"n\"\u003Emodule \u0026#39;marimo\u0026#39; has no attribute \u0026#39;notification\u0026#39;\u003C/span\u003E\n\u003C/pre\u003E\u003C/div\u003E\n\u003C/span\u003E", "type": "stream"}], "id": "Hbol", "outputs": [{"ename": "exception", "evalue": "module 'marimo' has no attribute 'notification'", "traceback": null, "type": "error"}]}, {"code_hash": "ae6176c366881e7b334d536a6a4860a5", "console": [], "id": "MJUe", "outputs": [{"ename": "exception", "evalue": "An ancestor raised an exception (AttributeError): ", "traceback": null, "type": "error"}]}, {"code_hash": "170ff6c75442add92b65232c51558154", "console": [], "id": "vblA", "outputs": [{"ename": "multiple-defs", "evalue": "The variable 'X_sorted' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'ax' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'fig' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'sorted_idx' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'y_pred_sorted' was defined by another cell", "traceback": [], "type": "error"}]}, {"code_hash": "59a3865c53fa126bd4cf2f338bb8a1d3", "console": [], "id": "bkHC", "outputs": [{"ename": "exception", "evalue": "An ancestor raised an exception (AttributeError): ", "traceback": null, "type": "error"}]}, {"code_hash": "de9208afe44eff71d520ff697cad4770", "console": [], "id": "lEQa", "outputs": [{"ename": "exception", "evalue": "An ancestor raised an exception (AttributeError): ", "traceback": null, "type": "error"}]}, {"code_hash": "df993a47fd4bb8dc97b0a17617769ede", "console": [], "id": "PKri", "outputs": [{"ename": "exception", "evalue": "An ancestor raised an exception (AttributeError): ", "traceback": null, "type": "error"}]}, {"code_hash": "306bc2687366587cb56a645ff49a1445", "console": [], "id": "Xref", "outputs": [{"ename": "multiple-defs", "evalue": "The variable 'X_sorted' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'ax' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'fig' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'sorted_idx' was defined by another cell", "traceback": [], "type": "error"}, {"ename": "multiple-defs", "evalue": "The variable 'y_pred_sorted' was defined by another cell", "traceback": [], "type": "error"}]}, {"code_hash": "87605417fa79512c613421afe7a9f671", "console": [], "id": "SFPL", "outputs": [{"ename": "exception", "evalue": "An ancestor raised an exception (AttributeError): ", "traceback": null, "type": "error"}]}], "metadata": {"marimo_version": "0.23.3", "script_metadata_hash": null}, "version": "1"},
|
| 292 |
+
"runtimeConfig": null,
|
| 293 |
+
}),
|
| 294 |
+
writable: false,
|
| 295 |
+
configurable: false,
|
| 296 |
+
});
|
| 297 |
+
</script>
|
| 298 |
+
|
| 299 |
+
<marimo-code hidden="">
|
| 300 |
+
import%20marimo%0A%0A__generated_with%20%3D%20%220.23.3%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20%23%20Remove%20and%20warn%20if%20matplotlib%20is%20missing.%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20except%20ModuleNotFoundError%3A%0A%20%20%20%20%20%20%20%20plt%20%3D%20None%0A%20%20%20%20%20%20%20%20mo.notification(%22Matplotlib%20is%20not%20installed.%20Plots%20will%20be%20skipped%20or%20limited.%22%2C%20kind%3D%22warning%22)%0A%0A%20%20%20%20from%20sklearn.datasets%20import%20make_regression%0A%20%20%20%20from%20sklearn.linear_model%20import%20LinearRegression%0A%0A%20%20%20%20return%20LinearRegression%2C%20make_regression%2C%20mo%2C%20np%2C%20plt%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%22%23%20Linear%20Regression%3A%20A%20Beginner's%20Guide%22)%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%22Linear%20regression%20is%20a%20fundamental%20technique%20in%20machine%20learning%20used%20to%20model%20the%20relationship%20between%20a%20dependent%20variable%20and%20one%20or%20more%20independent%20variables.%22%0A%20%20%20%20)%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%22In%20this%20notebook%2C%20we'll%20explore%20what%20linear%20regression%20is%2C%20how%20it%20works%20mathematically%2C%20how%20to%20fit%20a%20model%20using%20scikit-learn%2C%20and%20how%20to%20implement%20it%20from%20scratch%20using%20gradient%20descent.%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(LinearRegression%2C%20make_regression%2C%20mo%2C%20np%2C%20plt)%3A%0A%20%20%20%20mo.md(%22%23%23%20What%20is%20Linear%20Regression%3F%22)%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%22Linear%20regression%20aims%20to%20find%20the%20'best%20fit'%20line%20through%20data%20points.%20This%20line%20is%20defined%20by%20weights%20(coefficients)%20and%20a%20bias%20term.%22%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Create%20sample%20data%0A%20%20%20%20X%2C%20y%20%3D%20make_regression(n_samples%3D100%2C%20n_features%3D1%2C%20noise%3D10%2C%20random_state%3D42)%0A%0A%20%20%20%20%23%20Fit%20sklearn%20linear%20regression%0A%20%20%20%20lin_reg%20%3D%20LinearRegression()%0A%20%20%20%20lin_reg.fit(X%2C%20y)%0A%0A%20%20%20%20%23%20Calculate%20predictions%0A%20%20%20%20y_pred%20%3D%20lin_reg.predict(X)%0A%0A%20%20%20%20if%20plt%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%23%20Sort%20the%20data%20for%20a%20prettier%20line%20plot%0A%20%20%20%20%20%20%20%20sorted_idx%20%3D%20np.argsort(X%5B%3A%2C%200%5D)%0A%20%20%20%20%20%20%20%20X_sorted%20%3D%20X%5Bsorted_idx%5D%0A%20%20%20%20%20%20%20%20y_pred_sorted%20%3D%20y_pred%5Bsorted_idx%5D%0A%20%20%20%20%20%20%20%20y_sorted%20%3D%20y%5Bsorted_idx%5D%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%0A%20%20%20%20%20%20%20%20fig%2C%20ax%20%3D%20plt.subplots(figsize%3D(10%2C%206))%0A%20%20%20%20%20%20%20%20ax.scatter(X%2C%20y%2C%20alpha%3D0.6%2C%20label%3D%22Data%20points%22)%0A%20%20%20%20%20%20%20%20ax.plot(X_sorted%2C%20y_pred_sorted%2C%20color%3D%22red%22%2C%20linewidth%3D2%2C%20label%3D%22Fitted%20line%22)%0A%20%20%20%20%20%20%20%20ax.set_xlabel(%22X%22)%0A%20%20%20%20%20%20%20%20ax.set_ylabel(%22y%22)%0A%20%20%20%20%20%20%20%20ax.legend()%0A%20%20%20%20%20%20%20%20ax.grid(True%2C%20alpha%3D0.3)%0A%20%20%20%20%20%20%20%20ax.set_title(%22Linear%20Regression%20Example%22)%0A%0A%20%20%20%20%20%20%20%20mo.display(fig)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20mo.notification(%22Skipping%20plot%3A%20matplotlib%20is%20not%20available.%22%2C%20kind%3D%22error%22)%0A%20%20%20%20return%20X%2C%20lin_reg%2C%20y%2C%20y_pred%0A%0A%0A%40app.cell%0Adef%20_(lin_reg%2C%20mo)%3A%0A%20%20%20%20mo.md(%22%23%23%20Mathematical%20Foundation%22)%0A%20%20%20%20mo.md(%22The%20linear%20regression%20model%20can%20be%20expressed%20as%3A%22)%0A%20%20%20%20mo.md(%22%24%24y%20%3D%20Xw%20%2B%20b%24%24%22)%0A%20%20%20%20mo.md(%22Where%3A%22)%0A%20%20%20%20mo.md(%22-%20%24y%24%20is%20the%20target%20variable%22)%0A%20%20%20%20mo.md(%22-%20%24X%24%20is%20the%20matrix%20of%20input%20features%22)%0A%20%20%20%20mo.md(%22-%20%24w%24%20is%20the%20vector%20of%20weights%22)%0A%20%20%20%20mo.md(%22-%20%24b%24%20is%20the%20bias%20term%22)%0A%0A%20%20%20%20mo.md(%22In%20our%20example%3A%22)%0A%20%20%20%20mo.md(f%22-%20Weights%20(w)%3A%20%7Blin_reg.coef_%5B0%5D%3A.2f%7D%22)%0A%20%20%20%20mo.md(f%22-%20Bias%20(b)%3A%20%7Blin_reg.intercept_%3A.2f%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20np%2C%20y%2C%20y_pred)%3A%0A%20%20%20%20mo.md(%22%23%23%20Loss%20Function%3A%20Mean%20Squared%20Error%20(MSE)%22)%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%22The%20goal%20of%20linear%20regression%20is%20to%20minimize%20the%20error%20between%20predicted%20and%20actual%20values.%20The%20most%20common%20loss%20function%20is%20Mean%20Squared%20Error%3A%22%0A%20%20%20%20)%0A%20%20%20%20mo.md(%22%24%24%5C%5Ctext%7BMSE%7D%20%3D%20%5C%5Cfrac%7B1%7D%7Bn%7D%20%5C%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%20(y_i%20-%20%5C%5Chat%7By%7D_i)%5E2%24%24%22)%0A%20%20%20%20mo.md(%22Where%3A%22)%0A%20%20%20%20mo.md(%22-%20%24y_i%24%20is%20the%20actual%20value%22)%0A%20%20%20%20mo.md(%22-%20%24%5C%5Chat%7By%7D_i%24%20is%20the%20predicted%20value%22)%0A%20%20%20%20mo.md(%22-%20%24n%24%20is%20the%20number%20of%20samples%22)%0A%0A%20%20%20%20%23%20Calculate%20MSE%0A%20%20%20%20mse%20%3D%20np.mean((y%20-%20y_pred)%20**%202)%0A%20%20%20%20mo.md(f%22In%20our%20example%2C%20the%20MSE%20is%20approximately%3A%20%7Bmse%3A.2f%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X%2C%20mo%2C%20np%2C%20y)%3A%0A%20%20%20%20mo.md(%22%23%23%20How%20Does%20It%20Work%3F%20Gradient%20Descent%22)%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20%22To%20minimize%20the%20MSE%2C%20we%20can%20use%20an%20optimization%20algorithm%20called%20gradient%20descent.%20This%20algorithm%20iteratively%20adjusts%20the%20weights%20and%20bias%20to%20reduce%20the%20loss%20function.%22%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Simple%20implementation%20of%20gradient%20descent%0A%20%20%20%20def%20gradient_descent(X%2C%20y%2C%20learning_rate%3D0.01%2C%20iterations%3D1000)%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20len(X)%0A%20%20%20%20%20%20%20%20w%20%3D%20np.random.randn(1)%0A%20%20%20%20%20%20%20%20b%20%3D%200.0%0A%0A%20%20%20%20%20%20%20%20%23%20Reshape%20X%0A%20%20%20%20%20%20%20%20x_vec%20%3D%20X.ravel()%0A%20%20%20%20%20%20%20%20for%20i%20in%20range(iterations)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20y_pred%20%3D%20x_vec%20*%20w%20%2B%20b%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Compute%20gradients%0A%20%20%20%20%20%20%20%20%20%20%20%20dw%20%3D%20(-2%20%2F%20m)%20*%20np.sum(x_vec%20*%20(y%20-%20y_pred))%0A%20%20%20%20%20%20%20%20%20%20%20%20db%20%3D%20(-2%20%2F%20m)%20*%20np.sum(y%20-%20y_pred)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20w%20-%3D%20learning_rate%20*%20dw%0A%20%20%20%20%20%20%20%20%20%20%20%20b%20-%3D%20learning_rate%20*%20db%0A%0A%20%20%20%20%20%20%20%20return%20w%2C%20b%0A%0A%20%20%20%20w_gd%2C%20b_gd%20%3D%20gradient_descent(X%2C%20y)%0A%20%20%20%20y_pred_gd%20%3D%20X.ravel()%20*%20w_gd%20%2B%20b_gd%0A%0A%20%20%20%20mse_gd%20%3D%20np.mean((y%20-%20y_pred_gd)%20**%202)%0A%0A%20%20%20%20mo.md(%22Using%20gradient%20descent%20(learning%20rate%3D0.01%2C%201000%20iterations)%3A%22)%0A%20%20%20%20mo.md(f%22-%20Weights%20(w)%3A%20%7Bw_gd%5B0%5D%3A.2f%7D%22)%0A%20%20%20%20mo.md(f%22-%20Bias%20(b)%3A%20%7Bb_gd%3A.2f%7D%22)%0A%20%20%20%20mo.md(f%22-%20MSE%3A%20%7Bmse_gd%3A.2f%7D%22)%0A%20%20%20%20return%20(y_pred_gd%2C)%0A%0A%0A%40app.cell%0Adef%20_(X%2C%20mo%2C%20np%2C%20plt%2C%20y%2C%20y_pred%2C%20y_pred_gd)%3A%0A%20%20%20%20mo.md(%22%23%23%20Visualizing%20the%20Process%22)%0A%20%20%20%20if%20plt%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20fig%2C%20ax%20%3D%20plt.subplots(figsize%3D(10%2C%206))%0A%20%20%20%20%20%20%20%20ax.scatter(X%2C%20y%2C%20alpha%3D0.6%2C%20label%3D%22Data%20points%22)%0A%0A%20%20%20%20%20%20%20%20%23%20For%20clean%20lines%2C%20sort%20the%20X%20for%20both%20predictions%0A%20%20%20%20%20%20%20%20sorted_idx%20%3D%20np.argsort(X%5B%3A%2C%200%5D)%0A%20%20%20%20%20%20%20%20X_sorted%20%3D%20X%5Bsorted_idx%5D%0A%20%20%20%20%20%20%20%20y_pred_sorted%20%3D%20y_pred%5Bsorted_idx%5D%0A%20%20%20%20%20%20%20%20y_pred_gd_sorted%20%3D%20y_pred_gd%5Bsorted_idx%5D%0A%0A%20%20%20%20%20%20%20%20ax.plot(X_sorted%2C%20y_pred_sorted%2C%20color%3D%22red%22%2C%20linewidth%3D2%2C%20label%3D%22Sklearn%20fit%22)%0A%20%20%20%20%20%20%20%20ax.plot(X_sorted%2C%20y_pred_gd_sorted%2C%20color%3D%22green%22%2C%20linewidth%3D2%2C%20linestyle%3D%22--%22%2C%20label%3D%22Gradient%20descent%22)%0A%20%20%20%20%20%20%20%20ax.set_xlabel(%22X%22)%0A%20%20%20%20%20%20%20%20ax.set_ylabel(%22y%22)%0A%20%20%20%20%20%20%20%20ax.legend()%0A%20%20%20%20%20%20%20%20ax.grid(True%2C%20alpha%3D0.3)%0A%20%20%20%20%20%20%20%20ax.set_title(%22Comparison%3A%20Sklearn%20vs%20Gradient%20Descent%22)%0A%20%20%20%20%20%20%20%20mo.display(fig)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20mo.notification(%22Skipping%20plot%3A%20matplotlib%20is%20not%20available.%22%2C%20kind%3D%22error%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(%22%23%23%20Key%20Takeaways%22)%0A%20%20%20%20mo.md(%221.%20Linear%20regression%20models%20the%20relationship%20between%20variables%20using%20a%20linear%20equation.%22)%0A%20%20%20%20mo.md(%222.%20The%20parameters%20(weights%20and%20bias)%20are%20learned%20by%20minimizing%20the%20loss%20function.%22)%0A%20%20%20%20mo.md(%223.%20Mean%20Squared%20Error%20(MSE)%20is%20a%20common%20loss%20function.%22)%0A%20%20%20%20mo.md(%224.%20Gradient%20descent%20is%20commonly%20used%20to%20optimize%20parameters.%22)%0A%20%20%20%20mo.md(%225.%20Even%20simple%20models%20like%20linear%20regression%20can%20be%20very%20effective!%22)%0A%0A%20%20%20%20mo.md(%22---%22)%0A%20%20%20%20mo.md(%22**Try%20modifying%20the%20data%2C%20learning%20rate%2C%20or%20number%20of%20iterations%20above%20to%20see%20how%20the%20model%20changes!**%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
|
| 301 |
+
</marimo-code>
|
| 302 |
+
|
| 303 |
+
<marimo-code-hash hidden="">a84b1545db77625ca9cf9d6dc8c1e9c1</marimo-code-hash>
|
| 304 |
+
</body>
|
| 305 |
+
</html>
|
openenv_explainer_env.egg-info/PKG-INFO
CHANGED
|
@@ -3,12 +3,22 @@ Name: openenv-explainer_env
|
|
| 3 |
Version: 0.1.0
|
| 4 |
Summary: Interactive Explainer OpenEnv
|
| 5 |
Requires-Python: >=3.10
|
| 6 |
-
Requires-Dist: openenv-core[core]>=0.2.
|
| 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 |
Provides-Extra: dev
|
| 13 |
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 14 |
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
|
| 3 |
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"
|
openenv_explainer_env.egg-info/SOURCES.txt
CHANGED
|
@@ -14,6 +14,10 @@ openenv_explainer_env.egg-info/dependency_links.txt
|
|
| 14 |
openenv_explainer_env.egg-info/entry_points.txt
|
| 15 |
openenv_explainer_env.egg-info/requires.txt
|
| 16 |
openenv_explainer_env.egg-info/top_level.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
rewards/__init__.py
|
| 18 |
rewards/exploration.py
|
| 19 |
rewards/generation.py
|
|
|
|
| 14 |
openenv_explainer_env.egg-info/entry_points.txt
|
| 15 |
openenv_explainer_env.egg-info/requires.txt
|
| 16 |
openenv_explainer_env.egg-info/top_level.txt
|
| 17 |
+
research/__init__.py
|
| 18 |
+
research/retrieval.py
|
| 19 |
+
research/router.py
|
| 20 |
+
research/types.py
|
| 21 |
rewards/__init__.py
|
| 22 |
rewards/exploration.py
|
| 23 |
rewards/generation.py
|
openenv_explainer_env.egg-info/requires.txt
CHANGED
|
@@ -1,9 +1,19 @@
|
|
| 1 |
-
openenv-core[core]>=0.2.
|
| 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 |
|
| 8 |
[dev]
|
| 9 |
pytest>=8.0.0
|
|
|
|
| 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
|
pyproject.toml
CHANGED
|
@@ -8,12 +8,22 @@ version = "0.1.0"
|
|
| 8 |
description = "Interactive Explainer OpenEnv"
|
| 9 |
requires-python = ">=3.10"
|
| 10 |
dependencies = [
|
| 11 |
-
"openenv-core[core]>=0.2.
|
| 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 |
]
|
| 18 |
|
| 19 |
[project.optional-dependencies]
|
|
@@ -27,8 +37,8 @@ server = "explainer_env.server.app:main"
|
|
| 27 |
|
| 28 |
[tool.setuptools]
|
| 29 |
include-package-data = true
|
| 30 |
-
packages = ["explainer_env", "explainer_env.server", "explainer_env.rewards"]
|
| 31 |
-
package-dir = { "explainer_env" = ".", "explainer_env.server" = "server", "explainer_env.rewards" = "rewards" }
|
| 32 |
|
| 33 |
[dependency-groups]
|
| 34 |
dev = []
|
|
|
|
| 8 |
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]
|
|
|
|
| 37 |
|
| 38 |
[tool.setuptools]
|
| 39 |
include-package-data = true
|
| 40 |
+
packages = ["explainer_env", "explainer_env.server", "explainer_env.rewards", "explainer_env.research"]
|
| 41 |
+
package-dir = { "explainer_env" = ".", "explainer_env.server" = "server", "explainer_env.rewards" = "rewards", "explainer_env.research" = "research" }
|
| 42 |
|
| 43 |
[dependency-groups]
|
| 44 |
dev = []
|
research/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Research tools for the explainer environment."""
|
| 2 |
+
|
| 3 |
+
from .router import AVAILABLE_TOOLS, run_research_tool
|
| 4 |
+
from .types import ResearchChunk, ResearchResult
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"AVAILABLE_TOOLS",
|
| 8 |
+
"ResearchChunk",
|
| 9 |
+
"ResearchResult",
|
| 10 |
+
"run_research_tool",
|
| 11 |
+
]
|
research/retrieval.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 21 |
+
"a",
|
| 22 |
+
"an",
|
| 23 |
+
"is",
|
| 24 |
+
"are",
|
| 25 |
+
"was",
|
| 26 |
+
"were",
|
| 27 |
+
"be",
|
| 28 |
+
"been",
|
| 29 |
+
"being",
|
| 30 |
+
"have",
|
| 31 |
+
"has",
|
| 32 |
+
"had",
|
| 33 |
+
"do",
|
| 34 |
+
"does",
|
| 35 |
+
"did",
|
| 36 |
+
"will",
|
| 37 |
+
"would",
|
| 38 |
+
"could",
|
| 39 |
+
"should",
|
| 40 |
+
"to",
|
| 41 |
+
"of",
|
| 42 |
+
"in",
|
| 43 |
+
"for",
|
| 44 |
+
"on",
|
| 45 |
+
"with",
|
| 46 |
+
"at",
|
| 47 |
+
"by",
|
| 48 |
+
"from",
|
| 49 |
+
"as",
|
| 50 |
+
"and",
|
| 51 |
+
"but",
|
| 52 |
+
"or",
|
| 53 |
+
"this",
|
| 54 |
+
"that",
|
| 55 |
+
"these",
|
| 56 |
+
"those",
|
| 57 |
+
"it",
|
| 58 |
+
"its",
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def tokenize(text: str) -> list[str]:
|
| 63 |
+
"""Lowercase alphanumeric tokenization, stop words removed."""
|
| 64 |
+
return [
|
| 65 |
+
word
|
| 66 |
+
for word in re.findall(r"\w+", text.lower())
|
| 67 |
+
if word not in _STOP_WORDS and len(word) > 1
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def trim_text(text: str, max_chars: int = SECTION_MAX_CHARS) -> str:
|
| 72 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 73 |
+
return text[:max_chars].strip()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def chunk_markdown(text: str, fallback_title: str) -> list[tuple[str, str]]:
|
| 77 |
+
"""Split markdown-ish text into titled chunks."""
|
| 78 |
+
chunks: list[tuple[str, str]] = []
|
| 79 |
+
heading = fallback_title
|
| 80 |
+
lines: list[str] = []
|
| 81 |
+
|
| 82 |
+
for line in text.splitlines():
|
| 83 |
+
if line.startswith("#"):
|
| 84 |
+
body = "\n".join(lines).strip()
|
| 85 |
+
if body:
|
| 86 |
+
chunks.append((heading, body))
|
| 87 |
+
heading = line.lstrip("#").strip() or fallback_title
|
| 88 |
+
lines = []
|
| 89 |
+
else:
|
| 90 |
+
lines.append(line)
|
| 91 |
+
|
| 92 |
+
body = "\n".join(lines).strip()
|
| 93 |
+
if body:
|
| 94 |
+
chunks.append((heading, body))
|
| 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:
|
| 174 |
+
numerator = sum(float(x) * float(y) for x, y in zip(a, b))
|
| 175 |
+
a_norm = math.sqrt(sum(float(x) * float(x) for x in a))
|
| 176 |
+
b_norm = math.sqrt(sum(float(y) * float(y) for y in b))
|
| 177 |
+
if a_norm == 0 or b_norm == 0:
|
| 178 |
+
return 0.0
|
| 179 |
+
return numerator / (a_norm * b_norm)
|
research/router.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit research tools used by the explore phase."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import html
|
| 7 |
+
import re
|
| 8 |
+
import xml.etree.ElementTree as ET
|
| 9 |
+
from typing import Callable, Awaitable
|
| 10 |
+
|
| 11 |
+
import httpx
|
| 12 |
+
import wikipediaapi
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from .. import constants as _constants
|
| 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
|
| 23 |
+
|
| 24 |
+
MAX_SOURCE_RESULTS = 5
|
| 25 |
+
MAX_RETURNED_CHUNKS = 5
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def run_research_tool(tool: str, query: str, intent: str = "") -> ResearchResult:
|
| 29 |
+
"""Run a named research tool and return structured chunks."""
|
| 30 |
+
tool = tool.strip()
|
| 31 |
+
query = query.strip()
|
| 32 |
+
if tool not in _TOOL_RUNNERS:
|
| 33 |
+
return ResearchResult(tool=tool or "(missing)", query=query, error="Unknown research tool")
|
| 34 |
+
if not query:
|
| 35 |
+
return ResearchResult(tool=tool, query=query, error="Empty query")
|
| 36 |
+
return await _TOOL_RUNNERS[tool](query, intent)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
async def search_wikipedia(query: str, intent: str = "") -> ResearchResult:
|
| 40 |
+
try:
|
| 41 |
+
wiki = wikipediaapi.AsyncWikipedia(
|
| 42 |
+
user_agent="ExplainerEnv/1.0 (hackathon project)",
|
| 43 |
+
language="en",
|
| 44 |
+
)
|
| 45 |
+
search_results = await wiki.search(query, limit=MAX_SOURCE_RESULTS)
|
| 46 |
+
titles = list(search_results.pages) if search_results and search_results.pages else []
|
| 47 |
+
chunks: list[ResearchChunk] = []
|
| 48 |
+
|
| 49 |
+
for title in titles:
|
| 50 |
+
page = wiki.page(title)
|
| 51 |
+
if not await page.exists():
|
| 52 |
+
continue
|
| 53 |
+
summary = await page.summary
|
| 54 |
+
sections = await page.sections
|
| 55 |
+
docs: list[tuple[str, str]] = []
|
| 56 |
+
if summary:
|
| 57 |
+
docs.append((title, summary))
|
| 58 |
+
docs.extend(_flatten_wikipedia_sections(sections))
|
| 59 |
+
|
| 60 |
+
for section_title, text in docs:
|
| 61 |
+
snippet = trim_text(text)
|
| 62 |
+
if snippet:
|
| 63 |
+
chunks.append(
|
| 64 |
+
ResearchChunk(
|
| 65 |
+
source="wikipedia",
|
| 66 |
+
tool="search_wikipedia",
|
| 67 |
+
title=f"{title} - {section_title}",
|
| 68 |
+
url=f"https://en.wikipedia.org/wiki/{title.replace(' ', '_')}",
|
| 69 |
+
text=snippet,
|
| 70 |
+
metadata={"page": title},
|
| 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))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
async def search_hf_papers(query: str, intent: str = "") -> ResearchResult:
|
| 81 |
+
try:
|
| 82 |
+
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
|
| 83 |
+
resp = await client.get(
|
| 84 |
+
"https://huggingface.co/api/papers/search",
|
| 85 |
+
params={"q": query, "limit": MAX_SOURCE_RESULTS},
|
| 86 |
+
headers={"User-Agent": "ExplainerEnv/1.0"},
|
| 87 |
+
)
|
| 88 |
+
resp.raise_for_status()
|
| 89 |
+
papers = resp.json() or []
|
| 90 |
+
chunks: list[ResearchChunk] = []
|
| 91 |
+
|
| 92 |
+
for paper in papers[:MAX_SOURCE_RESULTS]:
|
| 93 |
+
paper_id = paper.get("id", "")
|
| 94 |
+
title = paper.get("title", "Untitled")
|
| 95 |
+
url = f"https://huggingface.co/papers/{paper_id}" if paper_id else ""
|
| 96 |
+
text_chunks = [("Abstract", paper.get("summary", ""))]
|
| 97 |
+
|
| 98 |
+
if paper_id:
|
| 99 |
+
md_resp = await client.get(
|
| 100 |
+
f"https://huggingface.co/papers/{paper_id}.md",
|
| 101 |
+
headers={"User-Agent": "ExplainerEnv/1.0"},
|
| 102 |
+
)
|
| 103 |
+
if md_resp.status_code == 200 and md_resp.text.strip():
|
| 104 |
+
text_chunks = chunk_markdown(md_resp.text, "Abstract")
|
| 105 |
+
|
| 106 |
+
for section_title, text in text_chunks:
|
| 107 |
+
snippet = trim_text(text)
|
| 108 |
+
if snippet:
|
| 109 |
+
chunks.append(
|
| 110 |
+
ResearchChunk(
|
| 111 |
+
source="hf_papers",
|
| 112 |
+
tool="search_hf_papers",
|
| 113 |
+
title=f"{title} - {section_title}",
|
| 114 |
+
url=url,
|
| 115 |
+
text=snippet,
|
| 116 |
+
metadata={"paper_id": paper_id},
|
| 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))
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
async def search_arxiv(query: str, intent: str = "") -> ResearchResult:
|
| 127 |
+
try:
|
| 128 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 129 |
+
resp = await client.get(
|
| 130 |
+
"https://export.arxiv.org/api/query",
|
| 131 |
+
params={
|
| 132 |
+
"search_query": f"all:{query}",
|
| 133 |
+
"start": 0,
|
| 134 |
+
"max_results": MAX_SOURCE_RESULTS,
|
| 135 |
+
"sortBy": "relevance",
|
| 136 |
+
"sortOrder": "descending",
|
| 137 |
+
},
|
| 138 |
+
headers={"User-Agent": "ExplainerEnv/1.0"},
|
| 139 |
+
)
|
| 140 |
+
resp.raise_for_status()
|
| 141 |
+
|
| 142 |
+
root = ET.fromstring(resp.text)
|
| 143 |
+
ns = {"atom": "http://www.w3.org/2005/Atom", "arxiv": "http://arxiv.org/schemas/atom"}
|
| 144 |
+
chunks: list[ResearchChunk] = []
|
| 145 |
+
for entry in root.findall("atom:entry", ns):
|
| 146 |
+
title = _xml_text(entry, "atom:title", ns) or "Untitled"
|
| 147 |
+
summary = _xml_text(entry, "atom:summary", ns)
|
| 148 |
+
url = _xml_text(entry, "atom:id", ns)
|
| 149 |
+
published = _xml_text(entry, "atom:published", ns)
|
| 150 |
+
authors = [
|
| 151 |
+
_xml_text(author, "atom:name", ns)
|
| 152 |
+
for author in entry.findall("atom:author", ns)
|
| 153 |
+
]
|
| 154 |
+
categories = [
|
| 155 |
+
cat.attrib.get("term", "")
|
| 156 |
+
for cat in entry.findall("atom:category", ns)
|
| 157 |
+
if cat.attrib.get("term")
|
| 158 |
+
]
|
| 159 |
+
snippet = trim_text(summary)
|
| 160 |
+
if snippet:
|
| 161 |
+
chunks.append(
|
| 162 |
+
ResearchChunk(
|
| 163 |
+
source="arxiv",
|
| 164 |
+
tool="search_arxiv",
|
| 165 |
+
title=html.unescape(title),
|
| 166 |
+
url=url,
|
| 167 |
+
text=html.unescape(snippet),
|
| 168 |
+
metadata={
|
| 169 |
+
"published": published,
|
| 170 |
+
"authors": [a for a in authors if a],
|
| 171 |
+
"categories": categories,
|
| 172 |
+
},
|
| 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))
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
async def search_scholar(query: str, intent: str = "") -> ResearchResult:
|
| 183 |
+
try:
|
| 184 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 185 |
+
resp = await client.get(
|
| 186 |
+
"https://api.semanticscholar.org/graph/v1/paper/search",
|
| 187 |
+
params={
|
| 188 |
+
"query": query,
|
| 189 |
+
"limit": MAX_SOURCE_RESULTS,
|
| 190 |
+
"fields": "title,abstract,url,year,citationCount,venue,authors",
|
| 191 |
+
},
|
| 192 |
+
headers={"User-Agent": "ExplainerEnv/1.0"},
|
| 193 |
+
)
|
| 194 |
+
resp.raise_for_status()
|
| 195 |
+
papers = resp.json().get("data", [])
|
| 196 |
+
|
| 197 |
+
chunks: list[ResearchChunk] = []
|
| 198 |
+
for paper in papers:
|
| 199 |
+
abstract = paper.get("abstract") or ""
|
| 200 |
+
snippet = trim_text(abstract)
|
| 201 |
+
if not snippet:
|
| 202 |
+
continue
|
| 203 |
+
chunks.append(
|
| 204 |
+
ResearchChunk(
|
| 205 |
+
source="semantic_scholar",
|
| 206 |
+
tool="search_scholar",
|
| 207 |
+
title=paper.get("title", "Untitled"),
|
| 208 |
+
url=paper.get("url", ""),
|
| 209 |
+
text=snippet,
|
| 210 |
+
metadata={
|
| 211 |
+
"year": paper.get("year"),
|
| 212 |
+
"venue": paper.get("venue"),
|
| 213 |
+
"citation_count": paper.get("citationCount", 0),
|
| 214 |
+
},
|
| 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))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
async def fetch_docs(query: str, intent: str = "") -> ResearchResult:
|
| 225 |
+
urls = _select_doc_urls(query)
|
| 226 |
+
if not urls:
|
| 227 |
+
return ResearchResult("fetch_docs", query, error="No allowed documentation target matched query")
|
| 228 |
+
|
| 229 |
+
chunks: list[ResearchChunk] = []
|
| 230 |
+
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
|
| 231 |
+
for title, url in urls[:MAX_SOURCE_RESULTS]:
|
| 232 |
+
try:
|
| 233 |
+
resp = await client.get(url, headers={"User-Agent": "ExplainerEnv/1.0"})
|
| 234 |
+
resp.raise_for_status()
|
| 235 |
+
text = _extract_page_text(resp.text)
|
| 236 |
+
except Exception:
|
| 237 |
+
continue
|
| 238 |
+
for section_title, body in chunk_markdown(text, title):
|
| 239 |
+
snippet = trim_text(body)
|
| 240 |
+
if snippet:
|
| 241 |
+
chunks.append(
|
| 242 |
+
ResearchChunk(
|
| 243 |
+
source="docs",
|
| 244 |
+
tool="fetch_docs",
|
| 245 |
+
title=f"{title} - {section_title}",
|
| 246 |
+
url=url,
|
| 247 |
+
text=snippet,
|
| 248 |
+
metadata={"doc": title},
|
| 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 |
+
|
| 256 |
+
async def search_hf_hub(query: str, intent: str = "") -> ResearchResult:
|
| 257 |
+
try:
|
| 258 |
+
from huggingface_hub import HfApi
|
| 259 |
+
except Exception as exc:
|
| 260 |
+
return ResearchResult("search_hf_hub", query, error=f"huggingface_hub unavailable: {exc}")
|
| 261 |
+
|
| 262 |
+
def _load() -> list[ResearchChunk]:
|
| 263 |
+
api = HfApi()
|
| 264 |
+
chunks: list[ResearchChunk] = []
|
| 265 |
+
|
| 266 |
+
for model in api.list_models(search=query, limit=2):
|
| 267 |
+
text = " ".join(str(x) for x in [model.modelId, model.pipeline_tag, model.tags] if x)
|
| 268 |
+
chunks.append(
|
| 269 |
+
ResearchChunk(
|
| 270 |
+
source="hf_hub_model",
|
| 271 |
+
tool="search_hf_hub",
|
| 272 |
+
title=model.modelId,
|
| 273 |
+
url=f"https://huggingface.co/{model.modelId}",
|
| 274 |
+
text=trim_text(text),
|
| 275 |
+
metadata={"downloads": getattr(model, "downloads", None)},
|
| 276 |
+
)
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
for dataset in api.list_datasets(search=query, limit=2):
|
| 280 |
+
dataset_id = getattr(dataset, "id", "")
|
| 281 |
+
text = " ".join(str(x) for x in [dataset_id, getattr(dataset, "tags", None)] if x)
|
| 282 |
+
chunks.append(
|
| 283 |
+
ResearchChunk(
|
| 284 |
+
source="hf_hub_dataset",
|
| 285 |
+
tool="search_hf_hub",
|
| 286 |
+
title=dataset_id,
|
| 287 |
+
url=f"https://huggingface.co/datasets/{dataset_id}",
|
| 288 |
+
text=trim_text(text),
|
| 289 |
+
metadata={"downloads": getattr(dataset, "downloads", None)},
|
| 290 |
+
)
|
| 291 |
+
)
|
| 292 |
+
return chunks
|
| 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))
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
_TOOL_RUNNERS: dict[str, Callable[[str, str], Awaitable[ResearchResult]]] = {
|
| 303 |
+
"search_wikipedia": search_wikipedia,
|
| 304 |
+
"search_hf_papers": search_hf_papers,
|
| 305 |
+
"search_arxiv": search_arxiv,
|
| 306 |
+
"search_scholar": search_scholar,
|
| 307 |
+
"fetch_docs": fetch_docs,
|
| 308 |
+
"search_hf_hub": search_hf_hub,
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
_SKIP_SECTIONS = frozenset({
|
| 312 |
+
"references",
|
| 313 |
+
"external links",
|
| 314 |
+
"see also",
|
| 315 |
+
"further reading",
|
| 316 |
+
"notes",
|
| 317 |
+
"citations",
|
| 318 |
+
"bibliography",
|
| 319 |
+
"sources",
|
| 320 |
+
})
|
| 321 |
+
|
| 322 |
+
_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"),
|
| 329 |
+
("Manim reference", "https://docs.manim.community/en/stable/reference.html"),
|
| 330 |
+
],
|
| 331 |
+
"numpy": [("NumPy user guide", "https://numpy.org/doc/stable/user/")],
|
| 332 |
+
"scipy": [("SciPy user guide", "https://docs.scipy.org/doc/scipy/tutorial/")],
|
| 333 |
+
"sklearn": [("scikit-learn user guide", "https://scikit-learn.org/stable/user_guide.html")],
|
| 334 |
+
"pandas": [("pandas user guide", "https://pandas.pydata.org/docs/user_guide/")],
|
| 335 |
+
"pytorch": [("PyTorch docs", "https://pytorch.org/docs/stable/index.html")],
|
| 336 |
+
"plotly": [("Plotly Python docs", "https://plotly.com/python/")],
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _flatten_wikipedia_sections(sections, max_depth: int = 2, depth: int = 0) -> list[tuple[str, str]]:
|
| 341 |
+
result: list[tuple[str, str]] = []
|
| 342 |
+
for section in sections:
|
| 343 |
+
if section.title.lower() in _SKIP_SECTIONS:
|
| 344 |
+
continue
|
| 345 |
+
if section.text.strip():
|
| 346 |
+
result.append((section.title, section.text.strip()))
|
| 347 |
+
if depth < max_depth and section.sections:
|
| 348 |
+
result.extend(_flatten_wikipedia_sections(section.sections, max_depth, depth + 1))
|
| 349 |
+
return result
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _xml_text(node, path: str, ns: dict[str, str]) -> str:
|
| 353 |
+
found = node.find(path, ns)
|
| 354 |
+
return found.text.strip() if found is not None and found.text else ""
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def _select_doc_urls(query: str) -> list[tuple[str, str]]:
|
| 358 |
+
lower = query.lower()
|
| 359 |
+
selected: list[tuple[str, str]] = []
|
| 360 |
+
for key, urls in _DOC_URLS.items():
|
| 361 |
+
if key in lower or key.replace("sklearn", "scikit-learn") in lower:
|
| 362 |
+
selected.extend(urls)
|
| 363 |
+
if not selected and any(term in lower for term in ("notebook", "animation", "plot", "chart", "lint")):
|
| 364 |
+
selected.extend(_DOC_URLS["marimo"])
|
| 365 |
+
selected.extend(_DOC_URLS["manim"])
|
| 366 |
+
selected.extend(_DOC_URLS["plotly"])
|
| 367 |
+
return selected
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _extract_page_text(markup: str) -> str:
|
| 371 |
+
try:
|
| 372 |
+
import trafilatura
|
| 373 |
+
|
| 374 |
+
extracted = trafilatura.extract(markup, output_format="markdown")
|
| 375 |
+
if extracted:
|
| 376 |
+
return extracted
|
| 377 |
+
except Exception:
|
| 378 |
+
pass
|
| 379 |
+
text = re.sub(r"<(script|style).*?</\1>", " ", markup, flags=re.DOTALL | re.IGNORECASE)
|
| 380 |
+
text = re.sub(r"<[^>]+>", " ", text)
|
| 381 |
+
return html.unescape(re.sub(r"\s+", " ", text))
|
research/types.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Typed research results shared by exploration tools and rewards."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class ResearchChunk:
|
| 11 |
+
"""A ranked passage returned by a research tool."""
|
| 12 |
+
|
| 13 |
+
source: str
|
| 14 |
+
tool: str
|
| 15 |
+
title: str
|
| 16 |
+
url: str
|
| 17 |
+
text: str
|
| 18 |
+
score: float = 0.0
|
| 19 |
+
rank: int = 0
|
| 20 |
+
metadata: dict[str, Any] = field(default_factory=dict)
|
| 21 |
+
|
| 22 |
+
@property
|
| 23 |
+
def snippet(self) -> str:
|
| 24 |
+
return self.text
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class ResearchResult:
|
| 29 |
+
"""Structured response from a research tool."""
|
| 30 |
+
|
| 31 |
+
tool: str
|
| 32 |
+
query: str
|
| 33 |
+
chunks: list[ResearchChunk] = field(default_factory=list)
|
| 34 |
+
error: str = ""
|
| 35 |
+
raw_count: int = 0
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def ok(self) -> bool:
|
| 39 |
+
return not self.error and bool(self.chunks)
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def text(self) -> str:
|
| 43 |
+
return "\n\n".join(chunk.text for chunk in self.chunks)
|
| 44 |
+
|
| 45 |
+
@property
|
| 46 |
+
def sources(self) -> set[str]:
|
| 47 |
+
return {chunk.source for chunk in self.chunks}
|
| 48 |
+
|
| 49 |
+
def render(self) -> str:
|
| 50 |
+
"""Render compact context for the agent."""
|
| 51 |
+
if self.error:
|
| 52 |
+
return f"{self.tool} error: {self.error}"
|
| 53 |
+
if not self.chunks:
|
| 54 |
+
return f"No results for {self.tool}: {self.query}"
|
| 55 |
+
|
| 56 |
+
parts = []
|
| 57 |
+
for chunk in self.chunks:
|
| 58 |
+
url = f"\nURL: {chunk.url}" if chunk.url else ""
|
| 59 |
+
parts.append(
|
| 60 |
+
f"[{chunk.rank}] {chunk.source}: {chunk.title}{url}\n{chunk.text}"
|
| 61 |
+
)
|
| 62 |
+
return "\n\n---\n\n".join(parts)
|
rewards/README.md
CHANGED
|
@@ -1,107 +1,112 @@
|
|
| 1 |
# Rewards
|
| 2 |
|
| 3 |
-
Multi-component reward system for the
|
| 4 |
|
| 5 |
## Episode Flow
|
| 6 |
|
| 7 |
```
|
| 8 |
-
reset()
|
| 9 |
```
|
| 10 |
|
| 11 |
-
Each step returns a per-step reward. The agent learns
|
| 12 |
|
| 13 |
## Exploration Rewards (`exploration.py`)
|
| 14 |
|
| 15 |
-
Per-step reward for each `explore` action. Gated by information need
|
| 16 |
|
| 17 |
| Component | Weight | Range | Description |
|
| 18 |
|---|---|---|---|
|
| 19 |
-
| `
|
| 20 |
-
| `
|
| 21 |
-
| `
|
| 22 |
-
| `
|
| 23 |
-
| `
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
**Gating mechanism**: `info_need = 1 - sufficiency`. Raw reward is scaled by `0.3 + 0.7 * info_need`, so high sufficiency
|
| 26 |
|
| 27 |
## Generation Rewards (`generation.py`)
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
| Component | Weight | Range | Description |
|
| 32 |
|---|---|---|---|
|
| 33 |
-
| `
|
| 34 |
-
| `
|
| 35 |
-
| `
|
| 36 |
-
| `
|
| 37 |
-
| `
|
| 38 |
-
| `narration` | 0.10* | 0–1 | Narration quality (manim only; words, scene markers) |
|
| 39 |
-
| `context_usage` | 0.20 | 0–1 | Code references terms from exploration research |
|
| 40 |
|
| 41 |
-
*For marimo format, narration weight (0.
|
| 42 |
|
| 43 |
-
|
| 44 |
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
|
| 49 |
-
|
|
| 50 |
-
|---|---|---|
|
| 51 |
-
|
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
|
| 56 |
-
**
|
| 57 |
|
| 58 |
-
##
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
| 63 |
-
|
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
##
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|---|---|
|
| 76 |
-
| Clarity | Is the concept explained clearly for the target tier? |
|
| 77 |
-
| Accuracy | Is the content technically correct? |
|
| 78 |
-
| Engagement | Does the code create an engaging, interactive experience? |
|
| 79 |
-
| Completeness | Does it cover the key aspects of the topic? |
|
| 80 |
-
| Appropriateness | Is the depth appropriate for the audience tier? |
|
| 81 |
-
|
| 82 |
-
### Configuration
|
| 83 |
-
|
| 84 |
-
Set environment variables:
|
| 85 |
-
- `JUDGE_API_URL` (required) — OpenAI-compatible endpoint (e.g. vLLM, ollama, OpenAI)
|
| 86 |
-
- `JUDGE_API_KEY` (optional) — Bearer token for the API
|
| 87 |
-
- `JUDGE_MODEL` (optional, default: `gpt-4o-mini`) — Model to use for judging
|
| 88 |
-
|
| 89 |
-
### Usage
|
| 90 |
-
|
| 91 |
-
```python
|
| 92 |
-
from rewards.llm_judge import judge_explainability, is_available
|
| 93 |
-
|
| 94 |
-
if is_available():
|
| 95 |
-
score, details = judge_explainability(
|
| 96 |
-
code="import marimo as mo\n...",
|
| 97 |
-
topic="Linear Regression",
|
| 98 |
-
tier="beginner",
|
| 99 |
-
fmt="marimo",
|
| 100 |
-
)
|
| 101 |
-
print(f"Explainability score: {score:.2f}")
|
| 102 |
-
print(f"Rationale: {details.get('rationale', '')}")
|
| 103 |
-
```
|
| 104 |
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
|
|
|
| 1 |
# Rewards
|
| 2 |
|
| 3 |
+
Multi-component reward system for the explore -> generate -> repair episode.
|
| 4 |
|
| 5 |
## Episode Flow
|
| 6 |
|
| 7 |
```
|
| 8 |
+
reset() --> [explore x 0..3] --> generate x 1 --> [repair x 0..1] --> done
|
| 9 |
```
|
| 10 |
|
| 11 |
+
Each step returns a per-step reward. The agent learns what tool to use, what to retrieve, when to stop exploring, and how to repair broken artifacts.
|
| 12 |
|
| 13 |
## Exploration Rewards (`exploration.py`)
|
| 14 |
|
| 15 |
+
Per-step reward for each `explore` action. Gated by information need -- once the agent has enough info, further exploration yields diminishing returns.
|
| 16 |
|
| 17 |
| Component | Weight | Range | Description |
|
| 18 |
|---|---|---|---|
|
| 19 |
+
| `tool_choice` | 0.15 | 0-1 | Tool fits task difficulty and query intent |
|
| 20 |
+
| `query_relevance` | 0.20 | 0-1 | Topic, keyword, and intent overlap |
|
| 21 |
+
| `source_quality` | 0.20 | 0-1 | Retrieved chunks have useful metadata, content, and relevance |
|
| 22 |
+
| `coverage_delta` | 0.20 | 0-1 | Newly covered missing concepts/keywords |
|
| 23 |
+
| `result_novelty` | 0.15 | 0-1 | New normalized terms vs. previous context |
|
| 24 |
+
| `diversity` | 0.10 | 0-1 | Useful new source/tool diversity |
|
| 25 |
+
| `step_cost` | -0.05 | flat | Per-step penalty -- exploration must justify itself |
|
| 26 |
|
| 27 |
+
**Gating mechanism**: `info_need = 1 - sufficiency`. Raw reward is scaled by `0.3 + 0.7 * info_need`, so high sufficiency -> low reward for more exploration. This teaches the agent to stop when it has enough.
|
| 28 |
|
| 29 |
## Generation Rewards (`generation.py`)
|
| 30 |
|
| 31 |
+
Reward on `generate` and `repair` actions. Uses **multiplicative gates** instead of additive weights for code validity.
|
| 32 |
+
|
| 33 |
+
### Gates (multiplicative)
|
| 34 |
+
|
| 35 |
+
| Condition | Effect |
|
| 36 |
+
|---|---|
|
| 37 |
+
| Code doesn't parse (AST fails) | total = 0 |
|
| 38 |
+
| Code doesn't execute | total = quality * 0.4 |
|
| 39 |
+
| Code executes successfully | total = quality * 1.0 |
|
| 40 |
+
|
| 41 |
+
### Quality components
|
| 42 |
|
| 43 |
| Component | Weight | Range | Description |
|
| 44 |
|---|---|---|---|
|
| 45 |
+
| `coverage` | 0.20 | 0-1 | Fraction of task keywords in generated code |
|
| 46 |
+
| `format_match` | 0.10 | 0.3/1.0 | Chosen format matches task's preferred format |
|
| 47 |
+
| `structure` | 0.20* | 0-1 | Structural quality (cells/scenes, UI, viz, `marimo check`) |
|
| 48 |
+
| `narration` | 0.15* | 0-1 | Narration quality (manim only; words, scene markers) |
|
| 49 |
+
| `context_usage` | 0.35 | 0-1 | Code references terms from exploration research |
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
*For marimo format, narration weight (0.15) is redistributed to structure (-> 0.35 total).
|
| 52 |
|
| 53 |
+
### Marimo structure scoring
|
| 54 |
|
| 55 |
+
Additive scoring for good patterns:
|
| 56 |
+
- `import marimo` / `marimo.App()` / `@app.cell` count
|
| 57 |
+
- UI elements (`mo.ui.*`, `mo.md(`, etc.)
|
| 58 |
+
- Visualization libraries (`matplotlib`, `plotly`, etc.)
|
| 59 |
+
- Tier-appropriate cell count
|
| 60 |
|
| 61 |
+
Then `marimo check` CLI validates against 5 breaking rules (MB001-MB005). Per-violation penalties:
|
| 62 |
|
| 63 |
+
| Rule | Penalty | What it catches |
|
| 64 |
+
|---|---|---|
|
| 65 |
+
| MB001 | -0.30 | Unparsable cells |
|
| 66 |
+
| MB002 | -0.35 | Duplicate variable definitions across cells |
|
| 67 |
+
| MB003 | -0.40 | Cycle dependencies between cells |
|
| 68 |
+
| MB004 | -0.20 | Invalid setup cell dependencies |
|
| 69 |
+
| MB005 | -0.25 | Syntax errors within cells |
|
| 70 |
|
| 71 |
+
Clean code (no violations) gets +0.1 bonus.
|
| 72 |
|
| 73 |
+
**Skip penalty**: Generating without any exploration incurs -0.1 penalty.
|
| 74 |
|
| 75 |
+
### Repair scoring
|
| 76 |
|
| 77 |
+
If generation fails lint/build validation, the observation enters `repair` and exposes structured errors. One repair attempt is allowed:
|
| 78 |
+
|
| 79 |
+
| Condition | Effect |
|
| 80 |
+
|---|---|
|
| 81 |
+
| First generation succeeds | Full eligible generation reward; episode ends |
|
| 82 |
+
| Repair succeeds | Base generation reward * 0.8, plus a small bonus for fixing prior error codes |
|
| 83 |
+
| Repair fails | Base generation reward * 0.3; episode ends |
|
| 84 |
+
| Code repeated unchanged | Additional penalty |
|
| 85 |
|
| 86 |
+
## Search Sources (`sources.py`)
|
| 87 |
+
|
| 88 |
+
All search calls are **async** (httpx + wikipediaapi.AsyncWikipedia). Content is retrieved at section/chunk level and ranked using **BM25** to surface the most relevant parts.
|
| 89 |
|
| 90 |
+
| Source | Library | Use Case | Retrieval |
|
| 91 |
+
|---|---|---|---|
|
| 92 |
+
| Wikipedia | `wikipediaapi.AsyncWikipedia` | Fundamentals | Search 3-5 pages -> section tree -> global BM25 ranking |
|
| 93 |
+
| HuggingFace Papers | httpx -> `huggingface.co/api/papers/search` | ML/AI research | Search 3-5 papers -> markdown chunks -> global BM25 ranking |
|
| 94 |
+
| arXiv | httpx -> arXiv Atom API | Math, algorithms, ML, statistics papers | Search 3-5 papers -> abstracts -> global BM25 ranking |
|
| 95 |
+
| Semantic Scholar | httpx -> Graph API | Scholarly metadata and abstracts | Search 3-5 papers -> abstracts -> global BM25 ranking |
|
| 96 |
+
| Docs | httpx + trafilatura | API/library/code details | Fetch allowlisted docs -> chunk -> global BM25 ranking |
|
| 97 |
+
| HF Hub | `huggingface_hub.HfApi` | Model cards, datasets, Spaces | Search Hub entities -> metadata/card snippets |
|
| 98 |
|
| 99 |
+
Agents choose tools explicitly. Each tool fetches multiple candidates, chunks them, and returns the top 3-5 chunks globally. Optional small local embeddings can rerank the BM25 shortlist when `EMBEDDINGS_ENABLED=1`.
|
| 100 |
|
| 101 |
+
## Sandbox (`sandbox.py`)
|
| 102 |
|
| 103 |
+
Validation follows a fast-to-slow pipeline. Each stage gates the next.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
+
| Check | Tool | Timeout | Purpose |
|
| 106 |
+
|---|---|---|---|
|
| 107 |
+
| `ast_parses` | Python `ast.parse` | ~0ms | Catch syntax errors |
|
| 108 |
+
| `check_marimo` | `marimo check --format json --select MB` | 8s | Catch structural violations |
|
| 109 |
+
| `run_marimo` | `marimo export html` | 7s | Full execution test |
|
| 110 |
+
| `run_manim` | `manim render -ql` | 30s | Full render test |
|
| 111 |
|
| 112 |
+
`check_marimo` runs in ~100-200ms and catches MB001-MB005. If it fails, `run_marimo` is skipped (saves ~15s per broken submission).
|
rewards/__init__.py
CHANGED
|
@@ -2,15 +2,10 @@
|
|
| 2 |
|
| 3 |
from .exploration import compute_explore_reward
|
| 4 |
from .generation import compute_generate_reward
|
| 5 |
-
from .
|
| 6 |
-
from .sources import search, search_hf_papers, search_wikipedia
|
| 7 |
|
| 8 |
__all__ = [
|
| 9 |
"compute_explore_reward",
|
| 10 |
"compute_generate_reward",
|
| 11 |
-
"run_marimo",
|
| 12 |
-
"run_manim",
|
| 13 |
"search",
|
| 14 |
-
"search_hf_papers",
|
| 15 |
-
"search_wikipedia",
|
| 16 |
]
|
|
|
|
| 2 |
|
| 3 |
from .exploration import compute_explore_reward
|
| 4 |
from .generation import compute_generate_reward
|
| 5 |
+
from .sources import search
|
|
|
|
| 6 |
|
| 7 |
__all__ = [
|
| 8 |
"compute_explore_reward",
|
| 9 |
"compute_generate_reward",
|
|
|
|
|
|
|
| 10 |
"search",
|
|
|
|
|
|
|
| 11 |
]
|
rewards/exploration.py
CHANGED
|
@@ -1,51 +1,67 @@
|
|
| 1 |
-
"""Reward components for the exploration phase.
|
| 2 |
-
|
| 3 |
-
During exploration, the agent searches for papers/resources relevant to the
|
| 4 |
-
task topic. Rewards measure query quality, result relevance, research breadth,
|
| 5 |
-
and exploration efficiency (knowing when to stop).
|
| 6 |
-
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
|
|
|
| 13 |
if not query or not query.strip():
|
| 14 |
return 0.0
|
| 15 |
|
| 16 |
-
query_lower = query.strip().lower()
|
| 17 |
score = 0.0
|
| 18 |
|
| 19 |
if topic.lower() in query_lower:
|
| 20 |
-
score += 0.
|
| 21 |
|
| 22 |
keywords = [k.strip().lower() for k in keywords_csv.split(",") if k.strip()]
|
| 23 |
if keywords:
|
| 24 |
hits = sum(1 for kw in keywords if kw in query_lower)
|
| 25 |
-
score += 0.
|
| 26 |
|
| 27 |
if len(query_lower.split()) >= 3:
|
| 28 |
-
score += 0.
|
| 29 |
|
| 30 |
-
|
|
|
|
| 31 |
|
|
|
|
| 32 |
|
| 33 |
-
def result_novelty(
|
| 34 |
-
new_content: str, accumulated_context: list[str]
|
| 35 |
-
) -> float:
|
| 36 |
-
"""Score how much new information this result adds (0-1).
|
| 37 |
|
| 38 |
-
|
| 39 |
-
"""
|
| 40 |
if not new_content or not new_content.strip():
|
| 41 |
return 0.0
|
| 42 |
-
if not
|
| 43 |
return 1.0
|
| 44 |
|
| 45 |
-
new_words = set(
|
| 46 |
seen_words: set[str] = set()
|
| 47 |
-
for ctx in
|
| 48 |
-
seen_words.update(
|
| 49 |
|
| 50 |
if not new_words:
|
| 51 |
return 0.0
|
|
@@ -57,82 +73,169 @@ def result_novelty(
|
|
| 57 |
def research_breadth(accumulated_context: list[str], min_sources: int = 2) -> float:
|
| 58 |
"""Score whether the agent gathered enough sources (0-1)."""
|
| 59 |
n = len(accumulated_context)
|
| 60 |
-
if n >= min_sources
|
| 61 |
-
return 1.0
|
| 62 |
-
return n / min_sources
|
| 63 |
|
| 64 |
|
| 65 |
def content_sufficiency(
|
| 66 |
-
task_content: str,
|
| 67 |
-
keywords_csv: str,
|
| 68 |
-
accumulated_context: list[str],
|
| 69 |
) -> float:
|
| 70 |
-
"""
|
| 71 |
-
|
| 72 |
-
Combines the task's own content with accumulated research. When this is
|
| 73 |
-
high (>0.8), further exploration has diminishing value — the agent already
|
| 74 |
-
has enough information.
|
| 75 |
-
"""
|
| 76 |
keywords = [k.strip().lower() for k in keywords_csv.split(",") if k.strip()]
|
| 77 |
if not keywords:
|
| 78 |
-
return 1.0
|
| 79 |
|
| 80 |
-
|
| 81 |
-
combined = task_content.lower()
|
| 82 |
for ctx in accumulated_context:
|
| 83 |
-
combined += " " +
|
| 84 |
|
| 85 |
-
|
| 86 |
-
return hits / len(keywords)
|
| 87 |
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
def compute_explore_reward(
|
| 100 |
query: str,
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
topic: str,
|
| 103 |
keywords_csv: str,
|
| 104 |
task_content: str,
|
|
|
|
|
|
|
| 105 |
accumulated_context: list[str],
|
|
|
|
| 106 |
) -> tuple[float, dict]:
|
| 107 |
-
"""Compute per-step exploration reward. Returns (total, components).
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
sufficiency = content_sufficiency(task_content, keywords_csv, accumulated_context)
|
| 117 |
|
| 118 |
-
# Information need: how much value exploration still has
|
| 119 |
info_need = max(0.0, 1.0 - sufficiency)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
| 127 |
total = max(0.0, total)
|
| 128 |
|
| 129 |
components = {
|
|
|
|
| 130 |
"query_relevance": round(q_rel, 3),
|
|
|
|
|
|
|
| 131 |
"result_novelty": round(novelty, 3),
|
| 132 |
-
"
|
|
|
|
| 133 |
"content_sufficiency": round(sufficiency, 3),
|
| 134 |
"info_need": round(info_need, 3),
|
| 135 |
"step_cost": STEP_COST,
|
| 136 |
"explore_total": round(total, 4),
|
| 137 |
}
|
| 138 |
return total, components
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reward components for the exploration phase."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 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
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Individual scorers
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
|
| 28 |
+
|
| 29 |
+
def query_relevance(query: str, topic: str, keywords_csv: str, intent: str = "") -> float:
|
| 30 |
+
"""Score how relevant and specific the search query is to the task (0-1)."""
|
| 31 |
if not query or not query.strip():
|
| 32 |
return 0.0
|
| 33 |
|
| 34 |
+
query_lower = f"{query} {intent}".strip().lower()
|
| 35 |
score = 0.0
|
| 36 |
|
| 37 |
if topic.lower() in query_lower:
|
| 38 |
+
score += 0.35
|
| 39 |
|
| 40 |
keywords = [k.strip().lower() for k in keywords_csv.split(",") if k.strip()]
|
| 41 |
if keywords:
|
| 42 |
hits = sum(1 for kw in keywords if kw in query_lower)
|
| 43 |
+
score += 0.35 * (hits / len(keywords))
|
| 44 |
|
| 45 |
if len(query_lower.split()) >= 3:
|
| 46 |
+
score += 0.15
|
| 47 |
|
| 48 |
+
if any(term in query_lower for term in ("equation", "intuition", "visual", "example", "code")):
|
| 49 |
+
score += 0.15
|
| 50 |
|
| 51 |
+
return min(1.0, score)
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
def result_novelty(new_content: str, previous_context: list[str]) -> float:
|
| 55 |
+
"""Score how much new information this result adds (0-1)."""
|
| 56 |
if not new_content or not new_content.strip():
|
| 57 |
return 0.0
|
| 58 |
+
if not previous_context:
|
| 59 |
return 1.0
|
| 60 |
|
| 61 |
+
new_words = set(tokenize(new_content))
|
| 62 |
seen_words: set[str] = set()
|
| 63 |
+
for ctx in previous_context:
|
| 64 |
+
seen_words.update(tokenize(ctx))
|
| 65 |
|
| 66 |
if not new_words:
|
| 67 |
return 0.0
|
|
|
|
| 73 |
def research_breadth(accumulated_context: list[str], min_sources: int = 2) -> float:
|
| 74 |
"""Score whether the agent gathered enough sources (0-1)."""
|
| 75 |
n = len(accumulated_context)
|
| 76 |
+
return 1.0 if n >= min_sources else n / min_sources
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
def content_sufficiency(
|
| 80 |
+
task_content: str, keywords_csv: str, accumulated_context: list[str],
|
|
|
|
|
|
|
| 81 |
) -> float:
|
| 82 |
+
"""Fraction of task keywords covered in task content + research (0-1)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
keywords = [k.strip().lower() for k in keywords_csv.split(",") if k.strip()]
|
| 84 |
if not keywords:
|
| 85 |
+
return 1.0
|
| 86 |
|
| 87 |
+
combined = _normalized_text(task_content)
|
|
|
|
| 88 |
for ctx in accumulated_context:
|
| 89 |
+
combined += " " + _normalized_text(ctx)
|
| 90 |
|
| 91 |
+
return sum(1 for kw in keywords if _normalized_text(kw) in combined) / len(keywords)
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
+
def tool_choice_score(tool: str, difficulty: str, query: str, intent: str = "") -> float:
|
| 95 |
+
"""Reward selecting a tool that fits the task/source need."""
|
| 96 |
+
text = f"{query} {intent}".lower()
|
| 97 |
+
codeish = any(term in text for term in ("api", "code", "library", "plot", "chart", "lint", "marimo", "manim"))
|
| 98 |
+
paperish = any(term in text for term in ("paper", "research", "recent", "citation", "state of the art"))
|
| 99 |
+
hubish = any(term in text for term in ("model", "dataset", "space", "hugging face", "hf hub"))
|
| 100 |
|
| 101 |
+
if codeish:
|
| 102 |
+
return 1.0 if tool == "fetch_docs" else 0.65
|
| 103 |
+
if hubish:
|
| 104 |
+
return 1.0 if tool == "search_hf_hub" else 0.65
|
| 105 |
+
if difficulty == "hard" or paperish:
|
| 106 |
+
return 1.0 if tool in {"search_arxiv", "search_hf_papers", "search_scholar"} else 0.55
|
| 107 |
+
if difficulty == "medium":
|
| 108 |
+
return 1.0 if tool in {"search_wikipedia", "search_arxiv", "search_scholar"} else 0.75
|
| 109 |
+
return 1.0 if tool in {"search_wikipedia", "fetch_docs"} else 0.65
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def source_quality(result: ResearchResult) -> float:
|
| 113 |
+
"""Measure whether retrieved chunks are usable, not whether the source is trusted."""
|
| 114 |
+
if result.error or not result.chunks:
|
| 115 |
+
return 0.0
|
| 116 |
+
|
| 117 |
+
scores = []
|
| 118 |
+
for chunk in result.chunks:
|
| 119 |
+
metadata = 0.0
|
| 120 |
+
if chunk.title:
|
| 121 |
+
metadata += 0.35
|
| 122 |
+
if chunk.url:
|
| 123 |
+
metadata += 0.35
|
| 124 |
+
if chunk.metadata:
|
| 125 |
+
metadata += 0.30
|
| 126 |
+
|
| 127 |
+
word_count = len(tokenize(chunk.text))
|
| 128 |
+
content = min(1.0, word_count / 80)
|
| 129 |
+
relevance = 1.0 if chunk.score > 0 else 0.5
|
| 130 |
+
scores.append(0.35 * metadata + 0.45 * content + 0.20 * relevance)
|
| 131 |
+
|
| 132 |
+
return min(1.0, sum(scores) / max(len(scores), 1))
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def coverage_delta(
|
| 136 |
+
keywords_csv: str,
|
| 137 |
+
task_content: str,
|
| 138 |
+
previous_context: list[str],
|
| 139 |
+
new_content: str,
|
| 140 |
+
) -> float:
|
| 141 |
+
"""Score newly covered keywords/concepts added by this result."""
|
| 142 |
+
keywords = [k.strip().lower() for k in keywords_csv.split(",") if k.strip()]
|
| 143 |
+
if not keywords:
|
| 144 |
+
return 1.0
|
| 145 |
+
|
| 146 |
+
before = _normalized_text(task_content + " " + " ".join(previous_context))
|
| 147 |
+
after = before + " " + _normalized_text(new_content)
|
| 148 |
+
missing_before = [kw for kw in keywords if _normalized_text(kw) not in before]
|
| 149 |
+
if not missing_before:
|
| 150 |
+
return 0.0
|
| 151 |
+
newly_covered = sum(1 for kw in missing_before if _normalized_text(kw) in after)
|
| 152 |
+
return newly_covered / len(missing_before)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def diversity_score(tool: str, used_tools: set[str], result: ResearchResult) -> float:
|
| 156 |
+
"""Reward useful source diversity without rewarding empty calls."""
|
| 157 |
+
if result.error or not result.chunks:
|
| 158 |
+
return 0.0
|
| 159 |
+
if tool not in used_tools:
|
| 160 |
+
return 1.0
|
| 161 |
+
unique_urls = {chunk.url for chunk in result.chunks if chunk.url}
|
| 162 |
+
return 0.5 if len(unique_urls) > 1 else 0.25
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# ---------------------------------------------------------------------------
|
| 166 |
+
# Gating
|
| 167 |
+
# ---------------------------------------------------------------------------
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _exploration_gate(sufficiency: float) -> float:
|
| 171 |
+
"""Multiplier based on information need.
|
| 172 |
+
|
| 173 |
+
High sufficiency → low multiplier (exploration has little value).
|
| 174 |
+
Low sufficiency → high multiplier (exploration has high value).
|
| 175 |
+
Range: [0.3, 1.0].
|
| 176 |
+
"""
|
| 177 |
+
info_need = max(0.0, 1.0 - sufficiency)
|
| 178 |
+
return 0.3 + 0.7 * info_need
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# ---------------------------------------------------------------------------
|
| 182 |
+
# Main reward function
|
| 183 |
+
# ---------------------------------------------------------------------------
|
| 184 |
|
| 185 |
|
| 186 |
def compute_explore_reward(
|
| 187 |
query: str,
|
| 188 |
+
tool: str,
|
| 189 |
+
intent: str,
|
| 190 |
+
result: ResearchResult,
|
| 191 |
topic: str,
|
| 192 |
keywords_csv: str,
|
| 193 |
task_content: str,
|
| 194 |
+
difficulty: str,
|
| 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
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _normalized_text(text: str) -> str:
|
| 241 |
+
return " ".join(tokenize(text))
|
rewards/generation.py
CHANGED
|
@@ -2,19 +2,56 @@
|
|
| 2 |
|
| 3 |
After exploration, the agent generates marimo/manim code. Rewards measure
|
| 4 |
code quality, execution success, keyword coverage, format match, structural
|
| 5 |
-
quality,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
|
|
|
|
|
|
| 10 |
from typing import TYPE_CHECKING
|
| 11 |
|
| 12 |
-
from .sandbox import ast_parses
|
| 13 |
|
| 14 |
if TYPE_CHECKING:
|
| 15 |
from ..task_bank import Task
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# ---------------------------------------------------------------------------
|
| 19 |
# Individual scorers
|
| 20 |
# ---------------------------------------------------------------------------
|
|
@@ -28,8 +65,7 @@ def keyword_coverage(code: str, keywords_csv: str) -> float:
|
|
| 28 |
if not keywords:
|
| 29 |
return 0.0
|
| 30 |
code_lower = code.lower()
|
| 31 |
-
|
| 32 |
-
return hits / len(keywords)
|
| 33 |
|
| 34 |
|
| 35 |
def format_match(chosen_format: str, task: Task) -> float:
|
|
@@ -43,8 +79,14 @@ def format_match(chosen_format: str, task: Task) -> float:
|
|
| 43 |
|
| 44 |
|
| 45 |
def marimo_structure(code: str, task: Task) -> float:
|
| 46 |
-
"""Score structural quality of a marimo notebook (0-1).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
score = 0.0
|
|
|
|
|
|
|
| 48 |
if "import marimo" in code or "from marimo" in code:
|
| 49 |
score += 0.2
|
| 50 |
if "marimo.App" in code or "mo.App" in code:
|
|
@@ -54,22 +96,28 @@ def marimo_structure(code: str, task: Task) -> float:
|
|
| 54 |
score += 0.2
|
| 55 |
elif cell_count >= 1:
|
| 56 |
score += 0.1
|
|
|
|
| 57 |
ui_patterns = ["mo.ui.", "mo.md(", "mo.Html", "mo.accordion", "mo.callout"]
|
| 58 |
-
|
| 59 |
-
|
| 60 |
viz_patterns = ["plt.", "px.", "altair", "matplotlib", "plotly", "mo.ui.slider"]
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
if task.tier == "advanced" and cell_count >= 6:
|
| 67 |
-
score += 0.1
|
| 68 |
-
elif task.tier == "intermediate" and cell_count >= 4:
|
| 69 |
score += 0.1
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
| 71 |
score += 0.1
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
def manim_structure(code: str, task: Task) -> float:
|
|
@@ -83,37 +131,24 @@ def manim_structure(code: str, task: Task) -> float:
|
|
| 83 |
score += 0.2
|
| 84 |
if "def construct" in code:
|
| 85 |
score += 0.1
|
|
|
|
| 86 |
anim_patterns = [
|
| 87 |
-
"self.play(",
|
| 88 |
-
"
|
| 89 |
-
"Create(",
|
| 90 |
-
"FadeIn(",
|
| 91 |
-
"FadeOut(",
|
| 92 |
-
"Transform(",
|
| 93 |
-
"Write(",
|
| 94 |
-
"MoveToTarget",
|
| 95 |
-
"Indicate(",
|
| 96 |
"ReplacementTransform(",
|
| 97 |
]
|
| 98 |
anim_hits = sum(1 for p in anim_patterns if p in code)
|
| 99 |
score += min(0.3, anim_hits * 0.05)
|
|
|
|
| 100 |
math_patterns = ["MathTex(", "Tex(", "Axes(", "NumberPlane(", "Graph("]
|
| 101 |
-
|
| 102 |
-
if math_hits > 0:
|
| 103 |
-
score += 0.1
|
| 104 |
-
if task.tier == "advanced" and anim_hits >= 6:
|
| 105 |
-
score += 0.1
|
| 106 |
-
elif task.tier == "intermediate" and anim_hits >= 4:
|
| 107 |
-
score += 0.1
|
| 108 |
-
elif task.tier == "beginner" and anim_hits >= 2:
|
| 109 |
score += 0.1
|
| 110 |
-
return min(1.0, score)
|
| 111 |
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
if fmt == "marimo":
|
| 115 |
-
return marimo_structure(code, task)
|
| 116 |
-
return manim_structure(code, task)
|
| 117 |
|
| 118 |
|
| 119 |
def narration_score(narration: str, fmt: str) -> float:
|
|
@@ -122,15 +157,14 @@ def narration_score(narration: str, fmt: str) -> float:
|
|
| 122 |
return 1.0
|
| 123 |
if not narration or not narration.strip():
|
| 124 |
return 0.0
|
| 125 |
-
score = 0.0
|
| 126 |
words = narration.split()
|
|
|
|
| 127 |
if len(words) >= 30:
|
| 128 |
score += 0.4
|
| 129 |
elif len(words) >= 10:
|
| 130 |
score += 0.2
|
| 131 |
scene_markers = ["scene", "step", "first", "next", "then", "finally", "now"]
|
| 132 |
-
|
| 133 |
-
score += min(0.3, marker_hits * 0.1)
|
| 134 |
if len(words) >= 50:
|
| 135 |
score += 0.3
|
| 136 |
elif len(words) >= 20:
|
|
@@ -139,35 +173,25 @@ def narration_score(narration: str, fmt: str) -> float:
|
|
| 139 |
|
| 140 |
|
| 141 |
def context_usage(code: str, accumulated_context: list[str]) -> float:
|
| 142 |
-
"""Score whether the generated code incorporates research findings (0-1).
|
| 143 |
-
|
| 144 |
-
Higher score if the code references terms found during exploration.
|
| 145 |
-
"""
|
| 146 |
if not accumulated_context:
|
| 147 |
-
return 0.5
|
| 148 |
|
| 149 |
context_words: set[str] = set()
|
| 150 |
for ctx in accumulated_context:
|
| 151 |
-
context_words.update(
|
| 152 |
-
w.lower() for w in ctx.split() if len(w) > 3
|
| 153 |
-
)
|
| 154 |
|
| 155 |
if not context_words:
|
| 156 |
return 0.5
|
| 157 |
|
| 158 |
-
code_words = set(
|
| 159 |
overlap = code_words & context_words
|
| 160 |
return min(1.0, len(overlap) / max(len(context_words), 1) * 5)
|
| 161 |
|
| 162 |
|
| 163 |
-
# --
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
W_COVERAGE = 0.15
|
| 167 |
-
W_FORMAT = 0.10
|
| 168 |
-
W_STRUCTURE = 0.15
|
| 169 |
-
W_NARRATION = 0.10
|
| 170 |
-
W_CONTEXT_USE = 0.20 # rewards using exploration findings
|
| 171 |
|
| 172 |
|
| 173 |
def compute_generate_reward(
|
|
@@ -178,33 +202,36 @@ def compute_generate_reward(
|
|
| 178 |
exec_success: bool,
|
| 179 |
accumulated_context: list[str],
|
| 180 |
) -> tuple[float, dict]:
|
| 181 |
-
"""Compute the generation-phase reward. Returns (total, components).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
c_valid = 1.0 if ast_parses(code) else 0.0
|
| 183 |
c_runs = 1.0 if exec_success else 0.0
|
| 184 |
c_coverage = keyword_coverage(code, task.keywords)
|
| 185 |
c_format = format_match(fmt, task)
|
| 186 |
-
c_struct =
|
| 187 |
c_narr = narration_score(narration, fmt)
|
| 188 |
c_ctx = context_usage(code, accumulated_context)
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
total = (
|
| 199 |
-
W_CODE_VALID * c_valid
|
| 200 |
-
+ W_CODE_RUNS * c_runs
|
| 201 |
-
+ W_COVERAGE * c_coverage
|
| 202 |
-
+ W_FORMAT * c_format
|
| 203 |
-
+ w_struct * c_struct
|
| 204 |
-
+ w_narr * c_narr
|
| 205 |
-
+ W_CONTEXT_USE * c_ctx
|
| 206 |
)
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
components = {
|
| 209 |
"code_valid": round(c_valid, 3),
|
| 210 |
"code_runs": round(c_runs, 3),
|
|
@@ -216,3 +243,44 @@ def compute_generate_reward(
|
|
| 216 |
"generate_total": round(total, 4),
|
| 217 |
}
|
| 218 |
return total, components
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 27 |
|
| 28 |
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
# Component weights
|
| 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 |
# ---------------------------------------------------------------------------
|
| 56 |
# Individual scorers
|
| 57 |
# ---------------------------------------------------------------------------
|
|
|
|
| 65 |
if not keywords:
|
| 66 |
return 0.0
|
| 67 |
code_lower = code.lower()
|
| 68 |
+
return sum(1 for kw in keywords if kw in code_lower) / len(keywords)
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def format_match(chosen_format: str, task: Task) -> float:
|
|
|
|
| 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``
|
| 85 |
+
for breaking violations (duplicate defs, cycles, etc.).
|
| 86 |
+
"""
|
| 87 |
score = 0.0
|
| 88 |
+
|
| 89 |
+
# Positive signals
|
| 90 |
if "import marimo" in code or "from marimo" in code:
|
| 91 |
score += 0.2
|
| 92 |
if "marimo.App" in code or "mo.App" in code:
|
|
|
|
| 96 |
score += 0.2
|
| 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 |
+
|
| 120 |
+
return max(0.0, min(1.0, score))
|
| 121 |
|
| 122 |
|
| 123 |
def manim_structure(code: str, task: Task) -> float:
|
|
|
|
| 131 |
score += 0.2
|
| 132 |
if "def construct" in code:
|
| 133 |
score += 0.1
|
| 134 |
+
|
| 135 |
anim_patterns = [
|
| 136 |
+
"self.play(", "self.wait(", "Create(", "FadeIn(", "FadeOut(",
|
| 137 |
+
"Transform(", "Write(", "MoveToTarget", "Indicate(",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
"ReplacementTransform(",
|
| 139 |
]
|
| 140 |
anim_hits = sum(1 for p in anim_patterns if p in code)
|
| 141 |
score += min(0.3, anim_hits * 0.05)
|
| 142 |
+
|
| 143 |
math_patterns = ["MathTex(", "Tex(", "Axes(", "NumberPlane(", "Graph("]
|
| 144 |
+
if any(p in code for p in math_patterns):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
score += 0.1
|
|
|
|
| 146 |
|
| 147 |
+
tier_thresholds = {"advanced": 6, "intermediate": 4, "beginner": 2}
|
| 148 |
+
if anim_hits >= tier_thresholds.get(task.tier, 2):
|
| 149 |
+
score += 0.1
|
| 150 |
|
| 151 |
+
return min(1.0, score)
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
def narration_score(narration: str, fmt: str) -> float:
|
|
|
|
| 157 |
return 1.0
|
| 158 |
if not narration or not narration.strip():
|
| 159 |
return 0.0
|
|
|
|
| 160 |
words = narration.split()
|
| 161 |
+
score = 0.0
|
| 162 |
if len(words) >= 30:
|
| 163 |
score += 0.4
|
| 164 |
elif len(words) >= 10:
|
| 165 |
score += 0.2
|
| 166 |
scene_markers = ["scene", "step", "first", "next", "then", "finally", "now"]
|
| 167 |
+
score += min(0.3, sum(0.1 for m in scene_markers if m in narration.lower()))
|
|
|
|
| 168 |
if len(words) >= 50:
|
| 169 |
score += 0.3
|
| 170 |
elif len(words) >= 20:
|
|
|
|
| 173 |
|
| 174 |
|
| 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 |
+
# ---------------------------------------------------------------------------
|
| 193 |
+
# Main reward function
|
| 194 |
+
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
def compute_generate_reward(
|
|
|
|
| 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),
|
|
|
|
| 243 |
"generate_total": round(total, 4),
|
| 244 |
}
|
| 245 |
return total, components
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def adjust_repair_reward(
|
| 249 |
+
base_reward: float,
|
| 250 |
+
*,
|
| 251 |
+
repair_success: bool,
|
| 252 |
+
previous_error_codes: list[str],
|
| 253 |
+
new_error_codes: list[str],
|
| 254 |
+
previous_code: str,
|
| 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()
|
rewards/sandbox.py
CHANGED
|
@@ -1,11 +1,45 @@
|
|
| 1 |
"""Sandbox execution for marimo and manim code."""
|
| 2 |
|
| 3 |
import ast
|
|
|
|
| 4 |
import subprocess
|
| 5 |
import tempfile
|
|
|
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
def ast_parses(code: str) -> bool:
|
| 10 |
"""Check whether the code is valid Python (AST-parseable)."""
|
| 11 |
try:
|
|
@@ -34,19 +68,78 @@ def extract_scene_class(code: str) -> str | None:
|
|
| 34 |
return None
|
| 35 |
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
|
| 40 |
f.write(code)
|
| 41 |
f.flush()
|
| 42 |
tmp = f.name
|
| 43 |
try:
|
| 44 |
result = subprocess.run(
|
| 45 |
-
["marimo", "
|
| 46 |
capture_output=True,
|
| 47 |
text=True,
|
| 48 |
timeout=timeout,
|
| 49 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
if result.returncode == 0:
|
| 51 |
return True, "marimo export succeeded"
|
| 52 |
return False, result.stderr[:500]
|
|
@@ -81,3 +174,68 @@ def run_manim(code: str, timeout: int = 30) -> tuple[bool, str]:
|
|
| 81 |
return False, "manim not installed"
|
| 82 |
except subprocess.TimeoutExpired:
|
| 83 |
return False, "manim render timed out"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Sandbox execution for marimo and manim code."""
|
| 2 |
|
| 3 |
import ast
|
| 4 |
+
import json
|
| 5 |
import subprocess
|
| 6 |
import tempfile
|
| 7 |
+
from dataclasses import dataclass, field
|
| 8 |
+
from typing import Any
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
|
| 12 |
+
@dataclass
|
| 13 |
+
class SandboxResult:
|
| 14 |
+
"""Structured lint/build result returned to the environment."""
|
| 15 |
+
|
| 16 |
+
fmt: str
|
| 17 |
+
parses: bool
|
| 18 |
+
check_passed: bool
|
| 19 |
+
exec_success: bool
|
| 20 |
+
message: str
|
| 21 |
+
errors: list[dict[str, Any]] = field(default_factory=list)
|
| 22 |
+
|
| 23 |
+
@property
|
| 24 |
+
def error_codes(self) -> list[str]:
|
| 25 |
+
codes: list[str] = []
|
| 26 |
+
for error in self.errors:
|
| 27 |
+
code = error.get("code")
|
| 28 |
+
if code and code not in codes:
|
| 29 |
+
codes.append(str(code))
|
| 30 |
+
return codes
|
| 31 |
+
|
| 32 |
+
def render_errors(self) -> str:
|
| 33 |
+
if not self.errors:
|
| 34 |
+
return self.message
|
| 35 |
+
lines = []
|
| 36 |
+
for error in self.errors:
|
| 37 |
+
code = error.get("code", "error")
|
| 38 |
+
message = error.get("message", "")
|
| 39 |
+
lines.append(f"{code}: {message}".strip())
|
| 40 |
+
return "\n".join(lines)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
def ast_parses(code: str) -> bool:
|
| 44 |
"""Check whether the code is valid Python (AST-parseable)."""
|
| 45 |
try:
|
|
|
|
| 68 |
return None
|
| 69 |
|
| 70 |
|
| 71 |
+
# ---------------------------------------------------------------------------
|
| 72 |
+
# Marimo validation via `marimo check` CLI
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def check_marimo(code: str, timeout: int = 8) -> tuple[bool, str, list[str]]:
|
| 77 |
+
"""Run ``marimo check`` on code. Returns (passed, message, rule_codes).
|
| 78 |
+
|
| 79 |
+
Catches breaking rules MB001-MB005: unparsable cells, duplicate
|
| 80 |
+
definitions, cycle dependencies, setup cell issues, syntax errors.
|
| 81 |
+
Runs in ~100-200ms — much faster than full ``marimo export html``.
|
| 82 |
+
"""
|
| 83 |
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
|
| 84 |
f.write(code)
|
| 85 |
f.flush()
|
| 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:
|
| 106 |
+
return False, "marimo not installed", ["MARIMO_MISSING"]
|
| 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 |
+
|
| 114 |
+
|
| 115 |
+
# ---------------------------------------------------------------------------
|
| 116 |
+
# Full execution
|
| 117 |
+
# ---------------------------------------------------------------------------
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def run_marimo(code: str, timeout: int = 15, *, skip_check: bool = False) -> tuple[bool, str]:
|
| 121 |
+
"""Export a marimo notebook to HTML, optionally running static checks first.
|
| 122 |
+
|
| 123 |
+
Runs ``marimo check`` first (fast static analysis). If that fails,
|
| 124 |
+
returns immediately without the expensive export step.
|
| 125 |
+
"""
|
| 126 |
+
check_timeout = min(8, max(1, timeout // 2))
|
| 127 |
+
if not skip_check:
|
| 128 |
+
passed, msg, _violations = check_marimo(code, timeout=check_timeout)
|
| 129 |
+
if not passed:
|
| 130 |
+
return False, msg
|
| 131 |
+
|
| 132 |
+
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
|
| 133 |
+
f.write(code)
|
| 134 |
+
f.flush()
|
| 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]
|
|
|
|
| 174 |
return False, "manim not installed"
|
| 175 |
except subprocess.TimeoutExpired:
|
| 176 |
return False, "manim render timed out"
|
| 177 |
+
|
| 178 |
+
|
| 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":
|
| 192 |
+
check_passed, check_msg, codes = check_marimo(code)
|
| 193 |
+
if not check_passed:
|
| 194 |
+
return SandboxResult(
|
| 195 |
+
fmt=fmt,
|
| 196 |
+
parses=True,
|
| 197 |
+
check_passed=False,
|
| 198 |
+
exec_success=False,
|
| 199 |
+
message=check_msg,
|
| 200 |
+
errors=[{"code": code, "message": check_msg} for code in codes],
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
exec_success, exec_msg = run_marimo(code, skip_check=True)
|
| 204 |
+
return SandboxResult(
|
| 205 |
+
fmt=fmt,
|
| 206 |
+
parses=True,
|
| 207 |
+
check_passed=True,
|
| 208 |
+
exec_success=exec_success,
|
| 209 |
+
message=exec_msg,
|
| 210 |
+
errors=[] if exec_success else [{"code": "MARIMO_EXPORT", "message": exec_msg}],
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
if fmt == "manim":
|
| 214 |
+
scene = extract_scene_class(code)
|
| 215 |
+
if scene is None:
|
| 216 |
+
return SandboxResult(
|
| 217 |
+
fmt=fmt,
|
| 218 |
+
parses=True,
|
| 219 |
+
check_passed=False,
|
| 220 |
+
exec_success=False,
|
| 221 |
+
message="No Scene subclass found in code",
|
| 222 |
+
errors=[{"code": "MANIM_NO_SCENE", "message": "No Scene subclass found."}],
|
| 223 |
+
)
|
| 224 |
+
exec_success, exec_msg = run_manim(code)
|
| 225 |
+
return SandboxResult(
|
| 226 |
+
fmt=fmt,
|
| 227 |
+
parses=True,
|
| 228 |
+
check_passed=True,
|
| 229 |
+
exec_success=exec_success,
|
| 230 |
+
message=exec_msg,
|
| 231 |
+
errors=[] if exec_success else [{"code": "MANIM_RENDER", "message": exec_msg}],
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
return SandboxResult(
|
| 235 |
+
fmt=fmt,
|
| 236 |
+
parses=True,
|
| 237 |
+
check_passed=False,
|
| 238 |
+
exec_success=False,
|
| 239 |
+
message=f"Unknown format: {fmt}",
|
| 240 |
+
errors=[{"code": "UNKNOWN_FORMAT", "message": f"Unknown format: {fmt}"}],
|
| 241 |
+
)
|
rewards/sources.py
CHANGED
|
@@ -1,321 +1,34 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
- HuggingFace Papers: ML-focused semantic search via huggingface_hub
|
| 5 |
-
- Wikipedia: general topics via wikipediaapi (section-level + BM25 RAG)
|
| 6 |
-
|
| 7 |
-
The agent's query is routed to the most appropriate source, or the agent
|
| 8 |
-
can specify a source prefix (e.g. "wiki: merge sort", "hf: attention").
|
| 9 |
-
|
| 10 |
-
All external calls use async I/O (httpx / wikipediaapi.AsyncWikipedia).
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
-
|
| 16 |
-
import
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
import httpx
|
| 20 |
-
import wikipediaapi
|
| 21 |
-
|
| 22 |
-
HF_MAX_RESULTS = 1
|
| 23 |
-
WIKI_TOP_SECTIONS = 3
|
| 24 |
-
|
| 25 |
-
# BM25 parameters
|
| 26 |
-
_BM25_K1 = 1.5
|
| 27 |
-
_BM25_B = 0.75
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
# ---------------------------------------------------------------------------
|
| 31 |
-
# BM25 scoring (pure Python, no external deps)
|
| 32 |
-
# ---------------------------------------------------------------------------
|
| 33 |
-
|
| 34 |
-
_STOP_WORDS = {
|
| 35 |
-
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
| 36 |
-
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
| 37 |
-
"should", "may", "might", "shall", "can", "need", "dare", "ought",
|
| 38 |
-
"to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
|
| 39 |
-
"into", "through", "during", "before", "after", "and", "but", "or",
|
| 40 |
-
"not", "no", "nor", "so", "yet", "both", "either", "neither",
|
| 41 |
-
"this", "that", "these", "those", "it", "its", "he", "she", "they",
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def _tokenize(text: str) -> list[str]:
|
| 46 |
-
"""Lowercase alphanumeric tokenization, stop words removed."""
|
| 47 |
-
return [w for w in re.findall(r"\w+", text.lower()) if w not in _STOP_WORDS and len(w) > 1]
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def _bm25_rank(
|
| 51 |
-
query: str, documents: list[tuple[str, str]], top_k: int = 3
|
| 52 |
-
) -> list[tuple[float, str, str]]:
|
| 53 |
-
"""Rank (title, text) documents against query using BM25.
|
| 54 |
-
|
| 55 |
-
Returns top_k results sorted by score descending.
|
| 56 |
-
"""
|
| 57 |
-
if not documents:
|
| 58 |
-
return []
|
| 59 |
-
|
| 60 |
-
query_terms = _tokenize(query)
|
| 61 |
-
if not query_terms:
|
| 62 |
-
return [(0.0, t, txt) for t, txt in documents[:top_k]]
|
| 63 |
-
|
| 64 |
-
# Precompute document token stats
|
| 65 |
-
doc_tokens = [_tokenize(f"{title} {text}") for title, text in documents]
|
| 66 |
-
doc_lengths = [len(t) for t in doc_tokens]
|
| 67 |
-
avgdl = sum(doc_lengths) / max(len(doc_lengths), 1)
|
| 68 |
-
n_docs = len(documents)
|
| 69 |
-
|
| 70 |
-
# Document frequency per query term
|
| 71 |
-
df: dict[str, int] = {}
|
| 72 |
-
for term in set(query_terms):
|
| 73 |
-
df[term] = sum(1 for tokens in doc_tokens if term in tokens)
|
| 74 |
-
|
| 75 |
-
# Score each document
|
| 76 |
-
scored: list[tuple[float, str, str]] = []
|
| 77 |
-
for i, (title, text) in enumerate(documents):
|
| 78 |
-
tf_counts = Counter(doc_tokens[i])
|
| 79 |
-
dl = doc_lengths[i]
|
| 80 |
-
score = 0.0
|
| 81 |
-
for term in query_terms:
|
| 82 |
-
if term not in df or df[term] == 0:
|
| 83 |
-
continue
|
| 84 |
-
idf = math.log((n_docs - df[term] + 0.5) / (df[term] + 0.5) + 1.0)
|
| 85 |
-
tf = tf_counts.get(term, 0)
|
| 86 |
-
numerator = tf * (_BM25_K1 + 1)
|
| 87 |
-
denominator = tf + _BM25_K1 * (1 - _BM25_B + _BM25_B * dl / max(avgdl, 1))
|
| 88 |
-
score += idf * numerator / denominator
|
| 89 |
-
scored.append((score, title, text))
|
| 90 |
-
|
| 91 |
-
scored.sort(key=lambda x: x[0], reverse=True)
|
| 92 |
-
return scored[:top_k]
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
# ---------------------------------------------------------------------------
|
| 96 |
-
# Wikipedia section flattening
|
| 97 |
-
# ---------------------------------------------------------------------------
|
| 98 |
-
|
| 99 |
-
_SKIP_SECTIONS = {
|
| 100 |
-
"references", "external links", "see also", "further reading",
|
| 101 |
-
"notes", "citations", "bibliography", "sources",
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def _flatten_sections(
|
| 106 |
-
sections: list[wikipediaapi.WikipediaPageSection],
|
| 107 |
-
max_depth: int = 2,
|
| 108 |
-
_depth: int = 0,
|
| 109 |
-
) -> list[tuple[str, str]]:
|
| 110 |
-
"""Flatten Wikipedia section tree into (title, text) pairs."""
|
| 111 |
-
result: list[tuple[str, str]] = []
|
| 112 |
-
for section in sections:
|
| 113 |
-
if section.title.lower() in _SKIP_SECTIONS:
|
| 114 |
-
continue
|
| 115 |
-
if section.text.strip():
|
| 116 |
-
result.append((section.title, section.text.strip()))
|
| 117 |
-
if _depth < max_depth and section.sections:
|
| 118 |
-
result.extend(
|
| 119 |
-
_flatten_sections(section.sections, max_depth, _depth + 1)
|
| 120 |
-
)
|
| 121 |
-
return result
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
# ---------------------------------------------------------------------------
|
| 125 |
-
# Wikipedia (async, section-level BM25)
|
| 126 |
-
# ---------------------------------------------------------------------------
|
| 127 |
-
|
| 128 |
-
async def search_wikipedia(
|
| 129 |
-
query: str, top_sections: int = WIKI_TOP_SECTIONS
|
| 130 |
-
) -> str:
|
| 131 |
-
"""Search Wikipedia and return the most relevant sections via BM25.
|
| 132 |
-
|
| 133 |
-
Flow: search(query) -> top page -> get sections -> BM25 rank -> top-k.
|
| 134 |
-
"""
|
| 135 |
-
try:
|
| 136 |
-
wiki = wikipediaapi.AsyncWikipedia(
|
| 137 |
-
user_agent="ExplainerEnv/1.0 (hackathon project)",
|
| 138 |
-
language="en",
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
# Search for the top page
|
| 142 |
-
search_results = await wiki.search(query, limit=1)
|
| 143 |
-
if not search_results or not search_results.pages:
|
| 144 |
-
return f"No Wikipedia results for: {query}"
|
| 145 |
-
|
| 146 |
-
# pages is a dict keyed by title
|
| 147 |
-
title = next(iter(search_results.pages))
|
| 148 |
-
page = wiki.page(title)
|
| 149 |
-
|
| 150 |
-
# Check page exists
|
| 151 |
-
exists = await page.exists()
|
| 152 |
-
if not exists:
|
| 153 |
-
return f"No Wikipedia article found for: {query}"
|
| 154 |
-
|
| 155 |
-
# Get summary + sections
|
| 156 |
-
summary = await page.summary
|
| 157 |
-
sections = await page.sections
|
| 158 |
|
| 159 |
-
# Build document list: summary as first doc, then flattened sections
|
| 160 |
-
docs: list[tuple[str, str]] = []
|
| 161 |
-
if summary:
|
| 162 |
-
docs.append((title, summary))
|
| 163 |
-
docs.extend(_flatten_sections(sections))
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
parts = []
|
| 172 |
-
for score, sec_title, sec_text in ranked:
|
| 173 |
-
# Truncate long sections to keep total size reasonable
|
| 174 |
-
trimmed = sec_text[:800] if len(sec_text) > 800 else sec_text
|
| 175 |
-
parts.append(f"## {sec_title}\n{trimmed}")
|
| 176 |
-
|
| 177 |
-
return f"Wikipedia: {title}\n\n" + "\n\n---\n\n".join(parts)
|
| 178 |
-
|
| 179 |
-
except Exception as e:
|
| 180 |
-
return f"Wikipedia search error: {e}"
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
# ---------------------------------------------------------------------------
|
| 184 |
-
# HuggingFace Papers (async, httpx + read_paper)
|
| 185 |
-
# ---------------------------------------------------------------------------
|
| 186 |
-
|
| 187 |
-
async def search_hf_papers(
|
| 188 |
-
query: str, max_results: int = HF_MAX_RESULTS
|
| 189 |
) -> str:
|
| 190 |
-
"""
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
if not papers:
|
| 206 |
-
return f"No HF papers found for: {query}"
|
| 207 |
-
|
| 208 |
-
paper = papers[0]
|
| 209 |
-
paper_id = paper.get("id", "")
|
| 210 |
-
title = paper.get("title", "Untitled")
|
| 211 |
-
summary = paper.get("summary", "")
|
| 212 |
-
|
| 213 |
-
if not paper_id:
|
| 214 |
-
# No paper ID — return just the search result
|
| 215 |
-
return f"Title: {title}\nAbstract: {summary[:600]}"
|
| 216 |
-
|
| 217 |
-
# 2. Read paper markdown content
|
| 218 |
-
md_resp = await client.get(
|
| 219 |
-
f"https://huggingface.co/papers/{paper_id}.md",
|
| 220 |
-
headers={"User-Agent": "ExplainerEnv/1.0"},
|
| 221 |
-
follow_redirects=True,
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
if md_resp.status_code == 200 and md_resp.text.strip():
|
| 225 |
-
md_content = md_resp.text
|
| 226 |
-
# Chunk markdown by headings
|
| 227 |
-
chunks = _chunk_markdown(md_content)
|
| 228 |
-
if chunks:
|
| 229 |
-
ranked = _bm25_rank(query, chunks, top_k=3)
|
| 230 |
-
parts = [f"Title: {title}\nPaper ID: {paper_id}\n"]
|
| 231 |
-
for _score, sec_title, sec_text in ranked:
|
| 232 |
-
trimmed = sec_text[:800] if len(sec_text) > 800 else sec_text
|
| 233 |
-
parts.append(f"## {sec_title}\n{trimmed}")
|
| 234 |
-
return "\n\n---\n\n".join(parts)
|
| 235 |
-
|
| 236 |
-
# Fallback: return abstract only
|
| 237 |
-
return (
|
| 238 |
-
f"Title: {title}\n"
|
| 239 |
-
f"Paper ID: {paper_id}\n"
|
| 240 |
-
f"Abstract: {summary[:600]}"
|
| 241 |
-
)
|
| 242 |
-
|
| 243 |
-
except Exception as e:
|
| 244 |
-
return f"HF Papers search error: {e}"
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
def _chunk_markdown(md_text: str) -> list[tuple[str, str]]:
|
| 248 |
-
"""Split markdown text into (heading, body) chunks."""
|
| 249 |
-
chunks: list[tuple[str, str]] = []
|
| 250 |
-
current_heading = "Introduction"
|
| 251 |
-
current_lines: list[str] = []
|
| 252 |
-
|
| 253 |
-
for line in md_text.split("\n"):
|
| 254 |
-
if line.startswith("#"):
|
| 255 |
-
# Save previous chunk
|
| 256 |
-
body = "\n".join(current_lines).strip()
|
| 257 |
-
if body:
|
| 258 |
-
chunks.append((current_heading, body))
|
| 259 |
-
# Start new chunk
|
| 260 |
-
current_heading = line.lstrip("#").strip() or "Section"
|
| 261 |
-
current_lines = []
|
| 262 |
-
else:
|
| 263 |
-
current_lines.append(line)
|
| 264 |
-
|
| 265 |
-
# Save last chunk
|
| 266 |
-
body = "\n".join(current_lines).strip()
|
| 267 |
-
if body:
|
| 268 |
-
chunks.append((current_heading, body))
|
| 269 |
-
|
| 270 |
-
return chunks
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
# ---------------------------------------------------------------------------
|
| 274 |
-
# Router
|
| 275 |
-
# ---------------------------------------------------------------------------
|
| 276 |
-
|
| 277 |
-
# Keywords that suggest ML/AI topics (used when category is not available)
|
| 278 |
-
_ML_KEYWORDS = {
|
| 279 |
-
"neural", "network", "transformer", "attention", "embedding", "gradient",
|
| 280 |
-
"backpropagation", "cnn", "rnn", "lstm", "gpt", "bert", "diffusion",
|
| 281 |
-
"reinforcement", "generative", "discriminative", "autoencoder", "vae",
|
| 282 |
-
"gan", "fine-tuning", "pretraining", "tokenizer", "llm", "rlhf",
|
| 283 |
-
"classification", "regression", "clustering", "deep learning",
|
| 284 |
-
"machine learning", "optimization", "sgd", "adam", "batch normalization",
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
def _is_ml_topic(query: str) -> bool:
|
| 289 |
-
"""Heuristic: does the query look like an ML/AI topic?"""
|
| 290 |
-
query_lower = query.lower()
|
| 291 |
-
return any(kw in query_lower for kw in _ML_KEYWORDS)
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
async def search(query: str, category_hint: str = "") -> str:
|
| 295 |
-
"""Route a search query to the best source.
|
| 296 |
-
|
| 297 |
-
The agent can override by prefixing the query:
|
| 298 |
-
- "hf: attention mechanism" -> HF Papers only
|
| 299 |
-
- "wiki: merge sort" -> Wikipedia only
|
| 300 |
-
|
| 301 |
-
Otherwise, uses keyword heuristic to route ML topics to HF Papers
|
| 302 |
-
and everything else to Wikipedia.
|
| 303 |
-
"""
|
| 304 |
-
query = query.strip()
|
| 305 |
-
|
| 306 |
-
# Explicit source prefix
|
| 307 |
-
lower = query.lower()
|
| 308 |
-
if lower.startswith("hf:"):
|
| 309 |
-
return await search_hf_papers(query[3:].strip())
|
| 310 |
-
if lower.startswith("wiki:"):
|
| 311 |
-
return await search_wikipedia(query[5:].strip())
|
| 312 |
-
|
| 313 |
-
# Auto-route based on keyword heuristic
|
| 314 |
-
if _is_ml_topic(query) or _is_ml_topic(category_hint):
|
| 315 |
-
hf = await search_hf_papers(query)
|
| 316 |
-
if "error" in hf.lower() or "no hf papers" in hf.lower():
|
| 317 |
-
return await search_wikipedia(query)
|
| 318 |
-
return hf
|
| 319 |
-
|
| 320 |
-
# Default: Wikipedia
|
| 321 |
-
return await search_wikipedia(query)
|
|
|
|
| 1 |
+
"""Compatibility wrapper for the structured research tool layer.
|
| 2 |
|
| 3 |
+
New code should use ``explainer_env.research.run_research_tool`` directly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
+
try:
|
| 9 |
+
from ..research import run_research_tool
|
| 10 |
+
except ImportError: # pragma: no cover - supports direct test execution
|
| 11 |
+
from research import run_research_tool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
async def search(
|
| 15 |
+
query: str,
|
| 16 |
+
category_hint: str = "",
|
| 17 |
+
difficulty: str = "easy",
|
| 18 |
+
tool: str | None = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
) -> str:
|
| 20 |
+
"""Run a research tool and render its result as text."""
|
| 21 |
+
selected_tool = tool or _default_tool(query, category_hint, difficulty)
|
| 22 |
+
result = await run_research_tool(selected_tool, query, category_hint)
|
| 23 |
+
return result.render()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _default_tool(query: str, category_hint: str, difficulty: str) -> str:
|
| 27 |
+
text = f"{query} {category_hint}".lower()
|
| 28 |
+
if any(term in text for term in ("api", "code", "plot", "marimo", "manim")):
|
| 29 |
+
return "fetch_docs"
|
| 30 |
+
if any(term in text for term in ("model", "dataset", "hugging face", "hf hub")):
|
| 31 |
+
return "search_hf_hub"
|
| 32 |
+
if difficulty == "hard":
|
| 33 |
+
return "search_arxiv"
|
| 34 |
+
return "search_wikipedia"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/app.py
CHANGED
|
@@ -58,7 +58,7 @@ def main(host: str = "0.0.0.0", port: int = 8000):
|
|
| 58 |
This function enables running the server without Docker:
|
| 59 |
uv run --project . server
|
| 60 |
uv run --project . server --port 8001
|
| 61 |
-
python -m
|
| 62 |
|
| 63 |
Args:
|
| 64 |
host: Host address to bind to (default: "0.0.0.0")
|
|
|
|
| 58 |
This function enables running the server without Docker:
|
| 59 |
uv run --project . server
|
| 60 |
uv run --project . server --port 8001
|
| 61 |
+
python -m explainer_env.server.app
|
| 62 |
|
| 63 |
Args:
|
| 64 |
host: Host address to bind to (default: "0.0.0.0")
|
server/explainer_env_environment.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
Research
|
| 3 |
|
| 4 |
Episode flow:
|
| 5 |
1. reset() → agent gets a topic + tier
|
| 6 |
-
2. step(explore) ×
|
| 7 |
-
3. step(generate) × 1 → agent produces marimo/manim code
|
|
|
|
| 8 |
|
| 9 |
Each step returns a per-step reward. The final generate step also includes
|
| 10 |
a generation reward that accounts for how well the code uses the research.
|
|
@@ -20,22 +21,22 @@ from openenv.core.env_server.interfaces import Environment
|
|
| 20 |
from openenv.core.env_server.types import State
|
| 21 |
|
| 22 |
try:
|
|
|
|
| 23 |
from ..models import ExplainerAction, ExplainerObservation
|
|
|
|
| 24 |
from ..rewards.exploration import compute_explore_reward
|
| 25 |
-
from ..rewards.generation import compute_generate_reward
|
| 26 |
-
from ..rewards.sandbox import
|
| 27 |
-
from ..rewards.sources import search as search_sources
|
| 28 |
from ..task_bank import ALL_TASKS, EASY_TASKS, HARD_TASKS, MEDIUM_TASKS, Task
|
| 29 |
except ImportError:
|
|
|
|
| 30 |
from models import ExplainerAction, ExplainerObservation
|
|
|
|
| 31 |
from rewards.exploration import compute_explore_reward
|
| 32 |
-
from rewards.generation import compute_generate_reward
|
| 33 |
-
from rewards.sandbox import
|
| 34 |
-
from rewards.sources import search as search_sources
|
| 35 |
from task_bank import ALL_TASKS, EASY_TASKS, HARD_TASKS, MEDIUM_TASKS, Task
|
| 36 |
|
| 37 |
-
MAX_EXPLORE_STEPS = 3
|
| 38 |
-
|
| 39 |
|
| 40 |
class ExplainerEnvironment(Environment):
|
| 41 |
"""
|
|
@@ -56,7 +57,16 @@ class ExplainerEnvironment(Environment):
|
|
| 56 |
self._current_task: Task | None = None
|
| 57 |
self._difficulty_pool: list[Task] = EASY_TASKS
|
| 58 |
self._accumulated_context: list[str] = []
|
|
|
|
| 59 |
self._explore_steps: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
# ------------------------------------------------------------------
|
| 62 |
# Sync interface (fallback — OpenEnv prefers async when overridden)
|
|
@@ -78,6 +88,14 @@ class ExplainerEnvironment(Environment):
|
|
| 78 |
done=True,
|
| 79 |
reward=-1.0,
|
| 80 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
try:
|
| 83 |
if action.action_type == "explore":
|
|
@@ -85,6 +103,8 @@ class ExplainerEnvironment(Environment):
|
|
| 85 |
return asyncio.run(self._handle_explore(action, task))
|
| 86 |
elif action.action_type == "generate":
|
| 87 |
return self._handle_generate(action, task)
|
|
|
|
|
|
|
| 88 |
else:
|
| 89 |
return self._make_obs(
|
| 90 |
task,
|
|
@@ -121,12 +141,22 @@ class ExplainerEnvironment(Environment):
|
|
| 121 |
done=True,
|
| 122 |
reward=-1.0,
|
| 123 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
try:
|
| 126 |
if action.action_type == "explore":
|
| 127 |
return await self._handle_explore(action, task)
|
| 128 |
elif action.action_type == "generate":
|
| 129 |
return self._handle_generate(action, task)
|
|
|
|
|
|
|
| 130 |
else:
|
| 131 |
return self._make_obs(
|
| 132 |
task,
|
|
@@ -154,20 +184,40 @@ class ExplainerEnvironment(Environment):
|
|
| 154 |
episode_id=episode_id or str(uuid4()), step_count=0
|
| 155 |
)
|
| 156 |
self._accumulated_context = []
|
|
|
|
| 157 |
self._explore_steps = 0
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
else:
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
|
| 172 |
t = self._current_task
|
| 173 |
return ExplainerObservation(
|
|
@@ -177,17 +227,31 @@ class ExplainerEnvironment(Environment):
|
|
| 177 |
keywords=t.keywords,
|
| 178 |
data_available=t.data_available,
|
| 179 |
phase="explore",
|
| 180 |
-
feedback=
|
|
|
|
|
|
|
|
|
|
| 181 |
search_results="",
|
| 182 |
explored_context="",
|
| 183 |
explore_steps_left=MAX_EXPLORE_STEPS,
|
|
|
|
|
|
|
| 184 |
done=False,
|
| 185 |
reward=0.0,
|
| 186 |
)
|
| 187 |
|
| 188 |
async def _handle_explore(self, action: ExplainerAction, task: Task) -> ExplainerObservation:
|
| 189 |
-
"""Process an explore action:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
if self._explore_steps >= MAX_EXPLORE_STEPS:
|
|
|
|
| 191 |
return self._make_obs(
|
| 192 |
task,
|
| 193 |
phase="generate",
|
|
@@ -197,6 +261,8 @@ class ExplainerEnvironment(Environment):
|
|
| 197 |
|
| 198 |
self._explore_steps += 1
|
| 199 |
query = action.query.strip()
|
|
|
|
|
|
|
| 200 |
|
| 201 |
if not query:
|
| 202 |
return self._make_obs(
|
|
@@ -206,39 +272,68 @@ class ExplainerEnvironment(Environment):
|
|
| 206 |
reward=0.0,
|
| 207 |
)
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
# Compute per-step exploration reward
|
| 214 |
reward, components = compute_explore_reward(
|
| 215 |
query=query,
|
| 216 |
-
|
|
|
|
|
|
|
| 217 |
topic=task.topic,
|
| 218 |
keywords_csv=task.keywords,
|
| 219 |
task_content=task.content,
|
|
|
|
|
|
|
| 220 |
accumulated_context=self._accumulated_context,
|
|
|
|
| 221 |
)
|
| 222 |
|
| 223 |
steps_left = MAX_EXPLORE_STEPS - self._explore_steps
|
| 224 |
-
if steps_left >
|
| 225 |
phase = "explore"
|
| 226 |
-
hint = f"{steps_left}
|
|
|
|
|
|
|
|
|
|
| 227 |
else:
|
| 228 |
phase = "generate"
|
| 229 |
-
hint = "
|
|
|
|
| 230 |
|
| 231 |
return self._make_obs(
|
| 232 |
task,
|
| 233 |
phase=phase,
|
| 234 |
-
feedback=f"{hint}\nReward: {components}",
|
| 235 |
search_results=results_text,
|
| 236 |
reward=reward,
|
| 237 |
-
metadata={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
)
|
| 239 |
|
| 240 |
def _handle_generate(self, action: ExplainerAction, task: Task) -> ExplainerObservation:
|
| 241 |
-
"""Process a generate action: run sandbox,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
fmt = action.format or "marimo"
|
| 243 |
code = action.code
|
| 244 |
narration = action.narration
|
|
@@ -251,17 +346,7 @@ class ExplainerEnvironment(Environment):
|
|
| 251 |
skip_penalty = 0.0
|
| 252 |
penalty_msg = ""
|
| 253 |
|
| 254 |
-
|
| 255 |
-
parses = ast_parses(code)
|
| 256 |
-
exec_success = False
|
| 257 |
-
exec_msg = ""
|
| 258 |
-
if parses:
|
| 259 |
-
if fmt == "marimo":
|
| 260 |
-
exec_success, exec_msg = run_marimo(code)
|
| 261 |
-
elif fmt == "manim":
|
| 262 |
-
exec_success, exec_msg = run_manim(code)
|
| 263 |
-
else:
|
| 264 |
-
exec_msg = "Code has syntax errors and cannot be parsed."
|
| 265 |
|
| 266 |
# Generation reward
|
| 267 |
reward, components = compute_generate_reward(
|
|
@@ -269,35 +354,129 @@ class ExplainerEnvironment(Environment):
|
|
| 269 |
fmt=fmt,
|
| 270 |
narration=narration,
|
| 271 |
task=task,
|
| 272 |
-
exec_success=exec_success,
|
| 273 |
accumulated_context=self._accumulated_context,
|
| 274 |
)
|
| 275 |
reward = max(0.0, reward + skip_penalty)
|
| 276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
# Feedback
|
| 278 |
parts = []
|
| 279 |
if penalty_msg:
|
| 280 |
parts.append(penalty_msg)
|
| 281 |
-
if not parses:
|
| 282 |
parts.append("SYNTAX ERROR: code does not parse.")
|
| 283 |
-
elif not exec_success:
|
| 284 |
-
parts.append(f"EXECUTION FAILED: {
|
| 285 |
else:
|
| 286 |
-
parts.append(f"EXECUTION OK: {
|
| 287 |
parts.append(
|
| 288 |
f"Reward: {', '.join(f'{k}={v}' for k, v in components.items())}"
|
| 289 |
)
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
return self._make_obs(
|
| 292 |
task,
|
| 293 |
-
phase=
|
| 294 |
feedback="\n".join(parts),
|
| 295 |
reward=reward,
|
| 296 |
-
done=
|
|
|
|
| 297 |
metadata={
|
| 298 |
"step": self._state.step_count,
|
| 299 |
"phase": "generate",
|
| 300 |
"explore_steps_used": self._explore_steps,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
**components,
|
| 302 |
},
|
| 303 |
)
|
|
@@ -311,6 +490,7 @@ class ExplainerEnvironment(Environment):
|
|
| 311 |
reward: float = 0.0,
|
| 312 |
done: bool = False,
|
| 313 |
search_results: str = "",
|
|
|
|
| 314 |
metadata: dict | None = None,
|
| 315 |
) -> ExplainerObservation:
|
| 316 |
"""Helper to build a consistent observation."""
|
|
@@ -325,6 +505,9 @@ class ExplainerEnvironment(Environment):
|
|
| 325 |
search_results=search_results,
|
| 326 |
explored_context="\n---\n".join(self._accumulated_context),
|
| 327 |
explore_steps_left=MAX_EXPLORE_STEPS - self._explore_steps,
|
|
|
|
|
|
|
|
|
|
| 328 |
done=done,
|
| 329 |
reward=reward,
|
| 330 |
metadata=metadata or {},
|
|
|
|
| 1 |
"""
|
| 2 |
+
Research -> Interactive Explainer Environment (multi-step, async).
|
| 3 |
|
| 4 |
Episode flow:
|
| 5 |
1. reset() → agent gets a topic + tier
|
| 6 |
+
2. step(explore) × 0..MAX_EXPLORE → agent calls research tools
|
| 7 |
+
3. step(generate) × 1 → agent produces marimo/manim code
|
| 8 |
+
4. step(repair) × 0..MAX_REPAIR → agent fixes lint/build errors if needed
|
| 9 |
|
| 10 |
Each step returns a per-step reward. The final generate step also includes
|
| 11 |
a generation reward that accounts for how well the code uses the research.
|
|
|
|
| 21 |
from openenv.core.env_server.types import State
|
| 22 |
|
| 23 |
try:
|
| 24 |
+
from ..constants import MAX_EXPLORE_STEPS, MAX_REPAIR_STEPS
|
| 25 |
from ..models import ExplainerAction, ExplainerObservation
|
| 26 |
+
from ..research import AVAILABLE_TOOLS, run_research_tool
|
| 27 |
from ..rewards.exploration import compute_explore_reward
|
| 28 |
+
from ..rewards.generation import adjust_repair_reward, compute_generate_reward
|
| 29 |
+
from ..rewards.sandbox import validate_code
|
|
|
|
| 30 |
from ..task_bank import ALL_TASKS, EASY_TASKS, HARD_TASKS, MEDIUM_TASKS, Task
|
| 31 |
except ImportError:
|
| 32 |
+
from constants import MAX_EXPLORE_STEPS, MAX_REPAIR_STEPS
|
| 33 |
from models import ExplainerAction, ExplainerObservation
|
| 34 |
+
from research import AVAILABLE_TOOLS, run_research_tool
|
| 35 |
from rewards.exploration import compute_explore_reward
|
| 36 |
+
from rewards.generation import adjust_repair_reward, compute_generate_reward
|
| 37 |
+
from rewards.sandbox import validate_code
|
|
|
|
| 38 |
from task_bank import ALL_TASKS, EASY_TASKS, HARD_TASKS, MEDIUM_TASKS, Task
|
| 39 |
|
|
|
|
|
|
|
| 40 |
|
| 41 |
class ExplainerEnvironment(Environment):
|
| 42 |
"""
|
|
|
|
| 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
|
| 63 |
+
self._phase: str = "explore"
|
| 64 |
+
self._done: bool = False
|
| 65 |
+
self._last_code: str = ""
|
| 66 |
+
self._last_format: str = "marimo"
|
| 67 |
+
self._last_narration: str = ""
|
| 68 |
+
self._last_errors: str = ""
|
| 69 |
+
self._last_error_codes: list[str] = []
|
| 70 |
|
| 71 |
# ------------------------------------------------------------------
|
| 72 |
# Sync interface (fallback — OpenEnv prefers async when overridden)
|
|
|
|
| 88 |
done=True,
|
| 89 |
reward=-1.0,
|
| 90 |
)
|
| 91 |
+
if self._done:
|
| 92 |
+
return self._make_obs(
|
| 93 |
+
task,
|
| 94 |
+
phase="done",
|
| 95 |
+
feedback="Episode is already done. Call reset() to start a new one.",
|
| 96 |
+
reward=0.0,
|
| 97 |
+
done=True,
|
| 98 |
+
)
|
| 99 |
|
| 100 |
try:
|
| 101 |
if action.action_type == "explore":
|
|
|
|
| 103 |
return asyncio.run(self._handle_explore(action, task))
|
| 104 |
elif action.action_type == "generate":
|
| 105 |
return self._handle_generate(action, task)
|
| 106 |
+
elif action.action_type == "repair":
|
| 107 |
+
return self._handle_repair(action, task)
|
| 108 |
else:
|
| 109 |
return self._make_obs(
|
| 110 |
task,
|
|
|
|
| 141 |
done=True,
|
| 142 |
reward=-1.0,
|
| 143 |
)
|
| 144 |
+
if self._done:
|
| 145 |
+
return self._make_obs(
|
| 146 |
+
task,
|
| 147 |
+
phase="done",
|
| 148 |
+
feedback="Episode is already done. Call reset() to start a new one.",
|
| 149 |
+
reward=0.0,
|
| 150 |
+
done=True,
|
| 151 |
+
)
|
| 152 |
|
| 153 |
try:
|
| 154 |
if action.action_type == "explore":
|
| 155 |
return await self._handle_explore(action, task)
|
| 156 |
elif action.action_type == "generate":
|
| 157 |
return self._handle_generate(action, task)
|
| 158 |
+
elif action.action_type == "repair":
|
| 159 |
+
return self._handle_repair(action, task)
|
| 160 |
else:
|
| 161 |
return self._make_obs(
|
| 162 |
task,
|
|
|
|
| 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
|
| 190 |
+
self._phase = "explore"
|
| 191 |
+
self._done = False
|
| 192 |
+
self._last_code = ""
|
| 193 |
+
self._last_format = "marimo"
|
| 194 |
+
self._last_narration = ""
|
| 195 |
+
self._last_errors = ""
|
| 196 |
+
self._last_error_codes = []
|
| 197 |
+
|
| 198 |
+
# Allow selecting a specific task by topic name
|
| 199 |
+
topic = kwargs.get("topic", None)
|
| 200 |
+
if topic:
|
| 201 |
+
match = next((t for t in ALL_TASKS if t.topic == topic), None)
|
| 202 |
+
if match:
|
| 203 |
+
self._current_task = match
|
| 204 |
+
else:
|
| 205 |
+
# Fallback to random if topic not found
|
| 206 |
+
rng = random.Random(seed) if seed is not None else random.Random()
|
| 207 |
+
self._current_task = rng.choice(ALL_TASKS)
|
| 208 |
else:
|
| 209 |
+
difficulty = kwargs.get("difficulty", None)
|
| 210 |
+
if difficulty == "medium":
|
| 211 |
+
pool = MEDIUM_TASKS
|
| 212 |
+
elif difficulty == "hard":
|
| 213 |
+
pool = HARD_TASKS
|
| 214 |
+
elif difficulty == "easy":
|
| 215 |
+
pool = EASY_TASKS
|
| 216 |
+
else:
|
| 217 |
+
pool = self._difficulty_pool
|
| 218 |
|
| 219 |
+
rng = random.Random(seed) if seed is not None else random.Random()
|
| 220 |
+
self._current_task = rng.choice(pool) if pool else rng.choice(ALL_TASKS)
|
| 221 |
|
| 222 |
t = self._current_task
|
| 223 |
return ExplainerObservation(
|
|
|
|
| 227 |
keywords=t.keywords,
|
| 228 |
data_available=t.data_available,
|
| 229 |
phase="explore",
|
| 230 |
+
feedback=(
|
| 231 |
+
"Research phase: choose a tool and query relevant to the topic. "
|
| 232 |
+
f"Available tools: {', '.join(AVAILABLE_TOOLS)}."
|
| 233 |
+
),
|
| 234 |
search_results="",
|
| 235 |
explored_context="",
|
| 236 |
explore_steps_left=MAX_EXPLORE_STEPS,
|
| 237 |
+
repair_attempts_left=MAX_REPAIR_STEPS,
|
| 238 |
+
available_tools=list(AVAILABLE_TOOLS),
|
| 239 |
done=False,
|
| 240 |
reward=0.0,
|
| 241 |
)
|
| 242 |
|
| 243 |
async def _handle_explore(self, action: ExplainerAction, task: Task) -> ExplainerObservation:
|
| 244 |
+
"""Process an explore action: call a research tool and score the result."""
|
| 245 |
+
if self._phase not in {"explore", "generate"}:
|
| 246 |
+
return self._make_obs(
|
| 247 |
+
task,
|
| 248 |
+
phase=self._phase,
|
| 249 |
+
feedback=f"Cannot explore during phase '{self._phase}'.",
|
| 250 |
+
reward=0.0,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
if self._explore_steps >= MAX_EXPLORE_STEPS:
|
| 254 |
+
self._phase = "generate"
|
| 255 |
return self._make_obs(
|
| 256 |
task,
|
| 257 |
phase="generate",
|
|
|
|
| 261 |
|
| 262 |
self._explore_steps += 1
|
| 263 |
query = action.query.strip()
|
| 264 |
+
intent = action.intent.strip()
|
| 265 |
+
tool = action.tool or "search_wikipedia"
|
| 266 |
|
| 267 |
if not query:
|
| 268 |
return self._make_obs(
|
|
|
|
| 272 |
reward=0.0,
|
| 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)
|
| 283 |
|
| 284 |
# Compute per-step exploration reward
|
| 285 |
reward, components = compute_explore_reward(
|
| 286 |
query=query,
|
| 287 |
+
tool=tool,
|
| 288 |
+
intent=intent,
|
| 289 |
+
result=result,
|
| 290 |
topic=task.topic,
|
| 291 |
keywords_csv=task.keywords,
|
| 292 |
task_content=task.content,
|
| 293 |
+
difficulty=task.difficulty,
|
| 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
|
| 300 |
+
if steps_left > 1:
|
| 301 |
phase = "explore"
|
| 302 |
+
hint = f"Research going well — {steps_left} more steps available. Keep searching or move to generation."
|
| 303 |
+
elif steps_left == 1:
|
| 304 |
+
phase = "explore"
|
| 305 |
+
hint = "Last research step available. Search for any missing context, or proceed to generate."
|
| 306 |
else:
|
| 307 |
phase = "generate"
|
| 308 |
+
hint = "Research phase complete. Time to generate your explanation."
|
| 309 |
+
self._phase = phase
|
| 310 |
|
| 311 |
return self._make_obs(
|
| 312 |
task,
|
| 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 |
+
},
|
| 325 |
)
|
| 326 |
|
| 327 |
def _handle_generate(self, action: ExplainerAction, task: Task) -> ExplainerObservation:
|
| 328 |
+
"""Process a generate action: run sandbox, maybe open repair phase."""
|
| 329 |
+
if self._phase not in {"explore", "generate"}:
|
| 330 |
+
return self._make_obs(
|
| 331 |
+
task,
|
| 332 |
+
phase=self._phase,
|
| 333 |
+
feedback=f"Cannot generate during phase '{self._phase}'.",
|
| 334 |
+
reward=0.0,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
fmt = action.format or "marimo"
|
| 338 |
code = action.code
|
| 339 |
narration = action.narration
|
|
|
|
| 346 |
skip_penalty = 0.0
|
| 347 |
penalty_msg = ""
|
| 348 |
|
| 349 |
+
sandbox = validate_code(fmt, code)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
# Generation reward
|
| 352 |
reward, components = compute_generate_reward(
|
|
|
|
| 354 |
fmt=fmt,
|
| 355 |
narration=narration,
|
| 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
|
| 369 |
parts = []
|
| 370 |
if penalty_msg:
|
| 371 |
parts.append(penalty_msg)
|
| 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(
|
| 379 |
f"Reward: {', '.join(f'{k}={v}' for k, v in components.items())}"
|
| 380 |
)
|
| 381 |
|
| 382 |
+
done = sandbox.exec_success or self._repair_steps >= MAX_REPAIR_STEPS
|
| 383 |
+
phase = "done" if done else "repair"
|
| 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,
|
| 391 |
+
phase=phase,
|
| 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",
|
| 399 |
"explore_steps_used": self._explore_steps,
|
| 400 |
+
"sandbox_message": sandbox.message,
|
| 401 |
+
"error_codes": sandbox.error_codes,
|
| 402 |
+
**components,
|
| 403 |
+
},
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
def _handle_repair(self, action: ExplainerAction, task: Task) -> ExplainerObservation:
|
| 407 |
+
"""Process one repair attempt after a failed generate action."""
|
| 408 |
+
if self._phase != "repair":
|
| 409 |
+
return self._make_obs(
|
| 410 |
+
task,
|
| 411 |
+
phase=self._phase,
|
| 412 |
+
feedback="Repair is only available after a failed generate step.",
|
| 413 |
+
reward=0.0,
|
| 414 |
+
done=self._done,
|
| 415 |
+
)
|
| 416 |
+
if self._repair_steps >= MAX_REPAIR_STEPS:
|
| 417 |
+
self._phase = "done"
|
| 418 |
+
self._done = True
|
| 419 |
+
return self._make_obs(
|
| 420 |
+
task,
|
| 421 |
+
phase="done",
|
| 422 |
+
feedback="No repair attempts left.",
|
| 423 |
+
reward=0.0,
|
| 424 |
+
done=True,
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
self._repair_steps += 1
|
| 428 |
+
fmt = action.format or self._last_format or "marimo"
|
| 429 |
+
code = action.code
|
| 430 |
+
narration = action.narration or self._last_narration
|
| 431 |
+
previous_code = self._last_code
|
| 432 |
+
previous_errors = list(self._last_error_codes)
|
| 433 |
+
|
| 434 |
+
sandbox = validate_code(fmt, code)
|
| 435 |
+
base_reward, components = compute_generate_reward(
|
| 436 |
+
code=code,
|
| 437 |
+
fmt=fmt,
|
| 438 |
+
narration=narration,
|
| 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,
|
| 445 |
+
repair_success=sandbox.exec_success,
|
| 446 |
+
previous_error_codes=previous_errors,
|
| 447 |
+
new_error_codes=sandbox.error_codes,
|
| 448 |
+
previous_code=previous_code,
|
| 449 |
+
repaired_code=code,
|
| 450 |
+
)
|
| 451 |
+
components.update(repair_components)
|
| 452 |
+
|
| 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",
|
| 476 |
+
"explore_steps_used": self._explore_steps,
|
| 477 |
+
"repair_steps_used": self._repair_steps,
|
| 478 |
+
"sandbox_message": sandbox.message,
|
| 479 |
+
"error_codes": sandbox.error_codes,
|
| 480 |
**components,
|
| 481 |
},
|
| 482 |
)
|
|
|
|
| 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:
|
| 496 |
"""Helper to build a consistent observation."""
|
|
|
|
| 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,
|
| 509 |
+
last_errors=self._last_errors if last_errors is None else last_errors,
|
| 510 |
+
available_tools=list(AVAILABLE_TOOLS),
|
| 511 |
done=done,
|
| 512 |
reward=reward,
|
| 513 |
metadata=metadata or {},
|
tests/test_client_server.py
CHANGED
|
@@ -42,7 +42,12 @@ def run_tests(base_url: str):
|
|
| 42 |
print(f" reset: topic={obs.topic!r}, phase={obs.phase}")
|
| 43 |
|
| 44 |
# --- explore ---
|
| 45 |
-
action = ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
result = sc.step(action)
|
| 47 |
assert not result.done
|
| 48 |
assert result.observation.explore_steps_left == 2
|
|
@@ -55,6 +60,12 @@ def run_tests(base_url: str):
|
|
| 55 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n mo.md('hi')\n return\n",
|
| 56 |
)
|
| 57 |
result = sc.step(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
assert result.done
|
| 59 |
assert isinstance(result.reward, (int, float))
|
| 60 |
print(f" generate: reward={result.reward:.3f}, done={result.done}")
|
|
@@ -75,13 +86,15 @@ def main():
|
|
| 75 |
if args.url:
|
| 76 |
run_tests(args.url)
|
| 77 |
else:
|
|
|
|
| 78 |
proc = subprocess.Popen(
|
| 79 |
-
["uv", "run", "server"],
|
|
|
|
| 80 |
stdout=subprocess.PIPE,
|
| 81 |
stderr=subprocess.PIPE,
|
| 82 |
)
|
| 83 |
try:
|
| 84 |
-
url = "http://localhost:
|
| 85 |
if not wait_for_server(url):
|
| 86 |
stderr = proc.stderr.read().decode() if proc.stderr else ""
|
| 87 |
print(f"FAIL: server did not start\n{stderr}", file=sys.stderr)
|
|
|
|
| 42 |
print(f" reset: topic={obs.topic!r}, phase={obs.phase}")
|
| 43 |
|
| 44 |
# --- explore ---
|
| 45 |
+
action = ExplainerAction(
|
| 46 |
+
action_type="explore",
|
| 47 |
+
tool="search_wikipedia",
|
| 48 |
+
query=obs.topic,
|
| 49 |
+
intent="overview",
|
| 50 |
+
)
|
| 51 |
result = sc.step(action)
|
| 52 |
assert not result.done
|
| 53 |
assert result.observation.explore_steps_left == 2
|
|
|
|
| 60 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n mo.md('hi')\n return\n",
|
| 61 |
)
|
| 62 |
result = sc.step(action)
|
| 63 |
+
if not result.done:
|
| 64 |
+
result = sc.step(ExplainerAction(
|
| 65 |
+
action_type="repair",
|
| 66 |
+
format="marimo",
|
| 67 |
+
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n mo.md('hi')\n return\n",
|
| 68 |
+
))
|
| 69 |
assert result.done
|
| 70 |
assert isinstance(result.reward, (int, float))
|
| 71 |
print(f" generate: reward={result.reward:.3f}, done={result.done}")
|
|
|
|
| 86 |
if args.url:
|
| 87 |
run_tests(args.url)
|
| 88 |
else:
|
| 89 |
+
port = "8010"
|
| 90 |
proc = subprocess.Popen(
|
| 91 |
+
["uv", "run", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", port],
|
| 92 |
+
cwd=str(Path(__file__).resolve().parents[1]),
|
| 93 |
stdout=subprocess.PIPE,
|
| 94 |
stderr=subprocess.PIPE,
|
| 95 |
)
|
| 96 |
try:
|
| 97 |
+
url = f"http://localhost:{port}"
|
| 98 |
if not wait_for_server(url):
|
| 99 |
stderr = proc.stderr.read().decode() if proc.stderr else ""
|
| 100 |
print(f"FAIL: server did not start\n{stderr}", file=sys.stderr)
|
tests/test_docker.py
CHANGED
|
@@ -76,10 +76,17 @@ def run_tests(base_url: str):
|
|
| 76 |
print(f" reset: topic={result.observation.topic!r}")
|
| 77 |
|
| 78 |
action = ExplainerAction(
|
|
|
|
| 79 |
format="marimo",
|
| 80 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
| 81 |
)
|
| 82 |
result = sc.step(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
assert isinstance(result.reward, (int, float))
|
| 84 |
print(f" step: reward={result.reward:.3f}, done={result.done}")
|
| 85 |
|
|
|
|
| 76 |
print(f" reset: topic={result.observation.topic!r}")
|
| 77 |
|
| 78 |
action = ExplainerAction(
|
| 79 |
+
action_type="generate",
|
| 80 |
format="marimo",
|
| 81 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
| 82 |
)
|
| 83 |
result = sc.step(action)
|
| 84 |
+
if not result.done:
|
| 85 |
+
result = sc.step(ExplainerAction(
|
| 86 |
+
action_type="repair",
|
| 87 |
+
format="marimo",
|
| 88 |
+
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
| 89 |
+
))
|
| 90 |
assert isinstance(result.reward, (int, float))
|
| 91 |
print(f" step: reward={result.reward:.3f}, done={result.done}")
|
| 92 |
|
tests/test_environment.py
CHANGED
|
@@ -29,7 +29,12 @@ def test_reset_deterministic_with_seed():
|
|
| 29 |
def test_explore_step():
|
| 30 |
env = ExplainerEnvironment()
|
| 31 |
env.reset(seed=1)
|
| 32 |
-
action = ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
obs = env.step(action)
|
| 34 |
assert obs.done is False
|
| 35 |
assert obs.explore_steps_left == 2
|
|
@@ -40,7 +45,7 @@ def test_explore_step():
|
|
| 40 |
def test_explore_empty_query():
|
| 41 |
env = ExplainerEnvironment()
|
| 42 |
env.reset(seed=1)
|
| 43 |
-
action = ExplainerAction(action_type="explore", query="")
|
| 44 |
obs = env.step(action)
|
| 45 |
assert obs.reward == 0.0
|
| 46 |
assert "Empty query" in obs.feedback
|
|
@@ -50,7 +55,11 @@ def test_explore_max_steps():
|
|
| 50 |
env = ExplainerEnvironment()
|
| 51 |
env.reset(seed=1)
|
| 52 |
for i in range(3):
|
| 53 |
-
obs = env.step(ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
assert obs.phase == "generate"
|
| 55 |
assert obs.explore_steps_left == 0
|
| 56 |
|
|
@@ -59,17 +68,20 @@ def test_explore_then_generate():
|
|
| 59 |
env = ExplainerEnvironment()
|
| 60 |
env.reset(seed=1)
|
| 61 |
# Explore
|
| 62 |
-
obs = env.step(ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
assert obs.done is False
|
| 64 |
-
assert obs.
|
| 65 |
# Generate
|
| 66 |
obs = env.step(ExplainerAction(
|
| 67 |
action_type="generate",
|
| 68 |
format="marimo",
|
| 69 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
| 70 |
))
|
| 71 |
-
assert obs.
|
| 72 |
-
assert obs.phase == "done"
|
| 73 |
assert isinstance(obs.reward, (int, float))
|
| 74 |
|
| 75 |
|
|
@@ -81,13 +93,14 @@ def test_generate_without_explore_penalty():
|
|
| 81 |
format="marimo",
|
| 82 |
code="x = 1",
|
| 83 |
))
|
| 84 |
-
assert obs.done is
|
|
|
|
| 85 |
assert "penalty" in obs.feedback.lower() or "without" in obs.feedback.lower()
|
| 86 |
|
| 87 |
|
| 88 |
def test_step_without_reset():
|
| 89 |
env = ExplainerEnvironment()
|
| 90 |
-
action = ExplainerAction(action_type="explore", query="test")
|
| 91 |
obs = env.step(action)
|
| 92 |
assert obs.done is True
|
| 93 |
assert obs.reward == -1.0
|
|
@@ -96,7 +109,11 @@ def test_step_without_reset():
|
|
| 96 |
def test_generate_reward_in_metadata():
|
| 97 |
env = ExplainerEnvironment()
|
| 98 |
env.reset(seed=1)
|
| 99 |
-
env.step(ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
obs = env.step(ExplainerAction(
|
| 101 |
action_type="generate",
|
| 102 |
format="marimo",
|
|
@@ -120,7 +137,11 @@ def test_step_increments_count():
|
|
| 120 |
env = ExplainerEnvironment()
|
| 121 |
env.reset(seed=1)
|
| 122 |
assert env.state.step_count == 0
|
| 123 |
-
env.step(ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
assert env.state.step_count == 1
|
| 125 |
env.step(ExplainerAction(action_type="generate", format="marimo", code="x=1"))
|
| 126 |
assert env.state.step_count == 2
|
|
@@ -134,10 +155,30 @@ def test_bad_code_does_not_crash():
|
|
| 134 |
format="marimo",
|
| 135 |
code=")))syntax error(((",
|
| 136 |
))
|
| 137 |
-
assert obs.done is
|
|
|
|
| 138 |
assert "SYNTAX ERROR" in obs.feedback
|
| 139 |
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
if __name__ == "__main__":
|
| 142 |
tests = [
|
| 143 |
test_reset_returns_observation,
|
|
@@ -152,6 +193,7 @@ if __name__ == "__main__":
|
|
| 152 |
test_state_episode_id_changes,
|
| 153 |
test_step_increments_count,
|
| 154 |
test_bad_code_does_not_crash,
|
|
|
|
| 155 |
]
|
| 156 |
passed = 0
|
| 157 |
for t in tests:
|
|
|
|
| 29 |
def test_explore_step():
|
| 30 |
env = ExplainerEnvironment()
|
| 31 |
env.reset(seed=1)
|
| 32 |
+
action = ExplainerAction(
|
| 33 |
+
action_type="explore",
|
| 34 |
+
tool="search_wikipedia",
|
| 35 |
+
query="gradient descent optimization",
|
| 36 |
+
intent="beginner explanation",
|
| 37 |
+
)
|
| 38 |
obs = env.step(action)
|
| 39 |
assert obs.done is False
|
| 40 |
assert obs.explore_steps_left == 2
|
|
|
|
| 45 |
def test_explore_empty_query():
|
| 46 |
env = ExplainerEnvironment()
|
| 47 |
env.reset(seed=1)
|
| 48 |
+
action = ExplainerAction(action_type="explore", tool="search_wikipedia", query="")
|
| 49 |
obs = env.step(action)
|
| 50 |
assert obs.reward == 0.0
|
| 51 |
assert "Empty query" in obs.feedback
|
|
|
|
| 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",
|
| 61 |
+
query=f"search {i}",
|
| 62 |
+
))
|
| 63 |
assert obs.phase == "generate"
|
| 64 |
assert obs.explore_steps_left == 0
|
| 65 |
|
|
|
|
| 68 |
env = ExplainerEnvironment()
|
| 69 |
env.reset(seed=1)
|
| 70 |
# Explore
|
| 71 |
+
obs = env.step(ExplainerAction(
|
| 72 |
+
action_type="explore",
|
| 73 |
+
tool="search_wikipedia",
|
| 74 |
+
query="gradient descent",
|
| 75 |
+
))
|
| 76 |
assert obs.done is False
|
| 77 |
+
assert obs.search_results != ""
|
| 78 |
# Generate
|
| 79 |
obs = env.step(ExplainerAction(
|
| 80 |
action_type="generate",
|
| 81 |
format="marimo",
|
| 82 |
code="import marimo as mo\napp = mo.App()\n@app.cell\ndef _():\n return\n",
|
| 83 |
))
|
| 84 |
+
assert obs.phase in ("repair", "done")
|
|
|
|
| 85 |
assert isinstance(obs.reward, (int, float))
|
| 86 |
|
| 87 |
|
|
|
|
| 93 |
format="marimo",
|
| 94 |
code="x = 1",
|
| 95 |
))
|
| 96 |
+
assert obs.done is False
|
| 97 |
+
assert obs.phase == "repair"
|
| 98 |
assert "penalty" in obs.feedback.lower() or "without" in obs.feedback.lower()
|
| 99 |
|
| 100 |
|
| 101 |
def test_step_without_reset():
|
| 102 |
env = ExplainerEnvironment()
|
| 103 |
+
action = ExplainerAction(action_type="explore", tool="search_wikipedia", query="test")
|
| 104 |
obs = env.step(action)
|
| 105 |
assert obs.done is True
|
| 106 |
assert obs.reward == -1.0
|
|
|
|
| 109 |
def test_generate_reward_in_metadata():
|
| 110 |
env = ExplainerEnvironment()
|
| 111 |
env.reset(seed=1)
|
| 112 |
+
env.step(ExplainerAction(
|
| 113 |
+
action_type="explore",
|
| 114 |
+
tool="search_wikipedia",
|
| 115 |
+
query="gradient descent",
|
| 116 |
+
))
|
| 117 |
obs = env.step(ExplainerAction(
|
| 118 |
action_type="generate",
|
| 119 |
format="marimo",
|
|
|
|
| 137 |
env = ExplainerEnvironment()
|
| 138 |
env.reset(seed=1)
|
| 139 |
assert env.state.step_count == 0
|
| 140 |
+
env.step(ExplainerAction(
|
| 141 |
+
action_type="explore",
|
| 142 |
+
tool="search_wikipedia",
|
| 143 |
+
query="test",
|
| 144 |
+
))
|
| 145 |
assert env.state.step_count == 1
|
| 146 |
env.step(ExplainerAction(action_type="generate", format="marimo", code="x=1"))
|
| 147 |
assert env.state.step_count == 2
|
|
|
|
| 155 |
format="marimo",
|
| 156 |
code=")))syntax error(((",
|
| 157 |
))
|
| 158 |
+
assert obs.done is False
|
| 159 |
+
assert obs.phase == "repair"
|
| 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(
|
| 167 |
+
action_type="generate",
|
| 168 |
+
format="marimo",
|
| 169 |
+
code="x = 1",
|
| 170 |
+
))
|
| 171 |
+
obs = env.step(ExplainerAction(
|
| 172 |
+
action_type="repair",
|
| 173 |
+
format="marimo",
|
| 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__":
|
| 183 |
tests = [
|
| 184 |
test_reset_returns_observation,
|
|
|
|
| 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:
|
tests/test_models.py
CHANGED
|
@@ -9,9 +9,16 @@ from models import ExplainerAction, ExplainerObservation
|
|
| 9 |
|
| 10 |
|
| 11 |
def test_action_explore():
|
| 12 |
-
a = ExplainerAction(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
assert a.action_type == "explore"
|
|
|
|
| 14 |
assert a.query == "attention mechanism"
|
|
|
|
| 15 |
assert a.code == ""
|
| 16 |
assert a.format is None
|
| 17 |
|
|
@@ -38,12 +45,24 @@ def test_action_generate_manim():
|
|
| 38 |
assert a.narration != ""
|
| 39 |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
def test_observation_defaults():
|
| 42 |
obs = ExplainerObservation()
|
| 43 |
assert obs.topic == ""
|
| 44 |
assert obs.tier == "beginner"
|
| 45 |
assert obs.phase == "explore"
|
| 46 |
assert obs.explore_steps_left == 3
|
|
|
|
| 47 |
assert obs.done is False
|
| 48 |
|
| 49 |
|
|
@@ -72,6 +91,7 @@ if __name__ == "__main__":
|
|
| 72 |
test_action_explore()
|
| 73 |
test_action_generate_marimo()
|
| 74 |
test_action_generate_manim()
|
|
|
|
| 75 |
test_observation_defaults()
|
| 76 |
test_observation_full()
|
| 77 |
-
print("PASS: test_models (
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def test_action_explore():
|
| 12 |
+
a = ExplainerAction(
|
| 13 |
+
action_type="explore",
|
| 14 |
+
tool="search_arxiv",
|
| 15 |
+
query="attention mechanism",
|
| 16 |
+
intent="visual intuition",
|
| 17 |
+
)
|
| 18 |
assert a.action_type == "explore"
|
| 19 |
+
assert a.tool == "search_arxiv"
|
| 20 |
assert a.query == "attention mechanism"
|
| 21 |
+
assert a.intent == "visual intuition"
|
| 22 |
assert a.code == ""
|
| 23 |
assert a.format is None
|
| 24 |
|
|
|
|
| 45 |
assert a.narration != ""
|
| 46 |
|
| 47 |
|
| 48 |
+
def test_action_repair():
|
| 49 |
+
a = ExplainerAction(
|
| 50 |
+
action_type="repair",
|
| 51 |
+
format="marimo",
|
| 52 |
+
code="x = 1",
|
| 53 |
+
repair_notes="fixed syntax",
|
| 54 |
+
)
|
| 55 |
+
assert a.action_type == "repair"
|
| 56 |
+
assert a.repair_notes == "fixed syntax"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
def test_observation_defaults():
|
| 60 |
obs = ExplainerObservation()
|
| 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 |
|
|
|
|
| 91 |
test_action_explore()
|
| 92 |
test_action_generate_marimo()
|
| 93 |
test_action_generate_manim()
|
| 94 |
+
test_action_repair()
|
| 95 |
test_observation_defaults()
|
| 96 |
test_observation_full()
|
| 97 |
+
print("PASS: test_models (6/6)")
|
tests/test_rewards.py
CHANGED
|
@@ -6,10 +6,13 @@ from pathlib import Path
|
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
|
| 8 |
from rewards.exploration import (
|
|
|
|
| 9 |
compute_explore_reward,
|
| 10 |
query_relevance,
|
| 11 |
research_breadth,
|
| 12 |
result_novelty,
|
|
|
|
|
|
|
| 13 |
)
|
| 14 |
from rewards.generation import (
|
| 15 |
compute_generate_reward,
|
|
@@ -20,6 +23,7 @@ from rewards.generation import (
|
|
| 20 |
narration_score,
|
| 21 |
)
|
| 22 |
from rewards.sandbox import ast_parses
|
|
|
|
| 23 |
from task_bank import ALL_TASKS
|
| 24 |
|
| 25 |
MARIMO_TASK = next(t for t in ALL_TASKS if t.topic == "Linear Regression")
|
|
@@ -53,19 +57,73 @@ def test_research_breadth():
|
|
| 53 |
assert research_breadth(["a", "b"], min_sources=2) == 1.0
|
| 54 |
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def test_explore_reward_integration():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
reward, comp = compute_explore_reward(
|
| 58 |
query="linear regression least squares",
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
topic="Linear Regression",
|
| 61 |
keywords_csv="linear regression,least squares,MSE",
|
| 62 |
task_content="Linear regression is a method for modeling the relationship between variables.",
|
|
|
|
|
|
|
| 63 |
accumulated_context=["first search result"],
|
|
|
|
| 64 |
)
|
| 65 |
assert reward > 0.1
|
|
|
|
| 66 |
assert "query_relevance" in comp
|
| 67 |
-
assert "
|
| 68 |
-
assert "
|
| 69 |
assert "content_sufficiency" in comp
|
| 70 |
|
| 71 |
|
|
@@ -195,6 +253,9 @@ if __name__ == "__main__":
|
|
| 195 |
test_query_relevance,
|
| 196 |
test_result_novelty,
|
| 197 |
test_research_breadth,
|
|
|
|
|
|
|
|
|
|
| 198 |
test_explore_reward_integration,
|
| 199 |
test_keyword_coverage,
|
| 200 |
test_format_match,
|
|
|
|
| 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,
|
| 12 |
research_breadth,
|
| 13 |
result_novelty,
|
| 14 |
+
source_quality,
|
| 15 |
+
tool_choice_score,
|
| 16 |
)
|
| 17 |
from rewards.generation import (
|
| 18 |
compute_generate_reward,
|
|
|
|
| 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 |
|
| 29 |
MARIMO_TASK = next(t for t in ALL_TASKS if t.topic == "Linear Regression")
|
|
|
|
| 57 |
assert research_breadth(["a", "b"], min_sources=2) == 1.0
|
| 58 |
|
| 59 |
|
| 60 |
+
def test_tool_choice_score():
|
| 61 |
+
assert tool_choice_score("search_arxiv", "hard", "recent research paper") == 1.0
|
| 62 |
+
assert tool_choice_score("fetch_docs", "easy", "marimo plotting api") == 1.0
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_source_quality():
|
| 66 |
+
result = ResearchResult(
|
| 67 |
+
tool="search_arxiv",
|
| 68 |
+
query="linear regression",
|
| 69 |
+
chunks=[
|
| 70 |
+
ResearchChunk(
|
| 71 |
+
source="arxiv",
|
| 72 |
+
tool="search_arxiv",
|
| 73 |
+
title="A paper",
|
| 74 |
+
url="https://arxiv.org/abs/1",
|
| 75 |
+
text="linear regression least squares optimization " * 10,
|
| 76 |
+
score=1.0,
|
| 77 |
+
metadata={"year": 2024},
|
| 78 |
+
)
|
| 79 |
+
],
|
| 80 |
+
)
|
| 81 |
+
assert source_quality(result) > 0.7
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def test_coverage_delta():
|
| 85 |
+
assert coverage_delta(
|
| 86 |
+
"linear regression,MSE",
|
| 87 |
+
"linear regression",
|
| 88 |
+
[],
|
| 89 |
+
"mean squared error MSE",
|
| 90 |
+
) > 0.0
|
| 91 |
+
|
| 92 |
+
|
| 93 |
def test_explore_reward_integration():
|
| 94 |
+
result = ResearchResult(
|
| 95 |
+
tool="search_wikipedia",
|
| 96 |
+
query="linear regression least squares",
|
| 97 |
+
chunks=[
|
| 98 |
+
ResearchChunk(
|
| 99 |
+
source="wikipedia",
|
| 100 |
+
tool="search_wikipedia",
|
| 101 |
+
title="Linear regression",
|
| 102 |
+
url="https://example.test",
|
| 103 |
+
text="Linear regression minimizes squared error with least squares.",
|
| 104 |
+
score=1.0,
|
| 105 |
+
metadata={"page": "Linear regression"},
|
| 106 |
+
)
|
| 107 |
+
],
|
| 108 |
+
)
|
| 109 |
reward, comp = compute_explore_reward(
|
| 110 |
query="linear regression least squares",
|
| 111 |
+
tool="search_wikipedia",
|
| 112 |
+
intent="beginner explanation",
|
| 113 |
+
result=result,
|
| 114 |
topic="Linear Regression",
|
| 115 |
keywords_csv="linear regression,least squares,MSE",
|
| 116 |
task_content="Linear regression is a method for modeling the relationship between variables.",
|
| 117 |
+
difficulty="easy",
|
| 118 |
+
previous_context=[],
|
| 119 |
accumulated_context=["first search result"],
|
| 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 |
|
|
|
|
| 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,
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|