# AGENTS.md Guide for coding agents operating in this repository. MiroFish is a multi-agent AI prediction engine: Flask backend (`backend/`) + Vue 3 frontend (`frontend/`) + shared i18n (`locales/`). Monorepo orchestrated by root `package.json` via `concurrently`. Code comments/docstrings are **English**; identifiers are English. License: AGPL-3.0. ## Build / Run / Test Commands All commands run from repo root unless noted. Node >=18, Python >=3.11 <=3.12, `uv` for Python packages. ```bash # Install everything (root + frontend npm + backend uv venv) npm run setup:all # Dev (both services, concurrent, --kill-others) npm run dev npm run backend # cd backend && uv run python run.py (Flask :5001) npm run frontend # cd frontend && npm run dev (Vite :3000) # Production build (frontend only) npm run build # Backend deps only npm run setup:backend # cd backend && uv sync # Docker docker compose up -d # reads root .env, ports 3000+5001, mounts backend/uploads ``` ### Tests **There are no automated tests.** `pytest` + `pytest-asyncio` are declared in `backend/pyproject.toml` but unused — no `tests/` dir, no `conftest.py`, no `[tool.pytest.ini_options]`. Frontend has no vitest/jest. If you add tests, run them with: ```bash cd backend && uv run pytest # all cd backend && uv run pytest path/to/test_file.py # single file cd backend && uv run pytest path/to/test_file.py::test_name # single test ``` `backend/scripts/test_profile_format.py` is a **manual print-based script**, not a pytest test — run it with `uv run python scripts/test_profile_format.py`. ### Lint / Format **None configured.** No ruff/black/flake8/mypy/isort on the backend; no eslint/prettier on the frontend. No CI lint workflow (only Docker image build). Match the existing de-facto style described below; do not add a linter unless asked. ## Backend Conventions (Python / Flask) **Package layout:** `backend/app/` with `api/` (blueprints), `models/` (dataclasses), `services/` (business logic), `utils/` (logger, retry, llm_client, locale). Entry point `backend/run.py`. Each `__init__.py` is a manifest: docstring + relative imports + `__all__`. **Imports:** Relative for intra-package (`from .config import Config`, `from ..services.x import Y`). Absolute `from app...` only in `run.py` and `scripts/` (which shim `sys.path`). Group: stdlib → third-party → local, blank-line separated. Late imports inside functions are acceptable for optional/heavy deps. **Naming:** `snake_case` modules/functions, `PascalCase` classes, `UPPER_SNAKE` constants, `_`-prefixed private helpers. Blueprints: `snake_case_bp` (`graph_bp`, `simulation_bp`, `report_bp`). String IDs prefixed (`proj_`, `sim_`, `mirofish_`). **Models:** `@dataclass` + `(str, Enum)` for status enums. Hand-written `to_dict()` / `from_dict()` on every model. **No app-defined Pydantic models** — pydantic is only used in generated ontology code templates and the OASIS SDK. **Type hints:** Annotate function signatures (`typing.Dict/Any/List/Optional/ Callable/Tuple`). Locals usually unannotated. Be consistent with neighbors. **Errors:** No custom exception classes; raise `ValueError` for bad state. Every Flask route wraps in `try/except Exception as e:` returning the uniform envelope: ```python except Exception as e: return jsonify({"success": False, "error": str(e), "traceback": traceback.format_exc()}), 500 ``` Success: `{"success": True, "data": {...}}` (often + `"count"`, `"message"`). `ValueError` → 400/404. Background threads catch exceptions and flip the `TaskManager`/`Project` status to `FAILED`. **Logging:** `from ..utils.logger import get_logger` then `logger = get_logger('mirofish.')` at module top (areas: `api`, `api.simulation`, `build`, `request`, `retry`, `simulation`, etc.). f-string messages. Do **not** use `logging.getLogger(__name__)` — `ontology_generator.py` does this and is the one known inconsistency. Rotating file handler writes to `backend/logs/.log`. **Async:** Flask is sync (`threaded=True`). Long work goes to `threading.Thread(target=..., daemon=True)`. **Background threads must re-call `set_locale()`** — locale is thread-local; capture `get_locale()` before spawning and restore it inside the thread. Retry helpers in `utils/retry.py`: `retry_with_backoff` (sync), `retry_with_backoff_async` (async), `RetryableAPIClient`. **Config:** `app/config.py` `Config` class loads root `.env` via `dotenv.load_dotenv('../../.env')` (relative to `config.py`). Required: `LLM_API_KEY`. Optional: `LLM_BASE_URL`, `LLM_MODEL_NAME`, `LLM_BOOST_*`, `FLASK_HOST/PORT/DEBUG`, `OASIS_DEFAULT_MAX_ROUNDS`, `REPORT_AGENT_*`, `SECRET_KEY`. `Config.validate()` is called in `run.py` before serving. Frontend env: `VITE_API_BASE_URL` (default `http://localhost:5001`). **Docstrings:** Module docstring (English, one line) at top of every `.py`. Functions/classes: English summary, Google-style `Args:`/`Returns:`. Flask routes embed request/response JSON shapes as literal blocks — treat these as the API docs. **Windows:** `run.py` applies a UTF-8 stdout fix before other imports. Keep that block first. ## Frontend Conventions (Vue 3 + Vite, plain JS) **SFCs:** `