Files
2026-06-22 16:40:24 +02:00

284 lines
9.9 KiB
PHP

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