add suggestion management
PHP Tests / php-tests (push) Has been cancelled

This commit is contained in:
Nyan Lin Paing
2026-09-01 00:16:17 +07:00
parent b8d31e3dc4
commit 31ed52500a
12 changed files with 1226 additions and 5 deletions
@@ -3,6 +3,7 @@
namespace Modules\Shared\Bnfexpress;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Modules\Shared\Bnfexpress\Exceptions\BnfexpressApiException;
use Modules\Shared\Bnfexpress\Support\BnfexpressSignature;
@@ -163,6 +164,159 @@ class BnfexpressAdminClient
return $this->request('GET', "/admin/ev/history/{$userId}/{$sessionId}");
}
// --- Suggestions ------------------------------------------------------
/**
* @return array<string, mixed>
*/
public function listSuggestions(?string $q = null, ?int $limit = null, ?int $offset = null): array
{
return $this->request('GET', '/admin/suggestions', query: array_filter([
'q' => $q,
'limit' => $limit,
'offset' => $offset,
], fn (mixed $value): bool => $value !== null));
}
/**
* @return array<string, mixed>
*/
public function getSuggestion(int|string $id): array
{
return $this->request('GET', "/admin/suggestions/{$id}");
}
/**
* @return array<string, mixed>
*/
public function createSuggestion(string $textDisplay, string $lang, ?string $intent = null, int $weight = 0, string $source = 'admin'): array
{
return $this->request('POST', '/admin/suggestions', body: [
'text_display' => $textDisplay,
'lang' => $lang,
'intent' => $intent,
'weight' => $weight,
'source' => $source,
]);
}
/**
* @return array<string, mixed>
*/
public function updateSuggestion(int|string $id, ?string $textDisplay = null, ?string $lang = null, ?string $intent = null, ?int $weight = null, ?string $source = null): array
{
return $this->request('PATCH', "/admin/suggestions/{$id}", body: array_filter([
'text_display' => $textDisplay,
'lang' => $lang,
'intent' => $intent,
'weight' => $weight,
'source' => $source,
], fn (mixed $value): bool => $value !== null));
}
/**
* @return array<string, mixed>
*/
public function deleteSuggestion(int|string $id): array
{
return $this->request('DELETE', "/admin/suggestions/{$id}");
}
/**
* @param list<array{text: string, lang: string, intent?: string|null}> $items
* @return array{created: int, skipped: int, trie_rebuilt: bool}
*/
public function batchCreateSuggestions(array $items): array
{
return $this->request('POST', '/admin/suggestions/batch', body: ['items' => $items]);
}
/**
* @param list<int|string> $ids
* @return array{deleted: int, skipped: int}
*/
public function batchDeleteSuggestions(array $ids): array
{
return $this->request('DELETE', '/admin/suggestions/batch', body: ['ids' => $ids]);
}
// --- Suggestion misses --------------------------------------------------
/**
* @return array<int, array<string, mixed>>
*/
public function listSuggestionMisses(?bool $wasUsed = null, ?int $limit = null, ?int $offset = null): array
{
return $this->request('GET', '/admin/suggestion-misses', query: array_filter([
'was_used' => $wasUsed,
'limit' => $limit,
'offset' => $offset,
], fn (mixed $value): bool => $value !== null));
}
/**
* @return array<string, mixed>
*/
public function dismissSuggestionMiss(int|string $id): array
{
return $this->request('DELETE', "/admin/suggestion-misses/{$id}");
}
/**
* @param list<int|string> $missIds
* @return array{created: int, skipped: int, trie_rebuilt: bool}
*/
public function promoteSuggestionMisses(array $missIds, ?string $lang = null, ?string $intent = null): array
{
return $this->request('POST', '/admin/suggestion-misses/promote', body: array_filter([
'miss_ids' => $missIds,
'lang' => $lang,
'intent' => $intent,
], fn (mixed $value): bool => $value !== null));
}
// --- Suggestion sync/embeddings ------------------------------------------
/**
* @return array{job_id: string}
*/
public function syncSuggestions(): array
{
return $this->request('POST', '/admin/suggestions/sync-chroma');
}
/**
* @return array{status: string, result: mixed}
*/
public function getSuggestionSyncStatus(string $jobId): array
{
return $this->request('GET', "/admin/suggestions/sync-chroma/{$jobId}");
}
/**
* @return array<string, mixed>
*/
public function syncOneSuggestion(int|string $id): array
{
return $this->request('POST', "/admin/suggestions/{$id}/sync-chroma");
}
/**
* @return array<string, mixed>
*/
public function reloadSuggestionIndex(): array
{
return $this->request('POST', '/admin/suggestions/reload-index');
}
/**
* @return array<string, mixed>
*/
public function deleteSuggestionEmbedding(int|string $id): array
{
return $this->request('DELETE', "/admin/suggestions/{$id}/chroma");
}
// --- Request plumbing ------------------------------------------------
/**
@@ -183,7 +337,12 @@ class BnfexpressAdminClient
try {
$response = match ($method) {
'GET' => $pending->get($path, $query),
'DELETE' => $pending->delete($path, $query),
// DELETE with a body (e.g. batchDeleteSuggestions) must send it the same
// way POST/PATCH do — $query is never used as delete()'s $data here, that
// param means something else (a JSON body) than what its name implies.
'DELETE' => $body !== null
? $pending->withBody($rawBody, 'application/json')->delete($path)
: $pending->delete($path),
'POST' => $pending->withBody($rawBody, 'application/json')->post($path),
'PATCH' => $pending->withBody($rawBody, 'application/json')->patch($path),
default => throw new \InvalidArgumentException("Unsupported HTTP method [{$method}]."),
@@ -193,12 +352,34 @@ class BnfexpressAdminClient
}
if (! $response->successful()) {
throw new BnfexpressApiException(
(string) ($response->json('detail') ?? "bnfexpress request failed with status {$response->status()}."),
$response->status(),
);
throw new BnfexpressApiException($this->errorMessage($response), $response->status());
}
return (array) $response->json();
}
/**
* bnfexpress's {"detail": "..."} is usually a plain string, but FastAPI's
* own request-validation failures (422s) return `detail` as a list of
* {loc, msg, type} objects instead casting that straight to string
* produces the literal, useless "Array" (with a PHP warning). Handle
* both shapes.
*/
private function errorMessage(Response $response): string
{
$detail = $response->json('detail');
if (is_string($detail)) {
return $detail;
}
if (is_array($detail)) {
return implode(' ', array_map(
fn (mixed $item): string => is_array($item) ? (string) ($item['msg'] ?? json_encode($item)) : (string) $item,
$detail,
));
}
return "bnfexpress request failed with status {$response->status()}.";
}
}