Initial commit
This commit is contained in:
@@ -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