feat: Enhance Support Reply Service with tone instructions and article details

- 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.
This commit is contained in:
your name
2026-05-13 22:25:45 +02:00
parent c94d3f85e8
commit 9244899f9b
22 changed files with 813 additions and 123 deletions

View File

@@ -0,0 +1,81 @@
<?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,
],
]);
}
}