move into sonardesk

This commit is contained in:
Mark Gregory
2026-10-12 14:50:13 +01:00
parent 5f0d2ceb72
commit 38b4a7406f
879 changed files with 24021 additions and 1 deletions
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
'default_provider' => env('AI_DEFAULT_PROVIDER'),
'timeout' => (int) env('AI_TIMEOUT', 60),
'max_output_tokens' => (int) env('AI_MAX_OUTPUT_TOKENS', 2048),
'temperature' => (float) env('AI_TEMPERATURE', 0.3),
'retain_executions_days' => (int) env('AI_RETAIN_EXECUTIONS_DAYS', 90),
];
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Modules\AI\Console;
use Illuminate\Console\Command;
use Modules\AI\Models\AIExecution;
final class PruneAIExecutions extends Command
{
protected $signature = 'ai:prune {--days=}';
protected $description = 'Remove old AI execution records.';
public function handle(): int
{
$days = (int) ($this->option('days') ?: config('ai.retain_executions_days', 90));
$count = AIExecution::where('created_at', '<', now()->subDays($days))->delete();
$this->info("Deleted {$count} AI executions.");
return self::SUCCESS;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Modules\AI\Console;
use Illuminate\Console\Command;
use Modules\AI\Contracts\AIManager;
final class TestAIProvider extends Command
{
protected $signature = 'ai:test {provider}';
protected $description = 'Test an AI provider connection.';
public function handle(AIManager $manager): int
{
$result = $manager->testProvider((int) $this->argument('provider'));
$this->{$result['ok'] ? 'info' : 'error'}($result['message']);
return $result['ok'] ? self::SUCCESS : self::FAILURE;
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Modules\AI\Contracts;
use Modules\AI\Models\AIExecution;
use Modules\AI\Models\AIPrompt;
interface AIManager
{
public function execute(AIPrompt|string $prompt, array $variables = [], array $options = []): AIExecution;
public function testProvider(int $providerId): array;
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\AI\Contracts;
use Modules\AI\Models\AIProvider;
interface AIProviderDriver
{
public function generate(AIProvider $provider, array $messages, array $options = []): array;
public function test(AIProvider $provider): array;
}
@@ -0,0 +1,90 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('ai_providers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('driver', 40)->default('openai_compatible');
$table->string('base_url')->nullable();
$table->text('api_key')->nullable();
$table->string('default_model')->nullable();
$table->json('models')->nullable();
$table->json('options')->nullable();
$table->boolean('is_active')->default(true)->index();
$table->boolean('is_default')->default(false)->index();
$table->timestamp('last_tested_at')->nullable();
$table->string('last_test_status', 20)->nullable();
$table->text('last_test_message')->nullable();
$table->timestamps();
});
Schema::create('ai_prompts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('purpose')->nullable()->index();
$table->text('system_prompt')->nullable();
$table->longText('user_template');
$table->json('variables')->nullable();
$table->unsignedInteger('version')->default(1);
$table->boolean('is_active')->default(true)->index();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
Schema::create('ai_executions', function (Blueprint $table) {
$table->id();
$table->uuid('execution_id')->unique();
$table->foreignId('ai_provider_id')->nullable()->constrained('ai_providers')->nullOnDelete();
$table->foreignId('ai_prompt_id')->nullable()->constrained('ai_prompts')->nullOnDelete();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('model')->nullable();
$table->string('purpose')->nullable()->index();
$table->longText('input')->nullable();
$table->longText('output')->nullable();
$table->json('context')->nullable();
$table->json('usage')->nullable();
$table->string('status', 20)->default('pending')->index();
$table->text('error')->nullable();
$table->unsignedInteger('duration_ms')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Schema::create('ai_knowledge_sources', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('source_kind', 40)->default('manual')->index();
$table->nullableMorphs('source');
$table->longText('content')->nullable();
$table->string('checksum', 64)->nullable()->index();
$table->json('metadata')->nullable();
$table->string('status', 20)->default('pending')->index();
$table->unsignedInteger('chunk_count')->default(0);
$table->timestamp('indexed_at')->nullable();
$table->timestamps();
});
Schema::create('ai_knowledge_chunks', function (Blueprint $table) {
$table->id();
$table->foreignId('ai_knowledge_source_id')->constrained('ai_knowledge_sources')->cascadeOnDelete();
$table->unsignedInteger('position');
$table->longText('content');
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['ai_knowledge_source_id', 'position']);
});
}
public function down(): void
{
Schema::dropIfExists('ai_knowledge_chunks');
Schema::dropIfExists('ai_knowledge_sources');
Schema::dropIfExists('ai_executions');
Schema::dropIfExists('ai_prompts');
Schema::dropIfExists('ai_providers');
}
};
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Modules\AI\Drivers;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Modules\AI\Contracts\AIProviderDriver;
use Modules\AI\Models\AIProvider;
final class OpenAICompatibleDriver implements AIProviderDriver
{
private function client(AIProvider $provider): PendingRequest
{
$client = Http::acceptJson()->timeout((int) config('ai.timeout', 60));
return filled($provider->api_key) ? $client->withToken($provider->api_key) : $client;
}
public function generate(AIProvider $provider, array $messages, array $options = []): array
{
$base = rtrim((string) ($provider->base_url ?: 'https://api.openai.com/v1'), '/');
$payload = ['model' => $options['model'] ?? $provider->default_model, 'messages' => $messages, 'temperature' => $options['temperature'] ?? config('ai.temperature', 0.3), 'max_tokens' => $options['max_tokens'] ?? config('ai.max_output_tokens', 2048)];
$response = $this->client($provider)->post($base.'/chat/completions', array_filter($payload, fn ($v) => $v !== null));
$response->throw();
$json = $response->json();
return ['text' => (string) data_get($json, 'choices.0.message.content', ''), 'usage' => (array) data_get($json, 'usage', []), 'raw' => $json];
}
public function test(AIProvider $provider): array
{
$result = $this->generate($provider, [['role' => 'user', 'content' => 'Reply with OK only.']], ['max_tokens' => 8, 'temperature' => 0]);
return ['ok' => true, 'message' => trim($result['text']) ?: 'Provider responded successfully.'];
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace Modules\AI\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\AI\Contracts\AIManager;
use Modules\AI\Models\AIPrompt;
final class AIApiController extends Controller
{
public function execute(Request $request, AIPrompt $prompt, AIManager $manager): JsonResponse
{
$data = $request->validate(['variables' => 'nullable|array', 'provider_id' => 'nullable|integer', 'model' => 'nullable|string|max:150']);
$execution = $manager->execute($prompt, $data['variables'] ?? [], array_filter(['provider_id' => $data['provider_id'] ?? null, 'model' => $data['model'] ?? null]));
return response()->json(['data' => ['execution_id' => $execution->execution_id, 'status' => $execution->status, 'output' => $execution->output, 'usage' => $execution->usage, 'error' => $execution->error]], $execution->status === 'completed' ? 200 : 422);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace Modules\AI\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Modules\AI\Contracts\AIManager;
use Modules\AI\Models\AIExecution;
use Modules\AI\Models\AIKnowledgeSource;
use Modules\AI\Models\AIPrompt;
use Modules\AI\Models\AIProvider;
use Modules\AI\Services\KnowledgeIndexer;
final class AIController extends Controller
{
public function index(): View
{
return view('ai::admin.index', ['providers' => AIProvider::latest()->get(), 'prompts' => AIPrompt::latest()->paginate(15, '*', 'prompts'), 'executions' => AIExecution::latest()->limit(15)->get(), 'sources' => AIKnowledgeSource::latest()->limit(15)->get()]);
}
public function storeProvider(Request $request): RedirectResponse
{
$data = $request->validate(['name' => 'required|string|max:100', 'driver' => 'required|in:openai_compatible,openai,lm_studio,ollama', 'base_url' => 'nullable|url', 'api_key' => 'nullable|string', 'default_model' => 'required|string|max:150', 'is_default' => 'nullable|boolean']);
if ($request->boolean('is_default')) {
AIProvider::query()->update(['is_default' => false]);
}
$data['is_default'] = $request->boolean('is_default');
AIProvider::create($data);
return back()->with('status', 'AI provider created.');
}
public function testProvider(AIProvider $provider, AIManager $manager): RedirectResponse
{
$result = $manager->testProvider($provider->id);
return back()->with($result['ok'] ? 'status' : 'error', $result['message']);
}
public function storePrompt(Request $request): RedirectResponse
{
$data = $request->validate(['name' => 'required|string|max:120', 'slug' => 'nullable|alpha_dash|max:140', 'purpose' => 'nullable|string|max:100', 'system_prompt' => 'nullable|string', 'user_template' => 'required|string']);
$data['slug'] = $data['slug'] ?: Str::slug($data['name']);
$data['created_by'] = auth()->id();
AIPrompt::create($data);
return back()->with('status', 'Prompt created.');
}
public function execute(Request $request, AIPrompt $prompt, AIManager $manager): RedirectResponse
{
$data = $request->validate(['input' => 'required|string|max:50000', 'provider_id' => 'nullable|exists:ai_providers,id']);
$execution = $manager->execute($prompt, ['input' => $data['input']], ['provider_id' => $data['provider_id'] ?? null]);
return back()->with($execution->status === 'completed' ? 'status' : 'error', $execution->status === 'completed' ? 'AI execution completed.' : $execution->error);
}
public function storeSource(Request $request, KnowledgeIndexer $indexer): RedirectResponse
{
$data = $request->validate(['name' => 'required|string|max:150', 'content' => 'required|string|max:1000000']);
$source = AIKnowledgeSource::create(['name' => $data['name'], 'source_kind' => 'manual', 'content' => $data['content'], 'status' => 'pending']);
$indexer->index($source);
return back()->with('status','Knowledge source indexed.');
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\AI\Models;
use Illuminate\Database\Eloquent\Model;
final class AIExecution extends Model
{
protected $fillable = ['execution_id', 'ai_provider_id', 'ai_prompt_id', 'user_id', 'model', 'purpose', 'input', 'output', 'context', 'usage', 'status', 'error', 'duration_ms', 'completed_at'];
protected $casts = ['context' => 'array', 'usage' => 'array', 'completed_at' => 'datetime'];
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\AI\Models;
use Illuminate\Database\Eloquent\Model;
final class AIKnowledgeChunk extends Model
{
protected $fillable = ['ai_knowledge_source_id', 'position', 'content', 'metadata'];
protected $casts = ['metadata' => 'array'];
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Modules\AI\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
final class AIKnowledgeSource extends Model
{
protected $fillable = ['name', 'source_kind', 'source_type', 'source_id', 'content', 'checksum', 'metadata', 'status', 'chunk_count', 'indexed_at'];
protected $casts = ['metadata' => 'array', 'indexed_at' => 'datetime'];
public function source(): MorphTo
{
return $this->morphTo();
}
public function chunks(): HasMany
{
return $this->hasMany(AIKnowledgeChunk::class);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\AI\Models;
use Illuminate\Database\Eloquent\Model;
final class AIPrompt extends Model
{
protected $fillable = ['name', 'slug', 'purpose', 'system_prompt', 'user_template', 'variables', 'version', 'is_active', 'created_by'];
protected $casts = ['variables' => 'array', 'is_active' => 'boolean'];
public function render(array $variables): string
{
return preg_replace_callback('/\{\{\s*([\w.]+)\s*\}\}/', fn ($m) => (string) data_get($variables, $m[1], ''), $this->user_template) ?? $this->user_template;
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Modules\AI\Models;
use Illuminate\Database\Eloquent\Model;
final class AIProvider extends Model
{
protected $fillable = ['name', 'driver', 'base_url', 'api_key', 'default_model', 'models', 'options', 'is_active', 'is_default', 'last_tested_at', 'last_test_status', 'last_test_message'];
protected $casts = ['api_key' => 'encrypted', 'models' => 'array', 'options' => 'array', 'is_active' => 'boolean', 'is_default' => 'boolean', 'last_tested_at' => 'datetime'];
protected $hidden = ['api_key'];
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Modules\AI\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\AI\Console\PruneAIExecutions;
use Modules\AI\Console\TestAIProvider;
use Modules\AI\Contracts\AIManager as AIManagerContract;
use Modules\AI\Services\AIManager;
final class AIServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path('AI', 'Config/config.php'), 'ai');
$this->app->singleton(AIManagerContract::class, AIManager::class);
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('AI', 'Database/Migrations'));
$this->loadViewsFrom(module_path('AI', 'Resources/views'), 'ai');
if ($this->app->runningInConsole()) {
$this->commands([TestAIProvider::class, PruneAIExecutions::class]);
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Modules\AI\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
Route::middleware('web')->group(module_path('AI', 'Routes/web.php'));
Route::middleware('api')->prefix('api/v1')->group(module_path('AI', 'Routes/api.php'));
}
}
+21
View File
@@ -0,0 +1,21 @@
<x-layouts.app :title="__('AI Platform')">
<div class="space-y-6">
<div><flux:heading size="xl">AI Platform</flux:heading><flux:text>Configure hosted or local models, reusable prompts, execution history and knowledge sources.</flux:text></div>
@if(session('status'))<flux:callout variant="success">{{ session('status') }}</flux:callout>@endif
@if(session('error'))<flux:callout variant="danger">{{ session('error') }}</flux:callout>@endif
<div class="grid gap-4 md:grid-cols-4">
<flux:card><flux:text>Providers</flux:text><flux:heading size="xl">{{ $providers->count() }}</flux:heading></flux:card>
<flux:card><flux:text>Prompts</flux:text><flux:heading size="xl">{{ $prompts->total() }}</flux:heading></flux:card>
<flux:card><flux:text>Executions</flux:text><flux:heading size="xl">{{ \Modules\AI\Models\AIExecution::count() }}</flux:heading></flux:card>
<flux:card><flux:text>Knowledge sources</flux:text><flux:heading size="xl">{{ \Modules\AI\Models\AIKnowledgeSource::count() }}</flux:heading></flux:card>
</div>
<div class="grid gap-6 xl:grid-cols-2">
<flux:card class="space-y-4"><flux:heading>Provider</flux:heading><form method="POST" action="{{ route('administration.ai.providers.store') }}" class="space-y-3">@csrf
<flux:input name="name" label="Name" required/><flux:select name="driver" label="Driver"><option value="openai_compatible">OpenAI compatible</option><option value="openai">OpenAI</option><option value="lm_studio">LM Studio</option><option value="ollama">Ollama</option></flux:select>
<flux:input name="base_url" label="Base URL" placeholder="http://127.0.0.1:1234/v1"/><flux:input name="api_key" type="password" label="API key"/><flux:input name="default_model" label="Default model" required/><flux:checkbox name="is_default" value="1" label="Default provider"/><flux:button type="submit" variant="primary">Add provider</flux:button></form>
<div class="divide-y">@foreach($providers as $provider)<div class="flex items-center justify-between py-3"><div><strong>{{ $provider->name }}</strong><div class="text-sm opacity-70">{{ $provider->driver }} · {{ $provider->default_model }}</div></div><form method="POST" action="{{ route('administration.ai.providers.test',$provider) }}">@csrf<flux:button size="sm">Test</flux:button></form></div>@endforeach</div></flux:card>
<flux:card class="space-y-4"><flux:heading>Reusable prompt</flux:heading><form method="POST" action="{{ route('administration.ai.prompts.store') }}" class="space-y-3">@csrf
<flux:input name="name" label="Name" required/><flux:input name="slug" label="Slug"/><flux:input name="purpose" label="Purpose" placeholder="summarisation"/><flux:textarea name="system_prompt" label="System prompt"/><flux:textarea name="user_template" label="User template" placeholder="Summarise: [input]" required/><flux:button type="submit" variant="primary">Add prompt</flux:button></form></flux:card>
<flux:card class="space-y-4"><flux:heading>Knowledge source</flux:heading><form method="POST" action="{{ route('administration.ai.knowledge.store') }}" class="space-y-3">@csrf<flux:input name="name" label="Name" required/><flux:textarea name="content" label="Content" rows="10" required/><flux:button type="submit" variant="primary">Index source</flux:button></form><div class="divide-y">@foreach($sources as $source)<div class="py-2"><strong>{{ $source->name }}</strong><div class="text-sm opacity-70">{{ $source->status }} · {{ $source->chunk_count }} chunks</div></div>@endforeach</div></flux:card>
<flux:card class="space-y-4"><flux:heading>Recent executions</flux:heading><div class="divide-y">@forelse($executions as $execution)<div class="py-3"><div class="flex justify-between"><strong>{{ $execution->purpose ?: 'Prompt execution' }}</strong><span>{{ $execution->status }}</span></div><div class="text-sm opacity-70">{{ $execution->model }} · {{ $execution->duration_ms }} ms · {{ $execution->created_at?->diffForHumans() }}</div></div>@empty<flux:text>No executions yet.</flux:text>@endforelse</div></flux:card>
</div></div></x-layouts.app>
+6
View File
@@ -0,0 +1,6 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\AI\Http\Controllers\AIApiController;
Route::post('/ai/prompts/{prompt:slug}/execute', [AIApiController::class, 'execute'])->middleware(['auth.api', 'ability:ai.execute'])->name('api.ai.execute');
+13
View File
@@ -0,0 +1,13 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\AI\Http\Controllers\AIController;
Route::middleware(['auth', 'permission:ai.view'])->prefix('administration/ai')->name('administration.ai.')->group(function () {
Route::get('/', [AIController::class, 'index'])->name('index');
Route::post('/providers', [AIController::class, 'storeProvider'])->middleware('permission:ai.manage')->name('providers.store');
Route::post('/providers/{provider}/test', [AIController::class, 'testProvider'])->middleware('permission:ai.manage')->name('providers.test');
Route::post('/prompts', [AIController::class, 'storePrompt'])->middleware('permission:ai.manage')->name('prompts.store');
Route::post('/prompts/{prompt}/execute', [AIController::class, 'execute'])->middleware('permission:ai.execute')->name('prompts.execute');
Route::post('/knowledge', [AIController::class, 'storeSource'])->middleware('permission:ai.manage')->name('knowledge.store');
});
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Modules\AI\Services;
use Illuminate\Support\Str;
use Modules\AI\Contracts\AIManager as AIManagerContract;
use Modules\AI\Contracts\AIProviderDriver;
use Modules\AI\Drivers\OpenAICompatibleDriver;
use Modules\AI\Models\AIExecution;
use Modules\AI\Models\AIPrompt;
use Modules\AI\Models\AIProvider;
final class AIManager implements AIManagerContract
{
public function execute(AIPrompt|string $prompt, array $variables = [], array $options = []): AIExecution
{
$promptModel = is_string($prompt) ? AIPrompt::where('slug', $prompt)->where('is_active', true)->firstOrFail() : $prompt;
$provider = $this->provider($options['provider_id'] ?? null);
$input = $promptModel->render($variables);
$execution = AIExecution::create(['execution_id' => (string) Str::uuid(), 'ai_provider_id' => $provider->id, 'ai_prompt_id' => $promptModel->id, 'user_id' => auth()->id(), 'model' => $options['model'] ?? $provider->default_model, 'purpose' => $promptModel->purpose, 'input' => $input, 'context' => $variables, 'status' => 'running']);
$started = microtime(true);
try {
$messages = [];
if (filled($promptModel->system_prompt)) {
$messages[] = ['role' => 'system', 'content' => $promptModel->system_prompt];
}
$messages[] = ['role' => 'user', 'content' => $input];
$result = $this->driver($provider)->generate($provider, $messages, $options);
$execution->update(['output' => $result['text'], 'usage' => $result['usage'], 'status' => 'completed', 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'completed_at' => now()]);
} catch (\Throwable $e) {
report($e);
$execution->update(['status' => 'failed', 'error' => $e->getMessage(), 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'completed_at' => now()]);
}
return $execution->refresh();
}
public function testProvider(int $providerId): array
{
$provider = AIProvider::findOrFail($providerId);
try {
$result = $this->driver($provider)->test($provider);
$provider->update(['last_tested_at' => now(), 'last_test_status' => 'passed', 'last_test_message' => $result['message']]);
return $result;
} catch (\Throwable $e) {
$provider->update(['last_tested_at' => now(), 'last_test_status' => 'failed', 'last_test_message' => $e->getMessage()]);
return ['ok' => false, 'message' => $e->getMessage()];
}
}
private function provider(?int $id): AIProvider
{
return AIProvider::query()->when($id, fn ($q) => $q->whereKey($id))->when(! $id, fn ($q) => $q->where('is_default', true))->where('is_active', true)->first()
?? AIProvider::where('is_active', true)->firstOrFail();
}
private function driver(AIProvider $provider): AIProviderDriver
{
return match ($provider->driver) {
'openai_compatible','openai','lm_studio','ollama' => app(OpenAICompatibleDriver::class), default => throw new \InvalidArgumentException("Unsupported AI driver [{$provider->driver}].")
};
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Modules\AI\Services;
use Modules\AI\Models\AIKnowledgeSource;
final class KnowledgeIndexer
{
public function index(AIKnowledgeSource $source, int $chunkSize = 1800): AIKnowledgeSource
{
$content = trim((string) $source->content);
$source->chunks()->delete();
$chunks = $content === '' ? [] : $this->split($content, max(500, $chunkSize));
foreach ($chunks as $position => $chunk) {
$source->chunks()->create(['position' => $position + 1, 'content' => $chunk]);
}
$source->update(['checksum' => hash('sha256', $content), 'status' => 'indexed', 'chunk_count' => count($chunks), 'indexed_at' => now()]);
return $source->refresh();
}
private function split(string $content, int $size): array
{
$paragraphs = preg_split('/\R{2,}/', $content) ?: [$content];
$chunks = [];
$buffer = '';
foreach ($paragraphs as $paragraph) {
if (strlen($buffer) + strlen($paragraph) + 2 > $size && $buffer !== '') {
$chunks[] = trim($buffer);
$buffer = '';
} $buffer .= $paragraph."\n\n";
}
if (trim($buffer) !== '') {
$chunks[] = trim($buffer);
}
return $chunks;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Modules\AI\Tests\Feature;
use Modules\AI\Contracts\AIManager;
use Modules\AI\Providers\AIServiceProvider;
use Tests\TestCase;
final class ModuleBootTest extends TestCase
{
public function test_ai_module_boots(): void
{
$this->assertTrue(class_exists(AIServiceProvider::class));
$this->assertTrue(app()->bound(AIManager::class));
}
}
+1
View File
@@ -0,0 +1 @@
{"name":"AI","alias":"ai","version":"1.0.0","description":"Provider-neutral artificial intelligence services, reusable prompts, execution history and knowledge sources.","keywords":["ai","llm","prompts","knowledge","rag"],"priority":140,"providers":["Modules\\AI\\Providers\\AIServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Media","Documents","Automation"]}