feat(ai): resolve Steam app IDs from the library, don't make the model guess — 0.29.0
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
||||||
release tag (so the auto-updater, D18, can compare versions).
|
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
|
## [0.28.1] - 2026-05-22
|
||||||
### Fixed
|
### Fixed
|
||||||
- **AI explanations were misreading stale/benign logs.** Three fixes so the model analyses the
|
- **AI explanations were misreading stale/benign logs.** Three fixes so the model analyses the
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rigdoctor"
|
name = "rigdoctor"
|
||||||
version = "0.28.1"
|
version = "0.29.0"
|
||||||
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||||
|
|
||||||
__version__ = "0.28.1"
|
__version__ = "0.29.0"
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ Answers are *grounded*: we pass the actual findings plus matched reference facts
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from .. import config
|
from .. import config
|
||||||
from . import ai_knowledge
|
from . import ai_knowledge
|
||||||
|
|
||||||
|
_APPID_RE = re.compile(r"\b\d{5,7}\b") # Steam app IDs are 5–7 digits
|
||||||
|
|
||||||
PROVIDERS = ("ollama", "claude")
|
PROVIDERS = ("ollama", "claude")
|
||||||
OLLAMA_DEFAULT_ENDPOINT = "http://localhost:11434"
|
OLLAMA_DEFAULT_ENDPOINT = "http://localhost:11434"
|
||||||
# Suggested Ollama model — strong instruction-following that fits an 8 GB GPU at Q4. Because we
|
# 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"
|
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:
|
def build_prompt(findings_text: str) -> str:
|
||||||
"""The user-message content: matched reference facts + the collected findings."""
|
"""The user-message content: app-ID glossary + matched reference facts + the findings."""
|
||||||
facts = ai_knowledge.relevant(findings_text)
|
|
||||||
parts = []
|
parts = []
|
||||||
|
glossary = appid_glossary(findings_text)
|
||||||
|
if glossary:
|
||||||
|
parts.append(glossary)
|
||||||
|
parts.append("")
|
||||||
|
facts = ai_knowledge.relevant(findings_text)
|
||||||
if facts:
|
if facts:
|
||||||
parts.append("Reference facts (use these to interpret the findings):")
|
parts.append("Reference facts (use these to interpret the findings):")
|
||||||
parts += [f"- {f}" for f in facts]
|
parts += [f"- {f}" for f in facts]
|
||||||
|
|||||||
@@ -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", [])]
|
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:
|
def rescan(cfg: dict | None = None) -> ScanResult:
|
||||||
"""Scan the selected libraries, diff against the cache, and persist the result.
|
"""Scan the selected libraries, diff against the cache, and persist the result.
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,23 @@ class PromptTests(unittest.TestCase):
|
|||||||
text = ai.format_findings([F()])
|
text = ai.format_findings([F()])
|
||||||
self.assertIn("[WARN] GPU: Hot — 92C", text)
|
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):
|
class ExplainTests(unittest.TestCase):
|
||||||
def _cfg(self, **over):
|
def _cfg(self, **over):
|
||||||
|
|||||||
Reference in New Issue
Block a user