diff --git a/api/apps/restful_apis/system_api.py b/api/apps/restful_apis/system_api.py index efc011bd87..4e587035f8 100644 --- a/api/apps/restful_apis/system_api.py +++ b/api/apps/restful_apis/system_api.py @@ -62,6 +62,13 @@ def version(): return get_json_result(data=get_ragflow_version()) +@manager.route("/language", methods=["GET"]) # noqa: F821 +def language(): + """Backend runtime language detection for front-end compatibility.""" + logging.info("Language endpoint called, returning python") + return get_json_result(data={"language": "python"}) + + @manager.route("/system/status", methods=["GET"]) # noqa: F821 @login_required def status(): diff --git a/internal/handler/system.go b/internal/handler/system.go index 14206f952a..252a81299e 100644 --- a/internal/handler/system.go +++ b/internal/handler/system.go @@ -260,3 +260,9 @@ func (h *SystemHandler) ListEnvironments(c *gin.Context) { common.SuccessWithData(c, environments, "SUCCESS") } + +// Language returns the backend runtime language so the front end can +// choose the appropriate code path (Go vs Python). +func (h *SystemHandler) Language(c *gin.Context) { + common.SuccessWithData(c, map[string]string{"language": "go"}, "success") +} diff --git a/internal/router/router.go b/internal/router/router.go index 9b6dd388f5..0fb637e488 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -159,6 +159,9 @@ func (r *Router) Setup(engine *gin.Engine) { apiNoAuth.GET("/system/config", r.systemHandler.GetConfig) apiNoAuth.GET("/system/version", r.systemHandler.GetVersion) apiNoAuth.GET("/system/healthz", r.systemHandler.Healthz) + // Backend runtime language detection. The front end calls this once + // to choose between Go and Python code paths. + apiNoAuth.GET("/language", r.systemHandler.Language) // Pipeline catalog. Public static data (shipped with the binary), // no auth required. The front end uses it to populate the parser diff --git a/web/src/hooks/use-user-setting-request.tsx b/web/src/hooks/use-user-setting-request.tsx index 3b498e2cff..1e4baba73e 100644 --- a/web/src/hooks/use-user-setting-request.tsx +++ b/web/src/hooks/use-user-setting-request.tsx @@ -10,6 +10,11 @@ import { } from '@/interfaces/database/user-setting'; import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system'; import { DEFAULT_LANGUAGE_CODE, supportedLanguages } from '@/locales/config'; +import kbService from '@/services/knowledge-service'; +import { + getBackendLanguage, + subscribeBackendLanguage, +} from '@/utils/backend-runtime'; import userService, { addTenantUser, agreeTenant, @@ -19,6 +24,7 @@ import userService, { } from '@/services/user-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useMemo, useState } from 'react'; +import { useSyncExternalStore } from 'react'; import { useTranslation } from 'react-i18next'; import { useWarnEmptyModel } from './use-warn-empty-model'; @@ -38,6 +44,7 @@ export const enum UserSettingApiAction { AgreeTenant = 'agreeTenant', SetLangfuseConfig = 'setLangfuseConfig', DeleteLangfuseConfig = 'deleteLangfuseConfig', + ListPipelines = 'listPipelines', FetchLangfuseConfig = 'fetchLangfuseConfig', } @@ -102,6 +109,27 @@ export const useSelectParserList = (): Array<{ const { data: tenantInfo } = useFetchTenantInfo(true); const { t } = useTranslation(); + // Detect backend runtime language (Go vs Python) so we can choose + // the matching parser-list code path at runtime. + // fetchBackendLanguage / getBackendLanguage handle their own caching + // internally; no need for an extra useQuery layer. + const backendLang = useSyncExternalStore( + subscribeBackendLanguage, + getBackendLanguage, + getBackendLanguage, + ); + + // Go backend: fetch pipeline catalog dynamically. + const { data: pipelineListData } = useQuery({ + queryKey: [UserSettingApiAction.ListPipelines], + queryFn: async () => { + const { data } = await kbService.listPipelines(); + return data; + }, + staleTime: Infinity, + enabled: backendLang === 'go', + }); + const defaultParsers = useMemo( () => [ { value: 'naive', label: t('knowledgeConfiguration.parserLabel.naive') }, @@ -135,6 +163,25 @@ export const useSelectParserList = (): Array<{ ); const parserList = useMemo(() => { + // Go backend: prefer the dynamic pipeline catalog from the API. + if (backendLang === 'go') { + const pipelineList: Array<{ parser_id: string; title: string; dsl: Record }> = + pipelineListData?.data ?? []; + if (pipelineList.length > 0) { + const labelFromAPI = (parserId: string, title: string) => { + const key = `knowledgeConfiguration.parserLabel.${parserId}`; + const translated = t(key); + return translated !== key ? translated : title; + }; + return pipelineList.map((item) => ({ + value: item.parser_id, + label: labelFromAPI(item.parser_id, item.title), + })); + } + } + + // Python backend (or fallback): use tenant-level parser_ids or + // the hardcoded default parsers. const parserArray: Array = tenantInfo?.parser_ids?.split(',') ?? []; const filteredArray = parserArray.filter((x) => x.trim() !== ''); @@ -146,7 +193,7 @@ export const useSelectParserList = (): Array<{ const arr = x.split(':'); return { value: arr[0], label: arr[1] }; }); - }, [tenantInfo, defaultParsers]); + }, [tenantInfo, defaultParsers, backendLang, pipelineListData, t]); return parserList; }; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index f14e8077d7..b4ee9a5056 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -778,6 +778,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim 'Re-parse existing documents for the new column roles to take effect.', parserLabel: { naive: 'General', + general: 'General', qa: 'Q&A', resume: 'Resume', manual: 'Manual', diff --git a/web/src/services/knowledge-service.ts b/web/src/services/knowledge-service.ts index bebfffeb31..3b21f8790b 100644 --- a/web/src/services/knowledge-service.ts +++ b/web/src/services/knowledge-service.ts @@ -19,6 +19,7 @@ const { documentThumbnails, documentIngest, listTagByKnowledgeIds, + listPipelines, setMeta, getMeta, getMetaKeys, @@ -66,6 +67,10 @@ const methods = { url: retrievalTestShare, method: 'post', }, + listPipelines: { + url: listPipelines, + method: 'get', + }, pipelineRerun: { url: api.pipelineRerun, method: 'post', diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index fa212a24a1..892c119df6 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -181,6 +181,7 @@ export default { `${restAPIv1}/datasets/${datasetId}/ingestions/${logId}`, fetchPipelineDatasetLogs: (datasetId: string) => `${restAPIv1}/datasets/${datasetId}/ingestions`, + listPipelines: `${restAPIv1}/pipelines?type=builtin`, runIndex: (datasetId: string, indexType: string) => `${restAPIv1}/datasets/${datasetId}/index?type=${indexType.toLowerCase()}`, traceIndex: (datasetId: string, indexType: string) => diff --git a/web/src/utils/backend-runtime.ts b/web/src/utils/backend-runtime.ts new file mode 100644 index 0000000000..42823078d1 --- /dev/null +++ b/web/src/utils/backend-runtime.ts @@ -0,0 +1,40 @@ +/** + * Backend runtime language detection. + * + * Fetches /api/v1/language once at module load (app start) and caches the + * result. Components subscribe via React's useSyncExternalStore; there is no + * per-component fetch or useEffect. + * + * Pattern: module-level fetch + listener set, same subscription approach as + * enterprise billingStatus.ts. + */ + +type Listener = () => void; +const listeners = new Set(); + +let backendLanguage: string | null = null; + +// Kick off the fetch at module load — app start, not component mount. +const promise: Promise = fetch('/api/v1/language') + .then((r) => r.json()) + .then((body: { data?: { language?: string } }) => { + backendLanguage = body.data?.language === 'go' ? 'go' : 'python'; + listeners.forEach((fn) => fn()); + return backendLanguage; + }) + .catch(() => { + backendLanguage = 'python'; + listeners.forEach((fn) => fn()); + return 'python'; + }); + +export const fetchBackendLanguage = (): Promise => promise; + +export const getBackendLanguage = (): string | null => backendLanguage; + +export const subscribeBackendLanguage = (listener: Listener): (() => void) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +};