Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbe365429d |
No files matched your search
@@ -112,6 +112,9 @@ window.__CM_RUNTIME_CONFIG__ = {
|
|||||||
// A 系统跳转地址:人物、组织、事件等列表项打开链接使用。
|
// A 系统跳转地址:人物、组织、事件等列表项打开链接使用。
|
||||||
VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain",
|
VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain",
|
||||||
|
|
||||||
|
// B 系统(XWT检索)头像代理;按部署环境设置为浏览器可访问的地址。
|
||||||
|
VITE_AIPI_MINIO_PROXY_URL: "http://minio-proxy.b.sit.4.cn/facebook-proxy",
|
||||||
|
|
||||||
// B 系统 knowes 根地址。
|
// B 系统 knowes 根地址。
|
||||||
VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes",
|
VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes",
|
||||||
|
|
||||||
|
|||||||
@@ -599,8 +599,8 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const yUserId = getYUserId();
|
const yUserId = getYUserId();
|
||||||
const userToken = getStoredAuth()?.access_token;
|
const notebookToken = getNotebookToken();
|
||||||
const iframeTokenParam = userToken ? `&token=${encodeURIComponent(userToken)}` : "";
|
const iframeTokenParam = notebookToken ? `&token=${encodeURIComponent(notebookToken)}` : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
|||||||
@@ -188,9 +188,12 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
|
|||||||
const [directionType, setDirectionType] = useState<string>('TW');
|
const [directionType, setDirectionType] = useState<string>('TW');
|
||||||
const directionValue = useRef<string>('TW');
|
const directionValue = useRef<string>('TW');
|
||||||
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || 'https://ch1.b.uat.4.cn/consumer';
|
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || 'https://ch1.b.uat.4.cn/consumer';
|
||||||
const backendUrl = typeof window !== 'undefined'
|
const minioProxyUrl = readRuntimeEnv("VITE_AIPI_MINIO_PROXY_URL") || 'http://minio-proxy.b.sit.4.cn/facebook-proxy';
|
||||||
? (readRuntimeEnv("VITE_BACKEND_BASE_URL") || 'http://localhost:8001')
|
const actorBaseUrl = readRuntimeEnv("VITE_AIPI_B2ACTOR_URL") || 'http://ty.b.uat.4.cn';
|
||||||
: 'http://localhost:8001';
|
const avatarUrl = (profileImage: string | null) => profileImage
|
||||||
|
? `${minioProxyUrl}?${new URLSearchParams({ url: profileImage, is_avatar: 'true' })}`
|
||||||
|
: `${actorBaseUrl}/manage/img/default.png`;
|
||||||
|
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || 'http://localhost:8001';
|
||||||
const [selectedSearchSources, setSelectedSearchSources] = useState<UserDataItem[]>([]);
|
const [selectedSearchSources, setSelectedSearchSources] = useState<UserDataItem[]>([]);
|
||||||
const [isImporting, setIsImporting] = useState(false);
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
|
||||||
@@ -288,9 +291,7 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
|
|||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
const result: UserDataItem[] = data.data.list;
|
const result: UserDataItem[] = data.data.list;
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
item.profile_image = item.profile_image
|
item.profile_image = avatarUrl(item.profile_image);
|
||||||
? `http://minio-proxy.b.sit.4.cn/facebook-proxy?url=${item.profile_image}&is_avatar=true`
|
|
||||||
: 'http://ty.b.uat.4.cn/manage/img/default.png';
|
|
||||||
item.classification = `${item.political_stand}-${item.history_key}-${item.history_title}`;
|
item.classification = `${item.political_stand}-${item.history_key}-${item.history_title}`;
|
||||||
item.title = item.user_name || item.screen_name || item.user_id;
|
item.title = item.user_name || item.screen_name || item.user_id;
|
||||||
item.content = item.agentInfo['账号描述'];
|
item.content = item.agentInfo['账号描述'];
|
||||||
@@ -330,9 +331,7 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
|
|||||||
const result: UserDataItem[] = data.data.data;
|
const result: UserDataItem[] = data.data.data;
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
item.hobbies = item.groupDiscussTags || [];
|
item.hobbies = item.groupDiscussTags || [];
|
||||||
item.profile_image = item.profile_image
|
item.profile_image = avatarUrl(item.profile_image);
|
||||||
? `http://minio-proxy.b.sit.4.cn/facebook-proxy?url=${item.profile_image}&is_avatar=true`
|
|
||||||
: 'http://ty.b.uat.4.cn/manage/img/default.png';
|
|
||||||
item.classification = `${item.political_stand}`;
|
item.classification = `${item.political_stand}`;
|
||||||
item.title = item.group_name || item.groupName;
|
item.title = item.group_name || item.groupName;
|
||||||
if (item.hx_info?.['用户画像']) {
|
if (item.hx_info?.['用户画像']) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function ContextWindowSettingsPage() {
|
|||||||
const update = useUpdateConversationContextSettings();
|
const update = useUpdateConversationContextSettings();
|
||||||
const enabled = data?.recent_turn_window_enabled ?? false;
|
const enabled = data?.recent_turn_window_enabled ?? false;
|
||||||
const recentTurns = data?.recent_turns ?? 3;
|
const recentTurns = data?.recent_turns ?? 3;
|
||||||
|
const useQuestionAsTitle = data?.use_question_as_title ?? false;
|
||||||
|
|
||||||
const handleChange = async (checked: boolean) => {
|
const handleChange = async (checked: boolean) => {
|
||||||
try {
|
try {
|
||||||
@@ -30,6 +31,19 @@ export function ContextWindowSettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleQuestionTitleChange = async (checked: boolean) => {
|
||||||
|
try {
|
||||||
|
await update.mutateAsync({ use_question_as_title: checked });
|
||||||
|
toast.success(
|
||||||
|
checked
|
||||||
|
? "已开启:对话标题直接使用首个问题"
|
||||||
|
: "已关闭:恢复回答后的标题生成",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "保存失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -62,6 +76,25 @@ export function ContextWindowSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-6 rounded-lg border p-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-medium">标题直接使用首个问题</div>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
开启后,首个问题会立即成为最终标题;回答结束后不再额外调用模型生成标题,也不轮询标题状态,避免影响下一轮提问。关闭后恢复原来的回答后标题生成方式。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2Icon className="text-muted-foreground size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Switch
|
||||||
|
checked={useQuestionAsTitle}
|
||||||
|
disabled={isError || update.isPending}
|
||||||
|
onCheckedChange={handleQuestionTitleChange}
|
||||||
|
aria-label="标题直接使用首个问题"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<p className="text-destructive text-sm">
|
<p className="text-destructive text-sm">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Clock3Icon, ListOrderedIcon, Loader2Icon, UsersIcon } from "lucide-react";
|
import { Clock3Icon, FileOutputIcon, ListOrderedIcon, Loader2Icon, UsersIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import {
|
import {
|
||||||
useRoundtableRoomLimitSettings,
|
useRoundtableRoomLimitSettings,
|
||||||
useUpdateRoundtableRoomLimitSettings,
|
useUpdateRoundtableRoomLimitSettings,
|
||||||
|
useWriteFileCompatibilitySettings,
|
||||||
} from "@/core/system-settings/hooks";
|
} from "@/core/system-settings/hooks";
|
||||||
import { loadRoundtableRoomSummary } from "@/roundtable-planning/api/rooms";
|
import { loadRoundtableRoomSummary } from "@/roundtable-planning/api/rooms";
|
||||||
|
|
||||||
@@ -16,7 +17,10 @@ export function RoundtableRoomSettingsPage() {
|
|||||||
const { data, isLoading, isError, isFetching, refetch } =
|
const { data, isLoading, isError, isFetching, refetch } =
|
||||||
useRoundtableRoomLimitSettings();
|
useRoundtableRoomLimitSettings();
|
||||||
const update = useUpdateRoundtableRoomLimitSettings();
|
const update = useUpdateRoundtableRoomLimitSettings();
|
||||||
|
const globalFileStream = useWriteFileCompatibilitySettings();
|
||||||
const enabled = data?.enabled ?? true;
|
const enabled = data?.enabled ?? true;
|
||||||
|
const fileStreamEnabled = data?.write_file_compatibility_enabled ?? false;
|
||||||
|
const globalFileStreamEnabled = globalFileStream.data?.enabled ?? false;
|
||||||
const [maxRooms, setMaxRooms] = useState(String(data?.max_rooms ?? 3));
|
const [maxRooms, setMaxRooms] = useState(String(data?.max_rooms ?? 3));
|
||||||
const [maxHoldMinutes, setMaxHoldMinutes] = useState(
|
const [maxHoldMinutes, setMaxHoldMinutes] = useState(
|
||||||
String(data?.max_hold_minutes ?? 30),
|
String(data?.max_hold_minutes ?? 30),
|
||||||
@@ -62,6 +66,7 @@ export function RoundtableRoomSettingsPage() {
|
|||||||
max_rooms?: number;
|
max_rooms?: number;
|
||||||
max_hold_minutes?: number;
|
max_hold_minutes?: number;
|
||||||
max_cycles?: number;
|
max_cycles?: number;
|
||||||
|
write_file_compatibility_enabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
await update.mutateAsync(next);
|
await update.mutateAsync(next);
|
||||||
@@ -215,6 +220,37 @@ export function RoundtableRoomSettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-6 rounded-lg border p-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 font-medium">
|
||||||
|
<FileOutputIcon className="size-4" />
|
||||||
|
会商文件流式兼容
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
会商生成文件并打开沙箱时使用兼容写入。全局「文件流式兼容」开着时,会商自动使用;全局关着时,打开本开关后只有会商使用,普通对话保持原来的写入方式。
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{globalFileStreamEnabled
|
||||||
|
? "当前会商会使用兼容写入,因为全局开关已开启。"
|
||||||
|
: fileStreamEnabled
|
||||||
|
? "当前只有会商使用兼容写入。"
|
||||||
|
: "当前会商使用各模型原来的写入方式。"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2Icon className="text-muted-foreground size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Switch
|
||||||
|
checked={fileStreamEnabled}
|
||||||
|
disabled={isError || update.isPending}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
void save({ write_file_compatibility_enabled: checked })
|
||||||
|
}
|
||||||
|
aria-label="会商启用文件流式兼容"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
当前全站占用:{activeCount == null ? "暂时无法读取" : `${activeCount} / ${data?.max_rooms ?? maxRooms}`}
|
当前全站占用:{activeCount == null ? "暂时无法读取" : `${activeCount} / ${data?.max_rooms ?? maxRooms}`}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { apiFetch } from "@/core/api";
|
import { apiFetch } from "@/core/api";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
|
||||||
|
export type ModelWriteFileTransport =
|
||||||
|
| "native"
|
||||||
|
| "buffered_gateway"
|
||||||
|
| "artifact_renderer"
|
||||||
|
| "text_bridge";
|
||||||
|
|
||||||
|
export type ModelWriteFileTransportToggle = "native" | "buffered_gateway";
|
||||||
|
|
||||||
export interface ManagedModel {
|
export interface ManagedModel {
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
@@ -23,6 +31,7 @@ export interface ManagedModel {
|
|||||||
thinking: Record<string, unknown> | null;
|
thinking: Record<string, unknown> | null;
|
||||||
use_responses_api: boolean | null;
|
use_responses_api: boolean | null;
|
||||||
output_version: string | null;
|
output_version: string | null;
|
||||||
|
write_file_transport: ModelWriteFileTransport;
|
||||||
extra_settings: Record<string, unknown>;
|
extra_settings: Record<string, unknown>;
|
||||||
api_key_configured: boolean;
|
api_key_configured: boolean;
|
||||||
api_key_source: string | null;
|
api_key_source: string | null;
|
||||||
@@ -120,6 +129,22 @@ export async function updateManagedModelReasoningEffortCapability(
|
|||||||
return response.json() as Promise<ManagedModel>;
|
return response.json() as Promise<ManagedModel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateManagedModelWriteFileTransport(
|
||||||
|
name: string,
|
||||||
|
writeFileTransport: ModelWriteFileTransportToggle,
|
||||||
|
): Promise<ManagedModel> {
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${getBackendBaseURL()}/api/admin/models/write-file-transport/${encodeURIComponent(name)}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ write_file_transport: writeFileTransport }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) return readError(response, "保存文件流式兼容设置失败");
|
||||||
|
return response.json() as Promise<ManagedModel>;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteManagedModel(name: string): Promise<void> {
|
export async function deleteManagedModel(name: string): Promise<void> {
|
||||||
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/${encodeURIComponent(name)}`, {
|
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/${encodeURIComponent(name)}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
testManagedModel,
|
testManagedModel,
|
||||||
updateManagedModel,
|
updateManagedModel,
|
||||||
updateManagedModelReasoningEffortCapability,
|
updateManagedModelReasoningEffortCapability,
|
||||||
|
updateManagedModelWriteFileTransport,
|
||||||
type ModelSavePayload,
|
type ModelSavePayload,
|
||||||
|
type ModelWriteFileTransportToggle,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
export const MANAGED_MODELS_QUERY_KEY = ["admin", "model-config"] as const;
|
export const MANAGED_MODELS_QUERY_KEY = ["admin", "model-config"] as const;
|
||||||
@@ -66,6 +68,25 @@ export function useUpdateManagedModelReasoningEffortCapability() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUpdateManagedModelWriteFileTransport() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
name,
|
||||||
|
writeFileTransport,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
writeFileTransport: ModelWriteFileTransportToggle;
|
||||||
|
}) => updateManagedModelWriteFileTransport(name, writeFileTransport),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: MANAGED_MODELS_QUERY_KEY }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["models"] }),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useDeleteManagedModel() {
|
export function useDeleteManagedModel() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -728,10 +728,12 @@ export interface MemoryRuntimeSettings {
|
|||||||
export interface ConversationContextSettings {
|
export interface ConversationContextSettings {
|
||||||
recent_turn_window_enabled: boolean;
|
recent_turn_window_enabled: boolean;
|
||||||
recent_turns: number;
|
recent_turns: number;
|
||||||
|
use_question_as_title: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversationContextUpdate {
|
export interface ConversationContextUpdate {
|
||||||
recent_turn_window_enabled: boolean;
|
recent_turn_window_enabled?: boolean;
|
||||||
|
use_question_as_title?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoundtableRoomLimitSettings {
|
export interface RoundtableRoomLimitSettings {
|
||||||
@@ -739,6 +741,7 @@ export interface RoundtableRoomLimitSettings {
|
|||||||
max_rooms: number;
|
max_rooms: number;
|
||||||
max_hold_minutes: number;
|
max_hold_minutes: number;
|
||||||
max_cycles: number;
|
max_cycles: number;
|
||||||
|
write_file_compatibility_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoundtableRoomLimitUpdate {
|
export interface RoundtableRoomLimitUpdate {
|
||||||
@@ -746,6 +749,7 @@ export interface RoundtableRoomLimitUpdate {
|
|||||||
max_rooms?: number;
|
max_rooms?: number;
|
||||||
max_hold_minutes?: number;
|
max_hold_minutes?: number;
|
||||||
max_cycles?: number;
|
max_cycles?: number;
|
||||||
|
write_file_compatibility_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrdinaryKnowledgeRetrievalSettings {
|
export interface OrdinaryKnowledgeRetrievalSettings {
|
||||||
|
|||||||
@@ -1340,11 +1340,11 @@ export function useThreadStream({
|
|||||||
runId = inflight?.run_id ?? null;
|
runId = inflight?.run_id ?? null;
|
||||||
if (runId) {
|
if (runId) {
|
||||||
activeRunIdRef.current = runId;
|
activeRunIdRef.current = runId;
|
||||||
} else {
|
} else {
|
||||||
const lastTerminal = runs.find((item) =>
|
const lastTerminal = runs.find((item) =>
|
||||||
TERMINAL_RUN_STATUSES.has(item.status),
|
TERMINAL_RUN_STATUSES.has(item.status),
|
||||||
);
|
);
|
||||||
if (lastTerminal) terminalStatus = lastTerminal.status;
|
if (lastTerminal) terminalStatus = lastTerminal.status;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!isCurrentRecovery()) return "unknown";
|
if (!isCurrentRecovery()) return "unknown";
|
||||||
@@ -1357,45 +1357,45 @@ export function useThreadStream({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terminalStatus) {
|
if (terminalStatus) {
|
||||||
try {
|
try {
|
||||||
return (await applyTerminalState(terminalStatus))
|
return (await applyTerminalState(terminalStatus))
|
||||||
? "terminal"
|
? "terminal"
|
||||||
: "unknown";
|
: "unknown";
|
||||||
} catch {
|
} catch {
|
||||||
if (isCurrentRecovery()) {
|
if (isCurrentRecovery()) {
|
||||||
lastStreamActivityAtRef.current = Date.now();
|
lastStreamActivityAtRef.current = Date.now();
|
||||||
setStreamHoldOpen(true);
|
setStreamHoldOpen(true);
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!runId) {
|
if (!runId) {
|
||||||
// The backend confirms there is no in-flight run, but an SDK reader
|
// The backend confirms there is no in-flight run, but an SDK reader
|
||||||
// can remain locally open after a proxy/SSE disconnect. Detach that
|
// can remain locally open after a proxy/SSE disconnect. Detach that
|
||||||
// reader only after removing its reconnect marker so stop() cannot
|
// reader only after removing its reconnect marker so stop() cannot
|
||||||
// accidentally cancel a server run.
|
// accidentally cancel a server run.
|
||||||
try {
|
try {
|
||||||
const readerStopped = await stopRecoveredRunReader({
|
const readerStopped = await stopRecoveredRunReader({
|
||||||
isCurrent: isCurrentRecovery,
|
isCurrent: isCurrentRecovery,
|
||||||
clearReconnect: () => clearActiveRunId(tid),
|
clearReconnect: () => clearActiveRunId(tid),
|
||||||
stop: () => threadStreamRef.current?.stop(),
|
stop: () => threadStreamRef.current?.stop(),
|
||||||
});
|
});
|
||||||
if (!readerStopped) return "unknown";
|
if (!readerStopped) return "unknown";
|
||||||
activeRunIdRef.current = null;
|
activeRunIdRef.current = null;
|
||||||
setStreamHoldOpen(false);
|
setStreamHoldOpen(false);
|
||||||
streamReconnectAttemptsRef.current = 0;
|
streamReconnectAttemptsRef.current = 0;
|
||||||
backgroundHistory.confirmTerminal();
|
backgroundHistory.confirmTerminal();
|
||||||
void backgroundHistory.refresh(false);
|
void backgroundHistory.refresh(false);
|
||||||
return "terminal";
|
return "terminal";
|
||||||
} catch {
|
} catch {
|
||||||
if (isCurrentRecovery()) {
|
if (isCurrentRecovery()) {
|
||||||
lastStreamActivityAtRef.current = Date.now();
|
lastStreamActivityAtRef.current = Date.now();
|
||||||
setStreamHoldOpen(true);
|
setStreamHoldOpen(true);
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A second UI failure can pause recovery while /runs is in flight.
|
// A second UI failure can pause recovery while /runs is in flight.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
CircleAlertIcon,
|
CircleAlertIcon,
|
||||||
CpuIcon,
|
CpuIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
FileOutputIcon,
|
||||||
GripVerticalIcon,
|
GripVerticalIcon,
|
||||||
KeyRoundIcon,
|
KeyRoundIcon,
|
||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
@@ -54,7 +55,9 @@ import {
|
|||||||
useSaveManagedModel,
|
useSaveManagedModel,
|
||||||
useTestManagedModel,
|
useTestManagedModel,
|
||||||
useUpdateManagedModelReasoningEffortCapability,
|
useUpdateManagedModelReasoningEffortCapability,
|
||||||
|
useUpdateManagedModelWriteFileTransport,
|
||||||
} from "@/core/model-management";
|
} from "@/core/model-management";
|
||||||
|
import { useWriteFileCompatibilitySettings } from "@/core/system-settings/hooks";
|
||||||
import {
|
import {
|
||||||
WorkspaceBody,
|
WorkspaceBody,
|
||||||
WorkspaceContainer,
|
WorkspaceContainer,
|
||||||
@@ -197,25 +200,44 @@ function SortableModelCard({
|
|||||||
dragEnabled,
|
dragEnabled,
|
||||||
reasoningCapabilityDisabled,
|
reasoningCapabilityDisabled,
|
||||||
reasoningCapabilityPending,
|
reasoningCapabilityPending,
|
||||||
|
globalWriteFileCompatEnabled,
|
||||||
|
writeFileCompatPending,
|
||||||
onConfigure,
|
onConfigure,
|
||||||
onDelete,
|
onDelete,
|
||||||
onReasoningCapabilityChange,
|
onReasoningCapabilityChange,
|
||||||
onReasoningEffortChange,
|
onReasoningEffortChange,
|
||||||
|
onWriteFileCompatChange,
|
||||||
}: {
|
}: {
|
||||||
model: ManagedModel;
|
model: ManagedModel;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
dragEnabled: boolean;
|
dragEnabled: boolean;
|
||||||
reasoningCapabilityDisabled: boolean;
|
reasoningCapabilityDisabled: boolean;
|
||||||
reasoningCapabilityPending: boolean;
|
reasoningCapabilityPending: boolean;
|
||||||
|
globalWriteFileCompatEnabled: boolean;
|
||||||
|
writeFileCompatPending: boolean;
|
||||||
onConfigure: () => void;
|
onConfigure: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onReasoningCapabilityChange: (enabled: boolean) => void;
|
onReasoningCapabilityChange: (enabled: boolean) => void;
|
||||||
onReasoningEffortChange: (effort: "low" | "medium" | "xhigh") => void;
|
onReasoningEffortChange: (effort: "low" | "medium" | "xhigh") => void;
|
||||||
|
onWriteFileCompatChange: (enabled: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: model.name,
|
id: model.name,
|
||||||
disabled: !dragEnabled,
|
disabled: !dragEnabled,
|
||||||
});
|
});
|
||||||
|
const writeFileCompatEligible =
|
||||||
|
(model.use === "langchain_openai:ChatOpenAI" ||
|
||||||
|
model.use === "deerflow.models.patched_openai:PatchedChatOpenAI") &&
|
||||||
|
!model.use_responses_api;
|
||||||
|
const perModelWriteFileCompat = model.write_file_transport === "buffered_gateway";
|
||||||
|
const writeFileCompatActive = perModelWriteFileCompat || globalWriteFileCompatEnabled;
|
||||||
|
const writeFileCompatHint = globalWriteFileCompatEnabled
|
||||||
|
? "全局文件流式兼容已开启,所有兼容模型均已强制启用;关闭全局开关后恢复此处的单独设置。"
|
||||||
|
: !writeFileCompatEligible
|
||||||
|
? "仅支持 OpenAI 兼容接口(Chat Completions)模型。"
|
||||||
|
: perModelWriteFileCompat
|
||||||
|
? "该模型已启用:文件正文改由普通文本流生成,适配缓存长工具参数的网关。"
|
||||||
|
: "针对会缓存长工具参数的内网模型网关;开启后文件正文改由普通文本流生成,对问答与文件卡片完全透明。";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
@@ -288,6 +310,9 @@ function SortableModelCard({
|
|||||||
{model.supports_vision && (
|
{model.supports_vision && (
|
||||||
<ModelCapabilityBadge icon={EyeIcon} label="视觉" className="bg-amber-500/10 text-amber-700 dark:text-amber-300" />
|
<ModelCapabilityBadge icon={EyeIcon} label="视觉" className="bg-amber-500/10 text-amber-700 dark:text-amber-300" />
|
||||||
)}
|
)}
|
||||||
|
{writeFileCompatActive && writeFileCompatEligible && (
|
||||||
|
<ModelCapabilityBadge icon={FileOutputIcon} label="文件流式兼容" className="bg-sky-500/10 text-sky-700 dark:text-sky-300" />
|
||||||
|
)}
|
||||||
{model.supports_reasoning_effort && (
|
{model.supports_reasoning_effort && (
|
||||||
<ModelCapabilityBadge label={`全局默认:${model.default_reasoning_effort === "low" ? "低" : model.default_reasoning_effort === "medium" ? "中" : "Ultra"}`} className="bg-muted text-muted-foreground" />
|
<ModelCapabilityBadge label={`全局默认:${model.default_reasoning_effort === "low" ? "低" : model.default_reasoning_effort === "medium" ? "中" : "Ultra"}`} className="bg-muted text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
@@ -332,6 +357,22 @@ function SortableModelCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-4 rounded-lg border border-border px-3 py-2.5">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
|
||||||
|
<span>文件流式兼容</span>
|
||||||
|
<Tag theme="default" variant="light" size="small">config.yaml</Tag>
|
||||||
|
{globalWriteFileCompatEnabled && <Tag theme="primary" variant="light" size="small">全局已开启</Tag>}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">{writeFileCompatHint}</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
value={writeFileCompatActive}
|
||||||
|
loading={writeFileCompatPending}
|
||||||
|
disabled={globalWriteFileCompatEnabled || !writeFileCompatEligible}
|
||||||
|
onChange={(value) => onWriteFileCompatChange(Boolean(value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -446,6 +487,9 @@ export default function ModelManagementPage() {
|
|||||||
const reorder = useReorderManagedModels();
|
const reorder = useReorderManagedModels();
|
||||||
const updateReasoningCapability =
|
const updateReasoningCapability =
|
||||||
useUpdateManagedModelReasoningEffortCapability();
|
useUpdateManagedModelReasoningEffortCapability();
|
||||||
|
const updateWriteFileTransport = useUpdateManagedModelWriteFileTransport();
|
||||||
|
const { data: writeFileCompatSettings } = useWriteFileCompatibilitySettings();
|
||||||
|
const globalWriteFileCompatEnabled = writeFileCompatSettings?.enabled ?? false;
|
||||||
const sourceModels = data?.models ?? [];
|
const sourceModels = data?.models ?? [];
|
||||||
const orderedModels = useMemo(() => {
|
const orderedModels = useMemo(() => {
|
||||||
if (!pendingOrder || pendingOrder.length !== sourceModels.length) return sourceModels;
|
if (!pendingOrder || pendingOrder.length !== sourceModels.length) return sourceModels;
|
||||||
@@ -511,6 +555,26 @@ export default function ModelManagementPage() {
|
|||||||
showGlobalMessage("error", error instanceof Error ? error.message : "保存全局思考强度失败");
|
showGlobalMessage("error", error instanceof Error ? error.message : "保存全局思考强度失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleWriteFileCompatChange = async (
|
||||||
|
model: ManagedModel,
|
||||||
|
enabled: boolean,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await updateWriteFileTransport.mutateAsync({
|
||||||
|
name: model.name,
|
||||||
|
writeFileTransport: enabled ? "buffered_gateway" : "native",
|
||||||
|
});
|
||||||
|
showGlobalMessage(
|
||||||
|
"success",
|
||||||
|
`模型“${model.display_name || model.name}”的文件流式兼容已${enabled ? "开启" : "关闭"},从下一次发起的问答开始生效。`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showGlobalMessage(
|
||||||
|
"error",
|
||||||
|
error instanceof Error ? error.message : "保存文件流式兼容设置失败",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
const dragEnabled = filter === "all" && !search.trim() && !reorder.isPending;
|
const dragEnabled = filter === "all" && !search.trim() && !reorder.isPending;
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||||
@@ -637,6 +701,11 @@ export default function ModelManagementPage() {
|
|||||||
updateReasoningCapability.isPending &&
|
updateReasoningCapability.isPending &&
|
||||||
updateReasoningCapability.variables?.name === model.name
|
updateReasoningCapability.variables?.name === model.name
|
||||||
}
|
}
|
||||||
|
globalWriteFileCompatEnabled={globalWriteFileCompatEnabled}
|
||||||
|
writeFileCompatPending={
|
||||||
|
updateWriteFileTransport.isPending &&
|
||||||
|
updateWriteFileTransport.variables?.name === model.name
|
||||||
|
}
|
||||||
onConfigure={() => setEditor(model)}
|
onConfigure={() => setEditor(model)}
|
||||||
onDelete={() => setDeleteTarget(model)}
|
onDelete={() => setDeleteTarget(model)}
|
||||||
onReasoningCapabilityChange={(enabled) =>
|
onReasoningCapabilityChange={(enabled) =>
|
||||||
@@ -645,6 +714,9 @@ export default function ModelManagementPage() {
|
|||||||
onReasoningEffortChange={(effort) =>
|
onReasoningEffortChange={(effort) =>
|
||||||
void handleReasoningEffortChange(model, effort)
|
void handleReasoningEffortChange(model, effort)
|
||||||
}
|
}
|
||||||
|
onWriteFileCompatChange={(enabled) =>
|
||||||
|
void handleWriteFileCompatChange(model, enabled)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1489,7 +1489,7 @@ scheduler runs once per deployment. Per run:
|
|||||||
- **可检测的模型失败**:`LLMErrorHandlingMiddleware` 在同一模型重试 3 次后**不抛异常**,而是返回兜底 `AIMessage` —— 现在给它打 `additional_kwargs[LLM_ERROR_MARKER="deerflow_llm_error"]=reason`(model_dump 保留,经 checkpointer 序列化仍在)。`is_llm_error_message(msg)` 据标记(回退按已知兜底文案正文)识别,返回 reason。
|
- **可检测的模型失败**:`LLMErrorHandlingMiddleware` 在同一模型重试 3 次后**不抛异常**,而是返回兜底 `AIMessage` —— 现在给它打 `additional_kwargs[LLM_ERROR_MARKER="deerflow_llm_error"]=reason`(model_dump 保留,经 checkpointer 序列化仍在)。`is_llm_error_message(msg)` 据标记(回退按已知兜底文案正文)识别,返回 reason。
|
||||||
- **候选模型链**:保留真正模型故障后的自动换模,但“仅有思考 / 暂时无正文 / 空交付”不再判作模型故障,绝不据此切换用户选择的模型。`app/gateway/roundtable_model_fallback.py::fallback_model_chain(current)` 按 `config.yaml models[]` 顺序给出 `[当前模型, …其余]`(去重保序)。`reason_text(reason)` 翻译成简短中文。总开关 env `ROUNDTABLE_MODEL_FALLBACK`(默认开,`0/false/off` 关);`ROUNDTABLE_MODEL_FALLBACK_MAX`(0=试完全部)。
|
- **候选模型链**:保留真正模型故障后的自动换模,但“仅有思考 / 暂时无正文 / 空交付”不再判作模型故障,绝不据此切换用户选择的模型。`app/gateway/roundtable_model_fallback.py::fallback_model_chain(current)` 按 `config.yaml models[]` 顺序给出 `[当前模型, …其余]`(去重保序)。`reason_text(reason)` 翻译成简短中文。总开关 env `ROUNDTABLE_MODEL_FALLBACK`(默认开,`0/false/off` 关);`ROUNDTABLE_MODEL_FALLBACK_MAX`(0=试完全部)。
|
||||||
- **触发换模型的情形**:① 单轮 run 抛异常(start_run 失败 / idle 卡死 → `timeout`,其余 `exception`);② 兜底错误文案(`is_llm_error_message`)。交付无效、仅有思考、空串以及普通总控轮既无正文又无派活**均不触发换模**;唯一例外是无工具的最终收口请求已经完整结束仍无正文,此时结果客观不可交付,才继续自动换下一个模型。席位仍只认剥掉 `<think>` 后的可见正文(`deerflow.agents.roundtable_orchestrator.delivery`:`visible_seat_text` / `classify_seat_delivery`):前台 `_special_run` 对无效交付 yield `status:"invalid_delivery"` 且不广播;`GET /last-ai-message` 回 `{delivered, content, length, invalid_reason}`(思考-only 标 `thinking_only`,总控席位清单标 ⚠️)。后台引擎只把可见正文写入 `deliveries`,无效席位继续走既有未交付处理,不切换模型。Step3 单例(report/summary/dashboard/ingest/结构化)的产物是落盘文件,同样只对真模型错误换模型。
|
- **触发换模型的情形**:① 单轮 run 抛异常(start_run 失败 / idle 卡死 → `timeout`,其余 `exception`);② 兜底错误文案(`is_llm_error_message`)。交付无效、仅有思考、空串以及普通总控轮既无正文又无派活**均不触发换模**;唯一例外是无工具的最终收口请求已经完整结束仍无正文,此时结果客观不可交付,才继续自动换下一个模型。席位仍只认剥掉 `<think>` 后的可见正文(`deerflow.agents.roundtable_orchestrator.delivery`:`visible_seat_text` / `classify_seat_delivery`):前台 `_special_run` 对无效交付 yield `status:"invalid_delivery"` 且不广播;`GET /last-ai-message` 回 `{delivered, content, length, invalid_reason}`(思考-only 标 `thinking_only`,总控席位清单标 ⚠️)。后台引擎只把可见正文写入 `deliveries`,无效席位继续走既有未交付处理,不切换模型。Step3 单例(report/summary/dashboard/ingest/结构化)的产物是落盘文件,同样只对真模型错误换模型。
|
||||||
- **Step2 总控工具与可配置收口轮数**:普通总控使用真正白名单,只允许 `agent_orchestration` / `ask_clarification`;`write_file`、`present_files`、技能目录与文件/命令工具均不可见。`system_settings.json -> roundtable_room_limit.max_cycles`(默认 8,范围 2..100;管理员在设置弹窗「会商并发与收口」中修改)控制 recommend 模式到第几轮切换为显式 `finalize_only`:空工具白名单、注入已有有效席位全文、要求直接输出最终总结(失败席位如实标缺口),不能表现为“已停止”。一次会商启动时锁定当时值,运行中修改从下一次研讨生效;前台实时会商与后台作业读取同一设置。chain / dag 的最终总结仍在既定席位完成后走同一纯正文路径;LangGraph 单轮 recursion limit 仍为 1000。
|
- **Step2 总控工具与可配置收口轮数**:普通总控使用真正白名单,只允许 `agent_orchestration` / `ask_clarification`;`write_file`、`present_files`、技能目录与文件/命令工具均不可见。`system_settings.json -> roundtable_room_limit.max_cycles`(默认 8,范围 2..100;管理员在设置弹窗「会商并发与收口」中修改)控制 recommend 模式到第几轮切换为显式 `finalize_only`:空工具白名单、注入已有有效席位全文、要求直接输出最终总结(失败席位如实标缺口),不能表现为“已停止”。一次会商启动时锁定当时值,运行中修改从下一次研讨生效;前台实时会商与后台作业读取同一设置。同一页的「会商文件流式兼容」(`write_file_compatibility_enabled`,默认关) 只作用于会商总控、席位、第三步和后台作业:全局文件流式兼容开着时会商一并使用;全局关着而本开关开着时,只有这些会商 run 改走 `buffered_gateway`。普通对话不读这个开关。chain / dag 的最终总结仍在既定席位完成后走同一纯正文路径;LangGraph 单轮 recursion limit 仍为 1000。
|
||||||
- **后台**:`InProcessRoundtableGateway._run_turn_with_fallback(...)` 包住 `_run_agent_turn`,逐候选模型试;换模型落 `model_switch` 诊断、全失败落 `model_fallback_exhausted`。`run_leader/run_seat/run_report/run_summary/run_dashboard_data` 全走它(report/summary/dashboard 的产物自愈重试保留,并记住已切到的可用模型用于后续续写轮)。各 Turn/Result 带回 `switches: list[ModelSwitch]`,引擎 `_emit_switches` → `JobProgressWriter.model_switch(...)` 往研讨时间线插一条 `role="system"` 系统提示气泡。
|
- **后台**:`InProcessRoundtableGateway._run_turn_with_fallback(...)` 包住 `_run_agent_turn`,逐候选模型试;换模型落 `model_switch` 诊断、全失败落 `model_fallback_exhausted`。`run_leader/run_seat/run_report/run_summary/run_dashboard_data` 全走它(report/summary/dashboard 的产物自愈重试保留,并记住已切到的可用模型用于后续续写轮)。各 Turn/Result 带回 `switches: list[ModelSwitch]`,引擎 `_emit_switches` → `JobProgressWriter.model_switch(...)` 往研讨时间线插一条 `role="system"` 系统提示气泡。
|
||||||
- **前台**:`_special_run`/`_leader_run` stream 收尾后仅检测明确的模型错误标记;命中才 yield `{status:"model_switch", role, agent_name, failed_model, next_model, reason}` 帧并用下一个模型重新流式,思考-only/空正文不进入该分支;终态帧带 `model_used`。结构化抽取 `flow_extract.py` 的 round1 `model.astream` 也包了同款换模型循环(emit `event:"model_switch"` 帧)。
|
- **前台**:`_special_run`/`_leader_run` stream 收尾后仅检测明确的模型错误标记;命中才 yield `{status:"model_switch", role, agent_name, failed_model, next_model, reason}` 帧并用下一个模型重新流式,思考-only/空正文不进入该分支;终态帧带 `model_used`。结构化抽取 `flow_extract.py` 的 round1 `model.astream` 也包了同款换模型循环(emit `event:"model_switch"` 帧)。
|
||||||
- **Step2 单席位隔离**:`engine._run_recommend/_run_chain/_run_dag` 的每个席位执行包 try/except —— 一个席位彻底失败(落 `seat_failed` 诊断 + 置空交付)不拖垮整批/整链/整个作业。
|
- **Step2 单席位隔离**:`engine._run_recommend/_run_chain/_run_dag` 的每个席位执行包 try/except —— 一个席位彻底失败(落 `seat_failed` 诊断 + 置空交付)不拖垮整批/整链/整个作业。
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from app.gateway.routers._roundtable_seed import (
|
|||||||
SUMMARY_AGENT_ID,
|
SUMMARY_AGENT_ID,
|
||||||
ensure_roundtable_functional_agents,
|
ensure_roundtable_functional_agents,
|
||||||
)
|
)
|
||||||
|
from app.gateway.routers.multi_agent import _roundtable_file_stream_context
|
||||||
from app.gateway.services import RUN_SCOPE_BACKGROUND_CHILD
|
from app.gateway.services import RUN_SCOPE_BACKGROUND_CHILD
|
||||||
from deerflow.agents.roundtable_orchestrator.delivery import classify_seat_delivery, visible_seat_text
|
from deerflow.agents.roundtable_orchestrator.delivery import classify_seat_delivery, visible_seat_text
|
||||||
from deerflow.agents.roundtable_orchestrator.report_json import extract_report_json
|
from deerflow.agents.roundtable_orchestrator.report_json import extract_report_json
|
||||||
@@ -365,6 +366,7 @@ async def _run_agent_turn(
|
|||||||
# 被上轮会话 memory.json 带偏(臆造席位 / 直接干活)。详见该处注释。
|
# 被上轮会话 memory.json 带偏(臆造席位 / 直接干活)。详见该处注释。
|
||||||
"memory_injection_enabled": False,
|
"memory_injection_enabled": False,
|
||||||
"allowed_tools": allowed_tools,
|
"allowed_tools": allowed_tools,
|
||||||
|
**_roundtable_file_stream_context(),
|
||||||
},
|
},
|
||||||
excluded_tools=excluded_tools,
|
excluded_tools=excluded_tools,
|
||||||
skill_stop_names=skill_stop_names,
|
skill_stop_names=skill_stop_names,
|
||||||
|
|||||||
@@ -224,6 +224,12 @@ class ReasoningEffortCapabilityRequest(BaseModel):
|
|||||||
default_reasoning_effort: Literal["low", "medium", "xhigh"] | None = None
|
default_reasoning_effort: Literal["low", "medium", "xhigh"] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class WriteFileTransportToggleRequest(BaseModel):
|
||||||
|
"""Per-model 文件流式兼容 switch: off keeps native, on selects buffered_gateway."""
|
||||||
|
|
||||||
|
write_file_transport: Literal["native", "buffered_gateway"]
|
||||||
|
|
||||||
|
|
||||||
class ModelOrderRequest(BaseModel):
|
class ModelOrderRequest(BaseModel):
|
||||||
"""The complete ordered set of model stable names."""
|
"""The complete ordered set of model stable names."""
|
||||||
|
|
||||||
@@ -313,6 +319,39 @@ def _validate_reasoning_capability(model: dict[str, Any], enabled: bool) -> None
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ``write_file_transport=buffered_gateway`` is implemented only by the patched
|
||||||
|
# OpenAI-compatible chat-completions classes (see deerflow.models.factory);
|
||||||
|
# every other driver is rejected there at construction time, so mirror the
|
||||||
|
# same constraint here with a clearer message.
|
||||||
|
_BUFFERED_GATEWAY_COMPATIBLE_USES = frozenset(
|
||||||
|
{
|
||||||
|
"langchain_openai:ChatOpenAI",
|
||||||
|
"deerflow.models.patched_openai:PatchedChatOpenAI",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_write_file_transport(model: dict[str, Any], transport: str) -> None:
|
||||||
|
if transport != WriteFileTransport.BUFFERED_GATEWAY.value:
|
||||||
|
return
|
||||||
|
if str(model.get("use") or "") not in _BUFFERED_GATEWAY_COMPATIBLE_USES:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail=(
|
||||||
|
"write_file compatibility requires an OpenAI-compatible "
|
||||||
|
"langchain_openai:ChatOpenAI model"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if bool(model.get("use_responses_api")):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail=(
|
||||||
|
"write_file compatibility requires the chat-completions API, "
|
||||||
|
"not use_responses_api"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _restore_capability(
|
async def _restore_capability(
|
||||||
store: ModelCapabilityStore,
|
store: ModelCapabilityStore,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
@@ -672,6 +711,48 @@ async def update_reasoning_effort_capability(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/write-file-transport/{model_name:path}",
|
||||||
|
response_model=ModelAdminItem,
|
||||||
|
)
|
||||||
|
async def update_write_file_transport(
|
||||||
|
model_name: str,
|
||||||
|
body: WriteFileTransportToggleRequest,
|
||||||
|
request: Request,
|
||||||
|
) -> ModelAdminItem:
|
||||||
|
"""Toggle one model's buffered-gateway write_file compatibility in config.yaml.
|
||||||
|
|
||||||
|
The global system-settings switch keeps overriding every compatible model
|
||||||
|
while it is enabled; this per-model value becomes effective as soon as the
|
||||||
|
global switch is turned off.
|
||||||
|
"""
|
||||||
|
|
||||||
|
await _require_admin(request)
|
||||||
|
async with _MUTATION_LOCK:
|
||||||
|
path = _resolve_config_path()
|
||||||
|
raw_before, _, models = await asyncio.to_thread(_read_config_data, path)
|
||||||
|
index = next((i for i, item in enumerate(models) if str(item.get("name")) == model_name), None)
|
||||||
|
if index is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Model '{model_name}' was not found")
|
||||||
|
updated = copy.deepcopy(models[index])
|
||||||
|
_validate_write_file_transport(updated, body.write_file_transport)
|
||||||
|
updated["write_file_transport"] = body.write_file_transport
|
||||||
|
try:
|
||||||
|
ModelConfig.model_validate(updated)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=f"Invalid model configuration: {exc}") from exc
|
||||||
|
next_models = [*models]
|
||||||
|
next_models[index] = updated
|
||||||
|
await _save_models(request, raw_before, next_models)
|
||||||
|
capabilities = await _capabilities_by_name(request)
|
||||||
|
capability = capabilities.get(model_name, {})
|
||||||
|
return _to_admin_item(
|
||||||
|
updated,
|
||||||
|
supports_reasoning_effort=bool(capability.get("supports_reasoning_effort", False)),
|
||||||
|
default_reasoning_effort=str(capability.get("default_reasoning_effort") or "xhigh"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{model_name:path}", response_model=ModelAdminItem)
|
@router.put("/{model_name:path}", response_model=ModelAdminItem)
|
||||||
async def update_model_configuration(model_name: str, body: ModelUpsertRequest, request: Request) -> ModelAdminItem:
|
async def update_model_configuration(model_name: str, body: ModelUpsertRequest, request: Request) -> ModelAdminItem:
|
||||||
admin = await _require_admin(request)
|
admin = await _require_admin(request)
|
||||||
|
|||||||
@@ -742,6 +742,7 @@ def _run_payload(
|
|||||||
cancel_on_disconnect: bool = False,
|
cancel_on_disconnect: bool = False,
|
||||||
extra_context: dict[str, Any] | None = None,
|
extra_context: dict[str, Any] | None = None,
|
||||||
files: list[dict[str, Any]] | None = None,
|
files: list[dict[str, Any]] | None = None,
|
||||||
|
roundtable_file_stream: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""构造 ``POST /api/threads/{thread_id}/runs/stream`` 的请求体。
|
"""构造 ``POST /api/threads/{thread_id}/runs/stream`` 的请求体。
|
||||||
|
|
||||||
@@ -819,9 +820,20 @@ def _run_payload(
|
|||||||
# 供 read_peer_delivery 工具按需读取)。只进 context dict,不进 prompt → 不调工具就不耗 token。
|
# 供 read_peer_delivery 工具按需读取)。只进 context dict,不进 prompt → 不调工具就不耗 token。
|
||||||
if extra_context:
|
if extra_context:
|
||||||
payload["context"].update(extra_context)
|
payload["context"].update(extra_context)
|
||||||
|
if roundtable_file_stream:
|
||||||
|
payload["context"].update(_roundtable_file_stream_context())
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _roundtable_file_stream_context() -> dict[str, bool]:
|
||||||
|
"""会商专用文件流式兼容。关闭时不写 context,本轮仍只看全局开关。"""
|
||||||
|
from deerflow.config.system_settings import get_roundtable_room_limit_settings
|
||||||
|
|
||||||
|
if get_roundtable_room_limit_settings().write_file_compatibility_enabled:
|
||||||
|
return {"roundtable_write_file_compatibility": True}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# POST /api/multi-agent/init — Step 2 席位 + 总控初始化(SSE)
|
# POST /api/multi-agent/init — Step 2 席位 + 总控初始化(SSE)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1697,6 +1709,7 @@ async def _leader_run(
|
|||||||
cancel_on_disconnect=True,
|
cancel_on_disconnect=True,
|
||||||
# 用户随干预上传的参考文件 → 总控可用 read_file 等工具读取后再派活。
|
# 用户随干预上传的参考文件 → 总控可用 read_file 等工具读取后再派活。
|
||||||
files=files,
|
files=files,
|
||||||
|
roundtable_file_stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
body = _mk_body(current_model)
|
body = _mk_body(current_model)
|
||||||
@@ -1980,6 +1993,7 @@ async def _special_run(
|
|||||||
),
|
),
|
||||||
# 用户随干预上传的参考文件 → 席位可用 read_file 等工具读取。
|
# 用户随干预上传的参考文件 → 席位可用 read_file 等工具读取。
|
||||||
files=files,
|
files=files,
|
||||||
|
roundtable_file_stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
t_payload_built = time.perf_counter()
|
t_payload_built = time.perf_counter()
|
||||||
|
|||||||
@@ -456,10 +456,15 @@ class ConversationContextResponse(BaseModel):
|
|||||||
default=3,
|
default=3,
|
||||||
description="Fixed number of real user turns retained by the sliding window",
|
description="Fixed number of real user turns retained by the sliding window",
|
||||||
)
|
)
|
||||||
|
use_question_as_title: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Whether the first user question is the final title, without post-answer title generation",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConversationContextUpdateRequest(BaseModel):
|
class ConversationContextUpdateRequest(BaseModel):
|
||||||
recent_turn_window_enabled: bool
|
recent_turn_window_enabled: bool | None = None
|
||||||
|
use_question_as_title: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class RoundtableRoomLimitResponse(BaseModel):
|
class RoundtableRoomLimitResponse(BaseModel):
|
||||||
@@ -467,6 +472,7 @@ class RoundtableRoomLimitResponse(BaseModel):
|
|||||||
max_rooms: int = 3
|
max_rooms: int = 3
|
||||||
max_hold_minutes: int = 30
|
max_hold_minutes: int = 30
|
||||||
max_cycles: int = 8
|
max_cycles: int = 8
|
||||||
|
write_file_compatibility_enabled: bool = False
|
||||||
|
|
||||||
|
|
||||||
class RoundtableRoomLimitUpdateRequest(BaseModel):
|
class RoundtableRoomLimitUpdateRequest(BaseModel):
|
||||||
@@ -474,6 +480,7 @@ class RoundtableRoomLimitUpdateRequest(BaseModel):
|
|||||||
max_rooms: int | None = Field(default=None, ge=1, le=20)
|
max_rooms: int | None = Field(default=None, ge=1, le=20)
|
||||||
max_hold_minutes: int | None = Field(default=None, ge=1, le=1440)
|
max_hold_minutes: int | None = Field(default=None, ge=1, le=1440)
|
||||||
max_cycles: int | None = Field(default=None, ge=2, le=100)
|
max_cycles: int | None = Field(default=None, ge=2, le=100)
|
||||||
|
write_file_compatibility_enabled: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class OrdinaryKnowledgeRetrievalResponse(BaseModel):
|
class OrdinaryKnowledgeRetrievalResponse(BaseModel):
|
||||||
@@ -556,6 +563,7 @@ def _conversation_context_response(settings: ConversationContextSettings) -> Con
|
|||||||
return ConversationContextResponse(
|
return ConversationContextResponse(
|
||||||
recent_turn_window_enabled=settings.recent_turn_window_enabled,
|
recent_turn_window_enabled=settings.recent_turn_window_enabled,
|
||||||
recent_turns=3,
|
recent_turns=3,
|
||||||
|
use_question_as_title=settings.use_question_as_title,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -573,13 +581,15 @@ async def update_conversation_context_setting(
|
|||||||
"""Enable or disable the fixed recent-three-turn window. Admin only."""
|
"""Enable or disable the fixed recent-three-turn window. Admin only."""
|
||||||
await _require_admin(request)
|
await _require_admin(request)
|
||||||
current = load_system_settings()
|
current = load_system_settings()
|
||||||
updated = current.conversation_context.model_copy(
|
updated_fields = body.model_dump(exclude_unset=True, exclude_none=True)
|
||||||
update={"recent_turn_window_enabled": body.recent_turn_window_enabled}
|
if not updated_fields:
|
||||||
)
|
raise HTTPException(status_code=422, detail="至少需要提供一个上下文设置项")
|
||||||
|
updated = current.conversation_context.model_copy(update=updated_fields)
|
||||||
saved = save_system_settings(current.model_copy(update={"conversation_context": updated}))
|
saved = save_system_settings(current.model_copy(update={"conversation_context": updated}))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Conversation recent-turn window changed: enabled=%s retained_turns=3",
|
"Conversation runtime settings changed: recent_turn_window=%s use_question_as_title=%s",
|
||||||
saved.conversation_context.recent_turn_window_enabled,
|
saved.conversation_context.recent_turn_window_enabled,
|
||||||
|
saved.conversation_context.use_question_as_title,
|
||||||
)
|
)
|
||||||
return _conversation_context_response(saved.conversation_context)
|
return _conversation_context_response(saved.conversation_context)
|
||||||
|
|
||||||
@@ -590,6 +600,7 @@ def _roundtable_room_limit_response(settings: RoundtableRoomLimitSettings) -> Ro
|
|||||||
max_rooms=settings.max_rooms,
|
max_rooms=settings.max_rooms,
|
||||||
max_hold_minutes=settings.max_hold_minutes,
|
max_hold_minutes=settings.max_hold_minutes,
|
||||||
max_cycles=settings.max_cycles,
|
max_cycles=settings.max_cycles,
|
||||||
|
write_file_compatibility_enabled=settings.write_file_compatibility_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -616,14 +627,17 @@ async def update_roundtable_room_limit_setting(
|
|||||||
updates["max_hold_minutes"] = body.max_hold_minutes
|
updates["max_hold_minutes"] = body.max_hold_minutes
|
||||||
if body.max_cycles is not None:
|
if body.max_cycles is not None:
|
||||||
updates["max_cycles"] = body.max_cycles
|
updates["max_cycles"] = body.max_cycles
|
||||||
|
if body.write_file_compatibility_enabled is not None:
|
||||||
|
updates["write_file_compatibility_enabled"] = body.write_file_compatibility_enabled
|
||||||
updated = current.roundtable_room_limit.model_copy(update=updates)
|
updated = current.roundtable_room_limit.model_copy(update=updates)
|
||||||
saved = save_system_settings(current.model_copy(update={"roundtable_room_limit": updated}))
|
saved = save_system_settings(current.model_copy(update={"roundtable_room_limit": updated}))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Roundtable runtime limits changed: enabled=%s max_rooms=%s max_hold_minutes=%s max_cycles=%s",
|
"Roundtable runtime limits changed: enabled=%s max_rooms=%s max_hold_minutes=%s max_cycles=%s write_file_compatibility=%s",
|
||||||
saved.roundtable_room_limit.enabled,
|
saved.roundtable_room_limit.enabled,
|
||||||
saved.roundtable_room_limit.max_rooms,
|
saved.roundtable_room_limit.max_rooms,
|
||||||
saved.roundtable_room_limit.max_hold_minutes,
|
saved.roundtable_room_limit.max_hold_minutes,
|
||||||
saved.roundtable_room_limit.max_cycles,
|
saved.roundtable_room_limit.max_cycles,
|
||||||
|
saved.roundtable_room_limit.write_file_compatibility_enabled,
|
||||||
)
|
)
|
||||||
return _roundtable_room_limit_response(saved.roundtable_room_limit)
|
return _roundtable_room_limit_response(saved.roundtable_room_limit)
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,8 @@ _CONTEXT_CONFIGURABLE_KEYS: frozenset[str] = frozenset(
|
|||||||
"roundtable_peer_deliveries",
|
"roundtable_peer_deliveries",
|
||||||
# 岗位会商席位 / 方案总结:每轮 outputs 下 write_file 上限(行动规划不传此键)。
|
# 岗位会商席位 / 方案总结:每轮 outputs 下 write_file 上限(行动规划不传此键)。
|
||||||
"position_artifact_cap",
|
"position_artifact_cap",
|
||||||
|
# 会商专用文件流式兼容。只由会商 run 写入;普通对话不带此键,仍只看全局开关。
|
||||||
|
"roundtable_write_file_compatibility",
|
||||||
# 上下文压缩开关:由聊天框下方的按钮控制(默认关闭),透传到 run config 后
|
# 上下文压缩开关:由聊天框下方的按钮控制(默认关闭),透传到 run config 后
|
||||||
# 在 _build_middlewares 里决定是否挂载 SummarizationMiddleware。
|
# 在 _build_middlewares 里决定是否挂载 SummarizationMiddleware。
|
||||||
"summarization_enabled",
|
"summarization_enabled",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ models:
|
|||||||
temperature: 1.0
|
temperature: 1.0
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_thinking: true
|
supports_thinking: true
|
||||||
|
write_file_transport: buffered_gateway
|
||||||
- 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
|
||||||
@@ -107,9 +108,6 @@ models:
|
|||||||
base_url: https://ai.hzfzlz.com/v1
|
base_url: https://ai.hzfzlz.com/v1
|
||||||
request_timeout: 600.0
|
request_timeout: 600.0
|
||||||
max_retries: 2
|
max_retries: 2
|
||||||
# 不带 max_tokens 时该中转站按自身小默认值截断输出(实测 ~64 token),
|
|
||||||
# 检索词 JSON 会被截断导致解析失败走兜底。
|
|
||||||
# write_file 写整页 HTML 时常超过 4096,截断后只剩 path,沙箱无法流式预览。
|
|
||||||
max_tokens: 32768
|
max_tokens: 32768
|
||||||
stream_usage: true
|
stream_usage: true
|
||||||
supports_thinking: false
|
supports_thinking: false
|
||||||
@@ -125,6 +123,7 @@ models:
|
|||||||
stream_usage: true
|
stream_usage: true
|
||||||
supports_thinking: false
|
supports_thinking: false
|
||||||
supports_vision: false
|
supports_vision: false
|
||||||
|
write_file_transport: native
|
||||||
- name: zai-org/GLM-5-FP8
|
- name: zai-org/GLM-5-FP8
|
||||||
display_name: zai-org/GLM-5-FP8
|
display_name: zai-org/GLM-5-FP8
|
||||||
use: langchain_openai:ChatOpenAI
|
use: langchain_openai:ChatOpenAI
|
||||||
@@ -195,6 +194,9 @@ models:
|
|||||||
temperature: 1.0
|
temperature: 1.0
|
||||||
supports_vision: false
|
supports_vision: false
|
||||||
supports_thinking: true
|
supports_thinking: true
|
||||||
|
# 不带 max_tokens 时该中转站按自身小默认值截断输出(实测 ~64 token),
|
||||||
|
# 检索词 JSON 会被截断导致解析失败走兜底。
|
||||||
|
# write_file 写整页 HTML 时常超过 4096,截断后只剩 path,沙箱无法流式预览。
|
||||||
# - name: deepseek-v4-flash
|
# - name: deepseek-v4-flash
|
||||||
# display_name: DeepSeek V4 Flash
|
# display_name: DeepSeek V4 Flash
|
||||||
# use: langchain_openai:ChatOpenAI
|
# use: langchain_openai:ChatOpenAI
|
||||||
|
|||||||
+8
-4
@@ -826,6 +826,8 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
# create_chat_model(force_disable_thinking=...) so enable_thinking=false is
|
# create_chat_model(force_disable_thinking=...) so enable_thinking=false is
|
||||||
# actually injected. Default off → no behavior change for normal chats.
|
# actually injected. Default off → no behavior change for normal chats.
|
||||||
thinking_force_disabled = bool(cfg.get("thinking_force_disabled", False))
|
thinking_force_disabled = bool(cfg.get("thinking_force_disabled", False))
|
||||||
|
# Roundtable runs opt in explicitly. Missing/false keeps the global switch.
|
||||||
|
write_file_compatibility = True if cfg.get("roundtable_write_file_compatibility") else None
|
||||||
reasoning_effort = cfg.get("reasoning_effort", None)
|
reasoning_effort = cfg.get("reasoning_effort", None)
|
||||||
requested_model_name: str | None = cfg.get("model_name") or cfg.get("model")
|
requested_model_name: str | None = cfg.get("model_name") or cfg.get("model")
|
||||||
is_plan_mode = cfg.get("is_plan_mode", False)
|
is_plan_mode = cfg.get("is_plan_mode", False)
|
||||||
@@ -1025,7 +1027,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
|
|
||||||
setup_tools = [setup_writing] if writing_setup_quick else [setup_writing, ask_clarification_tool]
|
setup_tools = [setup_writing] if writing_setup_quick else [setup_writing, ask_clarification_tool]
|
||||||
return create_agent(
|
return create_agent(
|
||||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
|
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config),
|
||||||
tools=setup_tools,
|
tools=setup_tools,
|
||||||
middleware=_build_setup_middlewares(
|
middleware=_build_setup_middlewares(
|
||||||
config,
|
config,
|
||||||
@@ -1062,7 +1064,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return create_agent(
|
return create_agent(
|
||||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
|
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config),
|
||||||
tools=setup_tools,
|
tools=setup_tools,
|
||||||
middleware=_build_setup_middlewares(
|
middleware=_build_setup_middlewares(
|
||||||
config,
|
config,
|
||||||
@@ -1087,7 +1089,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
if is_bootstrap:
|
if is_bootstrap:
|
||||||
# Special bootstrap agent with minimal prompt for initial custom agent creation flow
|
# Special bootstrap agent with minimal prompt for initial custom agent creation flow
|
||||||
return create_agent(
|
return create_agent(
|
||||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
|
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config),
|
||||||
tools=get_available_tools(
|
tools=get_available_tools(
|
||||||
include_mcp=not classic_knowledge_space_strict,
|
include_mcp=not classic_knowledge_space_strict,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
@@ -1097,6 +1099,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
allowed_tools=requested_allowed_tools,
|
allowed_tools=requested_allowed_tools,
|
||||||
allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS,
|
allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS,
|
||||||
app_config=resolved_app_config,
|
app_config=resolved_app_config,
|
||||||
|
write_file_compatibility=write_file_compatibility,
|
||||||
)
|
)
|
||||||
+ [setup_agent],
|
+ [setup_agent],
|
||||||
middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config),
|
middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config),
|
||||||
@@ -1130,6 +1133,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
allowed_tools=requested_allowed_tools,
|
allowed_tools=requested_allowed_tools,
|
||||||
allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS,
|
allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS,
|
||||||
app_config=resolved_app_config,
|
app_config=resolved_app_config,
|
||||||
|
write_file_compatibility=write_file_compatibility,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if agent_id == FORCED_RESEARCH_AGENT_ID:
|
if agent_id == FORCED_RESEARCH_AGENT_ID:
|
||||||
@@ -1243,7 +1247,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return create_agent(
|
return create_agent(
|
||||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
|
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config),
|
||||||
tools=default_tools,
|
tools=default_tools,
|
||||||
middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_id, app_config=resolved_app_config),
|
middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_id, app_config=resolved_app_config),
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
|
|||||||
+13
-6
@@ -12,6 +12,7 @@ from langchain.agents import AgentState
|
|||||||
from langchain.agents.middleware import AgentMiddleware
|
from langchain.agents.middleware import AgentMiddleware
|
||||||
from langgraph.runtime import Runtime
|
from langgraph.runtime import Runtime
|
||||||
|
|
||||||
|
from deerflow.config.system_settings import get_conversation_context_settings
|
||||||
from deerflow.config.title_config import get_title_config
|
from deerflow.config.title_config import get_title_config
|
||||||
from deerflow.models import create_chat_model
|
from deerflow.models import create_chat_model
|
||||||
from deerflow.utils.time import now_iso
|
from deerflow.utils.time import now_iso
|
||||||
@@ -209,14 +210,15 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]):
|
|||||||
return self._fallback_title(user_msg)
|
return self._fallback_title(user_msg)
|
||||||
|
|
||||||
def _should_set_provisional_title(self, state: TitleMiddlewareState) -> bool:
|
def _should_set_provisional_title(self, state: TitleMiddlewareState) -> bool:
|
||||||
"""Check if we should set an instant, question-derived placeholder title.
|
"""Check if we should set an instant, question-derived title.
|
||||||
|
|
||||||
Runs in ``before_model`` so the thread shows a sensible title the moment
|
Runs in ``before_model`` so the thread shows a sensible title the moment
|
||||||
the user sends their first message — eliminating the "untitled" window
|
the user sends their first message — eliminating the "untitled" window.
|
||||||
while the (possibly long) first answer streams. No LLM call, no latency.
|
When the administrator selects question-derived titles, this also works
|
||||||
|
when LLM title generation is disabled because no title model is needed.
|
||||||
"""
|
"""
|
||||||
config = self._get_title_config()
|
config = self._get_title_config()
|
||||||
if not config.enabled:
|
if not config.enabled and not get_conversation_context_settings().use_question_as_title:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Already has a title (provisional or final) — nothing to do.
|
# Already has a title (provisional or final) — nothing to do.
|
||||||
@@ -333,8 +335,13 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]):
|
|||||||
if not user_msg.strip():
|
if not user_msg.strip():
|
||||||
return None
|
return None
|
||||||
title = self._validated_fallback_title(state, user_msg)
|
title = self._validated_fallback_title(state, user_msg)
|
||||||
logger.debug("TitleMiddleware: set provisional title %r before first model call", title)
|
use_question_as_title = get_conversation_context_settings().use_question_as_title
|
||||||
return {"title": title, "title_provisional": True}
|
logger.debug(
|
||||||
|
"TitleMiddleware: set %s title %r before first model call",
|
||||||
|
"final question-derived" if use_question_as_title else "provisional question-derived",
|
||||||
|
title,
|
||||||
|
)
|
||||||
|
return {"title": title, "title_provisional": not use_question_as_title}
|
||||||
|
|
||||||
def _generate_title_result(self, state: TitleMiddlewareState) -> dict | None:
|
def _generate_title_result(self, state: TitleMiddlewareState) -> dict | None:
|
||||||
"""Generate a local fallback title without blocking on an LLM call."""
|
"""Generate a local fallback title without blocking on an LLM call."""
|
||||||
|
|||||||
+12
-25
@@ -576,6 +576,13 @@ class ConversationContextSettings(BaseModel):
|
|||||||
default=False,
|
default=False,
|
||||||
description="Whether model requests use the fixed recent-three-turn sliding window.",
|
description="Whether model requests use the fixed recent-three-turn sliding window.",
|
||||||
)
|
)
|
||||||
|
use_question_as_title: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description=(
|
||||||
|
"Whether the first user question is used as the final conversation title, "
|
||||||
|
"skipping post-answer LLM title generation."
|
||||||
|
),
|
||||||
|
)
|
||||||
model_config = ConfigDict(extra="ignore")
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
@@ -629,22 +636,12 @@ class RoundtableRoomLimitSettings(BaseModel):
|
|||||||
"The value is captured when an orchestration starts."
|
"The value is captured when an orchestration starts."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
model_config = ConfigDict(extra="ignore")
|
write_file_compatibility_enabled: bool = Field(
|
||||||
|
|
||||||
|
|
||||||
class ConversationContextSettings(BaseModel):
|
|
||||||
"""Deployment-wide short-context policy for chat model requests.
|
|
||||||
|
|
||||||
The recent-turn window is deliberately opt-in. When enabled, the lead
|
|
||||||
agent keeps the persisted conversation intact but sends only the latest
|
|
||||||
three real user turns (plus their assistant/tool continuations) to the
|
|
||||||
model. This avoids an extra summarization-model call while preserving the
|
|
||||||
complete transcript for the UI and checkpoint recovery.
|
|
||||||
"""
|
|
||||||
|
|
||||||
recent_turn_window_enabled: bool = Field(
|
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether model requests use the fixed recent-three-turn sliding window.",
|
description=(
|
||||||
|
"When true, roundtable leader/seat/step-3 runs use buffered-gateway "
|
||||||
|
"write_file streaming. Ordinary chat keeps the global switch."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
model_config = ConfigDict(extra="ignore")
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
@@ -714,7 +711,6 @@ class SystemSettings(BaseModel):
|
|||||||
write_file_compatibility: WriteFileCompatibilitySettings = Field(
|
write_file_compatibility: WriteFileCompatibilitySettings = Field(
|
||||||
default_factory=WriteFileCompatibilitySettings
|
default_factory=WriteFileCompatibilitySettings
|
||||||
)
|
)
|
||||||
conversation_context: ConversationContextSettings = Field(default_factory=ConversationContextSettings)
|
|
||||||
roundtable_room_limit: RoundtableRoomLimitSettings = Field(default_factory=RoundtableRoomLimitSettings)
|
roundtable_room_limit: RoundtableRoomLimitSettings = Field(default_factory=RoundtableRoomLimitSettings)
|
||||||
local_wiki_encoder: LocalWikiEncoderSettings = Field(default_factory=LocalWikiEncoderSettings)
|
local_wiki_encoder: LocalWikiEncoderSettings = Field(default_factory=LocalWikiEncoderSettings)
|
||||||
strategy_projects: StrategyProjectSettings = Field(default_factory=StrategyProjectSettings)
|
strategy_projects: StrategyProjectSettings = Field(default_factory=StrategyProjectSettings)
|
||||||
@@ -851,15 +847,6 @@ def get_roundtable_room_limit_settings() -> RoundtableRoomLimitSettings:
|
|||||||
return RoundtableRoomLimitSettings()
|
return RoundtableRoomLimitSettings()
|
||||||
|
|
||||||
|
|
||||||
def get_conversation_context_settings() -> ConversationContextSettings:
|
|
||||||
"""Return the admin-controlled recent-turn policy, failing closed."""
|
|
||||||
try:
|
|
||||||
return load_system_settings().conversation_context
|
|
||||||
except Exception:
|
|
||||||
logger.debug("Failed to read conversation context setting; keeping the recent-turn window disabled", exc_info=True)
|
|
||||||
return ConversationContextSettings()
|
|
||||||
|
|
||||||
|
|
||||||
def save_system_settings(settings: SystemSettings) -> SystemSettings:
|
def save_system_settings(settings: SystemSettings) -> SystemSettings:
|
||||||
"""Persist the settings document atomically, returning the saved value."""
|
"""Persist the settings document atomically, returning the saved value."""
|
||||||
global _settings_cache_key, _settings_cache_value
|
global _settings_cache_key, _settings_cache_value
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def _enable_stream_usage_by_default(model_use_path: str, model_settings_from_con
|
|||||||
model_settings_from_config["stream_usage"] = True
|
model_settings_from_config["stream_usage"] = True
|
||||||
|
|
||||||
|
|
||||||
def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *, app_config: AppConfig | None = None, force_disable_thinking: bool = False, **kwargs) -> BaseChatModel:
|
def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *, app_config: AppConfig | None = None, force_disable_thinking: bool = False, write_file_compatibility: bool | None = None, **kwargs) -> BaseChatModel:
|
||||||
"""Create a chat model instance from the config.
|
"""Create a chat model instance from the config.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -187,10 +187,17 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
configured_write_file_transport = model_config.write_file_transport
|
configured_write_file_transport = model_config.write_file_transport
|
||||||
runtime_write_file_compatibility = get_write_file_compatibility_settings()
|
# ``None`` follows the global switch, so a roundtable run with its own
|
||||||
|
# switch off still uses compatibility when the global switch is on.
|
||||||
|
# ``True`` is the roundtable opt-in used when the global switch is off.
|
||||||
|
compatibility_enabled = (
|
||||||
|
get_write_file_compatibility_settings().enabled
|
||||||
|
if write_file_compatibility is None
|
||||||
|
else write_file_compatibility
|
||||||
|
)
|
||||||
effective_write_file_transport = (
|
effective_write_file_transport = (
|
||||||
WriteFileTransport.BUFFERED_GATEWAY
|
WriteFileTransport.BUFFERED_GATEWAY
|
||||||
if runtime_write_file_compatibility.enabled
|
if compatibility_enabled
|
||||||
else configured_write_file_transport
|
else configured_write_file_transport
|
||||||
)
|
)
|
||||||
if model_config.tool_call_transport.value != "native":
|
if model_config.tool_call_transport.value != "native":
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ class SubagentExecutor:
|
|||||||
thread_data: ThreadDataState | None = None,
|
thread_data: ThreadDataState | None = None,
|
||||||
thread_id: str | None = None,
|
thread_id: str | None = None,
|
||||||
trace_id: str | None = None,
|
trace_id: str | None = None,
|
||||||
|
*,
|
||||||
|
write_file_compatibility: bool | None = None,
|
||||||
):
|
):
|
||||||
"""Initialize the executor.
|
"""Initialize the executor.
|
||||||
|
|
||||||
@@ -317,6 +319,7 @@ class SubagentExecutor:
|
|||||||
self.sandbox_state = sandbox_state
|
self.sandbox_state = sandbox_state
|
||||||
self.thread_data = thread_data
|
self.thread_data = thread_data
|
||||||
self.thread_id = thread_id
|
self.thread_id = thread_id
|
||||||
|
self.write_file_compatibility = True if write_file_compatibility else None
|
||||||
# Generate trace_id if not provided (for top-level calls)
|
# Generate trace_id if not provided (for top-level calls)
|
||||||
self.trace_id = trace_id or str(uuid.uuid4())[:8]
|
self.trace_id = trace_id or str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
@@ -334,7 +337,12 @@ class SubagentExecutor:
|
|||||||
app_config = self.app_config or get_app_config()
|
app_config = self.app_config or get_app_config()
|
||||||
if self.model_name is None:
|
if self.model_name is None:
|
||||||
self.model_name = resolve_subagent_model_name(self.config, self.parent_model, app_config=app_config)
|
self.model_name = resolve_subagent_model_name(self.config, self.parent_model, app_config=app_config)
|
||||||
model = create_chat_model(name=self.model_name, thinking_enabled=False, app_config=app_config)
|
model = create_chat_model(
|
||||||
|
name=self.model_name,
|
||||||
|
thinking_enabled=False,
|
||||||
|
app_config=app_config,
|
||||||
|
write_file_compatibility=self.write_file_compatibility,
|
||||||
|
)
|
||||||
|
|
||||||
from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares
|
from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ async def task_tool(
|
|||||||
effective_model = resolve_subagent_model_name(config, parent_model, app_config=resolved_app_config)
|
effective_model = resolve_subagent_model_name(config, parent_model, app_config=resolved_app_config)
|
||||||
|
|
||||||
# Subagents should not have subagent tools enabled (prevent recursive nesting)
|
# Subagents should not have subagent tools enabled (prevent recursive nesting)
|
||||||
|
parent_context = runtime.context if runtime is not None and isinstance(getattr(runtime, "context", None), dict) else {}
|
||||||
|
roundtable_file_stream = bool(parent_context.get("roundtable_write_file_compatibility"))
|
||||||
available_tools_kwargs = {
|
available_tools_kwargs = {
|
||||||
"model_name": effective_model,
|
"model_name": effective_model,
|
||||||
"groups": parent_tool_groups,
|
"groups": parent_tool_groups,
|
||||||
@@ -164,6 +166,8 @@ async def task_tool(
|
|||||||
}
|
}
|
||||||
if resolved_app_config is not None:
|
if resolved_app_config is not None:
|
||||||
available_tools_kwargs["app_config"] = resolved_app_config
|
available_tools_kwargs["app_config"] = resolved_app_config
|
||||||
|
if roundtable_file_stream:
|
||||||
|
available_tools_kwargs["write_file_compatibility"] = True
|
||||||
tools = get_available_tools(**available_tools_kwargs)
|
tools = get_available_tools(**available_tools_kwargs)
|
||||||
|
|
||||||
# Create executor
|
# Create executor
|
||||||
@@ -178,6 +182,8 @@ async def task_tool(
|
|||||||
}
|
}
|
||||||
if resolved_app_config is not None:
|
if resolved_app_config is not None:
|
||||||
executor_kwargs["app_config"] = resolved_app_config
|
executor_kwargs["app_config"] = resolved_app_config
|
||||||
|
if roundtable_file_stream:
|
||||||
|
executor_kwargs["write_file_compatibility"] = True
|
||||||
executor = SubagentExecutor(**executor_kwargs)
|
executor = SubagentExecutor(**executor_kwargs)
|
||||||
|
|
||||||
# Start background execution (always async to prevent blocking)
|
# Start background execution (always async to prevent blocking)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ def get_available_tools(
|
|||||||
allowed_tools: list[str] | None = None,
|
allowed_tools: list[str] | None = None,
|
||||||
allow_agent_orchestration: bool = False,
|
allow_agent_orchestration: bool = False,
|
||||||
app_config: AppConfig | None = None,
|
app_config: AppConfig | None = None,
|
||||||
|
write_file_compatibility: bool | None = None,
|
||||||
) -> list[BaseTool]:
|
) -> list[BaseTool]:
|
||||||
"""Get all available tools from config.
|
"""Get all available tools from config.
|
||||||
|
|
||||||
@@ -105,6 +106,8 @@ def get_available_tools(
|
|||||||
# requests entirely inside PatchedChatOpenAI.
|
# requests entirely inside PatchedChatOpenAI.
|
||||||
global_write_file_compatibility_enabled = (
|
global_write_file_compatibility_enabled = (
|
||||||
get_write_file_compatibility_settings().enabled
|
get_write_file_compatibility_settings().enabled
|
||||||
|
if write_file_compatibility is None
|
||||||
|
else write_file_compatibility
|
||||||
)
|
)
|
||||||
write_renderer_enabled = (
|
write_renderer_enabled = (
|
||||||
not global_write_file_compatibility_enabled
|
not global_write_file_compatibility_enabled
|
||||||
|
|||||||
+31
@@ -4543,6 +4543,37 @@ def test_factory_global_switch_overrides_every_compatible_model(
|
|||||||
assert second.write_file_transport == "buffered_gateway"
|
assert second.write_file_transport == "buffered_gateway"
|
||||||
|
|
||||||
|
|
||||||
|
def test_roundtable_override_enables_buffered_gateway_while_global_is_off(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
model_factory,
|
||||||
|
"get_write_file_compatibility_settings",
|
||||||
|
lambda: SimpleNamespace(enabled=False),
|
||||||
|
)
|
||||||
|
config = AppConfig.model_construct(
|
||||||
|
models=[
|
||||||
|
ModelConfig(
|
||||||
|
name="native-model",
|
||||||
|
use="langchain_openai:ChatOpenAI",
|
||||||
|
model="native-model",
|
||||||
|
api_key="test-key",
|
||||||
|
base_url=None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
ordinary = model_factory.create_chat_model("native-model", app_config=config)
|
||||||
|
roundtable = model_factory.create_chat_model(
|
||||||
|
"native-model",
|
||||||
|
app_config=config,
|
||||||
|
write_file_compatibility=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ordinary.write_file_transport == "native"
|
||||||
|
assert roundtable.write_file_transport == "buffered_gateway"
|
||||||
|
|
||||||
|
|
||||||
def test_factory_rejects_bridge_for_non_patched_provider(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_factory_rejects_bridge_for_non_patched_provider(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
config = AppConfig.model_construct(
|
config = AppConfig.model_construct(
|
||||||
models=[
|
models=[
|
||||||
|
|||||||
@@ -215,6 +215,121 @@ def test_reasoning_capability_route_precedes_generic_model_update_and_accepts_sl
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_file_transport_route_precedes_generic_model_update() -> None:
|
||||||
|
scope = {
|
||||||
|
"type": "http",
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/api/admin/models/write-file-transport/zai-org/GLM-5-FP8",
|
||||||
|
}
|
||||||
|
matching_paths = [route.path for route in router.router.routes if route.matches(scope)[0] is Match.FULL]
|
||||||
|
|
||||||
|
assert matching_paths[:2] == [
|
||||||
|
"/api/admin/models/write-file-transport/{model_name:path}",
|
||||||
|
"/api/admin/models/{model_name:path}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_request():
|
||||||
|
return SimpleNamespace(
|
||||||
|
state=SimpleNamespace(user=SimpleNamespace(system_role="admin")),
|
||||||
|
app=SimpleNamespace(state=SimpleNamespace()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_file_transport_toggle_persists_native_and_buffered_gateway(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
config_path = tmp_path / "config.yaml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""models:
|
||||||
|
- name: flash
|
||||||
|
use: langchain_openai:ChatOpenAI
|
||||||
|
model: flash
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
saved: dict[str, object] = {}
|
||||||
|
|
||||||
|
async def save_models(_request, raw_before: str, models: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||||
|
saved["raw_before"] = raw_before
|
||||||
|
saved["models"] = models
|
||||||
|
return models
|
||||||
|
|
||||||
|
monkeypatch.setattr(router, "_resolve_config_path", lambda: config_path)
|
||||||
|
monkeypatch.setattr(router, "_save_models", save_models)
|
||||||
|
|
||||||
|
enabled = await router.update_write_file_transport(
|
||||||
|
"flash",
|
||||||
|
router.WriteFileTransportToggleRequest(write_file_transport="buffered_gateway"),
|
||||||
|
_admin_request(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert enabled.write_file_transport == "buffered_gateway"
|
||||||
|
assert saved["models"] == [
|
||||||
|
{
|
||||||
|
"name": "flash",
|
||||||
|
"use": "langchain_openai:ChatOpenAI",
|
||||||
|
"model": "flash",
|
||||||
|
"write_file_transport": "buffered_gateway",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
disabled = await router.update_write_file_transport(
|
||||||
|
"flash",
|
||||||
|
router.WriteFileTransportToggleRequest(write_file_transport="native"),
|
||||||
|
_admin_request(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert disabled.write_file_transport == "native"
|
||||||
|
assert saved["models"][0]["write_file_transport"] == "native"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("use", "use_responses_api"),
|
||||||
|
[
|
||||||
|
("langchain_anthropic:ChatAnthropic", None),
|
||||||
|
("langchain_openai:ChatOpenAI", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_write_file_transport_rejects_incompatible_models(
|
||||||
|
use: str,
|
||||||
|
use_responses_api: bool | None,
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
config_path = tmp_path / "config.yaml"
|
||||||
|
original = f"""models:
|
||||||
|
- name: incompatible
|
||||||
|
use: {use}
|
||||||
|
model: incompatible
|
||||||
|
"""
|
||||||
|
if use_responses_api:
|
||||||
|
original = original.replace(
|
||||||
|
" model: incompatible\n",
|
||||||
|
" model: incompatible\n use_responses_api: true\n",
|
||||||
|
)
|
||||||
|
config_path.write_text(original, encoding="utf-8")
|
||||||
|
monkeypatch.setattr(router, "_resolve_config_path", lambda: config_path)
|
||||||
|
|
||||||
|
async def fail_save_models(_request, _raw_before, _models):
|
||||||
|
raise AssertionError("incompatible models must not be persisted")
|
||||||
|
|
||||||
|
monkeypatch.setattr(router, "_save_models", fail_save_models)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await router.update_write_file_transport(
|
||||||
|
"incompatible",
|
||||||
|
router.WriteFileTransportToggleRequest(write_file_transport="buffered_gateway"),
|
||||||
|
_admin_request(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 422
|
||||||
|
assert config_path.read_text(encoding="utf-8") == original
|
||||||
|
|
||||||
|
|
||||||
def _reasoning_request(store, *, config: AppConfig | None = None):
|
def _reasoning_request(store, *, config: AppConfig | None = None):
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
state=SimpleNamespace(
|
state=SimpleNamespace(
|
||||||
|
|||||||
@@ -745,6 +745,7 @@ async def test_admin_can_change_roundtable_room_limit_without_restart(
|
|||||||
assert ss.get_roundtable_room_limit_settings().max_rooms == 3
|
assert ss.get_roundtable_room_limit_settings().max_rooms == 3
|
||||||
assert ss.get_roundtable_room_limit_settings().max_hold_minutes == 30
|
assert ss.get_roundtable_room_limit_settings().max_hold_minutes == 30
|
||||||
assert ss.get_roundtable_room_limit_settings().max_cycles == 8
|
assert ss.get_roundtable_room_limit_settings().max_cycles == 8
|
||||||
|
assert ss.get_roundtable_room_limit_settings().write_file_compatibility_enabled is False
|
||||||
|
|
||||||
saved = await ss_router.update_roundtable_room_limit_setting(
|
saved = await ss_router.update_roundtable_room_limit_setting(
|
||||||
ss_router.RoundtableRoomLimitUpdateRequest(
|
ss_router.RoundtableRoomLimitUpdateRequest(
|
||||||
@@ -762,6 +763,36 @@ async def test_admin_can_change_roundtable_room_limit_without_restart(
|
|||||||
assert ss.load_system_settings().roundtable_room_limit.max_rooms == 5
|
assert ss.load_system_settings().roundtable_room_limit.max_rooms == 5
|
||||||
assert ss.load_system_settings().roundtable_room_limit.max_hold_minutes == 45
|
assert ss.load_system_settings().roundtable_room_limit.max_hold_minutes == 45
|
||||||
assert ss.load_system_settings().roundtable_room_limit.max_cycles == 12
|
assert ss.load_system_settings().roundtable_room_limit.max_cycles == 12
|
||||||
|
assert saved.write_file_compatibility_enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_roundtable_file_stream_is_only_attached_to_roundtable_runs(
|
||||||
|
tmp_runtime_home: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_patch_admin(monkeypatch)
|
||||||
|
base = dict(
|
||||||
|
agent_name="roundtable-seat",
|
||||||
|
model_name="test-model",
|
||||||
|
thread_id="thread-1",
|
||||||
|
new_message="test",
|
||||||
|
excluded_tools=[],
|
||||||
|
skill_stop_names=[],
|
||||||
|
)
|
||||||
|
before = _run_payload(**base, roundtable_file_stream=True)
|
||||||
|
assert "roundtable_write_file_compatibility" not in before["context"]
|
||||||
|
|
||||||
|
saved = await ss_router.update_roundtable_room_limit_setting(
|
||||||
|
ss_router.RoundtableRoomLimitUpdateRequest(write_file_compatibility_enabled=True),
|
||||||
|
SimpleNamespace(),
|
||||||
|
)
|
||||||
|
assert saved.write_file_compatibility_enabled is True
|
||||||
|
|
||||||
|
opted_in = _run_payload(**base, roundtable_file_stream=True)
|
||||||
|
assert opted_in["context"]["roundtable_write_file_compatibility"] is True
|
||||||
|
ordinary = _run_payload(**base)
|
||||||
|
assert "roundtable_write_file_compatibility" not in ordinary["context"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -114,6 +114,46 @@ def _title_config() -> TitleConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_question_title_switch_finalizes_question_before_first_model_call(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
title_middleware_module,
|
||||||
|
"get_conversation_context_settings",
|
||||||
|
lambda: SimpleNamespace(use_question_as_title=True),
|
||||||
|
)
|
||||||
|
middleware = TitleMiddleware(
|
||||||
|
title_config=_title_config().model_copy(update={"enabled": False})
|
||||||
|
)
|
||||||
|
state = {
|
||||||
|
"messages": [SimpleNamespace(type="human", content="请帮我分析这份合同")],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = middleware.before_model(state, runtime=SimpleNamespace())
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"title": "请帮我分析这份合同",
|
||||||
|
"title_provisional": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_question_title_switch_off_keeps_async_title_generation(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
title_middleware_module,
|
||||||
|
"get_conversation_context_settings",
|
||||||
|
lambda: SimpleNamespace(use_question_as_title=False),
|
||||||
|
)
|
||||||
|
middleware = TitleMiddleware(title_config=_title_config())
|
||||||
|
state = {
|
||||||
|
"messages": [SimpleNamespace(type="human", content="请帮我分析这份合同")],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = middleware.before_model(state, runtime=SimpleNamespace())
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"title": "请帮我分析这份合同",
|
||||||
|
"title_provisional": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_finalize_thread_title_updates_checkpoint_and_display_name(monkeypatch) -> None:
|
async def test_finalize_thread_title_updates_checkpoint_and_display_name(monkeypatch) -> None:
|
||||||
async def _fake_generate(self, state):
|
async def _fake_generate(self, state):
|
||||||
|
|||||||
@@ -493,4 +493,3 @@ task_deeplink:
|
|||||||
taskDirection: "正向引导 + 风险预警"
|
taskDirection: "正向引导 + 风险预警"
|
||||||
taskName: "任务01:暑期文旅舆情综合研判"
|
taskName: "任务01:暑期文旅舆情综合研判"
|
||||||
taskContent: "围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。"
|
taskContent: "围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。"
|
||||||
taskType: 1
|
|
||||||
Reference in new issue
Block a user