diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 29761d38e..3c5c501fd 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -75,6 +75,7 @@ async def rm(): @login_required async def save(): req = await get_request_json() + req['release'] = bool(req.get("release", "")) try: req["dsl"] = CanvasReplicaService.normalize_dsl(req["dsl"]) except ValueError as e: diff --git a/api/apps/sdk/session.py b/api/apps/sdk/session.py index 249a8e133..18bdc4ee5 100644 --- a/api/apps/sdk/session.py +++ b/api/apps/sdk/session.py @@ -83,7 +83,9 @@ async def create(tenant_id, chat_id): @manager.route("/agents//sessions", methods=["POST"]) # noqa: F821 @token_required async def create_agent_session(tenant_id, agent_id): - user_id = request.args.get("user_id", tenant_id) + req = await get_request_json() + user_id = req.get("user_id") or request.args.get("user_id", tenant_id) + release_mode = req.get("release", request.args.get("release", False)) e, cvs = UserCanvasService.get_by_id(agent_id) if not e: return get_error_data_result("Agent not found.") @@ -92,6 +94,8 @@ async def create_agent_session(tenant_id, agent_id): if not isinstance(cvs.dsl, str): cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) + if release_mode and not bool(cvs.release): + raise PermissionError("No available published version") session_id = get_uuid() canvas = Canvas(cvs.dsl, tenant_id, agent_id, canvas_id=cvs.id) canvas.reset() @@ -986,15 +990,35 @@ async def agent_bot_completions(agent_id): return get_error_data_result(message='Authentication error: API key is invalid!"') if req.get("stream", True): - resp = Response(agent_completion(objs[0].tenant_id, agent_id, **req), mimetype="text/event-stream") + async def stream(): + try: + async for answer in agent_completion(objs[0].tenant_id, agent_id, **req): + yield answer + except Exception as e: + logging.exception(e) + error_result = get_error_data_result(message=str(e) or "Unknown error") + yield "data:" + json.dumps( + { + "event": "message", + "data": {"content": f"Error {error_result['code']}: {error_result['message']}\n\n"}, + **error_result, + }, + ensure_ascii=False, + ) + "\n\n" + + resp = Response(stream(), mimetype="text/event-stream") resp.headers.add_header("Cache-control", "no-cache") resp.headers.add_header("Connection", "keep-alive") resp.headers.add_header("X-Accel-Buffering", "no") resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8") return resp - async for answer in agent_completion(objs[0].tenant_id, agent_id, **req): - return get_result(data=answer) + try: + async for answer in agent_completion(objs[0].tenant_id, agent_id, **req): + return get_result(data=answer) + except Exception as e: + logging.exception(e) + return get_error_data_result(message=str(e) or "Unknown error") return None diff --git a/api/db/db_models.py b/api/db/db_models.py index 8ba327838..563c079e9 100644 --- a/api/db/db_models.py +++ b/api/db/db_models.py @@ -1032,6 +1032,7 @@ class UserCanvas(DataBaseModel): title = CharField(max_length=255, null=True, help_text="Canvas title") permission = CharField(max_length=16, null=False, help_text="me|team", default="me", index=True) + release = BooleanField(null=False, help_text="is released", default=False, index=True) description = TextField(null=True, help_text="Canvas description") canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True) canvas_category = CharField(max_length=32, null=False, default="agent_canvas", help_text="Canvas category: agent_canvas|dataflow_canvas", index=True) @@ -1407,6 +1408,7 @@ def migrate_db(): alter_db_add_column(migrator, "task", "task_type", CharField(max_length=32, null=False, default="")) alter_db_add_column(migrator, "task", "priority", IntegerField(default=0)) alter_db_add_column(migrator, "user_canvas", "permission", CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)) + alter_db_add_column(migrator, "user_canvas", "release", BooleanField(null=False, help_text="is released", default=False, index=True)) alter_db_add_column(migrator, "llm", "is_tools", BooleanField(null=False, help_text="support tools", default=False)) alter_db_add_column(migrator, "mcp_server", "variables", JSONField(null=True, help_text="MCP Server variables", default=dict)) alter_db_rename_column(migrator, "task", "process_duation", "process_duration") diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index 99cb19900..838951a9a 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -195,10 +195,12 @@ async def completion(tenant_id, agent_id, session_id=None, **kwargs): inputs = kwargs.get("inputs", {}) user_id = kwargs.get("user_id", "") custom_header = kwargs.get("custom_header", "") + release_mode = str(kwargs.get("release", "")).strip().lower() if session_id: e, conv = API4ConversationService.get_by_id(session_id) - assert e, "Session not found!" + if not e: + raise LookupError("Session not found!") if not conv.message: conv.message = [] if not isinstance(conv.dsl, str): @@ -206,10 +208,15 @@ async def completion(tenant_id, agent_id, session_id=None, **kwargs): canvas = Canvas(conv.dsl, tenant_id, agent_id, canvas_id=agent_id, custom_header=custom_header) else: e, cvs = UserCanvasService.get_by_id(agent_id) - assert e, "Agent not found." - assert cvs.user_id == tenant_id, "You do not own the agent." + if not e: + raise LookupError("Agent not found.") + if cvs.user_id != tenant_id: + raise PermissionError("You do not own the agent.") + if release_mode == "true" and not bool(cvs.release): + raise PermissionError("No available published version") if not isinstance(cvs.dsl, str): cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) + session_id=get_uuid() canvas = Canvas(cvs.dsl, tenant_id, agent_id, canvas_id=cvs.id, custom_header=custom_header) canvas.reset() diff --git a/docs/references/http_api_reference.md b/docs/references/http_api_reference.md index 65a020ab7..8a45106a3 100644 --- a/docs/references/http_api_reference.md +++ b/docs/references/http_api_reference.md @@ -3682,6 +3682,7 @@ Asks a specified agent a question to start an AI-powered conversation. - `"inputs"`: `object` (optional) - `"user_id"`: `string` (optional) - `"return_trace"`: `boolean` (optional, default `false`) — include execution trace logs. + - `"release"`: `boolean` (optional, default `false`) - whether to visit the latest published canvas. #### Streaming events to handle diff --git a/docs/references/python_api_reference.md b/docs/references/python_api_reference.md index c0eeee3b3..80a8666e9 100644 --- a/docs/references/python_api_reference.md +++ b/docs/references/python_api_reference.md @@ -1593,6 +1593,10 @@ Creates a session with the current agent. The parameters in `begin` component. +Also supports: + +- `release` (`bool | str`, optional): Set to `True` (or `"true"`) to create the session in release mode (published version only). + #### Returns - Success: A `Session` object containing the following attributes: @@ -1610,6 +1614,8 @@ rag_object = RAGFlow(api_key="", base_url="http://: agent_id = "AGENT_ID" agent = rag_object.list_agents(id = agent_id)[0] session = agent.create_session() +# Or create in release mode: +# session = agent.create_session(release=True) ``` --- diff --git a/web/src/components/embed-dialog/index.tsx b/web/src/components/embed-dialog/index.tsx index 27e669457..268b84f4b 100644 --- a/web/src/components/embed-dialog/index.tsx +++ b/web/src/components/embed-dialog/index.tsx @@ -36,6 +36,7 @@ import { z } from 'zod'; const FormSchema = z.object({ visibleAvatar: z.boolean(), + publishAvatar: z.boolean(), locale: z.string(), embedType: z.enum(['fullscreen', 'widget']), enableStreaming: z.boolean(), @@ -62,6 +63,7 @@ function EmbedDialog({ resolver: zodResolver(FormSchema), defaultValues: { visibleAvatar: false, + publishAvatar: false, locale: '', embedType: 'fullscreen' as const, enableStreaming: false, @@ -79,7 +81,14 @@ function EmbedDialog({ }, []); const generateIframeSrc = useCallback(() => { - const { visibleAvatar, locale, embedType, enableStreaming, theme } = values; + const { + visibleAvatar, + publishAvatar, + locale, + embedType, + enableStreaming, + theme, + } = values; const baseRoute = embedType === 'widget' ? Routes.ChatWidget @@ -87,6 +96,9 @@ function EmbedDialog({ ? Routes.AgentShare : Routes.ChatShare; let src = `${location.origin}${baseRoute}?shared_id=${token}&from=${from}&auth=${beta}`; + if (publishAvatar) { + src += '&release=true'; + } if (visibleAvatar) { src += '&visible_avatar=1'; } @@ -245,6 +257,22 @@ function EmbedDialog({ )} /> + ( + + Publish Avatar + + + + + + )} + /> {values.embedType === 'widget' && ( { const val = JSON.parse(value?.data || ''); console.info('data:', val); - if (val.code === 500) { + if (typeof val?.code === 'number' && val.code !== 0) { message.error(val.message); } diff --git a/web/src/pages/agent/chat/use-send-agent-message.ts b/web/src/pages/agent/chat/use-send-agent-message.ts index 0d1fef17c..a76b4c11c 100644 --- a/web/src/pages/agent/chat/use-send-agent-message.ts +++ b/web/src/pages/agent/chat/use-send-agent-message.ts @@ -105,7 +105,13 @@ export function findInputFromList(eventList: IEventList) { } export function getLatestError(eventList: IEventList) { - return get(eventList.at(-1), 'data.outputs._ERROR'); + const latest = eventList.at(-1) as + | { code?: number; message?: string } + | undefined; + return ( + get(latest, 'data.outputs._ERROR') || + (latest?.code && latest.code !== 0 ? latest?.message : undefined) + ); } export const useGetBeginNodePrologue = () => { @@ -218,13 +224,15 @@ export const useSendAgentMessage = ({ isShared, refetch, isTaskMode: isTask, + releaseMode, }: { url?: string; addEventList?: (data: IEventList, messageId: string) => void; - beginParams?: any[]; + beginParams?: BeginQuery[]; isShared?: boolean; refetch?: () => void; isTaskMode?: boolean; + releaseMode?: string | null; }) => { const { id: agentId } = useParams(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); @@ -232,9 +240,10 @@ export const useSendAgentMessage = ({ const [sessionId, setSessionId] = useState(null); const { send, answerList, done, stopOutputMessage, resetAnswerList } = useSendMessageBySSE(url || api.runCanvas); + const firstAnswer = answerList[0]; const messageId = useMemo(() => { - return answerList[0]?.message_id; - }, [answerList]); + return firstAnswer?.message_id; + }, [firstAnswer]); const isTaskMode = useIsTaskMode(isTask); @@ -266,12 +275,12 @@ export const useSendAgentMessage = ({ const { stopMessage } = useStopMessage(); const stopConversation = useCallback(() => { - const taskId = answerList.at(0)?.task_id; + const taskId = firstAnswer?.task_id; stopOutputMessage(); if (!isShared) { stopMessage(taskId); } - }, [answerList, isShared, stopMessage, stopOutputMessage]); + }, [firstAnswer, isShared, stopMessage, stopOutputMessage]); const sendMessage = useCallback( async ({ @@ -303,6 +312,9 @@ export const useSendAgentMessage = ({ params.files = uploadResponseList; params.session_id = sessionId || exploreSessionId; + if (releaseMode) { + params.release = releaseMode; + } } try { @@ -334,6 +346,7 @@ export const useSendAgentMessage = ({ setValue, removeLatestMessage, refetch, + releaseMode, ], ); @@ -345,10 +358,14 @@ export const useSendAgentMessage = ({ .join('
'), role: MessageType.User, }); - await send({ ...body, session_id: sessionId }); + await send({ + ...body, + session_id: sessionId, + ...(releaseMode ? { release: releaseMode } : {}), + }); refetch?.(); }, - [addNewestOneQuestion, refetch, send, sessionId], + [addNewestOneQuestion, refetch, releaseMode, send, sessionId], ); // reset session @@ -396,7 +413,7 @@ export const useSendAgentMessage = ({ ], ); - const sendedTaskMessage = useRef(false); + const sendedTaskMessage = useRef(false); const sendMessageInTaskMode = useCallback(() => { if (isShared || !isTaskMode || sendedTaskMessage.current) { @@ -457,10 +474,10 @@ export const useSendAgentMessage = ({ }, [addEventList, answerList, addEventListFun, messageId]); useEffect(() => { - if (answerList[0]?.session_id) { - setSessionId(answerList[0]?.session_id); + if (firstAnswer?.session_id) { + setSessionId(firstAnswer.session_id); } - }, [answerList]); + }, [firstAnswer]); return { value, diff --git a/web/src/pages/agent/hooks/use-send-shared-message.ts b/web/src/pages/agent/hooks/use-send-shared-message.ts index 07f09ba9c..5bcb31dfc 100644 --- a/web/src/pages/agent/hooks/use-send-shared-message.ts +++ b/web/src/pages/agent/hooks/use-send-shared-message.ts @@ -6,6 +6,7 @@ import { buildRequestBody, useSendAgentMessage, } from '@/pages/agent/chat/use-send-agent-message'; +import { BeginQuery } from '@/pages/agent/interface'; import { isEmpty } from 'lodash'; import trim from 'lodash/trim'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -16,36 +17,54 @@ export const useSendButtonDisabled = (value: string) => { return trim(value) === ''; }; +const DATA_PREFIX = 'data_'; + +interface SharedChatSearchParams { + from: SharedFrom; + sharedId: string | null; + release: string | null; + locale: string | null; + theme: string | null; + data: Record; + visibleAvatar: boolean; +} + export const useGetSharedChatSearchParams = () => { const [searchParams] = useSearchParams(); - const data_prefix = 'data_'; const data = Object.fromEntries( searchParams .entries() - .filter(([key]) => key.startsWith(data_prefix)) - .map(([key, value]) => [key.replace(data_prefix, ''), value]), + .filter(([key]) => key.startsWith(DATA_PREFIX)) + .map(([key, value]) => [key.replace(DATA_PREFIX, ''), value]), ); return { from: searchParams.get('from') as SharedFrom, sharedId: searchParams.get('shared_id'), + release: searchParams.get('release'), locale: searchParams.get('locale'), theme: searchParams.get('theme'), - data: data, + data, visibleAvatar: searchParams.get('visible_avatar') ? searchParams.get('visible_avatar') !== '1' : true, - }; + } as SharedChatSearchParams; }; export const useSendNextSharedMessage = ( addEventList: (data: IEventList, messageId: string) => void, ) => { - const { from, sharedId: conversationId } = useGetSharedChatSearchParams(); - const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`; + const { + from, + sharedId: conversationId, + release, + } = useGetSharedChatSearchParams(); + const botType = from === SharedFrom.Agent ? 'agentbots' : 'chatbots'; + const releaseQuery = release ? `?release=${encodeURIComponent(release)}` : ''; + const url = `/api/v1/${botType}/${conversationId}/completions${releaseQuery}`; const { data: inputsData } = useFetchExternalAgentInputs(); - const [params, setParams] = useState([]); - const sendedTaskMessage = useRef(false); + const [params, setParams] = useState([]); + const sendedTaskMessage = useRef(false); const isTaskMode = inputsData.mode === AgentDialogueMode.Task; @@ -61,10 +80,10 @@ export const useSendNextSharedMessage = ( beginParams: params, isShared: true, isTaskMode, + releaseMode: release, }); - const ok = useCallback( - (params: any[]) => { + (params: BeginQuery[]) => { if (isTaskMode) { const msgBody = buildRequestBody(''); diff --git a/web/src/pages/next-chats/hooks/use-send-shared-message.ts b/web/src/pages/next-chats/hooks/use-send-shared-message.ts index ba160168d..b5d899ad2 100644 --- a/web/src/pages/next-chats/hooks/use-send-shared-message.ts +++ b/web/src/pages/next-chats/hooks/use-send-shared-message.ts @@ -21,38 +21,53 @@ export const useSendButtonDisabled = (value: string) => { return trim(value) === ''; }; +const DATA_PREFIX = 'data_'; + +interface SharedChatSearchParams { + from: SharedFrom; + sharedId: string | null; + release: string | null; + locale: string | null; + theme: string | null; + data: Record; + visibleAvatar: boolean; +} + export const useGetSharedChatSearchParams = () => { const [searchParams] = useSearchParams(); - const data_prefix = 'data_'; const data = Object.fromEntries( Array.from(searchParams.entries()) - .filter(([key]) => key.startsWith(data_prefix)) - .map(([key, value]) => [key.replace(data_prefix, ''), value]), + .filter(([key]) => key.startsWith(DATA_PREFIX)) + .map(([key, value]) => [key.replace(DATA_PREFIX, ''), value]), ); return { from: searchParams.get('from') as SharedFrom, sharedId: searchParams.get('shared_id'), + release: searchParams.get('release'), locale: searchParams.get('locale'), theme: searchParams.get('theme'), data: data, visibleAvatar: searchParams.get('visible_avatar') ? searchParams.get('visible_avatar') !== '1' : true, - }; + } as SharedChatSearchParams; }; export const useSendSharedMessage = () => { const { from, sharedId: conversationId, - data: data, + release, + data: sharedData, } = useGetSharedChatSearchParams(); + const botType = from === SharedFrom.Agent ? 'agentbots' : 'chatbots'; + const releaseQuery = release ? `?release=${encodeURIComponent(release)}` : ''; + const completionUrl = `/api/v1/${botType}/${conversationId}/completions${releaseQuery}`; const { createSharedConversation: setConversation } = useCreateNextSharedConversation(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); - const { send, answer, done, stopOutputMessage } = useSendMessageWithSse( - `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`, - ); + const { send, answer, done, stopOutputMessage } = + useSendMessageWithSse(completionUrl); const { derivedMessages, removeLatestMessage, @@ -79,6 +94,7 @@ export const useSendSharedMessage = () => { session_id: get(derivedMessages, '0.session_id'), reasoning: enableThinking, internet: enableInternet, + ...(release ? { release } : {}), }); if (isCompletionError(res)) { @@ -87,7 +103,14 @@ export const useSendSharedMessage = () => { removeLatestMessage(); } }, - [send, conversationId, derivedMessages, setValue, removeLatestMessage], + [ + send, + conversationId, + derivedMessages, + setValue, + removeLatestMessage, + release, + ], ); const handleSendMessage = useCallback( @@ -111,12 +134,16 @@ export const useSendSharedMessage = () => { const fetchSessionId = useCallback(async () => { const payload = { question: '' }; - const ret = await send({ ...payload, ...data }); + const ret = await send({ + ...payload, + ...sharedData, + ...(release ? { release } : {}), + }); if (isCompletionError(ret)) { message.error(ret?.data.message); setHasError(true); } - }, [send]); + }, [sharedData, release, send]); useEffect(() => { fetchSessionId();