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
@@ -22,6 +22,7 @@ import { latestInFlightWriteFilePreview } from "@/core/artifacts/write-file-in-f
|
|||||||
import { shouldAutoOpenWritingArtifact } from "@/core/threads/writing-mode";
|
import { shouldAutoOpenWritingArtifact } from "@/core/threads/writing-mode";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||||
|
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||||
import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag";
|
import { isSingleRoundRagMessage } from "@/core/threads/use-single-round-rag";
|
||||||
import { isMultiRoundRagMessage } from "@/core/threads/use-multi-round-rag";
|
import { isMultiRoundRagMessage } from "@/core/threads/use-multi-round-rag";
|
||||||
import { useSkills } from "@/core/skills/hooks";
|
import { useSkills } from "@/core/skills/hooks";
|
||||||
@@ -87,7 +88,11 @@ const ChatBox: React.FC<{
|
|||||||
threadId,
|
threadId,
|
||||||
disableArtifacts = false,
|
disableArtifacts = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { isMock, thread } = useThread();
|
const {
|
||||||
|
isMock,
|
||||||
|
thread,
|
||||||
|
knowledgeReferencesEnabled = false,
|
||||||
|
} = useThread();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const threadIdRef = useRef(threadId);
|
const threadIdRef = useRef(threadId);
|
||||||
const artifactsCountRef = useRef(0);
|
const artifactsCountRef = useRef(0);
|
||||||
@@ -106,8 +111,13 @@ const ChatBox: React.FC<{
|
|||||||
const writingModeEnabled = !!settings.context.writing_mode;
|
const writingModeEnabled = !!settings.context.writing_mode;
|
||||||
const artifactDirectoryModeEnabled =
|
const artifactDirectoryModeEnabled =
|
||||||
settings.display.artifactDirectoryModeEnabled ?? false;
|
settings.display.artifactDirectoryModeEnabled ?? false;
|
||||||
|
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||||
|
const referenceModeEntryEnabled =
|
||||||
|
citationDisplay?.reference_mode_enabled ?? false;
|
||||||
const ragModeEnabled =
|
const ragModeEnabled =
|
||||||
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
(referenceModeEntryEnabled && settings.display.ragModeEnabled) ||
|
||||||
|
isThreadRagModeLocked(threadId) ||
|
||||||
|
knowledgeReferencesEnabled;
|
||||||
// Frontend RAG modes write their own hidden source tool messages. They use
|
// Frontend RAG modes write their own hidden source tool messages. They use
|
||||||
// the existing reference panel, but must not request or write reference
|
// the existing reference panel, but must not request or write reference
|
||||||
// batches through DeerFlow's backend APIs.
|
// batches through DeerFlow's backend APIs.
|
||||||
@@ -121,7 +131,7 @@ const ChatBox: React.FC<{
|
|||||||
);
|
);
|
||||||
const referencesEnabled = ragModeEnabled || hasFrontendRag;
|
const referencesEnabled = ragModeEnabled || hasFrontendRag;
|
||||||
const shouldFetchBackendReferenceBatches =
|
const shouldFetchBackendReferenceBatches =
|
||||||
referencesEnabled && !hasFrontendRag;
|
referencesEnabled && !hasFrontendRag && !!threadId && threadId !== "new";
|
||||||
const { skills } = useSkills();
|
const { skills } = useSkills();
|
||||||
|
|
||||||
const canScanArtifactDirectory =
|
const canScanArtifactDirectory =
|
||||||
|
|||||||
@@ -93,14 +93,14 @@ import {
|
|||||||
|
|
||||||
import { useRecommendedQuestions } from "@/core/recommended-questions/hooks";
|
import { useRecommendedQuestions } from "@/core/recommended-questions/hooks";
|
||||||
import { useUserPreferences } from "@/core/user-preferences";
|
import { useUserPreferences } from "@/core/user-preferences";
|
||||||
import {
|
import {
|
||||||
isThreadRagModeLocked,
|
isThreadRagModeLocked,
|
||||||
lockThreadRagMode,
|
lockThreadRagMode,
|
||||||
useLocalSettings,
|
useLocalSettings,
|
||||||
} from "@/core/settings";
|
} from "@/core/settings";
|
||||||
|
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||||
|
|
||||||
import { CustomPromptBar } from "./custom-prompt-menu";
|
import { CustomPromptBar } from "./custom-prompt-menu";
|
||||||
import { useThread } from "./messages/context";
|
import { useThread } from "./messages/context";
|
||||||
import { ModeHoverGuide } from "./mode-hover-guide";
|
import { ModeHoverGuide } from "./mode-hover-guide";
|
||||||
import { Tooltip } from "./tooltip";
|
import { Tooltip } from "./tooltip";
|
||||||
@@ -221,9 +221,12 @@ export function InputBox({
|
|||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
return singleRoundRagActive ? extraModels : models;
|
return singleRoundRagActive ? extraModels : models;
|
||||||
}, [extraModels, models, singleRoundRagActive]);
|
}, [extraModels, models, singleRoundRagActive]);
|
||||||
const { thread, isMock } = useThread();
|
const { thread, isMock } = useThread();
|
||||||
const { textInput } = usePromptInputController();
|
const { textInput } = usePromptInputController();
|
||||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||||
|
const referenceModeEntryEnabled =
|
||||||
|
citationDisplay?.reference_mode_enabled ?? false;
|
||||||
|
|
||||||
const [followups, setFollowups] = useState<string[]>([]);
|
const [followups, setFollowups] = useState<string[]>([]);
|
||||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||||
@@ -830,9 +833,9 @@ export function InputBox({
|
|||||||
<span className="text-xs font-normal">记忆</span>
|
<span className="text-xs font-normal">记忆</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>}
|
</Tooltip>}
|
||||||
{!frontendRagActive && !hideContextTools && <Tooltip
|
{referenceModeEntryEnabled && !frontendRagActive && !hideContextTools && <Tooltip
|
||||||
content={
|
content={
|
||||||
ragModeEnabled
|
ragModeEnabled
|
||||||
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
||||||
: "参考文献模式已关闭:点击开启"
|
: "参考文献模式已关闭:点击开启"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import { createContext, useContext } from "react";
|
|||||||
import type { RuntimeDisplaySource } from "@/core/skill-display/runtime";
|
import type { RuntimeDisplaySource } from "@/core/skill-display/runtime";
|
||||||
import type { AgentThreadState } from "@/core/threads";
|
import type { AgentThreadState } from "@/core/threads";
|
||||||
|
|
||||||
export interface ThreadContextType {
|
export interface ThreadContextType {
|
||||||
thread: BaseStream<AgentThreadState>;
|
thread: BaseStream<AgentThreadState>;
|
||||||
isMock?: boolean;
|
isMock?: boolean;
|
||||||
/**
|
/**
|
||||||
* When set, the conversation is rendered from a public read-only share.
|
* True when the active conversation has selected LLMWiki knowledge bases.
|
||||||
|
* Knowledge-base Q&A should always expose its source documents, independent
|
||||||
|
* from the admin-controlled manual reference-mode button.
|
||||||
|
*/
|
||||||
|
knowledgeReferencesEnabled?: boolean;
|
||||||
|
/**
|
||||||
|
* When set, the conversation is rendered from a public read-only share.
|
||||||
* Artifact/image URLs then resolve to the unauthenticated
|
* Artifact/image URLs then resolve to the unauthenticated
|
||||||
* `/api/public/shares/{code}/files/...` endpoint instead of the
|
* `/api/public/shares/{code}/files/...` endpoint instead of the
|
||||||
* auth-gated thread artifact route.
|
* auth-gated thread artifact route.
|
||||||
|
|||||||
@@ -83,16 +83,18 @@ import { SourceCard } from "../skill-display-sources";
|
|||||||
import { MarkdownContent } from "./markdown-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
import { SingleRoundRagCitationDialog } from "./message-list-item";
|
import { SingleRoundRagCitationDialog } from "./message-list-item";
|
||||||
|
|
||||||
export function MessageGroup({
|
export function MessageGroup({
|
||||||
className,
|
className,
|
||||||
messages,
|
messages,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
displaySources = [],
|
displaySources = [],
|
||||||
}: {
|
ragModeEnabled = false,
|
||||||
className?: string;
|
}: {
|
||||||
messages: Message[];
|
className?: string;
|
||||||
isLoading?: boolean;
|
messages: Message[];
|
||||||
displaySources?: RuntimeDisplaySource[];
|
isLoading?: boolean;
|
||||||
|
displaySources?: RuntimeDisplaySource[];
|
||||||
|
ragModeEnabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showAbove, setShowAbove] = useState(
|
const [showAbove, setShowAbove] = useState(
|
||||||
@@ -110,9 +112,9 @@ export function MessageGroup({
|
|||||||
[messages, writingModeEnabled],
|
[messages, writingModeEnabled],
|
||||||
);
|
);
|
||||||
const { skills } = useSkills();
|
const { skills } = useSkills();
|
||||||
const skillDisplayMap = useMemo(
|
const skillDisplayMap = useMemo(
|
||||||
() => (settings.display.ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()),
|
() => (ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()),
|
||||||
[settings.display.ragModeEnabled, skills],
|
[ragModeEnabled, skills],
|
||||||
);
|
);
|
||||||
const lastToolCallStep = useMemo(() => {
|
const lastToolCallStep = useMemo(() => {
|
||||||
const filteredSteps = steps.filter((step) => step.type === "toolCall");
|
const filteredSteps = steps.filter((step) => step.type === "toolCall");
|
||||||
@@ -207,7 +209,7 @@ export function MessageGroup({
|
|||||||
{...step}
|
{...step}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
skillDisplayMap={skillDisplayMap}
|
skillDisplayMap={skillDisplayMap}
|
||||||
ragModeEnabled={settings.display.ragModeEnabled}
|
ragModeEnabled={ragModeEnabled}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
@@ -219,7 +221,7 @@ export function MessageGroup({
|
|||||||
isLast={true}
|
isLast={true}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
skillDisplayMap={skillDisplayMap}
|
skillDisplayMap={skillDisplayMap}
|
||||||
ragModeEnabled={settings.display.ragModeEnabled}
|
ragModeEnabled={ragModeEnabled}
|
||||||
/>
|
/>
|
||||||
</FlipDisplay>
|
</FlipDisplay>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ import {
|
|||||||
hasToolCalls,
|
hasToolCalls,
|
||||||
isHiddenFromUIMessage,
|
isHiddenFromUIMessage,
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||||
import { useSkills } from "@/core/skills/hooks";
|
import { useSkills } from "@/core/skills/hooks";
|
||||||
import {
|
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||||
buildSkillDisplayMap,
|
import {
|
||||||
|
buildSkillDisplayMap,
|
||||||
collectRuntimeDisplaySources,
|
collectRuntimeDisplaySources,
|
||||||
type RuntimeDisplaySource,
|
type RuntimeDisplaySource,
|
||||||
} from "@/core/skill-display/runtime";
|
} from "@/core/skill-display/runtime";
|
||||||
@@ -299,10 +300,17 @@ export function MessageList({
|
|||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const [settings] = useLocalSettings();
|
const [settings] = useLocalSettings();
|
||||||
const ragModeEnabled =
|
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||||
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
const referenceModeEntryEnabled =
|
||||||
|
citationDisplay?.reference_mode_enabled ?? false;
|
||||||
|
const knowledgeReferencesEnabled =
|
||||||
|
threadContext?.knowledgeReferencesEnabled === true;
|
||||||
|
const ragModeEnabled =
|
||||||
|
(referenceModeEntryEnabled && settings.display.ragModeEnabled) ||
|
||||||
|
isThreadRagModeLocked(threadId) ||
|
||||||
|
knowledgeReferencesEnabled;
|
||||||
const hasSingleRoundRag = useMemo(
|
const hasSingleRoundRag = useMemo(
|
||||||
() =>
|
() =>
|
||||||
messages.some(
|
messages.some(
|
||||||
@@ -778,12 +786,13 @@ export function MessageList({
|
|||||||
)) {
|
)) {
|
||||||
if (hasReasoning(message)) {
|
if (hasReasoning(message)) {
|
||||||
results.push(
|
results.push(
|
||||||
<MessageGroup
|
<MessageGroup
|
||||||
key={"thinking-group-" + message.id}
|
key={"thinking-group-" + message.id}
|
||||||
messages={[message]}
|
messages={[message]}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
displaySources={resolveDisplaySources(message)}
|
displaySources={resolveDisplaySources(message)}
|
||||||
/>,
|
ragModeEnabled={ragModeEnabled}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
results.push(
|
results.push(
|
||||||
@@ -831,11 +840,12 @@ export function MessageList({
|
|||||||
.find((message) => message.type === "ai");
|
.find((message) => message.type === "ai");
|
||||||
return (
|
return (
|
||||||
<div key={"group-" + group.id} className="w-full">
|
<div key={"group-" + group.id} className="w-full">
|
||||||
<MessageGroup
|
<MessageGroup
|
||||||
messages={group.messages}
|
messages={group.messages}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
displaySources={resolveDisplaySources(processingAi)}
|
displaySources={resolveDisplaySources(processingAi)}
|
||||||
/>
|
ragModeEnabled={ragModeEnabled}
|
||||||
|
/>
|
||||||
<MessageTokenUsageList
|
<MessageTokenUsageList
|
||||||
enabled={tokenUsageEnabled}
|
enabled={tokenUsageEnabled}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
|||||||
const [effectiveLayoutMode, setLayoutMode] = useLayoutMode();
|
const [effectiveLayoutMode, setLayoutMode] = useLayoutMode();
|
||||||
const cleanupWords =
|
const cleanupWords =
|
||||||
citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? [];
|
citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? [];
|
||||||
|
const referenceModeEntryEnabled =
|
||||||
|
citationDisplay?.reference_mode_enabled ?? false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShortcutDraft(
|
setShortcutDraft(
|
||||||
@@ -845,23 +847,45 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
|||||||
aria-label="回答速度指示器"
|
aria-label="回答速度指示器"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-between gap-4 rounded-lg border p-4">
|
{isAdmin && (
|
||||||
<div>
|
<div className="flex items-start justify-between gap-4 rounded-lg border p-4">
|
||||||
<div className="text-sm font-medium">RAG 引用模式</div>
|
<div>
|
||||||
<p className="text-muted-foreground mt-1 max-w-md text-xs leading-relaxed">
|
<div className="text-sm font-medium">参考文献模式入口</div>
|
||||||
开启后,已配置展示适配的技能结果会在问答中渲染为编号引用或列表,并支持点击跳转。
|
<p className="text-muted-foreground mt-1 max-w-md text-xs leading-relaxed">
|
||||||
</p>
|
管理员配置。开启后,普通问答和 iframe 问答输入区会显示「参考文献」按钮;默认关闭。
|
||||||
</div>
|
</p>
|
||||||
<Switch
|
</div>
|
||||||
checked={settings.display.ragModeEnabled}
|
<Switch
|
||||||
onCheckedChange={(checked) =>
|
checked={referenceModeEntryEnabled}
|
||||||
setSettings("display", { ragModeEnabled: checked })
|
disabled={updateCitationDisplay.isPending}
|
||||||
}
|
onCheckedChange={(checked) =>
|
||||||
aria-label="RAG 引用模式"
|
updateCitationDisplay.mutate({
|
||||||
/>
|
reference_mode_enabled: checked,
|
||||||
</div>
|
})
|
||||||
{isAdmin && (
|
}
|
||||||
<div className="rounded-lg border p-4">
|
aria-label="参考文献模式入口"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{referenceModeEntryEnabled && (
|
||||||
|
<div className="flex items-start justify-between gap-4 rounded-lg border p-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">RAG 引用模式</div>
|
||||||
|
<p className="text-muted-foreground mt-1 max-w-md text-xs leading-relaxed">
|
||||||
|
开启后,已配置展示适配的技能结果会在问答中渲染为编号引用或列表,并支持点击跳转。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings.display.ragModeEnabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setSettings("display", { ragModeEnabled: checked })
|
||||||
|
}
|
||||||
|
aria-label="RAG 引用模式"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="text-sm font-medium">参考文献去除词</div>
|
<div className="text-sm font-medium">参考文献去除词</div>
|
||||||
<p className="text-muted-foreground mt-1 max-w-2xl text-xs leading-relaxed">
|
<p className="text-muted-foreground mt-1 max-w-2xl text-xs leading-relaxed">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface PromptPrefixSettings {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
switch_label: string;
|
switch_label: string;
|
||||||
prompt_prefix: string;
|
prompt_prefix: string;
|
||||||
|
ordinary_qa_markdown_format_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginDomainAgentMappingDto {
|
export interface PluginDomainAgentMappingDto {
|
||||||
@@ -37,6 +38,7 @@ export interface PromptPrefixUpdate {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
switch_label?: string;
|
switch_label?: string;
|
||||||
prompt_prefix?: string;
|
prompt_prefix?: string;
|
||||||
|
ordinary_qa_markdown_format_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromPluginManagementDto(data: {
|
function fromPluginManagementDto(data: {
|
||||||
@@ -173,10 +175,12 @@ export interface PrivateSquareUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CitationDisplaySettings {
|
export interface CitationDisplaySettings {
|
||||||
|
reference_mode_enabled: boolean;
|
||||||
cleanup_words: string[];
|
cleanup_words: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CitationDisplayUpdate {
|
export interface CitationDisplayUpdate {
|
||||||
|
reference_mode_enabled?: boolean;
|
||||||
cleanup_words?: string[];
|
cleanup_words?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,14 @@ import { rememberAskTime } from "../chat-timestamps";
|
|||||||
import { getBackendBaseURL, getLangGraphBaseURL } from "../config";
|
import { getBackendBaseURL, getLangGraphBaseURL } from "../config";
|
||||||
import { useI18n } from "../i18n/hooks";
|
import { useI18n } from "../i18n/hooks";
|
||||||
import { OptType, writeYLog } from "../log/y-log";
|
import { OptType, writeYLog } from "../log/y-log";
|
||||||
import type { FileInMessage } from "../messages/utils";
|
import type { FileInMessage } from "../messages/utils";
|
||||||
import type { LocalSettings } from "../settings";
|
import type { LocalSettings } from "../settings";
|
||||||
import { isThreadRagModeLocked, lockThreadRagMode, useLocalSettings } from "../settings";
|
import {
|
||||||
|
isThreadRagModeLocked,
|
||||||
|
lockThreadRagMode,
|
||||||
|
useLocalSettings,
|
||||||
|
} from "../settings";
|
||||||
|
import { useCitationDisplaySettings } from "../system-settings/hooks";
|
||||||
import { useUpdateSubtask } from "../tasks/context";
|
import { useUpdateSubtask } from "../tasks/context";
|
||||||
import type { UploadedFileInfo } from "../uploads";
|
import type { UploadedFileInfo } from "../uploads";
|
||||||
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
|
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
|
||||||
@@ -566,8 +571,11 @@ export function useThreadStream({
|
|||||||
onToolStart,
|
onToolStart,
|
||||||
onToolEnd,
|
onToolEnd,
|
||||||
}: ThreadStreamOptions) {
|
}: ThreadStreamOptions) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [localSettings] = useLocalSettings();
|
const [localSettings] = useLocalSettings();
|
||||||
|
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||||
|
const referenceModeEntryEnabled =
|
||||||
|
citationDisplay?.reference_mode_enabled ?? false;
|
||||||
// Track the thread ID that is currently streaming to handle thread changes during streaming
|
// Track the thread ID that is currently streaming to handle thread changes during streaming
|
||||||
const [onStreamThreadId, setOnStreamThreadId] = useState(() => threadId);
|
const [onStreamThreadId, setOnStreamThreadId] = useState(() => threadId);
|
||||||
// Ref to track current thread ID across async callbacks without causing re-renders,
|
// Ref to track current thread ID across async callbacks without causing re-renders,
|
||||||
@@ -1499,15 +1507,15 @@ export function useThreadStream({
|
|||||||
...context,
|
...context,
|
||||||
...extraContext,
|
...extraContext,
|
||||||
};
|
};
|
||||||
const selectedLlmWikiIds = Array.isArray(
|
const selectedLlmWikiIds = Array.isArray(
|
||||||
mergedContext.llmwiki_knowledge_base_ids,
|
mergedContext.llmwiki_knowledge_base_ids,
|
||||||
)
|
)
|
||||||
? mergedContext.llmwiki_knowledge_base_ids
|
? mergedContext.llmwiki_knowledge_base_ids
|
||||||
: [];
|
: [];
|
||||||
const ragModeEnabled =
|
const ragModeEnabled =
|
||||||
localSettings.display.ragModeEnabled ||
|
(referenceModeEntryEnabled && localSettings.display.ragModeEnabled) ||
|
||||||
isThreadRagModeLocked(threadId) ||
|
isThreadRagModeLocked(threadId) ||
|
||||||
selectedLlmWikiIds.length > 0;
|
selectedLlmWikiIds.length > 0;
|
||||||
if (ragModeEnabled) {
|
if (ragModeEnabled) {
|
||||||
lockThreadRagMode(threadId);
|
lockThreadRagMode(threadId);
|
||||||
}
|
}
|
||||||
@@ -1625,8 +1633,18 @@ export function useThreadStream({
|
|||||||
sendInFlightRef.current = false;
|
sendInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[thread, _handleOnStart, t.uploads.uploadingFiles, context, localSettings.display.ragModeEnabled, queryClient, runOptions, confirmOrRejoin],
|
[
|
||||||
);
|
thread,
|
||||||
|
_handleOnStart,
|
||||||
|
t.uploads.uploadingFiles,
|
||||||
|
context,
|
||||||
|
referenceModeEntryEnabled,
|
||||||
|
localSettings.display.ragModeEnabled,
|
||||||
|
queryClient,
|
||||||
|
runOptions,
|
||||||
|
confirmOrRejoin,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const handleThreadStop = useCallback(async () => {
|
const handleThreadStop = useCallback(async () => {
|
||||||
const tid = threadIdRef.current;
|
const tid = threadIdRef.current;
|
||||||
|
|||||||
@@ -777,7 +777,13 @@ export default function AgentChatPage({
|
|||||||
: "ready";
|
: "ready";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread: displayThread }}>
|
<ThreadContext.Provider
|
||||||
|
value={{
|
||||||
|
thread: displayThread,
|
||||||
|
knowledgeReferencesEnabled:
|
||||||
|
knowledgeSelectionReady && selectedKnowledgeBaseIds.length > 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Root: fixed-height horizontal split. ChatBox (chat + artifact
|
{/* Root: fixed-height horizontal split. ChatBox (chat + artifact
|
||||||
preview) lives in the left flex cell; the agent detail panel is a
|
preview) lives in the left flex cell; the agent detail panel is a
|
||||||
separate flex sibling OUTSIDE ChatBox, so the artifact preview
|
separate flex sibling OUTSIDE ChatBox, so the artifact preview
|
||||||
|
|||||||
@@ -684,7 +684,14 @@ export default function ChatPage() {
|
|||||||
!isWujieEnv();
|
!isWujieEnv();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread: displayThread, isMock }}>
|
<ThreadContext.Provider
|
||||||
|
value={{
|
||||||
|
thread: displayThread,
|
||||||
|
isMock,
|
||||||
|
knowledgeReferencesEnabled:
|
||||||
|
knowledgeSelectionReady && selectedKnowledgeBaseIds.length > 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ChatBox threadId={threadId}>
|
<ChatBox threadId={threadId}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -39,8 +39,14 @@ import {
|
|||||||
useLocalSettings,
|
useLocalSettings,
|
||||||
useThreadSettings,
|
useThreadSettings,
|
||||||
} from "@/core/settings";
|
} from "@/core/settings";
|
||||||
import { usePluginManagementSettings } from "@/core/system-settings/hooks";
|
import {
|
||||||
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
|
useCitationDisplaySettings,
|
||||||
|
usePluginManagementSettings,
|
||||||
|
} from "@/core/system-settings/hooks";
|
||||||
|
import {
|
||||||
|
stopActiveThreadBeforeClarificationReply,
|
||||||
|
useThreadStream,
|
||||||
|
} from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
|
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@@ -379,6 +385,9 @@ export default function IframeChatPage() {
|
|||||||
}, [forceDefaultAssistant, pendingIframeSubmit, selectedAgentId]);
|
}, [forceDefaultAssistant, pendingIframeSubmit, selectedAgentId]);
|
||||||
|
|
||||||
const ragModeLocked = isThreadRagModeLocked(threadId);
|
const ragModeLocked = isThreadRagModeLocked(threadId);
|
||||||
|
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||||
|
const referenceModeEntryEnabled =
|
||||||
|
citationDisplay?.reference_mode_enabled ?? false;
|
||||||
const ragModeEnabled =
|
const ragModeEnabled =
|
||||||
localSettings.display.ragModeEnabled || ragModeLocked;
|
localSettings.display.ragModeEnabled || ragModeLocked;
|
||||||
const contextCompressionEnabled =
|
const contextCompressionEnabled =
|
||||||
@@ -525,6 +534,7 @@ export default function IframeChatPage() {
|
|||||||
memoryInjectionEnabled: Boolean(
|
memoryInjectionEnabled: Boolean(
|
||||||
settings.context.memory_injection_enabled ?? true,
|
settings.context.memory_injection_enabled ?? true,
|
||||||
),
|
),
|
||||||
|
referenceModeEntryEnabled,
|
||||||
ragModeEnabled,
|
ragModeEnabled,
|
||||||
ragModeLocked,
|
ragModeLocked,
|
||||||
contextCompressionEnabled,
|
contextCompressionEnabled,
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export default function PromptPrefixSettingsPage({
|
|||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const [switchLabel, setSwitchLabel] = useState("");
|
const [switchLabel, setSwitchLabel] = useState("");
|
||||||
const [promptPrefix, setPromptPrefix] = useState("");
|
const [promptPrefix, setPromptPrefix] = useState("");
|
||||||
|
const [ordinaryQaMarkdownFormatEnabled, setOrdinaryQaMarkdownFormatEnabled] =
|
||||||
|
useState(false);
|
||||||
// Only seed the draft from server data once. Re-syncing every time react-query
|
// Only seed the draft from server data once. Re-syncing every time react-query
|
||||||
// refetches (window focus, route remount, etc.) would silently overwrite the
|
// refetches (window focus, route remount, etc.) would silently overwrite the
|
||||||
// user's in-progress edits — and made the toggle look unresponsive: a click
|
// user's in-progress edits — and made the toggle look unresponsive: a click
|
||||||
@@ -57,6 +59,9 @@ export default function PromptPrefixSettingsPage({
|
|||||||
setEnabled(data.enabled);
|
setEnabled(data.enabled);
|
||||||
setSwitchLabel(data.switch_label);
|
setSwitchLabel(data.switch_label);
|
||||||
setPromptPrefix(data.prompt_prefix);
|
setPromptPrefix(data.prompt_prefix);
|
||||||
|
setOrdinaryQaMarkdownFormatEnabled(
|
||||||
|
data.ordinary_qa_markdown_format_enabled === true,
|
||||||
|
);
|
||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@@ -79,6 +84,8 @@ export default function PromptPrefixSettingsPage({
|
|||||||
enabled,
|
enabled,
|
||||||
switch_label: switchLabel,
|
switch_label: switchLabel,
|
||||||
prompt_prefix: promptPrefix,
|
prompt_prefix: promptPrefix,
|
||||||
|
ordinary_qa_markdown_format_enabled:
|
||||||
|
ordinaryQaMarkdownFormatEnabled,
|
||||||
});
|
});
|
||||||
toast.success("已保存",darkToastOpts.success);
|
toast.success("已保存",darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -95,10 +102,9 @@ export default function PromptPrefixSettingsPage({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 className={embedded ? "text-base font-semibold" : "text-xl font-semibold"}>问答前缀配置</h1>
|
<h1 className={embedded ? "text-base font-semibold" : "text-xl font-semibold"}>问答提示词配置</h1>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
管理员可在此配置问答页输入框中的「前缀开关」。开启后,用户在新会话中第一次提问时,
|
管理员可在此配置问答前缀和普通问答的固定 Markdown 格式提示词。
|
||||||
会将下方配置的前缀文本和原问题拼接后再发送给模型。
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,6 +153,21 @@ export default function PromptPrefixSettingsPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between gap-4 rounded-lg border p-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">固定格式提示词</div>
|
||||||
|
<p className="text-muted-foreground mt-1 max-w-md text-xs leading-relaxed">
|
||||||
|
开启后,普通问答的长篇内容会按“# 主标题、## 一、标题、###
|
||||||
|
(一) 标题、#### 1. 标题”的固定 Markdown 格式生成。默认关闭。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={ordinaryQaMarkdownFormatEnabled}
|
||||||
|
onCheckedChange={setOrdinaryQaMarkdownFormatEnabled}
|
||||||
|
aria-label="固定格式提示词"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button onClick={handleSave} disabled={update.isPending}>
|
<Button onClick={handleSave} disabled={update.isPending}>
|
||||||
{update.isPending && (
|
{update.isPending && (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ test("includes the context compression toggle in the iframe chat toolbar", () =>
|
|||||||
agentSelected: false,
|
agentSelected: false,
|
||||||
writingModeEnabled: false,
|
writingModeEnabled: false,
|
||||||
memoryInjectionEnabled: true,
|
memoryInjectionEnabled: true,
|
||||||
|
referenceModeEntryEnabled: true,
|
||||||
ragModeEnabled: false,
|
ragModeEnabled: false,
|
||||||
ragModeLocked: false,
|
ragModeLocked: false,
|
||||||
contextCompressionEnabled: false,
|
contextCompressionEnabled: false,
|
||||||
@@ -28,11 +29,27 @@ test("includes the context compression toggle in the iframe chat toolbar", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("hides the references toggle when reference mode is disabled globally", () => {
|
||||||
|
const buttons = buildIframeModeButtons({
|
||||||
|
agentSelected: false,
|
||||||
|
writingModeEnabled: false,
|
||||||
|
memoryInjectionEnabled: true,
|
||||||
|
referenceModeEntryEnabled: false,
|
||||||
|
ragModeEnabled: true,
|
||||||
|
ragModeLocked: false,
|
||||||
|
contextCompressionEnabled: false,
|
||||||
|
proModeEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(buttons.some((button) => button.key === "references"), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("returns only concrete toolbar button configs", () => {
|
test("returns only concrete toolbar button configs", () => {
|
||||||
const buttons = buildIframeModeButtons({
|
const buttons = buildIframeModeButtons({
|
||||||
agentSelected: true,
|
agentSelected: true,
|
||||||
writingModeEnabled: false,
|
writingModeEnabled: false,
|
||||||
memoryInjectionEnabled: true,
|
memoryInjectionEnabled: true,
|
||||||
|
referenceModeEntryEnabled: true,
|
||||||
ragModeEnabled: false,
|
ragModeEnabled: false,
|
||||||
ragModeLocked: false,
|
ragModeLocked: false,
|
||||||
contextCompressionEnabled: false,
|
contextCompressionEnabled: false,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type BuildIframeModeButtonsInput = {
|
|||||||
agentSelected: boolean;
|
agentSelected: boolean;
|
||||||
writingModeEnabled: boolean;
|
writingModeEnabled: boolean;
|
||||||
memoryInjectionEnabled: boolean;
|
memoryInjectionEnabled: boolean;
|
||||||
|
referenceModeEntryEnabled: boolean;
|
||||||
ragModeEnabled: boolean;
|
ragModeEnabled: boolean;
|
||||||
ragModeLocked: boolean;
|
ragModeLocked: boolean;
|
||||||
contextCompressionEnabled: boolean;
|
contextCompressionEnabled: boolean;
|
||||||
@@ -25,12 +26,13 @@ export function buildIframeModeButtons({
|
|||||||
agentSelected,
|
agentSelected,
|
||||||
writingModeEnabled,
|
writingModeEnabled,
|
||||||
memoryInjectionEnabled,
|
memoryInjectionEnabled,
|
||||||
|
referenceModeEntryEnabled,
|
||||||
ragModeEnabled,
|
ragModeEnabled,
|
||||||
ragModeLocked,
|
ragModeLocked,
|
||||||
contextCompressionEnabled,
|
contextCompressionEnabled,
|
||||||
proModeEnabled,
|
proModeEnabled,
|
||||||
}: BuildIframeModeButtonsInput): IframeModeButtonState[] {
|
}: BuildIframeModeButtonsInput): IframeModeButtonState[] {
|
||||||
return [
|
const buttons: IframeModeButtonState[] = [
|
||||||
{
|
{
|
||||||
key: "writing",
|
key: "writing",
|
||||||
label: "写作",
|
label: "写作",
|
||||||
@@ -63,4 +65,7 @@ export function buildIframeModeButtons({
|
|||||||
active: agentSelected,
|
active: agentSelected,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
return referenceModeEntryEnabled
|
||||||
|
? buttons
|
||||||
|
: buttons.filter((button) => button.key !== "references");
|
||||||
}
|
}
|
||||||
@@ -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** (`/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` |
|
| **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 |
|
| **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`. |
|
| **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. |
|
| **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 |
|
| **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.
|
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 按类型聚合)
|
### 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).
|
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
|
it as an encoded path segment (including inside a `#/…` hash route); password
|
||||||
remains an ordinary login query parameter with the key `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
|
## Technology Stack
|
||||||
|
|||||||
@@ -42,12 +42,17 @@ class PromptPrefixResponse(BaseModel):
|
|||||||
enabled: bool = Field(..., description="Whether the toggle is exposed to users")
|
enabled: bool = Field(..., description="Whether the toggle is exposed to users")
|
||||||
switch_label: str = Field(..., description="Toggle label shown in the chat input")
|
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")
|
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):
|
class PromptPrefixUpdateRequest(BaseModel):
|
||||||
enabled: bool | None = None
|
enabled: bool | None = None
|
||||||
switch_label: str | None = None
|
switch_label: str | None = None
|
||||||
prompt_prefix: str | None = None
|
prompt_prefix: str | None = None
|
||||||
|
ordinary_qa_markdown_format_enabled: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class PluginDomainAgentMappingItem(BaseModel):
|
class PluginDomainAgentMappingItem(BaseModel):
|
||||||
@@ -82,10 +87,12 @@ class PrivateSquareUpdateRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CitationDisplayResponse(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")
|
cleanup_words: list[str] = Field(default_factory=list, description="Global citation cleanup words")
|
||||||
|
|
||||||
|
|
||||||
class CitationDisplayUpdateRequest(BaseModel):
|
class CitationDisplayUpdateRequest(BaseModel):
|
||||||
|
reference_mode_enabled: bool | None = None
|
||||||
cleanup_words: list[str] | None = None
|
cleanup_words: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +117,7 @@ def _to_response(settings: PromptPrefixSettings) -> PromptPrefixResponse:
|
|||||||
enabled=settings.enabled,
|
enabled=settings.enabled,
|
||||||
switch_label=settings.switch_label,
|
switch_label=settings.switch_label,
|
||||||
prompt_prefix=settings.prompt_prefix,
|
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)
|
@router.get("/citation-display", response_model=CitationDisplayResponse)
|
||||||
async def get_citation_display() -> CitationDisplayResponse:
|
async def get_citation_display() -> CitationDisplayResponse:
|
||||||
"""Return global citation display cleanup settings."""
|
"""Return global citation display settings."""
|
||||||
settings = load_system_settings().citation_display
|
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)
|
@router.put("/citation-display", response_model=CitationDisplayResponse)
|
||||||
@@ -420,14 +431,22 @@ async def update_citation_display(
|
|||||||
body: CitationDisplayUpdateRequest,
|
body: CitationDisplayUpdateRequest,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> CitationDisplayResponse:
|
) -> CitationDisplayResponse:
|
||||||
"""Update global citation display cleanup settings. Admin only."""
|
"""Update global citation display settings. Admin only."""
|
||||||
await _require_admin(request)
|
await _require_admin(request)
|
||||||
|
|
||||||
current = load_system_settings()
|
current = load_system_settings()
|
||||||
words = [str(item).strip() for item in (body.cleanup_words or []) if str(item).strip()]
|
fields = body.model_dump(exclude_unset=True)
|
||||||
updated = current.citation_display.model_copy(update={"cleanup_words": words})
|
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}))
|
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)
|
@router.get("/private-square", response_model=PrivateSquareStatusResponse)
|
||||||
|
|||||||
+23
-9
@@ -9,7 +9,8 @@ from functools import lru_cache
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from zoneinfo import ZoneInfo
|
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.storage import get_or_new_skill_storage
|
||||||
from deerflow.skills.types import Skill, SkillCategory
|
from deerflow.skills.types import Skill, SkillCategory
|
||||||
from deerflow.subagents import get_available_subagent_names
|
from deerflow.subagents import get_available_subagent_names
|
||||||
@@ -486,7 +487,7 @@ task(description="Oracle Cloud analysis", prompt="...", subagent_type="general-p
|
|||||||
</subagent_system>"""
|
</subagent_system>"""
|
||||||
|
|
||||||
|
|
||||||
_ORDINARY_QA_MARKDOWN_FORMAT_SECTION = """
|
_ORDINARY_QA_MARKDOWN_FORMAT_SECTION = """
|
||||||
<ordinary_qa_markdown_format>
|
<ordinary_qa_markdown_format>
|
||||||
当普通问答需要输出带章节结构的较长内容时,必须采用以下 Markdown 规范,以便下载为 Word 后得到正确的自动编号和公文标题格式:
|
当普通问答需要输出带章节结构的较长内容时,必须采用以下 Markdown 规范,以便下载为 Word 后得到正确的自动编号和公文标题格式:
|
||||||
- 文档主标题使用 `# 主标题`;主标题只写标题文字,不添加章节编号。
|
- 文档主标题使用 `# 主标题`;主标题只写标题文字,不添加章节编号。
|
||||||
@@ -495,10 +496,19 @@ _ORDINARY_QA_MARKDOWN_FORMAT_SECTION = """
|
|||||||
- 默认禁止使用以 `-`、`*`、`+` 或 `•` 开头的无序圆点列表;并列内容优先改写为完整自然段,确需逐项表达时使用有序列表。只有用户明确要求无序列表时才使用。
|
- 默认禁止使用以 `-`、`*`、`+` 或 `•` 开头的无序圆点列表;并列内容优先改写为完整自然段,确需逐项表达时使用有序列表。只有用户明确要求无序列表时才使用。
|
||||||
- 简短问答不必为了套用格式而强行添加标题;本规范只约束实际使用的标题和列表。
|
- 简短问答不必为了套用格式而强行添加标题;本规范只约束实际使用的标题和列表。
|
||||||
</ordinary_qa_markdown_format>
|
</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>
|
<role>
|
||||||
You are {agent_name}, an internal super agent.
|
You are {agent_name}, an internal super agent.
|
||||||
</role>
|
</role>
|
||||||
@@ -1537,9 +1547,13 @@ When a search, knowledge, skill, or MCP tool returns a result list:
|
|||||||
if rag_mode_enabled
|
if rag_mode_enabled
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
ordinary_qa_format_section = (
|
ordinary_qa_format_section = (
|
||||||
_ORDINARY_QA_MARKDOWN_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
|
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 ""
|
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")
|
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")
|
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")
|
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")
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
@@ -255,8 +259,12 @@ class AppearanceSettings(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CitationDisplaySettings(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(
|
cleanup_words: list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Words or phrases used to remove noisy advertisement/disclaimer lines from citation display.",
|
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:
|
def test_formal_markdown_rules_are_scoped_to_ordinary_qa(monkeypatch) -> None:
|
||||||
config = _app_config(enabled=False)
|
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_memory_context", lambda *args, **kwargs: "")
|
||||||
monkeypatch.setattr(prompt_module, "get_agent_soul", 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_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 writing_prompt
|
||||||
assert '<ordinary_qa_markdown_format>' not in notebook_prompt
|
assert '<ordinary_qa_markdown_format>' not in notebook_prompt
|
||||||
assert '<ordinary_qa_markdown_format>' not in scheduled_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
|
import pytest
|
||||||
from langchain_core.messages import AIMessage, HumanMessage
|
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 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
|
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()
|
settings = ss.load_system_settings()
|
||||||
assert settings.prompt_prefix.enabled is False
|
assert settings.prompt_prefix.enabled is False
|
||||||
assert settings.prompt_prefix.prompt_prefix == ""
|
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:
|
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,
|
enabled=True,
|
||||||
switch_label="加入立场前缀",
|
switch_label="加入立场前缀",
|
||||||
prompt_prefix="你需要站在中方角度",
|
prompt_prefix="你需要站在中方角度",
|
||||||
|
ordinary_qa_markdown_format_enabled=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ss.save_system_settings(settings)
|
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.enabled is True
|
||||||
assert reloaded.prompt_prefix.switch_label == "加入立场前缀"
|
assert reloaded.prompt_prefix.switch_label == "加入立场前缀"
|
||||||
assert reloaded.prompt_prefix.prompt_prefix == "你需要站在中方角度"
|
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:
|
def test_prepend_prefix_tags_first_human_without_touching_content() -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user