From a4e819504cdddb268fb80c4bdfb5809c246dc143 Mon Sep 17 00:00:00 2001 From: buua436 Date: Thu, 13 Aug 2026 19:03:24 +0800 Subject: [PATCH] feat: support entity and topic wiki modes (#18216) --- .../utils/compilation_template_validation.py | 2 + .../init_data/compilation_templates/wiki.yaml | 2 +- .../knowlege_compile/wiki_incremental.py | 22 +-- .../dataset_wiki_generator.py | 164 ++++++++++++------ .../task_executor_refactor/task_handler.py | 12 +- .../integration/wiki/test_wiki_incremental.py | 12 +- .../originui/select-with-search.tsx | 6 +- .../database/compilation-template.ts | 1 + .../request/compilation-template.ts | 1 + web/src/locales/en.ts | 9 +- web/src/locales/zh.ts | 9 +- web/src/pages/agent/constant/pipeline.tsx | 2 +- .../agent/form/compilation-form/index.tsx | 30 ++-- .../components/template-configuration.tsx | 17 +- .../create-next/constant.ts | 2 +- .../create-next/schema.ts | 45 +++-- .../create-next/utils.ts | 14 +- .../components/template-configuration.tsx | 22 ++- .../edit-next/constant.ts | 2 +- .../compilation-templates/edit-next/schema.ts | 43 +++-- .../compilation-templates/edit-next/utils.ts | 14 +- 21 files changed, 278 insertions(+), 153 deletions(-) diff --git a/api/apps/restful_apis/utils/compilation_template_validation.py b/api/apps/restful_apis/utils/compilation_template_validation.py index 1ac3b6a3c6..2c83253e86 100644 --- a/api/apps/restful_apis/utils/compilation_template_validation.py +++ b/api/apps/restful_apis/utils/compilation_template_validation.py @@ -57,6 +57,8 @@ def validate_template_payload(req: dict, require_all: bool = True) -> str: if len(str((field or {}).get("rule") or "")) > 1024: return f"{section.capitalize()} field rule is too long." if config.get("kind") == "wiki" or req.get("kind") == "wiki": + if config.get("mode") not in ("entity", "topic"): + return "Wiki mode must be either 'entity' or 'topic'." for field in (config.get("claim") or {}).get("fields") or []: if not str((field or {}).get("statement") or "").strip(): return "Claim statement is required." diff --git a/api/db/init_data/compilation_templates/wiki.yaml b/api/db/init_data/compilation_templates/wiki.yaml index 343092ca03..f23cabd595 100644 --- a/api/db/init_data/compilation_templates/wiki.yaml +++ b/api/db/init_data/compilation_templates/wiki.yaml @@ -2,7 +2,7 @@ kind: wiki display_name: Wiki — Graph-based wiki config: kind: wiki - plan: yes + mode: entity instruction: | - Each page must be a proper encyclopedic article, NOT a flat bullet list: - 1. Opening paragraph (2-4 sentences defining what this is). No heading. diff --git a/rag/advanced_rag/knowlege_compile/wiki_incremental.py b/rag/advanced_rag/knowlege_compile/wiki_incremental.py index 8b65852ab3..91bc0e29ad 100644 --- a/rag/advanced_rag/knowlege_compile/wiki_incremental.py +++ b/rag/advanced_rag/knowlege_compile/wiki_incremental.py @@ -1,10 +1,10 @@ """Dual-mode wiki incremental compilation. -Mode A (no-plan, plan=no): +Entity mode: MAP → REDUCE → REFINE per-concept (generate/modify/re-synthesize) → FINALIZE 1 concept = 1 page (WeKnora style). Entities enrich concept pages via source chunks. -Mode B (with-plan, plan=yes): +Topic mode: MAP → REDUCE → PLAN (LLM grouping) → REFINE per-page → FINALIZE Incremental: embeddings retrieve page candidates; the LLM makes final routes. @@ -3321,7 +3321,7 @@ async def wiki_compile_incremental( embd_mdl, tenant_id: str, kb_id: str, - plan: bool = False, # True = Mode B, False = Mode A + mode: str, incremental: bool = False, # True = incremental run map_results: list[dict] | None = None, # from MAP phase deleted_doc_ids: set[str] | None = None, @@ -3330,7 +3330,7 @@ async def wiki_compile_incremental( """Main entry point for dual-mode wiki compilation. Args: - plan: True=Mode B (with PLAN), False=Mode A (no-plan, WeKnora style) + mode: ``entity`` for Mode A, or ``topic`` for Mode B. incremental: True=incremental update, False=full build map_results: MAP outputs. If None, loads from ES. deleted_doc_ids: Documents that were removed. @@ -3489,7 +3489,7 @@ async def wiki_compile_incremental( kb_id=kb_id, incremental=incremental, ) - if plan and incremental: + if mode == "topic" and incremental: try: from api.db.services.document_service import DocumentService @@ -3677,7 +3677,7 @@ async def wiki_compile_incremental( topic_pool = { _normalize_key(topic): topic for page in existing_pages.values() for topic in _as_str_list(page.get("topic_kwd")) if topic and _normalize_key(topic) != _normalize_key(WIKI_TOPIC_FALLBACK) } - if plan and existing_pages: + if mode == "topic" and existing_pages: plan_members = await _wiki_load_plan_group_members(tenant_id, kb_id) for page_id, names in plan_members.items(): if page_id in existing_pages and names: @@ -3728,7 +3728,7 @@ async def wiki_compile_incremental( topic_embeddings = await _wiki_prepare_topic_embeddings(doc_topics, embd_mdl, list(topic_pool.values())) topic_pool_lock = asyncio.Lock() - if plan: + if mode == "topic": summary = await _wiki_mode_b_run( deltas=deltas, existing_pages=existing_pages, @@ -4633,12 +4633,12 @@ async def wiki_handle_document_deleted( doc_id: str, chat_mdl, embd_mdl, - plan: bool = False, + mode: str, ) -> dict: """Clean up wiki pages + canonical entities when a document is deleted. Args: - plan: True=Mode B (update plan_group), False=Mode A + mode: ``topic`` updates plan groups; ``entity`` does not. Returns: {pages_modified, pages_deleted, errors} """ @@ -4706,7 +4706,7 @@ async def wiki_handle_document_deleted( if doc_id in source_doc_ids: source_doc_ids.remove(doc_id) - page_type = existing.get("page_type_kwd", "concept" if not plan else "entity") + page_type = existing.get("page_type_kwd", "concept" if mode == "entity" else "entity") if not source_doc_ids: await _wiki_refine_page( @@ -4757,7 +4757,7 @@ async def wiki_handle_document_deleted( ) summary["pages_modified"] += 1 - if plan: + if mode == "topic": plan_condition = { "compile_kwd": [WIKI_PLAN_GROUP_COMPILE_KWD], "page_id": [page_id], diff --git a/rag/svr/task_executor_refactor/dataset_wiki_generator.py b/rag/svr/task_executor_refactor/dataset_wiki_generator.py index a9649c2689..54ae52f20f 100644 --- a/rag/svr/task_executor_refactor/dataset_wiki_generator.py +++ b/rag/svr/task_executor_refactor/dataset_wiki_generator.py @@ -223,6 +223,60 @@ def _pipeline_compilation_template_ids(pipeline_id: str, tenant_id: str) -> list return template_ids +def _pipeline_compiler_llm_id(pipeline_id: str) -> str | None: + """Return the chat model configured on a pipeline's Compiler component.""" + pipeline_id = (pipeline_id or "").strip() + if not pipeline_id: + return None + from api.db.services.canvas_service import UserCanvasService + + ok, canvas = UserCanvasService.get_by_id(pipeline_id) + if not ok or not canvas: + return None + dsl = getattr(canvas, "dsl", None) + if isinstance(dsl, str): + try: + dsl = json.loads(dsl) + except Exception: + return None + if not isinstance(dsl, dict) or not isinstance(dsl.get("components"), dict): + return None + for component in dsl["components"].values(): + if not isinstance(component, dict): + continue + obj = component.get("obj") if isinstance(component.get("obj"), dict) else {} + component_name = obj.get("component_name") or component.get("component_name") or component.get("name") + if not isinstance(component_name, str) or component_name.lower() != "compiler": + continue + candidates = [ + obj.get("params") if isinstance(obj.get("params"), dict) else {}, + obj, + component.get("params") if isinstance(component.get("params"), dict) else {}, + component, + ] + for candidate in candidates: + llm_id = candidate.get("llm_id") + if isinstance(llm_id, str) and llm_id.strip(): + return llm_id.strip() + return None + return None + + +def _validate_wiki_eligible_docs(eligible: list[tuple[dict, str]]) -> dict[str, str | None]: + """Validate one Wiki template and return each doc's pipeline chat model.""" + template_ids = {template_id for _, template_id in eligible} + if len(template_ids) > 1: + raise ValueError("Eligible Wiki documents must use the same template") + pipeline_chat_llm_ids: dict[str, str | None] = {} + for doc, _ in eligible: + doc_id = str(doc.get("id") or "") + pipeline_id = (doc.get("pipeline_id") or "").strip() + if not pipeline_id: + raise ValueError(f"Wiki document {doc_id} must use a pipeline") + pipeline_chat_llm_ids[doc_id] = _pipeline_compiler_llm_id(pipeline_id) + return pipeline_chat_llm_ids + + def _wiki_eligible_docs(all_docs, tenant_id: str, skip_doc_ids=None) -> list[tuple[dict, str]]: """Docs eligible for wiki compilation, each paired with its wiki template id. @@ -536,16 +590,16 @@ async def _wiki_delete_deleted_doc_state( ) -# ----- mode (plan) persistence & full reset ---------------------------------- +# ----- mode persistence & full reset ---------------------------------------- def _wiki_mode_meta_id(kb_id: str) -> str: - """Stable row id for the KB-level mode (plan) meta record.""" + """Stable row id for the KB-level mode meta record.""" return f"wiki_mode_meta_{kb_id}" -async def _wiki_load_mode_plan(tenant_id: str, kb_id: str) -> bool | None: - """Return the plan (Mode B) value recorded by the previous build, or None +async def _wiki_load_mode(tenant_id: str, kb_id: str) -> str | None: + """Return the mode value recorded by the previous build, or None if this KB has never recorded a mode (e.g. first ever build).""" from common.doc_store.doc_store_base import OrderByExpr @@ -555,7 +609,7 @@ async def _wiki_load_mode_plan(tenant_id: str, kb_id: str) -> bool | None: try: res = await thread_pool_exec( settings.docStoreConn.search, - ["plan_kwd"], + ["mode_kwd"], [], {"compile_kwd": ["wiki_mode_meta"], "id": [_wiki_mode_meta_id(kb_id)]}, [], @@ -565,16 +619,14 @@ async def _wiki_load_mode_plan(tenant_id: str, kb_id: str) -> bool | None: index, [kb_id], ) - fm = settings.docStoreConn.get_fields(res, ["plan_kwd"]) or {} + fm = settings.docStoreConn.get_fields(res, ["mode_kwd"]) or {} for row in fm.values(): - val = row.get("plan_kwd") + val = row.get("mode_kwd") if isinstance(val, list): val = val[0] if val else "" val = str(val or "").strip() - if val in ("true", "1", "yes"): - return True - if val in ("false", "0", "no"): - return False + if val in ("entity", "topic"): + return val except Exception: logging.exception("wiki: failed to load mode meta for kb=%s", kb_id) return None @@ -619,12 +671,14 @@ def _wiki_embedding_fingerprint(embedding_model) -> str: return ":".join(part for part in (factory, model_id, name) if part) -async def _wiki_save_mode_plan(tenant_id: str, kb_id: str, plan: bool, embedding_fingerprint: str = "") -> None: +async def _wiki_save_mode(tenant_id: str, kb_id: str, mode: str, embedding_fingerprint: str = "") -> None: + if mode not in ("entity", "topic"): + raise ValueError(f"Unsupported wiki mode: {mode}") index = search.index_name(tenant_id) row = { "id": _wiki_mode_meta_id(kb_id), "compile_kwd": "wiki_mode_meta", - "plan_kwd": "true" if plan else "false", + "mode_kwd": mode, "embedding_model_kwd": embedding_fingerprint, "kb_id": kb_id, "create_timestamp_flt": float(__import__("time").time()), @@ -1339,11 +1393,12 @@ async def run_wiki( if not eligible: progress(1.0, "No documents are configured for wiki compilation.") return + pipeline_chat_llm_ids = _validate_wiki_eligible_docs(eligible) - # 3. Resolve chat models. MAP is per-(doc, template) so each pair - # uses its template's own ``llm_id``. REDUCE / PLAN / REFINE are + # 3. Resolve chat models. MAP is per document, so each document uses + # its pipeline Compiler's ``llm_id``. REDUCE / PLAN / REFINE are # KB-wide and need exactly one model — we pick the first eligible - # template's ``llm_id`` as the canonical KB chat model. + # pipeline Compiler's ``llm_id`` as the canonical KB chat model. llm_bundle_cache: dict[str, LLMBundle] = {} def _bundle_for(llm_id: str | None) -> LLMBundle: @@ -1413,13 +1468,14 @@ async def run_wiki( progress(1.0, "No valid templates resolved for wiki compilation.") return - # ``kb_chat_llm_id`` is captured from the first eligible template and + # ``kb_chat_llm_id`` is captured from the first eligible pipeline and # used as the canonical chat model for KB-wide REDUCE/PLAN/REFINE. # Writer instructions and page examples follow the same first-template- # wins rule. + first_doc = resolved_eligible[0][0] first_parser_cfg = resolved_eligible[0][2] first_parser_cfg = first_parser_cfg if isinstance(first_parser_cfg, dict) else {} - kb_chat_llm_id: Optional[str] = (first_parser_cfg.get("llm_id") or "").strip() or None + kb_chat_llm_id = pipeline_chat_llm_ids.get(str(first_doc.get("id") or "")) first_instruction = first_parser_cfg.get("instruction") first_example = first_parser_cfg.get("example") kb_writer_instruction: Optional[str] = first_instruction if isinstance(first_instruction, str) and first_instruction.strip() else None @@ -1468,7 +1524,7 @@ async def run_wiki( i, doc, template_id, parser_cfg, batch, batch_no = item doc_id = doc["id"] stats = doc_stats[i] - map_llm_id = (parser_cfg.get("llm_id") or "").strip() if isinstance(parser_cfg, dict) else "" + map_llm_id = pipeline_chat_llm_ids.get(str(doc_id)) phase1 = await wiki_map_from_chunks( chunks=batch, chat_mdl=map_llm_pool.wrap( @@ -1621,17 +1677,17 @@ async def run_wiki_incremental( ctx: TaskContext, embedding_model, load_chunks_for_doc: Callable[..., AsyncIterator[list[dict]]], - plan: bool = False, + mode: str | None = None, _map_rebuild_retry: bool = False, ) -> None: """Dual-mode wiki compilation with incremental support. - Mode A (plan=False, default): + Entity mode: 1 concept = 1 page (WeKnora style). MAP → REDUCE → per-concept REFINE → FINALIZE. Incremental: per-concept modify based on doc_change tracking. - Mode B (plan=True): + Topic mode: PLAN groups entities → per-page REFINE. Incremental: embeddings retrieve page candidates; the LLM makes final routes. @@ -1639,7 +1695,7 @@ async def run_wiki_incremental( ctx: Task context embedding_model: Embedding model load_chunks_for_doc: Chunk loader - plan: True=Mode B, False=Mode A (default) + mode: ``entity`` or ``topic`` """ from api.db.services.document_service import DocumentService from api.db.services.compilation_template_service import CompilationTemplateService @@ -1654,7 +1710,7 @@ async def run_wiki_incremental( from rag.advanced_rag.knowlege_compile.structure import LLMCallPool progress = ctx.progress_cb - progress(0.0, f"Loading documents for wiki {'PLAN' if plan else 'no-plan'} compilation...") + progress(0.0, "Loading documents for wiki compilation...") # 1. Check if this is incremental (existing MAP rows present) existing_map_doc_ids = await _wiki_existing_map_doc_ids(ctx.tenant_id, ctx.kb_id) @@ -1699,36 +1755,43 @@ async def run_wiki_incremental( if not eligible and not is_incremental: progress(1.0, "No enabled documents are configured for wiki compilation.") return + pipeline_chat_llm_ids = _validate_wiki_eligible_docs(eligible) if eligible else {} - # Re-resolve plan (Mode B) from the ELIGIBLE docs' templates. Each eligible + # Resolve mode from the eligible documents' templates. Each eligible # doc resolves to a wiki template either via its own parser_config or via # its ingestion pipeline (doc.pipeline_id → pipeline dsl → compiler → - # template). The task handler's `plan` param only looks at the KB-level - # parser_config and therefore misses the pipeline path; re-derive it here so - # a pipeline-bound template with plan=yes actually enables Mode B. - if not plan: - try: - for _doc, tid in eligible: - tpl = CompilationTemplateService.get_saved(tid, ctx.tenant_id) - cfg = (tpl.get("config") or {}) if tpl else {} - if isinstance(cfg, dict) and cfg.get("plan") in (True, "yes", "true"): - plan = True - break - except Exception: - pass # keep the handler-provided plan as fallback + # template). This also covers pipeline-bound templates. + resolved_modes = set() + for _doc, tid in eligible: + tpl = CompilationTemplateService.get_saved(tid, ctx.tenant_id) + cfg = (tpl.get("config") or {}) if tpl else {} + candidate_mode = cfg.get("mode") if isinstance(cfg, dict) else None + if candidate_mode not in ("entity", "topic"): + raise ValueError(f"Wiki template {tid} must define mode as 'entity' or 'topic'") + resolved_modes.add(candidate_mode) - # Mode-change detection. plan toggling (A↔B) is a config change: the page + if len(resolved_modes) > 1: + raise ValueError("Eligible Wiki templates must use the same mode") + if resolved_modes: + mode = resolved_modes.pop() + + if mode is None: + mode = await _wiki_load_mode(ctx.tenant_id, ctx.kb_id) + if mode not in ("entity", "topic"): + raise ValueError("Wiki template mode must be either 'entity' or 'topic'") + + # Mode-change detection. Switching entity/topic is a config change: the page # structures differ fundamentally (single-entity pages vs PLAN-grouped # pages), so switching modes must reset all wiki-derived state and rebuild # from scratch instead of incrementally mixing old-mode and new-mode pages. - prev_plan = await _wiki_load_mode_plan(ctx.tenant_id, ctx.kb_id) + previous_mode = await _wiki_load_mode(ctx.tenant_id, ctx.kb_id) previous_embedding = await _wiki_load_embedding_fingerprint(ctx.tenant_id, ctx.kb_id) current_embedding = _wiki_embedding_fingerprint(embedding_model) - mode_changed = prev_plan is not None and bool(prev_plan) != bool(plan) + mode_changed = is_incremental and previous_mode is not None and previous_mode != mode embedding_changed = bool(previous_embedding and current_embedding and previous_embedding != current_embedding) if is_incremental and (mode_changed or embedding_changed): if mode_changed: - reason = f"Mode switched (plan: {'on' if prev_plan else 'off'} -> {'on' if plan else 'off'})" + reason = f"Mode switched ({previous_mode} -> {mode})" else: reason = "Embedding model changed" progress(0.05, f"{reason}; rebuilding wiki from scratch...") @@ -1737,7 +1800,7 @@ async def run_wiki_incremental( is_incremental = False existing_map_doc_ids = set() deleted_doc_ids = set() - await _wiki_save_mode_plan(ctx.tenant_id, ctx.kb_id, bool(plan), current_embedding) + await _wiki_save_mode(ctx.tenant_id, ctx.kb_id, mode, current_embedding) # 3. Resolve chat model llm_bundle_cache: dict[str, LLMBundle] = {} @@ -1780,10 +1843,7 @@ async def run_wiki_incremental( doc_configs[d["id"]] = cfg if not first_template_found and isinstance(cfg, dict): first_template_found = True - llm_id = (cfg.get("llm_id") or "").strip() - kb_chat_llm_id = llm_id or None - if not kb_chat_llm_id: - kb_chat_llm_id = None + kb_chat_llm_id = pipeline_chat_llm_ids.get(str(d.get("id") or "")) except Exception: logging.exception("wiki: config resolve failed for doc %s", d["id"]) doc_configs[d["id"]] = {} @@ -1811,7 +1871,7 @@ async def run_wiki_incremental( return _, doc, template_id, parser_cfg, batch = item doc_id = doc["id"] - map_llm_id = (parser_cfg.get("llm_id") or "").strip() if isinstance(parser_cfg, dict) else "" + map_llm_id = pipeline_chat_llm_ids.get(str(doc_id)) result = await wiki_map_from_chunks( chunks=batch, @@ -1913,7 +1973,7 @@ async def run_wiki_incremental( ctx=ctx, embedding_model=embedding_model, load_chunks_for_doc=load_chunks_for_doc, - plan=plan, + mode=mode, _map_rebuild_retry=True, ) return @@ -1922,18 +1982,18 @@ async def run_wiki_incremental( # 5. Run incremental wiki compilation (Mode A or Mode B) kb_chat_mdl = _bundle_for(kb_chat_llm_id) if kb_chat_llm_id else _bundle_for(None) - progress(0.65, f"Wiki {'PLAN' if plan else 'no-plan'} incremental compilation ...") + progress(0.65, f"Wiki {mode} incremental compilation ...") summary = await wiki_compile_incremental( chat_mdl=map_llm_pool.wrap( kb_chat_mdl, priority=20, - label=f"wiki-{'plan' if plan else 'noplan'}-refine", + label=f"wiki-{mode}-refine", context=f"{ctx.kb_id}:refine", ), embd_mdl=embedding_model, tenant_id=ctx.tenant_id, kb_id=ctx.kb_id, - plan=plan, + mode=mode, incremental=is_incremental, map_results=all_map_results or None, deleted_doc_ids=deleted_doc_ids or None, diff --git a/rag/svr/task_executor_refactor/task_handler.py b/rag/svr/task_executor_refactor/task_handler.py index 88034de97c..42d33ed156 100644 --- a/rag/svr/task_executor_refactor/task_handler.py +++ b/rag/svr/task_executor_refactor/task_handler.py @@ -260,8 +260,8 @@ class TaskHandler: run_wiki_incremental, ) - # Parse plan: yes/no from the template config (default no-plan) - plan_enabled = False + # Parse the Wiki mode from the template config. + wiki_mode = None try: from api.db.services.compilation_template_service import ( CompilationTemplateService, @@ -274,17 +274,17 @@ class TaskHandler: for tid in _parser_config_compilation_template_ids(pc, self._task_context.tenant_id): tpl = CompilationTemplateService.get_saved(tid, self._task_context.tenant_id) cfg = (tpl.get("config") or {}) if tpl else {} - if isinstance(cfg, dict) and cfg.get("plan") in (True, "yes", "true"): - plan_enabled = True + if isinstance(cfg, dict) and cfg.get("mode") in ("entity", "topic"): + wiki_mode = cfg["mode"] break except Exception: - pass # default to no-plan + pass await run_wiki_incremental( self._task_context, embedding_model, self._load_chunks_for_doc, - plan=plan_enabled, + mode=wiki_mode, ) elif task_type == "skill": from rag.svr.task_executor_refactor.dataset_skill_generator import ( diff --git a/test/integration/wiki/test_wiki_incremental.py b/test/integration/wiki/test_wiki_incremental.py index c1fde374ba..59baad76e5 100644 --- a/test/integration/wiki/test_wiki_incremental.py +++ b/test/integration/wiki/test_wiki_incremental.py @@ -249,12 +249,12 @@ def test_wiki_plan_toggle_resets_state(wiki_dataset): import asyncio from rag.svr.task_executor_refactor import dataset_wiki_generator as dwg - # Mode-A (plan=off) build records plan_kwd=false in the mode meta row. - asyncio.run(dwg._wiki_save_mode_plan(tenant_id, ds_id, False)) - loaded = asyncio.run(dwg._wiki_load_mode_plan(tenant_id, ds_id)) - assert loaded is False, f"expected recorded mode plan=false, got {loaded!r}" + # Entity mode records its mode in the mode meta row. + asyncio.run(dwg._wiki_save_mode(tenant_id, ds_id, "entity")) + loaded = asyncio.run(dwg._wiki_load_mode(tenant_id, ds_id)) + assert loaded == "entity", f"expected recorded entity mode, got {loaded!r}" - # Toggling to plan=true (Mode B) is a config change: run_wiki_incremental + # Switching to topic mode is a config change: run_wiki_incremental # detects prev != new and resets all wiki-derived state so the next build # rebuilds cleanly in the new mode (no mixing of A/B page structures). asyncio.run(dwg._wiki_reset_all_wiki_state(tenant_id, ds_id)) @@ -262,4 +262,4 @@ def test_wiki_plan_toggle_resets_state(wiki_dataset): assert pages_after == 0, "full reset did not clear wiki state" # After reset the mode meta is gone too (first build of the new mode). - assert asyncio.run(dwg._wiki_load_mode_plan(tenant_id, ds_id)) is None, "mode meta was not cleared by reset" + assert asyncio.run(dwg._wiki_load_mode(tenant_id, ds_id)) is None, "mode meta was not cleared by reset" diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx index b49ac35b12..41eb7b0e7e 100644 --- a/web/src/components/originui/select-with-search.tsx +++ b/web/src/components/originui/select-with-search.tsx @@ -299,7 +299,9 @@ export const SelectWithSearch = forwardRef< )} -
+
{hasCustomSearchValue && ( ', value: [] }, }, diff --git a/web/src/pages/agent/form/compilation-form/index.tsx b/web/src/pages/agent/form/compilation-form/index.tsx index 74e1ac1c94..f947c58594 100644 --- a/web/src/pages/agent/form/compilation-form/index.tsx +++ b/web/src/pages/agent/form/compilation-form/index.tsx @@ -1,8 +1,8 @@ import { CompilationTemplateFormField } from '@/components/compilation-template-form-field'; import { LargeModelFormField } from '@/components/large-model-form-field'; -import { SwitchFormField } from '@/components/switch-fom-field'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; import { Form } from '@/components/ui/form'; -import { useTranslate } from '@/hooks/common-hooks'; import { zodResolver } from '@hookform/resolvers/zod'; import { memo } from 'react'; import { useForm } from 'react-hook-form'; @@ -20,13 +20,12 @@ import { Output } from '../components/output'; function useFormSchema() { const { t } = useTranslation(); - const FormSchema = z.object({ compilation_template_group_id: z .string() .min(1, t('knowledgeConfiguration.compilationTemplateRequired')), llm_id: z.string().optional(), - plan: z.boolean(), + mode: z.enum(['entity', 'topic']), }); return FormSchema; @@ -45,7 +44,7 @@ const CompilationForm = ({ }: INextOperatorForm) => { const defaultValues = useFormValues(initialCompilationValues, node); const ownerTenantId = useOwnerTenantId(); - const { t } = useTranslate('setting'); + const { t } = useTranslation(); const FormSchema = useFormSchema(); const form = useForm({ @@ -65,11 +64,22 @@ const CompilationForm = ({ name="llm_id" ownerTenantId={ownerTenantId} > - + + {(field) => ( + + )} + {!hideOutputs && (
diff --git a/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx b/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx index 3050e519b6..3e702e5f39 100644 --- a/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx +++ b/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx @@ -19,7 +19,6 @@ import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; import { SwitchFormField } from '@/components/switch-fom-field'; import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; @@ -219,13 +218,19 @@ export function TemplateConfiguration({ {isArtifacts && ( {(field) => ( - field.onChange(v)} + )} diff --git a/web/src/pages/user-setting/compilation-templates/create-next/constant.ts b/web/src/pages/user-setting/compilation-templates/create-next/constant.ts index 7e5a069931..3126132618 100644 --- a/web/src/pages/user-setting/compilation-templates/create-next/constant.ts +++ b/web/src/pages/user-setting/compilation-templates/create-next/constant.ts @@ -37,7 +37,7 @@ export const DefaultTemplateValues: TemplateSchemaType = { example: '', instruction: '', use_blueprint: false, - plan: true, + mode: 'entity', rechunk: false, rechunk_rules: '', }, diff --git a/web/src/pages/user-setting/compilation-templates/create-next/schema.ts b/web/src/pages/user-setting/compilation-templates/create-next/schema.ts index 90a77e0fb1..a9388f0b4c 100644 --- a/web/src/pages/user-setting/compilation-templates/create-next/schema.ts +++ b/web/src/pages/user-setting/compilation-templates/create-next/schema.ts @@ -43,22 +43,35 @@ export const buildSynthesisSchema = () => .passthrough(); export const buildTemplateSchema = (t: (key: string) => string) => - z.object({ - id: z.string().optional(), - name: z.string().min(1, t('setting.templateNameRequired')), - description: z.string().optional(), - llm_id: z.string().min(1, t('setting.llmForExtractionRequired')), - kind: z.string().min(1, t('setting.templateKindRequired')), - config: z.record( - z.union([ - buildRaptorConfigSchema(t), - buildSectionSchema(t), - buildSynthesisSchema(), - z.string(), - z.boolean(), - ]), - ), - }); + z + .object({ + id: z.string().optional(), + name: z.string().min(1, t('setting.templateNameRequired')), + description: z.string().optional(), + llm_id: z.string().min(1, t('setting.llmForExtractionRequired')), + kind: z.string().min(1, t('setting.templateKindRequired')), + config: z.record( + z.union([ + buildRaptorConfigSchema(t), + buildSectionSchema(t), + buildSynthesisSchema(), + z.string(), + z.boolean(), + ]), + ), + }) + .superRefine((template, context) => { + if ( + template.kind === 'wiki' && + !['entity', 'topic'].includes(String(template.config.mode)) + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['config', 'mode'], + message: t('setting.wikiModeRequired'), + }); + } + }); export const buildFormSchema = (t: (key: string) => string) => z.object({ diff --git a/web/src/pages/user-setting/compilation-templates/create-next/utils.ts b/web/src/pages/user-setting/compilation-templates/create-next/utils.ts index 17469eb126..1a01d35143 100644 --- a/web/src/pages/user-setting/compilation-templates/create-next/utils.ts +++ b/web/src/pages/user-setting/compilation-templates/create-next/utils.ts @@ -56,7 +56,7 @@ export const isConfigMetaKey = (key: string) => 'instruction', 'synthesis', 'use_blueprint', - 'plan', + 'mode', 'rechunk', 'rechunk_rules', ].includes(key); @@ -115,10 +115,10 @@ export const buildConfigFromBuiltin = ( use_blueprint: kind === CompilationTemplateKind.Artifacts && (instruction.length > 0 || example.length > 0), - plan: - typeof builtinTemplate.config?.plan === 'boolean' - ? builtinTemplate.config.plan - : true, + mode: + typeof builtinTemplate.config?.mode === 'string' + ? builtinTemplate.config.mode + : '', ...(kind !== CompilationTemplateKind.Tree ? { rechunk: builtinTemplate.config?.rechunk === true, @@ -187,7 +187,7 @@ export const transformDetailToForm = ( : {}), use_blueprint: detail.kind === CompilationTemplateKind.Artifacts && hasBlueprintContent, - plan: typeof config.plan === 'boolean' ? config.plan : true, + mode: typeof config.mode === 'string' ? config.mode : '', ...(detail.kind !== CompilationTemplateKind.Tree ? { rechunk: config.rechunk === true, @@ -273,7 +273,7 @@ export const transformTemplateToPayload = (template: TemplateSchemaType) => { config[key] = value as ICompilationTemplateConfigRequest[string]; return; } - if (key === 'plan') { + if (key === 'mode') { config[key] = value as ICompilationTemplateConfigRequest[string]; return; } diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/template-configuration.tsx b/web/src/pages/user-setting/compilation-templates/edit-next/components/template-configuration.tsx index 1b256478db..be5e5b8f88 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/components/template-configuration.tsx +++ b/web/src/pages/user-setting/compilation-templates/edit-next/components/template-configuration.tsx @@ -203,11 +203,23 @@ export function TemplateConfiguration({ {kind === CompilationTemplateKind.Artifacts && ( - + + {(field) => ( + + )} + )} {kind === CompilationTemplateKind.Tree ? ( diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/constant.ts b/web/src/pages/user-setting/compilation-templates/edit-next/constant.ts index aa76e7d902..6699dd8480 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/constant.ts +++ b/web/src/pages/user-setting/compilation-templates/edit-next/constant.ts @@ -35,7 +35,7 @@ export const DefaultTemplateValues: TemplateSchemaType = { example: '', instruction: '', use_blueprint: false, - plan: true, + mode: 'entity', rechunk: false, rechunk_rules: '', }, diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/schema.ts b/web/src/pages/user-setting/compilation-templates/edit-next/schema.ts index 1171b43284..3dcc63b6b3 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/schema.ts +++ b/web/src/pages/user-setting/compilation-templates/edit-next/schema.ts @@ -43,21 +43,34 @@ export const buildSynthesisSchema = () => .passthrough(); export const buildTemplateSchema = (t: (key: string) => string) => - z.object({ - id: z.string().optional(), - name: z.string().min(1, t('setting.templateNameRequired')), - description: z.string().optional(), - kind: z.string().min(1, t('setting.templateKindRequired')), - config: z.record( - z.union([ - buildRaptorConfigSchema(t), - buildSectionSchema(t), - buildSynthesisSchema(), - z.string(), - z.boolean(), - ]), - ), - }); + z + .object({ + id: z.string().optional(), + name: z.string().min(1, t('setting.templateNameRequired')), + description: z.string().optional(), + kind: z.string().min(1, t('setting.templateKindRequired')), + config: z.record( + z.union([ + buildRaptorConfigSchema(t), + buildSectionSchema(t), + buildSynthesisSchema(), + z.string(), + z.boolean(), + ]), + ), + }) + .superRefine((template, context) => { + if ( + template.kind === 'wiki' && + !['entity', 'topic'].includes(String(template.config.mode)) + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['config', 'mode'], + message: t('setting.wikiModeRequired'), + }); + } + }); export const buildFormSchema = (t: (key: string) => string) => z.object({ diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts b/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts index f5065fa0dc..99e89f980b 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts +++ b/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts @@ -52,7 +52,7 @@ export const isConfigMetaKey = (key: string) => 'instruction', 'synthesis', 'use_blueprint', - 'plan', + 'mode', 'rechunk', 'rechunk_rules', ].includes(key); @@ -109,10 +109,10 @@ export const buildConfigFromBuiltin = ( use_blueprint: kind === CompilationTemplateKind.Artifacts && (instruction.length > 0 || example.length > 0), - plan: - typeof builtinTemplate.config?.plan === 'boolean' - ? builtinTemplate.config.plan - : true, + mode: + typeof builtinTemplate.config?.mode === 'string' + ? builtinTemplate.config.mode + : '', ...(kind !== CompilationTemplateKind.Tree ? { rechunk: builtinTemplate.config?.rechunk === true, @@ -180,7 +180,7 @@ export const transformDetailToForm = ( : {}), use_blueprint: detail.kind === CompilationTemplateKind.Artifacts && hasBlueprintContent, - plan: typeof config.plan === 'boolean' ? config.plan : true, + mode: typeof config.mode === 'string' ? config.mode : '', ...(detail.kind !== CompilationTemplateKind.Tree ? { rechunk: config.rechunk === true, @@ -263,7 +263,7 @@ export const transformTemplateToPayload = (template: TemplateSchemaType) => { config[key] = value as ICompilationTemplateConfigRequest[string]; return; } - if (key === 'plan') { + if (key === 'mode') { config[key] = value as ICompilationTemplateConfigRequest[string]; return; }