Feat: published agent version control (#13410)

### What problem does this PR solve?

Feat: published agent version control

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Magicbook1108
2026-03-05 17:26:39 +08:00
committed by GitHub
parent 8c9b080499
commit 47540a4147
11 changed files with 175 additions and 43 deletions

View File

@ -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:

View File

@ -83,7 +83,9 @@ async def create(tenant_id, chat_id):
@manager.route("/agents/<agent_id>/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

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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="<YOUR_API_KEY>", base_url="http://<YOUR_BASE_URL>:
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)
```
---

View File

@ -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({
</FormItem>
)}
/>
<FormField
control={form.control}
name="publishAvatar"
render={({ field }) => (
<FormItem>
<FormLabel>Publish Avatar</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{values.embedType === 'widget' && (
<FormField
control={form.control}

View File

@ -145,7 +145,7 @@ export const useSendMessageBySSE = (url: string = api.completeConversation) => {
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);
}

View File

@ -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<string | null>(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('<br/>'),
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<boolean>(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,

View File

@ -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<string, string>;
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<any[]>([]);
const sendedTaskMessage = useRef<boolean>(false);
const [params, setParams] = useState<BeginQuery[]>([]);
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('');

View File

@ -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<string, string>;
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();