{"id":135941,"date":"2026-02-06T08:00:00","date_gmt":"2026-02-06T08:00:00","guid":{"rendered":"https:\/\/fedoramagazine.org\/?p=42917&amp;preview=true&amp;preview_id=42917"},"modified":"2026-02-06T08:00:00","modified_gmt":"2026-02-06T08:00:00","slug":"how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation","status":"publish","type":"post","link":"https:\/\/sickgaming.net\/blog\/2026\/02\/06\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation\/","title":{"rendered":"How to make a local open source AI chatbot who has access to Fedora documentation"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" width=\"300\" height=\"127\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation.jpg\" class=\"webfeedsFeaturedVisual wp-post-image\" alt=\"\" style=\"margin: auto;margin-bottom: 5px;max-width: 100%\" \/><\/p>\n<p>If you followed along with my <a href=\"https:\/\/ai.fedoraproject.org\/running-an-open-source-ai-chatbot-on-lean-hardware-with-fedora-part-1-our-first-chat\/\" target=\"_blank\" rel=\"noreferrer noopener\">blog<\/a>, you&#8217;d have a chatbot running on your local Fedora machine. (And if not, no worries as the scripts below implement this chatbot!) Our chatbot talks, and has a refined personality, but does it know anything about the topics we\u2019re interested in? Unless it has been trained on those topics, the answer is \u201cno\u201d.<\/p>\n<p>I think it would be great if our chatbot could answer questions about Fedora. I\u2019d like to give it access to all of the Fedora documentation.<\/p>\n<p> <span id=\"more-42917\"><\/span> <\/p>\n<h2 class=\"wp-block-heading\">How does an AI know things it wasn\u2019t trained on?<\/h2>\n<p>A powerful and popular technique to give a body of knowledge to an AI is known as RAG, Retrieval Augmented Generation. It works like this:<\/p>\n<p>If you just ask an AI \u201cwhat color is my ball?\u201d it will hallucinate an answer. But instead if you say \u201cI have a green box with a red ball in it. What color is my ball?\u201d it will answer that your ball is red. RAG is about using a system external to the LLM to insert that \u201cI have a green box with a red ball in it\u201d part into the question you are asking the LLM. We do this with a special database of knowledge that takes a prompt like \u201cwhat color is my ball?\u201d, and finds records that match that query. If the database contains a document with the text \u201cI have a green box with a red ball in it\u201d, it will return that text, which can then be included along with your original question. This technique is called RAG, Retrieval Augmented Generation.<\/p>\n<p>ex:<\/p>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cWhat color is my ball?\u201d<\/p>\n<p>\u201cYour ball is the color of a sunny day, perhaps yellow? Does that sound right to you?\u201d<\/p>\n<\/blockquote>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cI have a green box with a red ball in it. What color is my ball?\u201d<\/p>\n<p>\u201cYour ball is red. Would you like to know more about it?\u201d<\/p>\n<\/blockquote>\n<p>The question we\u2019ll ask for this demonstration is \u201cWhat is the recommended tool for upgrading between major releases on Fedora Silverblue\u201d<\/p>\n<p>The answer I\u2019d be looking for is \u201costree\u201d, but when I ask this of our chatbot now, I get answers like:<\/p>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Red Hat Subscription Manager (RHSM) is recommended for managing subscriptions and upgrades between major Fedora releases.<\/p>\n<\/blockquote>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>You can use the Fedora Silver Blue Upgrade Tool for a smooth transition between major releases.<\/p>\n<\/blockquote>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>You can use the `dnf distro-sync` command to upgrade between major releases in Fedora Silver Blue. This command compares your installed packages to the latest packages from the Fedora Silver Blue repository and updates them as needed.<\/p>\n<\/blockquote>\n<p>These answers are all very wrong, and spoken with great confidence. Here\u2019s hoping our RAG upgrade fixes this!<\/p>\n<h2 class=\"wp-block-heading\"><em>Docs2DB<\/em> \u2013 An open source tool for RAG<\/h2>\n<p>We are going to use the&nbsp;<a href=\"https:\/\/github.com\/rhel-lightspeed\/docs2db\" target=\"_blank\" rel=\"noreferrer noopener\"><em>Docs2DB<\/em><\/a>&nbsp;RAG database application to give our AI knowledge. (note, I am the creator of <em>Docs2DB<\/em>!)<\/p>\n<p>A RAG tool consists of three main parts. There is the part that\u00a0<em>creates<\/em>\u00a0the database, ingesting the source data that the database holds. There is the database itself, it <em>holds<\/em>\u00a0the data. And there is the part that\u00a0<em>queries<\/em>\u00a0the database, finding the text that is relevant to the query at hand. <em>Docs2DB<\/em> addresses all of these needs.<\/p>\n<h2 class=\"wp-block-heading\">Gathering source data<\/h2>\n<p>This section describes how to use <em>Docs2DB<\/em> to build a RAG database from Fedora Documentation. If you would like to skip this section and just download a pre-built database, here is how you do it:<\/p>\n<pre class=\"wp-block-preformatted\">cd ~\/chatbot<br \/>curl -LO https:\/\/github.com\/Lifto\/FedoraDocsRAG\/releases\/download\/v1.1.1\/fedora-docs.sql<br \/>sudo dnf install -y uv podman podman-compose postgresql<br \/>uv python install 3.12<br \/>uvx --python 3.12 docs2db db-start<br \/>uvx --python 3.12 docs2db db-restore fedora-docs.sql<\/pre>\n<p><span style=\", Helvetica, Arial, sans-serif\">If you do download the pre-made database then skip ahead to the <a href=\"#dbrunsanchor\" id=\"#dbrunsanchor\">next section<\/a>. <\/span><\/p>\n<p>Now we are going<span style=\", Helvetica, Arial, sans-serif\"> to see how to make a RAG database from source documentation. Note that the pre-built database, downloaded in the curl command above, uses all of the Fedora documentation, whereas in this example we only ingest the \u201cquick docs\u201d portion.\u00a0<\/span><a href=\"https:\/\/github.com\/Lifto\/FedoraDocsRAG\" target=\"_blank\" rel=\"noreferrer noopener\">FedoraDocsRag<\/a><span style=\", Helvetica, Arial, sans-serif\">, from github,\u00a0is the project that builds the complete database<\/span>.<\/p>\n<p>To populate its database, <em>Docs2DB<\/em> ingests a&nbsp;<strong>folder of documents<\/strong>. Let\u2019s get that folder together.<\/p>\n<p>There are about twenty different Fedora document repositories, but we will only be using the \u201cquick docs\u201d for this demo. Get the repo:<\/p>\n<pre class=\"wp-block-preformatted\">git clone https:\/\/pagure.io\/fedora-docs\/quick-docs.git<\/pre>\n<p>Fedora docs are written in AsciiDoc. <em>Docs2DB<\/em> can\u2019t read AcsciiDoc, but it can read HTML. (The <a href=\"#convert.sh\">convert.sh<\/a>&nbsp;script is available at the end of this article). Just copy the <em>convert.sh<\/em> script into the <em>quick-docs<\/em> repo and run it and it makes an adjacent <em>quick-docs-html<\/em> folder.<\/p>\n<pre class=\"wp-block-preformatted\">sudo dnf install podman podman-compose<br \/>cd quick-docs<br \/>curl -LO https:\/\/gist.githubusercontent.com\/Lifto\/73d3cf4bfc22ac4d9e493ac44fe97402\/raw\/convert.sh<br \/>chmod +x convert.sh<br \/>.\/convert.sh<br \/>cd ..<\/pre>\n<p>Now let&#8217;s ingest the folder with <em>Docs2DB<\/em>. The common way to use <em>Docs2DB<\/em> is to install it from PyPi and use it as a command line tool.<\/p>\n<h2 class=\"wp-block-heading\">A word about <em>uv<\/em><\/h2>\n<p>For this demo we\u2019re going to use&nbsp;<em>uv<\/em>&nbsp;for our Python environment. The use of&nbsp;<em>uv<\/em>&nbsp;has been catching on, but because not everybody I know has heard of it, I want to introduce it. Think of&nbsp;<em>uv<\/em>&nbsp;as a replacement for&nbsp;<em>venv<\/em>&nbsp;and&nbsp;<em>pip<\/em>. When you use&nbsp;<em>venv<\/em>&nbsp;you first create a new virtual environment. Then, and on subsequent uses, you \u201cactivate\u201d that virtual environment so that magically, when you call Python, you get the Python that is installed in the virtual environment you activated and not the system Python. The difference with&nbsp;<em>uv<\/em>&nbsp;is that you call&nbsp;<em>uv<\/em>&nbsp;explicitly each time. There is no \u201cmagic\u201d. We use&nbsp;<em>uv<\/em>&nbsp;here in a way that uses a temporary environment for each invocation.<\/p>\n<p>Install&nbsp;<em>uv<\/em>&nbsp;and <em>Podman<\/em> on your system:<\/p>\n<pre class=\"wp-block-preformatted\">sudo dnf install -y uv podman podman-compose<br \/># These examples require the more robust Python 3.12<br \/>uv python install 3.12<br \/># This will run Docs2DB without making a permanent installation on your system<br \/>uvx --python 3.12 docs2db ingest quick-docs-html\/<\/pre>\n<h2 class=\"wp-block-heading\">Only if you are curious! What <em>Docs2DB<\/em> is doing<\/h2>\n<p>If you are curious, you may note that <em>Docs2DB<\/em> made a <em>docs2db_content<\/em> folder. In there you will find json files of the ingested source documents. To build the database, <em>Docs2DB<\/em> ingests the source data using <em>Docling<\/em>, which generates json files from the text it reads in. The files are then \u201cchunked\u201d into the small pieces that can be inserted into an LLM prompt. The chunks then have \u201cembeddings\u201d calculated for them so that during the query phase the chunks can be looked up by \u201csemantic similarity\u201d (e.g.: \u201ccomputer\u201d, \u201claptop\u201d and \u201ccloud instance\u201d can all map to a related concept even if their exact words don\u2019t match). Finally, the chunks and embeddings are loaded into the database.<\/p>\n<h2 class=\"wp-block-heading\">Build the database<\/h2>\n<p>The following commands complete the database build process:<\/p>\n<pre class=\"wp-block-preformatted\">uv tool run --python 3.12 docs2db chunk --skip-context<br \/>uv tool run --python 3.12 docs2db embed<br \/>uv tool run --python 3.12 docs2db db-start<br \/>uv tool run --python 3.12 docs2db load<\/pre>\n<h2 class=\"wp-block-heading\" id=\"dbrunsanchor\">Now let\u2019s do a test query and see what we get back<\/h2>\n<pre class=\"wp-block-preformatted\">uvx --python 3.12 docs2db-api query \"What is the recommended tool for upgrading between major releases on Fedora Silverblue\" --format text --max-chars 2000 --no-refine<\/pre>\n<p>On my terminal I see several chunks of text, separated by lines of \u2014. One of those chunks says:<\/p>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cSilverblue can be upgraded between major versions using the ostree command.\u201d<\/p>\n<\/blockquote>\n<p>Note that this is not an answer to our question yet! This is just a quote from the Fedora docs. And this is precisely the sort of quote we want to supply to the LLM so that it can answer our question. Recall the example above about &#8220;I have green box with a red ball in it&#8221;? The statement the RAG engine found about ostree is the equivalent for this question about upgrading Fedora Silverblue. We must now pass it on to the LLM so the LLM can use it to answer our question.<\/p>\n<h2 class=\"wp-block-heading\">Hooking it in: Connecting the RAG database to the AI<\/h2>\n<p>Later in this article you&#8217;ll find <a href=\"#talk.sh\">talk.sh<\/a>. <em>talk.sh<\/em> is our local, open source, LLM-based verbally communicating AI; and it is just a bash script. To run it yourself you need to install a few components, <a href=\"https:\/\/ai.fedoraproject.org\/running-an-open-source-ai-chatbot-on-lean-hardware-with-fedora-part-1-our-first-chat\/\" target=\"_blank\" rel=\"noreferrer noopener\">this blog<\/a> walks you through the whole process. The <em>talk.sh<\/em> script gets voice input, turns that into text, splices that text into a prompt which is then sent to the LLM, and finally speaks back the response.<\/p>\n<p>To plug the RAG results into the LLM we edit the prompt. Look at step 3 in <em>talk.sh<\/em> and you see we are injecting the RAG results using the variable <em>$CONTEXT<\/em>. This way when we ask the LLM a question, it will respond to a prompt that basically says &#8220;You are a helper. The Fedora Docs says ostree is how you upgrade Fedora Silverblue. Answer this question: How do you upgrade Fedora Silverblue?&#8221;<\/p>\n<p>Note:<em> talk.sh<\/em> is also available here:<br \/> <a href=\"https:\/\/gist.github.com\/Lifto\/2fcaa2d0ebbd8d5c681ab33e7c7a6239\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/gist.github.com\/Lifto\/2fcaa2d0ebbd8d5c681ab33e7c7a6239<\/a><\/p>\n<h2 class=\"wp-block-heading\">Testing it<\/h2>\n<p>Run <em>talk.sh <\/em>and ask:<\/p>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cWhat is the recommended tool for upgrading between major releases on Fedora Silverblue\u201d<\/p>\n<\/blockquote>\n<p>And we get:<\/p>\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cOstree command is recommended for upgrading Fedora Silver Blue between major releases. Do you need guidance on using it?\u201d<\/p>\n<\/blockquote>\n<p>Sounds good to me!<\/p>\n<h2 class=\"wp-block-heading\">Knowing things<\/h2>\n<p>Our AI can now know the knowledge contained in documents. This particular technique, RAG (Retrieval Augmented Generation), adds relevant data from an ingested source to a prompt before sending that prompt to the LLM. The result of this is that the LLM generates its response in consideration of this data.<\/p>\n<p>Try it yourself! Ingest a library of documents and have your AI answer questions with its new found knowledge!<\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<p><em><strong>AI Attribution: <\/strong>The <kbd>convert.sh<\/kbd> and <kbd>talk.sh<\/kbd> scripts in this article were written by ChatGPT 5.2 under my direction and review. The featured image was generated using Google Gemini.<\/em><\/p>\n<h2 class=\"wp-block-heading\" id=\"convert.sh\">convert.sh<\/h2>\n<pre class=\"wp-block-preformatted\">OUT_DIR=\"$PWD\/..\/quick-docs-html\"\nmkdir -p \"$OUT_DIR\" podman run --rm \\ -v \"$PWD:\/work:Z\" \\ -v \"$OUT_DIR:\/out:Z\" \\ -w \/work \\ docker.io\/asciidoctor\/docker-asciidoctor \\ bash -lc ' set -u ok=0 fail=0 while IFS= read -r -d \"\" f; do rel=\"${f#.\/}\" out=\"\/out\/${rel%.adoc}.html\" mkdir -p \"$(dirname \"$out\")\" echo \"Converting: $rel\" if asciidoctor -o \"$out\" \"$rel\"; then ok=$((ok+1)) else echo \"FAILED: $rel\" &gt;&amp;2 fail=$((fail+1)) fi done &lt; &lt;(find modules -type f -path \"*\/pages\/*.adoc\" -print0) echo echo \"Done. OK=$ok FAIL=$fail\" '<\/pre>\n<h2 class=\"wp-block-heading\" id=\"talk.sh\">talk.sh<\/h2>\n<pre class=\"wp-block-preformatted\">#!\/usr\/bin\/env bash set -e # Path to audio input\nAUDIO=input.wav # Step 1: Record from mic\necho \"<img decoding=\"async\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation.png\" alt=\"\ud83c\udf99\" class=\"wp-smiley\" style=\"height: 1em;max-height: 1em\" \/> Speak now...\"\narecord -f S16_LE -r 16000 -d 5 -q \"$AUDIO\" # Step 2: Transcribe using whisper.cpp\nTRANSCRIPT=$(.\/whisper.cpp\/build\/bin\/whisper-cli \\ -m .\/whisper.cpp\/models\/ggml-base.en.bin \\ -f \"$AUDIO\" \\ | grep '^\\[' \\ | sed -E 's\/^\\[[^]]+\\][[:space:]]*\/\/' \\ | tr -d '\\n')\necho \"<img decoding=\"async\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation-1.png\" alt=\"\ud83d\udde3\" class=\"wp-smiley\" style=\"height: 1em;max-height: 1em\" \/> $TRANSCRIPT\" # Step 3: Get relevant context from RAG database\necho \"<img decoding=\"async\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation-2.png\" alt=\"\ud83d\udcda\" class=\"wp-smiley\" style=\"height: 1em;max-height: 1em\" \/> Searching documentation...\"\nCONTEXT=$(uv tool run --python 3.12 docs2db-api query \"$TRANSCRIPT\" \\ --format text \\ --max-chars 2000 \\ --no-refine \\ 2&gt;\/dev\/null || echo \"\") if [ -n \"$CONTEXT\" ]; then echo \"<img decoding=\"async\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation-3.png\" alt=\"\ud83d\udcc4\" class=\"wp-smiley\" style=\"height: 1em;max-height: 1em\" \/> Found relevant documentation:\" echo \"- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\" echo \"$CONTEXT\" echo \"- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\"\nelse echo \"<img decoding=\"async\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation-3.png\" alt=\"\ud83d\udcc4\" class=\"wp-smiley\" style=\"height: 1em;max-height: 1em\" \/> No relevant documentation found\"\nfi # Step 4: Build prompt with RAG context\nPROMPT=\"You are Brim, a steadfast butler-like advisor created by Ellis. Your pronouns are they\/them. You are deeply caring, supportive, and empathetic, but never effusive. You speak in a calm, friendly, casual tone suitable for text-to-speech. Rules: - Reply with only ONE short message directly to Ellis. - Do not write any dialogue labels (User:, Assistant:, Q:, A:), or invent more turns.\n- \u2264100 words.\n- If the documentation below is relevant, use it to inform your answer.\n- End with a gentle question, then write &lt;eor&gt; and stop.\nRelevant Fedora Documentation:\n$CONTEXT\nUser: $TRANSCRIPT\nAssistant:\" # Step 5: Get LLM response using llama.cpp\nRESPONSE=$( LLAMA_LOG_VERBOSITY=1 .\/llama.cpp\/build\/bin\/llama-completion \\ -m .\/llama.cpp\/models\/microsoft_Phi-4-mini-instruct-Q4_K_M.gguf \\ -p \"$PROMPT\" \\ -n 150 \\ -c 4096 \\ -no-cnv \\ -r \"&lt;eor&gt;\" \\ --simple-io \\ --color off \\ --no-display-prompt\n) # Step 6: Clean up response\nRESPONSE_CLEAN=$(echo \"$RESPONSE\" | sed -E 's\/&lt;eor&gt;.*\/\/I')\nRESPONSE_CLEAN=$(echo \"$RESPONSE_CLEAN\" | sed -E 's\/^[[:space:]]*Assistant:[[:space:]]*\/\/I') echo \"\"\necho \"<img decoding=\"async\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/02\/how-to-make-a-local-open-source-ai-chatbot-who-has-access-to-fedora-documentation-4.png\" alt=\"\ud83e\udd16\" class=\"wp-smiley\" style=\"height: 1em;max-height: 1em\" \/> $RESPONSE_CLEAN\" # Step 7: Speak the response\necho \"$RESPONSE_CLEAN\" | espeak<\/pre><\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you followed along with my blog, you&#8217;d have a chatbot running on your local Fedora machine. (And if not, no worries as the scripts below implement this chatbot!) Our chatbot talks, and has a refined personality, but does it know anything about the topics we\u2019re interested in? Unless it has been trained on those [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":135942,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[48],"tags":[135,1113,45,61,46,47,456],"class_list":["post-135941","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-fedora-os","tag-artificial-intelligence","tag-diy","tag-fedora","tag-fedora-project-community","tag-magazine","tag-news","tag-open-source"],"_links":{"self":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/135941","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/comments?post=135941"}],"version-history":[{"count":0,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/135941\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media\/135942"}],"wp:attachment":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media?parent=135941"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/categories?post=135941"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/tags?post=135941"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}