snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
434
apps/meteor/server/services/ai-search/service.ts
Normal file
434
apps/meteor/server/services/ai-search/service.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
import {
|
||||
AI_LICENSE_MODULE,
|
||||
AI_SEARCH_PAGE_SIZE,
|
||||
buildIntelligentSearchPipelineFilters,
|
||||
generateOpenAICompatibleSearchAnswer,
|
||||
listOpenAICompatibleModels,
|
||||
MAX_SEARCH_ANSWER_MESSAGES,
|
||||
MAX_SEARCH_ANSWER_TEXT_LENGTH,
|
||||
MAX_SEARCH_FILTER_VALUES,
|
||||
normalizeIntelligentSearchCandidates,
|
||||
searchIntelligentPipeline,
|
||||
type IntelligentSearchFilters,
|
||||
type IntelligentSearchPipelineConfig,
|
||||
type OpenAICompatibleProviderConfig,
|
||||
type SearchAnswerMessage,
|
||||
} from '@rocket.chat/ai-search';
|
||||
import type {
|
||||
AISearchAnswerMessage,
|
||||
AISearchAnswerResult,
|
||||
AISearchFilters,
|
||||
AISearchModelOption,
|
||||
AISearchResult,
|
||||
AISearchStatus,
|
||||
IAISearchService,
|
||||
} from '@rocket.chat/core-services';
|
||||
import { License, ServiceClass } from '@rocket.chat/core-services';
|
||||
import { isBannedSubscription, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Messages, Rooms, Subscriptions, Users } from '@rocket.chat/models';
|
||||
import { serverFetch, type ExtendedFetchOptions } from '@rocket.chat/server-fetch';
|
||||
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const logger = new Logger('AISearchService');
|
||||
|
||||
const fetchWithSsrfValidation = (url: string, options: Omit<ExtendedFetchOptions, 'allowList' | 'ignoreSsrfValidation'>) =>
|
||||
serverFetch(url, {
|
||||
...options,
|
||||
ignoreSsrfValidation: false,
|
||||
allowList: settings.get<string>('SSRF_Allowlist'),
|
||||
});
|
||||
|
||||
const asString = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const toDate = (value?: string | Date): Date | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const limitFilterValues = (values: string[] | undefined): string[] | undefined => {
|
||||
if (!values) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const limitedValues: string[] = [];
|
||||
for (const value of values) {
|
||||
if (value) {
|
||||
limitedValues.push(value);
|
||||
if (limitedValues.length === MAX_SEARCH_FILTER_VALUES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return limitedValues;
|
||||
};
|
||||
|
||||
const normalizeFilters = (filters: AISearchFilters = {}): IntelligentSearchFilters => ({
|
||||
rid: filters.rid,
|
||||
rids: limitFilterValues(filters.rids),
|
||||
roomNames: limitFilterValues(filters.roomNames),
|
||||
fromUsername: filters.fromUsername,
|
||||
fromUsernames: limitFilterValues(filters.fromUsernames),
|
||||
startDate: toDate(filters.startDate),
|
||||
endDate: toDate(filters.endDate),
|
||||
});
|
||||
|
||||
type OpenAICompatibleProviderSettings = Pick<OpenAICompatibleProviderConfig, 'baseUrl' | 'apiKey'> & {
|
||||
model?: string;
|
||||
};
|
||||
|
||||
export class AISearchService extends ServiceClass implements IAISearchService {
|
||||
protected name = 'ai-search';
|
||||
|
||||
private getPipelineConfig(): IntelligentSearchPipelineConfig | undefined {
|
||||
const baseUrl = settings.get<string>('AI_Intelligent_Search_Pipeline_Base_URL');
|
||||
const pipelineId = settings.get<string>('AI_Intelligent_Search_Pipeline_ID');
|
||||
const apiKey = settings.get<string>('AI_Intelligent_Search_API_Key');
|
||||
const apiKeySecret = settings.get<string>('AI_Intelligent_Search_API_Key_Secret');
|
||||
const queryTemplate = settings.get<string>('AI_Intelligent_Search_Query_Template');
|
||||
const minimumSimilarityPercent = settings.get<number>('AI_Intelligent_Search_Min_Similarity_Percent');
|
||||
|
||||
const normalizedBaseUrl = asString(baseUrl);
|
||||
const normalizedPipelineId = asString(pipelineId);
|
||||
const normalizedApiKey = asString(apiKey);
|
||||
const normalizedApiKeySecret = asString(apiKeySecret);
|
||||
|
||||
if (!normalizedBaseUrl || !normalizedPipelineId || !normalizedApiKey || !normalizedApiKeySecret) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: normalizedBaseUrl,
|
||||
pipelineId: normalizedPipelineId,
|
||||
apiKey: normalizedApiKey,
|
||||
apiKeySecret: normalizedApiKeySecret,
|
||||
queryTemplate: asString(queryTemplate),
|
||||
minimumSimilarityPercent: Number(minimumSimilarityPercent || 0),
|
||||
};
|
||||
}
|
||||
|
||||
private getAnswerProviderSettings(): OpenAICompatibleProviderSettings | undefined {
|
||||
const baseUrl = settings.get<string>('AI_LLM_OpenAI_Base_URL');
|
||||
const apiKey = settings.get<string>('AI_LLM_OpenAI_API_Key');
|
||||
const model = settings.get<string>('AI_LLM_OpenAI_Model');
|
||||
|
||||
const normalizedBaseUrl = asString(baseUrl);
|
||||
const normalizedApiKey = asString(apiKey);
|
||||
const normalizedModel = asString(model);
|
||||
|
||||
if (!normalizedBaseUrl || !normalizedApiKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: normalizedBaseUrl,
|
||||
apiKey: normalizedApiKey,
|
||||
model: normalizedModel,
|
||||
};
|
||||
}
|
||||
|
||||
private getAnswerProviderConfig(): OpenAICompatibleProviderConfig | undefined {
|
||||
const provider = this.getAnswerProviderSettings();
|
||||
if (!provider?.model) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'OpenAI compatible',
|
||||
baseUrl: provider.baseUrl,
|
||||
apiKey: provider.apiKey,
|
||||
model: provider.model,
|
||||
};
|
||||
}
|
||||
|
||||
async status(): Promise<AISearchStatus> {
|
||||
const hasIntelligentSearchLicense = await License.hasModule(AI_LICENSE_MODULE);
|
||||
const intelligentSearchEnabled = settings.get<boolean>('AI_Intelligent_Search_Enabled');
|
||||
const answerGenerationEnabled = settings.get<boolean>('AI_Intelligent_Search_Answer_Enabled');
|
||||
const pipelineConfig = this.getPipelineConfig();
|
||||
const answerProviderConfig = this.getAnswerProviderConfig();
|
||||
|
||||
return {
|
||||
hasIntelligentSearchLicense,
|
||||
intelligentSearchEnabled: intelligentSearchEnabled === true,
|
||||
intelligentSearchConfigured: Boolean(pipelineConfig),
|
||||
answerGenerationConfigured: answerGenerationEnabled === true && Boolean(answerProviderConfig) && Boolean(pipelineConfig),
|
||||
};
|
||||
}
|
||||
|
||||
private async getRoomMap(roomIds: string[]): Promise<Map<string, Pick<IRoom, '_id' | 't' | 'name' | 'fname'>>> {
|
||||
if (!roomIds.length) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const rooms = await Rooms.findByIds([...new Set(roomIds)], {
|
||||
projection: { _id: 1, t: 1, name: 1, fname: 1 },
|
||||
}).toArray();
|
||||
|
||||
return new Map(rooms.map((room) => [room._id, room]));
|
||||
}
|
||||
|
||||
private async getUserSubscribedRoomIdsForPipeline(userId: string): Promise<string[]> {
|
||||
const roomIds = await Subscriptions.findByUserId(userId, {
|
||||
projection: { rid: 1, status: 1 },
|
||||
})
|
||||
.map((subscription) => (isBannedSubscription(subscription) ? undefined : subscription.rid))
|
||||
.toArray();
|
||||
|
||||
return roomIds.filter((rid): rid is string => Boolean(rid));
|
||||
}
|
||||
|
||||
private async getSubscribedRoomIdSet(userId: string, roomIds: string[]): Promise<Set<string>> {
|
||||
const uniqueRoomIdSet = new Set<string>();
|
||||
for (const roomId of roomIds) {
|
||||
if (roomId) {
|
||||
uniqueRoomIdSet.add(roomId);
|
||||
}
|
||||
}
|
||||
const uniqueRoomIds = [...uniqueRoomIdSet];
|
||||
if (!uniqueRoomIds.length) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const subscriptions = await Subscriptions.findByUserIdAndRoomIds(userId, uniqueRoomIds, {
|
||||
projection: { rid: 1, status: 1 },
|
||||
}).toArray();
|
||||
|
||||
const subscribedRoomIds = new Set<string>();
|
||||
for (const subscription of subscriptions) {
|
||||
if (!isBannedSubscription(subscription)) {
|
||||
subscribedRoomIds.add(subscription.rid);
|
||||
}
|
||||
}
|
||||
return subscribedRoomIds;
|
||||
}
|
||||
|
||||
private async normalizeIntelligentResults(
|
||||
rawSearchResults: unknown,
|
||||
userId: string,
|
||||
limit = AI_SEARCH_PAGE_SIZE,
|
||||
): Promise<AISearchResult[]> {
|
||||
const candidates = normalizeIntelligentSearchCandidates(rawSearchResults, [], limit, logger);
|
||||
const msgIdSet = new Set<string>();
|
||||
for (const { msgId } of candidates) {
|
||||
if (msgId) {
|
||||
msgIdSet.add(msgId);
|
||||
}
|
||||
}
|
||||
const msgIds = [...msgIdSet];
|
||||
let messageMap = new Map<string, Pick<IMessage, '_id' | 'rid' | 'msg' | 'ts' | 'u'>>();
|
||||
|
||||
if (msgIds.length > 0) {
|
||||
messageMap = new Map(
|
||||
await Messages.findVisibleByIds(msgIds, {
|
||||
projection: { _id: 1, rid: 1, msg: 1, ts: 1, u: 1 },
|
||||
})
|
||||
.map((message) => [String(message._id), message] as const)
|
||||
.toArray(),
|
||||
);
|
||||
logger.debug({ msg: 'AI search messages fetched from DB', requested: msgIds.length, found: messageMap.size });
|
||||
}
|
||||
|
||||
const resultRoomIds = Array.from(messageMap.values(), ({ rid }) => rid);
|
||||
const [rooms, subscribedRoomIds] = await Promise.all([
|
||||
this.getRoomMap(resultRoomIds),
|
||||
this.getSubscribedRoomIdSet(userId, resultRoomIds),
|
||||
]);
|
||||
|
||||
const normalizedResults: AISearchResult[] = [];
|
||||
for (const result of candidates) {
|
||||
// candidates without a visible database message could surface stale pipeline text
|
||||
const dbMessage = result.msgId ? messageMap.get(result.msgId) : undefined;
|
||||
if (!dbMessage) {
|
||||
logger.debug({ msg: 'AI search result filtered: message not visible', msgId: result.msgId });
|
||||
continue;
|
||||
}
|
||||
|
||||
const { rid } = dbMessage;
|
||||
if (!rid || !subscribedRoomIds.has(rid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const room = rooms.get(rid);
|
||||
normalizedResults.push({
|
||||
_id: result.msgId || result._id,
|
||||
rid,
|
||||
msgId: result.msgId,
|
||||
text: dbMessage.msg || '',
|
||||
ts: dbMessage.ts?.toISOString(),
|
||||
u: dbMessage.u ? { username: dbMessage.u.username, name: dbMessage.u.name } : undefined,
|
||||
...(Number.isFinite(result.score) && { score: result.score }),
|
||||
...(room && { room }),
|
||||
});
|
||||
if (normalizedResults.length === limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedResults;
|
||||
}
|
||||
|
||||
private async getUserClassifications(userId: string): Promise<string[]> {
|
||||
const user = await Users.findOneById<Pick<IUser, 'roles'>>(userId, { projection: { roles: 1 } });
|
||||
return Array.from(new Set(['user', ...(user?.roles || [])]));
|
||||
}
|
||||
|
||||
private async getSubscribedRoomIds(userId: string, roomIds: string[]): Promise<string[]> {
|
||||
return [...(await this.getSubscribedRoomIdSet(userId, roomIds))];
|
||||
}
|
||||
|
||||
private async getSubscribedRoomIdsByName(userId: string, roomNames: string[] = []): Promise<string[]> {
|
||||
if (!roomNames.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rooms = await Promise.all(
|
||||
Array.from(new Set(roomNames)).map((roomName) => Rooms.findOneByNameOrFname(roomName, { projection: { _id: 1 } })),
|
||||
);
|
||||
|
||||
const roomIds = rooms.reduce<string[]>((roomIds, room) => {
|
||||
if (typeof room?._id === 'string') {
|
||||
roomIds.push(room._id);
|
||||
}
|
||||
|
||||
return roomIds;
|
||||
}, []);
|
||||
|
||||
return this.getSubscribedRoomIds(userId, roomIds);
|
||||
}
|
||||
|
||||
async search({
|
||||
query,
|
||||
userId,
|
||||
filters: rawFilters,
|
||||
limit = AI_SEARCH_PAGE_SIZE,
|
||||
}: {
|
||||
query: string;
|
||||
userId: string;
|
||||
filters?: AISearchFilters;
|
||||
limit?: number;
|
||||
}): Promise<AISearchResult[]> {
|
||||
const hasIntelligentSearchLicense = await License.hasModule(AI_LICENSE_MODULE);
|
||||
const intelligentSearchEnabled = settings.get<boolean>('AI_Intelligent_Search_Enabled');
|
||||
const config = this.getPipelineConfig();
|
||||
|
||||
if (!hasIntelligentSearchLicense || intelligentSearchEnabled !== true || !config) {
|
||||
logger.debug({
|
||||
msg: 'AI search skipped: unavailable',
|
||||
hasIntelligentSearchLicense,
|
||||
intelligentSearchEnabled: intelligentSearchEnabled === true,
|
||||
intelligentSearchConfigured: Boolean(config),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = normalizeFilters(rawFilters);
|
||||
const requestedRoomIds = [...new Set([...(filters.rids || []), ...(filters.rid ? [filters.rid] : [])])];
|
||||
const roomNameIds = await this.getSubscribedRoomIdsByName(userId, filters.roomNames);
|
||||
const scopedRoomIds = [...new Set([...requestedRoomIds, ...roomNameIds])];
|
||||
|
||||
// a room filter that resolves to no subscribed rooms must not fall through to the unscoped path
|
||||
if (filters.roomNames?.length && !scopedRoomIds.length) {
|
||||
logger.debug({ msg: 'AI search skipped: room name filters did not resolve to subscribed rooms' });
|
||||
return [];
|
||||
}
|
||||
const [pipelineRoomIds, classifications] = await Promise.all([
|
||||
scopedRoomIds.length ? this.getSubscribedRoomIds(userId, scopedRoomIds) : this.getUserSubscribedRoomIdsForPipeline(userId),
|
||||
this.getUserClassifications(userId),
|
||||
]);
|
||||
|
||||
if (!pipelineRoomIds.length) {
|
||||
logger.debug({ msg: 'AI search skipped: user has no room subscriptions' });
|
||||
return [];
|
||||
}
|
||||
|
||||
const pipelineFilters = buildIntelligentSearchPipelineFilters(pipelineRoomIds, {
|
||||
...filters,
|
||||
rids: [...(filters.rids || []), ...roomNameIds],
|
||||
});
|
||||
|
||||
if (!pipelineFilters) {
|
||||
logger.debug({ msg: 'AI search skipped: no subscribed rooms for filters', rid: filters.rid });
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await searchIntelligentPipeline({
|
||||
query,
|
||||
config,
|
||||
classifications,
|
||||
pipelineFilters,
|
||||
limit,
|
||||
fetch: fetchWithSsrfValidation,
|
||||
logger,
|
||||
});
|
||||
|
||||
return this.normalizeIntelligentResults(json, userId, limit);
|
||||
}
|
||||
|
||||
async answer({ query, messages }: { query: string; messages: AISearchAnswerMessage[] }): Promise<AISearchAnswerResult> {
|
||||
const hasIntelligentSearchLicense = await License.hasModule(AI_LICENSE_MODULE);
|
||||
const intelligentSearchEnabled = settings.get<boolean>('AI_Intelligent_Search_Enabled');
|
||||
const answerGenerationEnabled = settings.get<boolean>('AI_Intelligent_Search_Answer_Enabled');
|
||||
const pipelineConfig = this.getPipelineConfig();
|
||||
const provider = this.getAnswerProviderConfig();
|
||||
const systemPromptSetting = settings.get<string>('AI_Intelligent_Search_Answer_System_Prompt');
|
||||
|
||||
if (!hasIntelligentSearchLicense || intelligentSearchEnabled !== true || answerGenerationEnabled !== true || !pipelineConfig) {
|
||||
throw new Error('error-ai-not-enabled');
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('error-ai-provider-not-configured');
|
||||
}
|
||||
|
||||
const systemPrompt = asString(systemPromptSetting);
|
||||
|
||||
const sanitizedMessages: SearchAnswerMessage[] = messages.map(({ text, username, roomName, ts, score }) => ({
|
||||
text,
|
||||
username,
|
||||
roomName,
|
||||
ts,
|
||||
score,
|
||||
}));
|
||||
|
||||
return generateOpenAICompatibleSearchAnswer({
|
||||
query,
|
||||
messages: sanitizedMessages,
|
||||
provider,
|
||||
systemPrompt,
|
||||
fetch: fetchWithSsrfValidation,
|
||||
logger,
|
||||
maxMessages: MAX_SEARCH_ANSWER_MESSAGES,
|
||||
maxTextLength: MAX_SEARCH_ANSWER_TEXT_LENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
async models(): Promise<AISearchModelOption[]> {
|
||||
if (!(await License.hasModule(AI_LICENSE_MODULE))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const provider = this.getAnswerProviderSettings();
|
||||
const selectedModel = settings.get<string>('AI_LLM_OpenAI_Model');
|
||||
|
||||
return listOpenAICompatibleModels({
|
||||
provider,
|
||||
selectedModel: asString(selectedModel),
|
||||
fetch: fetchWithSsrfValidation,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user