Nativni PHP 8.3: nacrti odgovora uz pomoć umjetne inteligencije za Inbox, uz zadržavanje vaše kontrole
Odgovor AI-ja postaje koristan tek kada se ponaša kao pažljiv pomoćnik, a ne kao autonomni pošiljatelj. Za ulaznu poštu malog poduzeća najsigurnija je granica jednostavna: model predlaže tekst, aplikacija ga pohranjuje zasebno, a član osoblja ga pregledava, uređuje i šalje kroz postojeći tijek rada.
Ovaj vodič gradi tu granicu u Native PHP 8.3. Integracija koristi krajnju točku kompatibilnu s OpenAI-jem modela Smart Routing AI Model, izvorni cURL, SQLite za kompaktnu primjerenu ulaznu poštu, obrambeno mapiranje odgovora, ograničene ponovne pokušaje, PHPUnit testove i eksplicitna stanja neuspjeha. Nijedan generirani tekst ne šalje se automatski.
Pribavite pristup prije pisanja integracijskog koda
Započnite registracijom računa ili upotrijebite stranicu za prijavu ako ga već imate.
- Otvorite stranicu usluge Smart Routing AI Model.
- Odaberite dostupni paket Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu.
- Također upotrijebite dokumentaciju kako biste potvrdili identifikator modela koji podržava vaš aktivirani paket.
Ova usluga zahtijeva token. Autentikacija koristi Authorization: Bearer {serviceToken}. Ponovno generiranje servisnog tokena opoziva prethodno aktivni token, pa rotacija tokena mora ažurirati svaku implementiranu instancu koja poziva uslugu.
Točan zahtjev je POST https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions. Prije izrade značajke pošaljite jedan minimalni zahtjev. Lokalno zamijenite oba rezervirana mjesta; nikada ne predajte rezultirajuću naredbu sa stvarnom vjerodajnicom.
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_DOCUMENTED_MODEL",
"messages": [
{"role": "user", "content": "Draft a polite acknowledgement of a contact request."}
]
}'
Uspješan odgovor slijedi standardni JSON oblik u stilu OpenAI-ja. Implementacija će obrambeno čitati choices[0].message.content umjesto da pretpostavi kako je svako tijelo koje izgleda uspješno upotrebljivo.
Pripremite projekt Native PHP
Potrebni su vam PHP 8.3 ili noviji s cURL-om, PDO SQLiteom, JSON-om, Composerom i zapisivim direktorijem podataka aplikacije. Jedini paket za izvođenje je vlucas/phpdotenv u kompatibilnom rasponu ^5.6. PHPUnit ^11.0 pruža pokretač testova.
mkdir inbox-drafts
cd inbox-drafts
composer init --name=example/inbox-drafts --no-interaction
composer require vlucas/phpdotenv:^5.6
composer require --dev phpunit/phpunit:^11.0
mkdir -p src public var tests
composer config autoload.psr-4.App\\ src/
composer dump-autoload
Izradite .env za lokalni razvoj i isključite ga iz kontrole verzija. Produkcija treba ubrizgavati iste varijable putem upravitelja procesa ili spremišta tajni umjesto kopiranja datoteke razvojnog programera na poslužitelj.
SMART_ROUTING_TOKEN=YOUR_SERVICE_TOKEN
SMART_ROUTING_MODEL=YOUR_DOCUMENTED_MODEL
APP_DB_PATH=var/inbox.sqlite
APP_ENV=development
Projekt ima četiri granice: kontroler autentificira zahtjev osoblja, repozitorij upravlja podacima ulazne pošte, aplikacijska usluga koordinira izradu skice, a samo API klijent razumije udaljeni HTTP ugovor. To košta nekoliko malih klasa, ali sprječava da detalji cURL-a, izlaz modela i ažuriranja baze podataka procure u jednu krhku skriptu.
inbox-drafts/
public/draft.php
src/AI/SmartRoutingClient.php
src/Inbox/InboxRepository.php
src/Inbox/DraftReplyService.php
tests/SmartRoutingClientTest.php
var/inbox.sqlite
.env
composer.json
Izradite pohranu ulazne pošte s granicom pregleda
Važan stupac je draft_status. Generirani sadržaj ulazi u pending_review; nikada ne ulazi u stanje poslanog. Produkcijska ulazna pošta kasnije može povezati svoje postojeće sučelje za pregled i slanje s ovim poljima.
CREATE TABLE contact_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_name TEXT NOT NULL,
sender_email TEXT NOT NULL,
subject TEXT NOT NULL,
body TEXT NOT NULL,
ai_draft TEXT,
draft_status TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO contact_messages
(sender_name, sender_email, subject, body)
VALUES
('Jordan', '[email protected]', 'Saturday appointment',
'Do you have any appointments available this Saturday?');
Izgradite obrambenu API granicu
Sljedeća datoteka zajedno sadrži transportni spoj i klijent radi čitljivosti. U većoj bazi koda svaki javni tip može imati vlastitu datoteku. cURL prima ograničenje povezivanja od tri sekunde i ukupno ograničenje od dvadeset sekundi. Klijent ponovno pokušava kod mrežnih neuspjeha, HTTP-a 429 i poslužiteljskih pogrešaka, ali nikada kod neuspjeha validacije ili autentikacije.
<?php
// src/AI/SmartRoutingClient.php
declare(strict_types=1);
namespace App\AI;
final class TransportException extends \RuntimeException {}
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body,
) {}
}
interface Transport
{
public function postJson(string $url, array $headers, array $body): HttpResponse;
}
final class CurlTransport implements Transport
{
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 => 3,
CURLOPT_TIMEOUT => 20,
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;
},
]);
$bodyText = curl_exec($handle);
if ($bodyText === 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, $responseHeaders, $bodyText);
}
}
final readonly class DraftReply
{
public function __construct(public string $content) {}
}
final class AiFailure extends \RuntimeException
{
public function __construct(
public readonly string $kind,
public readonly bool $retryable,
string $message,
) {
parent::__construct($message);
}
}
final class SmartRoutingClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions';
public function __construct(
private readonly Transport $transport,
private readonly string $token,
private readonly string $model,
private readonly \Closure $logger,
private readonly \Closure $sleeper,
) {}
public function draft(string $customerMessage): DraftReply
{
$payload = [
'model' => $this->model,
'messages' => [
[
'role' => 'system',
'content' => 'Draft a concise, courteous business reply. '
. 'Do not claim an action was completed. Do not invent '
. 'prices, availability, policies, or commitments. '
. 'Return reply text only for human review.',
],
[
'role' => 'user',
'content' => "Customer message:\n---\n"
. $customerMessage . "\n---",
],
],
];
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->postJson(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Content-Type: application/json',
],
$payload,
);
} catch (TransportException $exception) {
($this->logger)('ai_transport_failure', [
'attempt' => $attempt,
'error_type' => $exception::class,
]);
if ($attempt === 3) {
throw new AiFailure(
'transport',
true,
'The drafting service could not be reached.'
);
}
($this->sleeper)($attempt);
continue;
}
if ($response->status >= 200 && $response->status < 300) {
return $this->mapResponse($response->body);
}
if (in_array($response->status, [401, 403], true)) {
throw new AiFailure(
'authentication',
false,
'The drafting service rejected its credential.'
);
}
$retryable = $response->status === 429
|| $response->status >= 500;
($this->logger)('ai_http_failure', [
'attempt' => $attempt,
'status' => $response->status,
]);
if (!$retryable || $attempt === 3) {
$kind = $response->status === 429 ? 'quota_or_rate_limit' : 'http';
throw new AiFailure(
$kind,
$retryable,
'The drafting service did not accept the request.'
);
}
($this->sleeper)($attempt);
}
throw new AiFailure('internal', false, 'Unexpected drafting failure.');
}
private function mapResponse(string $body): DraftReply
{
try {
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
throw new AiFailure(
'invalid_response',
false,
'The drafting service returned invalid JSON.'
);
}
$content = $decoded['choices'][0]['message']['content'] ?? null;
if (!is_string($content) || trim($content) === '') {
throw new AiFailure(
'invalid_response',
false,
'The drafting response contained no usable text.'
);
}
return new DraftReply(trim($content));
}
}
Funkcija spavanja trebala bi implementirati malo ograničeno postupno čekanje. Na primjer, proslijedite zatvaranje koje spava jednu sekundu nakon prvog ponovnog pokušaja i dvije sekunde nakon drugog. Zadržavanje mogućnosti ubrizgavanja čini testove trenutačnima i determinističkima. Status 429 može značiti privremeno ograničenje stope ili iscrpljenu kvotu; klijent radi samo ograničene pokušaje, a zatim izlaže specifičnu vrstu neuspjeha umjesto da se vrti u petlji.
Mapirajte skicu u domenu ulazne pošte
Aplikacijska usluga dohvaća samo poruku korisnika, zahtijeva skicu i transakcijski pohranjuje rezultat. Adrese e-pošte namjerno su isključene iz upita jer ne dodaju vrijednost pri izradi skice.
<?php
// src/Inbox/InboxRepository.php and src/Inbox/DraftReplyService.php
declare(strict_types=1);
namespace App\Inbox;
use App\AI\SmartRoutingClient;
final class InboxRepository
{
public function __construct(private readonly \PDO $pdo) {}
public function messageBody(int $id): ?string
{
$statement = $this->pdo->prepare(
'SELECT body FROM contact_messages WHERE id = :id'
);
$statement->execute(['id' => $id]);
$body = $statement->fetchColumn();
return is_string($body) ? $body : null;
}
public function savePendingDraft(int $id, string $draft): void
{
$statement = $this->pdo->prepare(
"UPDATE contact_messages
SET ai_draft = :draft,
draft_status = 'pending_review',
updated_at = CURRENT_TIMESTAMP
WHERE id = :id"
);
$statement->execute(['draft' => $draft, 'id' => $id]);
if ($statement->rowCount() !== 1) {
throw new \RuntimeException('Message disappeared before draft save.');
}
}
}
final class DraftReplyService
{
public function __construct(
private readonly InboxRepository $inbox,
private readonly SmartRoutingClient $client,
) {}
public function create(int $messageId): string
{
$body = $this->inbox->messageBody($messageId);
if ($body === null) {
throw new \OutOfBoundsException('Contact message not found.');
}
$draft = $this->client->draft($body);
$this->inbox->savePendingDraft($messageId, $draft->content);
return $draft->content;
}
}
Izložite radnju izrade skice samo za osoblje
Krajnja točka u nastavku pretpostavlja da je tijek prijave ulazne pošte već pohranio identifikator člana osoblja i CSRF token u sesiju. Prihvaća ID poruke, sinkrono izrađuje skicu i vraća JSON za zaslon pregleda. Nikada ne prihvaća proizvoljan tekst upita i nema mogućnost slanja.
<?php
// public/draft.php
declare(strict_types=1);
use App\AI\AiFailure;
use App\AI\CurlTransport;
use App\AI\SmartRoutingClient;
use App\Inbox\DraftReplyService;
use App\Inbox\InboxRepository;
use Dotenv\Dotenv;
require dirname(__DIR__) . '/vendor/autoload.php';
Dotenv::createImmutable(dirname(__DIR__))->safeLoad();
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 = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!isset($_SESSION['staff_user_id'], $_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrf)) {
http_response_code(403);
echo json_encode(['error' => 'forbidden']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$messageId = filter_var($input['message_id'] ?? null, FILTER_VALIDATE_INT);
if ($messageId === false || $messageId < 1) {
http_response_code(422);
echo json_encode(['error' => 'invalid_message_id']);
exit;
}
$token = $_ENV['SMART_ROUTING_TOKEN'] ?? '';
$model = $_ENV['SMART_ROUTING_MODEL'] ?? '';
if ($token === '' || $model === '') {
http_response_code(503);
echo json_encode(['error' => 'service_not_configured']);
exit;
}
$database = $_ENV['APP_DB_PATH'] ?? 'var/inbox.sqlite';
if (!str_starts_with($database, '/')) {
$database = dirname(__DIR__) . '/' . $database;
}
$pdo = new PDO('sqlite:' . $database, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$logger = static function (string $event, array $context): void {
error_log(json_encode(
['event' => $event] + $context,
JSON_THROW_ON_ERROR
));
};
$sleeper = static function (int $attempt): void {
sleep(min($attempt, 2));
};
$client = new SmartRoutingClient(
new CurlTransport(),
$token,
$model,
$logger,
$sleeper,
);
$service = new DraftReplyService(new InboxRepository($pdo), $client);
try {
$draft = $service->create($messageId);
echo json_encode([
'draft' => $draft,
'status' => 'pending_review',
], JSON_THROW_ON_ERROR);
} catch (OutOfBoundsException) {
http_response_code(404);
echo json_encode(['error' => 'message_not_found']);
} catch (AiFailure $failure) {
http_response_code($failure->kind === 'authentication' ? 503 : 502);
echo json_encode([
'error' => 'draft_unavailable',
'reason' => $failure->kind,
'retryable' => $failure->retryable,
], JSON_THROW_ON_ERROR);
}
Testirajte ponovne pokušaje i mapiranje odgovora bez mreže
Lažni transport bolji je od simulirane cURL funkcije: provjerava ponašanje klijenta na HTTP granici, a pritom ostaje deterministički. Ovi testovi pokrivaju oporavak, politiku autentikacije i neispravne uspješne odgovore.
<?php
// tests/SmartRoutingClientTest.php
declare(strict_types=1);
use App\AI\AiFailure;
use App\AI\HttpResponse;
use App\AI\SmartRoutingClient;
use App\AI\Transport;
use PHPUnit\Framework\TestCase;
final class FakeTransport implements Transport
{
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
{
private function client(FakeTransport $transport): SmartRoutingClient
{
return new SmartRoutingClient(
$transport,
'test-token',
'documented-model',
static fn () => null,
static fn () => null,
);
}
public function testRetriesServerFailureAndMapsDraft(): void
{
$fake = new FakeTransport([
new HttpResponse(503, [], '{}'),
new HttpResponse(200, [], json_encode([
'choices' => [[
'message' => ['content' => ' Thanks for contacting us. '],
]],
], JSON_THROW_ON_ERROR)),
]);
self::assertSame(
'Thanks for contacting us.',
$this->client($fake)->draft('Hello')->content,
);
self::assertSame(2, $fake->calls);
}
public function testDoesNotRetryAuthenticationFailure(): void
{
$fake = new FakeTransport([new HttpResponse(401, [], '{}')]);
try {
$this->client($fake)->draft('Hello');
self::fail('Expected AiFailure');
} catch (AiFailure $failure) {
self::assertSame('authentication', $failure->kind);
self::assertFalse($failure->retryable);
self::assertSame(1, $fake->calls);
}
}
public function testRejectsMalformedSuccessfulResponse(): void
{
$fake = new FakeTransport([
new HttpResponse(200, [], '{"choices":[]}'),
]);
$this->expectException(AiFailure::class);
$this->client($fake)->draft('Hello');
}
}
vendor/bin/phpunit tests
Sigurnost, vidljivost i implementacija
Poruke korisnika tretirajte kao osjetljive podatke. Šaljite samo ono što je modelu potrebno, zadržite omogućenu enkripciju prijenosa, ograničite rutu za skice na autentificirano osoblje, zadržite CSRF zaštitu i definirajte odgovarajuću politiku zadržavanja pohranjenih skica. Generirani tekst prikazujte s HTML izbjegavanjem u sučelju za pregled. Izlaz modela je nepouzdan sadržaj, čak i kada upit traži običan tekst.
Zapisnici trebaju sadržavati nazive događaja, HTTP status, broj pokušaja, latenciju i interni identifikator poruke kada je primjereno. Ne smiju sadržavati bearer token, poruku korisnika, generirani odgovor, adresu e-pošte ni potpuno udaljeno tijelo. Postavite upozorenja za trajne neuspjehe autentikacije, ponovljene odgovore 429, neispravne uspješne odgovore i povećane poslužiteljske neuspjehe.
Implementirajte s omogućenim cURL-om i PDO SQLiteom, pokrenite testove, inicijalizirajte shemu i osigurajte da samo PHP proces može pisati u direktorij baze podataka. Ubrizgajte token i model putem tajni implementacije. Ako se token ponovno generira, ažurirajte tajnu i ponovno pokrenite ili učitajte svakog PHP radnika kako nijedan proces ne bi zadržao zastarjelu konfiguraciju.
Za prometniju ulaznu poštu premjestite izradu skica u pokretač poslova kako stranica za pregled ne bi čekala latenciju modela. Očuvajte istu granicu klijenta i domene, dodajte provjeru idempotentnosti prije spremanja i prikažite vidljivo stanje „izrada skice”. Nemojte dodavati red čekanja samo kako biste prikrili nedostatak rukovanja vremenskim ograničenjima i neuspjesima.
Uobičajeni neuspjesi za koje vrijedi dizajnirati
- 401 ili 403: provjerite token ograničen na uslugu, aktivaciju paketa i tajnu implementacije. Nemojte automatski ponovno pokušavati.
- 429: zaustavite se nakon ograničenog postupnog čekanja. Zamolite pregledavatelja da pokuša ponovno kasnije i provjerite kvotu paketa ili stopu zahtjeva.
- Neuspjeh validacije klase 400: usporedite vrijednost modela i JSON zahtjev sa službenom dokumentacijom. Ponovni pokušaj nepromijenjenog zahtjeva neće ga popraviti.
- Prazan ili neispravan uspjeh: postojeću skicu ostavite netaknutom i prijavite
invalid_response. - Istek vremena ili 5xx: ponovno pokušajte samo nekoliko puta, a zatim sučelju vratite oporavljiv neuspjeh.
- Nepouzdano formuliranje: zahtijevajte ljudski pregled, prikažite izvornu poruku pored skice i nikada ne pretvarajte skicu u automatsku obvezu.
Završni popis za provjeru
- Aktivirani paket, dokumentirana vrijednost modela i trenutačni servisni token konfigurirani su.
- Minimalni autentificirani zahtjev uspijeva prema točnoj krajnjoj točki za chat-completions.
- Nijedna vjerodajnica ne pojavljuje se u kontroli izvornog koda, testnim podacima, zapisnicima aplikacije ni odgovorima preglednika.
- PHPUnit provjerava uspješno mapiranje, ograničeni ponovni pokušaj i neponovljivi neuspjeh autentikacije.
- Ruta za osoblje odbija neautentificirane zahtjeve, zahtjeve koji nisu POST, neispravne zahtjeve i zahtjeve s nepodudarnim CSRF-om.
- Generirani odgovor pohranjuje se samo kao
pending_review. - Zaslon pregleda izbjegava izlaz i zahtijeva da ga osoba uredi ili odobri prije slanja.
- Produkcijsko nadziranje razlikuje neuspjehe autentikacije, kvote, prijenosa, HTTP-a i neispravnog odgovora.
Najjača značajka ovdje nije tečan tekst. To je namjerni razmak između prijedloga i radnje. Uz usku API granicu, obrambeno mapiranje, iskrena stanja neuspjeha i stanje baze podataka samo za pregled, AI može ukloniti teret prazne stranice iz obične ulazne pošte bez uklanjanja osobe čija prosudba štiti odnos s korisnikom.