@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
181 lines • 7.25 kB
JavaScript
// ========= 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