149 lines
5.5 KiB
PHP
149 lines
5.5 KiB
PHP
<?php
|
||
|
||
namespace app\common\service\ai;
|
||
|
||
use app\common\model\ai\AiConfig;
|
||
|
||
class AiAssistantWebNewsRetriever
|
||
{
|
||
public static function retrieve(string $message, array $plan, array $historyMessages = []): array
|
||
{
|
||
if (!self::shouldRetrieve($message, $plan)) {
|
||
return [];
|
||
}
|
||
|
||
$client = new DeepSeekClient();
|
||
$result = $client->chatJson(self::systemPrompt(), self::userPrompt($message, $plan, $historyMessages), self::modelOptions());
|
||
if (empty($result['success']) || !is_array($result['parsed'] ?? null)) {
|
||
return [];
|
||
}
|
||
|
||
return self::normalizeItems($result['parsed'], $message);
|
||
}
|
||
|
||
private static function shouldRetrieve(string $message, array $plan): bool
|
||
{
|
||
if ((int) AiConfig::getVal('assistant_enable_web_news_search', '1') !== 1) {
|
||
return false;
|
||
}
|
||
|
||
$intent = (string) ($plan['intent'] ?? '');
|
||
$preferredDomain = (string) ($plan['preferred_domain'] ?? '');
|
||
$domains = (array) ($plan['domains'] ?? []);
|
||
|
||
if (in_array($intent, ['crypto_quote', 'match_schedule', 'match_result', 'lottery_analysis'], true)) {
|
||
return false;
|
||
}
|
||
|
||
if ($preferredDomain === 'article' || in_array('article', $domains, true) || in_array('post', $domains, true)) {
|
||
return true;
|
||
}
|
||
|
||
return (bool) preg_match('/资讯|新闻|消息|报道|热点|刚刚|最新|头条/u', $message);
|
||
}
|
||
|
||
private static function systemPrompt(): string
|
||
{
|
||
$today = date('Y-m-d');
|
||
return '你是世博头条 AI 助手的联网资讯检索器。'
|
||
. '请调用 web search 工具搜索与用户问题最相关、尽量最新的体育资讯或新闻。'
|
||
. '今天日期是 ' . $today . '。'
|
||
. '只输出 JSON:{"items":[{"title":"","source":"","url":"","summary":"","published_at":""}]}。'
|
||
. '要求:最多返回 5 条;title 和 url 必填;summary 只写 1-2 句;没有可靠结果时返回 {"items":[]}。';
|
||
}
|
||
|
||
private static function userPrompt(string $message, array $plan, array $historyMessages): string
|
||
{
|
||
$history = [];
|
||
foreach (array_slice($historyMessages, -4) as $row) {
|
||
$content = trim((string) ($row['content'] ?? ''));
|
||
if ($content === '') {
|
||
continue;
|
||
}
|
||
$history[] = [
|
||
'role' => (string) ($row['role'] ?? 'user'),
|
||
'content' => mb_substr($content, 0, 200),
|
||
];
|
||
}
|
||
|
||
return "用户问题:\n"
|
||
. $message
|
||
. "\n\n查询规划:\n"
|
||
. json_encode([
|
||
'intent' => $plan['intent'] ?? '',
|
||
'rewritten_query' => $plan['rewritten_query'] ?? '',
|
||
'alternate_queries' => $plan['alternate_queries'] ?? [],
|
||
'entities' => $plan['entities'] ?? [],
|
||
'date_range' => $plan['date_range'] ?? [],
|
||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||
. "\n\n最近对话:\n"
|
||
. json_encode($history, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||
. "\n\n请检索最新外网资讯并输出 JSON。";
|
||
}
|
||
|
||
private static function modelOptions(): array
|
||
{
|
||
return DeepSeekClient::defaultModelOptions([
|
||
'max_tokens' => 900,
|
||
'temperature' => 0.2,
|
||
'retry_times' => 1,
|
||
'retry_sleep_ms' => 600,
|
||
'tools' => [
|
||
['type' => 'web_search'],
|
||
],
|
||
'tool_choice' => 'required',
|
||
'include' => ['web_search_call.action.sources'],
|
||
]);
|
||
}
|
||
|
||
private static function normalizeItems(array $parsed, string $message): array
|
||
{
|
||
$items = [];
|
||
foreach ((array) ($parsed['items'] ?? []) as $row) {
|
||
$title = trim((string) ($row['title'] ?? ''));
|
||
$url = trim((string) ($row['url'] ?? ''));
|
||
if ($title === '' || $url === '' || !preg_match('#^https?://#i', $url)) {
|
||
continue;
|
||
}
|
||
|
||
$sourceName = trim((string) ($row['source'] ?? ''));
|
||
$publishedAt = trim((string) ($row['published_at'] ?? ''));
|
||
$summary = trim((string) ($row['summary'] ?? ''));
|
||
$contextSummary = trim(implode(' ', array_filter([
|
||
$sourceName !== '' ? '来源:' . $sourceName : '',
|
||
$publishedAt !== '' ? '时间:' . $publishedAt : '',
|
||
$summary,
|
||
])));
|
||
|
||
$items[] = [
|
||
'context' => [
|
||
'domain' => 'web_news',
|
||
'title' => $title,
|
||
'summary' => $contextSummary !== '' ? $contextSummary : $message,
|
||
'url' => $url,
|
||
'source' => $sourceName,
|
||
'published_at' => $publishedAt,
|
||
],
|
||
'source' => [
|
||
'type' => 'web_news',
|
||
'title' => $title,
|
||
'summary' => self::clipText($contextSummary !== '' ? $contextSummary : $summary, 140),
|
||
'path' => '/pages/webview/webview?url=' . urlencode($url),
|
||
'source_id' => 0,
|
||
],
|
||
];
|
||
}
|
||
|
||
return array_slice($items, 0, 5);
|
||
}
|
||
|
||
private static function clipText(string $text, int $length): string
|
||
{
|
||
$text = trim(preg_replace('/\s+/u', ' ', $text) ?: '');
|
||
if (mb_strlen($text) <= $length) {
|
||
return $text;
|
||
}
|
||
return mb_substr($text, 0, $length) . '...';
|
||
}
|
||
}
|