feat(turbo): add direct LLM chat mode with runtime toggle
Add 极速 mode that streams via /api/direct-chat without the lead agent stack, plus VITE_TURBO_MODE_VISIBLE in runtime-config.js (default off). Also disable global memory, tighten Qwen/vLLM thinking control for flash mode, and force thinking off on the direct-chat endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4f7f07504a
commit
d765afe2ac
23 files changed
+958
-57
No files matched your search
@@ -59,6 +59,9 @@
|
|||||||
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
|
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
|
||||||
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
|
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
|
||||||
# VITE_COMPACT_MENU_TYPE=A
|
# VITE_COMPACT_MENU_TYPE=A
|
||||||
|
# 问答页「极速」模式:true=在模式菜单显示并直连大模型;false 或未设=隐藏。
|
||||||
|
# 部署后优先改 dist/runtime-config.js 的 VITE_TURBO_MODE_VISIBLE,刷新即可。
|
||||||
|
# VITE_TURBO_MODE_VISIBLE=false
|
||||||
# VITE_GITHUB_OAUTH_TOKEN=
|
# VITE_GITHUB_OAUTH_TOKEN=
|
||||||
|
|
||||||
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
|
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ window.__CM_RUNTIME_CONFIG__ = {
|
|||||||
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。
|
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。
|
||||||
VITE_GITHUB_OAUTH_TOKEN: "",
|
VITE_GITHUB_OAUTH_TOKEN: "",
|
||||||
|
|
||||||
|
// 是否在问答输入框「模式」菜单显示「极速」:"true" 显示,"false" 隐藏。
|
||||||
|
// 默认关闭。开启后可直连大模型流式问答,不走智能体工具与技能。
|
||||||
|
// 部署后改 dist/runtime-config.js 再刷新页面即可,无需重新打包。
|
||||||
|
VITE_TURBO_MODE_VISIBLE: "true",
|
||||||
|
|
||||||
// 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。
|
// 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。
|
||||||
// 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。
|
// 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。
|
||||||
VITE_SENTIMENT_AGENT_VISIBLE: "true",
|
VITE_SENTIMENT_AGENT_VISIBLE: "true",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CheckIcon,
|
CheckIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
FolderTreeIcon,
|
FolderTreeIcon,
|
||||||
|
GaugeIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
LightbulbIcon,
|
LightbulbIcon,
|
||||||
PaperclipIcon,
|
PaperclipIcon,
|
||||||
@@ -68,6 +69,7 @@ import { isHiddenFromUIMessage } from "@/core/messages/utils";
|
|||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import type { Model } from "@/core/models/types";
|
import type { Model } from "@/core/models/types";
|
||||||
import type { AgentThreadContext } from "@/core/threads";
|
import type { AgentThreadContext } from "@/core/threads";
|
||||||
|
import { isTurboModeVisible } from "@/core/threads/use-direct-chat";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -223,7 +225,11 @@ export function InputBox({
|
|||||||
context.execution_profile === "single_round_rag";
|
context.execution_profile === "single_round_rag";
|
||||||
const multiRoundRagActive =
|
const multiRoundRagActive =
|
||||||
context.execution_profile === "multi_round_rag";
|
context.execution_profile === "multi_round_rag";
|
||||||
|
const turboModeVisible = isTurboModeVisible();
|
||||||
|
const directChatActive =
|
||||||
|
turboModeVisible && context.execution_profile === "direct";
|
||||||
const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
|
const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
|
||||||
|
const bypassAgentActive = frontendRagActive || directChatActive;
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
return singleRoundRagActive ? extraModels : models;
|
return singleRoundRagActive ? extraModels : models;
|
||||||
}, [extraModels, models, singleRoundRagActive]);
|
}, [extraModels, models, singleRoundRagActive]);
|
||||||
@@ -253,11 +259,37 @@ export function InputBox({
|
|||||||
recommendedQuestions.length > 0;
|
recommendedQuestions.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Multi-round RAG is a frontend execution profile, not a model option.
|
// Frontend-only execution profiles are not model options.
|
||||||
// Do not let the normal model fallback clear it on every render.
|
// Do not let the normal model fallback clear them on every render.
|
||||||
|
if (!turboModeVisible && context.execution_profile === "direct") {
|
||||||
|
onContextChange?.({
|
||||||
|
...context,
|
||||||
|
execution_profile: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (multiRoundRagActive) {
|
if (multiRoundRagActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (directChatActive) {
|
||||||
|
if (models.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentModel = models.find((m) => m.name === context.model_name);
|
||||||
|
const fallbackModel = currentModel ?? models[0]!;
|
||||||
|
if (
|
||||||
|
context.model_name === fallbackModel.name &&
|
||||||
|
context.execution_profile === "direct"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onContextChange?.({
|
||||||
|
...context,
|
||||||
|
model_name: fallbackModel.name,
|
||||||
|
execution_profile: "direct",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (modelOptions.length === 0) {
|
if (modelOptions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -281,7 +313,7 @@ export function InputBox({
|
|||||||
mode: nextMode,
|
mode: nextMode,
|
||||||
execution_profile: fallbackModel.execution_profile,
|
execution_profile: fallbackModel.execution_profile,
|
||||||
});
|
});
|
||||||
}, [context, modelOptions, multiRoundRagActive, onContextChange]);
|
}, [context, directChatActive, modelOptions, models, multiRoundRagActive, onContextChange, turboModeVisible]);
|
||||||
|
|
||||||
const selectedModel = useMemo(() => {
|
const selectedModel = useMemo(() => {
|
||||||
if (modelOptions.length === 0) {
|
if (modelOptions.length === 0) {
|
||||||
@@ -314,7 +346,10 @@ export function InputBox({
|
|||||||
model_name,
|
model_name,
|
||||||
mode: getResolvedMode(context.mode, model.supports_thinking ?? false),
|
mode: getResolvedMode(context.mode, model.supports_thinking ?? false),
|
||||||
reasoning_effort: context.reasoning_effort,
|
reasoning_effort: context.reasoning_effort,
|
||||||
execution_profile: model.execution_profile,
|
execution_profile:
|
||||||
|
context.execution_profile === "direct"
|
||||||
|
? "direct"
|
||||||
|
: model.execution_profile,
|
||||||
});
|
});
|
||||||
setModelDialogOpen(false);
|
setModelDialogOpen(false);
|
||||||
},
|
},
|
||||||
@@ -346,6 +381,16 @@ export function InputBox({
|
|||||||
[onContextChange, context, models, supportThinking],
|
[onContextChange, context, models, supportThinking],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDirectChatSelect = useCallback(() => {
|
||||||
|
const ordinaryModel =
|
||||||
|
models.find((model) => model.name === context.model_name) ?? models[0];
|
||||||
|
onContextChange?.({
|
||||||
|
...context,
|
||||||
|
model_name: ordinaryModel?.name ?? context.model_name,
|
||||||
|
execution_profile: "direct",
|
||||||
|
});
|
||||||
|
}, [context, models, onContextChange]);
|
||||||
|
|
||||||
const handleSingleRoundRagSelect = useCallback(() => {
|
const handleSingleRoundRagSelect = useCallback(() => {
|
||||||
const ragModel =
|
const ragModel =
|
||||||
extraModels.find((model) => model.name === context.model_name) ??
|
extraModels.find((model) => model.name === context.model_name) ??
|
||||||
@@ -749,7 +794,7 @@ export function InputBox({
|
|||||||
/>
|
/>
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu> */}
|
</PromptInputActionMenu> */}
|
||||||
{!frontendRagActive && (
|
{!bypassAgentActive && (
|
||||||
<AddAttachmentsButton
|
<AddAttachmentsButton
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||||
@@ -758,7 +803,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!frontendRagActive && !hideContextTools && <Tooltip
|
{!bypassAgentActive && !hideContextTools && <Tooltip
|
||||||
content={
|
content={
|
||||||
context.writing_mode
|
context.writing_mode
|
||||||
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
||||||
@@ -784,7 +829,7 @@ export function InputBox({
|
|||||||
<span className="text-xs font-normal">写作</span>
|
<span className="text-xs font-normal">写作</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>}
|
</Tooltip>}
|
||||||
{!frontendRagActive && <Tooltip
|
{!bypassAgentActive && <Tooltip
|
||||||
content={
|
content={
|
||||||
artifactDirectoryModeEnabled
|
artifactDirectoryModeEnabled
|
||||||
? "文件模式已开启:右侧会显示当前对话生成的全部文件目录"
|
? "文件模式已开启:右侧会显示当前对话生成的全部文件目录"
|
||||||
@@ -813,7 +858,7 @@ export function InputBox({
|
|||||||
<span className="text-xs font-normal">文件</span>
|
<span className="text-xs font-normal">文件</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>}
|
</Tooltip>}
|
||||||
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
{!bypassAgentActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
||||||
content={
|
content={
|
||||||
memoryInjectionEnabled
|
memoryInjectionEnabled
|
||||||
? "记忆注入已开启:对话前会注入历史记忆"
|
? "记忆注入已开启:对话前会注入历史记忆"
|
||||||
@@ -839,7 +884,7 @@ export function InputBox({
|
|||||||
<span className="text-xs font-normal">记忆</span>
|
<span className="text-xs font-normal">记忆</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>}
|
</Tooltip>}
|
||||||
{referenceModeEntryEnabled && !frontendRagActive && !hideContextTools && <Tooltip
|
{referenceModeEntryEnabled && !bypassAgentActive && !hideContextTools && <Tooltip
|
||||||
content={
|
content={
|
||||||
ragModeEnabled
|
ragModeEnabled
|
||||||
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
||||||
@@ -869,7 +914,7 @@ export function InputBox({
|
|||||||
<span className="text-xs font-normal">参考文献</span>
|
<span className="text-xs font-normal">参考文献</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>}
|
</Tooltip>}
|
||||||
{!frontendRagActive && <Tooltip
|
{!bypassAgentActive && <Tooltip
|
||||||
content={
|
content={
|
||||||
contextCompressionEnabled
|
contextCompressionEnabled
|
||||||
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
||||||
@@ -909,7 +954,9 @@ export function InputBox({
|
|||||||
: "flash"
|
: "flash"
|
||||||
}
|
}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
singleRoundRagActive
|
directChatActive
|
||||||
|
? `${t.inputBox.turboMode}:${t.inputBox.turboModeDescription}`
|
||||||
|
: singleRoundRagActive
|
||||||
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
|
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
|
||||||
: multiRoundRagActive
|
: multiRoundRagActive
|
||||||
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
|
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
|
||||||
@@ -921,7 +968,9 @@ export function InputBox({
|
|||||||
"gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
"gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||||
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
|
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
|
||||||
compact &&
|
compact &&
|
||||||
(frontendRagActive
|
(directChatActive
|
||||||
|
? "border-sky-200 bg-sky-50/60 text-sky-700 hover:bg-sky-100 dark:border-sky-700 dark:bg-sky-950/40 dark:text-sky-200"
|
||||||
|
: frontendRagActive
|
||||||
? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||||
: context.mode === "thinking"
|
: context.mode === "thinking"
|
||||||
? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200"
|
? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200"
|
||||||
@@ -933,7 +982,9 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{frontendRagActive ? (
|
{directChatActive ? (
|
||||||
|
<GaugeIcon className="size-3" />
|
||||||
|
) : frontendRagActive ? (
|
||||||
<BookMarkedIcon className="size-3" />
|
<BookMarkedIcon className="size-3" />
|
||||||
) : context.mode === "flash" ? (
|
) : context.mode === "flash" ? (
|
||||||
<ZapIcon className="size-3" />
|
<ZapIcon className="size-3" />
|
||||||
@@ -948,10 +999,14 @@ export function InputBox({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs font-normal",
|
"text-xs font-normal",
|
||||||
context.mode === "ultra" ? "golden-text" : "",
|
context.mode === "ultra" && !directChatActive && !frontendRagActive
|
||||||
|
? "golden-text"
|
||||||
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{frontendRagActive
|
{directChatActive
|
||||||
|
? t.inputBox.turboMode
|
||||||
|
: frontendRagActive
|
||||||
? multiRoundRagActive
|
? multiRoundRagActive
|
||||||
? "多轮 RAG"
|
? "多轮 RAG"
|
||||||
: "单轮 RAG"
|
: "单轮 RAG"
|
||||||
@@ -969,9 +1024,39 @@ export function InputBox({
|
|||||||
{t.inputBox.mode}
|
{t.inputBox.mode}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
|
{turboModeVisible && (
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!frontendRagActive && context.mode === "flash"
|
directChatActive
|
||||||
|
? "bg-sky-50 text-sky-800 hover:bg-sky-100 focus:bg-sky-100 dark:bg-sky-950/40 dark:text-sky-200 dark:hover:bg-sky-950/60 dark:focus:bg-sky-950/60"
|
||||||
|
: "text-muted-foreground/65",
|
||||||
|
)}
|
||||||
|
onSelect={handleDirectChatSelect}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-1 font-bold">
|
||||||
|
<GaugeIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 size-4",
|
||||||
|
directChatActive && "text-sky-700 dark:text-sky-200",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{t.inputBox.turboMode}
|
||||||
|
</div>
|
||||||
|
<div className="pl-7 text-xs">
|
||||||
|
{t.inputBox.turboModeDescription}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{directChatActive ? (
|
||||||
|
<CheckIcon className="ml-auto size-4" />
|
||||||
|
) : (
|
||||||
|
<div className="ml-auto size-4" />
|
||||||
|
)}
|
||||||
|
</PromptInputActionMenuItem>
|
||||||
|
)}
|
||||||
|
<PromptInputActionMenuItem
|
||||||
|
className={cn(
|
||||||
|
!bypassAgentActive && context.mode === "flash"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -982,7 +1067,7 @@ export function InputBox({
|
|||||||
<ZapIcon
|
<ZapIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!frontendRagActive && context.mode === "flash" &&
|
!bypassAgentActive && context.mode === "flash" &&
|
||||||
"text-accent-foreground",
|
"text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -992,7 +1077,7 @@ export function InputBox({
|
|||||||
{t.inputBox.flashModeDescription}
|
{t.inputBox.flashModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!frontendRagActive && context.mode === "flash" ? (
|
{!bypassAgentActive && context.mode === "flash" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -1001,7 +1086,7 @@ export function InputBox({
|
|||||||
{supportThinking && (
|
{supportThinking && (
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!frontendRagActive && context.mode === "thinking"
|
!bypassAgentActive && context.mode === "thinking"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -1012,7 +1097,7 @@ export function InputBox({
|
|||||||
<LightbulbIcon
|
<LightbulbIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!frontendRagActive && context.mode === "thinking" &&
|
!bypassAgentActive && context.mode === "thinking" &&
|
||||||
"text-accent-foreground",
|
"text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -1022,7 +1107,7 @@ export function InputBox({
|
|||||||
{t.inputBox.reasoningModeDescription}
|
{t.inputBox.reasoningModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!frontendRagActive && context.mode === "thinking" ? (
|
{!bypassAgentActive && context.mode === "thinking" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -1031,7 +1116,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!frontendRagActive && context.mode === "pro"
|
!bypassAgentActive && context.mode === "pro"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -1042,7 +1127,7 @@ export function InputBox({
|
|||||||
<GraduationCapIcon
|
<GraduationCapIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!frontendRagActive && context.mode === "pro" &&
|
!bypassAgentActive && context.mode === "pro" &&
|
||||||
"text-accent-foreground",
|
"text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -1052,7 +1137,7 @@ export function InputBox({
|
|||||||
{t.inputBox.proModeDescription}
|
{t.inputBox.proModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!frontendRagActive && context.mode === "pro" ? (
|
{!bypassAgentActive && context.mode === "pro" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -1060,7 +1145,7 @@ export function InputBox({
|
|||||||
</PromptInputActionMenuItem>
|
</PromptInputActionMenuItem>
|
||||||
<PromptInputActionMenuItem
|
<PromptInputActionMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
!frontendRagActive && context.mode === "ultra"
|
!bypassAgentActive && context.mode === "ultra"
|
||||||
? "text-accent-foreground"
|
? "text-accent-foreground"
|
||||||
: "text-muted-foreground/65",
|
: "text-muted-foreground/65",
|
||||||
)}
|
)}
|
||||||
@@ -1071,14 +1156,14 @@ export function InputBox({
|
|||||||
<RocketIcon
|
<RocketIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4",
|
"mr-2 size-4",
|
||||||
!frontendRagActive &&
|
!bypassAgentActive &&
|
||||||
context.mode === "ultra" &&
|
context.mode === "ultra" &&
|
||||||
"text-[#dabb5e]",
|
"text-[#dabb5e]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
!frontendRagActive &&
|
!bypassAgentActive &&
|
||||||
context.mode === "ultra" &&
|
context.mode === "ultra" &&
|
||||||
"golden-text",
|
"golden-text",
|
||||||
)}
|
)}
|
||||||
@@ -1090,7 +1175,7 @@ export function InputBox({
|
|||||||
{t.inputBox.ultraModeDescription}
|
{t.inputBox.ultraModeDescription}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!frontendRagActive && context.mode === "ultra" ? (
|
{!bypassAgentActive && context.mode === "ultra" ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<CheckIcon className="ml-auto size-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="ml-auto size-4" />
|
<div className="ml-auto size-4" />
|
||||||
@@ -1161,7 +1246,7 @@ export function InputBox({
|
|||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu>
|
</PromptInputActionMenu>
|
||||||
{!frontendRagActive && supportReasoningEffort && context.mode !== "flash" && (
|
{!bypassAgentActive && supportReasoningEffort && context.mode !== "flash" && (
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}>
|
<PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}>
|
||||||
<div className="text-xs font-normal">
|
<div className="text-xs font-normal">
|
||||||
@@ -1279,7 +1364,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
||||||
{!frontendRagActive && agentSelector && (
|
{!bypassAgentActive && agentSelector && (
|
||||||
<AgentSelector
|
<AgentSelector
|
||||||
selectedAgentId={agentSelector.selectedAgentId}
|
selectedAgentId={agentSelector.selectedAgentId}
|
||||||
onAgentChange={agentSelector.onAgentChange}
|
onAgentChange={agentSelector.onAgentChange}
|
||||||
@@ -1287,8 +1372,8 @@ export function InputBox({
|
|||||||
compact={compact}
|
compact={compact}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!frontendRagActive && knowledgeSelector}
|
{!bypassAgentActive && knowledgeSelector}
|
||||||
{!frontendRagActive && promptPrefix && (
|
{!bypassAgentActive && promptPrefix && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
promptPrefix.on
|
promptPrefix.on
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export const enUS: Translations = {
|
|||||||
mode: "Mode",
|
mode: "Mode",
|
||||||
flashMode: "Flash",
|
flashMode: "Flash",
|
||||||
flashModeDescription: "Fast and efficient, but may not be accurate",
|
flashModeDescription: "Fast and efficient, but may not be accurate",
|
||||||
|
turboMode: "Turbo",
|
||||||
|
turboModeDescription:
|
||||||
|
"Talk to the model directly with no agent tools or skills, for lower first-token latency",
|
||||||
reasoningMode: "Reasoning",
|
reasoningMode: "Reasoning",
|
||||||
reasoningModeDescription:
|
reasoningModeDescription:
|
||||||
"Reasoning before action, balance between time and accuracy",
|
"Reasoning before action, balance between time and accuracy",
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ export interface Translations {
|
|||||||
mode: string;
|
mode: string;
|
||||||
flashMode: string;
|
flashMode: string;
|
||||||
flashModeDescription: string;
|
flashModeDescription: string;
|
||||||
|
turboMode: string;
|
||||||
|
turboModeDescription: string;
|
||||||
reasoningMode: string;
|
reasoningMode: string;
|
||||||
reasoningModeDescription: string;
|
reasoningModeDescription: string;
|
||||||
proMode: string;
|
proMode: string;
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export const zhCN: Translations = {
|
|||||||
mode: "模式",
|
mode: "模式",
|
||||||
flashMode: "快速",
|
flashMode: "快速",
|
||||||
flashModeDescription: "快速且高效的完成任务,但可能不够精准",
|
flashModeDescription: "快速且高效的完成任务,但可能不够精准",
|
||||||
|
turboMode: "极速",
|
||||||
|
turboModeDescription: "直连大模型流式问答,不走智能体工具与技能,首字更快",
|
||||||
reasoningMode: "思考",
|
reasoningMode: "思考",
|
||||||
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
|
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
|
||||||
proMode: "专业问答",
|
proMode: "专业问答",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface Model {
|
|||||||
supports_thinking?: boolean;
|
supports_thinking?: boolean;
|
||||||
supports_reasoning_effort?: boolean;
|
supports_reasoning_effort?: boolean;
|
||||||
/** Execution profiles handled by the frontend instead of DeerFlow. */
|
/** Execution profiles handled by the frontend instead of DeerFlow. */
|
||||||
execution_profile?: "single_round_rag" | "multi_round_rag";
|
execution_profile?: "single_round_rag" | "multi_round_rag" | "direct";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenUsageSettings {
|
export interface TokenUsageSettings {
|
||||||
|
|||||||
@@ -1579,6 +1579,7 @@ export function useThreadStream({
|
|||||||
context: {
|
context: {
|
||||||
...mergedContext,
|
...mergedContext,
|
||||||
thinking_enabled: mergedContext.mode !== "flash",
|
thinking_enabled: mergedContext.mode !== "flash",
|
||||||
|
thinking_force_disabled: mergedContext.mode === "flash",
|
||||||
is_plan_mode:
|
is_plan_mode:
|
||||||
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
|
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
|
||||||
subagent_enabled: mergedContext.mode === "ultra",
|
subagent_enabled: mergedContext.mode === "ultra",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
|||||||
thread_id: string;
|
thread_id: string;
|
||||||
model_name: string | undefined;
|
model_name: string | undefined;
|
||||||
thinking_enabled: boolean;
|
thinking_enabled: boolean;
|
||||||
|
/** Flash mode: force vLLM/Qwen ``enable_thinking=false`` even without a per-model disable shape. */
|
||||||
|
thinking_force_disabled?: boolean;
|
||||||
is_plan_mode: boolean;
|
is_plan_mode: boolean;
|
||||||
subagent_enabled: boolean;
|
subagent_enabled: boolean;
|
||||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||||
@@ -29,8 +31,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
|||||||
llmwiki_knowledge_base_ids?: string[];
|
llmwiki_knowledge_base_ids?: string[];
|
||||||
/** Assistant knowledge-base ids selected for this conversation. */
|
/** Assistant knowledge-base ids selected for this conversation. */
|
||||||
assistant_knowledge_base_ids?: string[];
|
assistant_knowledge_base_ids?: string[];
|
||||||
/** Direct RAG modes handled in the frontend; these requests never enter Lead Agent. */
|
/** Frontend-only execution paths that never enter Lead Agent (RAG / 极速直连). */
|
||||||
execution_profile?: "single_round_rag" | "multi_round_rag";
|
execution_profile?: "single_round_rag" | "multi_round_rag" | "direct";
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */
|
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */
|
||||||
agent_name?: string;
|
agent_name?: string;
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { apiFetch } from "@/core/api/fetch-client";
|
||||||
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
import { readRuntimeFlag } from "@/env";
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
|
|
||||||
|
import { textOfMessage } from "./utils";
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = "workspace.direct-chat.";
|
||||||
|
const MAX_HISTORY_MESSAGES = 20;
|
||||||
|
|
||||||
|
export function isDirectChatProfile(context: unknown): boolean {
|
||||||
|
return (
|
||||||
|
typeof context === "object" &&
|
||||||
|
context !== null &&
|
||||||
|
(context as { execution_profile?: unknown }).execution_profile === "direct"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否在模式菜单展示「极速」。仅 runtime-config.js 显式为 "true" 时开启,默认关闭。 */
|
||||||
|
export function isTurboModeVisible(): boolean {
|
||||||
|
return readRuntimeFlag("VITE_TURBO_MODE_VISIBLE");
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageKey(threadId: string) {
|
||||||
|
return `${STORAGE_PREFIX}${threadId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredMessages(threadId: string): Message[] {
|
||||||
|
if (typeof window === "undefined" || !threadId) return [];
|
||||||
|
try {
|
||||||
|
const raw = window.sessionStorage.getItem(storageKey(threadId));
|
||||||
|
const parsed: unknown = raw ? JSON.parse(raw) : [];
|
||||||
|
return Array.isArray(parsed) ? (parsed as Message[]) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStoredMessages(threadId: string, messages: Message[]) {
|
||||||
|
if (typeof window === "undefined" || !threadId) return;
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(storageKey(threadId), JSON.stringify(messages));
|
||||||
|
} catch {
|
||||||
|
// Quota errors must never interrupt an already-running answer.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbortError(error: unknown): boolean {
|
||||||
|
return error instanceof DOMException
|
||||||
|
? error.name === "AbortError"
|
||||||
|
: error instanceof Error && error.name === "AbortError";
|
||||||
|
}
|
||||||
|
|
||||||
|
function historyFromMessages(messages: Message[]): Array<{
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
}> {
|
||||||
|
const history: Array<{ role: "user" | "assistant"; content: string }> = [];
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.type !== "human" && message.type !== "ai") continue;
|
||||||
|
const content = textOfMessage(message)?.trim();
|
||||||
|
if (!content) continue;
|
||||||
|
history.push({
|
||||||
|
role: message.type === "human" ? "user" : "assistant",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return history.slice(-MAX_HISTORY_MESSAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSseBlocks(buffer: string): {
|
||||||
|
events: Array<{ event?: string; data: string }>;
|
||||||
|
rest: string;
|
||||||
|
} {
|
||||||
|
const parts = buffer.split("\n\n");
|
||||||
|
const rest = parts.pop() ?? "";
|
||||||
|
const events: Array<{ event?: string; data: string }> = [];
|
||||||
|
for (const part of parts) {
|
||||||
|
let event: string | undefined;
|
||||||
|
const dataLines: string[] = [];
|
||||||
|
for (const line of part.split("\n")) {
|
||||||
|
if (line.startsWith(":")) continue;
|
||||||
|
if (line.startsWith("event:")) event = line.slice(6).trim();
|
||||||
|
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
||||||
|
}
|
||||||
|
if (dataLines.length > 0) {
|
||||||
|
events.push({ event, data: dataLines.join("\n") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { events, rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readErrorDetail(response: Response): Promise<string> {
|
||||||
|
try {
|
||||||
|
const payload: unknown = await response.json();
|
||||||
|
if (
|
||||||
|
payload &&
|
||||||
|
typeof payload === "object" &&
|
||||||
|
"detail" in payload &&
|
||||||
|
typeof (payload as { detail: unknown }).detail === "string"
|
||||||
|
) {
|
||||||
|
return (payload as { detail: string }).detail;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to status text.
|
||||||
|
}
|
||||||
|
return `极速问答失败(${response.status})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend-only adapter that talks to Gateway ``POST /api/direct-chat/stream``.
|
||||||
|
* It never submits a LangGraph run or touches the lead-agent stack.
|
||||||
|
*/
|
||||||
|
export function useDirectChatMessages(threadId: string | undefined) {
|
||||||
|
const [messages, setMessages] = useState<Message[]>(() =>
|
||||||
|
readStoredMessages(threadId ?? ""),
|
||||||
|
);
|
||||||
|
const messagesRef = useRef(messages);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const controllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = readStoredMessages(threadId ?? "");
|
||||||
|
messagesRef.current = stored;
|
||||||
|
setMessages(stored);
|
||||||
|
setError(null);
|
||||||
|
}, [threadId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesRef.current = messages;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => () => controllerRef.current?.abort(), []);
|
||||||
|
|
||||||
|
const updateMessages = useCallback(
|
||||||
|
(targetThreadId: string, updater: (current: Message[]) => Message[]) => {
|
||||||
|
setMessages((current) => {
|
||||||
|
const next = updater(current);
|
||||||
|
messagesRef.current = next;
|
||||||
|
writeStoredMessages(targetThreadId, next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stop = useCallback(() => controllerRef.current?.abort(), []);
|
||||||
|
|
||||||
|
const submit = useCallback(
|
||||||
|
async ({
|
||||||
|
targetThreadId,
|
||||||
|
query,
|
||||||
|
modelName,
|
||||||
|
}: {
|
||||||
|
targetThreadId: string;
|
||||||
|
query: string;
|
||||||
|
modelName?: string;
|
||||||
|
}) => {
|
||||||
|
if (isLoading) return;
|
||||||
|
const question = query.trim();
|
||||||
|
if (!question) return;
|
||||||
|
|
||||||
|
const humanId = createClientId("direct-human");
|
||||||
|
const assistantId = createClientId("direct-ai");
|
||||||
|
const clientTs = new Date().toISOString();
|
||||||
|
const history = historyFromMessages(messagesRef.current);
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: humanId,
|
||||||
|
type: "human",
|
||||||
|
content: [{ type: "text", text: question }],
|
||||||
|
additional_kwargs: { client_ts: clientTs, direct_chat: true },
|
||||||
|
};
|
||||||
|
const initialAssistant: Message = {
|
||||||
|
id: assistantId,
|
||||||
|
type: "ai",
|
||||||
|
content: "",
|
||||||
|
additional_kwargs: { direct_chat: true },
|
||||||
|
};
|
||||||
|
updateMessages(targetThreadId, (current) => [
|
||||||
|
...current,
|
||||||
|
userMessage,
|
||||||
|
initialAssistant,
|
||||||
|
]);
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
controllerRef.current = controller;
|
||||||
|
let answer = "";
|
||||||
|
|
||||||
|
const refreshAssistant = (content: string) => {
|
||||||
|
updateMessages(targetThreadId, (current) =>
|
||||||
|
current.map((item) =>
|
||||||
|
item.id === assistantId
|
||||||
|
? ({
|
||||||
|
...item,
|
||||||
|
content,
|
||||||
|
additional_kwargs: {
|
||||||
|
...item.additional_kwargs,
|
||||||
|
direct_chat: true,
|
||||||
|
},
|
||||||
|
} as Message)
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${getBackendBaseURL()}/api/direct-chat/stream`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
signal: controller.signal,
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_name: modelName,
|
||||||
|
messages: [...history, { role: "user", content: question }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await readErrorDetail(response));
|
||||||
|
}
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error("极速问答未返回数据流。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const parsed = parseSseBlocks(buffer);
|
||||||
|
buffer = parsed.rest;
|
||||||
|
for (const block of parsed.events) {
|
||||||
|
let payload: unknown = null;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(block.data);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.event === "error") {
|
||||||
|
const detail =
|
||||||
|
payload &&
|
||||||
|
typeof payload === "object" &&
|
||||||
|
"detail" in payload &&
|
||||||
|
typeof (payload as { detail: unknown }).detail === "string"
|
||||||
|
? (payload as { detail: string }).detail
|
||||||
|
: "极速问答失败。";
|
||||||
|
throw new Error(detail);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
block.event === "delta" &&
|
||||||
|
payload &&
|
||||||
|
typeof payload === "object" &&
|
||||||
|
"text" in payload &&
|
||||||
|
typeof (payload as { text: unknown }).text === "string"
|
||||||
|
) {
|
||||||
|
answer += (payload as { text: string }).text;
|
||||||
|
refreshAssistant(answer);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
block.event === "result" &&
|
||||||
|
payload &&
|
||||||
|
typeof payload === "object" &&
|
||||||
|
"answer" in payload &&
|
||||||
|
typeof (payload as { answer: unknown }).answer === "string"
|
||||||
|
) {
|
||||||
|
answer = (payload as { answer: string }).answer;
|
||||||
|
refreshAssistant(answer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!answer) {
|
||||||
|
throw new Error("模型未返回任何文本。");
|
||||||
|
}
|
||||||
|
} catch (caught) {
|
||||||
|
if (!isAbortError(caught)) {
|
||||||
|
const message =
|
||||||
|
caught instanceof Error ? caught.message : "极速问答失败。";
|
||||||
|
setError(message);
|
||||||
|
updateMessages(targetThreadId, (current) =>
|
||||||
|
current.map((item) =>
|
||||||
|
item.id === assistantId
|
||||||
|
? ({
|
||||||
|
...item,
|
||||||
|
content: answer || message,
|
||||||
|
additional_kwargs: {
|
||||||
|
...item.additional_kwargs,
|
||||||
|
direct_chat: true,
|
||||||
|
},
|
||||||
|
} as Message)
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (controllerRef.current === controller) controllerRef.current = null;
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isLoading, updateMessages],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { messages, isLoading, error, submit, stop };
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { useLocation } from "react-router-dom";
|
|||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -48,6 +49,7 @@ import { useNotification } from "@/core/notification/hooks";
|
|||||||
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
|
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
|
||||||
import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks";
|
import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks";
|
||||||
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
|
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
|
||||||
|
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
|
||||||
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
|
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
|
||||||
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";
|
||||||
@@ -560,9 +562,26 @@ export default function AgentChatPage({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const directChat = useDirectChatMessages(threadId);
|
||||||
|
const directChatSelected =
|
||||||
|
isDirectChatProfile(settings.context) && isTurboModeVisible();
|
||||||
|
|
||||||
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
|
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
|
||||||
// 流式 stub),成功且真实产物落地后自动撤出。
|
// 流式 stub),成功且真实产物落地后自动撤出。
|
||||||
const { displayThread } = useResearchWritingFeed(threadId, thread);
|
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
|
||||||
|
const displayThread = useMemo(() => {
|
||||||
|
const base = researchDisplayThread;
|
||||||
|
if (directChat.messages.length === 0 && !directChat.isLoading) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
const messages = [...base.messages, ...directChat.messages];
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
values: { ...base.values, messages },
|
||||||
|
messages,
|
||||||
|
isLoading: base.isLoading || directChat.isLoading,
|
||||||
|
} as typeof base;
|
||||||
|
}, [directChat.isLoading, directChat.messages, researchDisplayThread]);
|
||||||
|
|
||||||
// ── agentfx「入库」协助卡 ──────────────────────────────────────────────────
|
// ── agentfx「入库」协助卡 ──────────────────────────────────────────────────
|
||||||
// 任务工作区(taskBind)里,内置研判智能体写出的 task-reports.json 产物落地且
|
// 任务工作区(taskBind)里,内置研判智能体写出的 task-reports.json 产物落地且
|
||||||
@@ -743,6 +762,62 @@ export default function AgentChatPage({
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage, extraContext?: Record<string, unknown>) => {
|
(message: PromptInputMessage, extraContext?: Record<string, unknown>) => {
|
||||||
|
if (directChatSelected) {
|
||||||
|
if (message.files && message.files.length > 0) {
|
||||||
|
toast.error("极速问答暂不支持附件,请移除附件后重试。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasStartedConversation(true);
|
||||||
|
saveLastAgentId(agent_id);
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
||||||
|
);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
let targetThreadId = threadId;
|
||||||
|
if (isNewThread) {
|
||||||
|
const created = await getAPIClient().threads.create({
|
||||||
|
metadata: {
|
||||||
|
agent_id,
|
||||||
|
...(Object.keys(conversationMetadata).length > 0
|
||||||
|
? conversationMetadata
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
targetThreadId = created.thread_id;
|
||||||
|
knowledgeSelectionThread.current = targetThreadId;
|
||||||
|
activeThreadIdRef.current = targetThreadId;
|
||||||
|
setThreadId(targetThreadId);
|
||||||
|
setIsNewThread(false);
|
||||||
|
history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
`${window.location.pathname}${window.location.search}#${agentChatsBase}/${targetThreadId}`,
|
||||||
|
);
|
||||||
|
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
|
||||||
|
if (title) {
|
||||||
|
void getAPIClient().threads
|
||||||
|
.updateState(targetThreadId, { values: { title } })
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetThreadId) {
|
||||||
|
toast.error("无法创建会话。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await directChat.submit({
|
||||||
|
targetThreadId,
|
||||||
|
query: message.text,
|
||||||
|
modelName: settings.context.model_name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "极速问答失败。",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setHasStartedConversation(true);
|
setHasStartedConversation(true);
|
||||||
// Remember this agent so the new-chat landing screen pre-selects it next
|
// Remember this agent so the new-chat landing screen pre-selects it next
|
||||||
// time (covers both direct agent chats and the ChatPage handoff replay).
|
// time (covers both direct agent chats and the ChatPage handoff replay).
|
||||||
@@ -767,7 +842,7 @@ export default function AgentChatPage({
|
|||||||
...pageReportThemeContext,
|
...pageReportThemeContext,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[sendMessage, threadId, agent_id, settings.context.writing_mode, writingArtifactPath, baseMode],
|
[sendMessage, threadId, agent_id, settings.context.writing_mode, settings.context.model_name, writingArtifactPath, baseMode, directChat, directChatSelected, isNewThread, conversationMetadata, agentChatsBase, setThreadId, setIsNewThread],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。
|
// 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。
|
||||||
@@ -852,8 +927,12 @@ export default function AgentChatPage({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
|
if (directChat.isLoading) {
|
||||||
|
directChat.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [directChat, thread]);
|
||||||
|
|
||||||
const isLandingState = isNewThread && !hasStartedConversation;
|
const isLandingState = isNewThread && !hasStartedConversation;
|
||||||
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
|
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
|
||||||
@@ -867,7 +946,7 @@ export default function AgentChatPage({
|
|||||||
|
|
||||||
const status = thread.error
|
const status = thread.error
|
||||||
? "error"
|
? "error"
|
||||||
: thread.isLoading
|
: thread.isLoading || directChat.isLoading
|
||||||
? "streaming"
|
? "streaming"
|
||||||
: "ready";
|
: "ready";
|
||||||
|
|
||||||
@@ -1084,7 +1163,7 @@ export default function AgentChatPage({
|
|||||||
autoFocus
|
autoFocus
|
||||||
status={status}
|
status={status}
|
||||||
context={settings.context}
|
context={settings.context}
|
||||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||||
onContextChange={(context) =>
|
onContextChange={(context) =>
|
||||||
setSettings("context", context)
|
setSettings("context", context)
|
||||||
}
|
}
|
||||||
@@ -1104,7 +1183,7 @@ export default function AgentChatPage({
|
|||||||
pendingKnowledgeSelection.current === null &&
|
pendingKnowledgeSelection.current === null &&
|
||||||
pendingAssistantKnowledgeSelection.current === null
|
pendingAssistantKnowledgeSelection.current === null
|
||||||
}
|
}
|
||||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||||
label="知识空间"
|
label="知识空间"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -1175,7 +1254,7 @@ export default function AgentChatPage({
|
|||||||
assistantId={agent_id}
|
assistantId={agent_id}
|
||||||
status={status}
|
status={status}
|
||||||
context={settings.context}
|
context={settings.context}
|
||||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||||
onContextChange={(context) =>
|
onContextChange={(context) =>
|
||||||
setSettings("context", context)
|
setSettings("context", context)
|
||||||
}
|
}
|
||||||
@@ -1190,7 +1269,7 @@ export default function AgentChatPage({
|
|||||||
assistantValue={selectedAssistantKnowledgeBaseIds}
|
assistantValue={selectedAssistantKnowledgeBaseIds}
|
||||||
onChange={changeKnowledgeSelection}
|
onChange={changeKnowledgeSelection}
|
||||||
onAssistantChange={changeAssistantKnowledgeSelection}
|
onAssistantChange={changeAssistantKnowledgeSelection}
|
||||||
disabled={isDemoMode || knowledgeSelectionLoading}
|
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
|
||||||
readOnly
|
readOnly
|
||||||
label="知识空间"
|
label="知识空间"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import { clearResearchReportStream } from "@/core/threads/use-research-report-st
|
|||||||
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
|
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
|
||||||
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
|
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
|
||||||
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
|
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
|
||||||
|
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
|
||||||
import { useMultiRoundRagMessages, mergeMessagesByTime, toLangGraphMessages } from "@/core/threads/use-multi-round-rag";
|
import { useMultiRoundRagMessages, mergeMessagesByTime, toLangGraphMessages } from "@/core/threads/use-multi-round-rag";
|
||||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
@@ -348,9 +349,12 @@ export default function ChatPage() {
|
|||||||
});
|
});
|
||||||
const singleRoundRag = useSingleRoundRagMessages(threadId);
|
const singleRoundRag = useSingleRoundRagMessages(threadId);
|
||||||
const multiRoundRag = useMultiRoundRagMessages(threadId);
|
const multiRoundRag = useMultiRoundRagMessages(threadId);
|
||||||
|
const directChat = useDirectChatMessages(threadId);
|
||||||
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
|
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
|
||||||
const multiRoundRagSelected = isMultiRoundRagProfile(settings.context);
|
const multiRoundRagSelected = isMultiRoundRagProfile(settings.context);
|
||||||
const frontendRagSelected = isFrontendRagProfile(settings.context);
|
const frontendRagSelected = isFrontendRagProfile(settings.context);
|
||||||
|
const directChatSelected =
|
||||||
|
isDirectChatProfile(settings.context) && isTurboModeVisible();
|
||||||
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
|
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
|
||||||
// 流式 stub),成功且真实产物落地后自动撤出。
|
// 流式 stub),成功且真实产物落地后自动撤出。
|
||||||
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
|
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
|
||||||
@@ -360,13 +364,15 @@ export default function ChatPage() {
|
|||||||
if (
|
if (
|
||||||
singleRoundRag.messages.length === 0 &&
|
singleRoundRag.messages.length === 0 &&
|
||||||
ragMessages.length === 0 &&
|
ragMessages.length === 0 &&
|
||||||
|
directChat.messages.length === 0 &&
|
||||||
!singleRoundRag.isLoading &&
|
!singleRoundRag.isLoading &&
|
||||||
!multiRoundRag.isLoading
|
!multiRoundRag.isLoading &&
|
||||||
|
!directChat.isLoading
|
||||||
) {
|
) {
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
const messages = mergeMessagesByTime(
|
const messages = mergeMessagesByTime(
|
||||||
[...base.messages, ...singleRoundRag.messages],
|
[...base.messages, ...singleRoundRag.messages, ...directChat.messages],
|
||||||
ragMessages,
|
ragMessages,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -374,9 +380,14 @@ export default function ChatPage() {
|
|||||||
values: { ...base.values, messages },
|
values: { ...base.values, messages },
|
||||||
messages,
|
messages,
|
||||||
isLoading:
|
isLoading:
|
||||||
base.isLoading || singleRoundRag.isLoading || multiRoundRag.isLoading,
|
base.isLoading ||
|
||||||
|
singleRoundRag.isLoading ||
|
||||||
|
multiRoundRag.isLoading ||
|
||||||
|
directChat.isLoading,
|
||||||
} as typeof base;
|
} as typeof base;
|
||||||
}, [
|
}, [
|
||||||
|
directChat.isLoading,
|
||||||
|
directChat.messages,
|
||||||
multiRoundRag.isLoading,
|
multiRoundRag.isLoading,
|
||||||
multiRoundRag.messages,
|
multiRoundRag.messages,
|
||||||
researchDisplayThread,
|
researchDisplayThread,
|
||||||
@@ -392,6 +403,52 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage) => {
|
(message: PromptInputMessage) => {
|
||||||
|
if (directChatSelected) {
|
||||||
|
if (message.files && message.files.length > 0) {
|
||||||
|
toast.error("极速问答暂不支持附件,请移除附件后重试。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasStartedConversation(true);
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
||||||
|
);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
let targetThreadId = threadId;
|
||||||
|
if (isNewThread) {
|
||||||
|
const created = await getAPIClient(isMock).threads.create({});
|
||||||
|
targetThreadId = created.thread_id;
|
||||||
|
setThreadId(targetThreadId);
|
||||||
|
setIsNewThread(false);
|
||||||
|
history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
`${window.location.pathname}${window.location.search}#${routeBase}/chats/${targetThreadId}`,
|
||||||
|
);
|
||||||
|
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
|
||||||
|
if (title) {
|
||||||
|
void getAPIClient(isMock).threads
|
||||||
|
.updateState(targetThreadId, { values: { title } })
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetThreadId) {
|
||||||
|
toast.error("无法创建会话。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await directChat.submit({
|
||||||
|
targetThreadId,
|
||||||
|
query: message.text,
|
||||||
|
modelName: settings.context.model_name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "极速问答失败。",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (frontendRagSelected) {
|
if (frontendRagSelected) {
|
||||||
if (message.files && message.files.length > 0) {
|
if (message.files && message.files.length > 0) {
|
||||||
toast.error("RAG 问答暂不支持附件,请移除附件后重试。");
|
toast.error("RAG 问答暂不支持附件,请移除附件后重试。");
|
||||||
@@ -560,6 +617,8 @@ export default function ChatPage() {
|
|||||||
multiRoundRag,
|
multiRoundRag,
|
||||||
multiRoundRagSelected,
|
multiRoundRagSelected,
|
||||||
frontendRagSelected,
|
frontendRagSelected,
|
||||||
|
directChat,
|
||||||
|
directChatSelected,
|
||||||
routeBase,
|
routeBase,
|
||||||
setIsNewThread,
|
setIsNewThread,
|
||||||
setThreadId,
|
setThreadId,
|
||||||
@@ -655,6 +714,10 @@ export default function ChatPage() {
|
|||||||
saveLastAgentId(agentId);
|
saveLastAgentId(agentId);
|
||||||
}, []);
|
}, []);
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
|
if (directChat.isLoading) {
|
||||||
|
directChat.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (singleRoundRag.isLoading) {
|
if (singleRoundRag.isLoading) {
|
||||||
singleRoundRag.stop();
|
singleRoundRag.stop();
|
||||||
return;
|
return;
|
||||||
@@ -664,7 +727,7 @@ export default function ChatPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [multiRoundRag, singleRoundRag, thread]);
|
}, [directChat, multiRoundRag, singleRoundRag, thread]);
|
||||||
|
|
||||||
const promptPrefixProp = useMemo(() => {
|
const promptPrefixProp = useMemo(() => {
|
||||||
if (!promptPrefixSettings?.enabled) return undefined;
|
if (!promptPrefixSettings?.enabled) return undefined;
|
||||||
@@ -914,7 +977,8 @@ export default function ChatPage() {
|
|||||||
agentHandoffPending ||
|
agentHandoffPending ||
|
||||||
knowledgeSelectionLoading ||
|
knowledgeSelectionLoading ||
|
||||||
singleRoundRag.isLoading ||
|
singleRoundRag.isLoading ||
|
||||||
multiRoundRag.isLoading
|
multiRoundRag.isLoading ||
|
||||||
|
directChat.isLoading
|
||||||
}
|
}
|
||||||
onContextChange={(context) =>
|
onContextChange={(context) =>
|
||||||
setSettings("context", context)
|
setSettings("context", context)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from app.gateway.routers import (
|
|||||||
compact_menu_overrides,
|
compact_menu_overrides,
|
||||||
dashboard_sessions,
|
dashboard_sessions,
|
||||||
deep_research,
|
deep_research,
|
||||||
|
direct_chat,
|
||||||
external_llmwiki,
|
external_llmwiki,
|
||||||
feedback,
|
feedback,
|
||||||
fixed_questions,
|
fixed_questions,
|
||||||
@@ -1803,6 +1804,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
|||||||
# 开放问答接口 /api/open/chat —— 无需登录,指定模型 + 智能体做问答,可关闭思考模式
|
# 开放问答接口 /api/open/chat —— 无需登录,指定模型 + 智能体做问答,可关闭思考模式
|
||||||
app.include_router(open_chat.router)
|
app.include_router(open_chat.router)
|
||||||
|
|
||||||
|
# 极速问答 /api/direct-chat/stream —— 直连大模型流式输出,不走 lead agent / 工具 / 技能
|
||||||
|
app.include_router(direct_chat.router)
|
||||||
|
|
||||||
# 开放接口 /api/open/3qfx/ask —— 无需登录,按 taskId 直接发起 3qfx 第一次问答(后台运行,落成真实会话)
|
# 开放接口 /api/open/3qfx/ask —— 无需登录,按 taskId 直接发起 3qfx 第一次问答(后台运行,落成真实会话)
|
||||||
app.include_router(open_3qfx.router)
|
app.include_router(open_3qfx.router)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Direct LLM chat — no lead agent, tools, skills, or memory.
|
||||||
|
|
||||||
|
Used by the frontend 「极速」mode: stream tokens from ``create_chat_model``
|
||||||
|
with thinking forced off so first-token latency matches a bare chat client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from deerflow.config import get_app_config
|
||||||
|
from deerflow.models import create_chat_model
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/direct-chat", tags=["direct-chat"])
|
||||||
|
|
||||||
|
_HEARTBEAT_SECONDS = 15.0
|
||||||
|
_MAX_MESSAGES = 40
|
||||||
|
|
||||||
|
|
||||||
|
class DirectChatMessage(BaseModel):
|
||||||
|
role: Literal["user", "assistant", "system"]
|
||||||
|
content: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class DirectChatRequest(BaseModel):
|
||||||
|
messages: list[DirectChatMessage] = Field(..., min_length=1)
|
||||||
|
model_name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk_text(chunk: Any) -> str:
|
||||||
|
content = getattr(chunk, "content", None)
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts: list[str] = []
|
||||||
|
for block in content:
|
||||||
|
if isinstance(block, str):
|
||||||
|
parts.append(block)
|
||||||
|
elif isinstance(block, dict) and isinstance(block.get("text"), str):
|
||||||
|
parts.append(block["text"])
|
||||||
|
return "".join(parts)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _build_lc_messages(body: DirectChatRequest) -> list[BaseMessage]:
|
||||||
|
built: list[BaseMessage] = []
|
||||||
|
for item in body.messages[-_MAX_MESSAGES:]:
|
||||||
|
text = (item.content or "").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
if item.role == "user":
|
||||||
|
built.append(HumanMessage(content=text))
|
||||||
|
elif item.role == "assistant":
|
||||||
|
built.append(AIMessage(content=text))
|
||||||
|
else:
|
||||||
|
built.append(SystemMessage(content=text))
|
||||||
|
if not any(isinstance(msg, HumanMessage) for msg in built):
|
||||||
|
raise HTTPException(status_code=400, detail="至少需要一条 user 消息")
|
||||||
|
return built
|
||||||
|
|
||||||
|
|
||||||
|
def _sse(event: str, data: Any) -> str:
|
||||||
|
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_model_name(name: str | None) -> str:
|
||||||
|
config = get_app_config()
|
||||||
|
if not name:
|
||||||
|
if not config.models:
|
||||||
|
raise HTTPException(status_code=400, detail="未配置可用模型")
|
||||||
|
return config.models[0].name
|
||||||
|
if config.get_model_config(name) is None:
|
||||||
|
raise HTTPException(status_code=400, detail=f"模型不存在: {name}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
async def _direct_chat_event_stream(body: DirectChatRequest, model_name: str) -> AsyncIterator[str]:
|
||||||
|
lc_messages = _build_lc_messages(body)
|
||||||
|
queue: asyncio.Queue[tuple[str, Any]] = asyncio.Queue()
|
||||||
|
pieces: list[str] = []
|
||||||
|
|
||||||
|
async def produce() -> None:
|
||||||
|
try:
|
||||||
|
model = create_chat_model(
|
||||||
|
name=model_name,
|
||||||
|
thinking_enabled=False,
|
||||||
|
force_disable_thinking=True,
|
||||||
|
)
|
||||||
|
async for chunk in model.astream(lc_messages):
|
||||||
|
text = _chunk_text(chunk)
|
||||||
|
if text:
|
||||||
|
pieces.append(text)
|
||||||
|
await queue.put(("delta", {"text": text}))
|
||||||
|
except Exception as exc: # noqa: BLE001 — stream errors become SSE error events
|
||||||
|
logger.exception("direct_chat 调用模型失败 (model=%s)", model_name)
|
||||||
|
await queue.put(("error", {"detail": f"模型调用失败:{exc}"}))
|
||||||
|
finally:
|
||||||
|
await queue.put(("__end__", None))
|
||||||
|
|
||||||
|
task = asyncio.create_task(produce())
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
event, data = await asyncio.wait_for(queue.get(), timeout=_HEARTBEAT_SECONDS)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
yield ": keepalive\n\n"
|
||||||
|
continue
|
||||||
|
if event == "__end__":
|
||||||
|
break
|
||||||
|
if event == "error":
|
||||||
|
yield _sse("error", data)
|
||||||
|
return
|
||||||
|
yield _sse(event, data)
|
||||||
|
|
||||||
|
answer = "".join(pieces)
|
||||||
|
if not answer:
|
||||||
|
yield _sse("error", {"detail": "模型未返回任何文本。"})
|
||||||
|
return
|
||||||
|
yield _sse("result", {"answer": answer, "model_name": model_name})
|
||||||
|
finally:
|
||||||
|
if not task.done():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stream")
|
||||||
|
async def direct_chat_stream(body: DirectChatRequest) -> StreamingResponse:
|
||||||
|
"""Stream a plain chat completion. No agent graph, tools, or skills."""
|
||||||
|
model_name = _resolve_model_name(body.model_name)
|
||||||
|
_build_lc_messages(body)
|
||||||
|
return StreamingResponse(
|
||||||
|
_direct_chat_event_stream(body, model_name),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -33,6 +33,16 @@ concurrency:
|
|||||||
fail_open: true
|
fail_open: true
|
||||||
|
|
||||||
models:
|
models:
|
||||||
|
- name: MiniMax-M2.7
|
||||||
|
display_name: MiniMax-M2.7
|
||||||
|
use: langchain_openai:ChatOpenAI
|
||||||
|
model: MiniMax-M2.7
|
||||||
|
api_key: sk-cp-mrsAGtzDpUl0TpOBPVoiRG_pWF1kDDN9_mDAnONBs6HIPcNUexSKtSbx2QOxfKBvJT6Xw6hDTV6hdkJYFWfNA8UtHQ08TiGJiosdRoW6q1sGb6Cb7D85NHk
|
||||||
|
base_url: https://api.minimaxi.com/v1
|
||||||
|
max_tokens: 4096
|
||||||
|
temperature: 1.0
|
||||||
|
supports_vision: true
|
||||||
|
supports_thinking: true
|
||||||
- name: MiniMax-M2.7-fash
|
- name: MiniMax-M2.7-fash
|
||||||
display_name: MiniMax-M2.7-fash
|
display_name: MiniMax-M2.7-fash
|
||||||
use: langchain_openai:ChatOpenAI
|
use: langchain_openai:ChatOpenAI
|
||||||
@@ -466,7 +476,7 @@ checkpointer:
|
|||||||
# 三层覆盖优先级(高->低):
|
# 三层覆盖优先级(高->低):
|
||||||
# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处
|
# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处
|
||||||
memory:
|
memory:
|
||||||
enabled: true
|
enabled: false # 临时关闭跨会话记忆(提取/注入/Hindsight 召回与 retain、memory 工具)
|
||||||
version:
|
version:
|
||||||
v1 # 记忆版本: 'v1'(原版 DeerFlow, memory.json + LLM 自动提取)
|
v1 # 记忆版本: 'v1'(原版 DeerFlow, memory.json + LLM 自动提取)
|
||||||
# 'v2'(USER.md/MEMORY.md + 可选 Hindsight)
|
# 'v2'(USER.md/MEMORY.md + 可选 Hindsight)
|
||||||
|
|||||||
+3
-1
@@ -433,7 +433,9 @@ def _build_middlewares(
|
|||||||
|
|
||||||
# Add MemoryMiddleware (after TitleMiddleware) — skip in bootstrap mode (agent creation flow) and ephemeral runs
|
# Add MemoryMiddleware (after TitleMiddleware) — skip in bootstrap mode (agent creation flow) and ephemeral runs
|
||||||
memory_injection_enabled = cfg.get("memory_injection_enabled", True)
|
memory_injection_enabled = cfg.get("memory_injection_enabled", True)
|
||||||
if not is_bootstrap and not is_writing_setup and not is_report_structure_setup and not is_ephemeral:
|
memory_cfg = getattr(resolved_app_config, "memory", None)
|
||||||
|
memory_globally_enabled = memory_cfg is not None and getattr(memory_cfg, "enabled", False)
|
||||||
|
if memory_globally_enabled and not is_bootstrap and not is_writing_setup and not is_report_structure_setup and not is_ephemeral:
|
||||||
middlewares.append(MemoryMiddleware(agent_name=agent_name, memory_config=resolved_app_config.memory, injection_enabled=memory_injection_enabled))
|
middlewares.append(MemoryMiddleware(agent_name=agent_name, memory_config=resolved_app_config.memory, injection_enabled=memory_injection_enabled))
|
||||||
|
|
||||||
# KnowledgeRagMiddleware — inject knowledge-base recall (phase 2). Gated
|
# KnowledgeRagMiddleware — inject knowledge-base recall (phase 2). Gated
|
||||||
|
|||||||
+6
-1
@@ -883,11 +883,16 @@ def _get_memory_context(agent_name: str | None = None, *, app_config: AppConfig
|
|||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
from deerflow.agents.memory.manager import build_memory_manager
|
from deerflow.agents.memory.manager import build_memory_manager
|
||||||
from deerflow.config.memory_config import get_effective_memory_config
|
from deerflow.config.memory_config import get_effective_memory_config, get_memory_config
|
||||||
from deerflow.runtime.user_context import get_effective_user_id
|
from deerflow.runtime.user_context import get_effective_user_id
|
||||||
|
|
||||||
|
if not get_memory_config().enabled:
|
||||||
|
return ""
|
||||||
|
|
||||||
user_id = get_effective_user_id()
|
user_id = get_effective_user_id()
|
||||||
config = get_effective_memory_config(user_id)
|
config = get_effective_memory_config(user_id)
|
||||||
|
if not config.enabled or not config.injection.enabled:
|
||||||
|
return ""
|
||||||
|
|
||||||
manager = build_memory_manager(
|
manager = build_memory_manager(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
============
|
============
|
||||||
``get_effective_memory_config(user_id)`` 按优先级合并 (高 -> 低):
|
``get_effective_memory_config(user_id)`` 按优先级合并 (高 -> 低):
|
||||||
1. per-user 覆盖 数据库 ``user_memory_configs`` 表 (经进程内 cache 读取)
|
1. per-user 覆盖 数据库 ``user_memory_configs`` 表 (经进程内 cache 读取)
|
||||||
(有 per-user 记录时自动强制 enabled=True)
|
(仅白名单字段,不能覆盖 ``enabled``)
|
||||||
2. config.yaml ``memory:`` 段 (即 ``get_memory_config()``)
|
2. config.yaml ``memory:`` 段 (即 ``get_memory_config()``)
|
||||||
3. Pydantic 默认值
|
3. Pydantic 默认值
|
||||||
|
|
||||||
@@ -254,13 +254,12 @@ def get_effective_memory_config(user_id: str) -> MemoryConfig:
|
|||||||
"""返回某用户的生效记忆配置 (同步,从进程内 cache 读取 per-user 覆盖)。
|
"""返回某用户的生效记忆配置 (同步,从进程内 cache 读取 per-user 覆盖)。
|
||||||
|
|
||||||
优先级 (高 -> 低):per-user DB 覆盖 (经 cache) > config.yaml > 默认值。
|
优先级 (高 -> 低):per-user DB 覆盖 (经 cache) > config.yaml > 默认值。
|
||||||
有 per-user 记录时自动强制 enabled=True。
|
``enabled`` 始终以 config.yaml 为准,用户覆盖不能把它重新打开。
|
||||||
"""
|
"""
|
||||||
base = _memory_config.model_dump()
|
base = _memory_config.model_dump()
|
||||||
recall_budget = _recall_budget_cache.get(user_id)
|
recall_budget = _recall_budget_cache.get(user_id)
|
||||||
if recall_budget is not None:
|
if recall_budget is not None:
|
||||||
base.setdefault("hindsight", {})["recall_budget"] = recall_budget
|
base.setdefault("hindsight", {})["recall_budget"] = recall_budget
|
||||||
base["enabled"] = True
|
|
||||||
try:
|
try:
|
||||||
return MemoryConfig(**base)
|
return MemoryConfig(**base)
|
||||||
except Exception as exc: # noqa: BLE001 - 覆盖产生非法配置时降级
|
except Exception as exc: # noqa: BLE001 - 覆盖产生非法配置时降级
|
||||||
|
|||||||
@@ -31,6 +31,96 @@ def _vllm_disable_chat_template_kwargs(chat_template_kwargs: dict) -> dict:
|
|||||||
return disable_kwargs
|
return disable_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
# Qwen3.8 chat templates only accept these three effort names.
|
||||||
|
_QWEN_REASONING_EFFORT = {
|
||||||
|
"minimal": "low",
|
||||||
|
"low": "low",
|
||||||
|
"medium": "medium",
|
||||||
|
"high": "xhigh",
|
||||||
|
"xhigh": "xhigh",
|
||||||
|
}
|
||||||
|
|
||||||
|
_OPENAI_THINKING_OFF_EXTRA_BODY = {
|
||||||
|
"enable_thinking": False,
|
||||||
|
"chat_template_kwargs": {"enable_thinking": False},
|
||||||
|
"thinking": {"type": "disabled"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_vllm_provider(model_use: str) -> bool:
|
||||||
|
lowered = (model_use or "").lower()
|
||||||
|
return "vllm_provider" in lowered or lowered.endswith(":vllmchatmodel")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_qwen38_model(model_config) -> bool:
|
||||||
|
blob = f"{getattr(model_config, 'name', '')} {getattr(model_config, 'model', '')}".lower()
|
||||||
|
return "qwen3.8" in blob
|
||||||
|
|
||||||
|
|
||||||
|
def _should_control_openai_thinking(model_config) -> bool:
|
||||||
|
"""Whether this OpenAI-compatible model needs explicit thinking on/off flags.
|
||||||
|
|
||||||
|
Qwen3.8 (and current vLLM reasoning parsers) default to thinking at ``xhigh``.
|
||||||
|
If DeerFlow does not send ``enable_thinking: false``, the first visible answer
|
||||||
|
token waits until that hidden reasoning finishes — third-party chat UIs that
|
||||||
|
disable thinking or stream the reasoning field therefore feel much faster.
|
||||||
|
"""
|
||||||
|
if getattr(model_config, "supports_thinking", False):
|
||||||
|
return True
|
||||||
|
if _is_vllm_provider(getattr(model_config, "use", "")):
|
||||||
|
return True
|
||||||
|
return _is_qwen38_model(model_config)
|
||||||
|
|
||||||
|
|
||||||
|
def _map_qwen_reasoning_effort(effort: object) -> str | None:
|
||||||
|
if effort is None:
|
||||||
|
return None
|
||||||
|
return _QWEN_REASONING_EFFORT.get(str(effort).strip().lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_openai_thinking_control(
|
||||||
|
model_config,
|
||||||
|
model_class,
|
||||||
|
model_settings_from_config: dict,
|
||||||
|
*,
|
||||||
|
thinking_enabled: bool,
|
||||||
|
reasoning_effort: object,
|
||||||
|
) -> None:
|
||||||
|
"""Inject vLLM/Qwen thinking flags when the UI toggle is on or off."""
|
||||||
|
if "extra_body" not in getattr(model_class, "model_fields", {}):
|
||||||
|
return
|
||||||
|
qwen_vllm = _is_vllm_provider(getattr(model_config, "use", "")) or _is_qwen38_model(model_config)
|
||||||
|
if thinking_enabled:
|
||||||
|
if not qwen_vllm:
|
||||||
|
return
|
||||||
|
enable_body: dict = {
|
||||||
|
"enable_thinking": True,
|
||||||
|
"chat_template_kwargs": {"enable_thinking": True},
|
||||||
|
}
|
||||||
|
mapped_effort = _map_qwen_reasoning_effort(reasoning_effort)
|
||||||
|
if mapped_effort:
|
||||||
|
enable_body["chat_template_kwargs"]["reasoning_effort"] = mapped_effort
|
||||||
|
model_settings_from_config["extra_body"] = _deep_merge_dicts(
|
||||||
|
model_settings_from_config.get("extra_body"),
|
||||||
|
enable_body,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not _should_control_openai_thinking(model_config):
|
||||||
|
return
|
||||||
|
disable_body = (
|
||||||
|
{
|
||||||
|
"enable_thinking": False,
|
||||||
|
"chat_template_kwargs": {"enable_thinking": False},
|
||||||
|
}
|
||||||
|
if qwen_vllm
|
||||||
|
else _OPENAI_THINKING_OFF_EXTRA_BODY
|
||||||
|
)
|
||||||
|
model_settings_from_config["extra_body"] = _deep_merge_dicts(
|
||||||
|
model_settings_from_config.get("extra_body"),
|
||||||
|
disable_body,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _enable_stream_usage_by_default(model_use_path: str, model_settings_from_config: dict) -> None:
|
def _enable_stream_usage_by_default(model_use_path: str, model_settings_from_config: dict) -> None:
|
||||||
"""Enable stream usage for OpenAI-compatible models unless explicitly configured.
|
"""Enable stream usage for OpenAI-compatible models unless explicitly configured.
|
||||||
|
|
||||||
@@ -127,10 +217,19 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
|
|||||||
elif has_thinking_settings and effective_wte.get("thinking", {}).get("type"):
|
elif has_thinking_settings and effective_wte.get("thinking", {}).get("type"):
|
||||||
# Native langchain_anthropic: thinking is a direct constructor parameter
|
# Native langchain_anthropic: thinking is a direct constructor parameter
|
||||||
model_settings_from_config["thinking"] = {"type": "disabled"}
|
model_settings_from_config["thinking"] = {"type": "disabled"}
|
||||||
|
requested_reasoning_effort = kwargs.get("reasoning_effort")
|
||||||
if not model_config.supports_reasoning_effort:
|
if not model_config.supports_reasoning_effort:
|
||||||
kwargs.pop("reasoning_effort", None)
|
kwargs.pop("reasoning_effort", None)
|
||||||
model_settings_from_config.pop("reasoning_effort", None)
|
model_settings_from_config.pop("reasoning_effort", None)
|
||||||
|
|
||||||
|
_apply_openai_thinking_control(
|
||||||
|
model_config,
|
||||||
|
model_class,
|
||||||
|
model_settings_from_config,
|
||||||
|
thinking_enabled=thinking_enabled,
|
||||||
|
reasoning_effort=requested_reasoning_effort,
|
||||||
|
)
|
||||||
|
|
||||||
# Hard override: force-disable thinking regardless of the per-model config shape.
|
# Hard override: force-disable thinking regardless of the per-model config shape.
|
||||||
# Injected only for OpenAI-compatible classes (those declaring `extra_body`);
|
# Injected only for OpenAI-compatible classes (those declaring `extra_body`);
|
||||||
# other providers would reject the kwarg, so they are skipped (they ignore it).
|
# other providers would reject the kwarg, so they are skipped (they ignore it).
|
||||||
|
|||||||
+12
-2
@@ -208,11 +208,21 @@ def _restore_reasoning_content(payload_msg: dict, orig_msg: AIMessage) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _extract_reasoning_content(payload: dict | None) -> str | None:
|
def _extract_reasoning_content(payload: dict | None) -> str | None:
|
||||||
"""Extract reasoning text from provider payload delta/message objects."""
|
"""Extract reasoning text from provider payload delta/message objects.
|
||||||
|
|
||||||
|
vLLM Qwen reasoning parsers use ``reasoning_content`` on streaming deltas
|
||||||
|
and ``reasoning`` on some non-stream / older builds. Read both so the UI
|
||||||
|
can show the first thinking token instead of waiting for answer content.
|
||||||
|
"""
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return None
|
return None
|
||||||
reasoning_content = payload.get("reasoning_content")
|
reasoning_content = payload.get("reasoning_content")
|
||||||
return reasoning_content if isinstance(reasoning_content, str) and reasoning_content else None
|
if isinstance(reasoning_content, str) and reasoning_content:
|
||||||
|
return reasoning_content
|
||||||
|
reasoning = payload.get("reasoning")
|
||||||
|
if isinstance(reasoning, str) and reasoning:
|
||||||
|
return reasoning
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _append_reasoning_content(message: AIMessage | AIMessageChunk, reasoning_content: str) -> AIMessage | AIMessageChunk:
|
def _append_reasoning_content(message: AIMessage | AIMessageChunk, reasoning_content: str) -> AIMessage | AIMessageChunk:
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ def _convert_delta_to_message_chunk_with_reasoning(_dict: Mapping[str, Any], def
|
|||||||
additional_kwargs["function_call"] = function_call
|
additional_kwargs["function_call"] = function_call
|
||||||
|
|
||||||
reasoning = _dict.get("reasoning")
|
reasoning = _dict.get("reasoning")
|
||||||
|
if reasoning is None:
|
||||||
|
reasoning = _dict.get("reasoning_content")
|
||||||
if reasoning is not None:
|
if reasoning is not None:
|
||||||
additional_kwargs["reasoning"] = reasoning
|
additional_kwargs["reasoning"] = reasoning
|
||||||
reasoning_text = _reasoning_to_text(reasoning)
|
reasoning_text = _reasoning_to_text(reasoning)
|
||||||
|
|||||||
@@ -239,3 +239,66 @@ def test_force_disable_thinking_skips_generic_openai_models_without_thinking_sup
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert chat_model.extra_body is None
|
assert chat_model.extra_body is None
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_body(chat_model) -> dict:
|
||||||
|
body = chat_model.extra_body or {}
|
||||||
|
return body if isinstance(body, dict) else dict(body)
|
||||||
|
|
||||||
|
|
||||||
|
def test_qwen38_flash_mode_disables_default_xhigh_thinking() -> None:
|
||||||
|
"""Qwen3.8 defaults to thinking=xhigh; flash must send enable_thinking=false."""
|
||||||
|
config = AppConfig.model_construct(
|
||||||
|
models=[
|
||||||
|
ModelConfig(
|
||||||
|
name="qwen3.8-27b",
|
||||||
|
use="langchain_openai:ChatOpenAI",
|
||||||
|
model="Qwen/Qwen3.8-27B",
|
||||||
|
api_key="test-key",
|
||||||
|
base_url="http://vllm.example/v1",
|
||||||
|
supports_thinking=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_model = create_chat_model("qwen3.8-27b", thinking_enabled=False, app_config=config)
|
||||||
|
extra_body = _extra_body(chat_model)
|
||||||
|
|
||||||
|
assert extra_body.get("enable_thinking") is False
|
||||||
|
assert extra_body.get("chat_template_kwargs", {}).get("enable_thinking") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_qwen38_thinking_mode_sends_medium_reasoning_effort() -> None:
|
||||||
|
config = AppConfig.model_construct(
|
||||||
|
models=[
|
||||||
|
ModelConfig(
|
||||||
|
name="qwen3.8-27b",
|
||||||
|
use="langchain_openai:ChatOpenAI",
|
||||||
|
model="qwen3.8-27b",
|
||||||
|
api_key="test-key",
|
||||||
|
base_url="http://vllm.example/v1",
|
||||||
|
supports_thinking=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_model = create_chat_model(
|
||||||
|
"qwen3.8-27b",
|
||||||
|
thinking_enabled=True,
|
||||||
|
reasoning_effort="medium",
|
||||||
|
app_config=config,
|
||||||
|
)
|
||||||
|
extra_body = _extra_body(chat_model)
|
||||||
|
kwargs = extra_body.get("chat_template_kwargs") or {}
|
||||||
|
|
||||||
|
assert extra_body.get("enable_thinking") is True
|
||||||
|
assert kwargs.get("enable_thinking") is True
|
||||||
|
assert kwargs.get("reasoning_effort") == "medium"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_reasoning_reads_vllm_reasoning_field() -> None:
|
||||||
|
from deerflow.models.patched_openai import _extract_reasoning_content
|
||||||
|
|
||||||
|
assert _extract_reasoning_content({"reasoning": "先分析问题"}) == "先分析问题"
|
||||||
|
assert _extract_reasoning_content({"reasoning_content": "逐步推理"}) == "逐步推理"
|
||||||
|
assert _extract_reasoning_content({"content": "答案"}) is None
|
||||||
@@ -264,7 +264,7 @@ ai_writing:
|
|||||||
# 三层覆盖优先级(高->低):
|
# 三层覆盖优先级(高->低):
|
||||||
# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处
|
# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处
|
||||||
memory:
|
memory:
|
||||||
enabled: true
|
enabled: false # 临时关闭跨会话记忆(提取/注入/Hindsight 召回与 retain、memory 工具)
|
||||||
version: v1
|
version: v1
|
||||||
provider: hindsight # 'builtin' | 'hindsight'(外部 provider 名)
|
provider: hindsight # 'builtin' | 'hindsight'(外部 provider 名)
|
||||||
# builtin 始终启用,这里只决定 external 选哪个
|
# builtin 始终启用,这里只决定 external 选哪个
|
||||||
|
|||||||
Reference in new issue
Block a user