mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-14 14:03:47 -06:00
feat: guide AI agents to the fewest files
llms.txt and an ARD manifest name the pages and the JSON API, and lead an agent setting up a device to --for-games --required-only. Every page registers four read-only WebMCP tools: the setup command, a hash lookup, an emulator's files and the site search.
This commit is contained in:
1 parent
072380c268
commit
24d7db0ab3
8 files changed
+432
-3
No files matched your search
@@ -32,7 +32,7 @@ import urllib.parse
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from collections.abc import Callable, Iterator
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import TypeVar
|
||||
@@ -92,6 +92,7 @@ import sitemarkup as markup
|
||||
import siteentries
|
||||
import siteinputs
|
||||
import retired_files
|
||||
import siteagents
|
||||
import sitestarlight
|
||||
from siteexports import generate_exports_page, load_export_reports
|
||||
from siteguides import (
|
||||
@@ -8065,7 +8066,7 @@ def generate_emulators_index(profiles: dict) -> str:
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _snippet_noun(names, firmware: int, fetched: int) -> str:
|
||||
def _snippet_noun(names: Iterable[str], firmware: int, fetched: int) -> str:
|
||||
"""What an emulator's files are called in its snippet. A Switch
|
||||
emulator reads keys, and "eden keys" is what readers search for; Ship
|
||||
of Harkinian reads game ROMs, which are no BIOS."""
|
||||
@@ -12528,6 +12529,9 @@ def _generate(args: argparse.Namespace) -> None:
|
||||
stats = compute_stats(db, coverages, profiles)
|
||||
stats["composition"] = compute_composition(db)
|
||||
write_if_changed(str(public / "stats.json"), generate_stats(stats))
|
||||
# llms.txt and the ARD manifest an agent reads first, from the registry.
|
||||
siteagents.write(public, sorted(((name, cov["platform"]) for name, cov in coverages.items()),
|
||||
key=lambda row: row[1].casefold()), write_if_changed)
|
||||
|
||||
# Computed once: the gap analysis page and the published gaps export must
|
||||
# report the same files.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""What the site tells an AI agent about itself.
|
||||
|
||||
- ``llms.txt`` (https://llmstxt.org): the pages and data an agent should
|
||||
read first, in Markdown, at the site's root.
|
||||
- ``ard.json``: an Agentic Resource Discovery manifest
|
||||
(https://github.com/ards-project/ard-spec) listing that file and the JSON
|
||||
API, linked from every page's head with ``rel="ard"``.
|
||||
|
||||
Both are written from the registry at each build, so a new platform or a
|
||||
moved page cannot leave them stale. The WebMCP tools a page registers live
|
||||
in website/src/scripts/agent-tools.ts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
SITE = "https://abdess.github.io/retrobios/"
|
||||
PUBLISHER = "abdess.github.io"
|
||||
SH = "curl -fsSL https://abdess.github.io/retrobios/i | sh -s --"
|
||||
PS = "& ([scriptblock]::Create((irm https://abdess.github.io/retrobios/i.ps1)))"
|
||||
|
||||
START_PAGES = (
|
||||
("Which pack do I need", "which-pack/", "pick a device or setup, get the folder its BIOS files go in, "
|
||||
"one command that installs and checks them, or a ZIP pack"),
|
||||
("First install", "wiki/first-install/", "the steps from nothing to a working BIOS folder"),
|
||||
("What are BIOS files", "wiki/what-are-bios-files/", "why emulators need them and where they come from"),
|
||||
("Troubleshooting", "wiki/troubleshooting/", "a file the emulator does not find or refuses"),
|
||||
("Common questions", "wiki/faq/", "short answers"),
|
||||
)
|
||||
|
||||
DATA = (
|
||||
("API index", "api/v1/index.json", "every dataset of the JSON API, versioned"),
|
||||
("Catalog", "api/v1/catalog.json", "each dataset, manifest, export and download with its size, SHA-256 and schema"),
|
||||
("Emulators", "api/v1/emulators.json", "every emulator profile: the files it reads, required or not, "
|
||||
"their hashes and the source line that reads each. One document "
|
||||
"per emulator at api/v1/emulators/<id>.json"),
|
||||
("Core names", "api/v1/cores.json", "each core name and alias, with the profile it designates"),
|
||||
("Systems", "api/v1/systems.json", "each system id, its name and the section holding its files"),
|
||||
("Platforms", "api/v1/platforms.json", "each setup (RetroArch, Batocera...), its BIOS folder and files"),
|
||||
("Hash lookup", "api/v1/lookup/00.json", "what a hash (MD5, SHA-1, SHA-256, CRC32) designates, sharded "
|
||||
"by its first two hex characters: api/v1/lookup/<xx>.json"),
|
||||
)
|
||||
|
||||
MORE = (
|
||||
("Emulators", "emulators/", "one page per emulator"),
|
||||
("Systems", "systems/", "one page per console maker, every file with its hashes"),
|
||||
("Installer", "wiki/installer/", "every option of install.py"),
|
||||
("What we count", "wiki/privacy/", "the site's cookie-free counting"),
|
||||
("Is it legal?", "wiki/legal/", "the project's position"),
|
||||
("Contributing", "contributing/", "add a missing file or an emulator profile"),
|
||||
)
|
||||
|
||||
|
||||
def llms_txt(platforms: list[tuple[str, str]]) -> str:
|
||||
"""The site's llms.txt. ``platforms`` lists (id, display name) of every
|
||||
registered setup, each with its page."""
|
||||
lines = [
|
||||
"# RetroBIOS",
|
||||
"",
|
||||
"> BIOS and firmware files for emulators, each checked against the emulator's source "
|
||||
"code: which files an emulator reads, whether it starts without them, where each one "
|
||||
"goes on every setup, their hashes, and an installer that puts them in place.",
|
||||
"",
|
||||
"Every claim about a file cites the line of emulator source code that reads it. The "
|
||||
"files themselves are served by the GitHub repository "
|
||||
"(https://github.com/Abdess/retrobios) and its releases.",
|
||||
"",
|
||||
"## Setting up a device for its user",
|
||||
"",
|
||||
"Most people want to play now, with the fewest files. The installer does that in one "
|
||||
"command: it finds the setup and its BIOS folder, reads the games on the card, and with "
|
||||
"`--for-games --required-only` installs only the files those games cannot start "
|
||||
"without. Without these two options it installs every file of the setup's list "
|
||||
"(gigabytes), even with `--yes`.",
|
||||
"",
|
||||
f"1. Preview, nothing written: `{SH} --for-games --required-only --dry-run`",
|
||||
f"2. Install, no questions: `{SH} --for-games --required-only --yes`",
|
||||
f"3. On Windows (PowerShell): `{PS} --for-games --required-only --yes`",
|
||||
"",
|
||||
"- Run it on the device itself (its terminal or SSH), or on a computer with the device's "
|
||||
"card plugged in. The installer recognises the handheld systems on the card, among them "
|
||||
"ArkOS, dArkOS, ArchR and UnofficialOS (R36S and its clones), muOS, KNULLI, ROCKNIX, "
|
||||
"AmberELEC, EmuELEC, Onion, spruceOS and MinUI. "
|
||||
"The [Linux handhelds page](" + SITE + "platforms/linux-handhelds/) maps each device "
|
||||
"to its system and folder.",
|
||||
"- From a computer, if the card is not found, name its BIOS folder: `--dest <card>/bios` "
|
||||
"(ArkOS and dArkOS: the `bios` folder of the EASYROMS part).",
|
||||
"- A games folder somewhere else: `--for-games <folder>`, one subfolder per system.",
|
||||
"- Later, to see what is missing or wrong: `--check`. Machine-readable progress: "
|
||||
"`--events` (JSON lines, one per event, documented on the installer page).",
|
||||
"- A game the installer cannot identify gets every BIOS of its system, and the run names it.",
|
||||
"",
|
||||
"## Start",
|
||||
"",
|
||||
*(f"- [{title}]({SITE}{path}): {what}" for title, path, what in START_PAGES),
|
||||
"",
|
||||
"## Setups",
|
||||
"",
|
||||
*(f"- [{name}]({SITE}platforms/{pid}/): its BIOS folder and files" for pid, name in platforms),
|
||||
"",
|
||||
"## Data",
|
||||
"",
|
||||
*(f"- [{title}]({SITE}{path}): {what}" for title, path, what in DATA),
|
||||
"",
|
||||
"## Optional",
|
||||
"",
|
||||
*(f"- [{title}]({SITE}{path}): {what}" for title, path, what in MORE),
|
||||
]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def ard_manifest() -> dict:
|
||||
"""The ARD manifest: the llms.txt and the JSON API, each with the
|
||||
questions it answers."""
|
||||
return {
|
||||
"entries": [
|
||||
{
|
||||
"identifier": f"urn:air:{PUBLISHER}:retrobios:guide",
|
||||
"displayName": "RetroBIOS guide for agents",
|
||||
"type": "text/markdown",
|
||||
"url": f"{SITE}llms.txt",
|
||||
"description": "The pages and datasets of RetroBIOS: BIOS and firmware files for "
|
||||
"emulators, checked against each emulator's source code.",
|
||||
"representativeQueries": [
|
||||
"install the minimum BIOS files for the games on my R36S running dArkOS",
|
||||
"which BIOS files does mGBA need",
|
||||
"where do PlayStation BIOS files go on Batocera",
|
||||
"how do I install BIOS files for RetroArch",
|
||||
],
|
||||
"tags": ["bios", "firmware", "emulation", "retrogaming"],
|
||||
},
|
||||
{
|
||||
"identifier": f"urn:air:{PUBLISHER}:retrobios:api",
|
||||
"displayName": "RetroBIOS JSON API",
|
||||
"type": "application/json",
|
||||
"url": f"{SITE}api/v1/index.json",
|
||||
"description": "Versioned JSON datasets: emulator profiles, platforms, systems, core "
|
||||
"names and a hash lookup.",
|
||||
"representativeQueries": [
|
||||
"what file has the MD5 490f666e1afb15b7362b406ed1cea246",
|
||||
"which files does the beetle_psx core read",
|
||||
"list the systems Batocera supports",
|
||||
],
|
||||
"capabilities": ["HashLookup", "EmulatorFiles", "PlatformFiles"],
|
||||
"tags": ["api", "json", "bios"],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def write(public: Path, platforms: list[tuple[str, str]],
|
||||
write_if_changed: Callable[[str, str], object]) -> None:
|
||||
write_if_changed(str(public / "llms.txt"), llms_txt(platforms))
|
||||
write_if_changed(str(public / "ard.json"), json.dumps(ard_manifest(), indent=1) + "\n")
|
||||
Reference in new issue
Block a user