deploy: auto commit server changes 2026-06-12 14:57:05
This commit is contained in:
@@ -21,13 +21,29 @@ class AiAssistantContextService
|
||||
'AVAX' => ['AVAX', 'Avalanche'],
|
||||
];
|
||||
|
||||
public static function build(string $message, int $userId, int $sessionId): array
|
||||
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);
|
||||
$matchItems = self::searchMatches($message);
|
||||
$cryptoItems = self::resolveCryptoContext($message);
|
||||
$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 $plan = []): array
|
||||
{
|
||||
$sources = [];
|
||||
foreach ($hits as $hit) {
|
||||
$sources[] = self::sourceFromKbHit($hit);
|
||||
@@ -41,6 +57,7 @@ class AiAssistantContextService
|
||||
|
||||
$sources = self::uniqueSources($sources);
|
||||
$context = [
|
||||
'query_plan' => self::compactPlan($plan),
|
||||
'knowledge_hits' => array_map([self::class, 'compactHit'], $hits),
|
||||
'match_context' => array_column($matchItems, 'context'),
|
||||
'crypto_realtime' => array_column($cryptoItems, 'context'),
|
||||
@@ -53,50 +70,80 @@ class AiAssistantContextService
|
||||
];
|
||||
}
|
||||
|
||||
private static function retrieveKnowledge(string $message, int $userId, int $sessionId, int $topK): array
|
||||
private static function retrieveKnowledge(string $message, int $userId, int $sessionId, int $topK, array $plan = []): array
|
||||
{
|
||||
$result = KbService::retrieve(
|
||||
'assistant_chat',
|
||||
$message,
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'ref_id' => $sessionId,
|
||||
'domains' => ['article', 'post', 'lottery', 'match'],
|
||||
],
|
||||
['title' => $message],
|
||||
$topK
|
||||
);
|
||||
$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'];
|
||||
}
|
||||
|
||||
if (empty($result['success']) || empty($result['hits']) || !is_array($result['hits'])) {
|
||||
$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 [];
|
||||
}
|
||||
|
||||
return $result['hits'];
|
||||
}
|
||||
|
||||
private static function searchMatches(string $message): array
|
||||
{
|
||||
$terms = self::queryTerms($message);
|
||||
$terms = self::matchTerms($message, $plan);
|
||||
if (empty($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$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);
|
||||
}
|
||||
});
|
||||
$matchDateRange = self::matchDateRange($message, $plan);
|
||||
$preferSchedule = self::preferScheduleMatchContext($message, $plan);
|
||||
|
||||
$rows = $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 desc')
|
||||
->limit(3)
|
||||
->select()
|
||||
->toArray();
|
||||
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 [];
|
||||
}
|
||||
@@ -134,13 +181,116 @@ class AiAssistantContextService
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
private static function resolveCryptoContext(string $message): array
|
||||
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);
|
||||
$symbols = self::detectCryptoSymbols($message, $plan);
|
||||
if (empty($symbols)) {
|
||||
return [];
|
||||
}
|
||||
@@ -188,8 +338,19 @@ class AiAssistantContextService
|
||||
return $items;
|
||||
}
|
||||
|
||||
private static function detectCryptoSymbols(string $message): array
|
||||
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) {
|
||||
@@ -284,11 +445,29 @@ class AiAssistantContextService
|
||||
|
||||
private static function queryTerms(string $message): array
|
||||
{
|
||||
$terms = preg_split('/[\s,,。!?;、::\/\\\\]+/u', trim($message)) ?: [];
|
||||
$terms = array_values(array_filter(array_map('trim', $terms), static function (string $term) {
|
||||
return mb_strlen($term) >= 2 && !in_array($term, ['今天', '最近', '分析', '什么', '一下'], true);
|
||||
}));
|
||||
return array_unique($terms);
|
||||
$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
|
||||
@@ -299,4 +478,145 @@ class AiAssistantContextService
|
||||
}
|
||||
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'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
class AiAssistantOrchestrator
|
||||
{
|
||||
public static function run(string $message, array $historyMessages, int $userId, int $sessionId): array
|
||||
{
|
||||
$trace = new AiAssistantTrace($message, $historyMessages);
|
||||
|
||||
$trace->plan = AiAssistantPlanner::plan($message, $historyMessages);
|
||||
$trace = AiAssistantRetriever::retrieve($trace, $userId, $sessionId);
|
||||
$trace = AiAssistantRanker::rank($trace);
|
||||
$trace = AiAssistantResponder::respond($trace);
|
||||
|
||||
return [
|
||||
'success' => !empty($trace->responseResult['success']),
|
||||
'answer' => $trace->finalAnswer,
|
||||
'sources' => $trace->finalSources,
|
||||
'context_text' => $trace->contextText,
|
||||
'plan' => $trace->plan,
|
||||
'result' => $trace->responseResult,
|
||||
'trace' => $trace->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\model\ai\AiConfig;
|
||||
|
||||
class AiAssistantPlanner
|
||||
{
|
||||
private const ALLOWED_DOMAINS = ['article', 'post', 'lottery', 'match', 'crypto'];
|
||||
private const DEFAULT_DOMAINS = ['article', 'post', 'lottery', 'match'];
|
||||
private const DATE_PRESETS = ['today', 'tomorrow', 'yesterday', 'latest', 'recent'];
|
||||
private const CRYPTO_SYMBOLS = ['BTC', 'ETH', 'BNB', 'SOL', 'XRP', 'DOGE', 'TON', 'TRX', 'ADA', 'AVAX'];
|
||||
|
||||
public static function plan(string $message, array $historyMessages = []): array
|
||||
{
|
||||
if ((int) AiConfig::getVal('assistant_enable_query_planner', '1') !== 1) {
|
||||
return self::fallbackPlan($message);
|
||||
}
|
||||
|
||||
$client = new DeepSeekClient();
|
||||
$result = $client->chatJson(self::systemPrompt(), self::userPrompt($message, $historyMessages), self::modelOptions());
|
||||
if (!empty($result['success']) && is_array($result['parsed'] ?? null)) {
|
||||
$plan = self::normalizePlan($result['parsed'], $message);
|
||||
$plan['_meta'] = [
|
||||
'source' => 'llm',
|
||||
'model' => (string) ($result['model'] ?? ''),
|
||||
'provider_label' => (string) ($result['provider_label'] ?? ''),
|
||||
];
|
||||
return $plan;
|
||||
}
|
||||
|
||||
$plan = self::fallbackPlan($message);
|
||||
$plan['_meta'] = [
|
||||
'source' => 'fallback',
|
||||
'error' => (string) ($result['error'] ?? 'planner_failed'),
|
||||
];
|
||||
return $plan;
|
||||
}
|
||||
|
||||
private static function systemPrompt(): string
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
return '你是世博头条 AI 助手的查询规划器。'
|
||||
. '你的任务不是直接回答用户,而是把问题改写成便于后续检索和回答的结构化 JSON。'
|
||||
. '今天日期是 ' . $today . '。'
|
||||
. '必须只输出一个 JSON 对象,不要输出额外说明。'
|
||||
. '字段要求:'
|
||||
. 'intent(如 match_schedule/match_result/crypto_quote/lottery_analysis/news_search/general_search)、'
|
||||
. 'rewritten_query(字符串)、alternate_queries(字符串数组)、domains(article/post/lottery/match/crypto 数组)、'
|
||||
. 'preferred_domain(单个领域)、entities(teams/leagues/symbols/keywords 数组)、'
|
||||
. 'date_range(preset/date/start_date/end_date,可为空对象)、needs_realtime(布尔)、'
|
||||
. 'answer_format(固定 markdown)、confidence(0-1 数值)。'
|
||||
. '如果用户提到今天/明天/昨天/最新/最近,要在 date_range 或 intent 中明确体现。';
|
||||
}
|
||||
|
||||
private static function userPrompt(string $message, array $historyMessages): string
|
||||
{
|
||||
$history = [];
|
||||
foreach (array_slice($historyMessages, -6) as $row) {
|
||||
$role = (string) ($row['role'] ?? 'user');
|
||||
$content = trim((string) ($row['content'] ?? ''));
|
||||
if ($content === '') {
|
||||
continue;
|
||||
}
|
||||
$history[] = [
|
||||
'role' => $role,
|
||||
'content' => mb_substr($content, 0, 300),
|
||||
];
|
||||
}
|
||||
|
||||
return "对话历史:\n"
|
||||
. json_encode($history, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
. "\n\n当前用户问题:\n"
|
||||
. $message
|
||||
. "\n\n请输出查询规划 JSON。";
|
||||
}
|
||||
|
||||
private static function modelOptions(): array
|
||||
{
|
||||
return DeepSeekClient::defaultModelOptions([
|
||||
'max_tokens' => 800,
|
||||
'temperature' => 0.15,
|
||||
'retry_times' => 1,
|
||||
'retry_sleep_ms' => 600,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function fallbackPlan(string $message): array
|
||||
{
|
||||
return self::normalizePlan([
|
||||
'intent' => self::inferIntent($message),
|
||||
'rewritten_query' => $message,
|
||||
'alternate_queries' => self::buildAlternateQueries($message),
|
||||
'domains' => self::inferDomains($message),
|
||||
'preferred_domain' => self::inferPreferredDomain($message),
|
||||
'entities' => self::inferEntities($message),
|
||||
'date_range' => self::inferDateRange($message),
|
||||
'needs_realtime' => self::inferNeedsRealtime($message),
|
||||
'answer_format' => 'markdown',
|
||||
'confidence' => 0.45,
|
||||
], $message);
|
||||
}
|
||||
|
||||
private static function normalizePlan(array $parsed, string $message): array
|
||||
{
|
||||
$intent = trim((string) ($parsed['intent'] ?? ''));
|
||||
if ($intent === '') {
|
||||
$intent = self::inferIntent($message);
|
||||
}
|
||||
|
||||
$rewrittenQuery = trim((string) ($parsed['rewritten_query'] ?? ''));
|
||||
if ($rewrittenQuery === '') {
|
||||
$rewrittenQuery = $message;
|
||||
}
|
||||
|
||||
$alternateQueries = [];
|
||||
foreach ((array) ($parsed['alternate_queries'] ?? []) as $query) {
|
||||
$query = trim((string) $query);
|
||||
if ($query !== '' && $query !== $rewrittenQuery) {
|
||||
$alternateQueries[] = $query;
|
||||
}
|
||||
}
|
||||
$alternateQueries = array_slice(array_values(array_unique($alternateQueries)), 0, 4);
|
||||
|
||||
$domains = [];
|
||||
foreach ((array) ($parsed['domains'] ?? []) as $domain) {
|
||||
$domain = trim((string) $domain);
|
||||
if (in_array($domain, self::ALLOWED_DOMAINS, true)) {
|
||||
$domains[] = $domain;
|
||||
}
|
||||
}
|
||||
if (empty($domains)) {
|
||||
$domains = self::inferDomains($message);
|
||||
}
|
||||
$domains = array_values(array_unique($domains));
|
||||
|
||||
$preferredDomain = trim((string) ($parsed['preferred_domain'] ?? ''));
|
||||
if (!in_array($preferredDomain, $domains, true)) {
|
||||
$preferredDomain = self::inferPreferredDomain($message);
|
||||
}
|
||||
if (!in_array($preferredDomain, $domains, true)) {
|
||||
$preferredDomain = (string) ($domains[0] ?? '');
|
||||
}
|
||||
|
||||
$entities = [
|
||||
'teams' => self::normalizeTextArray($parsed['entities']['teams'] ?? []),
|
||||
'leagues' => self::normalizeTextArray($parsed['entities']['leagues'] ?? []),
|
||||
'symbols' => self::normalizeTextArray($parsed['entities']['symbols'] ?? []),
|
||||
'keywords' => self::normalizeTextArray($parsed['entities']['keywords'] ?? []),
|
||||
];
|
||||
if (empty($entities['teams']) && empty($entities['leagues']) && empty($entities['symbols']) && empty($entities['keywords'])) {
|
||||
$entities = self::inferEntities($message);
|
||||
}
|
||||
|
||||
$dateRange = self::normalizeDateRange((array) ($parsed['date_range'] ?? []), $message);
|
||||
$needsRealtime = filter_var($parsed['needs_realtime'] ?? self::inferNeedsRealtime($message), FILTER_VALIDATE_BOOL);
|
||||
$answerFormat = trim((string) ($parsed['answer_format'] ?? 'markdown')) ?: 'markdown';
|
||||
$confidence = (float) ($parsed['confidence'] ?? 0.55);
|
||||
$confidence = max(0.0, min(1.0, $confidence));
|
||||
|
||||
return [
|
||||
'intent' => $intent,
|
||||
'rewritten_query' => $rewrittenQuery,
|
||||
'alternate_queries' => $alternateQueries,
|
||||
'domains' => $domains,
|
||||
'preferred_domain' => $preferredDomain,
|
||||
'entities' => $entities,
|
||||
'date_range' => $dateRange,
|
||||
'needs_realtime' => $needsRealtime,
|
||||
'answer_format' => $answerFormat === 'markdown' ? 'markdown' : 'markdown',
|
||||
'confidence' => $confidence,
|
||||
];
|
||||
}
|
||||
|
||||
private static function normalizeDateRange(array $dateRange, string $message): array
|
||||
{
|
||||
$preset = trim((string) ($dateRange['preset'] ?? ''));
|
||||
if (!in_array($preset, self::DATE_PRESETS, true)) {
|
||||
$preset = (string) (self::inferDateRange($message)['preset'] ?? '');
|
||||
}
|
||||
|
||||
$date = self::normalizeDateText((string) ($dateRange['date'] ?? ''));
|
||||
$startDate = self::normalizeDateText((string) ($dateRange['start_date'] ?? ''));
|
||||
$endDate = self::normalizeDateText((string) ($dateRange['end_date'] ?? ''));
|
||||
|
||||
if ($preset === '' && $date === '' && $startDate === '' && $endDate === '') {
|
||||
return self::inferDateRange($message);
|
||||
}
|
||||
|
||||
return array_filter([
|
||||
'preset' => $preset,
|
||||
'date' => $date,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
], static fn($value) => $value !== '');
|
||||
}
|
||||
|
||||
private static function normalizeDateText(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function normalizeTextArray($values): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ((array) $values as $value) {
|
||||
$value = trim((string) $value);
|
||||
if ($value !== '') {
|
||||
$rows[] = $value;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($rows));
|
||||
}
|
||||
|
||||
private static function inferIntent(string $message): string
|
||||
{
|
||||
if (preg_match('/BTC|ETH|BNB|SOL|XRP|DOGE|TON|TRX|ADA|AVAX|比特币|以太坊|行情/u', $message)) {
|
||||
return 'crypto_quote';
|
||||
}
|
||||
if (preg_match('/赛程|今天|今日|明天|下一场|下场|即将|未开赛/u', $message)) {
|
||||
return 'match_schedule';
|
||||
}
|
||||
if (preg_match('/结果|比分|赛果|战绩|上一场|上场|最近比赛/u', $message)) {
|
||||
return 'match_result';
|
||||
}
|
||||
if (preg_match('/六合|彩票|号码|特码|开奖结果|开奖记录/u', $message)) {
|
||||
return 'lottery_analysis';
|
||||
}
|
||||
if (preg_match('/资讯|新闻|文章|帖子|社区/u', $message)) {
|
||||
return 'news_search';
|
||||
}
|
||||
return 'general_search';
|
||||
}
|
||||
|
||||
private static function inferDomains(string $message): array
|
||||
{
|
||||
return match (self::inferIntent($message)) {
|
||||
'crypto_quote' => ['crypto', 'article'],
|
||||
'match_schedule', 'match_result' => ['match', 'article', 'post'],
|
||||
'lottery_analysis' => ['lottery', 'post'],
|
||||
'news_search' => ['article', 'post'],
|
||||
default => self::DEFAULT_DOMAINS,
|
||||
};
|
||||
}
|
||||
|
||||
private static function inferPreferredDomain(string $message): string
|
||||
{
|
||||
return match (self::inferIntent($message)) {
|
||||
'crypto_quote' => 'crypto',
|
||||
'match_schedule', 'match_result' => 'match',
|
||||
'lottery_analysis' => 'lottery',
|
||||
'news_search' => 'article',
|
||||
default => 'article',
|
||||
};
|
||||
}
|
||||
|
||||
private static function inferEntities(string $message): array
|
||||
{
|
||||
$keywords = preg_split('/[\s,,。!?;、::\/\\\\]+/u', trim($message)) ?: [];
|
||||
$keywords = array_values(array_filter(array_map(static fn($item) => trim((string) $item), $keywords), static fn($item) => mb_strlen($item) >= 2));
|
||||
|
||||
$leagues = [];
|
||||
foreach (['世界杯', 'NBA', '英超', '西甲', '欧冠', '中超'] as $league) {
|
||||
if (mb_strpos($message, $league) !== false) {
|
||||
$leagues[] = $league;
|
||||
}
|
||||
}
|
||||
|
||||
$symbols = [];
|
||||
foreach (self::CRYPTO_SYMBOLS as $symbol) {
|
||||
if (preg_match('/\b' . preg_quote($symbol, '/') . '\b/i', $message)) {
|
||||
$symbols[] = $symbol;
|
||||
}
|
||||
}
|
||||
|
||||
$teams = [];
|
||||
foreach ($keywords as $keyword) {
|
||||
if (preg_match('/队|联|赛|资讯|分析|行情/u', $keyword)) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($keyword, $leagues, true) && !in_array(strtoupper($keyword), $symbols, true)) {
|
||||
$teams[] = $keyword;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'teams' => array_values(array_unique($teams)),
|
||||
'leagues' => array_values(array_unique($leagues)),
|
||||
'symbols' => array_values(array_unique($symbols)),
|
||||
'keywords' => array_values(array_unique($keywords)),
|
||||
];
|
||||
}
|
||||
|
||||
private static function inferDateRange(string $message): array
|
||||
{
|
||||
if (preg_match('/今天|今日/u', $message)) {
|
||||
return ['preset' => 'today', 'date' => date('Y-m-d')];
|
||||
}
|
||||
if (preg_match('/明天/u', $message)) {
|
||||
return ['preset' => 'tomorrow', 'date' => date('Y-m-d', strtotime('+1 day'))];
|
||||
}
|
||||
if (preg_match('/昨天|昨日/u', $message)) {
|
||||
return ['preset' => 'yesterday', 'date' => date('Y-m-d', strtotime('-1 day'))];
|
||||
}
|
||||
if (preg_match('/最新|最近/u', $message)) {
|
||||
return ['preset' => 'latest'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function inferNeedsRealtime(string $message): bool
|
||||
{
|
||||
return in_array(self::inferIntent($message), ['crypto_quote', 'match_schedule', 'match_result'], true);
|
||||
}
|
||||
|
||||
private static function buildAlternateQueries(string $message): array
|
||||
{
|
||||
$queries = [];
|
||||
if (preg_match('/今天|今日/u', $message)) {
|
||||
$queries[] = str_replace(['今天', '今日'], date('Y-m-d'), $message);
|
||||
}
|
||||
if (preg_match('/世界杯/u', $message)) {
|
||||
$queries[] = '世界杯 今日赛程';
|
||||
}
|
||||
if (preg_match('/BTC|比特币/u', $message)) {
|
||||
$queries[] = 'BTC 实时行情';
|
||||
}
|
||||
return array_values(array_unique(array_filter($queries)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\model\ai\AiConfig;
|
||||
|
||||
class AiAssistantRanker
|
||||
{
|
||||
public static function rank(AiAssistantTrace $trace): AiAssistantTrace
|
||||
{
|
||||
$knowledgeHits = $trace->retrievals['knowledge_hits'] ?? [];
|
||||
$preferredDomain = (string) ($trace->plan['preferred_domain'] ?? '');
|
||||
|
||||
usort($knowledgeHits, static function (array $left, array $right) use ($preferredDomain) {
|
||||
$leftScore = (float) ($left['score'] ?? 0);
|
||||
$rightScore = (float) ($right['score'] ?? 0);
|
||||
|
||||
if ($preferredDomain !== '') {
|
||||
if (($left['domain'] ?? '') === $preferredDomain) {
|
||||
$leftScore += 0.05;
|
||||
}
|
||||
if (($right['domain'] ?? '') === $preferredDomain) {
|
||||
$rightScore += 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
return $rightScore <=> $leftScore;
|
||||
});
|
||||
|
||||
$limit = max(1, min(12, (int) AiConfig::getVal('assistant_kb_topk', '6')));
|
||||
$trace->rankedHits = array_slice($knowledgeHits, 0, $limit);
|
||||
|
||||
$payload = AiAssistantContextService::composePayload(
|
||||
$trace->rankedHits,
|
||||
$trace->retrievals['match_items'] ?? [],
|
||||
$trace->retrievals['crypto_items'] ?? [],
|
||||
$trace->plan
|
||||
);
|
||||
|
||||
$trace->finalSources = $payload['sources'] ?? [];
|
||||
$trace->contextText = (string) ($payload['context_text'] ?? '{}');
|
||||
$trace->hasContext = !empty($payload['has_context']);
|
||||
|
||||
return $trace;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\model\ai\AiConfig;
|
||||
|
||||
class AiAssistantResponder
|
||||
{
|
||||
public static function respond(AiAssistantTrace $trace): AiAssistantTrace
|
||||
{
|
||||
$messages = self::buildModelMessages($trace->historyMessages, $trace->originalMessage, $trace->plan, $trace->contextText);
|
||||
$result = self::callAssistantModel($messages);
|
||||
|
||||
$trace->responseResult = $result;
|
||||
$trace->finalAnswer = !empty($result['success'])
|
||||
? trim((string) ($result['content'] ?? ''))
|
||||
: 'AI助手暂时没有生成成功,请稍后再试。';
|
||||
|
||||
if ($trace->finalAnswer === '') {
|
||||
$trace->finalAnswer = '我暂时没有找到足够的站内依据,请换个关键词再问一次。';
|
||||
}
|
||||
|
||||
return $trace;
|
||||
}
|
||||
|
||||
private static function buildModelMessages(array $history, string $message, array $plan, string $contextText): array
|
||||
{
|
||||
$messages = [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => AiConfig::getVal('assistant_system_prompt', self::defaultSystemPrompt()),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($history as $row) {
|
||||
$role = $row['role'] === 'assistant' ? 'assistant' : 'user';
|
||||
$content = trim((string) ($row['content'] ?? ''));
|
||||
if ($content !== '') {
|
||||
$messages[] = ['role' => $role, 'content' => mb_substr($content, 0, 1000)];
|
||||
}
|
||||
}
|
||||
|
||||
$planSummary = json_encode([
|
||||
'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,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
|
||||
$messages[] = [
|
||||
'role' => 'user',
|
||||
'content' => "用户问题:\n{$message}\n\n查询理解结果:\n"
|
||||
. $planSummary
|
||||
. "\n\n站内资料与实时上下文:\n"
|
||||
. ($contextText ?: '{}')
|
||||
. "\n\n请按照“直接结论、依据摘要、相关内容、风险提示”的结构回答。"
|
||||
. "如果站内资料不足,请明确说明,不要编造。彩票、赛事预测、加密行情必须提示不构成投资或购彩建议。",
|
||||
];
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private static function callAssistantModel(array $messages): array
|
||||
{
|
||||
$client = new DeepSeekClient();
|
||||
$optionsList = [self::defaultModelOptions()];
|
||||
|
||||
$failedErrors = [];
|
||||
$lastResult = ['success' => false, 'error' => 'AI助手模型服务暂时不可用', 'tokens' => 0];
|
||||
foreach ($optionsList as $options) {
|
||||
$result = $client->chatMessages($messages, $options);
|
||||
if (!empty($result['success'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$lastResult = $result;
|
||||
$provider = (string) ($result['provider_label'] ?? ($options['provider_label'] ?? 'AI模型'));
|
||||
$failedErrors[] = $provider . ': ' . (string) ($result['error'] ?? '调用失败');
|
||||
}
|
||||
|
||||
if (!empty($failedErrors)) {
|
||||
$lastResult['error'] = implode(' | ', $failedErrors);
|
||||
}
|
||||
return $lastResult;
|
||||
}
|
||||
|
||||
private static function defaultModelOptions(): array
|
||||
{
|
||||
return DeepSeekClient::defaultModelOptions([
|
||||
'max_tokens' => 1600,
|
||||
'temperature' => 0.45,
|
||||
'retry_times' => 3,
|
||||
'retry_sleep_ms' => 1200,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function defaultSystemPrompt(): string
|
||||
{
|
||||
return '你是世博头条 AI 助手,面向普通用户回答站内体育资讯、赛事、社区、彩票和加密行情相关问题。'
|
||||
. '必须优先依据提供的站内资料和实时上下文回答;资料不足时要直接说明不足并建议用户换关键词。'
|
||||
. '不要声称可以访问后台、源码、服务器或未提供的内部数据。'
|
||||
. '回答使用中文,结构清晰,语气专业克制。涉及赛事预测、彩票或加密行情时,必须说明仅供参考,不构成投资或购彩建议。';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
class AiAssistantRetriever
|
||||
{
|
||||
public static function retrieve(AiAssistantTrace $trace, int $userId, int $sessionId): AiAssistantTrace
|
||||
{
|
||||
$context = AiAssistantContextService::build($trace->originalMessage, $userId, $sessionId, $trace->plan);
|
||||
|
||||
$trace->retrievals = [
|
||||
'knowledge_hits' => $context['knowledge_hits'] ?? [],
|
||||
'match_items' => $context['match_items'] ?? [],
|
||||
'crypto_items' => $context['crypto_items'] ?? [],
|
||||
];
|
||||
$trace->hasContext = !empty($context['has_context']);
|
||||
|
||||
return $trace;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ class AiAssistantService
|
||||
}
|
||||
|
||||
$history = self::historyMessages((int) $session->id);
|
||||
$context = AiAssistantContextService::build($message, $userId, (int) $session->id);
|
||||
|
||||
AiChatMessage::create([
|
||||
'session_id' => (int) $session->id,
|
||||
@@ -45,24 +44,29 @@ class AiAssistantService
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
$messages = self::buildModelMessages($history, $message, $context);
|
||||
$result = self::callAssistantModel($messages);
|
||||
$orchestrated = AiAssistantOrchestrator::run($message, $history, $userId, (int) $session->id);
|
||||
$result = is_array($orchestrated['result'] ?? null)
|
||||
? $orchestrated['result']
|
||||
: ['success' => false, 'error' => 'AI助手模型服务暂时不可用', 'tokens' => 0];
|
||||
|
||||
$status = !empty($result['success']) ? AiChatMessage::STATUS_SUCCESS : AiChatMessage::STATUS_FAILED;
|
||||
$answer = !empty($result['success'])
|
||||
? trim((string) ($result['content'] ?? ''))
|
||||
: 'AI助手暂时没有生成成功,请稍后再试。';
|
||||
$answer = trim((string) ($orchestrated['answer'] ?? ($result['content'] ?? '')));
|
||||
if ($answer === '' && empty($result['success'])) {
|
||||
$answer = 'AI助手暂时没有生成成功,请稍后再试。';
|
||||
}
|
||||
if ($answer === '') {
|
||||
$answer = '我暂时没有找到足够的站内依据,请换个关键词再问一次。';
|
||||
}
|
||||
|
||||
$sources = is_array($orchestrated['sources'] ?? null) ? $orchestrated['sources'] : [];
|
||||
|
||||
$assistantMessage = AiChatMessage::create([
|
||||
'session_id' => (int) $session->id,
|
||||
'user_id' => $userId,
|
||||
'client_id' => $clientId,
|
||||
'role' => AiChatMessage::ROLE_ASSISTANT,
|
||||
'content' => $answer,
|
||||
'sources_json' => json_encode($context['sources'] ?? [], JSON_UNESCAPED_UNICODE),
|
||||
'sources_json' => json_encode($sources, JSON_UNESCAPED_UNICODE),
|
||||
'model' => (string) ($result['model'] ?? self::defaultModelOptions()['model']),
|
||||
'tokens_used' => (int) ($result['tokens'] ?? 0),
|
||||
'cost_ms' => (int) ($result['cost_ms'] ?? 0),
|
||||
@@ -85,7 +89,7 @@ class AiAssistantService
|
||||
'data' => [
|
||||
'session' => self::formatSession($session->toArray()),
|
||||
'message' => self::formatMessage($assistantMessage->toArray()),
|
||||
'sources' => $context['sources'] ?? [],
|
||||
'sources' => $sources,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
class AiAssistantTrace
|
||||
{
|
||||
public string $originalMessage = '';
|
||||
public array $historyMessages = [];
|
||||
public array $plan = [];
|
||||
public array $retrievals = [
|
||||
'knowledge_hits' => [],
|
||||
'match_items' => [],
|
||||
'crypto_items' => [],
|
||||
];
|
||||
public array $rankedHits = [];
|
||||
public array $finalSources = [];
|
||||
public string $contextText = '{}';
|
||||
public string $finalAnswer = '';
|
||||
public array $responseResult = [];
|
||||
public bool $hasContext = false;
|
||||
|
||||
public function __construct(string $originalMessage, array $historyMessages = [])
|
||||
{
|
||||
$this->originalMessage = $originalMessage;
|
||||
$this->historyMessages = $historyMessages;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'original_message' => $this->originalMessage,
|
||||
'history_messages' => $this->historyMessages,
|
||||
'plan' => $this->plan,
|
||||
'retrievals' => $this->retrievals,
|
||||
'ranked_hits' => $this->rankedHits,
|
||||
'final_sources' => $this->finalSources,
|
||||
'context_text' => $this->contextText,
|
||||
'final_answer' => $this->finalAnswer,
|
||||
'response_result' => $this->responseResult,
|
||||
'has_context' => $this->hasContext,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user