{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type {\n  IKnowledgeAdapter,\n  IKnowledgeService,\n  AdapterContext,\n  AdapterSearchOptions,\n} from '@objectstack/spec/contracts';\nimport type {\n  KnowledgeDocument,\n  KnowledgeHit,\n  KnowledgeSource,\n} from '@objectstack/spec/ai';\nimport { KNOWLEDGE_SERVICE } from '@objectstack/spec/contracts';\n\n/**\n * Subset of `fetch` used by the adapter. Inject in tests; defaults to\n * the global `fetch`.\n */\nexport type FetchLike = (\n  input: string,\n  init?: {\n    method?: string;\n    headers?: Record<string, string>;\n    body?: string;\n    signal?: AbortSignal;\n  },\n) => Promise<{\n  ok: boolean;\n  status: number;\n  statusText: string;\n  text(): Promise<string>;\n  json(): Promise<unknown>;\n}>;\n\nexport interface KnowledgeRagflowAdapterOptions {\n  /** RAGFlow endpoint, e.g. `http://localhost:9380`. */\n  endpoint: string;\n  /** RAGFlow API key (Bearer token). */\n  apiKey: string;\n  /** Adapter id. @default 'ragflow' */\n  id?: string;\n  /** Override `fetch` for tests. */\n  fetch?: FetchLike;\n  /** Request timeout in milliseconds. @default 30000 */\n  timeoutMs?: number;\n}\n\ninterface RagflowSourceOptions {\n  datasetId: string;\n  /** Optional rerank model id (overrides dataset default). */\n  rerankModel?: string;\n  /** Optional similarity threshold passed through to RAGFlow. */\n  similarityThreshold?: number;\n  /** Optional vector vs keyword weight in [0,1]. */\n  vectorSimilarityWeight?: number;\n}\n\nfunction extractRagflowOptions(source: KnowledgeSource): RagflowSourceOptions {\n  const opts = ((source as unknown as { options?: Record<string, unknown> }).options ?? {}) as\n    Record<string, unknown>;\n  const datasetId = opts.datasetId;\n  if (typeof datasetId !== 'string' || !datasetId) {\n    throw new Error(\n      `RAGFlow adapter requires source.options.datasetId on source '${source.id}'`,\n    );\n  }\n  return {\n    datasetId,\n    rerankModel: typeof opts.rerankModel === 'string' ? opts.rerankModel : undefined,\n    similarityThreshold:\n      typeof opts.similarityThreshold === 'number' ? opts.similarityThreshold : undefined,\n    vectorSimilarityWeight:\n      typeof opts.vectorSimilarityWeight === 'number'\n        ? opts.vectorSimilarityWeight\n        : undefined,\n  };\n}\n\n/**\n * RAGFlow adapter. Maps {@link KnowledgeDocument} upserts to the\n * dataset's chunk API, delegates retrieval to `/api/v1/retrieval`, and\n * returns {@link KnowledgeHit}s with `sourceRecordId` preserved so the\n * orchestrator can run a permission re-check.\n */\nexport class KnowledgeRagflowAdapter implements IKnowledgeAdapter {\n  readonly id: string;\n  private readonly endpoint: string;\n  private readonly apiKey: string;\n  private readonly fetchImpl: FetchLike;\n  private readonly timeoutMs: number;\n\n  constructor(opts: KnowledgeRagflowAdapterOptions) {\n    if (!opts.endpoint) throw new Error('RAGFlow adapter: endpoint required');\n    if (!opts.apiKey) throw new Error('RAGFlow adapter: apiKey required');\n    this.id = opts.id ?? 'ragflow';\n    this.endpoint = opts.endpoint.replace(/\\/+$/, '');\n    this.apiKey = opts.apiKey;\n    this.fetchImpl = opts.fetch ?? ((globalThis as { fetch?: FetchLike }).fetch as FetchLike);\n    this.timeoutMs = opts.timeoutMs ?? 30000;\n    if (!this.fetchImpl) {\n      throw new Error('RAGFlow adapter: no fetch available; pass options.fetch');\n    }\n  }\n\n  async upsert(docs: KnowledgeDocument[], ctx: AdapterContext): Promise<void> {\n    const { datasetId } = extractRagflowOptions(ctx.source);\n    // RAGFlow models documents in two layers: documents (file-like) and\n    // chunks (text blocks). We treat each KnowledgeDocument as a single\n    // chunk-set: delete existing chunks with the same external id, then\n    // upload as `content` chunks tagged with our document id.\n    for (const doc of docs) {\n      await this.deleteChunksByDocumentId(datasetId, doc.id);\n      await this.request(`/api/v1/datasets/${datasetId}/chunks`, {\n        method: 'POST',\n        body: JSON.stringify({\n          content: doc.content,\n          // RAGFlow accepts arbitrary metadata used for filtering at\n          // retrieval time. We always stamp `objectstack_doc_id` so\n          // delete() can find these chunks again.\n          important_keywords: doc.title ? [doc.title] : undefined,\n          metadata: {\n            ...(doc.metadata ?? {}),\n            objectstack_doc_id: doc.id,\n            objectstack_source_id: doc.sourceId,\n            objectstack_record_id: doc.sourceRecordId,\n            title: doc.title,\n          },\n        }),\n      });\n    }\n  }\n\n  async delete(documentIds: string[], ctx: AdapterContext): Promise<void> {\n    const { datasetId } = extractRagflowOptions(ctx.source);\n    for (const id of documentIds) {\n      await this.deleteChunksByDocumentId(datasetId, id);\n    }\n  }\n\n  async search(query: string, opts: AdapterSearchOptions): Promise<KnowledgeHit[]> {\n    const { datasetId, rerankModel, similarityThreshold, vectorSimilarityWeight } =\n      extractRagflowOptions(opts.source);\n    const body: Record<string, unknown> = {\n      question: query,\n      dataset_ids: [datasetId],\n      top_k: opts.topK,\n      keyword: true,\n    };\n    if (rerankModel) body.rerank_id = rerankModel;\n    if (typeof similarityThreshold === 'number') body.similarity_threshold = similarityThreshold;\n    if (typeof vectorSimilarityWeight === 'number')\n      body.vector_similarity_weight = vectorSimilarityWeight;\n    if (opts.filter) body.metadata_condition = { ...opts.filter };\n\n    const res = await this.request('/api/v1/retrieval', {\n      method: 'POST',\n      body: JSON.stringify(body),\n    });\n    const data = (res?.data ?? {}) as { chunks?: RagflowChunkHit[] };\n    const chunks = data.chunks ?? [];\n    return chunks.slice(0, opts.topK).map<KnowledgeHit>((c) => {\n      const md = (c.metadata ?? {}) as Record<string, unknown>;\n      const docId =\n        (md.objectstack_doc_id as string | undefined) ??\n        c.document_id ??\n        c.doc_id ??\n        c.id;\n      const recordId = md.objectstack_record_id as string | undefined;\n      return {\n        chunkId: c.id ?? `${docId}#${c.position ?? 0}`,\n        documentId: docId ?? c.id ?? 'unknown',\n        sourceId: opts.source.id,\n        sourceRecordId: recordId,\n        score: c.similarity ?? c.score ?? 0,\n        snippet: c.content ?? c.content_with_weight ?? '',\n        title: (md.title as string | undefined) ?? c.document_name,\n        metadata: md,\n      };\n    });\n  }\n\n  async healthCheck(): Promise<{ ok: boolean; message?: string }> {\n    try {\n      await this.request('/api/v1/datasets?page=1&page_size=1', { method: 'GET' });\n      return { ok: true };\n    } catch (err) {\n      return { ok: false, message: err instanceof Error ? err.message : String(err) };\n    }\n  }\n\n  private async deleteChunksByDocumentId(datasetId: string, docId: string): Promise<void> {\n    // Find chunks with our stamped metadata, then delete by chunk id.\n    const found = (await this.request(`/api/v1/retrieval`, {\n      method: 'POST',\n      body: JSON.stringify({\n        question: docId,\n        dataset_ids: [datasetId],\n        top_k: 256,\n        keyword: false,\n        metadata_condition: { objectstack_doc_id: docId },\n      }),\n    })) as { data?: { chunks?: Array<{ id?: string }> } };\n    const ids = (found.data?.chunks ?? [])\n      .map((c) => c.id)\n      .filter((x): x is string => typeof x === 'string');\n    if (ids.length === 0) return;\n    await this.request(`/api/v1/datasets/${datasetId}/chunks`, {\n      method: 'DELETE',\n      body: JSON.stringify({ chunk_ids: ids }),\n    });\n  }\n\n  private async request(\n    path: string,\n    init: { method: string; body?: string },\n  ): Promise<{ data?: unknown; code?: number; message?: string }> {\n    const controller = new AbortController();\n    const t = setTimeout(() => controller.abort(), this.timeoutMs);\n    try {\n      const res = await this.fetchImpl(`${this.endpoint}${path}`, {\n        method: init.method,\n        headers: {\n          'content-type': 'application/json',\n          authorization: `Bearer ${this.apiKey}`,\n        },\n        body: init.body,\n        signal: controller.signal,\n      });\n      const raw = await res.text();\n      let parsed: { data?: unknown; code?: number; message?: string } = {};\n      if (raw) {\n        try {\n          parsed = JSON.parse(raw) as typeof parsed;\n        } catch {\n          if (!res.ok) {\n            throw new Error(\n              `RAGFlow ${init.method} ${path} → ${res.status} ${res.statusText}: ${raw.slice(0, 200)}`,\n            );\n          }\n        }\n      }\n      if (!res.ok || (typeof parsed.code === 'number' && parsed.code !== 0 && parsed.code !== 200)) {\n        throw new Error(\n          `RAGFlow ${init.method} ${path} → ${res.status} ${res.statusText}${\n            parsed.message ? ` (${parsed.message})` : ''\n          }`,\n        );\n      }\n      return parsed;\n    } finally {\n      clearTimeout(t);\n    }\n  }\n}\n\ninterface RagflowChunkHit {\n  id?: string;\n  document_id?: string;\n  doc_id?: string;\n  document_name?: string;\n  content?: string;\n  content_with_weight?: string;\n  similarity?: number;\n  score?: number;\n  position?: number;\n  metadata?: Record<string, unknown>;\n}\n\n/* ---------------------------------------------------------------- */\n/* Kernel plugin glue                                               */\n/* ---------------------------------------------------------------- */\n\nexport interface KnowledgeRagflowPluginOptions extends KnowledgeRagflowAdapterOptions {}\n\nexport class KnowledgeRagflowPlugin implements Plugin {\n  name = 'com.objectstack.plugin.knowledge-ragflow';\n  version = '0.1.0';\n  type = 'standard';\n\n  private readonly adapter: KnowledgeRagflowAdapter;\n\n  constructor(opts: KnowledgeRagflowPluginOptions) {\n    this.adapter = new KnowledgeRagflowAdapter(opts);\n  }\n\n  async init(_ctx: PluginContext): Promise<void> {\n    // No-op: actual registration happens in start() once service is available.\n  }\n\n  async start(ctx: PluginContext): Promise<void> {\n    let svc: IKnowledgeService | undefined;\n    try {\n      svc = ctx.getService<IKnowledgeService>(KNOWLEDGE_SERVICE);\n    } catch {\n      ctx.logger.warn?.(\n        'KnowledgeRagflowPlugin: IKnowledgeService not registered — install KnowledgeServicePlugin first.',\n      );\n      return;\n    }\n    svc.registerAdapter(this.adapter.id, this.adapter);\n    ctx.logger.info?.(\n      `KnowledgeRagflowPlugin: adapter '${this.adapter.id}' registered (endpoint=${(this.adapter as unknown as { endpoint: string }).endpoint}).`,\n    );\n  }\n}\n"],"mappings":"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","names":[]}