/**
* Service Definition for the RAGFlow knowledge-base retrieval capability seam
* (`ctx.ragflow`): the provider registry and provider-selecting execution for
* retrieval. Duplicate ids are rejected. At execution time a configured
* provider must exist and be usable; without one, exactly one usable provider
* is required, so selection never depends on registration order.
*
* This module owns the `ctx.ragflow` key and nothing else — no HTTP, no tool.
* The provider lives in `@deepseek-ai/dsh-ragflow/http`, the model-facing tool
* in `@deepseek-ai/dsh-ragflow/tool`.
*
* @module @deepseek-ai/dsh-ragflow
*/
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {
RagflowRetrieveProvider,
RagflowRetrieveRequest,
RagflowRetrieveResult,
} from './types.ts'
import { RagflowError } from './types.ts'
export { RagflowError } from './types.ts'
export type {
RagflowChunk,
RagflowRetrieveProvider,
RagflowRetrieveRequest,
RagflowRetrieveResult,
} from './types.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
ragflow: RagflowRuntime
}
}
/** Selection inputs for execution-time provider resolution. */
interface Selection
{
/** The configured provider id for this capability, if any. */
readonly configuredId?: string
/** Providers registered for this capability kind. */
readonly providers: ReadonlyMap
}
/**
* Config for the RAGFlow seam. `retrieveProvider` pins which provider wins; it
* is optional (a single registered usable provider auto-selects). Operational
* overrides such as environment variables must feed this same field rather than
* introduce a hidden priority chain.
*/
export interface RagflowRuntimeConfig {
/** Explicit retrieval provider id. Omitted = auto-select when exactly one usable. */
readonly retrieveProvider?: string
}
/**
* The RAGFlow knowledge-base retrieval service. Registered as `ctx.ragflow`
* (one instance per context).
*
* Selection semantics (resolved at execution time, never order-dependent):
* - A configured id that is registered and `available()` → that provider.
* - A configured id not registered → `RAGFLOW_PROVIDER_CONFIGURED_MISSING`.
* - A configured id registered but unavailable →
* `RAGFLOW_PROVIDER_CONFIGURED_UNAVAILABLE`.
* - No id configured, exactly one registered usable provider → that provider.
* - No id configured, multiple usable providers → `RAGFLOW_PROVIDER_AMBIGUOUS`.
* - No id configured, no usable provider → `RAGFLOW_PROVIDER_UNAVAILABLE`.
*/
export class RagflowRuntime extends Service {
/**
* Provider selection config. The operational env override feeds the SAME
* field: `$DSH_RAGFLOW_PROVIDER` is equivalent to `retrieveProvider` and is
* NOT a hidden priority chain.
*/
static Config: z = z.object({
retrieveProvider: z.string(),
})
private retrieveProviders = new Map()
private readonly retrieveProviderId: string | undefined
constructor(ctx: Context, config: RagflowRuntimeConfig = {}) {
super(ctx, 'ragflow')
this.retrieveProviderId = config.retrieveProvider ?? process.env.DSH_RAGFLOW_PROVIDER
}
/**
* Register a retrieval provider. Throws {@link RagflowError}
* `RAGFLOW_DUPLICATE_PROVIDER` if its id is already registered. Returns a
* disposer; disposed with the calling fiber.
* @param provider - the provider; its `id` is the registry key.
* @returns the disposer that unregisters the provider.
*/
registerRetrieveProvider(provider: RagflowRetrieveProvider): () => void {
if (this.retrieveProviders.has(provider.id)) {
throw new RagflowError(`a ragflow provider with id "${provider.id}" is already registered`, 'RAGFLOW_DUPLICATE_PROVIDER')
}
const store = this.retrieveProviders
const dispose = this.ctx.effect(function* () {
store.set(provider.id, provider)
yield () => store.delete(provider.id)
}, 'ragflow.registerRetrieveProvider()')
// ctx.effect's disposer returns Promise; our disposer API is
// synchronous fire-and-forget — discard the (always-resolved) promise.
return () => void dispose()
}
/**
* Run one retrieval through the selected provider. Resolves the provider at
* call time with the selection rules above; throws {@link RagflowError} when
* the capability cannot run. The seam enforces `request.maxChunks` on the
* result: if the provider over-returns, `chunks[]` is truncated and
* `truncated` set. An empty result is not an error.
* @param request - the question, optional scope, and optional result bound.
* @param signal - optional cancellation signal forwarded to the provider.
* @returns the provider's chunks, capped to `request.maxChunks`.
*/
async retrieve(request: RagflowRetrieveRequest, signal?: AbortSignal): Promise {
const provider = resolveProvider({
providers: this.retrieveProviders,
...this.retrieveProviderId !== undefined ? { configuredId: this.retrieveProviderId } : {},
})
const result = await provider.retrieve(request, signal)
return capChunks(result, request.maxChunks)
}
}
interface ResolvableProvider {
readonly id: string
available(): boolean
}
/** Resolve the selected provider or throw the matching {@link RagflowError}. */
function resolveProvider(selection: Selection
): P {
const { configuredId, providers } = selection
if (configuredId !== undefined) {
const provider = providers.get(configuredId)
if (!provider) {
throw new RagflowError(`configured ragflow provider "${configuredId}" is not registered`, 'RAGFLOW_PROVIDER_CONFIGURED_MISSING')
}
if (!provider.available()) {
throw new RagflowError(`configured ragflow provider "${configuredId}" is registered but unavailable`, 'RAGFLOW_PROVIDER_CONFIGURED_UNAVAILABLE')
}
return provider
}
const usable = [...providers.values()].filter(provider => provider.available())
const [single] = usable
if (single === undefined) {
throw new RagflowError('no usable ragflow provider is registered', 'RAGFLOW_PROVIDER_UNAVAILABLE')
}
if (usable.length > 1) {
const ids = usable.map(provider => provider.id).join(', ')
throw new RagflowError(`multiple usable ragflow providers are registered (${ids}); configure one explicitly`, 'RAGFLOW_PROVIDER_AMBIGUOUS')
}
return single
}
/** Enforce `maxChunks` on a retrieval result: truncate `chunks[]` and flag it. */
export function capChunks(result: RagflowRetrieveResult, maxChunks: number | undefined): RagflowRetrieveResult {
if (maxChunks === undefined || result.chunks.length <= maxChunks) return result
return { ...result, chunks: result.chunks.slice(0, maxChunks), truncated: true }
}
export default RagflowRuntime