Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- Dockerfile +17 -0
- env.py +77 -0
- inference.py +73 -0
- models.py +15 -0
- openenv.yaml +19 -0
- requirements.txt +5 -0
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight Python image
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory in the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy all your files (env.py, models.py, etc.) into the container
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
# Install the required libraries
|
| 11 |
+
RUN pip install --no-cache-dir fastapi pydantic openenv-core uvicorn
|
| 12 |
+
|
| 13 |
+
# Expose the port your env.py is running on
|
| 14 |
+
EXPOSE 8000
|
| 15 |
+
|
| 16 |
+
# Command to run your environment server
|
| 17 |
+
CMD ["python", "env.py"]
|
env.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from types import SimpleNamespace
|
| 4 |
+
from openenv.core.env_server import Environment
|
| 5 |
+
from models import MyEnvV4Observation, MyEnvV4Action
|
| 6 |
+
|
| 7 |
+
class MyEnvV4Env(Environment):
|
| 8 |
+
def __init__(self):
|
| 9 |
+
super().__init__()
|
| 10 |
+
# Realistic Dataset with Digital Seduction/Phishing markers
|
| 11 |
+
self.dataset = [
|
| 12 |
+
{
|
| 13 |
+
"sender": "dean.office@manipal.edu",
|
| 14 |
+
"subject": "B.Tech Lab Exam Schedule",
|
| 15 |
+
"body": "Please find the attached PDF for the upcoming CSE lab exams.",
|
| 16 |
+
"headers": ["SPF: Pass", "DKIM: Pass"],
|
| 17 |
+
"label": "INBOX"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"sender": "verify-account@security-amazon.net",
|
| 21 |
+
"subject": "Urgent: Your account is locked!",
|
| 22 |
+
"body": "Digital Seduction Alert: High urgency used. Click http://bit.ly/fake-link to unlock.",
|
| 23 |
+
"headers": ["SPF: Fail", "DMARC: Fail"],
|
| 24 |
+
"label": "QUARANTINE"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"sender": "prize@lottery-winner.co",
|
| 28 |
+
"subject": "Congratulations! You won $10,000",
|
| 29 |
+
"body": "Reply with your bank details to claim your cash prize immediately.",
|
| 30 |
+
"headers": ["SPF: Neutral"],
|
| 31 |
+
"label": "SPAM"
|
| 32 |
+
}
|
| 33 |
+
]
|
| 34 |
+
self.current_step = 0
|
| 35 |
+
|
| 36 |
+
async def reset(self):
|
| 37 |
+
self.current_step = 0
|
| 38 |
+
return self._get_result()
|
| 39 |
+
|
| 40 |
+
def _get_result(self, reward=0.0, done=False):
|
| 41 |
+
if self.current_step >= len(self.dataset):
|
| 42 |
+
obs = MyEnvV4Observation(
|
| 43 |
+
sender="N/A", subject="N/A", body="N/A",
|
| 44 |
+
headers=[], echoed_message="End of Data"
|
| 45 |
+
)
|
| 46 |
+
return SimpleNamespace(observation=obs, reward=reward, done=True)
|
| 47 |
+
|
| 48 |
+
data = self.dataset[self.current_step]
|
| 49 |
+
obs = MyEnvV4Observation(
|
| 50 |
+
sender=data["sender"],
|
| 51 |
+
subject=data["subject"],
|
| 52 |
+
body=data["body"],
|
| 53 |
+
headers=data["headers"],
|
| 54 |
+
echoed_message=f"Step {self.current_step + 1}"
|
| 55 |
+
)
|
| 56 |
+
return SimpleNamespace(observation=obs, reward=reward, done=done)
|
| 57 |
+
|
| 58 |
+
async def step(self, action: MyEnvV4Action):
|
| 59 |
+
if self.current_step >= len(self.dataset):
|
| 60 |
+
return self._get_result(done=True)
|
| 61 |
+
|
| 62 |
+
correct_label = self.dataset[self.current_step]["label"]
|
| 63 |
+
# Exact match reward logic for 0.0 - 1.0 range
|
| 64 |
+
reward = 1.0 if action.message.strip().upper() == correct_label else 0.0
|
| 65 |
+
|
| 66 |
+
self.current_step += 1
|
| 67 |
+
done = self.current_step >= len(self.dataset)
|
| 68 |
+
|
| 69 |
+
return self._get_result(reward=reward, done=done)
|
| 70 |
+
|
| 71 |
+
async def close(self):
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
@classmethod
|
| 75 |
+
async def from_docker_image(cls, image_name: str):
|
| 76 |
+
"""Simulated helper for local/containerized runs."""
|
| 77 |
+
return cls()
|
inference.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import textwrap
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
from my_env_v4 import MyEnvV4Action, MyEnvV4Env
|
| 7 |
+
|
| 8 |
+
# Environment Configuration
|
| 9 |
+
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 10 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 11 |
+
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
| 12 |
+
TASK_NAME = "email-triage"
|
| 13 |
+
BENCHMARK = "mit-manipal-v4"
|
| 14 |
+
MAX_STEPS = 3
|
| 15 |
+
SUCCESS_THRESHOLD = 0.5
|
| 16 |
+
|
| 17 |
+
SYSTEM_PROMPT = """
|
| 18 |
+
You are an Email Security Agent. Triage the following email based on sender, headers, and body content.
|
| 19 |
+
Digital Seduction Rules:
|
| 20 |
+
- 'INBOX': Official domains (.edu, .gov) and passed security headers.
|
| 21 |
+
- 'SPAM': Marketing, gambling, or generic lottery win claims.
|
| 22 |
+
- 'QUARANTINE': Phishing, high-urgency threats, suspicious links (.net, .co), or failed headers (SPF/DMARC Fail).
|
| 23 |
+
|
| 24 |
+
REPLY WITH EXACTLY ONE WORD: 'INBOX', 'SPAM', or 'QUARANTINE'.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def log_start():
|
| 28 |
+
print(f"[START] task={TASK_NAME} env={BENCHMARK} model={MODEL_NAME}", flush=True)
|
| 29 |
+
|
| 30 |
+
def log_step(step, action, reward, done):
|
| 31 |
+
print(f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error=null", flush=True)
|
| 32 |
+
|
| 33 |
+
def log_end(success, steps, score, rewards):
|
| 34 |
+
r_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 35 |
+
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={r_str}", flush=True)
|
| 36 |
+
|
| 37 |
+
async def main():
|
| 38 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 39 |
+
env = MyEnvV4Env() # Local instance for testing, can use from_docker_image if needed
|
| 40 |
+
|
| 41 |
+
rewards = []
|
| 42 |
+
log_start()
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
result = await env.reset()
|
| 46 |
+
for step in range(1, MAX_STEPS + 1):
|
| 47 |
+
if result.done: break
|
| 48 |
+
|
| 49 |
+
obs = result.observation
|
| 50 |
+
prompt = f"Sender: {obs.sender}\nSubject: {obs.subject}\nBody: {obs.body}\nHeaders: {obs.headers}"
|
| 51 |
+
|
| 52 |
+
# OpenAI Call
|
| 53 |
+
response = client.chat.completions.create(
|
| 54 |
+
model=MODEL_NAME,
|
| 55 |
+
messages=[{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}],
|
| 56 |
+
max_tokens=10,
|
| 57 |
+
temperature=0.0 # Deterministic for testing
|
| 58 |
+
)
|
| 59 |
+
action_text = response.choices[0].message.content.strip().upper()
|
| 60 |
+
|
| 61 |
+
result = await env.step(MyEnvV4Action(message=action_text))
|
| 62 |
+
rewards.append(result.reward)
|
| 63 |
+
|
| 64 |
+
log_step(step, action_text, result.reward, result.done)
|
| 65 |
+
if result.done: break
|
| 66 |
+
|
| 67 |
+
total_score = sum(rewards) / MAX_STEPS
|
| 68 |
+
log_end(total_score >= SUCCESS_THRESHOLD, len(rewards), total_score, rewards)
|
| 69 |
+
finally:
|
| 70 |
+
await env.close()
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
asyncio.run(main())
|
models.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import Field
|
| 2 |
+
from typing import Optional, List
|
| 3 |
+
from openenv.core.env_server import Action, Observation
|
| 4 |
+
|
| 5 |
+
class MyEnvV4Observation(Observation):
|
| 6 |
+
"""What the Agent sees."""
|
| 7 |
+
sender: str
|
| 8 |
+
subject: str
|
| 9 |
+
body: str
|
| 10 |
+
headers: List[str]
|
| 11 |
+
echoed_message: str = "" # Required by sample inference script contract
|
| 12 |
+
|
| 13 |
+
class MyEnvV4Action(Action):
|
| 14 |
+
"""What the Agent chooses."""
|
| 15 |
+
message: str = Field(..., description="Action string: 'INBOX', 'SPAM', or 'QUARANTINE'")
|
openenv.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Metadata for OpenEnv Triage Agent
|
| 2 |
+
name: "mail-triage-v4"
|
| 3 |
+
version: "1.0.0"
|
| 4 |
+
description: "An automated triage agent for university mailboxes, specializing in Digital Seduction detection."
|
| 5 |
+
|
| 6 |
+
# Environment Specification
|
| 7 |
+
repo_url: "https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME"
|
| 8 |
+
task_type: "classification"
|
| 9 |
+
|
| 10 |
+
# Compliance Metrics
|
| 11 |
+
reward_range: [0.0, 1.0]
|
| 12 |
+
tags:
|
| 13 |
+
- security
|
| 14 |
+
- nlp
|
| 15 |
+
- mit-manipal-hackathon
|
| 16 |
+
|
| 17 |
+
# Typed Model References
|
| 18 |
+
observation_space: "models.MyEnvV4Observation"
|
| 19 |
+
action_space: "models.MyEnvV4Action"
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn==0.27.0
|
| 3 |
+
pydantic==2.6.1
|
| 4 |
+
openenv-core==0.1.5
|
| 5 |
+
openai==1.12.0
|