fix: improve agent handoff and leaderboard scheduling

This commit is contained in:
吉鑫升 committed 2026-06-05 21:46:04 +08:00
1 parent cbbde887d4
commit a41b5a6d4c
4 files changed
+75 -26

No files matched your search

@@ -133,8 +133,7 @@ export function SkillDetailDialog({
return stripYamlFrontmatter(contentData?.content ?? ""); return stripYamlFrontmatter(contentData?.content ?? "");
}, [contentData]); }, [contentData]);
const defaultTab: "detail" | "source" = const defaultTab: "detail" = "detail";
skill && !skill.detail ? "source" : "detail";
const selectedTab = activeTab ?? defaultTab; const selectedTab = activeTab ?? defaultTab;
function startEditing() { function startEditing() {
@@ -234,10 +233,6 @@ export function SkillDetailDialog({
{tx.filesSectionTitle} {tx.filesSectionTitle}
</TabsTrigger> </TabsTrigger>
)} )}
<TabsTrigger value="source" className="text-xs">
<FileCode2Icon className="mr-1 size-3.5" />
{tx.contentSectionTitle}
</TabsTrigger>
</TabsList> </TabsList>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
@@ -337,13 +332,9 @@ export function SkillDetailDialog({
<div className="text-muted-foreground rounded-lg border border-dashed px-4 py-8 text-center"> <div className="text-muted-foreground rounded-lg border border-dashed px-4 py-8 text-center">
<FileTextIcon className="mx-auto mb-2 size-6 opacity-40" /> <FileTextIcon className="mx-auto mb-2 size-6 opacity-40" />
<p className="text-sm">{tx.detailEmpty}</p> <p className="text-sm">{tx.detailEmpty}</p>
{isOwner ? ( {isOwner && canManage ? (
<p className="mt-1 text-xs">
Open the SKILL.md tab to view the skill source.
</p>
) : (
<p className="mt-1 text-xs">{tx.detailAdminOnlyHint}</p> <p className="mt-1 text-xs">{tx.detailAdminOnlyHint}</p>
)} ) : null}
</div> </div>
)} )}
</TabsContent> </TabsContent>
+8 -3
View File
@@ -131,8 +131,8 @@ export default function AgentChatPage() {
const pendingMessageHandled = useRef(false); const pendingMessageHandled = useRef(false);
useEffect(() => { useEffect(() => {
if (pendingMessageHandled.current) return; if (pendingMessageHandled.current) return;
if (!agent_id || !isNewThread) return; if (!agent_id) return;
let pending: { agentId?: string; text?: string } | null = null; let pending: { agentId?: string; threadId?: string; text?: string } | null = null;
try { try {
const raw = sessionStorage.getItem("pendingAgentMessage"); const raw = sessionStorage.getItem("pendingAgentMessage");
if (raw) pending = JSON.parse(raw); if (raw) pending = JSON.parse(raw);
@@ -140,10 +140,15 @@ export default function AgentChatPage() {
pending = null; pending = null;
} }
if (!pending || pending.agentId !== agent_id || !pending.text) return; if (!pending || pending.agentId !== agent_id || !pending.text) return;
if (pending.threadId) {
if (pending.threadId !== threadId) return;
} else if (!isNewThread) {
return;
}
pendingMessageHandled.current = true; pendingMessageHandled.current = true;
sessionStorage.removeItem("pendingAgentMessage"); sessionStorage.removeItem("pendingAgentMessage");
handleSubmit({ text: pending.text, files: [] }); handleSubmit({ text: pending.text, files: [] });
}, [agent_id, isNewThread, handleSubmit]); }, [agent_id, threadId, isNewThread, handleSubmit]);
const handleClarificationSubmit = useCallback( const handleClarificationSubmit = useCallback(
(answer: string) => { (answer: string) => {
+40 -11
View File
@@ -1,9 +1,11 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { getAPIClient } from "@/core/api";
import { usePromptPrefixSettings } from "@/core/system-settings/hooks"; import { usePromptPrefixSettings } from "@/core/system-settings/hooks";
import { useUserPreferencesQuery } from "@/core/user-preferences"; import { useUserPreferencesQuery } from "@/core/user-preferences";
import { ArtifactTrigger, useArtifacts } from "@/components/workspace/artifacts"; import { ArtifactTrigger, useArtifacts } from "@/components/workspace/artifacts";
@@ -58,6 +60,8 @@ export default function ChatPage() {
const hideAdminPrompt = userPreferences?.hide_admin_prompt_prefix === true; const hideAdminPrompt = userPreferences?.hide_admin_prompt_prefix === true;
const [promptPrefixOn, setPromptPrefixOn] = useState(false); const [promptPrefixOn, setPromptPrefixOn] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null); const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [agentHandoffPending, setAgentHandoffPending] = useState(false);
const agentHandoffPendingRef = useRef(false);
const { agents: selectorAgents, isLoading: selectorLoading } = const { agents: selectorAgents, isLoading: selectorLoading } =
useAgentSelector(); useAgentSelector();
const { selectedArtifact } = useArtifacts(); const { selectedArtifact } = useArtifacts();
@@ -148,15 +152,37 @@ export default function ChatPage() {
message.text && message.text &&
(!message.files || message.files.length === 0) (!message.files || message.files.length === 0)
) { ) {
try { if (agentHandoffPendingRef.current) return;
sessionStorage.setItem( const text = message.text.trim();
"pendingAgentMessage", agentHandoffPendingRef.current = true;
JSON.stringify({ agentId: selectedAgentId, text: message.text }), setAgentHandoffPending(true);
); setHasStartedConversation(true);
} catch { void (async () => {
// sessionStorage may be unavailable in private mode; fall through. try {
} const created = await getAPIClient(isMock).threads.create({
router.push(`/page/workspace/agents/${selectedAgentId}/chats/new`); metadata: { agent_id: selectedAgentId },
});
sessionStorage.setItem(
"pendingAgentMessage",
JSON.stringify({
agentId: selectedAgentId,
threadId: created.thread_id,
text,
}),
);
saveLastAgentId(selectedAgentId);
router.push(
`/page/workspace/agents/${selectedAgentId}/chats/${created.thread_id}`,
);
} catch (err) {
agentHandoffPendingRef.current = false;
setAgentHandoffPending(false);
setHasStartedConversation(false);
toast.error(
err instanceof Error ? err.message : "创建智能体对话失败",
);
}
})();
return; return;
} }
@@ -200,6 +226,8 @@ export default function ChatPage() {
hideAdminPrompt, hideAdminPrompt,
preferencesLoading, preferencesLoading,
selectedAgentId, selectedAgentId,
agentHandoffPending,
isMock,
router, router,
settings.context.writing_mode, settings.context.writing_mode,
writingArtifactPath, writingArtifactPath,
@@ -345,7 +373,8 @@ export default function ChatPage() {
context={settings.context} context={settings.context}
disabled={ disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading isUploading ||
agentHandoffPending
} }
onContextChange={(context) => onContextChange={(context) =>
setSettings("context", context) setSettings("context", context)
@@ -72,6 +72,14 @@ _LEADERBOARD_CACHE_TTL_SECONDS = max(
5.0, 5.0,
float(os.getenv("ADMIN_LEADERBOARD_CACHE_TTL_SECONDS", "300")), float(os.getenv("ADMIN_LEADERBOARD_CACHE_TTL_SECONDS", "300")),
) )
_LEADERBOARD_QUERY_PAUSE_SECONDS = max(
0.0,
float(os.getenv("ADMIN_LEADERBOARD_QUERY_PAUSE_SECONDS", "0.05")),
)
_LEADERBOARD_START_DELAY_SECONDS = max(
0.0,
float(os.getenv("ADMIN_LEADERBOARD_START_DELAY_SECONDS", "0.2")),
)
_leaderboard_semaphore = asyncio.Semaphore(_LEADERBOARD_MAX_CONCURRENT) _leaderboard_semaphore = asyncio.Semaphore(_LEADERBOARD_MAX_CONCURRENT)
_leaderboard_jobs: dict[str, asyncio.Task] = {} _leaderboard_jobs: dict[str, asyncio.Task] = {}
_leaderboard_cache: dict[str, dict] = {} _leaderboard_cache: dict[str, dict] = {}
@@ -1035,6 +1043,11 @@ def _leaderboard_cache_key(
return sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest() return sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()
async def _leaderboard_pause() -> None:
if _LEADERBOARD_QUERY_PAUSE_SECONDS > 0:
await asyncio.sleep(_LEADERBOARD_QUERY_PAUSE_SECONDS)
async def _build_user_leaderboard_response( async def _build_user_leaderboard_response(
session_factory, session_factory,
settings, settings,
@@ -1064,16 +1077,25 @@ async def _build_user_leaderboard_response(
"cleanup_system_questions": settings.cleanup_system_questions, "cleanup_system_questions": settings.cleanup_system_questions,
} }
overview = await _query_overview(session, since_dt, until_dt, **common) overview = await _query_overview(session, since_dt, until_dt, **common)
await _leaderboard_pause()
users = await _query_user_leaderboard(session, since_dt, until_dt, **common) users = await _query_user_leaderboard(session, since_dt, until_dt, **common)
await _leaderboard_pause()
skills = await _query_call_rankings(session, since_dt, until_dt, user_id=None, field="skill", limit=limit, **tool_common) skills = await _query_call_rankings(session, since_dt, until_dt, user_id=None, field="skill", limit=limit, **tool_common)
await _leaderboard_pause()
tools = await _query_call_rankings(session, since_dt, until_dt, user_id=None, field="tool", limit=limit, **tool_common) tools = await _query_call_rankings(session, since_dt, until_dt, user_id=None, field="tool", limit=limit, **tool_common)
await _leaderboard_pause()
models = await _query_model_rankings(session, since_dt, until_dt, user_id=None, limit=limit, **common) models = await _query_model_rankings(session, since_dt, until_dt, user_id=None, limit=limit, **common)
if not models: if not models:
models = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="model", limit=limit, **common) models = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="model", limit=limit, **common)
await _leaderboard_pause()
agents = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="agent", limit=limit, **common) agents = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="agent", limit=limit, **common)
await _leaderboard_pause()
run_statuses = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="status", limit=limit, **common) run_statuses = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="status", limit=limit, **common)
await _leaderboard_pause()
recent_questions = await _query_recent_questions(session, since_dt, until_dt, user_id=None, limit=limit, **question_common) recent_questions = await _query_recent_questions(session, since_dt, until_dt, user_id=None, limit=limit, **question_common)
await _leaderboard_pause()
frequent_questions = await _query_frequent_questions(session, since_dt, until_dt, limit=limit, **question_common) frequent_questions = await _query_frequent_questions(session, since_dt, until_dt, limit=limit, **question_common)
await _leaderboard_pause()
trends = await _query_trends(session, since_dt, until_dt, **common) trends = await _query_trends(session, since_dt, until_dt, **common)
return AdminLeaderboardResponse( return AdminLeaderboardResponse(
@@ -1113,6 +1135,8 @@ async def _refresh_leaderboard_cache(
) -> None: ) -> None:
acquired = False acquired = False
try: try:
if _LEADERBOARD_START_DELAY_SECONDS > 0:
await asyncio.sleep(_LEADERBOARD_START_DELAY_SECONDS)
await _leaderboard_semaphore.acquire() await _leaderboard_semaphore.acquire()
acquired = True acquired = True
response = await asyncio.wait_for( response = await asyncio.wait_for(