智能体对话选 Agent 直达、澄清问题批量提交、LLM 错误中文化等
- 前端:新会话选 Agent 后直接跳转并自动发送,智能体页输入框宽度与首页对齐 - 前端:澄清卡片支持多选+多问题合并提交,消除单题逐一发送的割裂感 - 前端:定时任务弹窗 Agent 下拉支持长描述截断,避免撑破布局 - 后端:ask_clarification 支持同回合多次调用以提多个独立问题 - 后端:LLM 错误处理与定时任务失败提示统一改为中文 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c74be2b789
commit
ba2bc46042
7 files changed
+327
-73
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { CheckIcon, SendIcon, XIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
Conversation,
|
||||
@@ -64,6 +64,111 @@ export function MessageList({
|
||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||
const updateSubtask = useUpdateSubtask();
|
||||
const messages = thread.messages;
|
||||
|
||||
// Track which clarification messages are still awaiting an answer.
|
||||
// Pending = clarification tool messages that appear after the last human message
|
||||
// (no user reply has been sent for them yet).
|
||||
const pendingClarificationIds = useMemo(() => {
|
||||
let lastHumanIndex = -1;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i]?.type === "human") {
|
||||
lastHumanIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const ids: string[] = [];
|
||||
for (let i = lastHumanIndex + 1; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (msg && msg.id && msg.type === "tool" && msg.name === "ask_clarification") {
|
||||
ids.push(msg.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}, [messages]);
|
||||
|
||||
const [clarificationSelections, setClarificationSelections] = useState<
|
||||
Map<string, ClarificationSelection>
|
||||
>(new Map());
|
||||
const [submittingClarification, setSubmittingClarification] = useState(false);
|
||||
|
||||
const handleClarificationSelectionChange = useCallback(
|
||||
(id: string, next: ClarificationSelection) => {
|
||||
setClarificationSelections((prev) => {
|
||||
const map = new Map(prev);
|
||||
map.set(id, next);
|
||||
return map;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmitAllClarifications = useCallback(async () => {
|
||||
if (pendingClarificationIds.length === 0 || !onClarificationSubmit) {
|
||||
return;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
for (let index = 0; index < pendingClarificationIds.length; index++) {
|
||||
const id = pendingClarificationIds[index]!;
|
||||
const message = messages.find((msg) => msg.id === id);
|
||||
const payload = message ? getClarificationPayload(message) : null;
|
||||
const selection = clarificationSelections.get(id) ?? {
|
||||
selectedIds: [],
|
||||
custom: "",
|
||||
};
|
||||
const options = (payload?.options ?? []).filter(
|
||||
(option) => typeof option.label === "string" && option.label.trim(),
|
||||
);
|
||||
const selectedLabels = selection.selectedIds
|
||||
.map(
|
||||
(sid) =>
|
||||
options.find((option) => (option.id || option.label) === sid)?.label,
|
||||
)
|
||||
.filter((label): label is string => Boolean(label && label.trim()));
|
||||
const trimmedCustom = selection.custom.trim();
|
||||
let part = "";
|
||||
if (selectedLabels.length > 0 && trimmedCustom) {
|
||||
part = `我选择:${selectedLabels.join("、")};补充:${trimmedCustom}`;
|
||||
} else if (selectedLabels.length > 0) {
|
||||
part = `我选择:${selectedLabels.join("、")}`;
|
||||
} else if (trimmedCustom) {
|
||||
part = trimmedCustom;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (pendingClarificationIds.length > 1) {
|
||||
const question = payload?.question?.trim();
|
||||
parts.push(
|
||||
question
|
||||
? `问题${index + 1}「${question}」:${part}`
|
||||
: `问题${index + 1}:${part}`,
|
||||
);
|
||||
} else {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
return;
|
||||
}
|
||||
const combined =
|
||||
pendingClarificationIds.length > 1
|
||||
? `针对你提出的多个问题,依次回答:\n${parts.map((p) => `- ${p}`).join("\n")}`
|
||||
: parts[0]!;
|
||||
setSubmittingClarification(true);
|
||||
try {
|
||||
await onClarificationSubmit(combined);
|
||||
} finally {
|
||||
setSubmittingClarification(false);
|
||||
}
|
||||
}, [pendingClarificationIds, messages, clarificationSelections, onClarificationSubmit]);
|
||||
|
||||
const totalPending = pendingClarificationIds.length;
|
||||
const hasAnyClarificationAnswer = useMemo(() => {
|
||||
return pendingClarificationIds.some((id) => {
|
||||
const sel = clarificationSelections.get(id);
|
||||
return (sel?.selectedIds.length ?? 0) > 0 || (sel?.custom.trim() ?? "").length > 0;
|
||||
});
|
||||
}, [pendingClarificationIds, clarificationSelections]);
|
||||
|
||||
if (thread.isThreadLoading && messages.length === 0) {
|
||||
return <MessageListSkeleton />;
|
||||
}
|
||||
@@ -89,14 +194,34 @@ export function MessageList({
|
||||
} else if (group.type === "assistant:clarification") {
|
||||
const message = group.messages[0];
|
||||
if (message && hasContent(message)) {
|
||||
const messageId = message.id ?? "";
|
||||
const pendingIndex = pendingClarificationIds.indexOf(messageId);
|
||||
const isPending = pendingIndex >= 0;
|
||||
const isLastPending =
|
||||
isPending && pendingIndex === totalPending - 1;
|
||||
return (
|
||||
<div key={group.id} className="w-full">
|
||||
<ClarificationCard
|
||||
fallbackContent={extractContentFromMessage(message)}
|
||||
isLoading={thread.isLoading}
|
||||
message={message}
|
||||
onSubmit={onClarificationSubmit}
|
||||
rehypePlugins={rehypePlugins}
|
||||
isPending={isPending}
|
||||
pendingIndex={isPending ? pendingIndex : undefined}
|
||||
pendingCount={isPending ? totalPending : undefined}
|
||||
isLastPending={isLastPending}
|
||||
selection={
|
||||
clarificationSelections.get(messageId) ?? {
|
||||
selectedIds: [],
|
||||
custom: "",
|
||||
}
|
||||
}
|
||||
onSelectionChange={(next) =>
|
||||
handleClarificationSelectionChange(messageId, next)
|
||||
}
|
||||
submitting={submittingClarification}
|
||||
canSubmit={hasAnyClarificationAnswer}
|
||||
onSubmitAll={handleSubmitAllClarifications}
|
||||
/>
|
||||
<MessageTokenUsageList
|
||||
enabled={tokenUsageEnabled}
|
||||
@@ -289,6 +414,11 @@ type ClarificationPayload = {
|
||||
allow_custom?: boolean;
|
||||
};
|
||||
|
||||
type ClarificationSelection = {
|
||||
selectedIds: string[];
|
||||
custom: string;
|
||||
};
|
||||
|
||||
export type SetupAgentApprovalPayload = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -447,19 +577,32 @@ function ClarificationCard({
|
||||
fallbackContent,
|
||||
isLoading,
|
||||
message,
|
||||
onSubmit,
|
||||
rehypePlugins,
|
||||
isPending,
|
||||
pendingIndex,
|
||||
pendingCount,
|
||||
isLastPending,
|
||||
selection,
|
||||
onSelectionChange,
|
||||
submitting,
|
||||
canSubmit,
|
||||
onSubmitAll,
|
||||
}: {
|
||||
fallbackContent: string;
|
||||
isLoading: boolean;
|
||||
message: unknown;
|
||||
onSubmit?: (answer: string) => void | Promise<void>;
|
||||
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||
isPending: boolean;
|
||||
pendingIndex?: number;
|
||||
pendingCount?: number;
|
||||
isLastPending: boolean;
|
||||
selection: ClarificationSelection;
|
||||
onSelectionChange: (next: ClarificationSelection) => void;
|
||||
submitting: boolean;
|
||||
canSubmit: boolean;
|
||||
onSubmitAll: () => void | Promise<void>;
|
||||
}) {
|
||||
const payload = getClarificationPayload(message);
|
||||
const [customAnswer, setCustomAnswer] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
@@ -479,27 +622,36 @@ function ClarificationCard({
|
||||
);
|
||||
}
|
||||
|
||||
const disabled = false;
|
||||
const disabled = !isPending || submitting;
|
||||
const multiPending = (pendingCount ?? 0) > 1;
|
||||
|
||||
async function submitAnswer(answer: string) {
|
||||
const trimmed = answer.trim();
|
||||
if (!trimmed) {
|
||||
function toggleOption(optionId: string) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit?.(trimmed);
|
||||
setSubmitted(true);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
const next = selection.selectedIds.includes(optionId)
|
||||
? selection.selectedIds.filter((id) => id !== optionId)
|
||||
: [...selection.selectedIds, optionId];
|
||||
onSelectionChange({ ...selection, selectedIds: next });
|
||||
}
|
||||
|
||||
function updateCustom(value: string) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onSelectionChange({ ...selection, custom: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-border bg-card text-card-foreground w-full rounded-lg border p-4 shadow-sm">
|
||||
<div className="space-y-2">
|
||||
<div className="text-muted-foreground text-xs font-medium">
|
||||
需要你的确认
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs font-medium">
|
||||
<span>需要你的确认</span>
|
||||
{multiPending && pendingIndex !== undefined && pendingCount ? (
|
||||
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-[10px]">
|
||||
{pendingIndex + 1} / {pendingCount}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{payload.context ? (
|
||||
<p className="text-muted-foreground text-sm leading-6">
|
||||
@@ -513,49 +665,84 @@ function ClarificationCard({
|
||||
|
||||
{options.length > 0 ? (
|
||||
<div className="mt-4 grid gap-2">
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.id || option.label}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-auto justify-start whitespace-normal px-3 py-2 text-left"
|
||||
disabled={disabled}
|
||||
onClick={() => void submitAnswer(`我选择:${option.label}`)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-col items-start gap-1">
|
||||
<span>{option.label}</span>
|
||||
{option.description ? (
|
||||
<span className="text-muted-foreground text-xs font-normal leading-5">
|
||||
{option.description}
|
||||
</span>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
可多选(再次点击取消选择)
|
||||
</div>
|
||||
{options.map((option) => {
|
||||
const optionId = option.id || option.label;
|
||||
const isSelected = selection.selectedIds.includes(optionId);
|
||||
return (
|
||||
<Button
|
||||
key={optionId}
|
||||
type="button"
|
||||
variant={isSelected ? "default" : "outline"}
|
||||
className="h-auto justify-start whitespace-normal px-3 py-2 text-left"
|
||||
disabled={disabled}
|
||||
onClick={() => toggleOption(optionId)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col items-start gap-1">
|
||||
<span>{option.label}</span>
|
||||
{option.description ? (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-normal leading-5",
|
||||
isSelected
|
||||
? "text-primary-foreground/80"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{isSelected ? (
|
||||
<CheckIcon className="ml-2 h-4 w-4 shrink-0" />
|
||||
) : null}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{payload.allow_custom !== false ? (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Textarea
|
||||
value={customAnswer}
|
||||
onChange={(event) => setCustomAnswer(event.currentTarget.value)}
|
||||
placeholder="如果以上选项都不合适,可以在这里输入你的回答"
|
||||
value={selection.custom}
|
||||
onChange={(event) => updateCustom(event.currentTarget.value)}
|
||||
placeholder={
|
||||
options.length > 0
|
||||
? "可选:补充说明(与所选项一起发送),或在此填写自定义回答"
|
||||
: "如果以上选项都不合适,可以在这里输入你的回答"
|
||||
}
|
||||
disabled={disabled}
|
||||
className="min-h-20 resize-none"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPending ? (
|
||||
isLastPending ? (
|
||||
<div className="mt-3 flex items-center justify-end gap-2">
|
||||
{multiPending ? (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
将一起提交以上 {pendingCount} 个问题的回答
|
||||
</span>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={disabled || !customAnswer.trim()}
|
||||
onClick={() => void submitAnswer(customAnswer)}
|
||||
disabled={!canSubmit || submitting}
|
||||
onClick={() => void onSubmitAll()}
|
||||
>
|
||||
<SendIcon className="h-4 w-4" />
|
||||
发送回答
|
||||
{submitting ? "发送中…" : multiPending ? "全部发送" : "发送回答"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground mt-3 text-right text-xs">
|
||||
选择后等待最后一题统一提交
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { BotIcon, PlusSquare } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -36,13 +36,30 @@ export default function AgentChatPage() {
|
||||
const routeBase = useRouteBase();
|
||||
const { t } = useI18n();
|
||||
const [showFollowups, setShowFollowups] = useState(false);
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const { agent_id } = useParams<{
|
||||
agent_id: string;
|
||||
}>();
|
||||
|
||||
// If ChatPage stashed an opening message for this agent, skip the landing
|
||||
// screen on first render so the user doesn't see a flash of the welcome UI
|
||||
// before the auto-sent message takes over.
|
||||
const hasPendingForThisAgent = () => {
|
||||
if (typeof window === "undefined" || !agent_id) return false;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("pendingAgentMessage");
|
||||
if (!raw) return false;
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed?.agentId === agent_id && !!parsed?.text;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(
|
||||
hasPendingForThisAgent,
|
||||
);
|
||||
|
||||
const { agent } = useAgent(agent_id);
|
||||
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread } =
|
||||
@@ -52,9 +69,10 @@ export default function AgentChatPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewThread) {
|
||||
setHasStartedConversation(false);
|
||||
setHasStartedConversation(hasPendingForThisAgent());
|
||||
}
|
||||
}, [isNewThread, threadId]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNewThread, threadId, agent_id]);
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
@@ -100,6 +118,26 @@ export default function AgentChatPage() {
|
||||
[sendMessage, threadId, agent_id],
|
||||
);
|
||||
|
||||
// ChatPage stashes the opening message in sessionStorage when the user picks
|
||||
// an agent on the landing screen, then redirects here. Replay it once so the
|
||||
// user doesn't have to re-type the question on the agent chat page.
|
||||
const pendingMessageHandled = useRef(false);
|
||||
useEffect(() => {
|
||||
if (pendingMessageHandled.current) return;
|
||||
if (!agent_id || !isNewThread || hasStartedConversation) return;
|
||||
let pending: { agentId?: string; text?: string } | null = null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("pendingAgentMessage");
|
||||
if (raw) pending = JSON.parse(raw);
|
||||
} catch {
|
||||
pending = null;
|
||||
}
|
||||
if (!pending || pending.agentId !== agent_id || !pending.text) return;
|
||||
pendingMessageHandled.current = true;
|
||||
sessionStorage.removeItem("pendingAgentMessage");
|
||||
handleSubmit({ text: pending.text, files: [] });
|
||||
}, [agent_id, isNewThread, hasStartedConversation, handleSubmit]);
|
||||
|
||||
const handleClarificationSubmit = useCallback(
|
||||
(answer: string) => {
|
||||
setHasStartedConversation(true);
|
||||
@@ -180,8 +218,8 @@ export default function AgentChatPage() {
|
||||
"relative w-full",
|
||||
isLandingState && "-translate-y-[calc(50vh-96px)]",
|
||||
isLandingState
|
||||
? "max-w-(--container-width-sm)"
|
||||
: "max-w-(--container-width-md)",
|
||||
? "max-w-(--container-width-md)"
|
||||
: "max-w-(--container-width-lg)",
|
||||
)}
|
||||
>
|
||||
<div className="absolute -top-4 right-0 left-0 z-0">
|
||||
|
||||
@@ -758,11 +758,11 @@ export default function ScheduledTasksPage() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={creating || !!editing} onOpenChange={(open) => !open && closeDialog()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="max-w-2xl overflow-hidden">
|
||||
<DialogHeader className="min-w-0">
|
||||
<DialogTitle>{creating ? "新建定时任务" : "编辑定时任务"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(event) =>
|
||||
@@ -798,15 +798,20 @@ export default function ScheduledTasksPage() {
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate">
|
||||
<SelectValue placeholder="选择执行智能体" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent
|
||||
position="popper"
|
||||
className="w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
|
||||
>
|
||||
<SelectItem value="__default__">默认助手(不指定)</SelectItem>
|
||||
{agents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
<span className="block truncate">
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
+11
@@ -464,11 +464,22 @@ You are {agent_name}, an internal super agent.
|
||||
- ❌ DO NOT skip clarification for "efficiency" - accuracy matters more than speed
|
||||
- ❌ DO NOT make assumptions when information is missing - ALWAYS ask
|
||||
- ❌ DO NOT proceed with guesses - STOP and call ask_clarification first
|
||||
- ❌ DO NOT cram several unrelated questions into one `question` string — split them into separate tool calls
|
||||
- ✅ Analyze the request in thinking → Identify unclear aspects → Ask BEFORE any action
|
||||
- ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY
|
||||
- ✅ After calling ask_clarification, execution will be interrupted automatically
|
||||
- ✅ Wait for user response - do NOT continue with assumptions
|
||||
|
||||
**Batching multiple independent questions:**
|
||||
- When the request leaves **several independent aspects** unclear (e.g. project type AND tech stack
|
||||
AND deployment target), call `ask_clarification` multiple times in the same turn — one call per
|
||||
aspect, each with its own focused `question` and `options`.
|
||||
- The frontend groups all clarification cards from one turn into a single submission, so the user
|
||||
answers them together in one reply. You will then receive ONE combined user message addressing
|
||||
every question in order — read it carefully and match answers back to the questions you asked.
|
||||
- If the next question only makes sense after the previous answer (sequential dependency), ask
|
||||
only the first one this turn and wait for the reply before asking the next.
|
||||
|
||||
**How to Use:**
|
||||
```python
|
||||
ask_clarification(
|
||||
|
||||
+7
-7
@@ -171,21 +171,21 @@ class LLMErrorHandlingMiddleware(AgentMiddleware[AgentState]):
|
||||
|
||||
def _build_retry_message(self, attempt: int, wait_ms: int, reason: str) -> str:
|
||||
seconds = max(1, round(wait_ms / 1000))
|
||||
reason_text = "provider is busy" if reason == "busy" else "provider request failed temporarily"
|
||||
return f"LLM request retry {attempt}/{self.retry_max_attempts}: {reason_text}. Retrying in {seconds}s."
|
||||
reason_text = "服务繁忙" if reason == "busy" else "网络/连接异常"
|
||||
return f"大模型因{reason_text}调用失败,正在第 {attempt}/{self.retry_max_attempts} 次重试,{seconds} 秒后继续…"
|
||||
|
||||
def _build_circuit_breaker_message(self) -> str:
|
||||
return "The configured LLM provider is currently unavailable due to continuous failures. Circuit breaker is engaged to protect the system. Please wait a moment before trying again."
|
||||
return "大模型因连续多次调用失败已被熔断保护,请稍候再试。"
|
||||
|
||||
def _build_user_message(self, exc: BaseException, reason: str) -> str:
|
||||
detail = _extract_error_detail(exc)
|
||||
if reason == "quota":
|
||||
return "The configured LLM provider rejected the request because the account is out of quota, billing is unavailable, or usage is restricted. Please fix the provider account and try again."
|
||||
return "大模型因账户额度不足、账单异常或使用受限而拒绝请求,请检查模型账户后再试。"
|
||||
if reason == "auth":
|
||||
return "The configured LLM provider rejected the request because authentication or access is invalid. Please check the provider credentials and try again."
|
||||
return "大模型因鉴权失败或无访问权限而拒绝请求,请检查模型的 API Key 与权限配置后再试。"
|
||||
if reason in {"busy", "transient"}:
|
||||
return "The configured LLM provider is temporarily unavailable after multiple retries. Please wait a moment and continue the conversation."
|
||||
return f"LLM request failed: {detail}"
|
||||
return "大模型因服务繁忙或网络异常,多次重试后仍未恢复,请稍候再继续。"
|
||||
return f"大模型因「{detail}」原因调用失败。"
|
||||
|
||||
def _emit_retry_event(self, attempt: int, wait_ms: int, reason: str) -> None:
|
||||
try:
|
||||
|
||||
+1
-1
@@ -713,7 +713,7 @@ class ScheduledTaskService:
|
||||
retry_count: int = 0,
|
||||
) -> None:
|
||||
"""Mark a run failed and reflect the outcome on the parent task."""
|
||||
result = _build_result(task, scheduled_for=scheduled_for, content=f"Scheduled task failed: {error}")
|
||||
result = _build_result(task, scheduled_for=scheduled_for, content=f"定时任务执行失败:{error}")
|
||||
await self.store.update_run(
|
||||
run_id,
|
||||
{"status": "failed", "finished_at": datetime.now(UTC), "error": error, "result": result, "retry_count": retry_count},
|
||||
|
||||
+20
-7
@@ -36,18 +36,31 @@ def ask_clarification_tool(
|
||||
- You're about to perform a potentially dangerous operation
|
||||
- You have a recommendation but need user approval
|
||||
|
||||
Batching multiple independent questions:
|
||||
- **You CAN and SHOULD call ask_clarification multiple times in the same turn** when the user's
|
||||
request leaves several **independent** aspects unclear (e.g. project type AND tech stack AND
|
||||
deployment target). Emit one tool call per aspect — do NOT cram several unrelated questions
|
||||
into one `question` string.
|
||||
- The frontend automatically groups all clarification cards in one turn into a single submission,
|
||||
so the user answers them together in one reply. You will then receive one combined user message
|
||||
addressing every question in order.
|
||||
- When the questions DEPEND on each other (the second only makes sense after the first is answered),
|
||||
ask only the first one and wait for the reply before asking the next.
|
||||
|
||||
Best practices:
|
||||
- Ask ONE clarification at a time for clarity
|
||||
- Be specific and clear in your question
|
||||
- Don't make assumptions when clarification is needed
|
||||
- For risky operations, ALWAYS ask for confirmation
|
||||
- After calling this tool, execution will be interrupted automatically
|
||||
- One question per ask_clarification call — keep each `question` focused on a single decision.
|
||||
- Be specific and clear in your question.
|
||||
- Provide `options` (with concise labels) whenever there is a discrete set of reasonable choices —
|
||||
this lets the user multi-select in the UI instead of typing free text.
|
||||
- Don't make assumptions when clarification is needed.
|
||||
- For risky operations, ALWAYS ask for confirmation.
|
||||
- After calling this tool (or several in one turn), execution will be interrupted automatically.
|
||||
|
||||
Args:
|
||||
question: The clarification question to ask the user. Be specific and clear.
|
||||
question: The clarification question to ask the user. Focused on a single decision; be specific and clear.
|
||||
clarification_type: The type of clarification needed (missing_info, ambiguous_requirement, approach_choice, risk_confirmation, suggestion).
|
||||
context: Optional context explaining why clarification is needed. Helps the user understand the situation.
|
||||
options: Optional list of choices (for approach_choice or suggestion types). Present clear options for the user to choose from.
|
||||
options: Optional list of choices (for approach_choice or suggestion types). Present clear options for the user to choose from; the UI supports multi-select.
|
||||
"""
|
||||
# This is a placeholder implementation
|
||||
# The actual logic is handled by ClarificationMiddleware which intercepts this tool call
|
||||
|
||||
Reference in new issue
Block a user