AI Chatbot in Drupal: ChatGPT Integration für Customer Support

Steven Schulz
Steven Schulz

AI Chatbot in Drupal: ChatGPT Integration für Customer Support

Mit dem Drupal AI Module können Sie intelligente Chatbots direkt in Ihre Website integrieren. Dieser Guide zeigt die komplette Implementation von der Basis-Integration bis zum fortgeschrittenen RAG-System (Retrieval Augmented Generation) mit Drupal-Content-Training.

Architektur-Übersicht

┌─────────────────────────────────────────────┐
│           Drupal Frontend (Twig)            │
│              Chatbot Widget                 │
└───────────────────┬─────────────────────────┘

┌───────────────────▼─────────────────────────┐
│         Custom Module (PHP/Service)         │
│    - Message Routing                        │
│    - Session Management                     │
│    - Context Building                       │
└───────────────────┬─────────────────────────┘

        ┌───────────┴──────────┐
        │                      │
┌───────▼────────┐    ┌────────▼───────┐
│   AI Module    │    │  Vector Store  │
│   (OpenAI)     │    │   (Embeddings) │
└────────────────┘    └────────────────┘

Setup: Basis-Chatbot

Module Installation

composer require drupal/ai
composer require drupal/ai_openai
composer require drupal/key

drush en ai ai_openai key -y

Custom Chatbot Module erstellen

drush generate module

# Module name: AI Chatbot
# Machine name: ai_chatbot
# Description: Intelligent chatbot for customer support

ai_chatbot.info.yml:

name: 'AI Chatbot'
type: module
description: 'Intelligent chatbot powered by OpenAI'
core_version_requirement: ^10 || ^11
package: 'AI'
dependencies:
  - drupal:node
  - ai:ai
  - ai:ai_openai
  - key:key

Chatbot Service

<?php
// src/Service/ChatbotService.php
namespace Drupal\ai_chatbot\Service;

use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Database\Connection;

/**
 * Chatbot service for AI conversations.
 */
class ChatbotService {

  protected $aiProvider;
  protected $database;
  protected $currentUser;
  protected $conversationHistory = [];

  public function __construct($ai_provider, Connection $database, AccountProxyInterface $current_user) {
    $this->aiProvider = $ai_provider;
    $this->database = $database;
    $this->currentUser = $current_user;
  }

  /**
   * Process user message and get AI response.
   */
  public function chat($message, $session_id = NULL) {
    // Session ID generieren falls nicht vorhanden
    if (!$session_id) {
      $session_id = $this->generateSessionId();
    }

    // Conversation History laden
    $this->loadConversationHistory($session_id);

    // System Context mit Drupal-Informationen
    $system_context = $this->buildSystemContext();

    // User Message zur History hinzufügen
    $this->conversationHistory[] = [
      'role' => 'user',
      'content' => $message,
      'timestamp' => time(),
    ];

    // Relevanten Drupal Content abrufen (RAG)
    $relevant_content = $this->findRelevantContent($message);

    // AI Request mit Context
    $messages = array_merge(
      [['role' => 'system', 'content' => $system_context]],
      $relevant_content ? [['role' => 'system', 'content' => "Relevanter Content:\n" . $relevant_content]] : [],
      array_map(function($msg) {
        return [
          'role' => $msg['role'],
          'content' => $msg['content']
        ];
      }, $this->conversationHistory)
    );

    try {
      $response = $this->aiProvider->chat([
        'model' => 'gpt-4o-mini',
        'messages' => $messages,
        'temperature' => 0.7,
        'max_tokens' => 500,
      ]);

      $ai_message = $response->getNormalized();

      // AI Response zur History hinzufügen
      $this->conversationHistory[] = [
        'role' => 'assistant',
        'content' => $ai_message,
        'timestamp' => time(),
      ];

      // Conversation speichern
      $this->saveConversationHistory($session_id);

      // Metriken tracken
      $this->trackMetrics($session_id, $message, $ai_message);

      return [
        'response' => $ai_message,
        'session_id' => $session_id,
        'timestamp' => time(),
      ];

    }
    catch (\Exception $e) {
      \Drupal::logger('ai_chatbot')->error('Chatbot error: @error', [
        '@error' => $e->getMessage(),
      ]);

      return [
        'response' => 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
        'session_id' => $session_id,
        'error' => TRUE,
      ];
    }
  }

  /**
   * Build system context with Drupal information.
   */
  protected function buildSystemContext() {
    $site_name = \Drupal::config('system.site')->get('name');
    $site_slogan = \Drupal::config('system.site')->get('slogan');

    return <<<CONTEXT
Du bist ein hilfreicher Assistent für die Website "{$site_name}".
Slogan: {$site_slogan}

Deine Aufgaben:
- Beantworte Fragen zu unseren Produkten und Services
- Hilf bei Navigation und Seitensuche
- Gib hilfreiche Tipps und Empfehlungen
- Sei freundlich, professionell und präzise
- Antworte auf Deutsch
- Verweise bei Bedarf auf relevante Seiten

Wenn du etwas nicht weißt, gib das ehrlich zu und biete an, den User an einen menschlichen Support-Mitarbeiter weiterzuleiten.
CONTEXT;
  }

  /**
   * Find relevant Drupal content for context (RAG).
   */
  protected function findRelevantContent($query) {
    // Einfache Keyword-basierte Suche (kann mit Vector Search erweitert werden)
    $keywords = $this->extractKeywords($query);

    if (empty($keywords)) {
      return NULL;
    }

    // Suche in Nodes
    $nids = \Drupal::entityQuery('node')
      ->condition('status', 1)
      ->condition('title', $keywords, 'IN')
      ->range(0, 3)
      ->execute();

    if (empty($nids)) {
      return NULL;
    }

    $nodes = \Drupal::entityTypeManager()
      ->getStorage('node')
      ->loadMultiple($nids);

    $content_snippets = [];
    foreach ($nodes as $node) {
      $body = $node->get('body')->value;
      $snippet = substr(strip_tags($body), 0, 300);

      $content_snippets[] = sprintf(
        "Titel: %s\nURL: %s\nInhalt: %s...",
        $node->getTitle(),
        $node->toUrl()->setAbsolute()->toString(),
        $snippet
      );
    }

    return implode("\n\n---\n\n", $content_snippets);
  }

  /**
   * Extract keywords from query.
   */
  protected function extractKeywords($text) {
    // Stopwords entfernen
    $stopwords = ['der', 'die', 'das', 'und', 'oder', 'ist', 'wie', 'was', 'wo'];

    $words = str_word_count(strtolower($text), 1, 'äöüß');
    $keywords = array_diff($words, $stopwords);

    return array_slice($keywords, 0, 5);
  }

  /**
   * Load conversation history from database.
   */
  protected function loadConversationHistory($session_id) {
    $result = $this->database->select('ai_chatbot_history', 'h')
      ->fields('h', ['role', 'content', 'timestamp'])
      ->condition('session_id', $session_id)
      ->condition('timestamp', time() - 3600, '>') // Letzte Stunde
      ->orderBy('timestamp', 'ASC')
      ->execute();

    $this->conversationHistory = [];
    foreach ($result as $row) {
      $this->conversationHistory[] = [
        'role' => $row->role,
        'content' => $row->content,
        'timestamp' => $row->timestamp,
      ];
    }
  }

  /**
   * Save conversation history to database.
   */
  protected function saveConversationHistory($session_id) {
    foreach ($this->conversationHistory as $message) {
      // Nur neue Messages (ohne ID) speichern
      $this->database->insert('ai_chatbot_history')
        ->fields([
          'session_id' => $session_id,
          'user_id' => $this->currentUser->id(),
          'role' => $message['role'],
          'content' => $message['content'],
          'timestamp' => $message['timestamp'],
        ])
        ->execute();
    }
  }

  /**
   * Generate unique session ID.
   */
  protected function generateSessionId() {
    return uniqid('chat_', TRUE);
  }

  /**
   * Track chatbot metrics.
   */
  protected function trackMetrics($session_id, $user_message, $ai_response) {
    $this->database->insert('ai_chatbot_metrics')
      ->fields([
        'session_id' => $session_id,
        'user_id' => $this->currentUser->id(),
        'message_length' => strlen($user_message),
        'response_length' => strlen($ai_response),
        'timestamp' => time(),
      ])
      ->execute();
  }
}

Database Schema

<?php
/**
 * Implements hook_schema().
 */
function ai_chatbot_schema() {
  $schema['ai_chatbot_history'] = [
    'description' => 'Stores chatbot conversation history',
    'fields' => [
      'id' => [
        'type' => 'serial',
        'not null' => TRUE,
      ],
      'session_id' => [
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
      ],
      'user_id' => [
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ],
      'role' => [
        'type' => 'varchar',
        'length' => 50,
        'not null' => TRUE,
      ],
      'content' => [
        'type' => 'text',
        'not null' => TRUE,
      ],
      'timestamp' => [
        'type' => 'int',
        'not null' => TRUE,
      ],
    ],
    'primary key' => ['id'],
    'indexes' => [
      'session_id' => ['session_id'],
      'timestamp' => ['timestamp'],
    ],
  ];

  $schema['ai_chatbot_metrics'] = [
    'description' => 'Stores chatbot metrics',
    'fields' => [
      'id' => [
        'type' => 'serial',
        'not null' => TRUE,
      ],
      'session_id' => [
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
      ],
      'user_id' => [
        'type' => 'int',
        'not null' => TRUE,
      ],
      'message_length' => [
        'type' => 'int',
        'not null' => TRUE,
      ],
      'response_length' => [
        'type' => 'int',
        'not null' => TRUE,
      ],
      'timestamp' => [
        'type' => 'int',
        'not null' => TRUE,
      ],
    ],
    'primary key' => ['id'],
  ];

  return $schema;
}

REST API Controller

<?php
// src/Controller/ChatbotApiController.php
namespace Drupal\ai_chatbot\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

/**
 * Chatbot API controller.
 */
class ChatbotApiController extends ControllerBase {

  /**
   * Chat endpoint.
   */
  public function chat(Request $request) {
    $data = json_decode($request->getContent(), TRUE);

    $message = $data['message'] ?? '';
    $session_id = $data['session_id'] ?? NULL;

    if (empty($message)) {
      return new JsonResponse(['error' => 'Message required'], 400);
    }

    /** @var \Drupal\ai_chatbot\Service\ChatbotService $chatbot */
    $chatbot = \Drupal::service('ai_chatbot.chatbot');

    $response = $chatbot->chat($message, $session_id);

    return new JsonResponse($response);
  }

  /**
   * Clear conversation history.
   */
  public function clearHistory(Request $request) {
    $data = json_decode($request->getContent(), TRUE);
    $session_id = $data['session_id'] ?? NULL;

    if (!$session_id) {
      return new JsonResponse(['error' => 'Session ID required'], 400);
    }

    \Drupal::database()->delete('ai_chatbot_history')
      ->condition('session_id', $session_id)
      ->execute();

    return new JsonResponse(['success' => TRUE]);
  }
}

Routing

# ai_chatbot.routing.yml
ai_chatbot.api.chat:
  path: '/api/chatbot/chat'
  defaults:
    _controller: '\Drupal\ai_chatbot\Controller\ChatbotApiController::chat'
    _title: 'Chatbot API'
  requirements:
    _permission: 'access content'
  methods: [POST]

ai_chatbot.api.clear:
  path: '/api/chatbot/clear'
  defaults:
    _controller: '\Drupal\ai_chatbot\Controller\ChatbotApiController::clearHistory'
    _title: 'Clear Chat History'
  requirements:
    _permission: 'access content'
  methods: [POST]

Frontend Widget (JavaScript)

// js/chatbot-widget.js
(function (Drupal, drupalSettings) {
  'use strict';

  Drupal.behaviors.aiChatbot = {
    attach: function (context, settings) {
      const widget = document.getElementById('ai-chatbot-widget');
      if (!widget || widget.dataset.initialized) return;

      widget.dataset.initialized = 'true';

      let sessionId = localStorage.getItem('chatbot_session_id') || null;

      const sendButton = widget.querySelector('.chatbot-send');
      const input = widget.querySelector('.chatbot-input');
      const messages = widget.querySelector('.chatbot-messages');

      sendButton.addEventListener('click', () => sendMessage());
      input.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') sendMessage();
      });

      async function sendMessage() {
        const message = input.value.trim();
        if (!message) return;

        // User Message anzeigen
        appendMessage('user', message);
        input.value = '';

        // Loading indicator
        const loadingId = appendMessage('assistant', '<em>Tippt...</em>');

        try {
          const response = await fetch('/api/chatbot/chat', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              message: message,
              session_id: sessionId,
            }),
          });

          const data = await response.json();

          // Session ID speichern
          if (data.session_id) {
            sessionId = data.session_id;
            localStorage.setItem('chatbot_session_id', sessionId);
          }

          // Loading entfernen und Response anzeigen
          document.getElementById(loadingId).remove();
          appendMessage('assistant', data.response);

        } catch (error) {
          document.getElementById(loadingId).remove();
          appendMessage('assistant', 'Fehler bei der Kommunikation. Bitte versuchen Sie es erneut.');
          console.error('Chatbot error:', error);
        }
      }

      function appendMessage(role, content) {
        const messageId = 'msg-' + Date.now();
        const messageEl = document.createElement('div');
        messageEl.id = messageId;
        messageEl.className = `chatbot-message chatbot-message--${role}`;
        messageEl.innerHTML = `
          <div class="chatbot-message__avatar"></div>
          <div class="chatbot-message__content">${content}</div>
        `;
        messages.appendChild(messageEl);
        messages.scrollTop = messages.scrollHeight;
        return messageId;
      }
    }
  };

})(Drupal, drupalSettings);

Twig Template

{# templates/chatbot-widget.html.twig #}
<div id="ai-chatbot-widget" class="chatbot-widget">
  <div class="chatbot-header">
    <h3>{{ 'Chat Support'|t }}</h3>
    <button class="chatbot-close" aria-label="{{ 'Close'|t }}">×</button>
  </div>

  <div class="chatbot-messages">
    <div class="chatbot-message chatbot-message--assistant">
      <div class="chatbot-message__avatar"></div>
      <div class="chatbot-message__content">
        {{ 'Hallo! Wie kann ich Ihnen helfen?'|t }}
      </div>
    </div>
  </div>

  <div class="chatbot-input-wrapper">
    <input
      type="text"
      class="chatbot-input"
      placeholder="{{ 'Ihre Nachricht...'|t }}"
      aria-label="{{ 'Chat message'|t }}"
    />
    <button class="chatbot-send" aria-label="{{ 'Send'|t }}">
      {{ 'Senden'|t }}
    </button>
  </div>
</div>

CSS Styling

/* css/chatbot-widget.css */
.chatbot-widget {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 380px;
  max-height: 600px;
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  display: flex;
  flex-direction: column;
  z-index: 9999;
}

.chatbot-header {
  padding: 16px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
  border-radius: 12px 12px 0 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chatbot-header h3 {
  margin: 0;
  font-size: 18px;
}

.chatbot-close {
  background: none;
  border: none;
  color: #fff;
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  line-height: 1;
}

.chatbot-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  max-height: 400px;
}

.chatbot-message {
  display: flex;
  gap: 12px;
  margin-bottom: 16px;
}

.chatbot-message--user {
  flex-direction: row-reverse;
}

.chatbot-message__avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: #e0e0e0;
  flex-shrink: 0;
}

.chatbot-message--assistant .chatbot-message__avatar {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.chatbot-message--user .chatbot-message__avatar {
  background: #4caf50;
}

.chatbot-message__content {
  background: #f5f5f5;
  padding: 10px 14px;
  border-radius: 18px;
  max-width: 70%;
}

.chatbot-message--user .chatbot-message__content {
  background: #4caf50;
  color: #fff;
}

.chatbot-input-wrapper {
  padding: 16px;
  border-top: 1px solid #e0e0e0;
  display: flex;
  gap: 8px;
}

.chatbot-input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid #e0e0e0;
  border-radius: 20px;
  font-size: 14px;
}

.chatbot-send {
  padding: 10px 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  font-weight: 600;
}

Advanced: Vector Search (RAG)

Vector Embeddings für Drupal Content

<?php
/**
 * Erstelle Embeddings für bessere Content-Suche.
 */
function create_node_embeddings($node) {
  $ai_provider = \Drupal::service('ai.provider');

  // Content für Embedding vorbereiten
  $text = $node->getTitle() . "\n\n" .
          strip_tags($node->get('body')->value);

  // Embedding erstellen
  $response = $ai_provider->embeddings([
    'model' => 'text-embedding-3-small',
    'input' => substr($text, 0, 8000), // Token limit
  ]);

  $embedding = $response->getEmbedding();

  // Speichern
  \Drupal::database()->merge('ai_chatbot_embeddings')
    ->key(['entity_type' => 'node', 'entity_id' => $node->id()])
    ->fields([
      'embedding' => json_encode($embedding),
      'content_hash' => md5($text),
      'updated' => time(),
    ])
    ->execute();
}

/**
 * Semantische Suche mit Vector Similarity.
 */
function semantic_content_search($query, $limit = 3) {
  $ai_provider = \Drupal::service('ai.provider');

  // Query Embedding
  $response = $ai_provider->embeddings([
    'model' => 'text-embedding-3-small',
    'input' => $query,
  ]);

  $query_embedding = $response->getEmbedding();

  // Cosine Similarity berechnen (PostgreSQL mit pgvector oder custom PHP)
  // Vereinfachtes Beispiel - in Production PostgreSQL pgvector nutzen
  $results = \Drupal::database()->query("
    SELECT entity_id, embedding
    FROM {ai_chatbot_embeddings}
    WHERE entity_type = 'node'
  ")->fetchAll();

  $scored_results = [];
  foreach ($results as $row) {
    $embedding = json_decode($row->embedding, TRUE);
    $similarity = cosine_similarity($query_embedding, $embedding);
    $scored_results[] = [
      'entity_id' => $row->entity_id,
      'similarity' => $similarity,
    ];
  }

  // Sortieren nach Similarity
  usort($scored_results, function($a, $b) {
    return $b['similarity'] <=> $a['similarity'];
  });

  return array_slice($scored_results, 0, $limit);
}

/**
 * Cosine Similarity berechnen.
 */
function cosine_similarity(array $a, array $b) {
  $dot_product = 0;
  $magnitude_a = 0;
  $magnitude_b = 0;

  for ($i = 0; $i < count($a); $i++) {
    $dot_product += $a[$i] * $b[$i];
    $magnitude_a += $a[$i] * $a[$i];
    $magnitude_b += $b[$i] * $b[$i];
  }

  $magnitude_a = sqrt($magnitude_a);
  $magnitude_b = sqrt($magnitude_b);

  if ($magnitude_a == 0 || $magnitude_b == 0) {
    return 0;
  }

  return $dot_product / ($magnitude_a * $magnitude_b);
}

Analytics & Reporting

<?php
/**
 * Chatbot Analytics Dashboard.
 */
function get_chatbot_analytics($start_date, $end_date) {
  $db = \Drupal::database();

  // Total Conversations
  $total_conversations = $db->query("
    SELECT COUNT(DISTINCT session_id)
    FROM {ai_chatbot_history}
    WHERE timestamp BETWEEN :start AND :end
  ", [':start' => $start_date, ':end' => $end_date])->fetchField();

  // Total Messages
  $total_messages = $db->query("
    SELECT COUNT(*)
    FROM {ai_chatbot_history}
    WHERE timestamp BETWEEN :start AND :end
  ", [':start' => $start_date, ':end' => $end_date])->fetchField();

  // Average Messages per Conversation
  $avg_messages = $total_conversations > 0
    ? round($total_messages / $total_conversations, 2)
    : 0;

  // Peak Hours
  $peak_hours = $db->query("
    SELECT HOUR(FROM_UNIXTIME(timestamp)) as hour, COUNT(*) as count
    FROM {ai_chatbot_history}
    WHERE timestamp BETWEEN :start AND :end
    GROUP BY hour
    ORDER BY count DESC
    LIMIT 5
  ", [':start' => $start_date, ':end' => $end_date])->fetchAll();

  return [
    'total_conversations' => $total_conversations,
    'total_messages' => $total_messages,
    'avg_messages_per_conversation' => $avg_messages,
    'peak_hours' => $peak_hours,
  ];
}

Fazit

Ein AI-Chatbot in Drupal ist mehr als nur ein Gimmick – mit der richtigen Implementation wird er zum wertvollen Customer-Support-Tool, das 24/7 verfügbar ist und kontinuierlich dazulernt.

Die Kombination aus Drupal Content (RAG) und OpenAI ChatGPT ermöglicht kontextbezogene, präzise Antworten basierend auf Ihrem eigenen Content.


Interesse an einem intelligenten Chatbot für Ihre Drupal-Website? Kontakt: mail@stevenschulz.net oder 04037420859

Häufig gestellte Fragen (FAQ)

Was ist RAG und warum ist es wichtig für Drupal Chatbots?
RAG (Retrieval Augmented Generation) bedeutet, dass der Chatbot auf Ihren eigenen Drupal-Content zugreifen kann. Statt nur allgemeine Antworten zu geben, nutzt der Bot relevante Artikel, Produkte und Seiten aus Ihrer Website für kontextbezogene, präzise Antworten.
Wie speichere ich Chatbot-Konversationen in Drupal?
Die Konversationshistorie wird in einer Custom Database Table (ai_chatbot_history) gespeichert. Jede Message wird mit Session-ID, User-ID, Rolle (user/assistant) und Timestamp gespeichert. Die letzten 60 Minuten einer Session werden für Kontext-Kontinuität geladen.
Welches OpenAI Modell sollte ich für einen Drupal Chatbot verwenden?
Für Customer Support empfiehlt sich gpt-4o-mini - es bietet ein gutes Preis-Leistungs-Verhältnis ($0.15/1M tokens), ist schnell genug für Echtzeit-Chat und liefert qualitativ hochwertige Antworten. Für komplexere Anwendungen nutzen Sie gpt-4o.
Wie kann ich meinen Chatbot mit Drupal-Inhalten trainieren?
Nutzen Sie Vector Embeddings: Erstellen Sie für jeden Node ein Embedding mit der OpenAI Embeddings API, speichern Sie diese in einer Datenbank und nutzen Sie Cosine Similarity für semantische Suche. Der Chatbot findet so automatisch relevanten Content zu User-Anfragen.
Was kostet ein AI Chatbot für eine Drupal-Website?
Die Kosten hängen vom Nutzungsvolumen ab. Mit gpt-4o-mini zahlen Sie ca. $0.15 pro 1 Million Input-Tokens. Bei durchschnittlich 100 Chat-Sessions täglich mit je 10 Messages kostet das etwa $5-15 pro Monat - deutlich günstiger als ein Live-Support-Team.

Das könnte Sie auch interessieren

← Zurück zum Blog