Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj
# Conflicts: # frontend-web/src/components/workspace/messages/message-group.tsx # offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py
This commit is contained in:
commit
57b6a89f20
21 files changed
+441
-115
No files matched your search
@@ -357,7 +357,7 @@ FastAPI application on port 8001 with health checks at `GET|HEAD /health`, `/api
|
||||
| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail |
|
||||
| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` |
|
||||
| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types |
|
||||
| **Word export** (`POST /api/writing/export/docx`) | Shared Markdown → DOCX generator used by conversation, artifact, scheduled-task, Deep Research, and AI-writing download surfaces. `app/gateway/word_export.py` builds OOXML with no Python conversion dependency: A4 portrait, 3.7/3.5/2.8/2.6 cm margins, real multilevel heading numbering (`一、` / `(一)` / `1.`), two-character first-line indent on heading levels 1–3, formal body/table styles, page numbers beginning at 1 with odd pages left-aligned and even pages right-aligned, and embedded `app/gateway/assets/fonts/方正小标宋简体.ttf`. Before numbering it removes complete manual heading prefixes (including `2.1` / `2.1.1`) and drops Markdown horizontal rules from Word output. The default ordinary-Q&A Lead Agent prompt emits the matching Markdown heading forms and avoids unordered bullet lists; writing setup, writing mode, notebook, scheduled runs, and SOUL-driven custom agents keep their independent prompts. The font loader validates its internal family, SHA-256, and OpenType embedding permission (`OS/2.fsType`) before export. Tests: `tests/test_word_export.py`, `tests/test_es_query_routing_prompt.py`. |
|
||||
| **Word export** (`POST /api/writing/export/docx`) | Shared Markdown → DOCX generator used by conversation, artifact, scheduled-task, Deep Research, and AI-writing download surfaces. `app/gateway/word_export.py` builds OOXML with no Python conversion dependency: A4 portrait, 3.7/3.5/2.8/2.6 cm margins, real multilevel heading numbering (`一、` / `(一)` / `1.`), two-character first-line indent on heading levels 1–3, formal body/table styles, page numbers beginning at 1 with odd pages left-aligned and even pages right-aligned, and embedded `app/gateway/assets/fonts/方正小标宋简体.ttf`. Before numbering it removes complete manual heading prefixes (including `2.1` / `2.1.1`) and drops Markdown horizontal rules from Word output. The ordinary-Q&A Lead Agent can emit the matching Markdown heading forms and avoid unordered bullet lists when the admin enables `prompt_prefix.ordinary_qa_markdown_format_enabled` in `system_settings.json`; the switch defaults off, while writing setup, writing mode, notebook, scheduled runs, and SOUL-driven custom agents keep their independent prompts. The font loader validates its internal family, SHA-256, and OpenType embedding permission (`OS/2.fsType`) before export. Tests: `tests/test_word_export.py`, `tests/test_es_query_routing_prompt.py`. |
|
||||
| **Deep Research** (`/api/deep-research`) | Durable, user-owned research sessions/jobs with replayable SSE events and hidden-thread artifacts. `basic`/`quick` use the compact runner; `detailed` creates bounded subtopics, searches/writes sections concurrently, and assembles a report; `deep` recursively searches follow-up questions under a deterministic breadth/depth query budget. `multi_agent` is a LangGraph 1.x editor/researcher/writer/reviewer graph: its initial plan emits `awaiting_input`, the recorder stores the interrupt and releases the lease, and `POST /jobs/{id}/resume` rebuilds the graph from the persisted approved/revised plan. A bounded `custom_outline` is frozen into the session; headings/list entries deterministically supply the detailed and multi-agent plan, and the shared writer receives it only as a guarded content constraint. Completed reports support owner-scoped follow-up (`GET /sessions/{id}/messages`, `POST /sessions/{id}/chat`): context is only the current report, selected sources and recent messages; source-id citations are persisted, while `allow_new_research=true` is explicitly rejected. Owners may change that evidence set only after the job stops via `PATCH /sessions/{id}/sources/{source_id}`; this applies to future follow-ups rather than rewriting a completed report. Optional report illustrations are gated by `config.yaml -> deep_research.image_generation`: the Harness `OpenAICompatibleImageProvider` requires base64 output, the shared runner step writes only bounded `research-image-01..04.(png|jpg|webp)` artifacts and Markdown uses private `deep-research://` links. `GET /sessions/{id}/artifacts/{name}` checks session ownership before serving an image; an unavailable provider yields a warning but never fails the text report. Runners live in `packages/harness/deerflow/agents/deep_research/runners/`, may never import `app.*`, and use only the `AdapterBundle`. |
|
||||
| **Gateway auth** | Owner-scoped Deep Research routes await the platform's asynchronous `get_current_user` resolver before they read or write persistence, ensuring each session and job operation receives a concrete user ID. Deep Research artifacts are always resolved from a whitelisted filename via `/mnt/user-data/outputs/<name>` before mapping to a host path, preserving the sandbox guard on native Windows and Linux hosts. |
|
||||
| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing |
|
||||
@@ -510,6 +510,20 @@ Admin-configurable, **access-control-free** categories that published agents/ski
|
||||
|
||||
Each agent/skill carries a single `square_id` column (`AgentRow` / `SkillRow`, `String(64)` NOT NULL default `''`, migration `20260611_01`, idempotent SQL in `column_additions_mysql.sql`). One column = at most one square, so an item can never be "published twice". `''` = unassigned ? resolved to the default square at read/filter time. Writes normalize via `normalize_square_id`: agents on create/update (`square_id` field), skills on publish (`PUT /api/skills/custom/{name}/published` accepts optional `square_id`). Listing filters: `GET /api/agents?square=` and `GET /api/skills?square=` keep only that square's items; the **default** square additionally sweeps in legacy `''` rows (`square_is_default`). Tests: `tests/test_publish_squares.py`. Frontend: `core/system-settings` (`usePublishSquares`/`useUpdatePublishSquares`), admin `PublishSquaresSettingsPage` (embedded in the appearance/settings hub), square picker in the agent edit dialog + per-skill card, and a ??-tab square sub-filter in both galleries.
|
||||
|
||||
### Reference-mode Entry Switch
|
||||
|
||||
The 参考文献/RAG citation-mode composer entry is system-controlled through
|
||||
`system_settings.json` → `citation_display.reference_mode_enabled` (default
|
||||
`false`). `GET /api/system-settings/citation-display` remains readable so the
|
||||
frontend can hide or show the entry consistently; `PUT` is admin-only and
|
||||
partially updates `reference_mode_enabled` and/or `cleanup_words`. The normal
|
||||
workspace input box, iframe chat toolbar, send context, right reference panel,
|
||||
and appearance settings all use this flag, while already locked citation
|
||||
threads and selected LLMWiki knowledge bases still render their source
|
||||
documents in the reference panel. Tests:
|
||||
`tests/test_prompt_prefix.py` and
|
||||
`frontend-web/src/pages/iframe-chat-mode-buttons.test.ts`.
|
||||
|
||||
### Agent Module Tag Config (zzJC / zzXD 按类型聚合)
|
||||
|
||||
Backs the zzJC / zzXD agent-chat left sidebar (「按类型聚合智能体 + 聊天记录」). An admin picks which agent **tags** belong to a module; the module's left sidebar then shows only agents under those tags (empty/unconfigured = show all visible agents). Stored in `system_settings.json` → `agent_modules` (`AgentModuleSettings`: `modules: {module_key → [tag_id, ...]}`); pure helpers `get_module_tag_ids` / `set_module_tag_ids` (de-dupe + strip). Endpoints (in `app/gateway/routers/system_settings.py`): `GET /api/system-settings/agent-modules/{key}` (readable by any user — the sidebar needs it to filter), `PUT /api/system-settings/agent-modules/{key}` (admin only, full replacement of the tag-id list). Module keys are frontend-defined (`AGENT_MODULES` in `components/workspace/agents/agent-module-sidebar.tsx`, keyed by the module's landing agent id: `6bf`→`zzjc`, `10d69…`→`zzxd`). Tests: `tests/test_agent_modules.py`. Frontend: `core/system-settings` (`useAgentModuleConfig`/`useUpdateAgentModuleConfig`), admin `AgentModuleSettingsDialog` (gear button in the agent-chat header).
|
||||
|
||||
@@ -679,6 +679,29 @@ login and theme query parameters, each shortcut may include ordered keyless
|
||||
it as an encoded path segment (including inside a `#/…` hash route); password
|
||||
remains an ordinary login query parameter with the key `password`.
|
||||
|
||||
### Citation display settings
|
||||
|
||||
Reference-mode entry points are controlled by `citation_display` in
|
||||
`.deer-flow/system_settings.json`. `reference_mode_enabled` defaults to `false`;
|
||||
when disabled, the frontend hides the "参考文献" composer button in both the
|
||||
normal workspace and iframe chat, and hides the per-user RAG citation toggle
|
||||
from appearance settings. Conversations with selected LLMWiki knowledge bases
|
||||
still show their source documents in the reference panel even when the manual
|
||||
entry is hidden. `GET /api/system-settings/citation-display` is
|
||||
readable by logged-in users so the UI can decide whether to expose the entry;
|
||||
`PUT /api/system-settings/citation-display` is admin-only and supports partial
|
||||
updates for both `reference_mode_enabled` and `cleanup_words`.
|
||||
|
||||
### Ordinary Q&A Markdown format switch
|
||||
|
||||
Administrators can opt into the formal long-answer Markdown prompt from the
|
||||
admin "问答提示词配置" panel. The value is persisted as
|
||||
`prompt_prefix.ordinary_qa_markdown_format_enabled` in
|
||||
`.deer-flow/system_settings.json` and defaults to `false`. When enabled, only
|
||||
ordinary Q&A receives the `# 主标题` / `## 一、标题` / `### (一) 标题` /
|
||||
`#### 1. 标题` rules; writing mode, notebook mode and scheduled runs remain
|
||||
unaffected. The change takes effect when the next prompt is built.
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@@ -42,12 +42,17 @@ class PromptPrefixResponse(BaseModel):
|
||||
enabled: bool = Field(..., description="Whether the toggle is exposed to users")
|
||||
switch_label: str = Field(..., description="Toggle label shown in the chat input")
|
||||
prompt_prefix: str = Field(..., description="Prefix text prepended to the first user message")
|
||||
ordinary_qa_markdown_format_enabled: bool = Field(
|
||||
default=False,
|
||||
description="Whether formal Markdown heading rules are injected into ordinary Q&A system prompts",
|
||||
)
|
||||
|
||||
|
||||
class PromptPrefixUpdateRequest(BaseModel):
|
||||
enabled: bool | None = None
|
||||
switch_label: str | None = None
|
||||
prompt_prefix: str | None = None
|
||||
ordinary_qa_markdown_format_enabled: bool | None = None
|
||||
|
||||
|
||||
class PluginDomainAgentMappingItem(BaseModel):
|
||||
@@ -82,10 +87,12 @@ class PrivateSquareUpdateRequest(BaseModel):
|
||||
|
||||
|
||||
class CitationDisplayResponse(BaseModel):
|
||||
reference_mode_enabled: bool = Field(default=False, description="Whether reference-mode entry points are exposed to users")
|
||||
cleanup_words: list[str] = Field(default_factory=list, description="Global citation cleanup words")
|
||||
|
||||
|
||||
class CitationDisplayUpdateRequest(BaseModel):
|
||||
reference_mode_enabled: bool | None = None
|
||||
cleanup_words: list[str] | None = None
|
||||
|
||||
|
||||
@@ -110,6 +117,7 @@ def _to_response(settings: PromptPrefixSettings) -> PromptPrefixResponse:
|
||||
enabled=settings.enabled,
|
||||
switch_label=settings.switch_label,
|
||||
prompt_prefix=settings.prompt_prefix,
|
||||
ordinary_qa_markdown_format_enabled=settings.ordinary_qa_markdown_format_enabled,
|
||||
)
|
||||
|
||||
|
||||
@@ -410,9 +418,12 @@ async def update_leaderboard_settings(
|
||||
|
||||
@router.get("/citation-display", response_model=CitationDisplayResponse)
|
||||
async def get_citation_display() -> CitationDisplayResponse:
|
||||
"""Return global citation display cleanup settings."""
|
||||
"""Return global citation display settings."""
|
||||
settings = load_system_settings().citation_display
|
||||
return CitationDisplayResponse(cleanup_words=settings.cleanup_words)
|
||||
return CitationDisplayResponse(
|
||||
reference_mode_enabled=settings.reference_mode_enabled,
|
||||
cleanup_words=settings.cleanup_words,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/citation-display", response_model=CitationDisplayResponse)
|
||||
@@ -420,14 +431,22 @@ async def update_citation_display(
|
||||
body: CitationDisplayUpdateRequest,
|
||||
request: Request,
|
||||
) -> CitationDisplayResponse:
|
||||
"""Update global citation display cleanup settings. Admin only."""
|
||||
"""Update global citation display settings. Admin only."""
|
||||
await _require_admin(request)
|
||||
|
||||
current = load_system_settings()
|
||||
words = [str(item).strip() for item in (body.cleanup_words or []) if str(item).strip()]
|
||||
updated = current.citation_display.model_copy(update={"cleanup_words": words})
|
||||
fields = body.model_dump(exclude_unset=True)
|
||||
update: dict[str, object] = {}
|
||||
if "reference_mode_enabled" in fields and fields["reference_mode_enabled"] is not None:
|
||||
update["reference_mode_enabled"] = bool(fields["reference_mode_enabled"])
|
||||
if "cleanup_words" in fields and fields["cleanup_words"] is not None:
|
||||
update["cleanup_words"] = [str(item).strip() for item in fields["cleanup_words"] if str(item).strip()]
|
||||
updated = current.citation_display.model_copy(update=update)
|
||||
saved = save_system_settings(current.model_copy(update={"citation_display": updated}))
|
||||
return CitationDisplayResponse(cleanup_words=saved.citation_display.cleanup_words)
|
||||
return CitationDisplayResponse(
|
||||
reference_mode_enabled=saved.citation_display.reference_mode_enabled,
|
||||
cleanup_words=saved.citation_display.cleanup_words,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/private-square", response_model=PrivateSquareStatusResponse)
|
||||
|
||||
+23
-9
@@ -9,7 +9,8 @@ from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from deerflow.config.agents_config import load_agent_soul
|
||||
from deerflow.config.agents_config import load_agent_soul
|
||||
from deerflow.config.system_settings import load_system_settings
|
||||
from deerflow.skills.storage import get_or_new_skill_storage
|
||||
from deerflow.skills.types import Skill, SkillCategory
|
||||
from deerflow.subagents import get_available_subagent_names
|
||||
@@ -486,7 +487,7 @@ task(description="Oracle Cloud analysis", prompt="...", subagent_type="general-p
|
||||
</subagent_system>"""
|
||||
|
||||
|
||||
_ORDINARY_QA_MARKDOWN_FORMAT_SECTION = """
|
||||
_ORDINARY_QA_MARKDOWN_FORMAT_SECTION = """
|
||||
<ordinary_qa_markdown_format>
|
||||
当普通问答需要输出带章节结构的较长内容时,必须采用以下 Markdown 规范,以便下载为 Word 后得到正确的自动编号和公文标题格式:
|
||||
- 文档主标题使用 `# 主标题`;主标题只写标题文字,不添加章节编号。
|
||||
@@ -495,10 +496,19 @@ _ORDINARY_QA_MARKDOWN_FORMAT_SECTION = """
|
||||
- 默认禁止使用以 `-`、`*`、`+` 或 `•` 开头的无序圆点列表;并列内容优先改写为完整自然段,确需逐项表达时使用有序列表。只有用户明确要求无序列表时才使用。
|
||||
- 简短问答不必为了套用格式而强行添加标题;本规范只约束实际使用的标题和列表。
|
||||
</ordinary_qa_markdown_format>
|
||||
"""
|
||||
|
||||
|
||||
SYSTEM_PROMPT_TEMPLATE = """
|
||||
"""
|
||||
|
||||
|
||||
def _ordinary_qa_markdown_format_enabled() -> bool:
|
||||
"""Return the admin-controlled formal Markdown prompt switch."""
|
||||
try:
|
||||
return bool(load_system_settings().prompt_prefix.ordinary_qa_markdown_format_enabled)
|
||||
except Exception:
|
||||
logger.debug("Failed to read the ordinary Q&A Markdown format switch; using disabled", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
SYSTEM_PROMPT_TEMPLATE = """
|
||||
<role>
|
||||
You are {agent_name}, an internal super agent.
|
||||
</role>
|
||||
@@ -1537,9 +1547,13 @@ When a search, knowledge, skill, or MCP tool returns a result list:
|
||||
if rag_mode_enabled
|
||||
else ""
|
||||
)
|
||||
ordinary_qa_format_section = (
|
||||
_ORDINARY_QA_MARKDOWN_FORMAT_SECTION
|
||||
if not is_bootstrap and not writing_mode and not is_notebook_mode and not is_scheduled_run
|
||||
ordinary_qa_format_section = (
|
||||
_ORDINARY_QA_MARKDOWN_FORMAT_SECTION
|
||||
if _ordinary_qa_markdown_format_enabled()
|
||||
and not is_bootstrap
|
||||
and not writing_mode
|
||||
and not is_notebook_mode
|
||||
and not is_scheduled_run
|
||||
else ""
|
||||
)
|
||||
|
||||
|
||||
+9
-1
@@ -153,6 +153,10 @@ class PromptPrefixSettings(BaseModel):
|
||||
enabled: bool = Field(default=False, description="Whether the prefix toggle is offered to users at all")
|
||||
switch_label: str = Field(default="加入立场前缀", description="Toggle label shown in the chat input")
|
||||
prompt_prefix: str = Field(default="", description="Text prepended to the first user message when enabled")
|
||||
ordinary_qa_markdown_format_enabled: bool = Field(
|
||||
default=False,
|
||||
description="Whether the formal Markdown heading rules are injected into ordinary Q&A system prompts.",
|
||||
)
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
@@ -255,8 +259,12 @@ class AppearanceSettings(BaseModel):
|
||||
|
||||
|
||||
class CitationDisplaySettings(BaseModel):
|
||||
"""System-wide cleanup rules for citation cards and detail dialogs."""
|
||||
"""System-wide controls for citation mode entry and display cleanup."""
|
||||
|
||||
reference_mode_enabled: bool = Field(
|
||||
default=False,
|
||||
description="Whether the reference-mode entry points are exposed to users.",
|
||||
)
|
||||
cleanup_words: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Words or phrases used to remove noisy advertisement/disclaimer lines from citation display.",
|
||||
|
||||
@@ -112,6 +112,7 @@ def test_es_query_routing_registration_log_explains_custom_agent_exclusion(caplo
|
||||
|
||||
def test_formal_markdown_rules_are_scoped_to_ordinary_qa(monkeypatch) -> None:
|
||||
config = _app_config(enabled=False)
|
||||
monkeypatch.setattr(prompt_module, "_ordinary_qa_markdown_format_enabled", lambda: True)
|
||||
monkeypatch.setattr(prompt_module, "_get_memory_context", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "get_agent_soul", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "get_skills_prompt_section", lambda *args, **kwargs: "")
|
||||
@@ -134,3 +135,19 @@ def test_formal_markdown_rules_are_scoped_to_ordinary_qa(monkeypatch) -> None:
|
||||
assert '<ordinary_qa_markdown_format>' not in writing_prompt
|
||||
assert '<ordinary_qa_markdown_format>' not in notebook_prompt
|
||||
assert '<ordinary_qa_markdown_format>' not in scheduled_prompt
|
||||
|
||||
|
||||
def test_formal_markdown_rules_are_disabled_when_switch_is_off(monkeypatch) -> None:
|
||||
config = _app_config(enabled=False)
|
||||
monkeypatch.setattr(prompt_module, "_ordinary_qa_markdown_format_enabled", lambda: False)
|
||||
monkeypatch.setattr(prompt_module, "_get_memory_context", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "get_agent_soul", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "get_skills_prompt_section", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "get_deferred_tools_prompt_section", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "_build_acp_section", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "_build_custom_mounts_section", lambda *args, **kwargs: "")
|
||||
monkeypatch.setattr(prompt_module, "_current_time_context", lambda: "")
|
||||
|
||||
ordinary_prompt = prompt_module.apply_prompt_template(app_config=config)
|
||||
|
||||
assert '<ordinary_qa_markdown_format>' not in ordinary_prompt
|
||||
@@ -9,7 +9,9 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from app.gateway.routers import system_settings as ss_router
|
||||
from app.gateway.services import _prepend_prompt_prefix, _prompt_prefix_opted_in
|
||||
from deerflow.agents.lead_agent import prompt as prompt_module
|
||||
from deerflow.config import system_settings as ss
|
||||
|
||||
|
||||
@@ -28,6 +30,9 @@ def test_load_returns_defaults_when_missing(tmp_runtime_home: Path) -> None:
|
||||
settings = ss.load_system_settings()
|
||||
assert settings.prompt_prefix.enabled is False
|
||||
assert settings.prompt_prefix.prompt_prefix == ""
|
||||
assert settings.prompt_prefix.ordinary_qa_markdown_format_enabled is False
|
||||
assert settings.citation_display.reference_mode_enabled is False
|
||||
assert settings.citation_display.cleanup_words == []
|
||||
|
||||
|
||||
def test_save_then_load_roundtrips(tmp_runtime_home: Path) -> None:
|
||||
@@ -36,6 +41,7 @@ def test_save_then_load_roundtrips(tmp_runtime_home: Path) -> None:
|
||||
enabled=True,
|
||||
switch_label="加入立场前缀",
|
||||
prompt_prefix="你需要站在中方角度",
|
||||
ordinary_qa_markdown_format_enabled=True,
|
||||
)
|
||||
)
|
||||
ss.save_system_settings(settings)
|
||||
@@ -49,6 +55,88 @@ def test_save_then_load_roundtrips(tmp_runtime_home: Path) -> None:
|
||||
assert reloaded.prompt_prefix.enabled is True
|
||||
assert reloaded.prompt_prefix.switch_label == "加入立场前缀"
|
||||
assert reloaded.prompt_prefix.prompt_prefix == "你需要站在中方角度"
|
||||
assert reloaded.prompt_prefix.ordinary_qa_markdown_format_enabled is True
|
||||
|
||||
|
||||
def test_runtime_markdown_format_switch_defaults_off_and_updates_without_restart(
|
||||
tmp_runtime_home: Path,
|
||||
) -> None:
|
||||
assert prompt_module._ordinary_qa_markdown_format_enabled() is False
|
||||
|
||||
ss.save_system_settings(
|
||||
ss.SystemSettings(
|
||||
prompt_prefix=ss.PromptPrefixSettings(
|
||||
ordinary_qa_markdown_format_enabled=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert prompt_module._ordinary_qa_markdown_format_enabled() is True
|
||||
|
||||
|
||||
def _patch_admin(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
admin = SimpleNamespace(
|
||||
id="admin1",
|
||||
system_role="admin",
|
||||
email="admin@example.com",
|
||||
)
|
||||
|
||||
async def _optional(_request):
|
||||
return admin
|
||||
|
||||
monkeypatch.setattr(ss_router, "get_optional_user_from_request", _optional)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_settings_update_can_enable_markdown_format_without_changing_prefix(
|
||||
tmp_runtime_home: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_patch_admin(monkeypatch)
|
||||
current = ss.SystemSettings(
|
||||
prompt_prefix=ss.PromptPrefixSettings(
|
||||
enabled=True,
|
||||
switch_label="加入立场前缀",
|
||||
prompt_prefix="你需要站在中方角度",
|
||||
)
|
||||
)
|
||||
ss.save_system_settings(current)
|
||||
|
||||
response = await ss_router.update_prompt_prefix(
|
||||
ss_router.PromptPrefixUpdateRequest(ordinary_qa_markdown_format_enabled=True),
|
||||
SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert response.ordinary_qa_markdown_format_enabled is True
|
||||
assert response.enabled is True
|
||||
assert response.switch_label == "加入立场前缀"
|
||||
assert response.prompt_prefix == "你需要站在中方角度"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_citation_display_update_preserves_unspecified_fields(
|
||||
tmp_runtime_home: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_patch_admin(monkeypatch)
|
||||
current = ss.SystemSettings(
|
||||
citation_display=ss.CitationDisplaySettings(
|
||||
cleanup_words=["广告", "免责声明"],
|
||||
)
|
||||
)
|
||||
ss.save_system_settings(current)
|
||||
|
||||
response = await ss_router.update_citation_display(
|
||||
ss_router.CitationDisplayUpdateRequest(reference_mode_enabled=True),
|
||||
SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert response.reference_mode_enabled is True
|
||||
assert response.cleanup_words == ["广告", "免责声明"]
|
||||
|
||||
payload = json.loads((tmp_runtime_home / "system_settings.json").read_text(encoding="utf-8"))
|
||||
assert payload["citation_display"]["reference_mode_enabled"] is True
|
||||
assert payload["citation_display"]["cleanup_words"] == ["广告", "免责声明"]
|
||||
|
||||
|
||||
def test_prepend_prefix_tags_first_human_without_touching_content() -> None:
|
||||
|
||||
Reference in new issue
Block a user