UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

181 lines • 7.25 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { mountPrefixOf } from "../../utils/key_prefix.js"; import { formatScore } from "../../utils/score.js"; import { rstripSlash } from "../../utils/slash.js"; import { compareCodePoints } from "../../utils/sort.js"; import { floatOption, intOption, textOption, validateOptions } from "../../vfs/search.js"; import { normalizeSlug, scalarString } from "../slug_tree/rows.js"; import { searchScope, targetEntries, validateQuery } from "../slug_tree/search.js"; import { difyPost } from "./client.js"; import { segmentText } from "./read.js"; import { DIFY_TREE, SLUG_NOUN } from "./tree.js"; export const METHODS = { semantic: 'semantic_search', fulltext: 'full_text_search', hybrid: 'hybrid_search', keyword: 'keyword_search', }; export async function searchSegments(accessor, query, paths, options = {}) { const method = options.method ?? 'semantic'; const topK = options.topK ?? 10; const threshold = options.threshold ?? 0; const searchMethod = validateArgs(query, method, topK, threshold); let mountPrefix = options.mountPrefix ?? ''; if (mountPrefix === '' && paths.length > 0 && paths[0] !== undefined) { mountPrefix = mountPrefixOf(paths[0].virtual, paths[0].vfsPath); } const retrievalModel = { search_method: searchMethod, top_k: Math.min(topK, 100), score_threshold_enabled: threshold > 0, score_threshold: threshold, reranking_enable: false, }; if (paths.length > 0) { const conditions = await metadataConditions(accessor, paths); if (conditions.length === 0) return []; retrievalModel.metadata_filtering_conditions = { logical_operator: 'or', conditions }; } const response = await difyPost(accessor, `/datasets/${accessor.config.datasetId}/retrieve`, { query, retrieval_model: retrievalModel, }); return renderRecords(responseRecords(response.records), accessor.config.slugMetadataName, mountPrefix); } function validateArgs(query, method, topK, threshold) { validateQuery(query, topK); if (threshold < 0 || threshold > 1) throw new Error('threshold must be in [0, 1]'); const searchMethod = Object.hasOwn(METHODS, method) ? METHODS[method] : undefined; if (searchMethod === undefined) { throw new Error('method must be one of semantic, fulltext, hybrid, keyword'); } return searchMethod; } async function metadataConditions(accessor, paths) { const targets = await targetEntries(DIFY_TREE, accessor, paths); const slugValues = []; const nameValues = []; for (const entry of targets.values()) { if (entry.extra.has_slug === true) { slugValues.push(String(entry.extra.raw_slug)); } else { nameValues.push(entry.name); } } const conditions = []; if (slugValues.length > 0) { conditions.push({ name: accessor.config.slugMetadataName, comparison_operator: 'in', value: [...slugValues].sort(compareCodePoints), }); } if (nameValues.length > 0) { conditions.push({ name: 'document_name', comparison_operator: 'in', value: [...nameValues].sort(compareCodePoints), }); } return conditions; } function responseRecords(value) { if (!Array.isArray(value)) { throw new Error('Dify search response records must be a list'); } const records = []; for (const record of value) { if (record === null || typeof record !== 'object' || Array.isArray(record)) { throw new Error('Dify search response records must be objects'); } records.push(record); } return records; } function renderRecords(records, slugMetadataName, mountPrefix) { const contents = []; for (const record of records) { const segment = record.segment; if (segment === null || typeof segment !== 'object' || Array.isArray(segment)) continue; const path = recordPath(record, slugMetadataName, mountPrefix); if (path === null) continue; const score = formatScore(record.score); const header = score === null ? path : `${path}:${score}`; contents.push([path, `${header}\n${segmentText(segment)}`]); } return contents; } function recordPath(record, slugMetadataName, mountPrefix) { const segment = record.segment; if (segment === null || typeof segment !== 'object' || Array.isArray(segment)) return null; const document = segment.document; if (document === null || typeof document !== 'object' || Array.isArray(document)) return null; const rawPath = documentPath(document, slugMetadataName); if (rawPath === null) return null; let normalized; try { normalized = normalizeSlug(rawPath, SLUG_NOUN); } catch (err) { console.debug(`Skipping Dify record with invalid slug/name: ${rawPath}: ${String(err)}`); return null; } const prefix = rstripSlash(mountPrefix); return prefix !== '' ? prefix + normalized : normalized; } function documentPath(document, slugMetadataName) { const metadata = document.doc_metadata; if (Array.isArray(metadata)) { for (const item of metadata) { if (item !== null && typeof item === 'object' && !Array.isArray(item)) { const record = item; if (record.name === slugMetadataName) { const value = scalarString(record.value); if (value !== null) return value; } } } } if (metadata !== null && typeof metadata === 'object' && !Array.isArray(metadata)) { const value = scalarString(metadata[slugMetadataName]); if (value !== null) return value; } return scalarString(document.name); } export async function searchMany(accessor, paths, query) { validateOptions(query, ['top_k', 'threshold', 'method']); const topK = intOption(query, 'top_k', 10); const method = textOption(query, 'method', 'semantic'); const threshold = floatOption(query, 'threshold', 0); validateArgs(query.query, method, topK, threshold); const [targets, prefix] = await searchScope(DIFY_TREE, accessor, paths); return searchSegments(accessor, query.query, targets, { method, topK, threshold, mountPrefix: prefix, }); } //# sourceMappingURL=search.js.map