- Added tone instruction retrieval to SupportReplyService. - Improved user feedback when no relevant article is found. - Included article URL and tone instruction in LLM prompt. - Updated response format to include source information. - Enhanced article management UI with search functionality and editing capabilities. - Introduced a new API endpoint for nearest articles based on vector search. - Added confidence badge component to display article confidence levels. - Implemented tests for article searching, editing, and nearest article API. - Removed obsolete .htaccess file.
82 lines
3.1 KiB
PHP
82 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Exceptions\OllamaUnavailableException;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Repositories\Contracts\ArticleRepositoryInterface;
|
|
use App\Services\EmbeddingService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class NearestArticleController extends Controller
|
|
{
|
|
public function __invoke(
|
|
Request $request,
|
|
EmbeddingService $embeddingService,
|
|
ArticleRepositoryInterface $articleRepository
|
|
): JsonResponse {
|
|
$validated = $request->validate([
|
|
'query' => ['required', 'string', 'min:2', 'max:1000'],
|
|
'limit' => ['sometimes', 'integer', 'min:1', 'max:20'],
|
|
'min_similarity' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
|
'include_content' => ['sometimes', 'boolean'],
|
|
]);
|
|
|
|
$query = trim($validated['query']);
|
|
$limit = (int) ($validated['limit'] ?? 5);
|
|
$minSimilarity = (float) ($validated['min_similarity'] ?? 0);
|
|
$includeContent = $request->boolean('include_content', false);
|
|
|
|
try {
|
|
$embedding = $embeddingService->embed($query);
|
|
} catch (OllamaUnavailableException $exception) {
|
|
return response()->json([
|
|
'message' => 'Embedding provider is unavailable.',
|
|
'error' => $exception->getMessage(),
|
|
], 503);
|
|
}
|
|
|
|
$embeddingContext = $embeddingService->context();
|
|
$candidates = $articleRepository->findSimilarByEmbedding(
|
|
embedding: $embedding,
|
|
limit: $limit,
|
|
embeddingContext: $embeddingContext,
|
|
filters: ['published_only' => true]
|
|
);
|
|
|
|
$results = collect($candidates)
|
|
->map(function ($candidate) use ($includeContent) {
|
|
$similarity = max(0, min(1, 1 - $candidate->distance));
|
|
$content = trim($candidate->content);
|
|
|
|
return [
|
|
'article_id' => $candidate->articleId,
|
|
'title' => $candidate->title,
|
|
'similarity' => round($similarity, 4),
|
|
'distance' => round($candidate->distance, 4),
|
|
'snippet' => str($content)->limit(220)->toString(),
|
|
'content' => $includeContent ? $content : null,
|
|
'source_url' => $candidate->sourceUrl,
|
|
'source_article_id' => $candidate->sourceArticleId,
|
|
'note' => $candidate->note,
|
|
'allowed_actions' => $candidate->allowedActions,
|
|
];
|
|
})
|
|
->filter(fn (array $result) => $result['similarity'] >= $minSimilarity)
|
|
->values();
|
|
|
|
return response()->json([
|
|
'data' => $results,
|
|
'meta' => [
|
|
'query' => $query,
|
|
'limit' => $limit,
|
|
'min_similarity' => $minSimilarity,
|
|
'published_only' => true,
|
|
'embedding_provider_instance_id' => $embeddingContext['provider_instance_id'] ?? null,
|
|
'embedding_model' => $embeddingContext['embedding_model'] ?? null,
|
|
],
|
|
]);
|
|
}
|
|
}
|