Armaansaini20 commited on
Commit Β·
c17c7eb
0
Parent(s):
Meta OpenEnv Submission: AegisGym v1.0.0
Browse files- Dockerfile +15 -0
- README.md +56 -0
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/client_env.cpython-313.pyc +0 -0
- __pycache__/grader.cpython-313.pyc +0 -0
- __pycache__/models.cpython-313.pyc +0 -0
- __pycache__/server.cpython-313.pyc +0 -0
- __pycache__/train.cpython-313.pyc +0 -0
- app.py +5 -0
- audit_logs.txt +0 -0
- audit_logs_full.txt +0 -0
- baseline_inference.py +52 -0
- check_consistency.py +28 -0
- client.py +46 -0
- client_env.py +67 -0
- deploy_hf.py +74 -0
- generate_matching_report.py +23 -0
- grader.py +49 -0
- inference.py +62 -0
- matching_dataset_logs.md +61 -0
- models.py +23 -0
- openenv.yaml +19 -0
- requirements.txt +4 -0
- sample_dataset.py +19 -0
- server.py +88 -0
- simulation.py +82 -0
- synced_report.json +44 -0
- synced_simulation.py +75 -0
- train.py +155 -0
- verify_consistency.py +26 -0
- verify_dataset_rollout.py +27 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
COPY requirements.txt .
|
| 8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
EXPOSE 8000
|
| 13 |
+
|
| 14 |
+
# Start OpenEnv API server
|
| 15 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AegisGym: Financial Compliance & AML Sandbox
|
| 2 |
+
|
| 3 |
+
AegisGym is a reinforcement learning sandbox for training and evaluating autonomous financial auditors. It simulates real-world banking compliance tasks, including Sanction Checks, Anti-Money Laundering (AML) detection, and Regulatory Alignment.
|
| 4 |
+
|
| 5 |
+
## π¦ Motivation
|
| 6 |
+
Financial institutions process millions of transactions daily. Human auditors often struggle with "smurfing" hidden in noise or complex regulatory clauses across jurisdictions. AegisGym provides a rigorous environment to train LLM-based agents to detect financial crime with high explainability and logical reasoning.
|
| 7 |
+
|
| 8 |
+
## π οΈ Environment Specification
|
| 9 |
+
|
| 10 |
+
### π Action Space
|
| 11 |
+
The agent must provide an `AuditAction`:
|
| 12 |
+
- `action_type`: `APPROVE`, `FLAG`, or `REQUEST_INFO`.
|
| 13 |
+
- `target_id`: The ID of the account or transaction.
|
| 14 |
+
- `regulation_citation`: A string citing the relevant regulation (e.g., "BSA-31-USC-5318").
|
| 15 |
+
|
| 16 |
+
### ποΈ Observation Space
|
| 17 |
+
The agent receives an `AuditObservation`:
|
| 18 |
+
- `transactions`: A list of recent transaction dictionaries.
|
| 19 |
+
- `account_metadata`: Details about the account age, tier, and history.
|
| 20 |
+
- `retrieved_regs`: Relevant regulatory guidelines fetched via RAG.
|
| 21 |
+
|
| 22 |
+
### π― Tasks & Difficulty
|
| 23 |
+
| Task ID | Name | Difficulty | Description |
|
| 24 |
+
|---------|------|------------|-------------|
|
| 25 |
+
| `easy_audit` | Sanction Check | Easy | Identify accounts on a blocklist. |
|
| 26 |
+
| `medium_audit` | Smurfing Detection | Medium | Detect structuring patterns under withdrawal limits. |
|
| 27 |
+
| `hard_audit` | Regulatory Alignment | Hard | Accurately cite complex regulations for high-risk tx. |
|
| 28 |
+
|
| 29 |
+
## π Setup & Usage
|
| 30 |
+
|
| 31 |
+
### Prerequisites
|
| 32 |
+
- Python 3.10+
|
| 33 |
+
- `openenv-core`
|
| 34 |
+
- Hugging Face Space (for deployment)
|
| 35 |
+
|
| 36 |
+
### Installation
|
| 37 |
+
```bash
|
| 38 |
+
pip install -r requirements.txt
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### Running Locally
|
| 42 |
+
```bash
|
| 43 |
+
uvicorn app:app --host 0.0.0.0 --port 7860
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Baseline Inference
|
| 47 |
+
```bash
|
| 48 |
+
export API_BASE_URL="https://api.openai.com/v1"
|
| 49 |
+
export MODEL_NAME="gpt-4o"
|
| 50 |
+
export OPENAI_API_KEY="your_key"
|
| 51 |
+
python inference.py
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## π Deployment
|
| 55 |
+
This environment is designed for Hugging Face Spaces. Use the provided `Dockerfile`.
|
| 56 |
+
- **Public URL:** [armaan020/AegisGym](https://huggingface.co/spaces/armaan020/AegisGym)
|
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (364 Bytes). View file
|
|
|
__pycache__/client_env.cpython-313.pyc
ADDED
|
Binary file (3.8 kB). View file
|
|
|
__pycache__/grader.cpython-313.pyc
ADDED
|
Binary file (2.75 kB). View file
|
|
|
__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (2.09 kB). View file
|
|
|
__pycache__/server.cpython-313.pyc
ADDED
|
Binary file (5.46 kB). View file
|
|
|
__pycache__/train.cpython-313.pyc
ADDED
|
Binary file (6.93 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openenv.core.env_server import create_fastapi_app
|
| 2 |
+
from server import AegisGymEnv
|
| 3 |
+
from models import AuditAction, AuditObservation
|
| 4 |
+
|
| 5 |
+
app = create_fastapi_app(AegisGymEnv, AuditAction, AuditObservation)
|
audit_logs.txt
ADDED
|
Binary file (2.66 kB). View file
|
|
|
audit_logs_full.txt
ADDED
|
Binary file (4.33 kB). View file
|
|
|
baseline_inference.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from server import AegisGymEnv
|
| 3 |
+
from models import AuditAction
|
| 4 |
+
|
| 5 |
+
def main():
|
| 6 |
+
print("--- AegisGym Final Baseline Inference Simulation (Official API) ---")
|
| 7 |
+
|
| 8 |
+
env = AegisGymEnv()
|
| 9 |
+
obs = env.reset()
|
| 10 |
+
|
| 11 |
+
total_score = 0.0
|
| 12 |
+
episodes = 10
|
| 13 |
+
|
| 14 |
+
for i in range(episodes):
|
| 15 |
+
tier = env.state.current_tier
|
| 16 |
+
print(f"\nEpisode {i+1} | Tier: {tier.upper()}")
|
| 17 |
+
|
| 18 |
+
# Mocking an agent's decision based on the tier
|
| 19 |
+
if tier == "easy":
|
| 20 |
+
action = AuditAction(
|
| 21 |
+
action_type="FLAG",
|
| 22 |
+
target_id="ACC-BL-001",
|
| 23 |
+
regulation_citation="Sanctions List Match"
|
| 24 |
+
)
|
| 25 |
+
elif tier == "medium":
|
| 26 |
+
action = AuditAction(
|
| 27 |
+
action_type="FLAG",
|
| 28 |
+
target_id="ACC-SMURF-99",
|
| 29 |
+
regulation_citation="Structuring/Smurfing 9000-10000 range"
|
| 30 |
+
)
|
| 31 |
+
else:
|
| 32 |
+
action = AuditAction(
|
| 33 |
+
action_type="FLAG",
|
| 34 |
+
target_id="ACC-REG-VIOLATOR",
|
| 35 |
+
regulation_citation="EU-AI-Act-Art-57"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# step() returns an AuditObservation instance only
|
| 39 |
+
obs = env.step(action)
|
| 40 |
+
print(f"Action Taken: {action.action_type} for {action.target_id}")
|
| 41 |
+
print(f"Reward: {obs.reward} | Done: {obs.done}")
|
| 42 |
+
|
| 43 |
+
total_score += obs.reward
|
| 44 |
+
if obs.done:
|
| 45 |
+
break
|
| 46 |
+
|
| 47 |
+
print(f"\n--- Reproducibility Report ---")
|
| 48 |
+
print(f"Total Episodes: {min(episodes, env.state.step_count)}")
|
| 49 |
+
print(f"Custom Agent Mean Score (Reward): {total_score / min(episodes, env.state.step_count)}")
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
main()
|
check_consistency.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
def check():
|
| 4 |
+
path = "audit_logs.txt"
|
| 5 |
+
if not os.path.exists(path):
|
| 6 |
+
print("Logs not found.")
|
| 7 |
+
return
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
# PowerShell results might be in UTF-16
|
| 11 |
+
with open(path, "rb") as f:
|
| 12 |
+
raw = f.read()
|
| 13 |
+
content = raw.decode("utf-16", "ignore")
|
| 14 |
+
except:
|
| 15 |
+
content = open(path, "r", errors="ignore").read()
|
| 16 |
+
|
| 17 |
+
questions = [
|
| 18 |
+
"What are the requirements to be listed on the Nasdaq?",
|
| 19 |
+
"When was Rule. 2010"
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
print("=== Consistency Check ===")
|
| 23 |
+
for q in questions:
|
| 24 |
+
found = q.lower() in content.lower()
|
| 25 |
+
print(f"Found '{q}': {found}")
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
check()
|
client.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openenv.core.env_client import EnvClient
|
| 2 |
+
from models import ComplianceAction
|
| 3 |
+
from server import ComplianceEnv
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
def run_test():
|
| 7 |
+
print("Initializing test environment...")
|
| 8 |
+
|
| 9 |
+
# In a real setup, client talks to a served environment via EnvClient
|
| 10 |
+
# For local demonstration, we instantiate the Env directly
|
| 11 |
+
env = ComplianceEnv()
|
| 12 |
+
|
| 13 |
+
obs = env.reset()
|
| 14 |
+
print("Initial Observation:")
|
| 15 |
+
print(obs.model_dump_json(indent=2))
|
| 16 |
+
|
| 17 |
+
# Simulate a bad action (approve a sanctioned entity)
|
| 18 |
+
# Let's force it to be a sanctioned entity for testing
|
| 19 |
+
env.db.current_entity = "EvilCorp"
|
| 20 |
+
|
| 21 |
+
action1 = ComplianceAction(
|
| 22 |
+
decision="APPROVE",
|
| 23 |
+
reasoning="The transaction seems fine.",
|
| 24 |
+
cited_regulation_id="NONE"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
new_obs, reward, done, info = env.step(action1)
|
| 28 |
+
print("\nAction 1: APPROVE EvilCorp")
|
| 29 |
+
print(f"Reward: {reward} (Expect negative)")
|
| 30 |
+
print(f"Trace info: {info}")
|
| 31 |
+
|
| 32 |
+
# Simulate a good action
|
| 33 |
+
env.db.current_entity = "EvilCorp"
|
| 34 |
+
action2 = ComplianceAction(
|
| 35 |
+
decision="BLOCK",
|
| 36 |
+
reasoning="Entity is on the sanctions list. Blocking as per AML regulations.",
|
| 37 |
+
cited_regulation_id="EU-AML-01"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
new_obs, reward, done, info = env.step(action2)
|
| 41 |
+
print("\nAction 2: BLOCK EvilCorp with good reasoning")
|
| 42 |
+
print(f"Reward: {reward} (Expect positive)")
|
| 43 |
+
print(f"Trace info: {info}")
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
run_test()
|
client_env.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AegisGym WebSocket client β concrete subclass of openenv EnvClient.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
client = AegisGymWsClient()
|
| 6 |
+
sync_client = client.sync()
|
| 7 |
+
obs = sync_client.reset()
|
| 8 |
+
obs = sync_client.step({...})
|
| 9 |
+
"""
|
| 10 |
+
from typing import Any, Dict
|
| 11 |
+
from openenv.core.env_client import EnvClient
|
| 12 |
+
from openenv.core.sync_client import SyncEnvClient
|
| 13 |
+
from models import AuditAction, AuditObservation
|
| 14 |
+
|
| 15 |
+
HF_SPACE_WSS = "wss://armaan020-aegisgym.hf.space"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class AegisGymWsClient(EnvClient):
|
| 19 |
+
"""Concrete EnvClient implementation for the AegisGym HF Space."""
|
| 20 |
+
|
| 21 |
+
def _step_payload(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
| 22 |
+
"""Convert an action dict into the WS step payload."""
|
| 23 |
+
return action if isinstance(action, dict) else action.model_dump()
|
| 24 |
+
|
| 25 |
+
def _parse_result(self, payload: Dict[str, Any]) -> Any:
|
| 26 |
+
"""Parse reset/step response from the server into usable result."""
|
| 27 |
+
return payload # keep as dict; training code accesses .observation, .reward, .done
|
| 28 |
+
|
| 29 |
+
def _parse_state(self, payload: Dict[str, Any]) -> Any:
|
| 30 |
+
"""Parse the state endpoint response."""
|
| 31 |
+
return payload
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_sync_client(ws_url: str = HF_SPACE_WSS) -> SyncEnvClient:
|
| 35 |
+
"""Return a synchronous wrapper over the WebSocket client."""
|
| 36 |
+
return AegisGymWsClient(base_url=ws_url).sync()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_live_env():
|
| 40 |
+
print("=== Testing Live AegisGym Space (wss) ===")
|
| 41 |
+
client = get_sync_client()
|
| 42 |
+
|
| 43 |
+
print("reset() ...")
|
| 44 |
+
result = client.reset()
|
| 45 |
+
print(f" result type: {type(result)}")
|
| 46 |
+
print(f" keys: {list(result.keys()) if isinstance(result, dict) else dir(result)}")
|
| 47 |
+
|
| 48 |
+
action = AuditAction(
|
| 49 |
+
action_type="FLAG",
|
| 50 |
+
target_id="ACC-BL-001",
|
| 51 |
+
regulation_citation="EU-AI-Act-Art-57"
|
| 52 |
+
).model_dump()
|
| 53 |
+
|
| 54 |
+
print("step(FLAG ACC-BL-001) ...")
|
| 55 |
+
result = client.step(action)
|
| 56 |
+
print(f" reward={result.get('reward') if isinstance(result, dict) else getattr(result,'reward',None)}")
|
| 57 |
+
print(f" done={result.get('done') if isinstance(result, dict) else getattr(result,'done',None)}")
|
| 58 |
+
|
| 59 |
+
print("state() ...")
|
| 60 |
+
s = client.state()
|
| 61 |
+
print(f" State: {s}")
|
| 62 |
+
|
| 63 |
+
print("\n=== Live environment OK! ===")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
test_live_env()
|
deploy_hf.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import subprocess
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
def check_huggingface_login():
|
| 7 |
+
"""Verify the user is logged into Hugging Face CLI."""
|
| 8 |
+
try:
|
| 9 |
+
from huggingface_hub import HfApi
|
| 10 |
+
api = HfApi()
|
| 11 |
+
api.whoami()
|
| 12 |
+
return True
|
| 13 |
+
except Exception:
|
| 14 |
+
return False
|
| 15 |
+
|
| 16 |
+
def init_space(space_name: str, private: bool = True):
|
| 17 |
+
"""Initialize a Hugging Face space for the environment."""
|
| 18 |
+
print(f"--- Initializing Hugging Face Space: {space_name} ---")
|
| 19 |
+
|
| 20 |
+
if not check_huggingface_login():
|
| 21 |
+
print("Error: You are not logged in to Hugging Face CLI. Please run 'huggingface-cli login' first.")
|
| 22 |
+
sys.exit(1)
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from huggingface_hub import HfApi
|
| 26 |
+
api = HfApi()
|
| 27 |
+
api.create_repo(repo_id=space_name, repo_type="space", space_sdk="docker", private=private, exist_ok=True)
|
| 28 |
+
print("Space created or verified successfully!")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"Failed to create space: {e}")
|
| 31 |
+
print("Note: If it already exists, deployment will proceed.")
|
| 32 |
+
|
| 33 |
+
def deploy_to_space(space_name: str):
|
| 34 |
+
"""Deploy the current directory to the Hugging Face Space."""
|
| 35 |
+
print(f"\n--- Deploying AegisGym to {space_name} ---")
|
| 36 |
+
|
| 37 |
+
# Normally this would involve git add/commit/push to the HF remote
|
| 38 |
+
# or using the huggingface_hub Python library to upload the folder seamlessly.
|
| 39 |
+
try:
|
| 40 |
+
from huggingface_hub import HfApi
|
| 41 |
+
api = HfApi()
|
| 42 |
+
print("Uploading files to Hub...")
|
| 43 |
+
|
| 44 |
+
# Don't upload the cache or local virtual environments
|
| 45 |
+
ignore_patterns = ["__pycache__/*", "*.git*", ".env", "venv/*"]
|
| 46 |
+
|
| 47 |
+
url = api.upload_folder(
|
| 48 |
+
folder_path=".",
|
| 49 |
+
repo_id=space_name,
|
| 50 |
+
repo_type="space",
|
| 51 |
+
ignore_patterns=ignore_patterns
|
| 52 |
+
)
|
| 53 |
+
print(f"Deployment successful! Your environment is live at: {url}")
|
| 54 |
+
|
| 55 |
+
except ImportError:
|
| 56 |
+
print("Error: 'huggingface_hub' is not installed. Run 'pip install huggingface_hub'.")
|
| 57 |
+
sys.exit(1)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Deployment failed: {e}")
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
parser = argparse.ArgumentParser(description="Deploy AegisGym to Hugging Face Spaces.")
|
| 63 |
+
parser.add_argument("space_name", help="The name of your huggingface space (e.g. username/aegisgym)")
|
| 64 |
+
parser.add_argument("--public", action="store_true", help="Make the space public (default is private)")
|
| 65 |
+
|
| 66 |
+
args = parser.parse_args()
|
| 67 |
+
|
| 68 |
+
init_space(args.space_name, private=not args.public)
|
| 69 |
+
deploy_to_space(args.space_name)
|
| 70 |
+
|
| 71 |
+
print("\n--- Next Steps ---")
|
| 72 |
+
print("1. Your Space is building the Docker container using your OpenEnv configurations.")
|
| 73 |
+
print(f"2. You can access it directly via: https://huggingface.co/spaces/{args.space_name}")
|
| 74 |
+
print("3. In your training script, switch your Env URL to match your Space.")
|
generate_matching_report.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
def generate_report():
|
| 4 |
+
with open("synced_report.json", "r") as f:
|
| 5 |
+
data = json.load(f)
|
| 6 |
+
|
| 7 |
+
with open("matching_dataset_logs.md", "w", encoding="utf-8") as f:
|
| 8 |
+
f.write("# Synchronized Audit & Dataset Report\n\n")
|
| 9 |
+
f.write("This report pairs each simulation episode with the exact dataset entry that triggered it.\n\n")
|
| 10 |
+
|
| 11 |
+
for e in data:
|
| 12 |
+
f.write(f"## Episode {e['episode']}\n")
|
| 13 |
+
f.write(f"### π Dataset Entry\n")
|
| 14 |
+
f.write(f"**Question:** {e['dataset_question']}\n\n")
|
| 15 |
+
f.write(f"**Context (Answer):** {e['dataset_answer']}\n\n")
|
| 16 |
+
f.write(f"### π Audit Log\n")
|
| 17 |
+
f.write(f"**LLM Reasoning:**\n```json\n{e['llm_reasoning']}\n```\n")
|
| 18 |
+
f.write(f"**Action:** {e['action']['action_type']} on {e['action']['target_id']}\n\n")
|
| 19 |
+
f.write(f"**Reward:** {e['reward']}\n\n")
|
| 20 |
+
f.write("---\n\n")
|
| 21 |
+
|
| 22 |
+
if __name__ == "__main__":
|
| 23 |
+
generate_report()
|
grader.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import Dict, Any, List
|
| 3 |
+
|
| 4 |
+
class Grader:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
# Dummy blacklist for Easy task
|
| 7 |
+
self.sanctioned_ids = ["ACC-BL-001", "ACC-BL-002", "ACC-BL-999"]
|
| 8 |
+
|
| 9 |
+
def grade_easy(self, target_id: str, is_flagged: bool) -> float:
|
| 10 |
+
"""
|
| 11 |
+
Sanction Hit: Binary match of AccountID vs Blacklist.
|
| 12 |
+
Returns identification accuracy (Pid)
|
| 13 |
+
"""
|
| 14 |
+
is_sanctioned = target_id in self.sanctioned_ids
|
| 15 |
+
if is_sanctioned and is_flagged:
|
| 16 |
+
return 1.0
|
| 17 |
+
elif not is_sanctioned and not is_flagged:
|
| 18 |
+
return 1.0
|
| 19 |
+
return 0.0
|
| 20 |
+
|
| 21 |
+
def grade_medium(self, transactions: List[Dict[str, Any]], target_id: str, is_flagged: bool) -> float:
|
| 22 |
+
"""
|
| 23 |
+
Smurfing Detection: Temporal window analysis of related transactions.
|
| 24 |
+
Checks for multiple transactions just under $10,000.
|
| 25 |
+
"""
|
| 26 |
+
smurf_count = sum(1 for tx in transactions if 9000 <= tx.get("amount", 0) < 10000)
|
| 27 |
+
has_smurfing = smurf_count >= 3
|
| 28 |
+
|
| 29 |
+
if has_smurfing and is_flagged:
|
| 30 |
+
return 1.0
|
| 31 |
+
elif not has_smurfing and not is_flagged:
|
| 32 |
+
return 1.0
|
| 33 |
+
return 0.0
|
| 34 |
+
|
| 35 |
+
def grade_hard(self, citation: str) -> float:
|
| 36 |
+
"""
|
| 37 |
+
Regulatory Mapping: LLM-as-a-judge + Cosine similarity (mocked here).
|
| 38 |
+
Returns citation accuracy (Pcit) based on simulated semantic match.
|
| 39 |
+
"""
|
| 40 |
+
if not citation:
|
| 41 |
+
return 0.0
|
| 42 |
+
|
| 43 |
+
# Simplified mock evaluation
|
| 44 |
+
valid_citations = ["EU-AI-Act-Art-57", "BSA-31-USC-5318"]
|
| 45 |
+
if any(v in citation for v in valid_citations):
|
| 46 |
+
return 1.0
|
| 47 |
+
elif len(citation) > 10:
|
| 48 |
+
return 0.5 # Partial credit for trying to cite something substantive
|
| 49 |
+
return 0.0
|
inference.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AegisGym Baseline Inference Script
|
| 3 |
+
Requirement: Must use OpenAI client and env vars for credentials.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
from client_env import get_sync_client
|
| 9 |
+
from models import AuditAction
|
| 10 |
+
|
| 11 |
+
# βββ Config (Required by Meta OpenEnv) ββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
|
| 13 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o")
|
| 14 |
+
HF_TOKEN = os.getenv("HF_TOKEN", "") # Used if hitting HF Inference Endpoints
|
| 15 |
+
ENV_URL = os.getenv("ENV_URL", "https://armaan020-aegisgym.hf.space")
|
| 16 |
+
|
| 17 |
+
client = OpenAI(api_key=HF_TOKEN or os.getenv("OPENAI_API_KEY"), base_url=API_BASE_URL)
|
| 18 |
+
|
| 19 |
+
SYSTEM_PROMPT = """You are a financial compliance auditor AI.
|
| 20 |
+
Respond ONLY with a JSON object:
|
| 21 |
+
{"action_type": "FLAG|APPROVE|REQUEST_INFO", "target_id": "<id>", "regulation_citation": "<cite>"}"""
|
| 22 |
+
|
| 23 |
+
def run_baseline(num_steps=3):
|
| 24 |
+
print(f"=== AegisGym Baseline Inference ===")
|
| 25 |
+
print(f"Model: {MODEL_NAME} | Env: {ENV_URL}")
|
| 26 |
+
|
| 27 |
+
env = get_sync_client(ENV_URL)
|
| 28 |
+
|
| 29 |
+
for i in range(num_steps):
|
| 30 |
+
print(f"\n--- Step {i+1} ---")
|
| 31 |
+
obs_payload = env.reset()
|
| 32 |
+
obs = obs_payload.get("observation", {})
|
| 33 |
+
|
| 34 |
+
user_msg = (
|
| 35 |
+
f"Transactions: {obs.get('transactions', [])}\n"
|
| 36 |
+
f"Context: {obs.get('retrieved_regs', '')}\n"
|
| 37 |
+
f"Account: {obs.get('account_metadata', {})}"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# OpenAI API Call
|
| 41 |
+
response = client.chat.completions.create(
|
| 42 |
+
model=MODEL_NAME,
|
| 43 |
+
messages=[
|
| 44 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 45 |
+
{"role": "user", "content": user_msg}
|
| 46 |
+
],
|
| 47 |
+
response_format={"type": "json_object"}
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
content = response.choices[0].message.content
|
| 51 |
+
print(f"LLM Response: {content}")
|
| 52 |
+
|
| 53 |
+
# Parse and Step
|
| 54 |
+
try:
|
| 55 |
+
action_data = json.loads(content)
|
| 56 |
+
result = env.step(action_data)
|
| 57 |
+
print(f"Reward: {result.get('reward')} | Done: {result.get('done')}")
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Parse Error: {e}")
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
run_baseline()
|
matching_dataset_logs.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Synchronized Audit & Dataset Report
|
| 2 |
+
|
| 3 |
+
This report pairs each simulation episode with the exact dataset entry that triggered it.
|
| 4 |
+
|
| 5 |
+
## Episode 1
|
| 6 |
+
### π Dataset Entry
|
| 7 |
+
**Question:** When was Rule. 2010 Standards of Commercial Honor and Principles of Trade Adopted?
|
| 8 |
+
|
| 9 |
+
**Context (Answer):** 2009-05-29 00:00:00
|
| 10 |
+
|
| 11 |
+
### π Audit Log
|
| 12 |
+
**LLM Reasoning:**
|
| 13 |
+
```json
|
| 14 |
+
{"action_type": "FLAG", "target_id": "ACC-BL-001", "regulation_citation": "EU-AI-Act-Art-57"}
|
| 15 |
+
```
|
| 16 |
+
**Action:** FLAG on ACC-BL-001
|
| 17 |
+
|
| 18 |
+
**Reward:** 0.5
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## Episode 2
|
| 23 |
+
### π Dataset Entry
|
| 24 |
+
**Question:** When was Rule Rule 2020. Use of Manipulative, Deceptive or Other Fraudulent Devices adopted
|
| 25 |
+
|
| 26 |
+
**Context (Answer):** 2009-05-29 00:00:00
|
| 27 |
+
|
| 28 |
+
### π Audit Log
|
| 29 |
+
**LLM Reasoning:**
|
| 30 |
+
```json
|
| 31 |
+
{
|
| 32 |
+
"action_type": "FLAG",
|
| 33 |
+
"target_id": "ACC-CLEAN-01",
|
| 34 |
+
"regulation_citation": "EU-AI-Act-Art-57"
|
| 35 |
+
}
|
| 36 |
+
```
|
| 37 |
+
**Action:** FLAG on ACC-CLEAN-01
|
| 38 |
+
|
| 39 |
+
**Reward:** -1.0
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Episode 3
|
| 44 |
+
### π Dataset Entry
|
| 45 |
+
**Question:** Which countries currently have bilateral FTAs in effect with the U.S.?
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
**Context (Answer):** "The U.S. currently has bilateral FTAs in effect with Australia, Bahrain, Chile, Israel, Jordan, Morocco, Oman, Singapore, Peru, Korea, and Columbia.
|
| 50 |
+
|
| 51 |
+
### π Audit Log
|
| 52 |
+
**LLM Reasoning:**
|
| 53 |
+
```json
|
| 54 |
+
{"action_type": "FLAG", "target_id": "ACC-BL-001", "regulation_citation": "EU-AI-Act-Art-57: Sandboxes required for high risk."}
|
| 55 |
+
```
|
| 56 |
+
**Action:** FLAG on ACC-BL-001
|
| 57 |
+
|
| 58 |
+
**Reward:** 0.5
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
models.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openenv.core.env_server import Action, Observation, State
|
| 2 |
+
from pydantic import Field
|
| 3 |
+
from typing import List, Dict, Any
|
| 4 |
+
|
| 5 |
+
class AuditAction(Action):
|
| 6 |
+
action_type: str = Field(description="One of: ['APPROVE', 'FLAG', 'REQUEST_INFO']")
|
| 7 |
+
target_id: str = Field(description="The ID of the account or transaction being evaluated.")
|
| 8 |
+
regulation_citation: str = Field(description="The specific regulation clause cited.")
|
| 9 |
+
|
| 10 |
+
class AuditObservation(Observation):
|
| 11 |
+
transactions: List[Dict[str, Any]] = Field(description="List of transaction dicts")
|
| 12 |
+
account_metadata: Dict[str, Any] = Field(description="Account metadata")
|
| 13 |
+
retrieved_regs: str = Field(description="RAG-retrieved sections of guidelines")
|
| 14 |
+
|
| 15 |
+
# Official OpenEnv returns these properties directly on the Observation
|
| 16 |
+
reward: float = Field(default=0.0, description="Reward gained in the step")
|
| 17 |
+
done: bool = Field(default=False, description="Whether the episode is complete")
|
| 18 |
+
|
| 19 |
+
class AuditState(State):
|
| 20 |
+
step_count: int = Field(description="Current step of the episode")
|
| 21 |
+
current_tier: str = Field(description="The compliance tier active for the episode")
|
| 22 |
+
|
| 23 |
+
# Required by base State (depending on library version, might require extra fields, but standard is dict-like or these fields)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
id: aegis_gym
|
| 2 |
+
version: "1.0.0"
|
| 3 |
+
name: "AegisGym: Financial Compliance & AML Sandbox"
|
| 4 |
+
description: "A sandbox for training agents to perform banking audits, sanction checks, and anti-money laundering (AML) detection."
|
| 5 |
+
author: "Armaan"
|
| 6 |
+
tags: ["finance", "compliance", "aml", "audit", "openenv"]
|
| 7 |
+
tasks:
|
| 8 |
+
- id: easy_audit
|
| 9 |
+
name: "Sanction Check"
|
| 10 |
+
description: "Evaluate a single account against a known sanctions list. Identify 'ACC-BL-*' accounts as high-risk."
|
| 11 |
+
difficulty: "easy"
|
| 12 |
+
- id: medium_audit
|
| 13 |
+
name: "Smurfing Detection"
|
| 14 |
+
description: "Analyze a stream of transactions to detect 'smurfing' (structuring) patterns where multiple deposits just under $10,000 are made."
|
| 15 |
+
difficulty: "medium"
|
| 16 |
+
- id: hard_audit
|
| 17 |
+
name: "Regulatory Alignment"
|
| 18 |
+
description: "Audit high-value transactions and cite the specific regulatory framework (e.g., EU AI Act, BSA) correctly."
|
| 19 |
+
difficulty: "hard"
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git
|
| 2 |
+
pydantic
|
| 3 |
+
uvicorn
|
| 4 |
+
openai
|
sample_dataset.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datasets import load_dataset
|
| 2 |
+
|
| 3 |
+
def sample_dataset():
|
| 4 |
+
print("Loading SecureFinAI-Lab/Regulations_QA...")
|
| 5 |
+
try:
|
| 6 |
+
# We'll use a streaming load or just take the first few examples
|
| 7 |
+
ds = load_dataset("SecureFinAI-Lab/Regulations_QA", split="train", streaming=True)
|
| 8 |
+
iterator = iter(ds)
|
| 9 |
+
print("\n--- Example 1 ---")
|
| 10 |
+
item1 = next(iterator)
|
| 11 |
+
print(item1)
|
| 12 |
+
print("\n--- Example 2 ---")
|
| 13 |
+
item2 = next(iterator)
|
| 14 |
+
print(item2)
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"Error loading dataset: {e}")
|
| 17 |
+
|
| 18 |
+
if __name__ == "__main__":
|
| 19 |
+
sample_dataset()
|
server.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import Dict, Any, Tuple
|
| 3 |
+
from openenv.core.env_server import Environment
|
| 4 |
+
from models import AuditAction, AuditObservation, AuditState
|
| 5 |
+
from grader import Grader
|
| 6 |
+
|
| 7 |
+
class AegisGymEnv(Environment):
|
| 8 |
+
def __init__(self):
|
| 9 |
+
super().__init__()
|
| 10 |
+
self.grader = Grader()
|
| 11 |
+
self.step_count = 0
|
| 12 |
+
self.current_tier = "easy"
|
| 13 |
+
self.max_steps = 100
|
| 14 |
+
|
| 15 |
+
# Test Data
|
| 16 |
+
self.current_target_id = "ACC-BL-001"
|
| 17 |
+
self.current_transactions = []
|
| 18 |
+
|
| 19 |
+
def reset(self) -> AuditObservation:
|
| 20 |
+
self.step_count = 0
|
| 21 |
+
self._set_next_scenario()
|
| 22 |
+
return self._create_observation(0.0, False)
|
| 23 |
+
|
| 24 |
+
@property
|
| 25 |
+
def state(self) -> AuditState:
|
| 26 |
+
return AuditState(
|
| 27 |
+
step_count=self.step_count,
|
| 28 |
+
current_tier=self.current_tier
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
def _create_observation(self, reward: float, done: bool) -> AuditObservation:
|
| 32 |
+
return AuditObservation(
|
| 33 |
+
transactions=self.current_transactions,
|
| 34 |
+
account_metadata={"age_days": 120, "tier": "standard", "target_id": self.current_target_id},
|
| 35 |
+
retrieved_regs="EU-AI-Act-Art-57: Sandboxes required for high risk. BSA-31-USC-5318: AML programs.",
|
| 36 |
+
reward=reward,
|
| 37 |
+
done=done
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
def step(self, action: AuditAction) -> AuditObservation:
|
| 41 |
+
self.step_count += 1
|
| 42 |
+
|
| 43 |
+
is_flagged = action.action_type in ["FLAG", "BLOCK", "REQUEST_INFO"]
|
| 44 |
+
|
| 45 |
+
p_id, p_cit = 0.0, 0.0
|
| 46 |
+
f_p, f_n = 0.0, 0.0
|
| 47 |
+
|
| 48 |
+
if self.current_tier == "easy":
|
| 49 |
+
p_id = self.grader.grade_easy(action.target_id, is_flagged)
|
| 50 |
+
is_sanctioned = action.target_id in self.grader.sanctioned_ids
|
| 51 |
+
if not is_sanctioned and is_flagged: f_p = 1.0
|
| 52 |
+
elif is_sanctioned and not is_flagged: f_n = 5.0
|
| 53 |
+
|
| 54 |
+
elif self.current_tier == "medium":
|
| 55 |
+
p_id = self.grader.grade_medium(self.current_transactions, action.target_id, is_flagged)
|
| 56 |
+
smurf_count = sum(1 for tx in self.current_transactions if 9000 <= tx.get("amount", 0) < 10000)
|
| 57 |
+
has_smurfing = smurf_count >= 3
|
| 58 |
+
if not has_smurfing and is_flagged: f_p = 1.0
|
| 59 |
+
elif has_smurfing and not is_flagged: f_n = 5.0
|
| 60 |
+
|
| 61 |
+
elif self.current_tier == "hard":
|
| 62 |
+
p_id = 1.0 if is_flagged else 0.0
|
| 63 |
+
p_cit = self.grader.grade_hard(action.regulation_citation)
|
| 64 |
+
if not is_flagged: f_n = 5.0
|
| 65 |
+
|
| 66 |
+
reward = (0.5 * p_id) + (0.5 * p_cit) - (1.0 * f_p) - (1.0 * f_n)
|
| 67 |
+
done = self.step_count >= self.max_steps
|
| 68 |
+
|
| 69 |
+
obs = self._create_observation(reward, done)
|
| 70 |
+
self._set_next_scenario()
|
| 71 |
+
return obs
|
| 72 |
+
|
| 73 |
+
def _set_next_scenario(self):
|
| 74 |
+
tiers = ["easy", "medium", "hard"]
|
| 75 |
+
self.current_tier = tiers[self.step_count % 3]
|
| 76 |
+
|
| 77 |
+
if self.current_tier == "easy":
|
| 78 |
+
self.current_target_id = random.choice(["ACC-BL-001", "ACC-CLEAN-01"])
|
| 79 |
+
self.current_transactions = [{"amount": 500, "currency": "USD"}]
|
| 80 |
+
elif self.current_tier == "medium":
|
| 81 |
+
self.current_target_id = "ACC-SMURF-99"
|
| 82 |
+
if random.random() > 0.5:
|
| 83 |
+
self.current_transactions = [{"amount": 9500, "currency": "USD"} for _ in range(4)]
|
| 84 |
+
else:
|
| 85 |
+
self.current_transactions = [{"amount": 5000, "currency": "USD"}, {"amount": 200, "currency": "USD"}]
|
| 86 |
+
elif self.current_tier == "hard":
|
| 87 |
+
self.current_target_id = "ACC-REG-VIOLATOR"
|
| 88 |
+
self.current_transactions = [{"amount": 50000, "currency": "USD", "note": "High risk AI deployment without sandbox"}]
|
simulation.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AegisGym Simulation Script
|
| 3 |
+
Runs multiple audit episodes using the LLM for inference (CPU-friendly).
|
| 4 |
+
This generates the logs and metrics to analyze the system's performance.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import torch
|
| 8 |
+
import json
|
| 9 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 10 |
+
from client_env import get_sync_client
|
| 11 |
+
from train import parse_action, SYSTEM_PROMPT
|
| 12 |
+
|
| 13 |
+
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 14 |
+
ENV_URL = "https://armaan020-aegisgym.hf.space"
|
| 15 |
+
|
| 16 |
+
def run_simulation(num_episodes=5):
|
| 17 |
+
print(f"=== Starting AegisGym Simulation (Inference Only) ===")
|
| 18 |
+
print(f"Model: {MODEL_NAME}")
|
| 19 |
+
print(f"Env: {ENV_URL}\n")
|
| 20 |
+
|
| 21 |
+
print(f"Loading model on CPU...")
|
| 22 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 23 |
+
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, torch_dtype="auto", device_map="cpu")
|
| 24 |
+
print("Model loaded.\n")
|
| 25 |
+
|
| 26 |
+
env = get_sync_client(ENV_URL)
|
| 27 |
+
|
| 28 |
+
total_reward = 0
|
| 29 |
+
results = []
|
| 30 |
+
|
| 31 |
+
for i in range(num_episodes):
|
| 32 |
+
print(f"--- Episode {i+1} ---")
|
| 33 |
+
result = env.reset()
|
| 34 |
+
obs_dict = result.get("observation", {})
|
| 35 |
+
|
| 36 |
+
state = env.state()
|
| 37 |
+
tier = state.get("current_tier", "easy")
|
| 38 |
+
|
| 39 |
+
user_msg = (
|
| 40 |
+
f"Audit the following transaction.\n\n"
|
| 41 |
+
f"Tier: {tier.upper()}\n"
|
| 42 |
+
f"Transactions: {obs_dict.get('transactions', [])}\n"
|
| 43 |
+
f"Context: {obs_dict.get('retrieved_regs', [])}\n"
|
| 44 |
+
f"Account: {obs_dict.get('account_metadata', {})}"
|
| 45 |
+
)
|
| 46 |
+
messages = [
|
| 47 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 48 |
+
{"role": "user", "content": user_msg},
|
| 49 |
+
]
|
| 50 |
+
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
|
| 51 |
+
|
| 52 |
+
print(f"[Audit Prompt]:\n{user_msg}")
|
| 53 |
+
|
| 54 |
+
# Inference
|
| 55 |
+
inputs = tokenizer(prompt, return_tensors="pt")
|
| 56 |
+
with torch.no_grad():
|
| 57 |
+
outputs = model.generate(**inputs, max_new_tokens=128)
|
| 58 |
+
|
| 59 |
+
completion = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 60 |
+
print(f"[Model Reasoning]:\n{completion}")
|
| 61 |
+
|
| 62 |
+
action = parse_action(completion)
|
| 63 |
+
print(f"[Action]: {action.action_type} on {action.target_id}")
|
| 64 |
+
|
| 65 |
+
step_result = env.step(action.model_dump())
|
| 66 |
+
reward = step_result.get("reward", 0.0)
|
| 67 |
+
done = step_result.get("done", False)
|
| 68 |
+
|
| 69 |
+
print(f"[Reward]: {reward} | [Done]: {done}\n")
|
| 70 |
+
total_reward += reward
|
| 71 |
+
results.append({
|
| 72 |
+
"episode": i+1,
|
| 73 |
+
"tier": tier,
|
| 74 |
+
"action": action.action_type,
|
| 75 |
+
"reward": reward
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
print(f"=== Simulation Complete ===")
|
| 79 |
+
print(f"Average Reward: {total_reward / num_episodes}")
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
run_simulation(num_episodes=3)
|
synced_report.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"episode": 1,
|
| 4 |
+
"dataset_question": "When was Rule. 2010 Standards of Commercial Honor and Principles of Trade Adopted?",
|
| 5 |
+
"dataset_answer": "2009-05-29 00:00:00",
|
| 6 |
+
"tier": "easy",
|
| 7 |
+
"llm_reasoning": "{\"action_type\": \"FLAG\", \"target_id\": \"ACC-BL-001\", \"regulation_citation\": \"EU-AI-Act-Art-57\"}",
|
| 8 |
+
"action": {
|
| 9 |
+
"metadata": {},
|
| 10 |
+
"action_type": "FLAG",
|
| 11 |
+
"target_id": "ACC-BL-001",
|
| 12 |
+
"regulation_citation": "EU-AI-Act-Art-57"
|
| 13 |
+
},
|
| 14 |
+
"reward": 0.5
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"episode": 2,
|
| 18 |
+
"dataset_question": "When was Rule Rule 2020. Use of Manipulative, Deceptive or Other Fraudulent Devices adopted",
|
| 19 |
+
"dataset_answer": "2009-05-29 00:00:00",
|
| 20 |
+
"tier": "easy",
|
| 21 |
+
"llm_reasoning": "{\n \"action_type\": \"FLAG\",\n \"target_id\": \"ACC-CLEAN-01\",\n \"regulation_citation\": \"EU-AI-Act-Art-57\"\n}",
|
| 22 |
+
"action": {
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"action_type": "FLAG",
|
| 25 |
+
"target_id": "ACC-CLEAN-01",
|
| 26 |
+
"regulation_citation": "EU-AI-Act-Art-57"
|
| 27 |
+
},
|
| 28 |
+
"reward": -1.0
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"episode": 3,
|
| 32 |
+
"dataset_question": "Which countries currently have bilateral FTAs in effect with the U.S.?\n\n",
|
| 33 |
+
"dataset_answer": "\"The U.S. currently has bilateral FTAs in effect with Australia, Bahrain, Chile, Israel, Jordan, Morocco, Oman, Singapore, Peru, Korea, and Columbia.",
|
| 34 |
+
"tier": "easy",
|
| 35 |
+
"llm_reasoning": "{\"action_type\": \"FLAG\", \"target_id\": \"ACC-BL-001\", \"regulation_citation\": \"EU-AI-Act-Art-57: Sandboxes required for high risk.\"}",
|
| 36 |
+
"action": {
|
| 37 |
+
"metadata": {},
|
| 38 |
+
"action_type": "FLAG",
|
| 39 |
+
"target_id": "ACC-BL-001",
|
| 40 |
+
"regulation_citation": "EU-AI-Act-Art-57: Sandboxes required for high risk."
|
| 41 |
+
},
|
| 42 |
+
"reward": 0.5
|
| 43 |
+
}
|
| 44 |
+
]
|
synced_simulation.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AegisGym Synchronized Simulation
|
| 3 |
+
Saves exact dataset entries alongside the audit logs they generated.
|
| 4 |
+
"""
|
| 5 |
+
import torch
|
| 6 |
+
import json
|
| 7 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 8 |
+
from datasets import load_dataset
|
| 9 |
+
from client_env import get_sync_client
|
| 10 |
+
from train import parse_action, SYSTEM_PROMPT
|
| 11 |
+
|
| 12 |
+
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 13 |
+
ENV_URL = "https://armaan020-aegisgym.hf.space"
|
| 14 |
+
|
| 15 |
+
def run_synced_simulation(num_episodes=3):
|
| 16 |
+
print(f"Loading model on CPU...")
|
| 17 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 18 |
+
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, torch_dtype="auto", device_map="cpu")
|
| 19 |
+
|
| 20 |
+
print("Loading dataset...")
|
| 21 |
+
ds = load_dataset("SecureFinAI-Lab/Regulations_QA", split="train", streaming=True)
|
| 22 |
+
it = iter(ds)
|
| 23 |
+
|
| 24 |
+
env = get_sync_client(ENV_URL)
|
| 25 |
+
full_report = []
|
| 26 |
+
|
| 27 |
+
for i in range(num_episodes):
|
| 28 |
+
print(f"--- Episode {i+1} ---")
|
| 29 |
+
item = next(it)
|
| 30 |
+
result = env.reset()
|
| 31 |
+
obs_dict = result.get("observation", {})
|
| 32 |
+
state = env.state()
|
| 33 |
+
tier = state.get("current_tier", "easy")
|
| 34 |
+
|
| 35 |
+
custom_prompt = item.get("question", "Audit the following transaction.")
|
| 36 |
+
dataset_answer = item.get("answer", "No specific guidance provided.")
|
| 37 |
+
|
| 38 |
+
user_msg = (
|
| 39 |
+
f"{custom_prompt}\n\n"
|
| 40 |
+
f"Tier: {tier.upper()}\n"
|
| 41 |
+
f"Transactions: {obs_dict.get('transactions', [])}\n"
|
| 42 |
+
f"Context: {obs_dict.get('retrieved_regs', [])}\n"
|
| 43 |
+
f"Regulatory Hint: {dataset_answer}\n"
|
| 44 |
+
f"Account: {obs_dict.get('account_metadata', {})}"
|
| 45 |
+
)
|
| 46 |
+
messages = [
|
| 47 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 48 |
+
{"role": "user", "content": user_msg},
|
| 49 |
+
]
|
| 50 |
+
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
|
| 51 |
+
|
| 52 |
+
inputs = tokenizer(prompt, return_tensors="pt")
|
| 53 |
+
with torch.no_grad():
|
| 54 |
+
outputs = model.generate(**inputs, max_new_tokens=128)
|
| 55 |
+
|
| 56 |
+
completion = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 57 |
+
action = parse_action(completion)
|
| 58 |
+
step_result = env.step(action.model_dump())
|
| 59 |
+
|
| 60 |
+
full_report.append({
|
| 61 |
+
"episode": i+1,
|
| 62 |
+
"dataset_question": custom_prompt,
|
| 63 |
+
"dataset_answer": dataset_answer,
|
| 64 |
+
"tier": tier,
|
| 65 |
+
"llm_reasoning": completion,
|
| 66 |
+
"action": action.model_dump(),
|
| 67 |
+
"reward": step_result.get("reward", 0.0)
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
with open("synced_report.json", "w") as f:
|
| 71 |
+
json.dump(full_report, f, indent=2)
|
| 72 |
+
print("\nSaved synced_report.json")
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
run_synced_simulation()
|
train.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AegisGym GRPO Training Script
|
| 3 |
+
Connects to the live HF Space for environment rollouts.
|
| 4 |
+
Run: python train.py
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
from datasets import Dataset
|
| 9 |
+
from trl import GRPOConfig, GRPOTrainer
|
| 10 |
+
from transformers import AutoTokenizer
|
| 11 |
+
from client_env import get_sync_client
|
| 12 |
+
from models import AuditAction
|
| 13 |
+
|
| 14 |
+
# βββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
ENV_URL = os.getenv("AEGISGYM_URL", "https://armaan020-aegisgym.hf.space")
|
| 16 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-0.5B-Instruct")
|
| 17 |
+
OUTPUT_DIR = "aegisgym-grpo-agent"
|
| 18 |
+
|
| 19 |
+
SYSTEM_PROMPT = """You are a financial compliance auditor AI.
|
| 20 |
+
Given a transaction scenario and regulatory context, respond with a JSON object:
|
| 21 |
+
{"action_type": "FLAG|APPROVE|REQUEST_INFO", "target_id": "<account_id>", "regulation_citation": "<regulation>"}
|
| 22 |
+
Be precise and concise."""
|
| 23 |
+
|
| 24 |
+
# βββ Action parser ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
def parse_action(text: str) -> AuditAction:
|
| 26 |
+
try:
|
| 27 |
+
start = text.find("{")
|
| 28 |
+
end = text.rfind("}") + 1
|
| 29 |
+
if start >= 0 and end > start:
|
| 30 |
+
data = json.loads(text[start:end])
|
| 31 |
+
return AuditAction(**data)
|
| 32 |
+
except Exception:
|
| 33 |
+
pass
|
| 34 |
+
return AuditAction(action_type="REQUEST_INFO", target_id="UNKNOWN", regulation_citation="parse_error")
|
| 35 |
+
|
| 36 |
+
# βββ Dataset Loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
from datasets import load_dataset
|
| 38 |
+
import itertools
|
| 39 |
+
|
| 40 |
+
print("Loading SecureFinAI-Lab/Regulations_QA dataset...")
|
| 41 |
+
try:
|
| 42 |
+
# Use a streaming dataset for efficiency
|
| 43 |
+
raw_dataset = load_dataset("SecureFinAI-Lab/Regulations_QA", split="train", streaming=True)
|
| 44 |
+
dataset_iterator = itertools.cycle(iter(raw_dataset))
|
| 45 |
+
print("Dataset loaded successfully.")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Warning: Failed to load dataset: {e}. Falling back to default prompts.")
|
| 48 |
+
dataset_iterator = None
|
| 49 |
+
|
| 50 |
+
# βββ Rollout function βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
def rollout_func(trainer, prompts, tokenizer):
|
| 52 |
+
"""One rollout episode connecting to the live AegisGym Space with dataset augmentation."""
|
| 53 |
+
from trl.experimental.openenv import generate_rollout_completions
|
| 54 |
+
|
| 55 |
+
env = get_sync_client(ENV_URL)
|
| 56 |
+
result = env.reset()
|
| 57 |
+
obs_dict = result.get("observation", {})
|
| 58 |
+
|
| 59 |
+
# Sample from dataset if available
|
| 60 |
+
dataset_context = ""
|
| 61 |
+
custom_prompt = "Audit the following transaction."
|
| 62 |
+
if dataset_iterator:
|
| 63 |
+
item = next(dataset_iterator)
|
| 64 |
+
custom_prompt = item.get("question", custom_prompt)
|
| 65 |
+
dataset_context = f"\nRegulatory Context: {item.get('answer', '')}"
|
| 66 |
+
|
| 67 |
+
all_prompt_ids, all_completion_ids, all_logprobs, rewards = [], [], [], []
|
| 68 |
+
|
| 69 |
+
state = env.state()
|
| 70 |
+
tier = state.get("current_tier", "easy")
|
| 71 |
+
|
| 72 |
+
# Combine dataset context with environment context
|
| 73 |
+
regs = obs_dict.get('retrieved_regs', "")
|
| 74 |
+
retrieved_regs = [regs] if isinstance(regs, str) else list(regs)
|
| 75 |
+
if dataset_context:
|
| 76 |
+
retrieved_regs.append(dataset_context)
|
| 77 |
+
|
| 78 |
+
user_msg = (
|
| 79 |
+
f"{custom_prompt}\n\n"
|
| 80 |
+
f"Tier: {tier.upper()}\n"
|
| 81 |
+
f"Transactions: {obs_dict.get('transactions', [])}\n"
|
| 82 |
+
f"Context: {retrieved_regs}\n"
|
| 83 |
+
f"Account: {obs_dict.get('account_metadata', {})}"
|
| 84 |
+
)
|
| 85 |
+
messages = [
|
| 86 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 87 |
+
{"role": "user", "content": user_msg},
|
| 88 |
+
]
|
| 89 |
+
prompt_text = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
|
| 90 |
+
|
| 91 |
+
out = generate_rollout_completions(trainer, [prompt_text])[0]
|
| 92 |
+
all_prompt_ids.extend(out["prompt_ids"])
|
| 93 |
+
all_completion_ids.extend(out["completion_ids"])
|
| 94 |
+
all_logprobs.extend(out["logprobs"])
|
| 95 |
+
|
| 96 |
+
completion_text = out.get("text") or tokenizer.decode(out["completion_ids"], skip_special_tokens=True)
|
| 97 |
+
action = parse_action(completion_text)
|
| 98 |
+
result = env.step(action.model_dump())
|
| 99 |
+
rewards.append(float(result.get("reward", 0.0)))
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
"prompt_ids": all_prompt_ids,
|
| 103 |
+
"completion_ids": all_completion_ids,
|
| 104 |
+
"logprobs": all_logprobs,
|
| 105 |
+
"env_reward": rewards[-1] if rewards else 0.0,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
# βββ Reward shim βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
def reward_compliance(completions, **kwargs):
|
| 110 |
+
rewards = kwargs.get("env_reward", [])
|
| 111 |
+
if not rewards:
|
| 112 |
+
return [0.0] * len(completions)
|
| 113 |
+
return [float(r) for r in rewards]
|
| 114 |
+
|
| 115 |
+
# βββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
def main():
|
| 117 |
+
print(f"Model : {MODEL_NAME}")
|
| 118 |
+
print(f"Env : {ENV_URL}")
|
| 119 |
+
|
| 120 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 121 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 122 |
+
|
| 123 |
+
dataset = Dataset.from_dict({"prompt": ["Audit the following transaction."] * 200})
|
| 124 |
+
|
| 125 |
+
config = GRPOConfig(
|
| 126 |
+
num_train_epochs = 1,
|
| 127 |
+
learning_rate = 5e-6,
|
| 128 |
+
per_device_train_batch_size = 1,
|
| 129 |
+
gradient_accumulation_steps = 4,
|
| 130 |
+
warmup_steps = 10,
|
| 131 |
+
num_generations = 2,
|
| 132 |
+
max_completion_length = 256,
|
| 133 |
+
use_vllm = False,
|
| 134 |
+
output_dir = OUTPUT_DIR,
|
| 135 |
+
logging_steps = 1,
|
| 136 |
+
save_steps = 25,
|
| 137 |
+
gradient_checkpointing = True,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
trainer = GRPOTrainer(
|
| 141 |
+
model = MODEL_NAME,
|
| 142 |
+
processing_class = tokenizer,
|
| 143 |
+
reward_funcs = [reward_compliance],
|
| 144 |
+
train_dataset = dataset,
|
| 145 |
+
args = config,
|
| 146 |
+
rollout_func = rollout_func,
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
print("\n=== Starting GRPO Training ===")
|
| 150 |
+
trainer.train()
|
| 151 |
+
trainer.save_model(OUTPUT_DIR)
|
| 152 |
+
print(f"\nModel saved to {OUTPUT_DIR}/")
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
main()
|
verify_consistency.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datasets import load_dataset
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def verify():
|
| 5 |
+
log_path = "audit_logs.txt"
|
| 6 |
+
if not os.path.exists(log_path):
|
| 7 |
+
print("Logs not found.")
|
| 8 |
+
return
|
| 9 |
+
|
| 10 |
+
# Read logs (UTF-16 from PowerShell redirection)
|
| 11 |
+
with open(log_path, "rb") as f:
|
| 12 |
+
content = f.read().decode("utf-16", "ignore")
|
| 13 |
+
|
| 14 |
+
print("--- Loading Dataset ---")
|
| 15 |
+
ds = load_dataset("SecureFinAI-Lab/Regulations_QA", split="train", streaming=True)
|
| 16 |
+
it = iter(ds)
|
| 17 |
+
|
| 18 |
+
print("--- Comparing Samples ---")
|
| 19 |
+
for i in range(5):
|
| 20 |
+
sample = next(it)
|
| 21 |
+
q = sample["question"]
|
| 22 |
+
found = q in content
|
| 23 |
+
print(f"Sample {i+1}: '{q[:50]}...' Found: {found}")
|
| 24 |
+
|
| 25 |
+
if __name__ == "__main__":
|
| 26 |
+
verify()
|
verify_dataset_rollout.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Verification script for dataset-augmented rollout function.
|
| 3 |
+
"""
|
| 4 |
+
from transformers import AutoTokenizer
|
| 5 |
+
from train import rollout_func, dataset_iterator
|
| 6 |
+
import trl.experimental.openenv
|
| 7 |
+
|
| 8 |
+
def verify():
|
| 9 |
+
print("=== Verifying Dataset-Augmented Rollout ===")
|
| 10 |
+
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 11 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 12 |
+
|
| 13 |
+
# Mock generation
|
| 14 |
+
def mock_gen(trainer, prompts):
|
| 15 |
+
print(f"\n[Generated Prompt Sample]:\n{prompts[0][:500]}...\n")
|
| 16 |
+
fake_completion = '{"action_type": "APPROVE", "target_id": "ACC-001", "regulation_citation": "EU-Regulation"}'
|
| 17 |
+
return [{"prompt_ids": [1], "completion_ids": [1], "logprobs": [0.0], "text": fake_completion}]
|
| 18 |
+
|
| 19 |
+
trl.experimental.openenv.generate_rollout_completions = mock_gen
|
| 20 |
+
|
| 21 |
+
print("Running rollout_func...")
|
| 22 |
+
result = rollout_func(None, ["prompt"], tokenizer)
|
| 23 |
+
print(f"\nRollout Finished!")
|
| 24 |
+
print(f"Reward: {result['env_reward']}")
|
| 25 |
+
|
| 26 |
+
if __name__ == "__main__":
|
| 27 |
+
verify()
|