Native PHP: Automatski razvrstajte zahtjeve za podršku uz Smart Routing AI
Kontaktni obrazac izgleda jednostavno sve dok svaka poruka ne završi u istom ulaznom sandučiću. Upiti o prodaji čekaju iza problema s lozinkama, pitanja o naplati prebacuju se između ljudi, a poruke koje su zaista rizične dobiju pažnju tek kada ih netko slučajno primijeti.
Korisna primjena AI-ja ovdje nije složeni chatbot. To je usko usmjerena usluga za donošenje odluka: pregledati poruku, dodijeliti jedan kontrolirani red, zabilježiti obrazloženje i sigurno prijeći na pričuvno rješenje kad se modelu ili mreži ne može vjerovati. Ovaj vodič gradi tu uslugu u izvornom PHP-u 8.3 koristeći cURL, SQLite putem PDO-a i model Smart Routing AI Model.
Pribavite pristup prije pisanja integracijskog koda
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Smart Routing AI Model. Odaberite dostupni plan Free, Plus ili Pro te dovršite njegovu aktivaciju.
- Posjetite službenu dokumentaciju usluge. Pronađite ploču Service token i kopirajte ondje prikazani token ograničen na uslugu.
- Pohranite taj token u konfiguraciju okruženja projekta. Ova usluga zahtijeva token; nije anonimna krajnja točka.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Ponovno generiranje tretirajte kao rotaciju vjerodajnica: ažurirajte svaku implementiranu instancu, ponovno pokrenite relevantne PHP radnike, provjerite novi token i uklonite svaku zastarjelu tajnu iz svojeg sustava za implementaciju.
Potvrdite krajnju točku minimalnim zahtjevom
Točan API poziv je POST https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions, autentificiran s Authorization: Bearer {serviceToken}. Prihvaća chat zahtjev kompatibilan s OpenAI-jem i vraća standardni odgovor u stilu OpenAI-ja.
Identifikator modela dostupan računu može ovisiti o aktiviranom planu. Kopirajte podržani identifikator iz službene dokumentacije ili konfiguracije plana i zamijenite njime YOUR_PLAN_MODEL; nagađanje imena modela pretvorilo bi provjeru implementacije u pogrešku konfiguracije.
curl --request POST \
--url https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions \
--header 'Authorization: Bearer YOUR_SERVICE_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"model": "YOUR_PLAN_MODEL",
"messages": [
{
"role": "user",
"content": "Classify this contact request: I need a copy of last month'\''s invoice."
}
]
}'
Uspješan odgovor trebao bi sadržavati sadržaj asistenta na choices[0].message.content. Aplikacija će provjeriti tu putanju umjesto da pretpostavi kako svaki uspješan HTTP odgovor sadrži upotrebljiv izlaz.
Nakon ovog probnog testa izradite nejavnu datoteku .env:
SMART_ROUTING_TOKEN=YOUR_SERVICE_TOKEN
SMART_ROUTING_MODEL=YOUR_PLAN_MODEL
DB_DSN=sqlite:/var/lib/contact-router/tickets.sqlite
Držite .env izvan korijena web-dokumenta, isključite je iz kontrole verzija i ograničite je na račun koji pokreće PHP. U spremniku ili na upravljanom hostu iste nazive ubrizgajte putem mehanizma za tajne platforme umjesto da datoteku ugradite u sliku.
Odaberite malu arhitekturu otpornu na kvarove
Preglednik šalje zahtjev na public/contact.php. Obradnik provjerava obrazac, traži odluku o usmjeravanju od namjenskog API klijenta i zapisuje poruku i odluku u jednu SQLite transakciju. Dostupna odredišta su sales, support, billing, abuse i manual_review.
Klasifikacija je ovdje sinkrona jer tako obična implementacija za mali tim ostaje razumljiva. Poziv ima stroga vremenska ograničenja, a kvar uzvodne usluge ne gubi prijavu: usmjerava se na manual_review. Zauzetija instalacija kasnije može smjestiti istog klijenta iza radnika, no to dodaje pitanja isporuke, deduplikacije i operativnog rada koja ovom projektu inače nisu potrebna.
Upotrijebite ovu strukturu projekta:
contact-router/
├── .env
├── bootstrap.php
├── composer.json
├── database/
│ └── schema.sql
├── public/
│ └── contact.php
├── src/
│ └── SmartRouting.php
└── tests/
└── SmartRoutingClientTest.php
PHP treba proširenja cURL i PDO SQLite. PHPUnit je jedina razvojna ovisnost:
{
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"classmap": ["src/"]
}
}
composer install
composer dump-autoload
mkdir -p /var/lib/contact-router
sqlite3 /var/lib/contact-router/tickets.sqlite < database/schema.sql
vendor/bin/phpunit tests
Izgradite obrambenu API granicu
Transport u nastavku izvodi jednu ograničenu HTTP operaciju. Pravila ponovnog pokušaja pripadaju klijentu više razine kako bi ih testovi mogli provjeravati bez stvarnih zahtjeva.
<?php
// src/SmartRouting.php
final readonly class HttpResponse
{
public function __construct(
public int $status,
public string $body,
public array $headers = [],
) {}
}
interface HttpTransport
{
public function postJson(string $url, array $headers, array $body): HttpResponse;
}
class TransportException extends RuntimeException {}
class RoutingException extends RuntimeException {}
class RoutingConfigurationException extends RoutingException {}
final class CurlTransport implements HttpTransport
{
public function postJson(string $url, array $headers, array $body): HttpResponse
{
$responseHeaders = [];
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 2000,
CURLOPT_TIMEOUT_MS => 8000,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
CURLOPT_HEADERFUNCTION => static function ($curl, string $line)
use (&$responseHeaders): int {
$length = strlen($line);
if (str_contains($line, ':')) {
[$name, $value] = explode(':', $line, 2);
$responseHeaders[strtolower(trim($name))] = trim($value);
}
return $length;
},
]);
$raw = curl_exec($handle);
if ($raw === false) {
$message = curl_error($handle);
curl_close($handle);
throw new TransportException($message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $raw, $responseHeaders);
}
}
final readonly class RoutingDecision
{
public function __construct(
public string $queue,
public float $confidence,
public string $summary,
public string $source = 'model',
) {}
}
Model prima zatvoren vokabular i zahtjev samo za JSON. Taj upit poboljšava dosljednost, ali nije sigurnosna granica. Vraćeni sadržaj i dalje je nepouzdan ulaz.
<?php
final class SmartRoutingClient
{
private const URL =
'https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions';
public function __construct(
private string $token,
private string $model,
private HttpTransport $transport,
private Closure $logger,
private Closure $sleeper,
) {}
public function classify(string $message): RoutingDecision
{
$payload = [
'model' => $this->model,
'messages' => [
[
'role' => 'system',
'content' => 'Route the contact message to exactly one queue: '
. 'sales, support, billing, or abuse. Return only JSON with '
. 'queue, confidence from 0 to 1, and a short summary.',
],
['role' => 'user', 'content' => $message],
],
];
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->postJson(
self::URL,
[
'Authorization: Bearer ' . $this->token,
'Content-Type: application/json',
],
$payload,
);
} catch (TransportException $exception) {
($this->logger)([
'outcome' => 'transport_error',
'attempt' => $attempt,
]);
if ($attempt === 3) {
throw new RoutingException('Routing service unavailable');
}
($this->sleeper)(2 ** ($attempt - 1));
continue;
}
if (in_array($response->status, [408, 429], true)
|| $response->status >= 500) {
($this->logger)([
'outcome' => 'retryable_http_error',
'status' => $response->status,
'attempt' => $attempt,
]);
if ($attempt === 3) {
throw new RoutingException('Routing service unavailable');
}
$retryAfter = ctype_digit($response->headers['retry-after'] ?? '')
? (int) $response->headers['retry-after']
: 2 ** ($attempt - 1);
($this->sleeper)(min(5, max(1, $retryAfter)));
continue;
}
if (in_array($response->status, [401, 403], true)) {
throw new RoutingConfigurationException(
'Service token rejected'
);
}
if ($response->status < 200 || $response->status >= 300) {
throw new RoutingException(
'Non-retryable routing response: ' . $response->status
);
}
return $this->mapResponse($response->body);
}
throw new RoutingException('Routing attempts exhausted');
}
private function mapResponse(string $body): RoutingDecision
{
try {
$envelope = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
$content = $envelope['choices'][0]['message']['content'] ?? null;
if (!is_string($content)) {
throw new UnexpectedValueException('Missing assistant content');
}
$result = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException|UnexpectedValueException $exception) {
throw new RoutingException('Malformed routing response');
}
$queue = $result['queue'] ?? null;
$confidence = $result['confidence'] ?? null;
$summary = $result['summary'] ?? null;
$allowed = ['sales', 'support', 'billing', 'abuse'];
if (!in_array($queue, $allowed, true)
|| !is_int($confidence) && !is_float($confidence)
|| $confidence < 0 || $confidence > 1
|| !is_string($summary) || trim($summary) === '') {
throw new RoutingException('Invalid routing decision');
}
if ($confidence < 0.65) {
return new RoutingDecision(
'manual_review',
(float) $confidence,
substr($summary, 0, 240),
'low_confidence',
);
}
return new RoutingDecision(
$queue,
(float) $confidence,
substr($summary, 0, 240),
);
}
}
Samo prolazne pogreške transporta, HTTP 408, HTTP 429 i pogreške poslužitelja dobivaju ograničena ponavljanja. Autentifikacija i uobičajene pogreške klijenta ne ponavljaju se: ponovno slanje istog neispravnog zahtjeva troši kvotu i odgađa posjetitelja. Numerička vrijednost Retry-After poštuje se, ali je ograničena kako jedan web zahtjev ne bi ostao otvoren neograničeno dugo.
Trajno pohranite prijavu i njezinu odluku zajedno
CREATE TABLE tickets (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
message TEXT NOT NULL,
queue TEXT NOT NULL,
confidence REAL NOT NULL,
route_summary TEXT NOT NULL,
route_source TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX tickets_queue_created
ON tickets (queue, created_at);
<?php
final class TicketRepository
{
public function __construct(private PDO $pdo) {}
public function save(
string $id,
string $email,
string $message,
RoutingDecision $decision,
): void {
$statement = $this->pdo->prepare(
'INSERT INTO tickets
(id, email, message, queue, confidence, route_summary,
route_source, created_at)
VALUES
(:id, :email, :message, :queue, :confidence, :summary,
:source, :created_at)'
);
$statement->execute([
'id' => $id,
'email' => $email,
'message' => $message,
'queue' => $decision->queue,
'confidence' => $decision->confidence,
'summary' => $decision->summary,
'source' => $decision->source,
'created_at' => gmdate('c'),
]);
}
}
Pohranite izvornu poruku za tim koji na nju mora odgovoriti, ali ne šaljite adresu e-pošte klasifikatoru kada je sama poruka dovoljna. Time se smanjuje nepotrebno otkrivanje podataka. Pravila zadržavanja i brisanja trebala bi obuhvatiti i izvorni tekst i sažetak koji je izradio model.
Povežite konfiguraciju i krajnju točku za kontakt
<?php
// bootstrap.php
require __DIR__ . '/vendor/autoload.php';
$env = parse_ini_file(__DIR__ . '/.env', false, INI_SCANNER_RAW);
if ($env === false) {
throw new RuntimeException('Environment configuration is missing');
}
foreach (['SMART_ROUTING_TOKEN', 'SMART_ROUTING_MODEL', 'DB_DSN'] as $key) {
if (!isset($env[$key]) || $env[$key] === '') {
throw new RuntimeException("Missing configuration: {$key}");
}
}
$pdo = new PDO($env['DB_DSN'], null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$logger = static function (array $context): void {
error_log(json_encode(
['event' => 'smart_routing'] + $context,
JSON_THROW_ON_ERROR
));
};
$sleeper = static fn (int $seconds) => sleep($seconds);
return [
'router' => new SmartRoutingClient(
$env['SMART_ROUTING_TOKEN'],
$env['SMART_ROUTING_MODEL'],
new CurlTransport(),
$logger,
$sleeper,
),
'tickets' => new TicketRepository($pdo),
];
<?php
// public/contact.php
declare(strict_types=1);
session_start();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$csrf = (string) ($_POST['csrf'] ?? '');
$expected = (string) ($_SESSION['csrf'] ?? '');
if ($expected === '' || !hash_equals($expected, $csrf)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid form session']);
exit;
}
$email = trim((string) ($_POST['email'] ?? ''));
$message = trim((string) ($_POST['message'] ?? ''));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)
|| strlen($email) > 254
|| strlen($message) < 10
|| strlen($message) > 10000) {
http_response_code(422);
echo json_encode(['error' => 'Check the submitted fields']);
exit;
}
$services = require dirname(__DIR__) . '/bootstrap.php';
$id = bin2hex(random_bytes(16));
try {
try {
$decision = $services['router']->classify($message);
} catch (RoutingException $exception) {
error_log(json_encode([
'event' => 'smart_routing_fallback',
'ticket_id' => $id,
'exception' => $exception::class,
]));
$decision = new RoutingDecision(
'manual_review',
0.0,
'Automatic routing unavailable',
'fallback',
);
}
$services['tickets']->save($id, $email, $message, $decision);
} catch (PDOException $exception) {
error_log(json_encode([
'event' => 'ticket_persistence_failed',
'ticket_id' => $id,
]));
http_response_code(503);
echo json_encode(['error' => 'Please try again later']);
exit;
}
http_response_code(202);
echo json_encode(['ticket_id' => $id, 'status' => 'accepted']);
Obrazac koji ovdje šalje podatke mora izraditi $_SESSION['csrf'] pomoću bin2hex(random_bytes(32)) i uključiti ga u skriveno polje csrf. Dodajte i ograničenje broja zahtjeva na rubnoj ili aplikacijskoj razini; CSRF zaštita ne sprječava automatizirani spam.
Testirajte bez pozivanja usluge
Lažni transport čini testove usmjeravanja brzim i determinističkim. Također dokazuje da se ponašanje ponovnog pokušaja slučajno ne pretvara u beskonačnu petlju.
<?php
// tests/SmartRoutingClientTest.php
use PHPUnit\Framework\TestCase;
final class FakeTransport implements HttpTransport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function postJson(string $url, array $headers, array $body): HttpResponse
{
$this->calls++;
return array_shift($this->responses);
}
}
final class SmartRoutingClientTest extends TestCase
{
public function testMapsBillingDecision(): void
{
$transport = new FakeTransport([
new HttpResponse(200, json_encode([
'choices' => [[
'message' => ['content' => json_encode([
'queue' => 'billing',
'confidence' => 0.93,
'summary' => 'Customer requests an invoice copy',
])],
]],
])),
]);
$client = new SmartRoutingClient(
'test-token',
'test-model',
$transport,
static fn (array $context) => null,
static fn (int $seconds) => null,
);
$decision = $client->classify('Please send last month’s invoice.');
self::assertSame('billing', $decision->queue);
self::assertSame(1, $transport->calls);
}
public function testRetriesRateLimitThenSucceeds(): void
{
$transport = new FakeTransport([
new HttpResponse(429, '{}', ['retry-after' => '1']),
new HttpResponse(200, json_encode([
'choices' => [[
'message' => ['content' => json_encode([
'queue' => 'support',
'confidence' => 0.88,
'summary' => 'Login assistance needed',
])],
]],
])),
]);
$client = new SmartRoutingClient(
'test-token',
'test-model',
$transport,
static fn (array $context) => null,
static fn (int $seconds) => null,
);
self::assertSame(
'support',
$client->classify('I cannot sign in to my account.')->queue
);
self::assertSame(2, $transport->calls);
}
}
Dodajte dodatne slučajeve za neispravne omotnice, nevažeće nazive redova, nisku pouzdanost, HTTP 401, tri uzastopna prolazna kvara i pogreške baze podataka. Testovi na razini kontrolera trebali bi potvrditi da kvarovi usmjeravanja i dalje trajno pohranjuju prijavu manual_review, dok kvarovi trajne pohrane vraćaju HTTP 503.
Upravljajte njime kao produkcijskom značajkom
Zapisnici bi trebali sadržavati identifikator zahtjeva ili prijave, HTTP status, broj pokušaja, latenciju, ishod i izvor pričuvnog rješenja. Nikada ne bi smjeli sadržavati bearer token, cijelu kontaktnu poruku, neobrađeno tijelo uzvodnog odgovora ni adresu e-pošte. Postavite upozorenja za trajne kvarove autentifikacije, rastući broj pričuvnih usmjeravanja, ponavljane HTTP 429 odgovore i pogreške trajne pohrane. Nekoliko pričuvnih usmjeravanja znači otpornost; rastući red pričuvnih usmjeravanja znači incident.
Implementirajte s korijenom dokumenata web-poslužitelja postavljenim na public/. Osigurajte da PHP radnik može pročitati svoju tajnu konfiguraciju i pisati u direktorij SQLite baze podataka, dok korisnik web-poslužitelja ne može preuzeti nijednu datoteku. Pokrenite stvaranje sheme ili migracije prije usmjeravanja prometa na novo izdanje. Nakon rotacije tajni okruženja ponovno pokrenite dugotrajne PHP-FPM radnike.
Uobičajeni kvarovi obično su konkretni:
- HTTP 401 ili 403: provjerite token ograničen na uslugu, aktivaciju plana i je li netko ponovno generirao token.
- HTTP 429: pregledajte upotrebu kvote i volumen zahtjeva. Ograničeni ponovni pokušaji mogu ublažiti kratko ograničenje, ali iscrpljena kvota mora ostati vidljiva.
- HTTP 400 ili 422: provjerite konfigurirani identifikator modela i JSON oblik kompatibilan s OpenAI-jem; ne ponavljajte nepromijenjeni ulaz.
- Svaka prijava završava u ručnoj provjeri: pregledajte zapisnike provjere odgovora, raspodjelu pouzdanosti i vraća li model prozni tekst oko zatraženog JSON-a.
- SQLite zaključavanje ili kvarovi pisanja: provjerite dozvole direktorija i volumen istodobnih upisa. Ako se upisi redovito sukobljavaju, premjestite repozitorij iza poslužiteljske baze podataka bez promjene granice usmjeravanja.
Završni kontrolni popis za provjeru
- Plan računa je aktivan, a dokumentirani identifikator modela konfiguriran je.
- Token postoji samo u tajnoj konfiguraciji podržanoj okruženjem.
- Minimalni zahtjev krajnjoj točki uspijeva s implementiranom vjerodajnicom.
- Primjeri prodaje, podrške, naplate i zlouporabe dolaze do predviđenih redova.
- Odgovori s niskom pouzdanošću, neispravni, s isteklim vremenom i ograničeni kvotom dolaze do
manual_review. - Kvarovi autentifikacije i provjere ne ponavljaju se naslijepo.
- Kvar baze podataka vraća 503 umjesto da lažno potvrdi prihvaćanje.
- Zapisnici otkrivaju operativne ishode bez otkrivanja poruka ili vjerodajnica.
- PHPUnit testovi prolaze prije implementacije, a produkcijski red pričuvnih usmjeravanja nadzire se.
Pametno usmjeravanje zaslužuje svoje mjesto kada ulazni sandučić čini mirnijim, a da pritom prijave ne čini krhkima. Zadržite ovlasti modela uskim, provjerite svaku odluku, sačuvajte izvornu prijavu i neizvjesnost učinite eksplicitnim redom. Produkcijsko pravilo koje se pamti jednostavno je: automatizacija može odabrati brzi put, ali joj se nikada ne smije dopustiti da izbriše siguran.