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 []; } } }