fix: improve agent handoff and leaderboard scheduling
This commit is contained in:
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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in new issue
Block a user