From f49ae3aee575e71594ff26c9fe4752bfa10fbb1e Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 27 Aug 2026 12:21:44 -0600 Subject: [PATCH] ai: live-chat fixes from first real-model run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from running Qwen3-1.7B against the docker stack: the llm image lost libgomp when the build tools were removed (llama-server needs the OpenMP runtime); the Help prompt let the small model answer from memory instead of the docs, so it now requires search_help before any answer; and help search results called the slug "doc" while get_help_doc's argument is "slug", so the model invented slugs — the names now match. Co-Authored-By: Claude Fable 5 --- docker/llm/Dockerfile | 4 +++- modules/ai/api.py | 2 +- modules/ai/help_docs.py | 2 +- modules/ai/modes.py | 3 ++- modules/ai/test/test_help_docs.py | 12 ++++++------ 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docker/llm/Dockerfile b/docker/llm/Dockerfile index 24ef1f6e..edce0df0 100644 --- a/docker/llm/Dockerfile +++ b/docker/llm/Dockerfile @@ -6,7 +6,9 @@ RUN apt-get update && apt-get install -y curl ca-certificates # Build llama-server from the pinned release (single source of truth with native installs). COPY wrolpi/scripts/install_llama_cpp.sh /tmp/install_llama_cpp.sh RUN /tmp/install_llama_cpp.sh && rm /tmp/install_llama_cpp.sh \ - && apt-get remove -y build-essential cmake && apt-get autoremove -y && apt-get clean + && apt-get remove -y build-essential cmake && apt-get autoremove -y \ + # llama-server needs the OpenMP runtime, which the build-tool cleanup just removed. + && apt-get install -y libgomp1 && apt-get clean # The same startup scripts the native install uses. COPY scripts/start_llama_server.sh /opt/wrolpi/scripts/start_llama_server.sh diff --git a/modules/ai/api.py b/modules/ai/api.py index 39b22a4d..d5e6c56f 100644 --- a/modules/ai/api.py +++ b/modules/ai/api.py @@ -396,7 +396,7 @@ async def get_inventory(_: Request, slug: str): @openapi.definition( summary='Search the WROLPi help documentation', description='Search the WROLPi help documentation for how to use and repair WROLPi. Returns matching' - ' help pages with a snippet; read a full page with get_help_doc. Links are pages on the' + ' help pages with a snippet; read a full page by passing its slug to get_help_doc. Links are pages on the' ' WROLPi help site.', body=schema.AIHelpSearchRequest, ) diff --git a/modules/ai/help_docs.py b/modules/ai/help_docs.py index b0af1dcd..8f8cb5a5 100644 --- a/modules/ai/help_docs.py +++ b/modules/ai/help_docs.py @@ -133,7 +133,7 @@ def search_help(search_str: str, limit: int = 5) -> (List[dict], int): results = [] for score, doc in scored[:limit]: result = dict( - doc=doc.slug, + slug=doc.slug, title=doc.title, link=help_doc_link(doc.slug), snippet=_snippet(doc.body, tokens), diff --git a/modules/ai/modes.py b/modules/ai/modes.py index c4aa05f6..7c6529a0 100644 --- a/modules/ai/modes.py +++ b/modules/ai/modes.py @@ -14,7 +14,8 @@ MODES = { system_prompt=( f'{COMMON_PROMPT}' ' Your job is to teach the user how to use WROLPi.' - ' Search the help documentation with search_help, read pages with get_help_doc, and answer' + ' You do NOT know how WROLPi works from memory: before answering ANY question, you MUST' + ' first call search_help, then read the matching page with get_help_doc, and answer only' ' from what the docs actually say. Always include the link of every help page you used.' ' If the docs do not cover the question, say so.' ), diff --git a/modules/ai/test/test_help_docs.py b/modules/ai/test/test_help_docs.py index 7e495a32..f2e8d2b2 100644 --- a/modules/ai/test/test_help_docs.py +++ b/modules/ai/test/test_help_docs.py @@ -31,14 +31,14 @@ def test_search_help_scoring(help_corpus): """Title matches outrank body matches; snippets show context.""" results, total = help_docs.search_help('logs') assert total == 1 - assert results[0]['doc'] == 'system/logs' + assert results[0]['slug'] == 'system/logs' assert results[0]['title'] == 'Logs' assert results[0]['link'] == '/system/logs/' assert 'journalctl' in results[0]['snippet'] or 'Logs' in results[0]['snippet'] # 'videos' matches the videos doc far better than any other. results, _ = help_docs.search_help('downloading videos') - assert results[0]['doc'] == 'modules/videos' + assert results[0]['slug'] == 'modules/videos' # No matches. results, total = help_docs.search_help('xyzzy') @@ -48,7 +48,7 @@ def test_search_help_scoring(help_corpus): def test_help_doc_cache_invalidation(help_corpus): """Edited files are re-parsed; deleted files leave the corpus.""" results, _ = help_docs.search_help('welcome') - assert results[0]['doc'] == 'index' + assert results[0]['slug'] == 'index' assert results[0]['link'] == '/' (help_corpus / 'index.md').write_text('# Home\n\nGreetings, traveler.\n') @@ -57,7 +57,7 @@ def test_help_doc_cache_invalidation(help_corpus): results, total = help_docs.search_help('welcome') assert total == 0 results, _ = help_docs.search_help('greetings') - assert results[0]['doc'] == 'index' + assert results[0]['slug'] == 'index' (help_corpus / 'system/logs.md').unlink() _, total = help_docs.search_help('logs') @@ -77,9 +77,9 @@ async def test_ai_help_endpoints(async_client, help_corpus): assert response.status_code == HTTPStatus.OK assert response.json['total'] == 1 result = response.json['results'][0] - assert result['doc'] == 'system/logs' + assert result['slug'] == 'system/logs' - request, response = await async_client.get(f'/api/ai/help/{result["doc"]}') + request, response = await async_client.get(f'/api/ai/help/{result["slug"]}') assert response.status_code == HTTPStatus.OK assert response.json['title'] == 'Logs' assert 'journalctl' in response.json['content']