
{"id":5383,"date":"2025-02-09T21:18:40","date_gmt":"2025-02-09T21:18:40","guid":{"rendered":"https:\/\/blog.gordonbuchan.com\/blog\/?p=5383"},"modified":"2025-02-10T18:12:19","modified_gmt":"2025-02-10T18:12:19","slug":"creating-a-wordpress-chatbot-using-facebook-ai-similarity-search-faiss-for-retrieval-augmented-generation-rag-and-an-external-large-language-model-llm-application-programming-interface-api","status":"publish","type":"post","link":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/02\/09\/creating-a-wordpress-chatbot-using-facebook-ai-similarity-search-faiss-for-retrieval-augmented-generation-rag-and-an-external-large-language-model-llm-application-programming-interface-api\/","title":{"rendered":"Creating a WordPress chatbot using Facebook AI\u00a0Similarity Search (FAISS) for retrieval augmented generation (RAG) and an external large language model (LLM) application programming interface (API)"},"content":{"rendered":"\n<p>This procedure describes how to create a WordPress chatbot using FAISS for RAG and an external LLM API. We start by scanning the database of WordPress posts, to create a FAISS vector database. We then create an API wrapper that combines hinting information from the local FAISS database with a call to a remote LLM API. This API wrapper is then called by a chatbot, which is then integrated into WordPress as a plugin. The user interface for the chatbot is added to the sidebar of the WordPress blog by adding a shortcode widget that references the chatbot&#8217;s PHP, JavaScript, and cascading stylesheet (CSS) elements.<\/p>\n\n\n\n<p>The chatbot accepts natural language queries, submits the queries to the RAG API wrapper, and displays results that contain the remote LLM API&#8217;s responses based on the text of blog posts scanned by the RAG system. Links to relevant blog posts are listed in the responses.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Using a recent Linux distribution to support Python 3.12 and some machine learning tools<\/h1>\n\n\n\n<p>In order to implement this procedure, we need a recent Linux distribution to support Python 3.12 and some machine learning tools. For this procedure we are using Ubuntu Server 24.04 LTS.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Using a server with relatively modest specifications<\/h1>\n\n\n\n<p>Most public-facing websites are hosted in virtual machines (VMs) on cloud servers, with relatively modest specifications. Because we are able to use an external LLM API service, we only need enough processing power to host the WordPress blog itself, as well as some Python and PHP code that implements the FAISS vector database, the RAG API wrapper, and the chatbot itself. For this procedure, we are deploying on a cloud server with 2GB RAM, 2 x vCPU, and 50GB SSD drive space.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Escalating to the root user<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo su\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Installing operating system dependencies<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\napt install php-curl libmariadb-dev python3-pip python3-venv\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating a virtual environment for Python<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncd \/var\/www\/html\nmkdir ragblog_workdir\ncd ragblog_workdir\npython3 -m venv ragblog_env\nsource ragblog_env\/bin\/activate\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Installing Python dependencies<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npip install faiss-cpu sentence-transformers numpy fastapi uvicorn requests python-dotenv mariadb\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating a .env file<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano .env\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text. Substitute values as appropriate for your environment:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nEXTERNAL_LLM_API=https:\/\/api.lemonfox.ai\/v1\/chat\/completions\nEXTERNAL_LLM_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nBLOG_URL_BASE=https:\/\/yourdomain\/blog\nDB_USER=db_user\nDB_PASSWORD=db_password\nDB_HOST=localhost\nDB_NAME=db_name\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the FAISS indexing script<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano rag_faiss.py\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport faiss\nimport numpy as np\nimport json\nimport os\nimport mariadb\nfrom sentence_transformers import SentenceTransformer\nfrom dotenv import load_dotenv\n\n# MIT license Gordon Buchan 2025\n# see https:\/\/opensource.org\/license\/mit\n# some of the code was generated with the assistance of AI tools.\n\n# Load environment variables from .env file\nload_dotenv(dotenv_path=&quot;.\/.env&quot;)\n\nDB_USER = os.getenv(&#039;DB_USER&#039;)\nDB_PASSWORD = os.getenv(&#039;DB_PASSWORD&#039;)\nDB_HOST = os.getenv(&#039;DB_HOST&#039;)\nDB_NAME = os.getenv(&#039;DB_NAME&#039;)\n\n\n# Load embedding model\nmodel = SentenceTransformer(&quot;all-MiniLM-L6-v2&quot;)\n\n# FAISS setup\nembedding_dim = 384\nindex_file = &quot;faiss_index.bin&quot;\nmetadata_file = &quot;faiss_metadata.json&quot;\n\n# Load FAISS index and metadata\nif os.path.exists(index_file):\n    index = faiss.read_index(index_file)\n    with open(metadata_file, &quot;r&quot;) as f:\n        metadata = json.load(f)\n        metadata = {int(k): v for k, v in metadata.items()}  # Ensure integer keys\n    print(f&quot;\ud83d\udcc2 Loaded existing FAISS index with {index.ntotal} embeddings.&quot;)\nelse:\n    index = faiss.IndexHNSWFlat(embedding_dim, 32)\n    metadata = {}\n    print(&quot;\ud83c\udd95 Created a new FAISS index.&quot;)\n\ndef chunk_text(text, chunk_size=500):\n    &quot;&quot;&quot;Split text into smaller chunks&quot;&quot;&quot;\n    words = text.split()\n    return &#x5B;&quot; &quot;.join(words&#x5B;i:i + chunk_size]) for i in range(0, len(words), chunk_size)]\n\ndef get_blog_posts():\n    &quot;&quot;&quot;Fetch published blog posts from WordPress database.&quot;&quot;&quot;\n    try:\n        conn = mariadb.connect(\n            user=DB_USER,\n            password=DB_PASSWORD,\n            host=DB_HOST,\n            database=DB_NAME\n        )\n        cursor = conn.cursor()\n\n        cursor.execute(&quot;&quot;&quot;\n            SELECT ID, post_title, post_content \n            FROM wp_posts \n            WHERE post_status=&#039;publish&#039; AND post_type=&#039;post&#039;\n        &quot;&quot;&quot;)\n\n        posts = cursor.fetchall()\n        conn.close()\n        return posts\n\n    except mariadb.Error as e:\n        print(f&quot;\u274c Database error: {e}&quot;)\n        return &#x5B;]\n\ndef index_blog_posts():\n    &quot;&quot;&quot;Index only new blog posts in FAISS&quot;&quot;&quot;\n    blog_posts = get_blog_posts()\n    if not blog_posts:\n        print(&quot;\u274c No blog posts found. Check database connection.&quot;)\n        return\n\n    vectors = &#x5B;]\n    new_metadata = {}\n    current_index = len(metadata)\n\n    print(f&quot;\ud83d\udcdd Found {len(blog_posts)} blog posts to check for indexing.&quot;)\n\n    for post_id, title, content in blog_posts:\n        if any(str(idx) for idx in metadata if metadata&#x5B;idx]&#x5B;&quot;post_id&quot;] == post_id):\n            print(f&quot;\ud83d\udd04 Skipping already indexed post: {title} (ID: {post_id})&quot;)\n            continue\n\n        chunks = chunk_text(content)\n        for chunk in chunks:\n            embedding = model.encode(chunk, normalize_embeddings=True)  # Normalize embeddings\n            vectors.append(embedding)\n            new_metadata&#x5B;current_index] = {\n                &quot;post_id&quot;: post_id,\n                &quot;title&quot;: title,\n                &quot;chunk_text&quot;: chunk\n            }\n            current_index += 1\n\n    if vectors:\n        faiss_vectors = np.array(vectors, dtype=np.float32)\n        index.add(faiss_vectors)\n\n        metadata.update(new_metadata)\n\n        faiss.write_index(index, index_file)\n        with open(metadata_file, &quot;w&quot;) as f:\n            json.dump(metadata, f, indent=4)\n\n        print(f&quot;\u2705 Indexed {len(new_metadata)} new chunks.&quot;)\n    else:\n        print(&quot;\u2705 No new posts to index.&quot;)\n\nif __name__ == &quot;__main__&quot;:\n    index_blog_posts()\n    print(&quot;\u2705 Indexing completed.&quot;)\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the FAISS retrieval API<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano faiss_search.py\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add text to the file:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport os\nimport faiss\nimport numpy as np\nimport json\nfrom sentence_transformers import SentenceTransformer  # \u2705 Ensure this is imported\n\n# MIT license Gordon Buchan 2025\n# see https:\/\/opensource.org\/license\/mit\n# some of the code was generated with the assistance of AI tools.\n\n# \u2705 Load the same embedding model used in `rag_api_wrapper.py`\nmodel = SentenceTransformer(&quot;all-MiniLM-L6-v2&quot;)\n\n# \u2705 Load FAISS index and metadata\nindex_file = &quot;faiss_index.bin&quot;\nmetadata_file = &quot;faiss_metadata.json&quot;\n\nembedding_dim = 384\n\nif os.path.exists(index_file):\n    index = faiss.read_index(index_file)\n    with open(metadata_file, &quot;r&quot;) as f:\n        metadata = json.load(f)\nelse:\n    index = faiss.IndexFlatL2(embedding_dim)\n    metadata = {}\n\ndef search_faiss(query_text, top_k=10):\n    &quot;&quot;&quot;Search FAISS index and retrieve relevant metadata&quot;&quot;&quot;\n    query_embedding = model.encode(query_text).reshape(1, -1)  # \u2705 Ensure `model` is used correctly\n    _, indices = index.search(query_embedding, top_k)\n\n    results = &#x5B;]\n    for idx in indices&#x5B;0]:\n        if str(idx) in metadata:  # \u2705 Convert index to string to match JSON keys\n            results.append(metadata&#x5B;str(idx)])\n\n    return results\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the RAG API wrapper<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano rag_api_wrapper.py\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfrom fastapi import FastAPI, HTTPException\nimport requests\nimport os\nimport json\nimport faiss\nimport numpy as np\nfrom dotenv import load_dotenv\nfrom sentence_transformers import SentenceTransformer\n\n# MIT license Gordon Buchan 2025\n# see https:\/\/opensource.org\/license\/mit\n# some of the code was generated with the assistance of AI tools.\n\n# Load environment variables\nload_dotenv(dotenv_path=&quot;.\/.env&quot;)\n\nEXTERNAL_LLM_API = os.getenv(&#039;EXTERNAL_LLM_API&#039;)\nEXTERNAL_LLM_API_KEY = os.getenv(&#039;EXTERNAL_LLM_API_KEY&#039;)\nBLOG_URL_BASE = os.getenv(&#039;BLOG_URL_BASE&#039;)\n\n# Load FAISS index and metadata\nembedding_dim = 384\nindex_file = &quot;faiss_index.bin&quot;\nmetadata_file = &quot;faiss_metadata.json&quot;\n\nif os.path.exists(index_file):\n    index = faiss.read_index(index_file)\n    with open(metadata_file, &quot;r&quot;) as f:\n        metadata = json.load(f)\n        metadata = {int(k): v for k, v in metadata.items()}  # Ensure integer keys\n    print(f&quot;\ud83d\udcc2 Loaded FAISS index with {index.ntotal} embeddings.&quot;)\nelse:\n    index = faiss.IndexHNSWFlat(embedding_dim, 32)\n    metadata = {}\n    print(&quot;\u274c No FAISS index found.&quot;)\n\n# Load embedding model\nmodel = SentenceTransformer(&quot;all-MiniLM-L6-v2&quot;)\n\napp = FastAPI()\n\ndef search_faiss(query_text, top_k=3):\n    &quot;&quot;&quot;Retrieve top K relevant chunks from FAISS index&quot;&quot;&quot;\n    if index.ntotal == 0:\n        return &#x5B;]\n\n    query_embedding = model.encode(query_text, normalize_embeddings=True).reshape(1, -1)\n    distances, indices = index.search(query_embedding, top_k)\n\n    results = &#x5B;]\n    for idx in indices&#x5B;0]:\n        if idx in metadata:\n            post_id = metadata&#x5B;idx]&#x5B;&quot;post_id&quot;]\n            title = metadata&#x5B;idx]&#x5B;&quot;title&quot;]\n            chunk_text = metadata&#x5B;idx]&#x5B;&quot;chunk_text&quot;]\n            post_url = f&quot;{BLOG_URL_BASE}\/?p={post_id}&quot;\n            \n            # Limit chunk text to 300 characters for cleaner display\n            short_chunk = chunk_text&#x5B;:300] + &quot;...&quot; if len(chunk_text) &gt; 300 else chunk_text\n            results.append(f&quot;\ud83d\udccc {title}: {short_chunk} (Read more: {post_url})&quot;)\n    \n    return results&#x5B;:3]  # Limit to max 3 sources\n\n@app.post(&quot;\/v1\/chat\/completions&quot;)\ndef chat_completions(request: dict):\n    if &quot;messages&quot; not in request:\n        raise HTTPException(status_code=400, detail=&quot;No messages provided.&quot;)\n\n    user_query = request&#x5B;&quot;messages&quot;]&#x5B;-1]&#x5B;&quot;content&quot;]\n\n    # Retrieve relevant blog context\n    context_snippets = search_faiss(user_query)\n    \n    context_text = &quot;\\n&quot;.join(context_snippets) if context_snippets else &quot;No relevant sources found.&quot;\n    \n    # Send query with context to LLM API\n    payload = {\n        &quot;model&quot;: &quot;llama-8b-chat&quot;,\n        &quot;messages&quot;: &#x5B;\n            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Use the following blog snippets to provide a detailed response.&quot;},\n            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: f&quot;{user_query}\\n\\nContext:\\n{context_text}&quot;}\n        ]\n    }\n    headers = {&quot;Authorization&quot;: f&quot;Bearer {EXTERNAL_LLM_API_KEY}&quot;}\n    response = requests.post(EXTERNAL_LLM_API, json=payload, headers=headers)\n\n    if response.status_code != 200:\n        raise HTTPException(status_code=500, detail=&quot;External LLM API request failed.&quot;)\n\n    llm_response = response.json()\n    response_text = llm_response&#x5B;&quot;choices&quot;]&#x5B;0]&#x5B;&quot;message&quot;]&#x5B;&quot;content&quot;]\n\n    return {\n        &quot;id&quot;: llm_response.get(&quot;id&quot;, &quot;generated_id&quot;),\n        &quot;object&quot;: &quot;chat.completion&quot;,\n        &quot;created&quot;: llm_response.get(&quot;created&quot;, 1700000000),\n        &quot;model&quot;: llm_response.get(&quot;model&quot;, &quot;llama-8b-chat&quot;),\n        &quot;choices&quot;: &#x5B;\n            {\n                &quot;message&quot;: {\n                    &quot;role&quot;: &quot;assistant&quot;,\n                    &quot;content&quot;: f&quot;{response_text}\\n\\n\ud83d\udcda Sources:\\n{context_text}&quot;\n                }\n            }\n        ],\n        &quot;usage&quot;: llm_response.get(&quot;usage&quot;, {&quot;prompt_tokens&quot;: 0, &quot;completion_tokens&quot;: 0, &quot;total_tokens&quot;: 0})\n    }\n\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Running the rag_faiss.py file manually to create the FAISS vector database<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython3 rag_faiss.py\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Starting the RAG API wrapper manually to test the system<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nuvicorn rag_api_wrapper:app --host 127.0.0.1 --port 8000\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Testing the RAG API wrapper with a curl command<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncurl -X POST http:\/\/localhost:8000\/v1\/chat\/completions -H &quot;Content-Type: application\/json&quot; -d &#039;{&quot;messages&quot;: &#x5B;{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;How do I use an external LLM API?&quot;}]}&#039;\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the ragblog-faiss service<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano ragblog-faiss.service\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;Unit]\nDescription=ragblog-faiss\nAfter=network.target\n \n&#x5B;Service]\nUser=root\nWorkingDirectory=\/var\/www\/html\/ragblog_workdir\nExecStart=\/usr\/bin\/bash -c &quot;source \/var\/www\/html\/ragblog_workdir\/ragblog_env\/bin\/activate &amp;amp;&amp;amp; python3 \/var\/www\/html\/ragblog_workdir\/rag_faiss.py&quot;\nRestart=always\nEnvironment=PYTHONUNBUFFERED=1\nStandardOutput=journal\nStandardError=journal\n \n&#x5B;Install]\nWantedBy=multi-user.target\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the ragblog-faiss timer<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano ragblog-faiss.timer\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;Unit]\nDescription=Run monitoring tasks every 5 minutes\n \n&#x5B;Timer]\nOnBootSec=5min\nOnUnitActiveSec=5min\n \n&#x5B;Install]\nWantedBy=timers.target\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Enabling and starting the ragblog-faiss service and the ragblog-faiss timer<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsystemctl daemon-reload\nsystemctl enable ragblog-faiss.service\nsystemctl start ragblog-faiss.service\nsystemctl enable ragblog-faiss.timer\nsystemctl start ragblog-faiss.timer\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the ragblog-api-wrapper service<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano \/etc\/systemd\/system\/ragblog-api-wrapper.service\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;Unit]\nDescription=ragblog-api-wrapper service\nAfter=network.target\n\n&#x5B;Service]\nUser=root\nWorkingDirectory=\/var\/www\/html\/ragblog_workdir\nExecStart=\/usr\/bin\/bash -c &quot;source \/var\/www\/html\/ragblog_workdir\/ragblog_env\/bin\/activate &amp;amp;&amp;amp; uvicorn rag_api_wrapper:app --host 127.0.0.1 --port 8000&quot;\nRestart=always\nEnvironment=PYTHONUNBUFFERED=1\nStandardOutput=journal\nStandardError=journal\n\n&#x5B;Install]\nWantedBy=multi-user.target\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Enabling and starting the ragblog-api wrapper service<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsystemctl daemon-reload\nsystemctl enable ragblog-api-wrapper.service\nsystemctl start ragblog-api.service\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the chatbot.php file<\/h1>\n\n\n\n<p>Enter the following command (adjust values to match your environment):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano \/var\/www\/html\/yourdomain.com\/blog\/chatbot.php\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&amp;lt;?php\nheader(&quot;Content-Type: text\/html&quot;);\n\n\/\/ MIT license Gordon Buchan 2025\n\/\/ see https:\/\/opensource.org\/license\/mit\n\/\/ some of the code was generated with the assistance of AI tools.\n\n\/\/ If this is a POST request, process the chatbot response\nif ($_SERVER&#x5B;&quot;REQUEST_METHOD&quot;] === &quot;POST&quot;) {\n    header(&quot;Content-Type: application\/json&quot;);\n\n    \/\/ Read raw POST input\n    $raw_input = file_get_contents(&quot;php:\/\/input&quot;);\n\n    \/\/ Debugging: Log received input\n    error_log(&quot;Received input: &quot; . $raw_input);\n\n    \/\/ If raw input is empty, fallback to $_POST\n    if (!$raw_input) {\n        $raw_input = json_encode($_POST);\n    }\n\n    \/\/ Decode JSON input\n    $data = json_decode($raw_input, true);\n\n    \/\/ Check if JSON decoding was successful\n    if (json_last_error() !== JSON_ERROR_NONE) {\n        echo json_encode(&#x5B;&quot;error&quot; =&gt; &quot;Invalid JSON format&quot;]);\n        exit;\n    }\n\n    \/\/ Validate the message field\n    if (!isset($data&#x5B;&quot;message&quot;]) || empty(trim($data&#x5B;&quot;message&quot;]))) {\n        echo json_encode(&#x5B;&quot;error&quot; =&gt; &quot;Invalid input: Message is empty&quot;]);\n        exit;\n    }\n\n    $user_message = trim($data&#x5B;&quot;message&quot;]); \/\/ Sanitize input\n\n    \/\/ API request to FastAPI server\n    $api_url = &quot;http:\/\/127.0.0.1:8000\/v1\/chat\/completions&quot;;\n    $payload = json_encode(&#x5B;\n        &quot;messages&quot; =&gt; &#x5B;\n            &#x5B;&quot;role&quot; =&gt; &quot;user&quot;, &quot;content&quot; =&gt; $user_message]\n        ]\n    ]);\n\n    $ch = curl_init();\n    curl_setopt($ch, CURLOPT_URL, $api_url);\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n    curl_setopt($ch, CURLOPT_POST, true);\n    curl_setopt($ch, CURLOPT_HTTPHEADER, &#x5B;&quot;Content-Type: application\/json&quot;]);\n    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);\n    curl_setopt($ch, CURLOPT_TIMEOUT, 10);\n\n    $response = curl_exec($ch);\n    $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n    curl_close($ch);\n\n    \/\/ Handle errors\n    if ($http_status !== 200) {\n        echo json_encode(&#x5B;&quot;error&quot; =&gt; &quot;API error: HTTP $http_status&quot;]);\n        exit;\n    }\n\n    \/\/ Return API response\n    echo $response;\n    exit;\n}\n\n\/\/ If not a POST request, show the chatbot UI\n?&gt;\n&amp;lt;!DOCTYPE html&gt;\n&amp;lt;html lang=&quot;en&quot;&gt;\n&amp;lt;head&gt;\n    &amp;lt;meta charset=&quot;UTF-8&quot;&gt;\n    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;\n    &amp;lt;title&gt;Standalone PHP Chatbot&amp;lt;\/title&gt;\n    &amp;lt;style&gt;\n        #chat-container {\n            width: 100%;\n            max-width: 350px;\n            padding: 10px;\n            border: 1px solid #ccc;\n            background-color: #fff;\n        }\n\n        #chat-input {\n            width: calc(100% - 60px);\n            padding: 5px;\n            margin-right: 5px;\n        }\n\n        button {\n            padding: 6px 10px;\n            cursor: pointer;\n        }\n\n        #chat-output {\n            margin-top: 10px;\n            padding: 5px;\n            background-color: #f9f9f9;\n            max-height: 200px;\n            overflow-y: auto; \/* Enables scrolling *\/\n            border: 1px solid #ddd;\n        }\n    &amp;lt;\/style&gt;\n&amp;lt;\/head&gt;\n&amp;lt;body&gt;\n\n&amp;lt;div id=&quot;chat-container&quot;&gt;\n    &amp;lt;input type=&quot;text&quot; id=&quot;chat-input&quot; placeholder=&quot;Ask me something...&quot;&gt;\n    &amp;lt;button onclick=&quot;sendMessage()&quot;&gt;Send&amp;lt;\/button&gt;\n    &amp;lt;div id=&quot;chat-output&quot;&gt;&amp;lt;\/div&gt;\n&amp;lt;\/div&gt;\n\n&amp;lt;script&gt;\n    async function sendMessage() {\n        let userMessage = document.getElementById(&quot;chat-input&quot;).value.trim();\n        let chatOutput = document.getElementById(&quot;chat-output&quot;);\n\n        if (!userMessage || userMessage.length &gt; 500) {\n            chatOutput.innerHTML = &quot;&amp;lt;i&gt;Invalid input. Please enter 1-500 characters.&amp;lt;\/i&gt;&quot;;\n            return;\n        }\n\n        chatOutput.innerHTML = &quot;&amp;lt;i&gt;Loading...&amp;lt;\/i&gt;&quot;; \/\/ Show loading text\n\n        try {\n            let response = await fetch(&quot;chatbot.php&quot;, {\n                method: &quot;POST&quot;,\n                headers: { &quot;Content-Type&quot;: &quot;application\/json&quot; },\n                body: JSON.stringify({ message: userMessage })\n            });\n\n            let data = await response.json();\n            if (data.error) {\n                throw new Error(data.error);\n            }\n\n            let formattedResponse = data.choices&#x5B;0].message.content.replace(\n                \/(https?:\\\/\\\/&#x5B;^\\s]+)\/g,\n                &#039;&amp;lt;a href=&quot;$1&quot; target=&quot;_blank&quot;&gt;$1&amp;lt;\/a&gt;&#039;\n            );\n\n            chatOutput.innerHTML = `&amp;lt;p&gt;${formattedResponse}&amp;lt;\/p&gt;`;\n        } catch (error) {\n            console.error(&quot;Error:&quot;, error);\n            chatOutput.innerHTML = `&amp;lt;i&gt;Error: ${error.message}&amp;lt;\/i&gt;`;\n        } finally {\n            document.getElementById(&quot;chat-input&quot;).value = &quot;&quot;;\n        }\n    }\n&amp;lt;\/script&gt;\n\n&amp;lt;\/body&gt;\n&amp;lt;\/html&gt;\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Testing the chatbot.php file<\/h1>\n\n\n\n<p>Use a web browser to visit the address of the chatbot (modify values to match your environment):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nhttps:\/\/yourdomain.com\/blog\/chatbot.php\n<\/pre><\/div>\n\n\n<p>Suggested query: &#8220;Tell me about LLMs.&#8221;<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"467\" height=\"379\" src=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-3.png\" alt=\"\" class=\"wp-image-5406\" srcset=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-3.png 467w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-3-300x243.png 300w\" sizes=\"auto, (max-width: 467px) 100vw, 467px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the WordPress plugin directory<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncd \/var\/www\/html\/yourdomain.com\/blog\/wp-content\/plugins\nmkdir rag-chatbot\ncd rag-chatbot\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the rag-chatbot.php file<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano rag-chatbot.php\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&amp;lt;?php\n\/**\n * Plugin Name: RAG Chatbot\n * Description: A WordPress chatbot powered by chatbot.php.\n * Version: 1.3\n * Author: Gordon Buchan\n *\/\n\n\/\/ MIT license Gordon Buchan 2025\n\/\/ see https:\/\/opensource.org\/license\/mit\n\/\/ some of the code was generated with the assistance of AI tools.\n\nfunction rag_chatbot_enqueue_scripts() {\n    \/\/ \u2705 Load scripts from the plugin directory\n    wp_enqueue_script(&#039;rag-chatbot-js&#039;, plugin_dir_url(__FILE__) . &#039;rag-chatbot.js&#039;, array(), &#039;1.3&#039;, true);\n    wp_enqueue_style(&#039;rag-chatbot-css&#039;, plugin_dir_url(__FILE__) . &#039;rag-chatbot.css&#039;, array(), &#039;1.3&#039;);\n}\nadd_action(&#039;wp_enqueue_scripts&#039;, &#039;rag_chatbot_enqueue_scripts&#039;);\n\nfunction rag_chatbot_shortcode() {\n    ob_start(); ?&gt;\n    &amp;lt;div id=&quot;chat-container&quot;&gt;\n        &amp;lt;input type=&quot;text&quot; id=&quot;chat-input&quot; placeholder=&quot;Ask me something...&quot;&gt;\n        &amp;lt;button onclick=&quot;sendMessage()&quot;&gt;Send&amp;lt;\/button&gt;\n        &amp;lt;div id=&quot;chat-output&quot;&gt;&amp;lt;\/div&gt;\n    &amp;lt;\/div&gt;\n    &amp;lt;?php\n    return ob_get_clean();\n}\nadd_shortcode(&#039;rag_chatbot&#039;, &#039;rag_chatbot_shortcode&#039;);\n?&gt;\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the rag-chatbot.js file<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano rag-chatbot.js\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nasync function sendMessage() {\n    let userMessage = document.getElementById(&quot;chat-input&quot;).value.trim();\n    let chatOutput = document.getElementById(&quot;chat-output&quot;);\n\n    console.log(&quot;User input:&quot;, userMessage); \/\/ Debugging log\n\n    if (!userMessage || userMessage.length &gt; 500) {\n        chatOutput.innerHTML = &quot;&amp;lt;i&gt;Invalid input. Please enter 1-500 characters.&amp;lt;\/i&gt;&quot;;\n        return;\n    }\n\n    chatOutput.innerHTML = &quot;&amp;lt;i&gt;Loading...&amp;lt;\/i&gt;&quot;;\n\n    try {\n        let response = await fetch(&quot;\/blog\/chatbot.php&quot;, { \/\/ \u2705 Calls chatbot.php in \/blog\/\n            method: &quot;POST&quot;,\n            headers: { &quot;Content-Type&quot;: &quot;application\/json&quot; },\n            body: JSON.stringify({ message: userMessage })\n        });\n\n        console.log(&quot;Response received:&quot;, response);\n\n        let data = await response.json();\n        if (data.error) {\n            throw new Error(data.error);\n        }\n\n        chatOutput.innerHTML = `&amp;lt;p&gt;${data.choices&#x5B;0].message.content.replace(\n            \/(https?:\\\/\\\/&#x5B;^\\s]+)\/g,\n            &#039;&amp;lt;a href=&quot;$1&quot; target=&quot;_blank&quot;&gt;$1&amp;lt;\/a&gt;&#039;\n        )}&amp;lt;\/p&gt;`;\n    } catch (error) {\n        console.error(&quot;Error:&quot;, error);\n        chatOutput.innerHTML = `&amp;lt;i&gt;Error: ${error.message}&amp;lt;\/i&gt;`;\n    } finally {\n        document.getElementById(&quot;chat-input&quot;).value = &quot;&quot;;\n    }\n}\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the rag-chatbot.css file<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano rag-chatbot.css\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\/* Chatbot container *\/\n#chat-container {\n    width: 100%;\n    max-width: 350px; \/* \u2705 Works in sidebars and posts *\/\n    padding: 10px;\n    border: 1px solid #ccc;\n    background-color: #fff;\n    border-radius: 5px;\n    box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n    font-family: Arial, sans-serif;\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n}\n\n\/* Input field *\/\n#chat-input {\n    width: calc(100% - 70px);\n    padding: 8px;\n    border: 1px solid #aaa;\n    border-radius: 3px;\n    font-size: 14px;\n}\n\n\/* Send button *\/\nbutton {\n    padding: 8px 12px;\n    cursor: pointer;\n    background-color: #0073aa; \/* \u2705 Matches WordPress admin blue *\/\n    color: #fff;\n    border: none;\n    border-radius: 3px;\n    font-size: 14px;\n    font-weight: bold;\n}\n\nbutton:hover {\n    background-color: #005d8c;\n}\n\n\/* Chat output area *\/\n#chat-output {\n    margin-top: 10px;\n    padding: 8px;\n    background-color: #f9f9f9;\n    max-height: 250px; \/* \u2705 Scrollable output *\/\n    overflow-y: auto;\n    border: 1px solid #ddd;\n    border-radius: 3px;\n    font-size: 14px;\n}\n\n\/* Ensures long responses don\u2019t overflow *\/\n#chat-output p {\n    margin: 0;\n    padding: 5px;\n    word-wrap: break-word;\n}\n\n\/* Links inside chatbot responses *\/\n#chat-output a {\n    color: #0073aa;\n    text-decoration: underline;\n}\n\n#chat-output a:hover {\n    color: #005d8c;\n}\n\n\/* Mobile responsiveness *\/\n@media (max-width: 500px) {\n    #chat-container {\n        max-width: 100%;\n    }\n\n    #chat-input {\n        width: 100%;\n    }\n}\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Activating the WordPress plugin<\/h1>\n\n\n\n<p>Enter the WordPress admin console.<\/p>\n\n\n\n<p>Go to the section &#8220;Plugins&#8221;<\/p>\n\n\n\n<p>Click on &#8220;Activate&#8221; for &#8220;RAG Chatbot&#8221;<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Adding the shortcode widget to add the chatbot to the WordPress sidebar<\/h1>\n\n\n\n<p>Go to the section  &#8220;Appearance&#8221; | &#8220;Widgets.&#8221;<\/p>\n\n\n\n<p>Select the sidebar area. Click on the &#8220;+&#8221; symbol. Search for &#8220;shortcode,&#8221; click on the short code icon.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"452\" height=\"284\" src=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-1.png\" alt=\"\" class=\"wp-image-5400\" srcset=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-1.png 452w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-1-300x188.png 300w\" sizes=\"auto, (max-width: 452px) 100vw, 452px\" \/><\/figure>\n\n\n\n<p>In the text box marked \u201cWrite shortcode here\u2026\u201d<br>Enter the shortcode:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;rag_chatbot]\n<\/pre><\/div>\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"847\" height=\"185\" src=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-5.png\" alt=\"\" class=\"wp-image-5409\" srcset=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-5.png 847w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-5-300x66.png 300w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-5-768x168.png 768w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/figure>\n\n\n\n<p>Click on Update.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Checking that the chatbot has been added to the sidebar of the blog<\/h1>\n\n\n\n<p>Go to the main page of the blog. Look to ensure that the short code element has been added to the blog\u2019s sidebar. Test the chatbot (suggested query: &#8220;Tell me about LLMs.&#8221;):<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"423\" height=\"498\" src=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-4.png\" alt=\"\" class=\"wp-image-5407\" srcset=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-4.png 423w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/02\/image-4-255x300.png 255w\" sizes=\"auto, (max-width: 423px) 100vw, 423px\" \/><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>This procedure describes how to create a WordPress chatbot using FAISS for RAG and an external LLM API. We start by scanning the database of WordPress posts, to create a FAISS vector database. We then create an API wrapper that combines hinting information from the local FAISS database with a call to a remote LLM &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/02\/09\/creating-a-wordpress-chatbot-using-facebook-ai-similarity-search-faiss-for-retrieval-augmented-generation-rag-and-an-external-large-language-model-llm-application-programming-interface-api\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Creating a WordPress chatbot using Facebook AI\u00a0Similarity Search (FAISS) for retrieval augmented generation (RAG) and an external large language model (LLM) application programming interface (API)&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3,1],"tags":[],"class_list":["post-5383","post","type-post","status-publish","format-standard","hentry","category-linux","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5383","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=5383"}],"version-history":[{"count":32,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5383\/revisions"}],"predecessor-version":[{"id":5426,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5383\/revisions\/5426"}],"wp:attachment":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=5383"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=5383"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=5383"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}