Muditsharmaaa commited on
Commit
ef84164
·
0 Parent(s):

Final hackathon submission

Browse files
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy project files
10
+ COPY . .
11
+
12
+ # Expose the default Hugging Face Space port
13
+ EXPOSE 7860
14
+
15
+ # Run the FastAPI server
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenEnv: Blockchain Certificate Administrator
2
+
3
+ ## 🌍 Environment Description & Motivation
4
+ The **Blockchain Certificate Administrator** environment simulates a real-world Web3 operations task. As educational institutions move toward blockchain-based credentialing, automated agents are needed to act as smart contract gatekeepers.
5
+
6
+ Instead of a toy game, this environment challenges the agent to process messy, real-world student data, strictly enforce business logic (grades >= 70, valid 42-character 0x wallet addresses), and manage a finite resource (gas balance) to successfully mint certificates without wasting funds.
7
+
8
+ ## 📊 Observation and Action Spaces
9
+
10
+ ### Observation Space
11
+ The agent observes the current state of the blockchain administration queue:
12
+ * `pending_requests` (List): A batch of student requests containing `request_id`, `student_name`, `course_name`, `final_score`, and `wallet_address`.
13
+ * `gas_balance` (Float): The remaining gas in the administrative wallet.
14
+ * `current_step` (Int): The current episode step.
15
+ * `last_error` (String | Null): Feedback if the previous JSON action was malformed.
16
+
17
+ ### Action Space
18
+ The agent must output a strictly typed JSON list of decisions:
19
+ * `decisions` (List): Contains objects with `request_id`, `decision` (strictly "mint" or "reject"), and a brief `reason`.
20
+
21
+ ## 🏆 Task Descriptions & Difficulty
22
+ 1. **Easy (`easy_mint`)**: The agent must successfully read the state and execute basic 'mint' actions without formatting errors, achieving at least a 30% success rate.
23
+ 2. **Medium (`medium_filter`)**: The agent must accurately filter out invalid requests (e.g., scores < 70, malformed wallets like missing '0x' or wrong length).
24
+ 3. **Hard (`hard_gas_management`)**: The agent must process consecutive batches of requests perfectly. Any invalid mints waste gas, which causes early termination and a score of 0.0.
25
+
26
+ ## 🚀 Setup and Usage Instructions
27
+
28
+ **1. Install Dependencies**
29
+ ```bash
30
+ pip install -r requirements.txt
__pycache__/env.cpython-312.pyc ADDED
Binary file (6.05 kB). View file
 
__pycache__/models.cpython-312.pyc ADDED
Binary file (2.86 kB). View file
 
env.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import string
3
+ from pydantic import BaseModel
4
+ from typing import List, Optional
5
+
6
+ # Import our schemas from models.py
7
+ from models import (
8
+ CertificateRequest, ActionDecision, BlockchainObservation,
9
+ BlockchainAction, BlockchainReward
10
+ )
11
+
12
+ # OpenEnv typically expects a return object with these fields for step/reset
13
+ class EnvResult(BaseModel):
14
+ observation: BlockchainObservation
15
+ reward: float = 0.0
16
+ done: bool = False
17
+ info: dict = {}
18
+
19
+ class BlockchainEnv:
20
+ def __init__(self, max_steps: int = 5):
21
+ self.max_steps = max_steps
22
+ self.current_step = 0
23
+ self.gas_balance = 0.0
24
+ self.pending_requests: List[CertificateRequest] = []
25
+ self.last_error: Optional[str] = None
26
+ self.gas_cost_per_mint = 0.1
27
+
28
+ def _generate_mock_wallet(self, valid: bool) -> str:
29
+ """Helper to generate realistic wallets to trick the agent."""
30
+ chars = ''.join(random.choices(string.ascii_lowercase + string.digits, k=40))
31
+ if valid:
32
+ return f"0x{chars}"
33
+ else:
34
+ # Invalid wallets: might be missing '0x', or wrong length
35
+ return random.choice([f"0x{chars[:30]}", f"1x{chars}", chars])
36
+
37
+ def _generate_requests(self, batch_size: int = 3) -> List[CertificateRequest]:
38
+ """Generates a mix of valid and invalid requests for the agent to process."""
39
+ requests = []
40
+ for i in range(batch_size):
41
+ is_valid = random.choice([True, False])
42
+ score = random.uniform(70.0, 99.0) if is_valid else random.uniform(40.0, 69.9)
43
+ wallet = self._generate_mock_wallet(is_valid)
44
+
45
+ requests.append(CertificateRequest(
46
+ request_id=f"REQ-{random.randint(1000, 9999)}",
47
+ student_name=f"Student_{i}",
48
+ course_name="Intro to Web3",
49
+ final_score=round(score, 2),
50
+ wallet_address=wallet
51
+ ))
52
+ return requests
53
+
54
+ def state(self) -> BlockchainObservation:
55
+ """Returns the current state without advancing the environment."""
56
+ return BlockchainObservation(
57
+ pending_requests=self.pending_requests,
58
+ gas_balance=round(self.gas_balance, 2),
59
+ current_step=self.current_step,
60
+ last_error=self.last_error
61
+ )
62
+
63
+ def reset(self) -> EnvResult:
64
+ """Resets the environment for a new episode."""
65
+ self.current_step = 0
66
+ self.gas_balance = 2.0 # Enough for roughly 20 valid mints
67
+ self.last_error = None
68
+ self.pending_requests = self._generate_requests(batch_size=3)
69
+
70
+ return EnvResult(observation=self.state())
71
+
72
+ def step(self, action: BlockchainAction) -> EnvResult:
73
+ """Processes the agent's action, updates state, and calculates reward."""
74
+ self.current_step += 1
75
+ self.last_error = None
76
+ step_reward = 0.0
77
+ correct_decisions = 0
78
+ total_requests = len(self.pending_requests)
79
+
80
+ # Create a dictionary of actions for easy lookup
81
+ decision_map = {dec.request_id: dec.decision for dec in action.decisions}
82
+
83
+ for req in self.pending_requests:
84
+ # Ground truth validation logic
85
+ is_valid_score = req.final_score >= 70.0
86
+ is_valid_wallet = req.wallet_address.startswith("0x") and len(req.wallet_address) == 42
87
+ should_mint = is_valid_score and is_valid_wallet
88
+
89
+ agent_decision = decision_map.get(req.request_id)
90
+
91
+ if agent_decision == "mint":
92
+ if should_mint and self.gas_balance >= self.gas_cost_per_mint:
93
+ self.gas_balance -= self.gas_cost_per_mint
94
+ correct_decisions += 1
95
+ else:
96
+ # Penalize for minting invalid certs or trying to mint without gas
97
+ step_reward -= 0.5
98
+ elif agent_decision == "reject":
99
+ if not should_mint:
100
+ correct_decisions += 1
101
+ else:
102
+ # Penalize for rejecting a valid student
103
+ step_reward -= 0.5
104
+ else:
105
+ self.last_error = f"Missing or invalid decision for {req.request_id}"
106
+
107
+ # Calculate partial reward for this step (max 1.0 per step for perfect batch)
108
+ if total_requests > 0:
109
+ step_reward += (correct_decisions / total_requests)
110
+
111
+ # Clamp reward to ensure it stays somewhat bounded
112
+ step_reward = max(0.0, min(1.0, step_reward))
113
+
114
+ # Check if episode is done
115
+ done = self.current_step >= self.max_steps or self.gas_balance <= 0
116
+
117
+ # Generate next batch if not done
118
+ if not done:
119
+ self.pending_requests = self._generate_requests(batch_size=3)
120
+ else:
121
+ self.pending_requests = []
122
+
123
+ return EnvResult(
124
+ observation=self.state(),
125
+ reward=step_reward,
126
+ done=done,
127
+ info={"correct_decisions": correct_decisions, "total_batch": total_requests}
128
+ )
graders.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Any
2
+
3
+ # A trajectory is typically passed as a list of dictionaries containing step data
4
+ # e.g., [{"reward": 0.5, "done": False, "info": {"correct_decisions": 2, "total_batch": 3}}, ...]
5
+
6
+ def _calculate_base_score(trajectory: List[dict]) -> float:
7
+ """Helper function to calculate the average reward across all steps."""
8
+ if not trajectory:
9
+ return 0.0
10
+
11
+ total_reward = sum(step.get("reward", 0.0) for step in trajectory)
12
+ max_possible_reward = len(trajectory) * 1.0 # 1.0 is max reward per step
13
+
14
+ if max_possible_reward == 0:
15
+ return 0.0
16
+
17
+ score = total_reward / max_possible_reward
18
+ return max(0.0, min(1.0, score)) # Clamp between 0.0 and 1.0
19
+
20
+ def grade_easy(trajectory: List[dict], env_state: Any = None) -> float:
21
+ """
22
+ Easy Task: Did the agent manage to get at least a 30% success rate?
23
+ This proves it understands the basic JSON schema and can make a 'mint' decision.
24
+ """
25
+ score = _calculate_base_score(trajectory)
26
+ # If they hit 30% efficiency, we give them full marks for the easy task
27
+ return 1.0 if score >= 0.3 else (score / 0.3)
28
+
29
+ def grade_medium(trajectory: List[dict], env_state: Any = None) -> float:
30
+ """
31
+ Medium Task: The agent needs to successfully filter out the bad wallets/scores.
32
+ Requires at least 60% overall efficiency.
33
+ """
34
+ score = _calculate_base_score(trajectory)
35
+ # Scales up to 1.0 based on hitting the 60% benchmark
36
+ if score >= 0.6:
37
+ return 1.0
38
+ return max(0.0, score)
39
+
40
+ def grade_hard(trajectory: List[dict], env_state: Any = None) -> float:
41
+ """
42
+ Hard Task: Strict grading.
43
+ The agent must achieve near perfection (90%+) and not run out of gas
44
+ (which would result in a premature 'done' flag with low rewards).
45
+ """
46
+ score = _calculate_base_score(trajectory)
47
+
48
+ # If the episode ended early because they wasted gas on bad mints, penalize heavily
49
+ if len(trajectory) < 5 and trajectory[-1].get("reward", 0.0) < 0:
50
+ return 0.0
51
+
52
+ # Standard raw score, very hard to get 1.0 unless the agent is flawless
53
+ return score
inference.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import textwrap
4
+ from typing import List, Optional
5
+ from openai import OpenAI
6
+
7
+ from models import BlockchainAction
8
+ from env import BlockchainEnv
9
+
10
+ # --- Mandatory Hackathon Variables ---
11
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY")
12
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
13
+ MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini") # Swap to your active model
14
+ TASK_NAME = os.getenv("MY_ENV_TASK", "hard_gas_management")
15
+ BENCHMARK = "blockchain_certificate_admin"
16
+ MAX_STEPS = 5
17
+
18
+ SYSTEM_PROMPT = textwrap.dedent(
19
+ """
20
+ You are a Smart Contract Administrator AI.
21
+ You will receive a list of student certificate requests and your current gas balance.
22
+ Rules:
23
+ 1. Only 'mint' if final_score >= 70 AND wallet_address is exactly 42 chars starting with '0x'.
24
+ 2. Otherwise, 'reject'.
25
+ 3. Minting costs 0.1 gas. Do not mint if gas balance is insufficient.
26
+
27
+ You MUST respond with valid JSON matching this schema exactly:
28
+ {
29
+ "decisions": [
30
+ {"request_id": "REQ-1234", "decision": "mint", "reason": "Valid score and wallet"},
31
+ {"request_id": "REQ-5678", "decision": "reject", "reason": "Invalid wallet length"}
32
+ ]
33
+ }
34
+ Output ONLY JSON. No markdown formatting, no backticks.
35
+ """
36
+ ).strip()
37
+
38
+ def log_start(task: str, env: str, model: str) -> None:
39
+ print(f"[START] task={task} env={env} model={model}", flush=True)
40
+
41
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
42
+ error_val = error if error else "null"
43
+ print(f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error={error_val}", flush=True)
44
+
45
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
46
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
47
+ print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
48
+
49
+ def main():
50
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
51
+ env = BlockchainEnv(max_steps=MAX_STEPS)
52
+
53
+ history: List[str] = []
54
+ rewards: List[float] = []
55
+ steps_taken = 0
56
+ score = 0.0
57
+
58
+ log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
59
+
60
+ # Initialize Environment
61
+ result = env.reset()
62
+
63
+ try:
64
+ for step in range(1, MAX_STEPS + 1):
65
+ if result.done:
66
+ break
67
+
68
+ obs = result.observation
69
+ user_prompt = f"Pending Requests: {obs.pending_requests}\nGas Balance: {obs.gas_balance}\nStep: {step}"
70
+
71
+ try:
72
+ # Call LLM
73
+ completion = client.chat.completions.create(
74
+ model=MODEL_NAME,
75
+ messages=[
76
+ {"role": "system", "content": SYSTEM_PROMPT},
77
+ {"role": "user", "content": user_prompt},
78
+ ],
79
+ temperature=0.0, # Deterministic logic preferred here
80
+ )
81
+ raw_response = (completion.choices[0].message.content or "").strip()
82
+
83
+ # Parse JSON and execute action
84
+ action_dict = json.loads(raw_response)
85
+ action = BlockchainAction(**action_dict)
86
+ result = env.step(action)
87
+
88
+ reward = result.reward
89
+ error = None
90
+ action_str = json.dumps(action_dict).replace(" ", "") # Compress for STDOUT
91
+
92
+ except Exception as e:
93
+ # If the LLM hallucinates or JSON parsing fails
94
+ reward = -1.0
95
+ error = str(e).replace("\n", " ")
96
+ action_str = "invalid_format"
97
+ result.done = True # End episode on fatal format error
98
+
99
+ rewards.append(reward)
100
+ steps_taken = step
101
+
102
+ log_step(step=step, action=action_str, reward=reward, done=result.done, error=error)
103
+
104
+ if result.done:
105
+ break
106
+
107
+ # Calculate final normalized score
108
+ score = sum(rewards) / steps_taken if steps_taken > 0 else 0.0
109
+ score = max(0.0, min(1.0, score))
110
+ success = score >= 0.6
111
+
112
+ finally:
113
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
114
+
115
+ if __name__ == "__main__":
116
+ main()
models.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional, Literal
3
+
4
+ # --- Nested Models for cleaner structure ---
5
+ class CertificateRequest(BaseModel):
6
+ request_id: str
7
+ student_name: str
8
+ course_name: str
9
+ final_score: float = Field(description="Score out of 100. Must be >= 70 to pass.")
10
+ wallet_address: str = Field(description="Must start with '0x' and be exactly 42 characters long.")
11
+
12
+ class ActionDecision(BaseModel):
13
+ request_id: str
14
+ decision: Literal["mint", "reject"] = Field(description="Whether to mint the certificate or reject the request.")
15
+ reason: str = Field(description="Short explanation of why it was minted or rejected.")
16
+
17
+ # ---------------------------------------------------------
18
+ # OBSERVATION SPACE (What the agent sees)
19
+ # ---------------------------------------------------------
20
+ class BlockchainObservation(BaseModel):
21
+ pending_requests: List[CertificateRequest] = Field(
22
+ description="List of student certificate requests awaiting processing."
23
+ )
24
+ gas_balance: float = Field(
25
+ description="Remaining gas balance in the admin wallet to pay for minting."
26
+ )
27
+ current_step: int = Field(
28
+ description="The current step in the episode."
29
+ )
30
+ last_error: Optional[str] = Field(
31
+ default=None,
32
+ description="Error message if the previous action was invalid."
33
+ )
34
+
35
+ # ---------------------------------------------------------
36
+ # ACTION SPACE (What the agent can do)
37
+ # ---------------------------------------------------------
38
+ class BlockchainAction(BaseModel):
39
+ decisions: List[ActionDecision] = Field(
40
+ description="The agent's decisions for the pending requests."
41
+ )
42
+
43
+ # ---------------------------------------------------------
44
+ # REWARD SPACE (How the agent is scored)
45
+ # ---------------------------------------------------------
46
+ class BlockchainReward(BaseModel):
47
+ score: float = Field(
48
+ ge=0.0, le=1.0,
49
+ description="Normalized score between 0.0 and 1.0 based on correct mints and rejections."
50
+ )
51
+ feedback: str = Field(
52
+ description="Feedback detailing gas wasted, incorrect mints, or missed valid certificates."
53
+ )
openenv.yaml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "1.0"
2
+ name: "blockchain_certificate_admin"
3
+ description: "An agent acts as a smart contract administrator, processing student certificates. It must validate scores, check for 42-character 0x wallet addresses, and manage its gas balance to successfully mint."
4
+
5
+ # Pointers to the Pydantic models we wrote in Step 2
6
+ observation_space: "models:BlockchainObservation"
7
+ action_space: "models:BlockchainAction"
8
+ reward_space: "models:BlockchainReward"
9
+
10
+ # Pointer to the core engine we wrote in Step 3
11
+ environment: "env:BlockchainEnv"
12
+
13
+ # The 3 mandatory tasks ranging from Easy to Hard
14
+ tasks:
15
+ - name: "easy_mint"
16
+ description: "Mint completely valid certificates without making syntax errors."
17
+ grader: "graders:grade_easy"
18
+
19
+ - name: "medium_filter"
20
+ description: "Successfully identify and reject invalid requests (e.g., scores under 70, malformed wallets)."
21
+ grader: "graders:grade_medium"
22
+
23
+ - name: "hard_gas_management"
24
+ description: "Process a full batch of complex requests over multiple steps without running out of gas due to penalizations."
25
+ grader: "graders:grade_hard"
pyproject.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "blockchain_certificate_admin"
7
+ version = "1.0.0"
8
+ description = "OpenEnv Blockchain Certificate environment"
9
+ dependencies = [
10
+ "pydantic>=2.0.0",
11
+ "openenv-core",
12
+ "openai",
13
+ "fastapi",
14
+ "uvicorn"
15
+ ]
16
+
17
+ [project.scripts]
18
+ server = "server.app:main"
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ openenv-core
2
+ pydantic>=2.0.0
3
+ openai
4
+ fastapi
5
+ uvicorn
server/app.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import uvicorn
4
+ from fastapi import FastAPI
5
+
6
+ # Ensure the server can find your root models.py and env.py
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+
9
+ from env import BlockchainEnv
10
+ from models import BlockchainAction
11
+
12
+ app = FastAPI()
13
+ env = BlockchainEnv()
14
+
15
+ @app.post("/reset")
16
+ def reset_env():
17
+ result = env.reset()
18
+ return {"status": "ok", "observation": result.observation.model_dump()}
19
+
20
+ @app.post("/step")
21
+ def step_env(action: BlockchainAction):
22
+ result = env.step(action)
23
+ return {
24
+ "observation": result.observation.model_dump(),
25
+ "reward": result.reward,
26
+ "done": result.done,
27
+ "info": result.info
28
+ }
29
+
30
+ @app.get("/state")
31
+ def get_state():
32
+ return env.state().model_dump()
33
+
34
+ # The entry point the validator is looking for
35
+ # The entry point the validator is looking for
36
+ def main():
37
+ uvicorn.run("server.app:app", host="0.0.0.0", port=7860)
38
+
39
+ if __name__ == "__main__":
40
+ main()
uv.lock ADDED
The diff for this file is too large to render. See raw diff