mirror of
https://github.com/langgenius/dify.git
synced 2026-09-02 22:36:15 +08:00
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
import {
|
|
type KnowledgeSpaceEmbeddingProfile as CoreKnowledgeSpaceEmbeddingProfile,
|
|
KnowledgeSpaceEmbeddingProfileSchema,
|
|
} from "@knowledge/core";
|
|
import type { EmbeddingProvider } from "@knowledge/embeddings";
|
|
|
|
import {
|
|
type KnowledgeSpaceManifestRepository,
|
|
observeKnowledgeSpaceEmbeddingDimension,
|
|
} from "./knowledge-space-manifest-repository";
|
|
|
|
/**
|
|
* Stable identity of a knowledge space's text-embedding vector space.
|
|
*
|
|
* `model` is the Dify-managed invocation model. `vectorSpaceId` is the immutable identity stored
|
|
* on projections and used by retrieval. A runtime response must echo the configured model exactly;
|
|
* accepting an unpersisted alias could silently change the vector semantics behind this identity.
|
|
*/
|
|
export type KnowledgeSpaceEmbeddingProfile = CoreKnowledgeSpaceEmbeddingProfile;
|
|
|
|
export interface ResolveKnowledgeSpaceEmbeddingInput {
|
|
/** Frozen active profile captured with the publication head; skips a mutable manifest reread. */
|
|
readonly profile?: KnowledgeSpaceEmbeddingProfile | undefined;
|
|
readonly knowledgeSpaceId: string;
|
|
readonly tenantId: string;
|
|
}
|
|
|
|
export interface ResolvedKnowledgeSpaceEmbedding extends KnowledgeSpaceEmbeddingProfile {
|
|
readonly providerInstance: EmbeddingProvider;
|
|
}
|
|
|
|
export interface KnowledgeSpaceEmbeddingResolver {
|
|
observeDimension?(input: ObserveResolvedKnowledgeSpaceEmbeddingDimensionInput): Promise<void>;
|
|
resolve(
|
|
input: ResolveKnowledgeSpaceEmbeddingInput,
|
|
): Promise<ResolvedKnowledgeSpaceEmbedding | null>;
|
|
}
|
|
|
|
export interface ObserveResolvedKnowledgeSpaceEmbeddingDimensionInput
|
|
extends ResolveKnowledgeSpaceEmbeddingInput {
|
|
readonly dimension: number;
|
|
readonly revision: number;
|
|
readonly vectorSpaceId: string;
|
|
}
|
|
|
|
export type KnowledgeSpaceEmbeddingProviderFactory = (
|
|
profile: KnowledgeSpaceEmbeddingProfile,
|
|
) => EmbeddingProvider;
|
|
|
|
export interface KnowledgeSpaceEmbeddingResolverOptions {
|
|
/** Optional deployment profile used only by legacy manifests without an embedding profile. */
|
|
readonly fallback?:
|
|
| {
|
|
readonly profile: KnowledgeSpaceEmbeddingProfile;
|
|
readonly provider: EmbeddingProvider;
|
|
}
|
|
| undefined;
|
|
readonly manifests: KnowledgeSpaceManifestRepository;
|
|
/** Bounded LRU size for provider instances keyed by immutable embedding profiles. */
|
|
readonly maxCachedProviders?: number | undefined;
|
|
readonly now?: (() => string) | undefined;
|
|
readonly providerFactory: KnowledgeSpaceEmbeddingProviderFactory;
|
|
}
|
|
|
|
export class KnowledgeSpaceEmbeddingProfileNotFoundError extends Error {
|
|
constructor(knowledgeSpaceId: string) {
|
|
super(`Embedding profile is not configured for knowledge space ${knowledgeSpaceId}`);
|
|
}
|
|
}
|
|
|
|
export class KnowledgeSpaceEmbeddingProfileChangedError extends Error {
|
|
constructor(vectorSpaceId: string) {
|
|
super(`Embedding profile changed while using vector space ${vectorSpaceId}`);
|
|
}
|
|
}
|
|
|
|
export class InvalidKnowledgeSpaceEmbeddingProfileError extends Error {}
|
|
|
|
/**
|
|
* Resolves the active profile in tenant + knowledge-space scope and reuses provider instances only
|
|
* for byte-for-byte identical profiles. Credentials are intentionally absent from the persisted
|
|
* profile and remain provider-factory/runtime concerns.
|
|
*/
|
|
export function createKnowledgeSpaceEmbeddingResolver({
|
|
fallback,
|
|
manifests,
|
|
maxCachedProviders = 256,
|
|
now = () => new Date().toISOString(),
|
|
providerFactory,
|
|
}: KnowledgeSpaceEmbeddingResolverOptions): KnowledgeSpaceEmbeddingResolver {
|
|
if (!Number.isSafeInteger(maxCachedProviders) || maxCachedProviders < 1) {
|
|
throw new Error("Knowledge space embedding resolver maxCachedProviders must be at least 1");
|
|
}
|
|
|
|
const providerCache = new Map<string, EmbeddingProvider>();
|
|
const parsedFallback = fallback
|
|
? {
|
|
profile: validateKnowledgeSpaceEmbeddingProfile(fallback.profile),
|
|
provider: fallback.provider,
|
|
}
|
|
: undefined;
|
|
|
|
return {
|
|
observeDimension: async ({
|
|
dimension,
|
|
knowledgeSpaceId,
|
|
revision,
|
|
tenantId,
|
|
vectorSpaceId,
|
|
}) => {
|
|
const normalizedKnowledgeSpaceId = requiredText(knowledgeSpaceId, "knowledgeSpaceId");
|
|
const normalizedTenantId = requiredText(tenantId, "tenantId");
|
|
const observed = await observeKnowledgeSpaceEmbeddingDimension(manifests, {
|
|
dimension,
|
|
expectedRevision: revision,
|
|
expectedVectorSpaceId: vectorSpaceId,
|
|
knowledgeSpaceId: normalizedKnowledgeSpaceId,
|
|
now,
|
|
tenantId: normalizedTenantId,
|
|
});
|
|
|
|
if (observed) {
|
|
return;
|
|
}
|
|
|
|
// Legacy manifests intentionally keep using the raw historical model as vectorSpaceId until
|
|
// an explicit rebuild publishes a canonical profile. There is no persisted profile to update.
|
|
const current = await manifests.get({
|
|
knowledgeSpaceId: normalizedKnowledgeSpaceId,
|
|
tenantId: normalizedTenantId,
|
|
});
|
|
if (
|
|
!current?.embeddingProfile &&
|
|
parsedFallback?.profile.vectorSpaceId === vectorSpaceId &&
|
|
parsedFallback.profile.revision === revision
|
|
) {
|
|
assertObservedEmbeddingDimension({
|
|
observedDimension: dimension,
|
|
profile: parsedFallback.profile,
|
|
});
|
|
return;
|
|
}
|
|
|
|
throw new KnowledgeSpaceEmbeddingProfileChangedError(vectorSpaceId);
|
|
},
|
|
resolve: async ({ knowledgeSpaceId, profile: frozenProfile, tenantId }) => {
|
|
const normalizedKnowledgeSpaceId = requiredText(knowledgeSpaceId, "knowledgeSpaceId");
|
|
const normalizedTenantId = requiredText(tenantId, "tenantId");
|
|
const manifest = frozenProfile
|
|
? undefined
|
|
: await manifests.get({
|
|
knowledgeSpaceId: normalizedKnowledgeSpaceId,
|
|
tenantId: normalizedTenantId,
|
|
});
|
|
|
|
if (!manifest && !frozenProfile) {
|
|
if (parsedFallback) {
|
|
return {
|
|
...parsedFallback.profile,
|
|
providerInstance: parsedFallback.provider,
|
|
};
|
|
}
|
|
|
|
throw new KnowledgeSpaceEmbeddingProfileNotFoundError(normalizedKnowledgeSpaceId);
|
|
}
|
|
|
|
const rawProfile =
|
|
frozenProfile ??
|
|
(manifest as unknown as { readonly embeddingProfile?: unknown }).embeddingProfile;
|
|
if (rawProfile === undefined) {
|
|
return parsedFallback
|
|
? {
|
|
...parsedFallback.profile,
|
|
providerInstance: parsedFallback.provider,
|
|
}
|
|
: null;
|
|
}
|
|
|
|
// Persisted profiles have already crossed a trust boundary, so require the core schema's
|
|
// canonical SHA-256 vector-space id. The looser parser is reserved for the internal legacy
|
|
// fallback whose historical projection key is the raw model name.
|
|
const profile = KnowledgeSpaceEmbeddingProfileSchema.parse(rawProfile);
|
|
const cacheKey = embeddingProviderCacheKey(profile);
|
|
const cachedProvider = providerCache.get(cacheKey);
|
|
const provider = cachedProvider ?? providerFactory(profile);
|
|
|
|
// Map insertion order provides a dependency-free LRU. Eviction only drops reusable runtime
|
|
// state; routing credentials remain daemon-side and are never part of this cache key.
|
|
if (cachedProvider) {
|
|
providerCache.delete(cacheKey);
|
|
} else if (providerCache.size >= maxCachedProviders) {
|
|
const leastRecentlyUsed = providerCache.keys().next().value;
|
|
if (leastRecentlyUsed !== undefined) {
|
|
providerCache.delete(leastRecentlyUsed);
|
|
}
|
|
}
|
|
providerCache.set(cacheKey, provider);
|
|
|
|
return { ...profile, providerInstance: provider };
|
|
},
|
|
};
|
|
}
|
|
|
|
export function validateKnowledgeSpaceEmbeddingProfile(
|
|
profile: KnowledgeSpaceEmbeddingProfile,
|
|
): KnowledgeSpaceEmbeddingProfile {
|
|
return parseKnowledgeSpaceEmbeddingProfile(profile);
|
|
}
|
|
|
|
export function assertObservedEmbeddingDimension({
|
|
observedDimension,
|
|
profile,
|
|
}: {
|
|
readonly observedDimension: number;
|
|
readonly profile: Pick<KnowledgeSpaceEmbeddingProfile, "dimension" | "vectorSpaceId">;
|
|
}): void {
|
|
if (!Number.isSafeInteger(observedDimension) || observedDimension < 1) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(
|
|
`Embedding response for vector space ${profile.vectorSpaceId} has invalid dimension=${observedDimension}`,
|
|
);
|
|
}
|
|
|
|
if (profile.dimension !== undefined && profile.dimension !== observedDimension) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(
|
|
`Embedding response for vector space ${profile.vectorSpaceId} has dimension=${observedDimension}; ` +
|
|
`expected observed dimension=${profile.dimension}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertEmbeddingModelMatchesProfile({
|
|
observedModel,
|
|
profile,
|
|
}: {
|
|
readonly observedModel: string;
|
|
readonly profile: Pick<KnowledgeSpaceEmbeddingProfile, "model" | "vectorSpaceId">;
|
|
}): void {
|
|
const normalizedObservedModel = observedModel.trim();
|
|
if (!normalizedObservedModel) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(
|
|
`Embedding response for vector space ${profile.vectorSpaceId} has an empty model`,
|
|
);
|
|
}
|
|
|
|
if (normalizedObservedModel !== profile.model) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(
|
|
`Embedding response model ${normalizedObservedModel} does not match configured model ` +
|
|
`${profile.model} for vector space ${profile.vectorSpaceId}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function parseKnowledgeSpaceEmbeddingProfile(value: unknown): KnowledgeSpaceEmbeddingProfile {
|
|
if (!isRecord(value)) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError("Embedding profile must be an object");
|
|
}
|
|
|
|
const revisionValue = value.revision ?? value.version;
|
|
const revision =
|
|
typeof revisionValue === "number" && Number.isSafeInteger(revisionValue) && revisionValue > 0
|
|
? revisionValue
|
|
: undefined;
|
|
if (revision === undefined) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(
|
|
"Embedding profile revision must be a positive integer",
|
|
);
|
|
}
|
|
|
|
const dimensionValue = value.dimension;
|
|
const dimension =
|
|
dimensionValue === undefined
|
|
? undefined
|
|
: typeof dimensionValue === "number" &&
|
|
Number.isSafeInteger(dimensionValue) &&
|
|
dimensionValue > 0
|
|
? dimensionValue
|
|
: null;
|
|
if (dimension === null) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(
|
|
"Embedding profile dimension must be a positive integer when present",
|
|
);
|
|
}
|
|
|
|
return {
|
|
...(dimension === undefined ? {} : { dimension }),
|
|
model: requiredProfileText(value.model, "model"),
|
|
pluginId: requiredProfileText(value.pluginId, "pluginId"),
|
|
provider: requiredProfileText(value.provider, "provider"),
|
|
revision,
|
|
vectorSpaceId: requiredProfileText(value.vectorSpaceId, "vectorSpaceId"),
|
|
};
|
|
}
|
|
|
|
function requiredProfileText(value: unknown, name: string): string {
|
|
if (typeof value !== "string" || !value.trim()) {
|
|
throw new InvalidKnowledgeSpaceEmbeddingProfileError(`Embedding profile ${name} is required`);
|
|
}
|
|
|
|
return value.trim();
|
|
}
|
|
|
|
function requiredText(value: string, name: string): string {
|
|
const normalized = value.trim();
|
|
if (!normalized) {
|
|
throw new Error(`Knowledge space embedding resolver ${name} is required`);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function embeddingProviderCacheKey(profile: KnowledgeSpaceEmbeddingProfile): string {
|
|
return JSON.stringify([
|
|
profile.pluginId,
|
|
profile.provider,
|
|
profile.model,
|
|
profile.vectorSpaceId,
|
|
profile.revision,
|
|
]);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|