633 lines
22 KiB
PHP
633 lines
22 KiB
PHP
<?php
|
||
|
||
namespace app\common\service\ai;
|
||
|
||
use app\common\model\ai\AiConfig;
|
||
use app\common\model\match\MatchEvent;
|
||
use think\facade\Cache;
|
||
|
||
class AiAssistantContextService
|
||
{
|
||
private const CRYPTO_SYMBOLS = [
|
||
'BTC' => ['BTC', '比特币', 'Bitcoin'],
|
||
'ETH' => ['ETH', '以太坊', 'Ethereum'],
|
||
'BNB' => ['BNB'],
|
||
'SOL' => ['SOL', 'Solana'],
|
||
'XRP' => ['XRP', 'Ripple'],
|
||
'DOGE' => ['DOGE', '狗狗币', 'Dogecoin'],
|
||
'TON' => ['TON', 'Toncoin'],
|
||
'TRX' => ['TRX', 'TRON', '波场'],
|
||
'ADA' => ['ADA', 'Cardano'],
|
||
'AVAX' => ['AVAX', 'Avalanche'],
|
||
];
|
||
|
||
private const QUERY_STOP_WORDS = [
|
||
'最新', '最近', '比赛结果', '比赛', '结果', '比分', '赛果', '赛程', '成绩', '战绩',
|
||
'球队', '赛事', '今天', '今日', '昨天', '昨日', '明天', '分析', '什么', '一下',
|
||
'查询', '查看', '看看', '给我', '有没有', '内容', '的',
|
||
];
|
||
|
||
public static function build(string $message, int $userId, int $sessionId, array $plan = []): array
|
||
{
|
||
$topK = max(1, min(12, (int) AiConfig::getVal('assistant_kb_topk', '6')));
|
||
$hits = self::retrieveKnowledge($message, $userId, $sessionId, $topK, $plan);
|
||
$matchItems = self::searchMatches($message, $plan);
|
||
$cryptoItems = self::resolveCryptoContext($message, $plan);
|
||
$payload = self::composePayload($hits, $matchItems, $cryptoItems, [], $plan);
|
||
|
||
return array_merge($payload, [
|
||
'knowledge_hits' => $hits,
|
||
'match_items' => $matchItems,
|
||
'crypto_items' => $cryptoItems,
|
||
]);
|
||
}
|
||
|
||
public static function composePayload(
|
||
array $hits,
|
||
array $matchItems,
|
||
array $cryptoItems,
|
||
array $webNewsItems = [],
|
||
array $plan = []
|
||
): array
|
||
{
|
||
$sources = [];
|
||
foreach ($webNewsItems as $item) {
|
||
$sources[] = $item['source'];
|
||
}
|
||
foreach ($hits as $hit) {
|
||
$sources[] = self::sourceFromKbHit($hit);
|
||
}
|
||
foreach ($matchItems as $item) {
|
||
$sources[] = $item['source'];
|
||
}
|
||
foreach ($cryptoItems as $item) {
|
||
$sources[] = $item['source'];
|
||
}
|
||
|
||
$sources = self::uniqueSources($sources);
|
||
$context = [
|
||
'query_plan' => self::compactPlan($plan),
|
||
'web_news_context' => array_column($webNewsItems, 'context'),
|
||
'knowledge_hits' => array_map([self::class, 'compactHit'], $hits),
|
||
'match_context' => array_column($matchItems, 'context'),
|
||
'crypto_realtime' => array_column($cryptoItems, 'context'),
|
||
];
|
||
|
||
return [
|
||
'context_text' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||
'sources' => array_values($sources),
|
||
'has_context' => !empty($webNewsItems) || !empty($hits) || !empty($matchItems) || !empty($cryptoItems),
|
||
];
|
||
}
|
||
|
||
private static function retrieveKnowledge(string $message, int $userId, int $sessionId, int $topK, array $plan = []): array
|
||
{
|
||
$queries = self::knowledgeQueries($message, $plan);
|
||
$filters = [
|
||
'user_id' => $userId,
|
||
'ref_id' => $sessionId,
|
||
'domains' => self::knowledgeDomains($plan),
|
||
];
|
||
if (!empty($plan['preferred_domain']) && in_array($plan['preferred_domain'], $filters['domains'], true)) {
|
||
$filters['preferred_domain'] = $plan['preferred_domain'];
|
||
}
|
||
|
||
$entities = is_array($plan['entities'] ?? null) ? $plan['entities'] : [];
|
||
$structuredContext = [
|
||
'title' => (string) ($plan['rewritten_query'] ?? $message),
|
||
'league' => (string) (($entities['leagues'][0] ?? '') ?: ''),
|
||
'teams' => is_array($entities['teams'] ?? null) ? $entities['teams'] : [],
|
||
];
|
||
|
||
$mergedHits = [];
|
||
foreach (array_slice($queries, 0, 3) as $index => $queryText) {
|
||
$result = KbService::retrieve('assistant_chat', $queryText, $filters, $structuredContext, max($topK, 6));
|
||
if (empty($result['success']) || empty($result['hits']) || !is_array($result['hits'])) {
|
||
continue;
|
||
}
|
||
|
||
foreach ($result['hits'] as $hit) {
|
||
$key = implode(':', [
|
||
(string) ($hit['domain'] ?? ''),
|
||
(string) ($hit['subtype'] ?? ''),
|
||
(int) ($hit['source_id'] ?? 0),
|
||
(int) ($hit['document_id'] ?? 0),
|
||
md5((string) ($hit['chunk_text'] ?? '')),
|
||
]);
|
||
$hit['score'] = round((float) ($hit['score'] ?? 0) - ($index * 0.01), 6);
|
||
if (!isset($mergedHits[$key]) || (float) ($hit['score'] ?? 0) > (float) ($mergedHits[$key]['score'] ?? 0)) {
|
||
$mergedHits[$key] = $hit;
|
||
}
|
||
}
|
||
}
|
||
|
||
$hits = array_values($mergedHits);
|
||
usort($hits, static fn(array $left, array $right) => ((float) ($right['score'] ?? 0)) <=> ((float) ($left['score'] ?? 0)));
|
||
return array_slice($hits, 0, $topK);
|
||
}
|
||
|
||
private static function searchMatches(string $message, array $plan = []): array
|
||
{
|
||
if (!self::shouldRetrieveMatchContext($message, $plan)) {
|
||
return [];
|
||
}
|
||
|
||
$terms = self::matchTerms($message, $plan);
|
||
if (empty($terms)) {
|
||
return [];
|
||
}
|
||
|
||
$matchDateRange = self::matchDateRange($message, $plan);
|
||
$preferSchedule = self::preferScheduleMatchContext($message, $plan);
|
||
|
||
try {
|
||
if ($preferSchedule) {
|
||
$rows = self::matchRowsByTerms($terms, 6, null, null, $matchDateRange, 'asc');
|
||
} elseif (self::preferCompletedMatchContext($message, $plan)) {
|
||
$rows = self::matchRowsByTerms($terms, 3, 2, null, $matchDateRange, 'desc');
|
||
if (count($rows) < 3) {
|
||
$rows = self::mergeMatchRows(
|
||
$rows,
|
||
self::matchRowsByTerms($terms, 3 - count($rows), null, 2, $matchDateRange, 'desc')
|
||
);
|
||
}
|
||
} else {
|
||
$rows = self::matchRowsByTerms($terms, 3, null, null, $matchDateRange, 'desc');
|
||
}
|
||
} catch (\Throwable $e) {
|
||
return [];
|
||
}
|
||
|
||
return array_map(static function (array $row) {
|
||
$title = trim(($row['home_team'] ?? '') . ' vs ' . ($row['away_team'] ?? ''));
|
||
$matchTime = !empty($row['match_time']) && is_numeric($row['match_time'])
|
||
? date('Y-m-d H:i', (int) $row['match_time'])
|
||
: (string) ($row['match_time'] ?? '');
|
||
$summary = sprintf(
|
||
'%s,%s,比分 %s-%s,赔率 主胜%s 平%s 客胜%s。',
|
||
$row['league_name'] ?? '',
|
||
$matchTime,
|
||
$row['home_score'] ?? 0,
|
||
$row['away_score'] ?? 0,
|
||
$row['home_odds'] ?? '-',
|
||
$row['draw_odds'] ?? '-',
|
||
$row['away_odds'] ?? '-'
|
||
);
|
||
|
||
return [
|
||
'context' => [
|
||
'domain' => 'match',
|
||
'title' => $title,
|
||
'summary' => $summary,
|
||
],
|
||
'source' => [
|
||
'type' => 'match',
|
||
'title' => $title,
|
||
'summary' => $summary,
|
||
'path' => '/pages/match_detail/match_detail?id=' . (int) $row['id'],
|
||
'source_id' => (int) $row['id'],
|
||
],
|
||
];
|
||
}, $rows);
|
||
}
|
||
|
||
private static function matchRowsByTerms(
|
||
array $terms,
|
||
int $limit,
|
||
?int $status = null,
|
||
?int $excludeStatus = null,
|
||
?array $matchDateRange = null,
|
||
string $orderBy = 'desc'
|
||
): array
|
||
{
|
||
$query = MatchEvent::where('is_show', 1);
|
||
$query->where(function ($q) use ($terms) {
|
||
foreach (array_slice($terms, 0, 5) as $term) {
|
||
$like = '%' . $term . '%';
|
||
$q->whereOr('home_team', 'like', $like)
|
||
->whereOr('away_team', 'like', $like)
|
||
->whereOr('league_name', 'like', $like);
|
||
}
|
||
});
|
||
|
||
if ($status !== null) {
|
||
$query->where('status', $status);
|
||
}
|
||
if ($excludeStatus !== null) {
|
||
$query->where('status', '<>', $excludeStatus);
|
||
}
|
||
if (is_array($matchDateRange) && count($matchDateRange) === 2) {
|
||
$query->whereBetween('match_time', [(int) $matchDateRange[0], (int) $matchDateRange[1]]);
|
||
}
|
||
|
||
$orderBy = strtolower($orderBy) === 'asc' ? 'asc' : 'desc';
|
||
|
||
return $query->field('id,league_name,home_team,away_team,match_time,status,home_score,away_score,home_odds,draw_odds,away_odds')
|
||
->order('match_time', $orderBy)
|
||
->limit($limit)
|
||
->select()
|
||
->toArray();
|
||
}
|
||
|
||
private static function preferScheduleMatchContext(string $message, array $plan = []): bool
|
||
{
|
||
if (($plan['intent'] ?? '') === 'match_schedule') {
|
||
return true;
|
||
}
|
||
return (bool) preg_match('/下一场|下场|赛程|未来|即将|未开赛|今天|今日|明天/u', $message);
|
||
}
|
||
|
||
private static function preferCompletedMatchContext(string $message, array $plan = []): bool
|
||
{
|
||
if (($plan['intent'] ?? '') === 'match_result') {
|
||
return true;
|
||
}
|
||
if (preg_match('/下一场|下场|赛程|未来|即将|未开赛|今天|今日/u', $message)) {
|
||
return false;
|
||
}
|
||
|
||
return (bool) preg_match('/最新|最近|结果|比分|赛果|成绩|战绩|上一场|上场/u', $message);
|
||
}
|
||
|
||
private static function matchDateRange(string $message, array $plan = []): ?array
|
||
{
|
||
$plannedRange = self::plannedDateRange($plan);
|
||
if ($plannedRange !== null) {
|
||
return $plannedRange;
|
||
}
|
||
|
||
$baseDate = strtotime(date('Y-m-d'));
|
||
if ($baseDate === false) {
|
||
return null;
|
||
}
|
||
|
||
if (preg_match('/今天|今日/u', $message)) {
|
||
return [$baseDate, $baseDate + 86400 - 1];
|
||
}
|
||
if (preg_match('/明天/u', $message)) {
|
||
$start = $baseDate + 86400;
|
||
return [$start, $start + 86400 - 1];
|
||
}
|
||
if (preg_match('/昨天|昨日/u', $message)) {
|
||
$start = $baseDate - 86400;
|
||
return [$start, $start + 86400 - 1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static function mergeMatchRows(array $primary, array $secondary): array
|
||
{
|
||
$rows = [];
|
||
$seen = [];
|
||
foreach (array_merge($primary, $secondary) as $row) {
|
||
$id = (int) ($row['id'] ?? 0);
|
||
if ($id <= 0 || isset($seen[$id])) {
|
||
continue;
|
||
}
|
||
$seen[$id] = true;
|
||
$rows[] = $row;
|
||
}
|
||
return $rows;
|
||
}
|
||
|
||
private static function resolveCryptoContext(string $message, array $plan = []): array
|
||
{
|
||
if (!self::shouldRetrieveCryptoContext($message, $plan)) {
|
||
return [];
|
||
}
|
||
if ((int) AiConfig::getVal('assistant_enable_crypto_realtime', '1') !== 1) {
|
||
return [];
|
||
}
|
||
|
||
$symbols = self::detectCryptoSymbols($message, $plan);
|
||
if (empty($symbols)) {
|
||
return [];
|
||
}
|
||
|
||
$items = [];
|
||
foreach (array_slice($symbols, 0, 3) as $symbol) {
|
||
$ticker = self::fetchBinanceTicker($symbol);
|
||
if (empty($ticker)) {
|
||
continue;
|
||
}
|
||
$price = (float) ($ticker['lastPrice'] ?? 0);
|
||
$change = (float) ($ticker['priceChangePercent'] ?? 0);
|
||
$high = (float) ($ticker['highPrice'] ?? 0);
|
||
$low = (float) ($ticker['lowPrice'] ?? 0);
|
||
$summary = sprintf(
|
||
'%s/USDT 当前价格 %.8g,24小时涨跌 %.2f%%,24小时高低 %.8g / %.8g。',
|
||
$symbol,
|
||
$price,
|
||
$change,
|
||
$high,
|
||
$low
|
||
);
|
||
$items[] = [
|
||
'context' => [
|
||
'domain' => 'crypto',
|
||
'symbol' => $symbol,
|
||
'summary' => $summary,
|
||
'raw' => [
|
||
'price' => $price,
|
||
'change_percent' => $change,
|
||
'high' => $high,
|
||
'low' => $low,
|
||
],
|
||
],
|
||
'source' => [
|
||
'type' => 'crypto',
|
||
'title' => $symbol . '/USDT 行情',
|
||
'summary' => $summary,
|
||
'path' => '/pages/crypto/crypto_detail?symbol=' . $symbol,
|
||
'source_id' => 0,
|
||
],
|
||
];
|
||
}
|
||
|
||
return $items;
|
||
}
|
||
|
||
private static function detectCryptoSymbols(string $message, array $plan = []): array
|
||
{
|
||
$plannedSymbols = [];
|
||
foreach ((array) ($plan['entities']['symbols'] ?? []) as $symbol) {
|
||
$symbol = strtoupper(trim((string) $symbol));
|
||
if ($symbol !== '') {
|
||
$plannedSymbols[] = $symbol;
|
||
}
|
||
}
|
||
if (!empty($plannedSymbols)) {
|
||
return array_values(array_unique($plannedSymbols));
|
||
}
|
||
|
||
$haystack = mb_strtolower($message);
|
||
$symbols = [];
|
||
foreach (self::CRYPTO_SYMBOLS as $symbol => $aliases) {
|
||
foreach ($aliases as $alias) {
|
||
if (mb_strpos($haystack, mb_strtolower($alias)) !== false) {
|
||
$symbols[] = $symbol;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return array_values(array_unique($symbols));
|
||
}
|
||
|
||
private static function fetchBinanceTicker(string $symbol): array
|
||
{
|
||
$cacheKey = 'assistant_crypto_ticker_' . $symbol;
|
||
$cached = Cache::get($cacheKey);
|
||
if (is_array($cached)) {
|
||
return $cached;
|
||
}
|
||
|
||
$url = 'https://api.binance.com/api/v3/ticker/24hr?symbol=' . rawurlencode($symbol . 'USDT');
|
||
$ch = curl_init($url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 8,
|
||
CURLOPT_CONNECTTIMEOUT => 4,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||
]);
|
||
$body = curl_exec($ch);
|
||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
$data = $httpCode === 200 ? json_decode((string) $body, true) : [];
|
||
if (is_array($data) && !empty($data['symbol'])) {
|
||
Cache::set($cacheKey, $data, 10);
|
||
return $data;
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
private static function sourceFromKbHit(array $hit): array
|
||
{
|
||
$domain = (string) ($hit['domain'] ?? '');
|
||
$sourceId = (int) ($hit['source_id'] ?? 0);
|
||
return [
|
||
'type' => $domain,
|
||
'title' => (string) ($hit['title'] ?? ''),
|
||
'summary' => self::clipText((string) ($hit['summary'] ?? $hit['chunk_text'] ?? ''), 120),
|
||
'path' => self::sourcePath($domain, $sourceId),
|
||
'source_id' => $sourceId,
|
||
];
|
||
}
|
||
|
||
private static function sourcePath(string $domain, int $sourceId): string
|
||
{
|
||
return match ($domain) {
|
||
'article' => '/pages/news_detail/news_detail?id=' . $sourceId,
|
||
'post' => '/packages_community/pages/post_detail?id=' . $sourceId,
|
||
'match' => '/pages/match_detail/match_detail?id=' . $sourceId,
|
||
'lottery' => '/pages/lottery_analysis/lottery_analysis',
|
||
default => '',
|
||
};
|
||
}
|
||
|
||
private static function compactHit(array $hit): array
|
||
{
|
||
return [
|
||
'domain' => $hit['domain'] ?? '',
|
||
'subtype' => $hit['subtype'] ?? '',
|
||
'source_id' => (int) ($hit['source_id'] ?? 0),
|
||
'title' => $hit['title'] ?? '',
|
||
'summary' => $hit['summary'] ?? '',
|
||
'content' => self::clipText((string) ($hit['chunk_text'] ?? ''), 360),
|
||
'score' => $hit['score'] ?? 0,
|
||
];
|
||
}
|
||
|
||
private static function uniqueSources(array $sources): array
|
||
{
|
||
$map = [];
|
||
foreach ($sources as $source) {
|
||
$key = ($source['type'] ?? '') . ':' . ($source['source_id'] ?? 0) . ':' . ($source['title'] ?? '');
|
||
if (!isset($map[$key]) && trim((string) ($source['title'] ?? '')) !== '') {
|
||
$map[$key] = $source;
|
||
}
|
||
}
|
||
return array_slice(array_values($map), 0, 8);
|
||
}
|
||
|
||
private static function queryTerms(string $message): array
|
||
{
|
||
$parts = preg_split('/[\s,,。!?;、::\/\\\\]+/u', trim($message)) ?: [];
|
||
$terms = [];
|
||
foreach ($parts as $part) {
|
||
$term = trim($part);
|
||
if ($term === '') {
|
||
continue;
|
||
}
|
||
if (self::isUsefulQueryTerm($term)) {
|
||
$terms[] = $term;
|
||
}
|
||
|
||
$normalized = str_replace(self::QUERY_STOP_WORDS, '', $term);
|
||
if ($normalized !== $term && self::isUsefulQueryTerm($normalized)) {
|
||
$terms[] = $normalized;
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($terms));
|
||
}
|
||
|
||
private static function isUsefulQueryTerm(string $term): bool
|
||
{
|
||
return mb_strlen($term) >= 2 && !in_array($term, self::QUERY_STOP_WORDS, true);
|
||
}
|
||
|
||
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) . '...';
|
||
}
|
||
|
||
private static function knowledgeQueries(string $message, array $plan): array
|
||
{
|
||
$queries = [];
|
||
$rewritten = trim((string) ($plan['rewritten_query'] ?? ''));
|
||
if ($rewritten !== '') {
|
||
$queries[] = $rewritten;
|
||
}
|
||
|
||
foreach ((array) ($plan['alternate_queries'] ?? []) as $query) {
|
||
$query = trim((string) $query);
|
||
if ($query !== '') {
|
||
$queries[] = $query;
|
||
}
|
||
}
|
||
|
||
$queries[] = $message;
|
||
return array_values(array_unique(array_filter($queries)));
|
||
}
|
||
|
||
private static function knowledgeDomains(array $plan): array
|
||
{
|
||
$domains = array_values(array_intersect(
|
||
(array) ($plan['domains'] ?? []),
|
||
['article', 'post', 'lottery', 'match']
|
||
));
|
||
|
||
return !empty($domains) ? $domains : ['article', 'post', 'lottery', 'match'];
|
||
}
|
||
|
||
private static function shouldRetrieveMatchContext(string $message, array $plan): bool
|
||
{
|
||
$domains = (array) ($plan['domains'] ?? []);
|
||
if (in_array('match', $domains, true) || ($plan['preferred_domain'] ?? '') === 'match') {
|
||
return true;
|
||
}
|
||
|
||
$entities = is_array($plan['entities'] ?? null) ? $plan['entities'] : [];
|
||
if (!empty($entities['teams']) || !empty($entities['leagues'])) {
|
||
return true;
|
||
}
|
||
|
||
return (bool) preg_match('/比赛|赛程|比分|赛果|战绩|世界杯|NBA|足球|篮球/u', $message);
|
||
}
|
||
|
||
private static function shouldRetrieveCryptoContext(string $message, array $plan): bool
|
||
{
|
||
$domains = (array) ($plan['domains'] ?? []);
|
||
if (in_array('crypto', $domains, true) || ($plan['preferred_domain'] ?? '') === 'crypto') {
|
||
return true;
|
||
}
|
||
|
||
if (!empty($plan['entities']['symbols'])) {
|
||
return true;
|
||
}
|
||
|
||
return (bool) preg_match('/BTC|ETH|BNB|SOL|XRP|DOGE|TON|TRX|ADA|AVAX|比特币|以太坊|行情/u', $message);
|
||
}
|
||
|
||
private static function matchTerms(string $message, array $plan): array
|
||
{
|
||
$terms = self::queryTerms($message);
|
||
|
||
$rewritten = trim((string) ($plan['rewritten_query'] ?? ''));
|
||
if ($rewritten !== '' && $rewritten !== $message) {
|
||
$terms = array_merge($terms, self::queryTerms($rewritten));
|
||
}
|
||
|
||
$entities = is_array($plan['entities'] ?? null) ? $plan['entities'] : [];
|
||
foreach (['teams', 'leagues', 'keywords'] as $key) {
|
||
foreach ((array) ($entities[$key] ?? []) as $term) {
|
||
$term = trim((string) $term);
|
||
if ($term !== '') {
|
||
$terms[] = $term;
|
||
}
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique(array_filter($terms)));
|
||
}
|
||
|
||
private static function plannedDateRange(array $plan): ?array
|
||
{
|
||
$dateRange = is_array($plan['date_range'] ?? null) ? $plan['date_range'] : [];
|
||
if (empty($dateRange)) {
|
||
return null;
|
||
}
|
||
|
||
$preset = (string) ($dateRange['preset'] ?? '');
|
||
if ($preset === 'today') {
|
||
return self::dayRange(date('Y-m-d'));
|
||
}
|
||
if ($preset === 'tomorrow') {
|
||
return self::dayRange(date('Y-m-d', strtotime('+1 day')));
|
||
}
|
||
if ($preset === 'yesterday') {
|
||
return self::dayRange(date('Y-m-d', strtotime('-1 day')));
|
||
}
|
||
|
||
if (!empty($dateRange['date'])) {
|
||
return self::dayRange((string) $dateRange['date']);
|
||
}
|
||
if (!empty($dateRange['start_date']) && !empty($dateRange['end_date'])) {
|
||
$start = strtotime((string) $dateRange['start_date'] . ' 00:00:00');
|
||
$end = strtotime((string) $dateRange['end_date'] . ' 23:59:59');
|
||
if ($start !== false && $end !== false) {
|
||
return [$start, $end];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static function dayRange(string $date): ?array
|
||
{
|
||
$start = strtotime($date . ' 00:00:00');
|
||
if ($start === false) {
|
||
return null;
|
||
}
|
||
return [$start, $start + 86400 - 1];
|
||
}
|
||
|
||
private static function compactPlan(array $plan): array
|
||
{
|
||
if (empty($plan)) {
|
||
return [];
|
||
}
|
||
|
||
return [
|
||
'intent' => $plan['intent'] ?? '',
|
||
'rewritten_query' => $plan['rewritten_query'] ?? '',
|
||
'alternate_queries' => $plan['alternate_queries'] ?? [],
|
||
'domains' => $plan['domains'] ?? [],
|
||
'preferred_domain' => $plan['preferred_domain'] ?? '',
|
||
'entities' => $plan['entities'] ?? [],
|
||
'date_range' => $plan['date_range'] ?? [],
|
||
'needs_realtime' => $plan['needs_realtime'] ?? false,
|
||
'confidence' => $plan['confidence'] ?? 0,
|
||
'meta' => $plan['_meta'] ?? [],
|
||
];
|
||
}
|
||
}
|