Spaces:
Sleeping
Sleeping
kabalan commited on
Commit ·
1b5c9b4
1
Parent(s): eea5114
Switch optimizer and generator from Gemini to Claude
Browse filesMigrates optimizer from Gemini 3.1 Pro to Claude Opus 4.6 and generator from Gemini Flash to Claude Sonnet 4.6. Updates client.py and slide_generator.py to use anthropic SDK instead of google-genai for their respective LLM calls. Adds anthropic>=0.40.0 to pyproject.toml dependencies. Updates log messages to reflect new model names.
- openenv/client.py +18 -21
- openenv/slide_generator.py +16 -27
- pyproject.toml +2 -1
openenv/client.py
CHANGED
|
@@ -26,18 +26,17 @@ import textwrap
|
|
| 26 |
from pathlib import Path
|
| 27 |
from typing import Any
|
| 28 |
|
|
|
|
|
|
|
| 29 |
from dotenv import load_dotenv
|
| 30 |
-
from
|
| 31 |
|
| 32 |
load_dotenv(Path(__file__).parent.parent / ".env")
|
| 33 |
-
from google.genai import types
|
| 34 |
-
import httpx
|
| 35 |
-
from loguru import logger
|
| 36 |
|
| 37 |
from models import SlideSkillObservation
|
| 38 |
|
| 39 |
SERVER_URL = "http://localhost:8000"
|
| 40 |
-
OPTIMIZER_MODEL = "
|
| 41 |
|
| 42 |
BASELINE_EXAMPLES_CONTENT = "(Empty — no prior optimization rounds)\n"
|
| 43 |
|
|
@@ -64,7 +63,7 @@ class SlideSkillClient:
|
|
| 64 |
|
| 65 |
Args:
|
| 66 |
session_id: Active session ID.
|
| 67 |
-
action: Dict matching
|
| 68 |
Must include "action_type" key.
|
| 69 |
"""
|
| 70 |
payload = {"session_id": session_id, "action": action}
|
|
@@ -94,15 +93,13 @@ class SlideSkillClient:
|
|
| 94 |
|
| 95 |
def call_optimizer_llm(
|
| 96 |
obs: SlideSkillObservation,
|
| 97 |
-
|
| 98 |
) -> dict[str, Any]:
|
| 99 |
"""
|
| 100 |
-
Call
|
| 101 |
the evaluation feedback.
|
| 102 |
|
| 103 |
Returns a dict suitable for the step() action parameter.
|
| 104 |
-
Uses ReplaceFileAction since the historical optimizer rewrites
|
| 105 |
-
the file wholesale.
|
| 106 |
"""
|
| 107 |
prompt = textwrap.dedent(f"""\
|
| 108 |
You are a McKinsey slide design optimizer. You are improving a
|
|
@@ -141,13 +138,13 @@ def call_optimizer_llm(
|
|
| 141 |
no code fences.
|
| 142 |
""")
|
| 143 |
|
| 144 |
-
response =
|
| 145 |
model=OPTIMIZER_MODEL,
|
| 146 |
-
|
| 147 |
-
|
| 148 |
)
|
| 149 |
|
| 150 |
-
new_content = response.text.strip()
|
| 151 |
|
| 152 |
return {
|
| 153 |
"action_type": "replace_file",
|
|
@@ -163,7 +160,7 @@ def run_optimization_loop(server_url: str = SERVER_URL, max_steps: int = 7) -> N
|
|
| 163 |
This mirrors the historical Skill Forge loop but driven through the
|
| 164 |
OpenEnv HTTP interface.
|
| 165 |
"""
|
| 166 |
-
|
| 167 |
|
| 168 |
with SlideSkillClient(base_url=server_url) as client:
|
| 169 |
logger.info(f"Starting optimization loop (max {max_steps} steps) | server={server_url}")
|
|
@@ -171,9 +168,9 @@ def run_optimization_loop(server_url: str = SERVER_URL, max_steps: int = 7) -> N
|
|
| 171 |
logger.info(f"Session: {session_id}")
|
| 172 |
|
| 173 |
# Step 0: baseline — generate slide with unmodified skill files.
|
| 174 |
-
logger.info("Step 0/baseline | generating slide (
|
| 175 |
logger.info("Step 0/baseline | running Node.js + LibreOffice → JPG...")
|
| 176 |
-
logger.info("Step 0/baseline | evaluating slide (Pro)...")
|
| 177 |
obs = client.step(
|
| 178 |
session_id,
|
| 179 |
{
|
|
@@ -189,11 +186,11 @@ def run_optimization_loop(server_url: str = SERVER_URL, max_steps: int = 7) -> N
|
|
| 189 |
logger.info("Episode complete (max_steps reached).")
|
| 190 |
break
|
| 191 |
|
| 192 |
-
logger.info(f"Step {step_idx}/{max_steps} | optimizing skill files (
|
| 193 |
-
action = call_optimizer_llm(obs,
|
| 194 |
-
logger.info(f"Step {step_idx}/{max_steps} | generating slide (
|
| 195 |
logger.info(f"Step {step_idx}/{max_steps} | running Node.js + LibreOffice → JPG...")
|
| 196 |
-
logger.info(f"Step {step_idx}/{max_steps} | evaluating slide (Pro)...")
|
| 197 |
obs = client.step(session_id, action)
|
| 198 |
|
| 199 |
delta_str = f"{obs.reward * 100:+.0f} pts"
|
|
|
|
| 26 |
from pathlib import Path
|
| 27 |
from typing import Any
|
| 28 |
|
| 29 |
+
import anthropic
|
| 30 |
+
import httpx
|
| 31 |
from dotenv import load_dotenv
|
| 32 |
+
from loguru import logger
|
| 33 |
|
| 34 |
load_dotenv(Path(__file__).parent.parent / ".env")
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
from models import SlideSkillObservation
|
| 37 |
|
| 38 |
SERVER_URL = "http://localhost:8000"
|
| 39 |
+
OPTIMIZER_MODEL = "claude-opus-4-6"
|
| 40 |
|
| 41 |
BASELINE_EXAMPLES_CONTENT = "(Empty — no prior optimization rounds)\n"
|
| 42 |
|
|
|
|
| 63 |
|
| 64 |
Args:
|
| 65 |
session_id: Active session ID.
|
| 66 |
+
action: Dict matching any SlideSkillAction schema.
|
| 67 |
Must include "action_type" key.
|
| 68 |
"""
|
| 69 |
payload = {"session_id": session_id, "action": action}
|
|
|
|
| 93 |
|
| 94 |
def call_optimizer_llm(
|
| 95 |
obs: SlideSkillObservation,
|
| 96 |
+
anthropic_client: anthropic.Anthropic,
|
| 97 |
) -> dict[str, Any]:
|
| 98 |
"""
|
| 99 |
+
Call Claude Opus 4.6 to generate a new DESIGN_RULES.md based on
|
| 100 |
the evaluation feedback.
|
| 101 |
|
| 102 |
Returns a dict suitable for the step() action parameter.
|
|
|
|
|
|
|
| 103 |
"""
|
| 104 |
prompt = textwrap.dedent(f"""\
|
| 105 |
You are a McKinsey slide design optimizer. You are improving a
|
|
|
|
| 138 |
no code fences.
|
| 139 |
""")
|
| 140 |
|
| 141 |
+
response = anthropic_client.messages.create(
|
| 142 |
model=OPTIMIZER_MODEL,
|
| 143 |
+
max_tokens=4096,
|
| 144 |
+
messages=[{"role": "user", "content": prompt}],
|
| 145 |
)
|
| 146 |
|
| 147 |
+
new_content = response.content[0].text.strip()
|
| 148 |
|
| 149 |
return {
|
| 150 |
"action_type": "replace_file",
|
|
|
|
| 160 |
This mirrors the historical Skill Forge loop but driven through the
|
| 161 |
OpenEnv HTTP interface.
|
| 162 |
"""
|
| 163 |
+
anthropic_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
| 164 |
|
| 165 |
with SlideSkillClient(base_url=server_url) as client:
|
| 166 |
logger.info(f"Starting optimization loop (max {max_steps} steps) | server={server_url}")
|
|
|
|
| 168 |
logger.info(f"Session: {session_id}")
|
| 169 |
|
| 170 |
# Step 0: baseline — generate slide with unmodified skill files.
|
| 171 |
+
logger.info("Step 0/baseline | generating slide (Sonnet 4.6)...")
|
| 172 |
logger.info("Step 0/baseline | running Node.js + LibreOffice → JPG...")
|
| 173 |
+
logger.info("Step 0/baseline | evaluating slide (Gemini 3.1 Pro)...")
|
| 174 |
obs = client.step(
|
| 175 |
session_id,
|
| 176 |
{
|
|
|
|
| 186 |
logger.info("Episode complete (max_steps reached).")
|
| 187 |
break
|
| 188 |
|
| 189 |
+
logger.info(f"Step {step_idx}/{max_steps} | optimizing skill files (Opus 4.6)...")
|
| 190 |
+
action = call_optimizer_llm(obs, anthropic_client)
|
| 191 |
+
logger.info(f"Step {step_idx}/{max_steps} | generating slide (Sonnet 4.6)...")
|
| 192 |
logger.info(f"Step {step_idx}/{max_steps} | running Node.js + LibreOffice → JPG...")
|
| 193 |
+
logger.info(f"Step {step_idx}/{max_steps} | evaluating slide (Gemini 3.1 Pro)...")
|
| 194 |
obs = client.step(session_id, action)
|
| 195 |
|
| 196 |
delta_str = f"{obs.reward * 100:+.0f} pts"
|
openenv/slide_generator.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
Slide Generator — orchestrates the full PPT generation pipeline.
|
| 3 |
|
| 4 |
Pipeline (in order):
|
| 5 |
-
1.
|
| 6 |
→ writes pptxgenjs JavaScript to generate.js in the session output dir.
|
| 7 |
2. Code patches are applied to the generated JS.
|
| 8 |
3. `node generate.js` runs in the session output dir → produces slide.pptx.
|
|
@@ -29,8 +29,7 @@ import subprocess
|
|
| 29 |
import textwrap
|
| 30 |
from pathlib import Path
|
| 31 |
|
| 32 |
-
|
| 33 |
-
from google.genai import types
|
| 34 |
|
| 35 |
from models import SlideSkillState
|
| 36 |
|
|
@@ -50,8 +49,8 @@ PDFTOPPM = (
|
|
| 50 |
or "pdftoppm"
|
| 51 |
)
|
| 52 |
|
| 53 |
-
#
|
| 54 |
-
GENERATOR_MODEL = "
|
| 55 |
GENERATOR_MAX_TOKENS = 4096
|
| 56 |
|
| 57 |
|
|
@@ -67,7 +66,7 @@ class SlideGenerator:
|
|
| 67 |
self.task_prompt = task_prompt_path.read_text(encoding="utf-8")
|
| 68 |
self.pptx_skill_dir = pptx_skill_dir
|
| 69 |
self.reference_dir = reference_dir
|
| 70 |
-
self._client =
|
| 71 |
|
| 72 |
def generate(
|
| 73 |
self,
|
|
@@ -180,7 +179,7 @@ class SlideGenerator:
|
|
| 180 |
node_error: str | None = None,
|
| 181 |
) -> str:
|
| 182 |
"""
|
| 183 |
-
Call
|
| 184 |
|
| 185 |
Reads skill files from the session directory first (so agent edits
|
| 186 |
take effect), falling back to the repo's pptx/ dir for any files
|
|
@@ -191,8 +190,6 @@ class SlideGenerator:
|
|
| 191 |
design_rules = (session_dir / "DESIGN_RULES.md").read_text(encoding="utf-8")
|
| 192 |
examples = (session_dir / "EXAMPLES.md").read_text(encoding="utf-8")
|
| 193 |
|
| 194 |
-
# Load pptx tooling from session dir (edited copies) with fallback
|
| 195 |
-
# to repo originals.
|
| 196 |
pptx_skill = self._read_pptx_skill(session_dir)
|
| 197 |
|
| 198 |
system_prompt = textwrap.dedent("""\
|
|
@@ -221,7 +218,6 @@ class SlideGenerator:
|
|
| 221 |
{self.task_prompt}
|
| 222 |
""")
|
| 223 |
|
| 224 |
-
# Inject hard constraints if any.
|
| 225 |
if state and state.constraints:
|
| 226 |
constraints_text = "\n".join(f"- {c}" for c in state.constraints)
|
| 227 |
user_message += textwrap.dedent(f"""
|
|
@@ -230,7 +226,6 @@ class SlideGenerator:
|
|
| 230 |
{constraints_text}
|
| 231 |
""")
|
| 232 |
|
| 233 |
-
# Inject JS templates if any.
|
| 234 |
if state and state.js_templates:
|
| 235 |
templates_text = ""
|
| 236 |
for name, code in state.js_templates.items():
|
|
@@ -254,27 +249,22 @@ class SlideGenerator:
|
|
| 254 |
{node_error}
|
| 255 |
""")
|
| 256 |
|
| 257 |
-
response = self._client.
|
| 258 |
model=GENERATOR_MODEL,
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
max_output_tokens=GENERATOR_MAX_TOKENS,
|
| 263 |
-
),
|
| 264 |
)
|
| 265 |
|
| 266 |
-
code = response.text.strip()
|
| 267 |
|
| 268 |
-
# Extract from markdown code fence if present
|
| 269 |
-
# despite instructions). Handles ```javascript, ```js, or plain ```.
|
| 270 |
fence_match = re.search(r"```(?:javascript|js)?\n(.*?)```", code, re.DOTALL)
|
| 271 |
if fence_match:
|
| 272 |
code = fence_match.group(1).strip()
|
| 273 |
|
| 274 |
# Rewrite all bare require('pkg') calls to absolute paths so the
|
| 275 |
-
# script works when run from any
|
| 276 |
-
# packages that actually exist in node_modules; unknown packages are
|
| 277 |
-
# left untouched (they'd fail at runtime but at least not silently).
|
| 278 |
node_modules = REPO_ROOT / "node_modules"
|
| 279 |
|
| 280 |
def _rewrite_require(m: re.Match) -> str:
|
|
@@ -283,12 +273,11 @@ class SlideGenerator:
|
|
| 283 |
pkg_path = node_modules / pkg
|
| 284 |
if pkg_path.exists():
|
| 285 |
return f"require({quote}{pkg_path}{quote})"
|
| 286 |
-
return m.group(0)
|
| 287 |
|
| 288 |
code = re.sub(r"require\((['\"])([^./][^'\"]*)\1\)", _rewrite_require, code)
|
| 289 |
|
| 290 |
-
#
|
| 291 |
-
# declaration to avoid "Identifier already declared" SyntaxError.
|
| 292 |
seen: set[str] = set()
|
| 293 |
deduped = []
|
| 294 |
for line in code.splitlines():
|
|
@@ -335,7 +324,7 @@ class SlideGenerator:
|
|
| 335 |
cwd=cwd,
|
| 336 |
capture_output=True,
|
| 337 |
text=True,
|
| 338 |
-
timeout=300,
|
| 339 |
)
|
| 340 |
if result.returncode != 0:
|
| 341 |
raise RuntimeError(
|
|
|
|
| 2 |
Slide Generator — orchestrates the full PPT generation pipeline.
|
| 3 |
|
| 4 |
Pipeline (in order):
|
| 5 |
+
1. Claude Sonnet 4.6 reads skill files + TASK_PROMPT.md + js_templates + constraints
|
| 6 |
→ writes pptxgenjs JavaScript to generate.js in the session output dir.
|
| 7 |
2. Code patches are applied to the generated JS.
|
| 8 |
3. `node generate.js` runs in the session output dir → produces slide.pptx.
|
|
|
|
| 29 |
import textwrap
|
| 30 |
from pathlib import Path
|
| 31 |
|
| 32 |
+
import anthropic
|
|
|
|
| 33 |
|
| 34 |
from models import SlideSkillState
|
| 35 |
|
|
|
|
| 49 |
or "pdftoppm"
|
| 50 |
)
|
| 51 |
|
| 52 |
+
# Claude Sonnet 4.6: fast and cost-effective for code generation.
|
| 53 |
+
GENERATOR_MODEL = "claude-sonnet-4-6"
|
| 54 |
GENERATOR_MAX_TOKENS = 4096
|
| 55 |
|
| 56 |
|
|
|
|
| 66 |
self.task_prompt = task_prompt_path.read_text(encoding="utf-8")
|
| 67 |
self.pptx_skill_dir = pptx_skill_dir
|
| 68 |
self.reference_dir = reference_dir
|
| 69 |
+
self._client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
| 70 |
|
| 71 |
def generate(
|
| 72 |
self,
|
|
|
|
| 179 |
node_error: str | None = None,
|
| 180 |
) -> str:
|
| 181 |
"""
|
| 182 |
+
Call Claude Sonnet 4.6 with skill files + task prompt as context.
|
| 183 |
|
| 184 |
Reads skill files from the session directory first (so agent edits
|
| 185 |
take effect), falling back to the repo's pptx/ dir for any files
|
|
|
|
| 190 |
design_rules = (session_dir / "DESIGN_RULES.md").read_text(encoding="utf-8")
|
| 191 |
examples = (session_dir / "EXAMPLES.md").read_text(encoding="utf-8")
|
| 192 |
|
|
|
|
|
|
|
| 193 |
pptx_skill = self._read_pptx_skill(session_dir)
|
| 194 |
|
| 195 |
system_prompt = textwrap.dedent("""\
|
|
|
|
| 218 |
{self.task_prompt}
|
| 219 |
""")
|
| 220 |
|
|
|
|
| 221 |
if state and state.constraints:
|
| 222 |
constraints_text = "\n".join(f"- {c}" for c in state.constraints)
|
| 223 |
user_message += textwrap.dedent(f"""
|
|
|
|
| 226 |
{constraints_text}
|
| 227 |
""")
|
| 228 |
|
|
|
|
| 229 |
if state and state.js_templates:
|
| 230 |
templates_text = ""
|
| 231 |
for name, code in state.js_templates.items():
|
|
|
|
| 249 |
{node_error}
|
| 250 |
""")
|
| 251 |
|
| 252 |
+
response = self._client.messages.create(
|
| 253 |
model=GENERATOR_MODEL,
|
| 254 |
+
max_tokens=GENERATOR_MAX_TOKENS,
|
| 255 |
+
system=system_prompt,
|
| 256 |
+
messages=[{"role": "user", "content": user_message}],
|
|
|
|
|
|
|
| 257 |
)
|
| 258 |
|
| 259 |
+
code = response.content[0].text.strip()
|
| 260 |
|
| 261 |
+
# Extract from markdown code fence if present.
|
|
|
|
| 262 |
fence_match = re.search(r"```(?:javascript|js)?\n(.*?)```", code, re.DOTALL)
|
| 263 |
if fence_match:
|
| 264 |
code = fence_match.group(1).strip()
|
| 265 |
|
| 266 |
# Rewrite all bare require('pkg') calls to absolute paths so the
|
| 267 |
+
# script works when run from any tmp directory.
|
|
|
|
|
|
|
| 268 |
node_modules = REPO_ROOT / "node_modules"
|
| 269 |
|
| 270 |
def _rewrite_require(m: re.Match) -> str:
|
|
|
|
| 273 |
pkg_path = node_modules / pkg
|
| 274 |
if pkg_path.exists():
|
| 275 |
return f"require({quote}{pkg_path}{quote})"
|
| 276 |
+
return m.group(0)
|
| 277 |
|
| 278 |
code = re.sub(r"require\((['\"])([^./][^'\"]*)\1\)", _rewrite_require, code)
|
| 279 |
|
| 280 |
+
# Deduplicate require lines to avoid "Identifier already declared".
|
|
|
|
| 281 |
seen: set[str] = set()
|
| 282 |
deduped = []
|
| 283 |
for line in code.splitlines():
|
|
|
|
| 324 |
cwd=cwd,
|
| 325 |
capture_output=True,
|
| 326 |
text=True,
|
| 327 |
+
timeout=300,
|
| 328 |
)
|
| 329 |
if result.returncode != 0:
|
| 330 |
raise RuntimeError(
|
pyproject.toml
CHANGED
|
@@ -10,7 +10,8 @@ requires-python = ">=3.12"
|
|
| 10 |
|
| 11 |
# Core runtime dependencies (required for the environment to run)
|
| 12 |
dependencies = [
|
| 13 |
-
"
|
|
|
|
| 14 |
"pydantic>=2.6.0", # Data models with discriminated unions
|
| 15 |
"httpx>=0.27.0", # HTTP client for client.py
|
| 16 |
"loguru>=0.7.0", # Structured logging for client
|
|
|
|
| 10 |
|
| 11 |
# Core runtime dependencies (required for the environment to run)
|
| 12 |
dependencies = [
|
| 13 |
+
"anthropic>=0.40.0", # Claude API client (generator: Sonnet 4.6, optimizer: Opus 4.6)
|
| 14 |
+
"google-genai>=1.0.0", # Gemini API client (evaluator: Gemini 3.1 Pro)
|
| 15 |
"pydantic>=2.6.0", # Data models with discriminated unions
|
| 16 |
"httpx>=0.27.0", # HTTP client for client.py
|
| 17 |
"loguru>=0.7.0", # Structured logging for client
|