From 12339c3282e11779c1b97c69b11666440f3c4ec3 Mon Sep 17 00:00:00 2001 From: Jessey van Offeren Date: Fri, 22 May 2026 13:40:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(ai):=20resolve=20Steam=20app=20IDs=20from?= =?UTF-8?q?=20the=20library,=20don't=20make=20the=20model=20guess=20?= =?UTF-8?q?=E2=80=94=200.29.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The model guessed "Rainbow Six Siege" for appID 2694490 (Path of Exile 2). We already know the names locally, so ground it: steam.appid_names() maps appid→name from the scanned library, and ai.build_prompt scans the text for app IDs and injects a resolved glossary. Only locally-known IDs are listed; no network, no fine-tuning. Tests + verified live (2694490 = Path of Exile 2). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- src/rigdoctor/__init__.py | 2 +- src/rigdoctor/core/ai.py | 32 ++++++++++++++++++++++++++++++-- src/rigdoctor/core/steam.py | 5 +++++ tests/test_ai.py | 17 +++++++++++++++++ 6 files changed, 62 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c91b0c1..ccbe9dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to RigDoctor are recorded here. Format follows (`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git release tag (so the auto-updater, D18, can compare versions). +## [0.29.0] - 2026-05-22 +### Added +- **AI now resolves Steam app IDs from your library instead of guessing.** When app IDs appear + in the logs/findings, RigDoctor looks them up in your scanned games (`steam.appid_names()`) and + injects an "App IDs (resolved from your installed games)" glossary into the prompt — so the + model names games correctly (e.g. `2694490 = Path of Exile 2`) rather than hallucinating. Only + IDs it can resolve locally are listed; no network, no model "training" needed. + ## [0.28.1] - 2026-05-22 ### Fixed - **AI explanations were misreading stale/benign logs.** Three fixes so the model analyses the diff --git a/pyproject.toml b/pyproject.toml index 07d3c50..78d801a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rigdoctor" -version = "0.28.1" +version = "0.29.0" description = "Modular hardware monitoring & crash diagnostics for Linux gamers." readme = "README.md" requires-python = ">=3.11" diff --git a/src/rigdoctor/__init__.py b/src/rigdoctor/__init__.py index 19e81dc..4fff45f 100644 --- a/src/rigdoctor/__init__.py +++ b/src/rigdoctor/__init__.py @@ -1,3 +1,3 @@ """RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers.""" -__version__ = "0.28.1" +__version__ = "0.29.0" diff --git a/src/rigdoctor/core/ai.py b/src/rigdoctor/core/ai.py index f690489..02f4630 100644 --- a/src/rigdoctor/core/ai.py +++ b/src/rigdoctor/core/ai.py @@ -16,12 +16,15 @@ Answers are *grounded*: we pass the actual findings plus matched reference facts from __future__ import annotations import json +import re import urllib.error import urllib.request from .. import config from . import ai_knowledge +_APPID_RE = re.compile(r"\b\d{5,7}\b") # Steam app IDs are 5–7 digits + PROVIDERS = ("ollama", "claude") OLLAMA_DEFAULT_ENDPOINT = "http://localhost:11434" # Suggested Ollama model — strong instruction-following that fits an 8 GB GPU at Q4. Because we @@ -89,10 +92,35 @@ def provider_label() -> str: return "not configured" +def appid_glossary(text: str) -> str: + """Resolve Steam app IDs that appear in `text` against the user's scanned library. + + We don't teach the model app IDs — we look them up locally and hand it the mapping, so it + names games correctly instead of guessing. Only IDs we can resolve are listed. + """ + candidates = set(_APPID_RE.findall(text)) + if not candidates: + return "" + try: + from . import steam + names = steam.appid_names() + except Exception: # never let a glossary lookup break an explanation + return "" + known = sorted((i, names[i]) for i in candidates if i in names) + if not known: + return "" + return "App IDs (resolved from your installed games):\n" + "\n".join( + f"- {appid} = {name}" for appid, name in known) + + def build_prompt(findings_text: str) -> str: - """The user-message content: matched reference facts + the collected findings.""" - facts = ai_knowledge.relevant(findings_text) + """The user-message content: app-ID glossary + matched reference facts + the findings.""" parts = [] + glossary = appid_glossary(findings_text) + if glossary: + parts.append(glossary) + parts.append("") + facts = ai_knowledge.relevant(findings_text) if facts: parts.append("Reference facts (use these to interpret the findings):") parts += [f"- {f}" for f in facts] diff --git a/src/rigdoctor/core/steam.py b/src/rigdoctor/core/steam.py index 4076fb3..089969f 100644 --- a/src/rigdoctor/core/steam.py +++ b/src/rigdoctor/core/steam.py @@ -318,6 +318,11 @@ def cached_games() -> list[Game]: return [Game(**{k: g[k] for k in Game.__dataclass_fields__ if k in g}) for g in cache.get("games", [])] +def appid_names() -> dict[str, str]: + """{appid: name} for the user's scanned games — lets us resolve IDs seen in logs (M14).""" + return {g.appid: g.name for g in cached_games() if g.appid and g.name} + + def rescan(cfg: dict | None = None) -> ScanResult: """Scan the selected libraries, diff against the cache, and persist the result. diff --git a/tests/test_ai.py b/tests/test_ai.py index d0307d0..ef96d7c 100644 --- a/tests/test_ai.py +++ b/tests/test_ai.py @@ -62,6 +62,23 @@ class PromptTests(unittest.TestCase): text = ai.format_findings([F()]) self.assertIn("[WARN] GPU: Hot — 92C", text) + def test_appid_glossary_resolves_known_ids(self): + from rigdoctor.core import steam + with mock.patch.object(steam, "appid_names", return_value={"2694490": "Path of Exile 2"}): + glossary = ai.appid_glossary("Steam log: removed AppID 2694490 ... pid 130544") + self.assertIn("2694490 = Path of Exile 2", glossary) + + def test_appid_glossary_ignores_unknown_ids(self): + from rigdoctor.core import steam + with mock.patch.object(steam, "appid_names", return_value={"570": "Dota 2"}): + self.assertEqual(ai.appid_glossary("pid 130544 used 8192 MiB"), "") # not in library + + def test_build_prompt_includes_glossary(self): + from rigdoctor.core import steam + with mock.patch.object(steam, "appid_names", return_value={"2694490": "Path of Exile 2"}): + prompt = ai.build_prompt("AppID 2694490 launched") + self.assertIn("Path of Exile 2", prompt) + class ExplainTests(unittest.TestCase): def _cfg(self, **over):