Initial commit
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class DocumentIngestionService
|
||||
{
|
||||
protected WeaviateService $weaviate;
|
||||
|
||||
public function __construct(WeaviateService $weaviate)
|
||||
{
|
||||
$this->weaviate = $weaviate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest content from a URL for a specific chatbot.
|
||||
*/
|
||||
public function ingestUrl(string $url, string $chatbotId): bool
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(15)->get($url);
|
||||
if (!$response->successful()) {
|
||||
Log::error("Failed to fetch URL for ingestion: {$url}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$html = $response->body();
|
||||
|
||||
// Extract Title
|
||||
preg_match('/<title>(.*?)<\/title>/is', $html, $matches);
|
||||
$title = isset($matches[1]) ? trim($matches[1]) : $url;
|
||||
|
||||
// Extract Clean Text Content
|
||||
$text = $this->cleanHtml($html);
|
||||
|
||||
if (empty($text)) {
|
||||
Log::warning("No text extracted from URL: {$url}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Chunk and Index
|
||||
$chunks = $this->chunkText($text, 800, 150);
|
||||
$formattedChunks = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$formattedChunks[] = [
|
||||
'content' => $chunk,
|
||||
'source' => $url,
|
||||
'title' => $title
|
||||
];
|
||||
}
|
||||
|
||||
return $this->weaviate->indexChunks($formattedChunks, $chatbotId);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error ingesting URL {$url} for chatbot {$chatbotId}: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crawl and ingest an entire website starting from a base URL.
|
||||
*/
|
||||
public function crawlAndIngestSite(string $baseUrl, string $chatbotId, int $maxPages = 50): int
|
||||
{
|
||||
$parsedBase = parse_url($baseUrl);
|
||||
$baseHost = $parsedBase['host'] ?? '';
|
||||
$baseScheme = $parsedBase['scheme'] ?? 'http';
|
||||
if (empty($baseHost)) {
|
||||
Log::error("Invalid base URL for site crawl: {$baseUrl}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
$queue = [$baseUrl];
|
||||
$visited = [];
|
||||
$indexedCount = 0;
|
||||
|
||||
Log::info("Starting site crawl of {$baseUrl} for chatbot {$chatbotId} (max pages: {$maxPages})");
|
||||
|
||||
while (!empty($queue) && $indexedCount < $maxPages) {
|
||||
$url = array_shift($queue);
|
||||
|
||||
// Normalize URL
|
||||
$url = strtok($url, '#');
|
||||
$url = rtrim($url, '/');
|
||||
|
||||
if (isset($visited[$url])) {
|
||||
continue;
|
||||
}
|
||||
$visited[$url] = true;
|
||||
|
||||
try {
|
||||
Log::info("Crawler visiting URL [{$indexedCount}/{$maxPages}]: {$url}");
|
||||
$response = Http::timeout(10)->get($url);
|
||||
if (!$response->successful()) {
|
||||
Log::warning("Crawler failed to fetch URL: {$url}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if response is HTML
|
||||
$contentType = $response->header('Content-Type');
|
||||
if ($contentType && strpos($contentType, 'text/html') === false) {
|
||||
Log::info("Crawler skipping non-HTML response at URL: {$url}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$html = $response->body();
|
||||
|
||||
// Extract Title
|
||||
preg_match('/<title>(.*?)<\/title>/is', $html, $matches);
|
||||
$title = isset($matches[1]) ? trim($matches[1]) : $url;
|
||||
|
||||
// Extract Clean Text Content
|
||||
$text = $this->cleanHtml($html);
|
||||
if (!empty($text)) {
|
||||
$chunks = $this->chunkText($text, 800, 150);
|
||||
$formattedChunks = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$formattedChunks[] = [
|
||||
'content' => $chunk,
|
||||
'source' => $url,
|
||||
'title' => $title
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($formattedChunks)) {
|
||||
$this->weaviate->indexChunks($formattedChunks, $chatbotId);
|
||||
$indexedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Discover links
|
||||
preg_match_all('/<a\s+[^>]*href="([^"]+)"/i', $html, $linkMatches);
|
||||
if (isset($linkMatches[1])) {
|
||||
foreach ($linkMatches[1] as $link) {
|
||||
$resolvedLink = $this->resolveUrl($link, $url);
|
||||
if (!$resolvedLink) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsedLink = parse_url($resolvedLink);
|
||||
$linkHost = $parsedLink['host'] ?? '';
|
||||
|
||||
// Only follow same host links using http/https protocols
|
||||
if ($linkHost === $baseHost && in_array($parsedLink['scheme'] ?? '', ['http', 'https'])) {
|
||||
$resolvedLink = strtok($resolvedLink, '#');
|
||||
$resolvedLink = rtrim($resolvedLink, '/');
|
||||
|
||||
if (!isset($visited[$resolvedLink]) && !in_array($resolvedLink, $queue)) {
|
||||
$queue[] = $resolvedLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Crawler error visiting URL {$url}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Crawler finished. Indexed {$indexedCount} pages.");
|
||||
return $indexedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a relative URL relative to a base URL.
|
||||
*/
|
||||
protected function resolveUrl(string $rel, string $base): ?string
|
||||
{
|
||||
if (preg_match('/^(mailto|tel|javascript|#):/i', $rel) || strpos($rel, '#') === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parse_url($rel, PHP_URL_SCHEME) != '') {
|
||||
return $rel;
|
||||
}
|
||||
|
||||
$baseParts = parse_url($base);
|
||||
$scheme = $baseParts['scheme'] ?? 'http';
|
||||
$host = $baseParts['host'] ?? '';
|
||||
$port = isset($baseParts['port']) ? ':' . $baseParts['port'] : '';
|
||||
$path = $baseParts['path'] ?? '/';
|
||||
|
||||
if (strpos($rel, '//') === 0) {
|
||||
return $scheme . ':' . $rel;
|
||||
}
|
||||
|
||||
if (strpos($rel, '/') === 0) {
|
||||
return $scheme . '://' . $host . $port . $rel;
|
||||
}
|
||||
|
||||
$dir = preg_replace('/\/[^\/]*$/', '', $path);
|
||||
return $scheme . '://' . $host . $port . $dir . '/' . $rel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest content from a PDF file for a specific chatbot using the Python script.
|
||||
*/
|
||||
public function ingestPdf(string $filePath, string $originalName, string $chatbotId, bool $enableVision = false): bool
|
||||
{
|
||||
try {
|
||||
if (!file_exists($filePath)) {
|
||||
Log::error("PDF file not found: {$filePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$scriptPath = base_path('scripts/parse_pdf.py');
|
||||
$mistralKey = config('services.mistral.key') ?? env('MISTRAL_API_KEY', '');
|
||||
$visionFlag = $enableVision ? '1' : '0';
|
||||
|
||||
// Run Python script
|
||||
$result = Process::run([
|
||||
'python',
|
||||
$scriptPath,
|
||||
$filePath,
|
||||
$mistralKey,
|
||||
$visionFlag
|
||||
]);
|
||||
|
||||
if (!$result->successful()) {
|
||||
Log::error("Python PDF parser script failed: " . $result->errorOutput());
|
||||
return false;
|
||||
}
|
||||
|
||||
$chunks = json_decode($result->output(), true);
|
||||
|
||||
if (isset($chunks['error'])) {
|
||||
Log::error("Python PDF parser error: " . $chunks['error']);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($chunks)) {
|
||||
Log::warning("No text chunks extracted from PDF: {$originalName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->weaviate->indexChunks($chunks, $chatbotId);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error ingesting PDF {$originalName} for chatbot {$chatbotId}: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean and strip HTML down to readability.
|
||||
*/
|
||||
protected function cleanHtml(string $html): string
|
||||
{
|
||||
// Remove scripts, styles, header, footer, nav
|
||||
$html = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $html);
|
||||
$html = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $html);
|
||||
$html = preg_replace('/<header\b[^>]*>(.*?)<\/header>/is', '', $html);
|
||||
$html = preg_replace('/<footer\b[^>]*>(.*?)<\/footer>/is', '', $html);
|
||||
$html = preg_replace('/<nav\b[^>]*>(.*?)<\/nav>/is', '', $html);
|
||||
|
||||
// Strip tags
|
||||
$text = strip_tags($html);
|
||||
|
||||
// Remove duplicate whitespace and newlines
|
||||
$text = preg_replace('/[ \t]+/', ' ', $text);
|
||||
$text = preg_replace('/\s*\n\s*/', "\n", $text);
|
||||
$text = preg_replace('/\n+/', "\n\n", $text);
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk text using sliding window.
|
||||
*/
|
||||
protected function chunkText(string $text, int $chunkSize = 800, int $overlap = 150): array
|
||||
{
|
||||
$words = explode(' ', $text);
|
||||
$chunks = [];
|
||||
$currentChunk = [];
|
||||
$currentLength = 0;
|
||||
|
||||
foreach ($words as $word) {
|
||||
$currentChunk[] = $word;
|
||||
$currentLength += strlen($word) + 1; // +1 for space
|
||||
|
||||
if ($currentLength >= $chunkSize) {
|
||||
$chunks[] = implode(' ', $currentChunk);
|
||||
|
||||
// Slide the window back by keeping some words for overlap
|
||||
$overlapWordsCount = (int) ($overlap / 6); // estimate 6 chars per word
|
||||
if ($overlapWordsCount > 0 && count($currentChunk) > $overlapWordsCount) {
|
||||
$currentChunk = array_slice($currentChunk, -$overlapWordsCount);
|
||||
$currentLength = strlen(implode(' ', $currentChunk));
|
||||
} else {
|
||||
$currentChunk = [];
|
||||
$currentLength = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (!empty($currentChunk)) {
|
||||
$chunks[] = implode(' ', $currentChunk);
|
||||
}
|
||||
|
||||
return array_filter($chunks, function ($chunk) {
|
||||
return strlen(trim($chunk)) > 50; // Filter out tiny chunks
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MistralService
|
||||
{
|
||||
protected string $apiKey;
|
||||
protected string $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.mistral.key') ?? env('MISTRAL_API_KEY', '');
|
||||
$this->model = config('services.mistral.model') ?? env('MISTRAL_MODEL', 'mistral-small-latest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response using RAG context and chat history.
|
||||
*/
|
||||
public function generateResponse(string $question, array $contextChunks, array $chatHistory = [], ?string $systemPrompt = null): ?string
|
||||
{
|
||||
if (empty($this->apiKey)) {
|
||||
Log::error("Mistral API key is not configured.");
|
||||
return "Désolé, la clé API Mistral AI n'est pas configurée dans l'application.";
|
||||
}
|
||||
|
||||
// Format context from chunks
|
||||
$contextText = "";
|
||||
if (!empty($contextChunks)) {
|
||||
$contextText = "Voici le contexte extrait des documents et sites internet indexés :\n\n";
|
||||
foreach ($contextChunks as $index => $chunk) {
|
||||
$num = $index + 1;
|
||||
$title = $chunk['title'] ?? 'Document';
|
||||
$source = $chunk['source'] ?? 'N/A';
|
||||
$contextText .= "[Document #{$num} : {$title} (Source: {$source})]\n{$chunk['content']}\n\n";
|
||||
}
|
||||
} else {
|
||||
$contextText = "Aucun contexte spécifique n'a été trouvé.";
|
||||
}
|
||||
|
||||
// Prepare system message
|
||||
if (empty($systemPrompt)) {
|
||||
$systemPrompt = "Tu es un assistant virtuel intelligent et chaleureux intégré au site internet. Ton but est d'aider les utilisateurs en répondant de manière claire, concise et professionnelle.\n\n";
|
||||
$systemPrompt .= "INSTRUCTIONS DE SÉCURITÉ CRITIQUES :\n";
|
||||
$systemPrompt .= "1. Réponds UNIQUEMENT et STRICTEMENT en t'appuyant sur le CONTEXTE fourni ci-dessous.\n";
|
||||
$systemPrompt .= "2. Si le contexte ne contient pas l'information nécessaire pour répondre à la question, réponds poliment que tu ne disposes pas de cette information car elle ne figure pas dans les documents indexés. N'utilise JAMAIS tes connaissances générales pour inventer, deviner ou extrapoler une réponse en dehors du contexte.\n";
|
||||
$systemPrompt .= "3. Ne fais aucune supposition et n'invente aucune information. Si le contexte est vide ou insuffisant, refuse de répondre en expliquant que l'information n'est pas répertoriée dans les sources indexées.\n";
|
||||
$systemPrompt .= "4. Réponds toujours en français, de manière structurée.\n";
|
||||
$systemPrompt .= "5. Cite obligatoirement la source des documents fournis qui t'ont permis de répondre (ex: 'D'après le document [Titre]...').\n\n";
|
||||
}
|
||||
|
||||
$systemPrompt .= "\n\n--- CONTEXTE DE RECHERCHE ---\n";
|
||||
$systemPrompt .= $contextText;
|
||||
|
||||
// Build messages array
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => $systemPrompt]
|
||||
];
|
||||
|
||||
// Append chat history (limit to last 6 messages to keep context window clean)
|
||||
$recentHistory = array_slice($chatHistory, -6);
|
||||
foreach ($recentHistory as $msg) {
|
||||
$messages[] = [
|
||||
'role' => $msg['role'] === 'user' ? 'user' : 'assistant',
|
||||
'content' => $msg['content']
|
||||
];
|
||||
}
|
||||
|
||||
// Add the current user question
|
||||
$messages[] = ['role' => 'user', 'content' => $question];
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => "Bearer {$this->apiKey}",
|
||||
'Content-Type' => 'application/json'
|
||||
])->timeout(30)->post('https://api.mistral.ai/v1/chat/completions', [
|
||||
'model' => $this->model,
|
||||
'messages' => $messages,
|
||||
'temperature' => 0.3, // Low temperature for more factual answers
|
||||
'max_tokens' => 1000
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$result = $response->json();
|
||||
return $result['choices'][0]['message']['content'] ?? null;
|
||||
}
|
||||
|
||||
Log::error("Mistral API Chat Completion failed: " . $response->body());
|
||||
return "Une erreur s'est produite lors de la génération de la réponse par Mistral AI (Code status: " . $response->status() . ").";
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Mistral Service Error: " . $e->getMessage());
|
||||
return "Impossible de contacter le service de génération de réponses Mistral. Veuillez réessayer.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WeaviateService
|
||||
{
|
||||
protected string $host;
|
||||
protected ?string $mistralKey;
|
||||
protected ?string $weaviateKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$host = config('services.weaviate.host', env('WEAVIATE_HOST', 'http://localhost:8080'));
|
||||
$host = trim($host, '"\' ');
|
||||
|
||||
if ($host && !str_starts_with($host, 'http://') && !str_starts_with($host, 'https://')) {
|
||||
$host = 'https://' . $host;
|
||||
}
|
||||
|
||||
$this->host = rtrim($host, '/');
|
||||
$this->mistralKey = config('services.mistral.key', env('MISTRAL_API_KEY', ''));
|
||||
$this->weaviateKey = config('services.weaviate.api_key', env('WEAVIATE_API_KEY', ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers for Weaviate HTTP requests.
|
||||
*/
|
||||
protected function getHeaders(): array
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
|
||||
if ($this->mistralKey) {
|
||||
$headers['X-Mistral-Api-Key'] = $this->mistralKey;
|
||||
}
|
||||
|
||||
if ($this->weaviateKey) {
|
||||
$headers['Authorization'] = 'Bearer ' . $this->weaviateKey;
|
||||
$headers['X-Weaviate-Api-Key'] = $this->weaviateKey;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check connection and initialize the schema class if it doesn't exist.
|
||||
*/
|
||||
public function initializeSchema(): bool
|
||||
{
|
||||
try {
|
||||
// Check if class already exists
|
||||
$response = Http::withHeaders($this->getHeaders())
|
||||
->get("{$this->host}/v1/schema/DocumentChunk");
|
||||
|
||||
if ($response->successful()) {
|
||||
// If it exists, let's verify if the property chatbotId exists
|
||||
$data = $response->json();
|
||||
$hasChatbotId = false;
|
||||
if (isset($data['properties'])) {
|
||||
foreach ($data['properties'] as $prop) {
|
||||
if ($prop['name'] === 'chatbotId') {
|
||||
$hasChatbotId = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If schema exists but doesn't have chatbotId property, we add it
|
||||
if (!$hasChatbotId) {
|
||||
$newProp = [
|
||||
'name' => 'chatbotId',
|
||||
'dataType' => ['text'],
|
||||
'description' => 'The unique ID of the chatbot/client',
|
||||
'moduleConfig' => [
|
||||
'text2vec-mistral' => [
|
||||
'skip' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
Http::withHeaders($this->getHeaders())
|
||||
->post("{$this->host}/v1/schema/DocumentChunk/properties", $newProp);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($response->status() === 404) {
|
||||
// Class doesn't exist, create it
|
||||
$schema = [
|
||||
'class' => 'DocumentChunk',
|
||||
'description' => 'A chunk of text from a web page or PDF document',
|
||||
'vectorizer' => 'text2vec-mistral',
|
||||
'moduleConfig' => [
|
||||
'text2vec-mistral' => [
|
||||
'model' => 'mistral-embed',
|
||||
'vectorizeClassName' => false
|
||||
]
|
||||
],
|
||||
'properties' => [
|
||||
[
|
||||
'name' => 'chatbotId',
|
||||
'dataType' => ['text'],
|
||||
'description' => 'The unique ID of the chatbot/client',
|
||||
'moduleConfig' => [
|
||||
'text2vec-mistral' => [
|
||||
'skip' => true
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'content',
|
||||
'dataType' => ['text'],
|
||||
'description' => 'The actual text content of the chunk',
|
||||
'moduleConfig' => [
|
||||
'text2vec-mistral' => [
|
||||
'skip' => false,
|
||||
'vectorizePropertyName' => false
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'source',
|
||||
'dataType' => ['text'],
|
||||
'description' => 'The URL or file path of the source document',
|
||||
'moduleConfig' => [
|
||||
'text2vec-mistral' => [
|
||||
'skip' => true
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'title',
|
||||
'dataType' => ['text'],
|
||||
'description' => 'The title of the source document',
|
||||
'moduleConfig' => [
|
||||
'text2vec-mistral' => [
|
||||
'skip' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$createResponse = Http::withHeaders($this->getHeaders())
|
||||
->post("{$this->host}/v1/schema", $schema);
|
||||
|
||||
if ($createResponse->successful()) {
|
||||
Log::info("Weaviate schema DocumentChunk initialized successfully.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::error("Failed to create Weaviate schema: " . $createResponse->body());
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Weaviate schema initialization error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Index a single chunk or multiple chunks of documents for a specific chatbot.
|
||||
*/
|
||||
public function indexChunks(array $chunks, string $chatbotId): bool
|
||||
{
|
||||
$this->initializeSchema();
|
||||
|
||||
try {
|
||||
$objects = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$objects[] = [
|
||||
'class' => 'DocumentChunk',
|
||||
'properties' => [
|
||||
'chatbotId' => $chatbotId,
|
||||
'content' => $chunk['content'],
|
||||
'source' => $chunk['source'],
|
||||
'title' => $chunk['title'] ?? 'Document',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$response = Http::withHeaders($this->getHeaders())
|
||||
->post("{$this->host}/v1/batch/objects", [
|
||||
'objects' => $objects
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::error("Failed to batch index chunks to Weaviate: " . $response->body());
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Weaviate batch index error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all indexed documents for a specific chatbot.
|
||||
*/
|
||||
public function clearChatbotData(string $chatbotId): bool
|
||||
{
|
||||
try {
|
||||
$payload = [
|
||||
'match' => [
|
||||
'class' => 'DocumentChunk',
|
||||
'where' => [
|
||||
'path' => ['chatbotId'],
|
||||
'operator' => 'Equal',
|
||||
'valueText' => $chatbotId
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$response = Http::withHeaders($this->getHeaders())
|
||||
->delete("{$this->host}/v1/batch/objects", $payload);
|
||||
|
||||
return $response->successful();
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Weaviate batch delete error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Weaviate for similar text chunks, scoped to a specific chatbot.
|
||||
*/
|
||||
public function search(string $query, string $chatbotId, int $limit = 5): array
|
||||
{
|
||||
$this->initializeSchema();
|
||||
try {
|
||||
$graphqlQuery = [
|
||||
'query' => '
|
||||
{
|
||||
Get {
|
||||
DocumentChunk(
|
||||
nearText: {
|
||||
concepts: ["' . addslashes($query) . '"]
|
||||
}
|
||||
where: {
|
||||
path: ["chatbotId"]
|
||||
operator: Equal
|
||||
valueText: "' . addslashes($chatbotId) . '"
|
||||
}
|
||||
limit: ' . $limit . '
|
||||
) {
|
||||
content
|
||||
source
|
||||
title
|
||||
_additional {
|
||||
distance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
];
|
||||
|
||||
$response = Http::withHeaders($this->getHeaders())
|
||||
->post("{$this->host}/v1/graphql", $graphqlQuery);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
$results = $data['data']['Get']['DocumentChunk'] ?? [];
|
||||
Log::info("Weaviate search for chatbot {$chatbotId} returned " . count($results) . " chunks.");
|
||||
return $results;
|
||||
}
|
||||
|
||||
Log::error("Weaviate GraphQL search failed: " . $response->body());
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Weaviate search error: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user