feat(agent):Supports custom agent and chat experience with refactoring (#957)

* feat: add agent management functionality with creation, editing, and deletion

* feat: enhance agent creation and chat experience

- Added AgentWelcome component to display agent description on new thread creation.
- Improved agent name validation with availability check during agent creation.
- Updated NewAgentPage to handle agent creation flow more effectively, including enhanced error handling and user feedback.
- Refactored chat components to streamline message handling and improve user experience.
- Introduced new bootstrap skill for personalized onboarding conversations, including detailed conversation phases and a structured SOUL.md template.
- Updated localization files to reflect new features and error messages.
- General code cleanup and optimizations across various components and hooks.

* Refactor workspace layout and agent management components

- Updated WorkspaceLayout to use useLayoutEffect for sidebar state initialization.
- Removed unused AgentFormDialog and related edit functionality from AgentCard.
- Introduced ArtifactTrigger component to manage artifact visibility.
- Enhanced ChatBox to handle artifact selection and display.
- Improved message list rendering logic to avoid loading states.
- Updated localization files to remove deprecated keys and add new translations.
- Refined hooks for local settings and thread management to improve performance and clarity.
- Added temporal awareness guidelines to deep research skill documentation.

* feat: refactor chat components and introduce thread management hooks

* feat: improve artifact file detail preview logic and clean up console logs

* feat: refactor lead agent creation logic and improve logging details

* feat: validate agent name format and enhance error handling in agent setup

* feat: simplify thread search query by removing unnecessary metadata

* feat: update query key in useDeleteThread and useRenameThread for consistency

* feat: add isMock parameter to thread and artifact handling for improved testing

* fix: reorder import of setup_agent for consistency in builtins module

* feat: append mock parameter to thread links in CaseStudySection for testing purposes

* fix: update load_agent_soul calls to use cfg.name for improved clarity

* fix: update date format in apply_prompt_template for consistency

* feat: integrate isMock parameter into artifact content loading for enhanced testing

* docs: add license section to SKILL.md for clarity and attribution

* feat(agent): enhance model resolution and agent configuration handling

* chore: remove unused import of _resolve_model_name from agents

* feat(agent): remove unused field

* fix(agent): set default value for requested_model_name in _resolve_model_name function

* feat(agent): update get_available_tools call to handle optional agent_config and improve middleware function signature

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
JeffJiang
2026-03-03 21:32:01 +08:00
committed by GitHub
parent 8342e88534
commit 7de94394d4
61 changed files with 3002 additions and 503 deletions
+53 -40
View File
@@ -1,42 +1,63 @@
import type { HumanMessage } from "@langchain/core/messages";
import type { AIMessage } from "@langchain/langgraph-sdk";
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
import { useStream, type UseStream } from "@langchain/langgraph-sdk/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { getAPIClient } from "../api";
import type { LocalSettings } from "../settings";
import { useUpdateSubtask } from "../tasks/context";
import { uploadFiles } from "../uploads";
import type {
AgentThread,
AgentThreadContext,
AgentThreadState,
} from "./types";
import type { AgentThread, AgentThreadState } from "./types";
export type ToolEndEvent = {
name: string;
data: unknown;
};
export type ThreadStreamOptions = {
threadId?: string | null | undefined;
context: LocalSettings["context"];
isMock?: boolean;
onStart?: (threadId: string) => void;
onFinish?: (state: AgentThreadState) => void;
onToolEnd?: (event: ToolEndEvent) => void;
};
export function useThreadStream({
threadId,
isNewThread,
context,
isMock,
onStart,
onFinish,
}: {
isNewThread: boolean;
threadId: string | null | undefined;
onFinish?: (state: AgentThreadState) => void;
}) {
onToolEnd,
}: ThreadStreamOptions) {
const [_threadId, setThreadId] = useState<string | null>(threadId ?? null);
const queryClient = useQueryClient();
const updateSubtask = useUpdateSubtask();
const thread = useStream<AgentThreadState>({
client: getAPIClient(),
client: getAPIClient(isMock),
assistantId: "lead_agent",
threadId: isNewThread ? undefined : threadId,
threadId: _threadId,
reconnectOnMount: true,
fetchStateHistory: { limit: 1 },
onCreated(meta) {
setThreadId(meta.thread_id);
onStart?.(meta.thread_id);
},
onLangChainEvent(event) {
if (event.event === "on_tool_end") {
onToolEnd?.({
name: event.name,
data: event.data,
});
}
},
onCustomEvent(event: unknown) {
console.info(event);
if (
typeof event === "object" &&
event !== null &&
@@ -76,25 +97,13 @@ export function useThreadStream({
);
},
});
return thread;
}
export function useSubmitThread({
threadId,
thread,
threadContext,
isNewThread,
afterSubmit,
}: {
isNewThread: boolean;
threadId: string | null | undefined;
thread: UseStream<AgentThreadState>;
threadContext: Omit<AgentThreadContext, "thread_id">;
afterSubmit?: () => void;
}) {
const queryClient = useQueryClient();
const callback = useCallback(
async (message: PromptInputMessage) => {
const sendMessage = useCallback(
async (
threadId: string,
message: PromptInputMessage,
extraContext?: Record<string, unknown>,
) => {
const text = message.text.trim();
// Upload files first if any
@@ -163,10 +172,10 @@ export function useSubmitThread({
},
],
},
] as HumanMessage[],
],
},
{
threadId: isNewThread ? threadId! : undefined,
threadId: threadId,
streamSubgraphs: true,
streamResumable: true,
streamMode: ["values", "messages-tuple", "custom"],
@@ -174,17 +183,21 @@ export function useSubmitThread({
recursion_limit: 1000,
},
context: {
...threadContext,
...extraContext,
...context,
thinking_enabled: context.mode !== "flash",
is_plan_mode: context.mode === "pro" || context.mode === "ultra",
subagent_enabled: context.mode === "ultra",
thread_id: threadId,
},
},
);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
afterSubmit?.();
// afterSubmit?.();
},
[thread, isNewThread, threadId, threadContext, queryClient, afterSubmit],
[thread, context, queryClient],
);
return callback;
return [thread, sendMessage] as const;
}
export function useThreads(
+3 -3
View File
@@ -1,11 +1,10 @@
import { type BaseMessage } from "@langchain/core/messages";
import type { Thread } from "@langchain/langgraph-sdk";
import type { Message, Thread } from "@langchain/langgraph-sdk";
import type { Todo } from "../todos";
export interface AgentThreadState extends Record<string, unknown> {
title: string;
messages: BaseMessage[];
messages: Message[];
artifacts: string[];
todos?: Todo[];
}
@@ -19,4 +18,5 @@ export interface AgentThreadContext extends Record<string, unknown> {
is_plan_mode: boolean;
subagent_enabled: boolean;
reasoning_effort?: "minimal" | "low" | "medium" | "high";
agent_name?: string;
}
+7 -4
View File
@@ -1,4 +1,4 @@
import type { BaseMessage } from "@langchain/core/messages";
import type { Message } from "@langchain/langgraph-sdk";
import type { AgentThread } from "./types";
@@ -6,12 +6,15 @@ export function pathOfThread(threadId: string) {
return `/workspace/chats/${threadId}`;
}
export function textOfMessage(message: BaseMessage) {
export function textOfMessage(message: Message) {
if (typeof message.content === "string") {
return message.content;
} else if (Array.isArray(message.content)) {
return message.content.find((part) => part.type === "text" && part.text)
?.text as string;
for (const part of message.content) {
if (part.type === "text") {
return part.text;
}
}
}
return null;
}