Add initial project structure and core functionality for Agent M
- Created .gitignore to exclude virtual environment, logs, and database files. - Updated README.md with project description, features, folder structure, requirements, and usage instructions. - Implemented versioning and developer ID in agentm/__init__.py. - Developed main application logic in agentm/app.py, including credential handling and screen navigation. - Added database initialization and ROM management logic in agentm/logic/db.py and agentm/logic/db_functions.py. - Integrated DIAMBRA login functionality in agentm/logic/diambra_login.py. - Created ROM verification and caching system in agentm/logic/roms.py. - Designed user interface components for home and login screens in agentm/views/home.py and agentm/views/login.py. - Added logging utility in agentm/utils/logger.py for better debugging and tracking. - Included assets such as game images, styles, and logos. - Updated requirements.txt with necessary dependencies for the project.
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.venv/*
|
||||||
|
.diambra/credentials
|
||||||
|
logs/agentm.log
|
||||||
|
*.pyc
|
||||||
|
agentm/data/agentM.db
|
||||||
184
README.md
@ -1,92 +1,92 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="logo.png" alt="Agent M Logo" width="200"/>
|
<img src="logo.png" alt="Agent M Logo" width="200"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">🕹️ Agent M</h1>
|
<h1 align="center">🕹️ Agent M</h1>
|
||||||
<h3 align="center">A DIAMBRA Utility by mscrnt</h3>
|
<h3 align="center">A DIAMBRA Utility by mscrnt</h3>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Agent M** is a retro-styled launcher for [DIAMBRA Arena](https://www.diambra.ai/), designed to make agent training and packaging effortless. It wraps the official `diambra` CLI with a powerful terminal UI (TUI) powered by [Textual](https://textual.textualize.io/), giving both beginners and power users a guided experience for environment setup, wrapper configuration, and agent submission.
|
**Agent M** is a retro-styled launcher for [DIAMBRA Arena](https://www.diambra.ai/), designed to make agent training and packaging effortless. It wraps the official `diambra` CLI with a powerful terminal UI (TUI) powered by [Textual](https://textual.textualize.io/), giving both beginners and power users a guided experience for environment setup, wrapper configuration, and agent submission.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
- 📦 **Project scaffolding**: Quickly generate ready-to-train agent folders
|
- 📦 **Project scaffolding**: Quickly generate ready-to-train agent folders
|
||||||
- 🎮 **Environment launcher**: Easily configure and run DIAMBRA games
|
- 🎮 **Environment launcher**: Easily configure and run DIAMBRA games
|
||||||
- 🧠 **Training config wizard**: Guide users through wrappers, hyperparameters, and PPO settings
|
- 🧠 **Training config wizard**: Guide users through wrappers, hyperparameters, and PPO settings
|
||||||
- 📊 **Evaluation screen**: Run saved agents interactively
|
- 📊 **Evaluation screen**: Run saved agents interactively
|
||||||
- 🛠️ **Docker + submission helper**: Build, tag, and submit your agent
|
- 🛠️ **Docker + submission helper**: Build, tag, and submit your agent
|
||||||
- 🔒 **Developer ID tracking**: All submissions include a unique developer ID (`mscrnt-0001`)
|
- 🔒 **Developer ID tracking**: All submissions include a unique developer ID (`mscrnt-0001`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 Folder Structure
|
## 📁 Folder Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
agent_m/
|
agent_m/
|
||||||
├── agentm/
|
├── agentm/
|
||||||
│ ├── __init__.py # Contains __version__, __developer_id__
|
│ ├── __init__.py # Contains __version__, __developer_id__
|
||||||
│ ├── main.py # Entry point
|
│ ├── main.py # Entry point
|
||||||
│ ├── app.py # Textual App class
|
│ ├── app.py # Textual App class
|
||||||
│ ├── views/ # Screens: home, config, training, eval
|
│ ├── views/ # Screens: home, config, training, eval
|
||||||
│ ├── components/ # Reusable widgets
|
│ ├── components/ # Reusable widgets
|
||||||
│ ├── logic/ # Non-UI logic: Docker, config, submit
|
│ ├── logic/ # Non-UI logic: Docker, config, submit
|
||||||
│ └── assets/ # ASCII, icons, splash art
|
│ └── assets/ # ASCII, icons, splash art
|
||||||
├── logo.png # Project icon
|
├── logo.png # Project icon
|
||||||
├── README.md
|
├── README.md
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── pyproject.toml (optional)
|
├── pyproject.toml (optional)
|
||||||
└── dist/ # Built .exe from PyInstaller
|
└── dist/ # Built .exe from PyInstaller
|
||||||
````
|
````
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧰 Requirements
|
## 🧰 Requirements
|
||||||
|
|
||||||
Install dependencies:
|
Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
> Includes: `textual`, `rich`, `diambra-arena`, `pyyaml`, `typer`
|
> Includes: `textual`, `rich`, `diambra-arena`, `pyyaml`, `typer`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ▶️ Running Agent M
|
## ▶️ Running Agent M
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m agentm
|
python -m agentm
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Build as Executable
|
## 📦 Build as Executable
|
||||||
|
|
||||||
Using [PyInstaller](https://pyinstaller.org/):
|
Using [PyInstaller](https://pyinstaller.org/):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pyinstaller --onefile agentm/main.py --name agentm --add-data "agentm/assets:splash"
|
pyinstaller --onefile agentm/main.py --name agentm --add-data "agentm/assets:splash"
|
||||||
```
|
```
|
||||||
|
|
||||||
The final `.exe` will be located in the `dist/` directory.
|
The final `.exe` will be located in the `dist/` directory.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧾 Developer Attribution
|
## 🧾 Developer Attribution
|
||||||
|
|
||||||
All agents submitted through Agent M include a hardcoded developer ID:
|
All agents submitted through Agent M include a hardcoded developer ID:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
__developer_id__ = "mscrnt-0001"
|
__developer_id__ = "mscrnt-0001"
|
||||||
```
|
```
|
||||||
|
|
||||||
This allows future reward attribution or referral tracking.
|
This allows future reward attribution or referral tracking.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📣 License
|
## 📣 License
|
||||||
|
|
||||||
[MIT](LICENSE) — Created by [mscrnt](https://github.com/mscrnt)
|
[MIT](LICENSE) — Created by [mscrnt](https://github.com/mscrnt)
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__developer_id__ = "mscrnt-0001"
|
||||||
|
|
||||||
|
# Always resolve to the root of the project (1 level up from /agentm/)
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DIAMBRA_CREDENTIALS_PATH = PROJECT_ROOT / ".diambra" / "credentials"
|
||||||
|
DIAMBRA_CREDENTIALS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not DIAMBRA_CREDENTIALS_PATH.exists():
|
||||||
|
DIAMBRA_CREDENTIALS_PATH.touch()
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from agentm.views.home import HomeView
|
||||||
|
from agentm.views.login import LoginView
|
||||||
|
from agentm import DIAMBRA_CREDENTIALS_PATH
|
||||||
|
from agentm.utils.logger import log_with_caller
|
||||||
|
from agentm.logic.db import initialize_database
|
||||||
|
|
||||||
|
class AgentMApp(App):
|
||||||
|
CSS_PATH = "assets/styles.tcss"
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Called when the app starts."""
|
||||||
|
log_with_caller("debug", "App mounted. Initializing database...")
|
||||||
|
initialize_database()
|
||||||
|
|
||||||
|
log_with_caller("debug", "Checking for credentials...")
|
||||||
|
|
||||||
|
if DIAMBRA_CREDENTIALS_PATH.exists():
|
||||||
|
token = DIAMBRA_CREDENTIALS_PATH.read_text().strip()
|
||||||
|
|
||||||
|
if token and len(token) > 10:
|
||||||
|
log_with_caller("info", "Found populated DIAMBRA credentials. Launching Home.")
|
||||||
|
self.push_screen(HomeView())
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
log_with_caller("warning", "Token file exists but appears empty or malformed.")
|
||||||
|
|
||||||
|
log_with_caller("info", "No valid credentials found. Launching Login screen.")
|
||||||
|
self.push_screen(LoginView())
|
||||||
|
|
||||||
|
async def on_login_view_login_success(self, message: LoginView.LoginSuccess) -> None:
|
||||||
|
"""Handle successful login event and switch to Home screen."""
|
||||||
|
log_with_caller("info", "Handling LoginSuccess event. Launching Home.")
|
||||||
|
self.push_screen(HomeView())
|
||||||
BIN
agentm/assets/game_images/doapp.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
agentm/assets/game_images/kof98umh.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
agentm/assets/game_images/mvsc.jpg
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
agentm/assets/game_images/samsh5sp.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
agentm/assets/game_images/sfiii3n.jpg
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
agentm/assets/game_images/soulclbr.jpg
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
agentm/assets/game_images/tektagt.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
agentm/assets/game_images/umk3.jpg
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
agentm/assets/game_images/xmvsf.jpg
Normal file
|
After Width: | Height: | Size: 258 KiB |
55
agentm/assets/logo.txt
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%@%%%%%@%#
|
||||||
|
#%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%%%%%%%%%%*++++++++++++%%%%%%%%%%@
|
||||||
|
%%%%%%%++++++++++++++++++++++++++%%%%%%%
|
||||||
|
%%%%%%++++++++++++++++++++++++++++++++++%%%%%@
|
||||||
|
%%%%%+++++++++++++++++++++++++++++++++++++++*%%%%%
|
||||||
|
%%%%%+++++++++@@@@@@@@@@@++++++++++++++++++++++++%%%%%
|
||||||
|
%%%%%+++++++++++@@@@@@@@@@@++++++++++++++++++++++++++%%%%@
|
||||||
|
%%%%++++++++++@@@@@@@@@@@@@@++*@@@@@@@@+++++++++++++++++%%%%
|
||||||
|
%%%%++++++++++++@@@@@@@@@@@@@@++@@@@@@@@@++++++++++++++++++%%%%#
|
||||||
|
%%%%+++++++++++++@@@@@@@@@@@@@@+=@@@@@@@@@+++@@@@@@+++++++++++%%%@
|
||||||
|
%%%%++++++++++++++@@@@@@@@@@@@@@++@@@@@@@@@+++@@@@@@++++++++++++%%%%
|
||||||
|
%%%%+++++++++++++++++++++++++++++++@@@@@@@@@++@@@@@@@++@@@@@@+++++%%%@
|
||||||
|
@%%%++++++%@@@@@@@@@@@@@@@@@@@@@@@+*@@@@@@@@@+*@@@@@@@+*@@@@@@@+++++%%%
|
||||||
|
%%%+++++++@@@@@@@@@@@@@@@@@@@@@@@++@@@@@@@@@@+*@@@@@@@++@@@@@@@++++++%%%
|
||||||
|
%%%%++++++*@@@@@@@@@@@@@@@@@@@@@@@++@@@@@@@@@@+*@@@@@@@+=@@@@@@@++++++%%%%
|
||||||
|
%%%+++++++@@@@@@@@@@@@@@@@@@@@@@@@++@@@@@@@@@@+*@@@@@@@+@@@@@@@@+++++++%%%
|
||||||
|
%%%%++++++=@@@@@@@@@@@@@@@@@@@@@@@@++@@@@@@@@@@++@@@@@@@+@@@@@@@@+++++++%%%@
|
||||||
|
%%%+++++++@@@@@@@@@@@@+++++++++++++++@@@@@@@@@@++@@@@@@@+@@@@@@@@+++++++#%%%
|
||||||
|
%%%++++++=@@@@@@@@@@@@+++@@@@@@@@@@@@@@@@@@@@@@+=@@@@@@%+@@@@@@@@++++++++%%%
|
||||||
|
%%%++++++@@@@@@@@@@@@@+++@@@@@@@@@@@@@@@@@@@@@++@@@@@@@++@@@@@@@@++++++++%%%
|
||||||
|
#%%%+++++@@@@@@@@@@@@@@++++++++*+++==@@@@@@@@=++@@@@@@@++@@@@@@@++++++++++%%%
|
||||||
|
%%%++++++@@@@@@@@@@@@@@@@@@@@@@@@@#++++++++++++++++++++++++++++++++++++++%%%
|
||||||
|
%%%+++++++@@@@@@@@@@@@@@@@@@@@@@@@@@+++@@@@@@@@@@@@@@@@@@@@@@@@++++++++++%%%
|
||||||
|
%%%+++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@++@@@@@@@@@@@@@@@@@@@@@@@+++++++++%%%%
|
||||||
|
%%%%++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@+@@@@@@@@@@@@@@@@@@@@@@@+++++++++%%%%
|
||||||
|
%%%+++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@+@@@@@@@@@@@@@@@@@@@@@@+++++++++%%%
|
||||||
|
%%%%++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=++++++++%%%%
|
||||||
|
%%%++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++%%%%
|
||||||
|
@%%%++++++++++++++%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++%%%
|
||||||
|
%%%%+++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=++++++++++%%%@
|
||||||
|
%%%%++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++%%%@
|
||||||
|
%%%%++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++++%%%@
|
||||||
|
@%%%%++++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++%%%%#
|
||||||
|
%%%%++++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++++%%%%
|
||||||
|
%%%%%++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++++%%%%@
|
||||||
|
%%%%%*+++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++++%%%%%
|
||||||
|
%%%%%%+++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++%%%%%%
|
||||||
|
%%%%%%%+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%@
|
||||||
|
%%%%%%%%@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%
|
||||||
|
@%%%%%%%%%%%@@@@@@@@@@%%%%%%%%%%%@
|
||||||
|
#%%%%%%%%%%%%%%%%%%%%%%%%#
|
||||||
|
*@%@@@@*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
150
agentm/assets/styles.tcss
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/* === Global App Styles === */
|
||||||
|
|
||||||
|
Screen {
|
||||||
|
background: #0e0e0e;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Resets === */
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0 1;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Headers === */
|
||||||
|
|
||||||
|
Header, .header {
|
||||||
|
dock: top;
|
||||||
|
height: 3;
|
||||||
|
content-align: center middle;
|
||||||
|
background: #1f1f1f;
|
||||||
|
color: #ed7d3a;
|
||||||
|
text-style: bold;
|
||||||
|
padding: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Buttons === */
|
||||||
|
|
||||||
|
Button {
|
||||||
|
background: #1f1f1f;
|
||||||
|
color: #ed7d3a;
|
||||||
|
border: solid #ed7d3a;
|
||||||
|
padding: 1 2;
|
||||||
|
margin: 1;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:hover {
|
||||||
|
background: #ed7d3a;
|
||||||
|
color: #0e0e0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:focus {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: solid #ed7d3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Inputs === */
|
||||||
|
|
||||||
|
Input {
|
||||||
|
background: #1f1f1f;
|
||||||
|
color: #f0f0f0;
|
||||||
|
border: solid #3a9bed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Input:focus {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: solid #ed7d3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Alerts === */
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #4cd964;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: #ed7d3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Login Form === */
|
||||||
|
|
||||||
|
#login_form {
|
||||||
|
layout: vertical;
|
||||||
|
align-horizontal: center;
|
||||||
|
align-vertical: middle;
|
||||||
|
width: 60%;
|
||||||
|
height: auto;
|
||||||
|
padding: 2 4;
|
||||||
|
border: solid #3a9bed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status_message {
|
||||||
|
color: #ed7d3a;
|
||||||
|
padding: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pw_row {
|
||||||
|
layout: horizontal;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pw_row > * {
|
||||||
|
margin-right: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pw_row > *:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Loading Overlay === */
|
||||||
|
|
||||||
|
#loading_overlay {
|
||||||
|
dock: top;
|
||||||
|
background: #1f1f1f;
|
||||||
|
color: #f0f0f0;
|
||||||
|
padding: 1 2;
|
||||||
|
text-style: bold;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Game Layout === */
|
||||||
|
|
||||||
|
.centered_layout {
|
||||||
|
layout: vertical;
|
||||||
|
align-horizontal: center;
|
||||||
|
align-vertical: middle;
|
||||||
|
padding: 2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rom_rows_container {
|
||||||
|
layout: vertical;
|
||||||
|
align-horizontal: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rom_row {
|
||||||
|
layout: horizontal;
|
||||||
|
align-horizontal: center;
|
||||||
|
align-vertical: middle;
|
||||||
|
padding: 1 2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm_button {
|
||||||
|
align-horizontal: center;
|
||||||
|
align-vertical: top;
|
||||||
|
margin-top: 1;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game_info {
|
||||||
|
padding: 1 2;
|
||||||
|
width: 100%;
|
||||||
|
color: #ed7d3a;
|
||||||
|
}
|
||||||
27
agentm/logic/db.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CACHE_DB_PATH = Path("agentm/data/agentM.db")
|
||||||
|
CACHE_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def get_db_conn():
|
||||||
|
return sqlite3.connect(CACHE_DB_PATH)
|
||||||
|
|
||||||
|
def initialize_database():
|
||||||
|
with get_db_conn() as conn:
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS roms (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
rom_file TEXT NOT NULL UNIQUE,
|
||||||
|
game_id TEXT NOT NULL,
|
||||||
|
sha256 TEXT,
|
||||||
|
difficulty_min INTEGER,
|
||||||
|
difficulty_max INTEGER,
|
||||||
|
characters TEXT,
|
||||||
|
keywords TEXT,
|
||||||
|
verified BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
verified_at TEXT
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
120
agentm/logic/db_functions.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from agentm.logic.db import get_db_conn
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached_rom(rom_file: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Retrieve verified ROM metadata from the database by ROM filename.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rom_file: The filename of the ROM (e.g., 'sfiii3n.zip').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary of ROM metadata if verified, otherwise None.
|
||||||
|
"""
|
||||||
|
with get_db_conn() as conn:
|
||||||
|
cur = conn.execute("""
|
||||||
|
SELECT sha256, verified_at, title, game_id,
|
||||||
|
difficulty_min, difficulty_max, characters, keywords
|
||||||
|
FROM roms WHERE rom_file = ? AND verified = 1
|
||||||
|
""", (rom_file,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
return {
|
||||||
|
"sha256": row[0],
|
||||||
|
"verified": True,
|
||||||
|
"verified_at": row[1],
|
||||||
|
"title": row[2],
|
||||||
|
"rom_file": rom_file,
|
||||||
|
"game_id": row[3],
|
||||||
|
"difficulty_min": row[4],
|
||||||
|
"difficulty_max": row[5],
|
||||||
|
"characters": json.loads(row[6]) if row[6] else [],
|
||||||
|
"keywords": json.loads(row[7]) if row[7] else [],
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_verified_roms() -> list[dict]:
|
||||||
|
"""
|
||||||
|
Return a list of all verified ROMs as dictionaries.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of dictionaries containing ROM metadata.
|
||||||
|
"""
|
||||||
|
with get_db_conn() as conn:
|
||||||
|
cur = conn.execute("""
|
||||||
|
SELECT sha256, verified_at, title, game_id, rom_file,
|
||||||
|
difficulty_min, difficulty_max, characters, keywords
|
||||||
|
FROM roms WHERE verified = 1
|
||||||
|
ORDER BY title ASC
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"sha256": row[0],
|
||||||
|
"verified": True,
|
||||||
|
"verified_at": row[1],
|
||||||
|
"title": row[2],
|
||||||
|
"game_id": row[3],
|
||||||
|
"rom_file": row[4],
|
||||||
|
"difficulty_min": row[5],
|
||||||
|
"difficulty_max": row[6],
|
||||||
|
"characters": json.loads(row[7]) if row[7] else [],
|
||||||
|
"keywords": json.loads(row[8]) if row[8] else [],
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_rom_record(
|
||||||
|
title: str,
|
||||||
|
rom_file: str,
|
||||||
|
game_id: str,
|
||||||
|
sha256: str,
|
||||||
|
difficulty_min: int = None,
|
||||||
|
difficulty_max: int = None,
|
||||||
|
characters: list[str] = None,
|
||||||
|
keywords: list[str] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Insert or replace a verified ROM entry in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: Game title.
|
||||||
|
rom_file: ROM file name.
|
||||||
|
game_id: Game ID used by DIAMBRA.
|
||||||
|
sha256: SHA256 checksum of the ROM.
|
||||||
|
difficulty_min: Minimum difficulty.
|
||||||
|
difficulty_max: Maximum difficulty.
|
||||||
|
characters: List of characters.
|
||||||
|
keywords: List of keywords.
|
||||||
|
"""
|
||||||
|
with get_db_conn() as conn:
|
||||||
|
conn.execute("""
|
||||||
|
INSERT OR REPLACE INTO roms (
|
||||||
|
title,
|
||||||
|
rom_file,
|
||||||
|
game_id,
|
||||||
|
sha256,
|
||||||
|
difficulty_min,
|
||||||
|
difficulty_max,
|
||||||
|
characters,
|
||||||
|
keywords,
|
||||||
|
verified,
|
||||||
|
verified_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
|
||||||
|
""", (
|
||||||
|
title,
|
||||||
|
rom_file,
|
||||||
|
game_id,
|
||||||
|
sha256,
|
||||||
|
difficulty_min,
|
||||||
|
difficulty_max,
|
||||||
|
json.dumps(characters or []),
|
||||||
|
json.dumps(keywords or []),
|
||||||
|
datetime.utcnow().isoformat()
|
||||||
|
))
|
||||||
28
agentm/logic/diambra_login.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import requests
|
||||||
|
from agentm import DIAMBRA_CREDENTIALS_PATH
|
||||||
|
from agentm.utils.logger import log_with_caller
|
||||||
|
|
||||||
|
LOGIN_API = "https://api.diambra.ai/api/auth/token/"
|
||||||
|
|
||||||
|
def login_to_diambra(email: str, password: str) -> str:
|
||||||
|
"""Log in to DIAMBRA and retrieve API token from /v1alpha1/token."""
|
||||||
|
payload = {
|
||||||
|
"username": email,
|
||||||
|
"password": password
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post("https://api.diambra.ai/api/v1alpha1/token", json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
token = response.json().get("token")
|
||||||
|
if not token:
|
||||||
|
raise Exception("Login succeeded but no token returned.")
|
||||||
|
|
||||||
|
log_with_caller("info", f"Successfully retrieved DIAMBRA token: {token[:6]}...")
|
||||||
|
return token
|
||||||
|
|
||||||
|
def save_diambra_token(token: str):
|
||||||
|
"""Writes the token to the .diambra/credentials file with no trailing newline."""
|
||||||
|
DIAMBRA_CREDENTIALS_PATH.write_bytes(token.encode("utf-8"))
|
||||||
|
log_with_caller("info", f"Saved DIAMBRA token to: {DIAMBRA_CREDENTIALS_PATH}")
|
||||||
|
|
||||||
127
agentm/logic/roms.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
agentm.logic.roms
|
||||||
|
This module handles the verification and caching of ROM files for the AgentM application.
|
||||||
|
It interacts with the DIAMBRA CLI to check ROM validity and updates the database accordingly.
|
||||||
|
It also provides functions to extract game information from the DIAMBRA CLI output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
from agentm.logic.db_functions import get_cached_rom, upsert_rom_record, get_all_verified_roms
|
||||||
|
from agentm.utils.logger import log_with_caller
|
||||||
|
|
||||||
|
ROM_FOLDER = "agentm/roms"
|
||||||
|
|
||||||
|
GAME_FILES = {
|
||||||
|
"Dead Or Alive ++": "doapp.zip",
|
||||||
|
"The King of Fighters '98 UM": "kof98umh.zip",
|
||||||
|
"Marvel vs. Capcom": "mvsc.zip",
|
||||||
|
"Samurai Shodown V Special": "samsh5sp.zip",
|
||||||
|
"Street Fighter III: 3rd Strike": "sfiii3n.zip",
|
||||||
|
"Soul Calibur": "soulclbr.zip",
|
||||||
|
"Tekken Tag Tournament": "tektagt.zip",
|
||||||
|
"Ultimate Mortal Kombat 3": "umk3.zip",
|
||||||
|
"X-Men vs. Street Fighter": "xmvsf.zip",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_image_path(rom_file: str) -> str:
|
||||||
|
"""Returns relative image path for a given ROM zip."""
|
||||||
|
base_name = os.path.splitext(rom_file)[0]
|
||||||
|
return f"agentm/assets/game_images/{base_name}.jpg"
|
||||||
|
|
||||||
|
def get_verified_roms():
|
||||||
|
verified = []
|
||||||
|
|
||||||
|
log_with_caller("debug", f"Starting ROM verification loop for {len(GAME_FILES)} files")
|
||||||
|
|
||||||
|
# Call `list-roms` once for efficiency
|
||||||
|
list_result = subprocess.run(["diambra", "arena", "list-roms"], capture_output=True, text=True)
|
||||||
|
full_list_output = list_result.stdout
|
||||||
|
|
||||||
|
for title, rom_file in GAME_FILES.items():
|
||||||
|
rom_path = os.path.join(ROM_FOLDER, rom_file)
|
||||||
|
log_with_caller("debug", f"Checking ROM path: {rom_path}")
|
||||||
|
|
||||||
|
if not os.path.exists(rom_path):
|
||||||
|
log_with_caller("warning", f"ROM not found: {rom_file} (expected at {rom_path})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cached = get_cached_rom(rom_file)
|
||||||
|
if cached and cached["verified"]:
|
||||||
|
log_with_caller("info", f"✓ Cached ROM is valid: {title} ({rom_file})")
|
||||||
|
verified.append(cached)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log_with_caller("debug", f"No valid cache found. Verifying with DIAMBRA CLI: {rom_file}")
|
||||||
|
result = subprocess.run(
|
||||||
|
["diambra", "arena", "check-roms", os.path.abspath(rom_path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
log_with_caller("debug", f"DIAMBRA output for {rom_file}:\n{result.stdout.strip()}")
|
||||||
|
|
||||||
|
if "Correct ROM file" in result.stdout:
|
||||||
|
sha256_match = re.search(r"sha256\s*=\s*([a-f0-9]{64})", result.stdout, re.IGNORECASE)
|
||||||
|
sha256 = sha256_match.group(1) if sha256_match else ""
|
||||||
|
game_id = rom_file.replace(".zip", "")
|
||||||
|
|
||||||
|
block = extract_game_block(full_list_output, game_id)
|
||||||
|
difficulty_min = extract_line_int(block, "Difficulty levels", index=0)
|
||||||
|
difficulty_max = extract_line_int(block, "Difficulty levels", index=1)
|
||||||
|
characters = extract_line_list(block, "Characters list")
|
||||||
|
keywords = extract_line_list(block, "Search keywords")
|
||||||
|
|
||||||
|
upsert_rom_record(
|
||||||
|
title=title,
|
||||||
|
rom_file=rom_file,
|
||||||
|
game_id=game_id,
|
||||||
|
sha256=sha256,
|
||||||
|
difficulty_min=difficulty_min,
|
||||||
|
difficulty_max=difficulty_max,
|
||||||
|
characters=characters,
|
||||||
|
keywords=keywords
|
||||||
|
)
|
||||||
|
|
||||||
|
log_with_caller("info", f"✓ Verified and cached: {title} ({rom_file})")
|
||||||
|
else:
|
||||||
|
log_with_caller("error", f"✗ Invalid ROM: {title} ({rom_file})")
|
||||||
|
|
||||||
|
verified = get_all_verified_roms()
|
||||||
|
log_with_caller("info", f"ROM verification completed: {len(verified)} valid game(s)")
|
||||||
|
return verified
|
||||||
|
|
||||||
|
|
||||||
|
def extract_game_block(output: str, game_id: str) -> str:
|
||||||
|
"""Extracts the block of text for the given game_id from `list-roms` output."""
|
||||||
|
lines = output.splitlines()
|
||||||
|
block = []
|
||||||
|
inside = False
|
||||||
|
for line in lines:
|
||||||
|
if f"game_id: {game_id.lower()}" in line.lower():
|
||||||
|
inside = True
|
||||||
|
elif inside and line.strip() == "":
|
||||||
|
break
|
||||||
|
if inside:
|
||||||
|
block.append(line)
|
||||||
|
return "\n".join(block)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_line_list(block: str, label: str) -> list[str]:
|
||||||
|
match = re.search(rf"{re.escape(label)}:\s*(\[[^\]]*\])", block)
|
||||||
|
if not match:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return eval(match.group(1), {"__builtins__": None}, {})
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def extract_line_int(block: str, label: str, index: int = 0) -> int:
|
||||||
|
match = re.search(rf"{re.escape(label)}:\s*Min\s*(\d+)\s*-\s*Max\s*(\d+)", block)
|
||||||
|
if match:
|
||||||
|
return int(match.group(index + 1))
|
||||||
|
return 0
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
# agentm/main.py
|
||||||
|
|
||||||
|
from agentm.app import AgentMApp
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = AgentMApp()
|
||||||
|
app.run()
|
||||||
BIN
agentm/roms/doapp.zip
Normal file
BIN
agentm/roms/kof98umh.zip
Normal file
BIN
agentm/roms/mvsc.zip
Normal file
BIN
agentm/roms/qsound_hle.zip
Normal file
BIN
agentm/roms/samsh5sp.zip
Normal file
BIN
agentm/roms/sfiii3n.zip
Normal file
BIN
agentm/roms/soulclbr.zip
Normal file
BIN
agentm/roms/tektagt.zip
Normal file
BIN
agentm/roms/umk3.zip
Normal file
BIN
agentm/roms/xmvsf.zip
Normal file
81
agentm/utils/logger.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import inspect
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
DEFAULT_UNIFIED_LOGFILE = "logs/agentm.log"
|
||||||
|
DEFAULT_LOG_LEVEL = os.environ.get("AGENTM_LOG_LEVEL", "DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger(name="AGENTM", level=None, max_bytes=5 * 1024 * 1024, backup_count=5):
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
|
||||||
|
if logger.handlers:
|
||||||
|
return logger # Already configured
|
||||||
|
|
||||||
|
level = level or DEFAULT_LOG_LEVEL
|
||||||
|
if isinstance(level, str):
|
||||||
|
level = getattr(logging, level.upper(), logging.INFO)
|
||||||
|
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
# 🎛 Rich Console Handler
|
||||||
|
console_handler = RichHandler(
|
||||||
|
rich_tracebacks=True,
|
||||||
|
markup=True,
|
||||||
|
show_time=True,
|
||||||
|
show_level=True,
|
||||||
|
show_path=False,
|
||||||
|
)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# 🛡 Unified File Logger
|
||||||
|
log_dir = os.getenv("AGENTM_LOG_DIR", "logs")
|
||||||
|
try:
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
unified_path = os.path.join(log_dir, "agentm.log")
|
||||||
|
file_handler = RotatingFileHandler(unified_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
|
||||||
|
file_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", "%Y-%m-%d %H:%M:%S")
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("⚠️ File logging disabled: could not create log directory or file.")
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_logger(level=None):
|
||||||
|
module_name = inspect.stack()[1].frame.f_globals.get("__name__", "unknown")
|
||||||
|
return get_logger(name="AGENTM", level=level) # Use unified logger name
|
||||||
|
|
||||||
|
|
||||||
|
def log_with_caller(level: str, message: str):
|
||||||
|
stack = inspect.stack()
|
||||||
|
|
||||||
|
callee = stack[1]
|
||||||
|
callee_func = callee.function
|
||||||
|
callee_module = callee.frame.f_globals.get("__name__", "unknown")
|
||||||
|
|
||||||
|
caller_func = "unknown"
|
||||||
|
caller_module = "unknown"
|
||||||
|
for frame in stack[2:]:
|
||||||
|
if frame.function not in {"wrapper", "inner", "<lambda>"}:
|
||||||
|
caller_func = frame.function
|
||||||
|
caller_module = frame.frame.f_globals.get("__name__", "unknown")
|
||||||
|
break
|
||||||
|
|
||||||
|
logger = get_logger("AGENTM")
|
||||||
|
full_message = (
|
||||||
|
f"{message} ← {callee_module}.{callee_func} "
|
||||||
|
f"→ called by {caller_module}.{caller_func}"
|
||||||
|
)
|
||||||
|
getattr(logger, level.lower())(full_message)
|
||||||
|
|
||||||
|
|
||||||
|
def set_global_log_level(level: str):
|
||||||
|
resolved_level = getattr(logging, level.upper(), logging.INFO)
|
||||||
|
logging.getLogger().setLevel(resolved_level)
|
||||||
|
for name in logging.root.manager.loggerDict:
|
||||||
|
logging.getLogger(name).setLevel(resolved_level)
|
||||||
|
log_with_caller("info", f"🔧 Global log level set to {level.upper()}")
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Static, Button
|
||||||
|
from textual.containers import Vertical, Horizontal
|
||||||
|
from textual.message import Message
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widget import Widget
|
||||||
|
import asyncio
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
from agentm.utils.logger import log_with_caller
|
||||||
|
from agentm.logic.roms import get_verified_roms, GAME_FILES
|
||||||
|
|
||||||
|
|
||||||
|
class GameSelected(Message):
|
||||||
|
def __init__(self, sender: Widget, metadata: dict):
|
||||||
|
self.metadata = metadata
|
||||||
|
super().__init__(sender)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressWidget(Widget):
|
||||||
|
message = reactive("Loading...")
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return f"[bold cyan]{self.message}[/bold cyan]"
|
||||||
|
|
||||||
|
|
||||||
|
class GameAccordion(Vertical):
|
||||||
|
def __init__(self, title: str, rom_file: str, metadata: dict, parent_view):
|
||||||
|
safe_id = rom_file.replace(".", "_").replace("-", "_")
|
||||||
|
super().__init__(id=f"accordion_{safe_id}")
|
||||||
|
self.title = title
|
||||||
|
self.rom_file = rom_file
|
||||||
|
self.safe_id = safe_id
|
||||||
|
self.metadata = metadata
|
||||||
|
self.parent_view = parent_view
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield Button(f"{self.title}", id=f"btn_{self.safe_id}", classes="game_button")
|
||||||
|
|
||||||
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == f"btn_{self.safe_id}":
|
||||||
|
await self.display_info()
|
||||||
|
|
||||||
|
async def display_info(self):
|
||||||
|
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
|
||||||
|
meta = self.metadata
|
||||||
|
|
||||||
|
info_lines = [
|
||||||
|
f"[b]Title:[/b] {meta['title']}",
|
||||||
|
f"[b]Game ID:[/b] {meta['game_id']}",
|
||||||
|
f"[b]Difficulty:[/b] {meta.get('difficulty_min')} - {meta.get('difficulty_max')}",
|
||||||
|
f"[b]Characters:[/b] {', '.join(meta.get('characters', []))}",
|
||||||
|
f"[b]Keywords:[/b] {', '.join(meta.get('keywords', []))}",
|
||||||
|
f"[b]SHA256:[/b] {meta['sha256']}",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.parent_view.shared_info_box.update("\n".join(info_lines))
|
||||||
|
self.parent_view.shared_confirm_button.label = f"✅ Confirm {meta['title']}"
|
||||||
|
self.parent_view.selected_game = meta
|
||||||
|
|
||||||
|
|
||||||
|
class HomeView(Screen):
|
||||||
|
BINDINGS = [("escape", "app.quit", "Quit")]
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
self.logo = Static(
|
||||||
|
"[b bright_magenta]\n\n"
|
||||||
|
" █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ███╗ ███╗\n"
|
||||||
|
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
||||||
|
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
||||||
|
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
|
||||||
|
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║ ██║\n"
|
||||||
|
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
|
||||||
|
classes="header",
|
||||||
|
expand=True,
|
||||||
|
)
|
||||||
|
self.progress_text = ProgressWidget()
|
||||||
|
|
||||||
|
yield Vertical(
|
||||||
|
self.logo,
|
||||||
|
Static("[bold green]LOADING...[/bold green]", expand=True),
|
||||||
|
self.progress_text,
|
||||||
|
id="loading_container"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_mount(self):
|
||||||
|
log_with_caller("debug", "HomeView mounted. Starting ROM verification.")
|
||||||
|
self.selected_game = None
|
||||||
|
self.run_worker(self.run_verification, thread=True, exclusive=True, name="rom-verification")
|
||||||
|
|
||||||
|
def run_verification(self):
|
||||||
|
total = len(GAME_FILES)
|
||||||
|
verified_roms = get_verified_roms()
|
||||||
|
|
||||||
|
for idx, rom in enumerate(verified_roms, start=1):
|
||||||
|
self.app.call_from_thread(
|
||||||
|
lambda title=rom['title'], idx=idx: setattr(
|
||||||
|
self.progress_text, "message",
|
||||||
|
f"Processing {title} ({idx}/{total})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
import time
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
self.app.call_from_thread(lambda: self.display_verified_roms(verified_roms))
|
||||||
|
|
||||||
|
async def display_verified_roms(self, verified_roms):
|
||||||
|
log_with_caller("info", f"ROM verification complete. Total: {len(verified_roms)}")
|
||||||
|
self.query_one("#loading_container").remove()
|
||||||
|
|
||||||
|
self.shared_info_box = Static("", id="game_info_box", classes="game_info")
|
||||||
|
self.shared_confirm_button = Button("✅ Confirm", id="confirm_button", classes="confirm_button")
|
||||||
|
|
||||||
|
await self.mount(
|
||||||
|
Vertical(
|
||||||
|
Static("🎮 Welcome to Agent M", classes="header"),
|
||||||
|
Static(
|
||||||
|
"This is an unofficial DIAMBRA launcher to help you easily train, evaluate, and submit RL agents for fighting games.\n\n"
|
||||||
|
"Verified ROMs are shown below. Click one to view game info and confirm selection.",
|
||||||
|
classes="body"
|
||||||
|
),
|
||||||
|
Static(f"✅ Found {len(verified_roms)} valid ROM(s).", id="status_message"),
|
||||||
|
Vertical(
|
||||||
|
Horizontal(id="rom_row_1", classes="rom_row"),
|
||||||
|
Horizontal(id="rom_row_2", classes="rom_row"),
|
||||||
|
id="rom_rows",
|
||||||
|
classes="rom_rows_container"
|
||||||
|
),
|
||||||
|
self.shared_info_box,
|
||||||
|
self.shared_confirm_button,
|
||||||
|
id="home_screen_container",
|
||||||
|
classes="centered_layout"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
per_row = ceil(len(verified_roms) / 2)
|
||||||
|
rows = [verified_roms[:per_row], verified_roms[per_row:]]
|
||||||
|
|
||||||
|
for i, row in enumerate(rows, start=1):
|
||||||
|
rom_row = self.query_one(f"#rom_row_{i}", Horizontal)
|
||||||
|
for rom in row:
|
||||||
|
await rom_row.mount(GameAccordion(
|
||||||
|
title=rom["title"],
|
||||||
|
rom_file=rom["rom_file"],
|
||||||
|
metadata=rom,
|
||||||
|
parent_view=self
|
||||||
|
))
|
||||||
|
|
||||||
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "confirm_button" and self.selected_game:
|
||||||
|
await self.app.push_screen("training", self.selected_game)
|
||||||
|
|
||||||
68
agentm/views/login.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Input, Button, Static
|
||||||
|
from textual.containers import Vertical, Horizontal
|
||||||
|
from textual.message import Message
|
||||||
|
from textual import events
|
||||||
|
from rich.text import Text
|
||||||
|
from agentm.logic.diambra_login import login_to_diambra, save_diambra_token
|
||||||
|
from agentm.utils.logger import log_with_caller
|
||||||
|
|
||||||
|
|
||||||
|
class LoginView(Screen):
|
||||||
|
class LoginSuccess(Message):
|
||||||
|
"""Message indicating login success."""
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield Vertical(
|
||||||
|
Static("🔒 Login to DIAMBRA", classes="header"),
|
||||||
|
Input(placeholder="Email", id="email_input"),
|
||||||
|
Horizontal(
|
||||||
|
Input(placeholder="Password", password=True, id="password_input"),
|
||||||
|
Button("👁", id="toggle_pw", variant="primary"),
|
||||||
|
id="pw_row"
|
||||||
|
),
|
||||||
|
Button("Login", id="login_button"),
|
||||||
|
Static("", id="status_message"),
|
||||||
|
Static(
|
||||||
|
Text.from_markup(
|
||||||
|
"Don't have an account? [bold blue link=https://old.dev.diambra.ai/register]Register here[/]"
|
||||||
|
),
|
||||||
|
id="register_link"
|
||||||
|
),
|
||||||
|
id="login_form"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "toggle_pw":
|
||||||
|
password_input = self.query_one("#password_input", Input)
|
||||||
|
password_input.password = not password_input.password
|
||||||
|
password_input.refresh() # Force UI to update
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.button.id == "login_button":
|
||||||
|
email_input = self.query_one("#email_input", Input)
|
||||||
|
password_input = self.query_one("#password_input", Input)
|
||||||
|
status = self.query_one("#status_message", Static)
|
||||||
|
|
||||||
|
email = email_input.value.strip()
|
||||||
|
password = password_input.value.strip()
|
||||||
|
|
||||||
|
if not email or not password:
|
||||||
|
status.update("[red]❌ Email and password required.[/red]")
|
||||||
|
return
|
||||||
|
|
||||||
|
status.update("🔐 Logging in...")
|
||||||
|
self.set_interval(0.1, lambda: self.perform_login(email, password), name="login_task")
|
||||||
|
|
||||||
|
def perform_login(self, email: str, password: str):
|
||||||
|
status = self.query_one("#status_message", Static)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = login_to_diambra(email, password)
|
||||||
|
save_diambra_token(token)
|
||||||
|
status.update("[green]✅ Login successful![/green]")
|
||||||
|
log_with_caller("info", "User logged in successfully.")
|
||||||
|
self.post_message(self.LoginSuccess())
|
||||||
|
except Exception as e:
|
||||||
|
status.update(f"[red]❌ {str(e)}[/red]")
|
||||||
|
log_with_caller("error", f"Login failed: {e}")
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
textual
|
||||||
|
rich
|
||||||
|
typer
|
||||||
|
diambra
|
||||||
|
diambra-arena
|
||||||
|
pyyaml
|
||||||
|
stable-baselines3
|
||||||
|
tensorboard
|
||||||
|
requests
|
||||||
|
sqlite3
|
||||||