Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +5 -1
- server/app.py +28 -35
- server/lint_coding_agent_environment.py +49 -54
Dockerfile
CHANGED
|
@@ -56,4 +56,8 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
| 56 |
|
| 57 |
# Start the server on 7860 with proxy support
|
| 58 |
# 'proxy_headers' is vital for HF reverse proxies to handle WebSockets correctly
|
| 59 |
-
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers", "--forwarded-allow-ips", "*"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
# Start the server on 7860 with proxy support
|
| 58 |
# 'proxy_headers' is vital for HF reverse proxies to handle WebSockets correctly
|
| 59 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers", "--forwarded-allow-ips", "*"]
|
| 60 |
+
|
| 61 |
+
WORKDIR /app/env
|
| 62 |
+
COPY . .
|
| 63 |
+
# This places QUESTIONS.json at /app/env/server/QUESTIONS.json
|
server/app.py
CHANGED
|
@@ -1,32 +1,5 @@
|
|
| 1 |
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
# All rights reserved.
|
| 3 |
-
#
|
| 4 |
-
# This source code is licensed under the BSD-style license found in the
|
| 5 |
-
# LICENSE file in the root directory of this source tree.
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
FastAPI application for the Lint Coding Agent Environment.
|
| 9 |
-
|
| 10 |
-
This module creates an HTTP server that exposes the LintCodingAgentEnvironment
|
| 11 |
-
over HTTP and WebSocket endpoints, compatible with EnvClient.
|
| 12 |
-
|
| 13 |
-
Endpoints:
|
| 14 |
-
- POST /reset: Reset the environment
|
| 15 |
-
- POST /step: Execute an action
|
| 16 |
-
- GET /state: Get current environment state
|
| 17 |
-
- GET /schema: Get action/observation schemas
|
| 18 |
-
- WS /ws: WebSocket endpoint for persistent sessions
|
| 19 |
-
|
| 20 |
-
Usage:
|
| 21 |
-
# Development (with auto-reload):
|
| 22 |
-
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 23 |
-
|
| 24 |
-
# Production:
|
| 25 |
-
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 26 |
-
|
| 27 |
-
# Or run directly:
|
| 28 |
-
python -m server.app
|
| 29 |
-
"""
|
| 30 |
|
| 31 |
import sys
|
| 32 |
import os
|
|
@@ -44,10 +17,11 @@ logging.basicConfig(
|
|
| 44 |
logger = logging.getLogger("ArchitectApp")
|
| 45 |
|
| 46 |
# --- ARCHITECT'S PATH INJECTION ---
|
|
|
|
| 47 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 48 |
project_root = os.path.abspath(os.path.join(current_dir, ".."))
|
| 49 |
|
| 50 |
-
for path in [project_root, current_dir]:
|
| 51 |
if path not in sys.path:
|
| 52 |
sys.path.insert(0, path)
|
| 53 |
|
|
@@ -58,11 +32,12 @@ except ImportError:
|
|
| 58 |
from openenv.core.env_server.http_server import create_app
|
| 59 |
|
| 60 |
# --- DOMAIN SPECIFIC IMPORTS ---
|
|
|
|
| 61 |
from models import LintCodingAgentAction, LintCodingAgentObservation
|
| 62 |
from server.lint_coding_agent_environment import LintCodingAgentEnvironment
|
| 63 |
|
| 64 |
# --- APP FACTORY ---
|
| 65 |
-
# 1. Create the environment logic app
|
| 66 |
base_app = create_app(
|
| 67 |
LintCodingAgentEnvironment,
|
| 68 |
LintCodingAgentAction,
|
|
@@ -70,19 +45,23 @@ base_app = create_app(
|
|
| 70 |
)
|
| 71 |
|
| 72 |
# 2. Create the Main Wrapper App
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
# --- DEBUG MIDDLEWARE ---
|
| 76 |
-
#
|
| 77 |
@app.middleware("http")
|
| 78 |
async def debug_logging(request: Request, call_next):
|
| 79 |
logger.info(f"π‘ Request: {request.method} {request.url.path}")
|
| 80 |
response = await call_next(request)
|
| 81 |
-
logger.info(f"β
|
| 82 |
return response
|
| 83 |
|
| 84 |
# --- MOUNTING (The /v1 Fix) ---
|
| 85 |
-
# This ensures that /v1/reset and /v1/ws are
|
| 86 |
app.mount("/v1", base_app)
|
| 87 |
|
| 88 |
# --- CORS SECURITY ---
|
|
@@ -96,7 +75,20 @@ app.add_middleware(
|
|
| 96 |
|
| 97 |
@app.get("/health")
|
| 98 |
def health_check():
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
def main():
|
| 102 |
"""
|
|
@@ -109,8 +101,9 @@ def main():
|
|
| 109 |
args = parser.parse_args()
|
| 110 |
|
| 111 |
logger.info(f"π OpenEnv Sandbox Starting on {args.host}:{args.port}")
|
| 112 |
-
logger.info(f"π
|
| 113 |
|
|
|
|
| 114 |
uvicorn.run(
|
| 115 |
app,
|
| 116 |
host=args.host,
|
|
|
|
| 1 |
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
# All rights reserved.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
import sys
|
| 5 |
import os
|
|
|
|
| 17 |
logger = logging.getLogger("ArchitectApp")
|
| 18 |
|
| 19 |
# --- ARCHITECT'S PATH INJECTION ---
|
| 20 |
+
# We force the root and server dirs into sys.path to ensure 'models' is found
|
| 21 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 22 |
project_root = os.path.abspath(os.path.join(current_dir, ".."))
|
| 23 |
|
| 24 |
+
for path in [project_root, current_dir, os.getcwd()]:
|
| 25 |
if path not in sys.path:
|
| 26 |
sys.path.insert(0, path)
|
| 27 |
|
|
|
|
| 32 |
from openenv.core.env_server.http_server import create_app
|
| 33 |
|
| 34 |
# --- DOMAIN SPECIFIC IMPORTS ---
|
| 35 |
+
# These are the schemas and logic we verified in previous steps
|
| 36 |
from models import LintCodingAgentAction, LintCodingAgentObservation
|
| 37 |
from server.lint_coding_agent_environment import LintCodingAgentEnvironment
|
| 38 |
|
| 39 |
# --- APP FACTORY ---
|
| 40 |
+
# 1. Create the environment logic app (The core OpenEnv engine)
|
| 41 |
base_app = create_app(
|
| 42 |
LintCodingAgentEnvironment,
|
| 43 |
LintCodingAgentAction,
|
|
|
|
| 45 |
)
|
| 46 |
|
| 47 |
# 2. Create the Main Wrapper App
|
| 48 |
+
# redirect_slashes=False is CRITICAL to prevent 307 Redirect loops on HF
|
| 49 |
+
app = FastAPI(
|
| 50 |
+
title="AnonAjay Architect API",
|
| 51 |
+
redirect_slashes=False
|
| 52 |
+
)
|
| 53 |
|
| 54 |
# --- DEBUG MIDDLEWARE ---
|
| 55 |
+
# Real-time telemetry for every request hitting the Space
|
| 56 |
@app.middleware("http")
|
| 57 |
async def debug_logging(request: Request, call_next):
|
| 58 |
logger.info(f"π‘ Request: {request.method} {request.url.path}")
|
| 59 |
response = await call_next(request)
|
| 60 |
+
logger.info(f"β
Outcome: {response.status_code}")
|
| 61 |
return response
|
| 62 |
|
| 63 |
# --- MOUNTING (The /v1 Fix) ---
|
| 64 |
+
# This ensures that /v1/reset and /v1/ws are mapped to the core engine
|
| 65 |
app.mount("/v1", base_app)
|
| 66 |
|
| 67 |
# --- CORS SECURITY ---
|
|
|
|
| 75 |
|
| 76 |
@app.get("/health")
|
| 77 |
def health_check():
|
| 78 |
+
"""Simple health probe for Docker and HF status checks."""
|
| 79 |
+
return {"status": "online", "message": "Lead Architect Server is Active", "port": 7860}
|
| 80 |
+
|
| 81 |
+
# Catch-all for the root to prevent confusing 404s in the browser
|
| 82 |
+
@app.get("/")
|
| 83 |
+
def root_info():
|
| 84 |
+
return {
|
| 85 |
+
"message": "Lint Coding Agent API is Running",
|
| 86 |
+
"endpoints": {
|
| 87 |
+
"docs": "/v1/docs",
|
| 88 |
+
"health": "/health",
|
| 89 |
+
"api_root": "/v1"
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
|
| 93 |
def main():
|
| 94 |
"""
|
|
|
|
| 101 |
args = parser.parse_args()
|
| 102 |
|
| 103 |
logger.info(f"π OpenEnv Sandbox Starting on {args.host}:{args.port}")
|
| 104 |
+
logger.info(f"π Geometry: /v1 mapped to OpenEnv Core")
|
| 105 |
|
| 106 |
+
# proxy_headers + forwarded_allow_ips are vital for HF reverse proxy support
|
| 107 |
uvicorn.run(
|
| 108 |
app,
|
| 109 |
host=args.host,
|
server/lint_coding_agent_environment.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
| 1 |
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
# All rights reserved.
|
| 3 |
-
#
|
| 4 |
-
# This source code is licensed under the BSD-style license found in the
|
| 5 |
-
# LICENSE file in the root directory of this source tree.
|
| 6 |
|
| 7 |
import json
|
| 8 |
import os
|
|
@@ -15,7 +12,7 @@ from typing import Tuple, Dict, Any, List
|
|
| 15 |
from openenv.core.env_server.interfaces import Environment
|
| 16 |
from openenv.core.env_server.types import State
|
| 17 |
|
| 18 |
-
#
|
| 19 |
logging.basicConfig(level=logging.INFO)
|
| 20 |
logger = logging.getLogger("ArchitectEnvironment")
|
| 21 |
|
|
@@ -28,7 +25,7 @@ except ImportError:
|
|
| 28 |
class LintCodingAgentEnvironment(Environment):
|
| 29 |
"""
|
| 30 |
Advanced Repository Sandbox.
|
| 31 |
-
|
| 32 |
"""
|
| 33 |
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 34 |
|
|
@@ -38,61 +35,65 @@ class LintCodingAgentEnvironment(Environment):
|
|
| 38 |
self.failed_queue = []
|
| 39 |
self.vfs: Dict[str, str] = {}
|
| 40 |
|
| 41 |
-
# ---
|
| 42 |
-
#
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# Candidate paths for QUESTIONS.json
|
| 46 |
-
candidates = [
|
| 47 |
-
os.path.join(current_file_dir, "QUESTIONS.json"),
|
| 48 |
-
os.path.join(os.getcwd(), "server", "QUESTIONS.json"),
|
| 49 |
-
os.path.join(os.getcwd(), "QUESTIONS.json"),
|
| 50 |
-
"/app/server/QUESTIONS.json"
|
| 51 |
-
]
|
| 52 |
-
|
| 53 |
-
json_path = None
|
| 54 |
-
for path in candidates:
|
| 55 |
-
if os.path.exists(path):
|
| 56 |
-
json_path = path
|
| 57 |
-
self.templates_base = os.path.join(os.path.dirname(path), "templates")
|
| 58 |
-
break
|
| 59 |
|
| 60 |
if json_path:
|
| 61 |
-
logger.info(f"
|
|
|
|
| 62 |
try:
|
| 63 |
with open(json_path, "r") as f:
|
| 64 |
self.curriculum = json.load(f)
|
| 65 |
self.all_levels = sorted(list(self.curriculum.keys()), key=int)
|
| 66 |
-
logger.info(f"π Curriculum
|
| 67 |
except Exception as e:
|
| 68 |
-
logger.error(f"β
|
| 69 |
-
self.
|
| 70 |
else:
|
| 71 |
-
logger.error(
|
| 72 |
-
self.
|
| 73 |
|
| 74 |
self.max_levels = int(self.all_levels[-1]) if self.all_levels else 1
|
| 75 |
|
| 76 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
self.curriculum = {"1": {"lang": "Python", "task": "Fallback: Init", "template_dir": "level_1", "ans": "print"}}
|
| 78 |
self.all_levels = ["1"]
|
| 79 |
-
self.templates_base = os.path.join(os.
|
| 80 |
|
| 81 |
def reset(self) -> LintCodingAgentObservation:
|
| 82 |
"""Full state reset for Phase 2 scoring."""
|
| 83 |
-
logger.info("π Resetting
|
| 84 |
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 85 |
self.level = 1
|
| 86 |
self.failed_queue = []
|
| 87 |
return self._load_template_state("Environment Reset: 15-Level Sprint Initialized.")
|
| 88 |
|
| 89 |
def _load_template_state(self, msg: str) -> LintCodingAgentObservation:
|
| 90 |
-
"""Loads physical files into the VFS with
|
| 91 |
task_data = self.curriculum.get(str(self.level), {})
|
| 92 |
template_dir_name = task_data.get("template_dir", f"level_{self.level}")
|
| 93 |
template_path = os.path.join(self.templates_base, template_dir_name)
|
| 94 |
|
| 95 |
-
logger.info(f"π
|
| 96 |
|
| 97 |
self.vfs = {}
|
| 98 |
if os.path.exists(template_path):
|
|
@@ -103,19 +104,19 @@ class LintCodingAgentEnvironment(Environment):
|
|
| 103 |
try:
|
| 104 |
with open(full_p, 'r', encoding='utf-8') as file:
|
| 105 |
self.vfs[rel_p] = file.read()
|
| 106 |
-
logger.info(f" π
|
| 107 |
except Exception as e:
|
| 108 |
-
logger.error(f" β οΈ
|
| 109 |
else:
|
| 110 |
-
logger.error(f"β
|
| 111 |
|
| 112 |
return self._get_observation(msg, 0.0, False)
|
| 113 |
|
| 114 |
def _check_hijacking(self, action: LintCodingAgentAction) -> Tuple[bool, str, float]:
|
| 115 |
-
"""
|
| 116 |
new_code = action.code_solution or ""
|
| 117 |
if len(new_code.strip()) < 10:
|
| 118 |
-
return True, "HIJACK: Solution rejected
|
| 119 |
|
| 120 |
code_only = re.sub(r'#.*', '', new_code).strip()
|
| 121 |
if len(code_only) < 5:
|
|
@@ -124,14 +125,14 @@ class LintCodingAgentEnvironment(Environment):
|
|
| 124 |
return False, "", 0.0
|
| 125 |
|
| 126 |
def _is_syntactically_valid(self, code: str, lang: str) -> Tuple[bool, str]:
|
| 127 |
-
"""
|
| 128 |
if lang.lower() == "python":
|
| 129 |
try:
|
| 130 |
ast.parse(code)
|
| 131 |
return True, "Syntax Valid"
|
| 132 |
except SyntaxError as e:
|
| 133 |
-
return False, f"SyntaxError: {e.msg}
|
| 134 |
-
return (True, "Non-Python
|
| 135 |
|
| 136 |
def step(self, action: LintCodingAgentAction) -> LintCodingAgentObservation:
|
| 137 |
self._state.step_count += 1
|
|
@@ -139,35 +140,33 @@ class LintCodingAgentEnvironment(Environment):
|
|
| 139 |
|
| 140 |
logger.info(f"π£ STEP {self._state.step_count} | Level: {self.level}")
|
| 141 |
|
| 142 |
-
# 1.
|
| 143 |
is_hijacked, hijack_msg, penalty = self._check_hijacking(action)
|
| 144 |
if is_hijacked:
|
| 145 |
logger.warning(f"π« {hijack_msg}")
|
| 146 |
return self._get_observation(hijack_msg, penalty, False)
|
| 147 |
|
| 148 |
-
# 2.
|
| 149 |
syntax_ok, error_msg = self._is_syntactically_valid(action.code_solution, task_data["lang"])
|
| 150 |
|
| 151 |
if not syntax_ok:
|
| 152 |
reward = -0.2
|
| 153 |
feedback = f"Transition Failed: {error_msg}"
|
| 154 |
-
logger.info(f"β Syntax Invalid: {error_msg}")
|
| 155 |
else:
|
|
|
|
| 156 |
logic_ok = task_data["ans"].lower() in action.code_solution.lower()
|
| 157 |
|
| 158 |
if logic_ok:
|
| 159 |
reward = 1.0
|
| 160 |
feedback = f"Level {self.level} Success."
|
| 161 |
-
logger.info(
|
| 162 |
self.level += 1
|
| 163 |
else:
|
| 164 |
reward = 0.1
|
| 165 |
feedback = f"Syntax OK | Logic Missing: {task_data['task']}"
|
| 166 |
-
logger.info(f"β οΈ Logic Mismatch.")
|
| 167 |
if str(self.level) not in self.failed_queue:
|
| 168 |
self.failed_queue.append(str(self.level))
|
| 169 |
|
| 170 |
-
# 3. Handle Progression
|
| 171 |
done = self.level > self.max_levels
|
| 172 |
|
| 173 |
if not done and reward > 0.8:
|
|
@@ -182,16 +181,12 @@ class LintCodingAgentEnvironment(Environment):
|
|
| 182 |
return LintCodingAgentObservation(
|
| 183 |
level=self.level,
|
| 184 |
language=task_data["lang"],
|
| 185 |
-
problem_statement=f"{feedback} |
|
| 186 |
code_context=json.dumps(self.vfs),
|
| 187 |
last_test_results=feedback,
|
| 188 |
reward=reward,
|
| 189 |
done=done,
|
| 190 |
-
metadata={
|
| 191 |
-
"step": self._state.step_count,
|
| 192 |
-
"vfs_files": list(self.vfs.keys()),
|
| 193 |
-
"total_levels": self.max_levels
|
| 194 |
-
}
|
| 195 |
)
|
| 196 |
|
| 197 |
@property
|
|
|
|
| 1 |
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
# All rights reserved.
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
import json
|
| 5 |
import os
|
|
|
|
| 12 |
from openenv.core.env_server.interfaces import Environment
|
| 13 |
from openenv.core.env_server.types import State
|
| 14 |
|
| 15 |
+
# Lead Architect Telemetry
|
| 16 |
logging.basicConfig(level=logging.INFO)
|
| 17 |
logger = logging.getLogger("ArchitectEnvironment")
|
| 18 |
|
|
|
|
| 25 |
class LintCodingAgentEnvironment(Environment):
|
| 26 |
"""
|
| 27 |
Advanced Repository Sandbox.
|
| 28 |
+
Enhanced with Deep Path Discovery to eliminate Docker 'File Not Found' errors.
|
| 29 |
"""
|
| 30 |
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 31 |
|
|
|
|
| 35 |
self.failed_queue = []
|
| 36 |
self.vfs: Dict[str, str] = {}
|
| 37 |
|
| 38 |
+
# --- ELITE DEEP RESOURCE DISCOVERY ---
|
| 39 |
+
# We don't guess paths; we find them.
|
| 40 |
+
json_path = self._discover_resource("QUESTIONS.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
if json_path:
|
| 43 |
+
logger.info(f"π― Resource Secured: {json_path}")
|
| 44 |
+
self.templates_base = os.path.join(os.path.dirname(json_path), "templates")
|
| 45 |
try:
|
| 46 |
with open(json_path, "r") as f:
|
| 47 |
self.curriculum = json.load(f)
|
| 48 |
self.all_levels = sorted(list(self.curriculum.keys()), key=int)
|
| 49 |
+
logger.info(f"π Curriculum Validated: {len(self.all_levels)} levels loaded.")
|
| 50 |
except Exception as e:
|
| 51 |
+
logger.error(f"β Read Failure on QUESTIONS.json: {e}")
|
| 52 |
+
self._apply_fallback()
|
| 53 |
else:
|
| 54 |
+
logger.error("β CRITICAL: Manifest QUESTIONS.json missing from all search roots.")
|
| 55 |
+
self._apply_fallback()
|
| 56 |
|
| 57 |
self.max_levels = int(self.all_levels[-1]) if self.all_levels else 1
|
| 58 |
|
| 59 |
+
def _discover_resource(self, filename: str) -> str:
|
| 60 |
+
"""Scans standard Docker and local roots to locate critical engine files."""
|
| 61 |
+
# Order of search: Current Dir -> Env Root -> App Root -> Explicit Docker Path
|
| 62 |
+
roots = [
|
| 63 |
+
os.path.dirname(os.path.abspath(__file__)),
|
| 64 |
+
os.getcwd(),
|
| 65 |
+
"/app/env/server",
|
| 66 |
+
"/app/env",
|
| 67 |
+
"/app"
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
for root in roots:
|
| 71 |
+
candidate = os.path.join(root, filename)
|
| 72 |
+
if os.path.exists(candidate):
|
| 73 |
+
return candidate
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
def _apply_fallback(self):
|
| 77 |
+
"""Prevents server crash by providing a baseline Level 1."""
|
| 78 |
self.curriculum = {"1": {"lang": "Python", "task": "Fallback: Init", "template_dir": "level_1", "ans": "print"}}
|
| 79 |
self.all_levels = ["1"]
|
| 80 |
+
self.templates_base = os.path.join(os.getcwd(), "templates")
|
| 81 |
|
| 82 |
def reset(self) -> LintCodingAgentObservation:
|
| 83 |
"""Full state reset for Phase 2 scoring."""
|
| 84 |
+
logger.info("π Resetting Sandbox State...")
|
| 85 |
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 86 |
self.level = 1
|
| 87 |
self.failed_queue = []
|
| 88 |
return self._load_template_state("Environment Reset: 15-Level Sprint Initialized.")
|
| 89 |
|
| 90 |
def _load_template_state(self, msg: str) -> LintCodingAgentObservation:
|
| 91 |
+
"""Loads physical files into the VFS with rigorous logging."""
|
| 92 |
task_data = self.curriculum.get(str(self.level), {})
|
| 93 |
template_dir_name = task_data.get("template_dir", f"level_{self.level}")
|
| 94 |
template_path = os.path.join(self.templates_base, template_dir_name)
|
| 95 |
|
| 96 |
+
logger.info(f"π VFS Switch | Level {self.level} | Root: {template_path}")
|
| 97 |
|
| 98 |
self.vfs = {}
|
| 99 |
if os.path.exists(template_path):
|
|
|
|
| 104 |
try:
|
| 105 |
with open(full_p, 'r', encoding='utf-8') as file:
|
| 106 |
self.vfs[rel_p] = file.read()
|
| 107 |
+
logger.info(f" π File Mounted: {rel_p}")
|
| 108 |
except Exception as e:
|
| 109 |
+
logger.error(f" β οΈ Read Warning {rel_p}: {e}")
|
| 110 |
else:
|
| 111 |
+
logger.error(f"β Directory Missing: {template_path}")
|
| 112 |
|
| 113 |
return self._get_observation(msg, 0.0, False)
|
| 114 |
|
| 115 |
def _check_hijacking(self, action: LintCodingAgentAction) -> Tuple[bool, str, float]:
|
| 116 |
+
"""Enforces architectural integrity."""
|
| 117 |
new_code = action.code_solution or ""
|
| 118 |
if len(new_code.strip()) < 10:
|
| 119 |
+
return True, "HIJACK: Solution rejected (insufficient depth).", -2.0
|
| 120 |
|
| 121 |
code_only = re.sub(r'#.*', '', new_code).strip()
|
| 122 |
if len(code_only) < 5:
|
|
|
|
| 125 |
return False, "", 0.0
|
| 126 |
|
| 127 |
def _is_syntactically_valid(self, code: str, lang: str) -> Tuple[bool, str]:
|
| 128 |
+
"""Structural validation using AST."""
|
| 129 |
if lang.lower() == "python":
|
| 130 |
try:
|
| 131 |
ast.parse(code)
|
| 132 |
return True, "Syntax Valid"
|
| 133 |
except SyntaxError as e:
|
| 134 |
+
return False, f"SyntaxError: {e.msg}"
|
| 135 |
+
return (True, "Non-Python") if len(code) > 5 else (False, "Empty")
|
| 136 |
|
| 137 |
def step(self, action: LintCodingAgentAction) -> LintCodingAgentObservation:
|
| 138 |
self._state.step_count += 1
|
|
|
|
| 140 |
|
| 141 |
logger.info(f"π£ STEP {self._state.step_count} | Level: {self.level}")
|
| 142 |
|
| 143 |
+
# 1. Integrity Check
|
| 144 |
is_hijacked, hijack_msg, penalty = self._check_hijacking(action)
|
| 145 |
if is_hijacked:
|
| 146 |
logger.warning(f"π« {hijack_msg}")
|
| 147 |
return self._get_observation(hijack_msg, penalty, False)
|
| 148 |
|
| 149 |
+
# 2. Evaluation Logic
|
| 150 |
syntax_ok, error_msg = self._is_syntactically_valid(action.code_solution, task_data["lang"])
|
| 151 |
|
| 152 |
if not syntax_ok:
|
| 153 |
reward = -0.2
|
| 154 |
feedback = f"Transition Failed: {error_msg}"
|
|
|
|
| 155 |
else:
|
| 156 |
+
# Check logic against manifest 'ans'
|
| 157 |
logic_ok = task_data["ans"].lower() in action.code_solution.lower()
|
| 158 |
|
| 159 |
if logic_ok:
|
| 160 |
reward = 1.0
|
| 161 |
feedback = f"Level {self.level} Success."
|
| 162 |
+
logger.info("β
Verification Passed.")
|
| 163 |
self.level += 1
|
| 164 |
else:
|
| 165 |
reward = 0.1
|
| 166 |
feedback = f"Syntax OK | Logic Missing: {task_data['task']}"
|
|
|
|
| 167 |
if str(self.level) not in self.failed_queue:
|
| 168 |
self.failed_queue.append(str(self.level))
|
| 169 |
|
|
|
|
| 170 |
done = self.level > self.max_levels
|
| 171 |
|
| 172 |
if not done and reward > 0.8:
|
|
|
|
| 181 |
return LintCodingAgentObservation(
|
| 182 |
level=self.level,
|
| 183 |
language=task_data["lang"],
|
| 184 |
+
problem_statement=f"{feedback} | Task: {task_data['task']}",
|
| 185 |
code_context=json.dumps(self.vfs),
|
| 186 |
last_test_results=feedback,
|
| 187 |
reward=reward,
|
| 188 |
done=done,
|
| 189 |
+
metadata={"step": self._state.step_count, "vfs_files": list(self.vfs.keys())}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
)
|
| 191 |
|
| 192 |
@property
|