v1.2.1
- Generic AI integration (Admin > Integracje > "Integracja AI"), optional and off by default: an OpenAI-compatible /chat/completions client (Groq, OpenAI, or a self-hosted Ollama instance) configured by base URL, optional API key, model, and an SSL-verification toggle. Foundation for the two AI features below and anything else that wants an LLM call in the future. - BookStack automatic content tagging (AI): "Otaguj nową treść"/"Otaguj wszystko ponownie" buttons plus `php artisan bookstack:tag-content` (--dry-run/--force/--limit=N) tag every book/chapter/page with matching helpdesk subcategory names, idempotent by default. - BookStack search refinement: "Przeszukuj" is now three independent checkboxes (Książki/Strony/Rozdziały) instead of a single dropdown, plus a new "Szukaj po" setting (nazwa/tagi/oba) — tag matching uses the bare subcategory name, matching what auto-tagging writes. - AI-driven ticket triage + summary (Admin > Integracje > "Automatyzacja AI dla zgłoszeń", via new scheduled ai:run-ticket-automation): five toggles auto-assign/correct category+subcategory, rewrite an unclear subject, and set priority from content, once per ticket in the background; every change is logged in the ticket's history. Separately, an AI summary + suggested action for every ticket, shown to operators only, with an admin-editable prompt. - Operators can now reassign a ticket to any team, not just one they belong to. - The auto-refresh countdown badges (ticket view, operator queue) are now clickable — fetch immediately and reset the countdown. - All 7 "cyclical" intervals (3 browser refresh countdowns, the notification bell poll, and the 4 background scheduled commands) are now configurable from Admin > Konfiguracja instead of fixed in code. - Fixed: an operator viewing a ticket that's deleted or moved outside their team scope mid-session is now redirected to the operator queue instead of hitting an error. - Docs: README/ARCHITECTURE/CLAUDE/install/wiki updated for all of the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
66
src/tests/Feature/AiClientTest.php
Normal file
66
src/tests/Feature/AiClientTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use App\Services\AiClient;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
test('enabled is false unless ai_enabled, ai_base_url and ai_model are all set — api key is optional', function () {
|
||||
expect(app(AiClient::class)->enabled())->toBeFalse();
|
||||
|
||||
Settings::set('ai_enabled', '1');
|
||||
Settings::set('ai_base_url', 'https://api.groq.test/openai/v1');
|
||||
Settings::set('ai_model', 'llama-3.3-70b-versatile');
|
||||
|
||||
expect(app(AiClient::class)->enabled())->toBeTrue();
|
||||
});
|
||||
|
||||
test('chat returns the assistant message content on success', function () {
|
||||
Settings::set('ai_enabled', '1');
|
||||
Settings::set('ai_base_url', 'https://api.groq.test/openai/v1');
|
||||
Settings::set('ai_model', 'llama-3.3-70b-versatile');
|
||||
|
||||
Http::fake([
|
||||
'api.groq.test/*' => Http::response(['choices' => [['message' => ['content' => 'hello']]]]),
|
||||
]);
|
||||
|
||||
expect(app(AiClient::class)->chat([['role' => 'user', 'content' => 'hi']]))->toBe('hello');
|
||||
});
|
||||
|
||||
test('chat returns null on a non-successful response instead of throwing', function () {
|
||||
Settings::set('ai_enabled', '1');
|
||||
Settings::set('ai_base_url', 'https://api.groq.test/openai/v1');
|
||||
Settings::set('ai_model', 'llama-3.3-70b-versatile');
|
||||
|
||||
Http::fake(['api.groq.test/*' => Http::response(['error' => 'nope'], 500)]);
|
||||
|
||||
expect(app(AiClient::class)->chat([['role' => 'user', 'content' => 'hi']]))->toBeNull();
|
||||
});
|
||||
|
||||
test('chat sends no Authorization header when no api key is configured (self-hosted Ollama style)', function () {
|
||||
Settings::set('ai_enabled', '1');
|
||||
Settings::set('ai_base_url', 'http://ollama.test/v1');
|
||||
Settings::set('ai_model', 'llama3');
|
||||
|
||||
Http::fake(['ollama.test/*' => Http::response(['choices' => [['message' => ['content' => 'ok']]]])]);
|
||||
|
||||
app(AiClient::class)->chat([['role' => 'user', 'content' => 'hi']]);
|
||||
|
||||
Http::assertSent(fn ($request) => ! $request->hasHeader('Authorization'));
|
||||
});
|
||||
|
||||
test('testConnection reports ok on a successful response', function () {
|
||||
Http::fake(['api.groq.test/*' => Http::response(['choices' => [['message' => ['content' => 'pong']]]])]);
|
||||
|
||||
$ok = app(AiClient::class)->testConnection('https://api.groq.test/openai/v1', 'key', 'llama3', true);
|
||||
|
||||
expect($ok)->toBe(['ok' => true, 'message' => null]);
|
||||
});
|
||||
|
||||
test('testConnection reports the provider error message on failure', function () {
|
||||
Http::fake(['api.groq.test/*' => Http::response(['error' => ['message' => 'bad model']], 400)]);
|
||||
|
||||
$error = app(AiClient::class)->testConnection('https://api.groq.test/openai/v1', 'key', 'bad-model', true);
|
||||
|
||||
expect($error['ok'])->toBeFalse();
|
||||
expect($error['message'])->toBe('bad model');
|
||||
});
|
||||
Reference in New Issue
Block a user