mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-19 20:14:32 +00:00
fix: track agent creation errors more
This commit is contained in:
1 parent
732f7d1a89
commit
a8f1e0959e
10 files changed
+221
-142
No files matched your search
@@ -178,6 +178,30 @@ def _resolve_folder_id(conn, folder_id, user):
|
|||||||
return str(folder["id"]), None
|
return str(folder["id"]), None
|
||||||
|
|
||||||
|
|
||||||
|
def _reject(message: str, user: str, field: str = "-"):
|
||||||
|
"""Log a request-validation rejection at WARN and return its 400 response.
|
||||||
|
|
||||||
|
Every validation branch in the agent write paths used to ``make_response``
|
||||||
|
a 400 without logging anything, so a rejected update left no server-side
|
||||||
|
trace — the only evidence was the request span's status code. The client
|
||||||
|
compounded it by discarding the response body, which made an entirely
|
||||||
|
deterministic failure undiagnosable from any telemetry we keep. Route all
|
||||||
|
400s through here so the field and user always reach the logs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: User-facing reason, returned verbatim in the response body.
|
||||||
|
user: Subject claim of the caller, for correlating with client reports.
|
||||||
|
field: Name of the offending field, or ``"-"`` when not field-specific.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Flask 400 response carrying ``{"success": False, "message": ...}``.
|
||||||
|
"""
|
||||||
|
current_app.logger.warning(
|
||||||
|
"Agent update rejected: %s (field=%s, user=%s)", message, field, user
|
||||||
|
)
|
||||||
|
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||||
|
|
||||||
|
|
||||||
def _format_agent_output(
|
def _format_agent_output(
|
||||||
agent: dict,
|
agent: dict,
|
||||||
*,
|
*,
|
||||||
@@ -771,14 +795,10 @@ class UpdateAgent(Resource):
|
|||||||
try:
|
try:
|
||||||
data[field] = json.loads(data[field])
|
data[field] = json.loads(data[field])
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
f"Invalid JSON format for field: {field}",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": f"Invalid JSON format for field: {field}",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
if data.get("json_schema") == "":
|
if data.get("json_schema") == "":
|
||||||
data["json_schema"] = None
|
data["json_schema"] = None
|
||||||
@@ -855,14 +875,10 @@ class UpdateAgent(Resource):
|
|||||||
if field == "status":
|
if field == "status":
|
||||||
new_status = data.get("status")
|
new_status = data.get("status")
|
||||||
if new_status not in ["draft", "published"]:
|
if new_status not in ["draft", "published"]:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Invalid status value. Must be 'draft' or 'published'",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": "Invalid status value. Must be 'draft' or 'published'",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
update_fields["status"] = new_status
|
update_fields["status"] = new_status
|
||||||
elif field == "source":
|
elif field == "source":
|
||||||
@@ -872,14 +888,8 @@ class UpdateAgent(Resource):
|
|||||||
elif looks_like_uuid(source_id):
|
elif looks_like_uuid(source_id):
|
||||||
update_fields["source_id"] = source_id
|
update_fields["source_id"] = source_id
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
f"Invalid source ID format: {source_id}", user, field
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": f"Invalid source ID format: {source_id}",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
elif field == "sources":
|
elif field == "sources":
|
||||||
sources_list = data.get("sources", []) or []
|
sources_list = data.get("sources", []) or []
|
||||||
@@ -893,14 +903,8 @@ class UpdateAgent(Resource):
|
|||||||
if looks_like_uuid(src):
|
if looks_like_uuid(src):
|
||||||
valid.append(src)
|
valid.append(src)
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
f"Invalid source ID in list: {src}", user, field
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": f"Invalid source ID in list: {src}",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
update_fields["extra_source_ids"] = valid
|
update_fields["extra_source_ids"] = valid
|
||||||
elif field == "chunks":
|
elif field == "chunks":
|
||||||
@@ -911,33 +915,20 @@ class UpdateAgent(Resource):
|
|||||||
try:
|
try:
|
||||||
chunks_int = int(chunks_value)
|
chunks_int = int(chunks_value)
|
||||||
if chunks_int < 0:
|
if chunks_int < 0:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Chunks value must be a non-negative integer",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": "Chunks value must be a non-negative integer",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
update_fields["chunks"] = chunks_int
|
update_fields["chunks"] = chunks_int
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
f"Invalid chunks value: {chunks_value}", user, field
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": f"Invalid chunks value: {chunks_value}",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
elif field == "tools":
|
elif field == "tools":
|
||||||
tools_list = data.get("tools", [])
|
tools_list = data.get("tools", [])
|
||||||
if not isinstance(tools_list, list):
|
if not isinstance(tools_list, list):
|
||||||
return make_response(
|
return _reject("Tools must be a list", user, field)
|
||||||
jsonify({"success": False, "message": "Tools must be a list"}),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
update_fields["tools"] = tools_list
|
update_fields["tools"] = tools_list
|
||||||
elif field == "json_schema":
|
elif field == "json_schema":
|
||||||
json_schema = data.get("json_schema")
|
json_schema = data.get("json_schema")
|
||||||
@@ -947,10 +938,7 @@ class UpdateAgent(Resource):
|
|||||||
json_schema
|
json_schema
|
||||||
)
|
)
|
||||||
except JsonSchemaValidationError:
|
except JsonSchemaValidationError:
|
||||||
return make_response(
|
return _reject("Invalid JSON schema", user, field)
|
||||||
jsonify({"success": False, "message": "Invalid JSON schema"}),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
update_fields["json_schema"] = None
|
update_fields["json_schema"] = None
|
||||||
elif field == "limited_token_mode":
|
elif field == "limited_token_mode":
|
||||||
@@ -962,14 +950,10 @@ class UpdateAgent(Resource):
|
|||||||
)
|
)
|
||||||
update_fields["limited_token_mode"] = bool_value
|
update_fields["limited_token_mode"] = bool_value
|
||||||
if bool_value and data.get("token_limit") is None:
|
if bool_value and data.get("token_limit") is None:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Token limit must be provided when limited token mode is enabled",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": "Token limit must be provided when limited token mode is enabled",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
elif field == "limited_request_mode":
|
elif field == "limited_request_mode":
|
||||||
raw_value = data.get("limited_request_mode", False)
|
raw_value = data.get("limited_request_mode", False)
|
||||||
@@ -980,40 +964,34 @@ class UpdateAgent(Resource):
|
|||||||
)
|
)
|
||||||
update_fields["limited_request_mode"] = bool_value
|
update_fields["limited_request_mode"] = bool_value
|
||||||
if bool_value and data.get("request_limit") is None:
|
if bool_value and data.get("request_limit") is None:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Request limit must be provided when limited request mode is enabled",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": "Request limit must be provided when limited request mode is enabled",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
elif field == "token_limit":
|
elif field == "token_limit":
|
||||||
token_limit = data.get("token_limit")
|
token_limit = data.get("token_limit")
|
||||||
update_fields["token_limit"] = int(token_limit) if token_limit else 0
|
update_fields["token_limit"] = int(token_limit) if token_limit else 0
|
||||||
|
# NOTE: unreachable from a multipart/form submit. ``data``
|
||||||
|
# then comes from ``request.form.to_dict()``, so this is
|
||||||
|
# the *string* "False" and ``not "False"`` is False. Left
|
||||||
|
# as-is deliberately: tightening it here would start
|
||||||
|
# rejecting form payloads that currently succeed.
|
||||||
if update_fields["token_limit"] > 0 and not data.get("limited_token_mode"):
|
if update_fields["token_limit"] > 0 and not data.get("limited_token_mode"):
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Token limit cannot be set when limited token mode is disabled",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": "Token limit cannot be set when limited token mode is disabled",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
elif field == "request_limit":
|
elif field == "request_limit":
|
||||||
request_limit = data.get("request_limit")
|
request_limit = data.get("request_limit")
|
||||||
update_fields["request_limit"] = int(request_limit) if request_limit else 0
|
update_fields["request_limit"] = int(request_limit) if request_limit else 0
|
||||||
|
# Same string-truthiness caveat as ``token_limit`` above.
|
||||||
if update_fields["request_limit"] > 0 and not data.get("limited_request_mode"):
|
if update_fields["request_limit"] > 0 and not data.get("limited_request_mode"):
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Request limit cannot be set when limited request mode is disabled",
|
||||||
{
|
user,
|
||||||
"success": False,
|
field,
|
||||||
"message": "Request limit cannot be set when limited request mode is disabled",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
elif field == "folder_id":
|
elif field == "folder_id":
|
||||||
folder_input = data.get("folder_id")
|
folder_input = data.get("folder_id")
|
||||||
@@ -1036,10 +1014,7 @@ class UpdateAgent(Resource):
|
|||||||
normalized = normalize_workflow_reference(workflow_input)
|
normalized = normalize_workflow_reference(workflow_input)
|
||||||
if not normalized:
|
if not normalized:
|
||||||
if workflow_required:
|
if workflow_required:
|
||||||
return make_response(
|
return _reject("Workflow is required", user, field)
|
||||||
jsonify({"success": False, "message": "Workflow is required"}),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
update_fields["workflow_id"] = None
|
update_fields["workflow_id"] = None
|
||||||
else:
|
else:
|
||||||
pg_workflow_id, wf_err = _resolve_workflow_for_user(
|
pg_workflow_id, wf_err = _resolve_workflow_for_user(
|
||||||
@@ -1055,12 +1030,7 @@ class UpdateAgent(Resource):
|
|||||||
elif looks_like_uuid(value):
|
elif looks_like_uuid(value):
|
||||||
update_fields["prompt_id"] = value
|
update_fields["prompt_id"] = value
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return _reject(f"Invalid prompt_id: {value}", user, field)
|
||||||
jsonify(
|
|
||||||
{"success": False, "message": f"Invalid prompt_id: {value}"}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
elif field == "allow_system_prompt_override":
|
elif field == "allow_system_prompt_override":
|
||||||
raw_value = data.get("allow_system_prompt_override", False)
|
raw_value = data.get("allow_system_prompt_override", False)
|
||||||
update_fields["allow_system_prompt_override"] = (
|
update_fields["allow_system_prompt_override"] = (
|
||||||
@@ -1072,28 +1042,14 @@ class UpdateAgent(Resource):
|
|||||||
value = data[field]
|
value = data[field]
|
||||||
if field in ["name", "description", "agent_type"]:
|
if field in ["name", "description", "agent_type"]:
|
||||||
if not value or not str(value).strip():
|
if not value or not str(value).strip():
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
f"Field '{field}' cannot be empty", user, field
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": f"Field '{field}' cannot be empty",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
update_fields[field] = value
|
update_fields[field] = value
|
||||||
if image_url:
|
if image_url:
|
||||||
update_fields["image"] = image_url
|
update_fields["image"] = image_url
|
||||||
if not update_fields:
|
if not update_fields:
|
||||||
return make_response(
|
return _reject("No valid update data provided", user)
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "No valid update data provided",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
newly_generated_key = None
|
newly_generated_key = None
|
||||||
final_status = update_fields.get("status", existing_agent.get("status"))
|
final_status = update_fields.get("status", existing_agent.get("status"))
|
||||||
@@ -1112,14 +1068,11 @@ class UpdateAgent(Resource):
|
|||||||
if not workflow_final:
|
if not workflow_final:
|
||||||
missing_published_fields.append("Workflow")
|
missing_published_fields.append("Workflow")
|
||||||
if missing_published_fields:
|
if missing_published_fields:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Cannot publish workflow agent. Missing required "
|
||||||
{
|
f"fields: {', '.join(missing_published_fields)}",
|
||||||
"success": False,
|
user,
|
||||||
"message": f"Cannot publish workflow agent. Missing required fields: {', '.join(missing_published_fields)}",
|
",".join(missing_published_fields),
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# ``prompt_id`` is intentionally omitted: the
|
# ``prompt_id`` is intentionally omitted: the
|
||||||
@@ -1163,14 +1116,11 @@ class UpdateAgent(Resource):
|
|||||||
):
|
):
|
||||||
missing_published_fields.append("Source or retriever")
|
missing_published_fields.append("Source or retriever")
|
||||||
if missing_published_fields:
|
if missing_published_fields:
|
||||||
return make_response(
|
return _reject(
|
||||||
jsonify(
|
"Cannot publish agent. Missing or invalid required "
|
||||||
{
|
f"fields: {', '.join(missing_published_fields)}",
|
||||||
"success": False,
|
user,
|
||||||
"message": f"Cannot publish agent. Missing or invalid required fields: {', '.join(missing_published_fields)}",
|
",".join(missing_published_fields),
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
)
|
||||||
if not existing_agent.get("key"):
|
if not existing_agent.get("key"):
|
||||||
newly_generated_key = str(uuid.uuid4())
|
newly_generated_key = str(uuid.uuid4())
|
||||||
|
|||||||
@@ -62,6 +62,29 @@ import WorkflowBuilder from './workflow/WorkflowBuilder';
|
|||||||
|
|
||||||
import type { Model } from '../models/types';
|
import type { Model } from '../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull the backend's own explanation out of a failed response.
|
||||||
|
*
|
||||||
|
* The agent write endpoints answer a rejected save with
|
||||||
|
* `{"success": false, "message": "<why>"}` — e.g. "Invalid chunks value: …"
|
||||||
|
* or "Field 'description' cannot be empty". Callers used to test only
|
||||||
|
* `response.ok` and throw a fixed string, so the one piece of information
|
||||||
|
* that could tell the user what to change was dropped on the floor.
|
||||||
|
*/
|
||||||
|
const extractApiError = async (
|
||||||
|
response: Response,
|
||||||
|
fallback: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const body = await response.json();
|
||||||
|
if (typeof body?.message === 'string' && body.message.trim())
|
||||||
|
return body.message;
|
||||||
|
} catch {
|
||||||
|
// Non-JSON body (proxy HTML error page, empty 502) — use the fallback.
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -126,6 +149,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [draftLoading, setDraftLoading] = useState(false);
|
const [draftLoading, setDraftLoading] = useState(false);
|
||||||
const [publishLoading, setPublishLoading] = useState(false);
|
const [publishLoading, setPublishLoading] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
const [jsonSchemaText, setJsonSchemaText] = useState('');
|
const [jsonSchemaText, setJsonSchemaText] = useState('');
|
||||||
const [jsonSchemaValid, setJsonSchemaValid] = useState(true);
|
const [jsonSchemaValid, setJsonSchemaValid] = useState(true);
|
||||||
const [isAdvancedSectionExpanded, setIsAdvancedSectionExpanded] =
|
const [isAdvancedSectionExpanded, setIsAdvancedSectionExpanded] =
|
||||||
@@ -312,11 +336,20 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setDraftLoading(true);
|
setDraftLoading(true);
|
||||||
|
setSubmitError(null);
|
||||||
const response =
|
const response =
|
||||||
effectiveMode === 'new'
|
effectiveMode === 'new'
|
||||||
? await userService.createAgent(formData, token)
|
? await userService.createAgent(formData, token)
|
||||||
: await userService.updateAgent(agent.id || '', formData, token);
|
: await userService.updateAgent(agent.id || '', formData, token);
|
||||||
if (!response.ok) throw new Error('Failed to create agent draft');
|
if (!response.ok) {
|
||||||
|
setSubmitError(
|
||||||
|
await extractApiError(
|
||||||
|
response,
|
||||||
|
t('agents.form.errors.saveDraftFailed'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const updatedAgent = {
|
const updatedAgent = {
|
||||||
@@ -329,7 +362,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
if (effectiveMode === 'new') setEffectiveMode('draft');
|
if (effectiveMode === 'new') setEffectiveMode('draft');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving draft:', error);
|
console.error('Error saving draft:', error);
|
||||||
throw new Error('Failed to save draft');
|
setSubmitError(t('agents.form.errors.saveDraftFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setDraftLoading(false);
|
setDraftLoading(false);
|
||||||
}
|
}
|
||||||
@@ -427,11 +460,20 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setPublishLoading(true);
|
setPublishLoading(true);
|
||||||
|
setSubmitError(null);
|
||||||
const response =
|
const response =
|
||||||
effectiveMode === 'new'
|
effectiveMode === 'new'
|
||||||
? await userService.createAgent(formData, token)
|
? await userService.createAgent(formData, token)
|
||||||
: await userService.updateAgent(agent.id || '', formData, token);
|
: await userService.updateAgent(agent.id || '', formData, token);
|
||||||
if (!response.ok) throw new Error('Failed to publish agent');
|
if (!response.ok) {
|
||||||
|
setSubmitError(
|
||||||
|
await extractApiError(
|
||||||
|
response,
|
||||||
|
t('agents.form.errors.publishFailed'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const updatedAgent = {
|
const updatedAgent = {
|
||||||
@@ -451,7 +493,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
setImageFile(null);
|
setImageFile(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing agent:', error);
|
console.error('Error publishing agent:', error);
|
||||||
throw new Error('Failed to publish agent');
|
setSubmitError(t('agents.form.errors.publishFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setPublishLoading(false);
|
setPublishLoading(false);
|
||||||
}
|
}
|
||||||
@@ -812,6 +854,15 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<span aria-hidden />
|
<span aria-hidden />
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{submitError && (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400"
|
||||||
|
>
|
||||||
|
<span className="h-4 w-4 shrink-0 bg-[url('/src/assets/circle-x.svg')] bg-contain bg-center bg-no-repeat" />
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1249,7 +1249,11 @@
|
|||||||
"description": "Das Löschen dieses Agenten ist endgültig und entfernt alle seine Zeitpläne und den Ausführungsverlauf.",
|
"description": "Das Löschen dieses Agenten ist endgültig und entfernt alle seine Zeitpläne und den Ausführungsverlauf.",
|
||||||
"deleteButton": "Agent löschen"
|
"deleteButton": "Agent löschen"
|
||||||
},
|
},
|
||||||
"externalKb": "Externe KB"
|
"externalKb": "Externe KB",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "Der Agent konnte nicht veröffentlicht werden. Bitte erneut versuchen.",
|
||||||
|
"saveDraftFailed": "Der Entwurf konnte nicht gespeichert werden. Bitte erneut versuchen."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Agenten-Protokolle",
|
"title": "Agenten-Protokolle",
|
||||||
|
|||||||
@@ -1250,7 +1250,11 @@
|
|||||||
"description": "Deleting this agent is permanent and will remove all its schedules and run history.",
|
"description": "Deleting this agent is permanent and will remove all its schedules and run history.",
|
||||||
"deleteButton": "Delete agent"
|
"deleteButton": "Delete agent"
|
||||||
},
|
},
|
||||||
"externalKb": "External KB"
|
"externalKb": "External KB",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "Could not publish the agent. Please try again.",
|
||||||
|
"saveDraftFailed": "Could not save the draft. Please try again."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Agent Logs",
|
"title": "Agent Logs",
|
||||||
|
|||||||
@@ -1249,7 +1249,11 @@
|
|||||||
"description": "Eliminar este agente es permanente y eliminará todas sus programaciones e historial de ejecuciones.",
|
"description": "Eliminar este agente es permanente y eliminará todas sus programaciones e historial de ejecuciones.",
|
||||||
"deleteButton": "Eliminar agente"
|
"deleteButton": "Eliminar agente"
|
||||||
},
|
},
|
||||||
"externalKb": "KB Externa"
|
"externalKb": "KB Externa",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "No se pudo publicar el agente. Inténtalo de nuevo.",
|
||||||
|
"saveDraftFailed": "No se pudo guardar el borrador. Inténtalo de nuevo."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Registros del Agente",
|
"title": "Registros del Agente",
|
||||||
|
|||||||
@@ -1249,7 +1249,11 @@
|
|||||||
"description": "このエージェントを削除すると元に戻せません。すべてのスケジュールと実行履歴も削除されます。",
|
"description": "このエージェントを削除すると元に戻せません。すべてのスケジュールと実行履歴も削除されます。",
|
||||||
"deleteButton": "エージェントを削除"
|
"deleteButton": "エージェントを削除"
|
||||||
},
|
},
|
||||||
"externalKb": "外部KB"
|
"externalKb": "外部KB",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "エージェントを公開できませんでした。もう一度お試しください。",
|
||||||
|
"saveDraftFailed": "下書きを保存できませんでした。もう一度お試しください。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "エージェントログ",
|
"title": "エージェントログ",
|
||||||
|
|||||||
@@ -1249,7 +1249,11 @@
|
|||||||
"description": "Удаление этого агента необратимо и удалит все его расписания и историю запусков.",
|
"description": "Удаление этого агента необратимо и удалит все его расписания и историю запусков.",
|
||||||
"deleteButton": "Удалить агента"
|
"deleteButton": "Удалить агента"
|
||||||
},
|
},
|
||||||
"externalKb": "Внешняя БЗ"
|
"externalKb": "Внешняя БЗ",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "Не удалось опубликовать агента. Попробуйте ещё раз.",
|
||||||
|
"saveDraftFailed": "Не удалось сохранить черновик. Попробуйте ещё раз."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Логи Агента",
|
"title": "Логи Агента",
|
||||||
|
|||||||
@@ -1249,7 +1249,11 @@
|
|||||||
"description": "刪除此代理將無法復原,並會移除其所有排程和執行歷史。",
|
"description": "刪除此代理將無法復原,並會移除其所有排程和執行歷史。",
|
||||||
"deleteButton": "刪除代理"
|
"deleteButton": "刪除代理"
|
||||||
},
|
},
|
||||||
"externalKb": "外部知識庫"
|
"externalKb": "外部知識庫",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "無法發布該助理,請重試。",
|
||||||
|
"saveDraftFailed": "無法儲存草稿,請重試。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "代理日誌",
|
"title": "代理日誌",
|
||||||
|
|||||||
@@ -1249,7 +1249,11 @@
|
|||||||
"description": "删除此代理将无法恢复,并会移除其所有计划和运行历史。",
|
"description": "删除此代理将无法恢复,并会移除其所有计划和运行历史。",
|
||||||
"deleteButton": "删除代理"
|
"deleteButton": "删除代理"
|
||||||
},
|
},
|
||||||
"externalKb": "外部知识库"
|
"externalKb": "外部知识库",
|
||||||
|
"errors": {
|
||||||
|
"publishFailed": "无法发布该助手,请重试。",
|
||||||
|
"saveDraftFailed": "无法保存草稿,请重试。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "代理日志",
|
"title": "代理日志",
|
||||||
|
|||||||
@@ -534,6 +534,56 @@ class TestUpdateAgent:
|
|||||||
response = UpdateAgent().put(str(agent["id"]))
|
response = UpdateAgent().put(str(agent["id"]))
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"payload,field",
|
||||||
|
[
|
||||||
|
({"status": "bogus"}, "status"),
|
||||||
|
({"source": "not-a-uuid"}, "source"),
|
||||||
|
({"sources": ["not-uuid"]}, "sources"),
|
||||||
|
({"chunks": -1}, "chunks"),
|
||||||
|
({"chunks": "not-an-int"}, "chunks"),
|
||||||
|
({"tools": "not-a-list"}, "tools"),
|
||||||
|
({"prompt_id": "not-a-uuid"}, "prompt_id"),
|
||||||
|
({"description": " "}, "description"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validation_rejection_is_logged(
|
||||||
|
self, app, pg_conn, caplog, payload, field
|
||||||
|
):
|
||||||
|
"""Every 400 must leave a WARN naming the field and the user.
|
||||||
|
|
||||||
|
Regression test for the silent-publish-failure bug: the route used to
|
||||||
|
``make_response(..., 400)`` without logging, so a rejected publish left
|
||||||
|
no server-side trace at all — the only evidence it happened was the
|
||||||
|
OTel span's status code. Combined with the frontend discarding the
|
||||||
|
response body, that made a deterministic validation failure impossible
|
||||||
|
to diagnose from any telemetry we keep.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from application.api.user.agents.routes import UpdateAgent
|
||||||
|
|
||||||
|
user = f"u-log-{field}"
|
||||||
|
agent = _seed_agent(pg_conn, user=user)
|
||||||
|
body = {"name": "n", "description": "d", "status": "draft", **payload}
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING), _patch_db(
|
||||||
|
pg_conn
|
||||||
|
), app.test_request_context(
|
||||||
|
f"/api/update_agent/{agent['id']}", method="PUT", json=body,
|
||||||
|
):
|
||||||
|
from flask import request
|
||||||
|
request.decoded_token = {"sub": user}
|
||||||
|
response = UpdateAgent().put(str(agent["id"]))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
warnings = [
|
||||||
|
r.getMessage() for r in caplog.records if r.levelno >= logging.WARNING
|
||||||
|
]
|
||||||
|
assert any(
|
||||||
|
field in msg and user in msg for msg in warnings
|
||||||
|
), f"no WARN naming field={field!r} and user={user!r}; got {warnings!r}"
|
||||||
|
|
||||||
def test_invalid_chunks_returns_400(self, app, pg_conn):
|
def test_invalid_chunks_returns_400(self, app, pg_conn):
|
||||||
from application.api.user.agents.routes import UpdateAgent
|
from application.api.user.agents.routes import UpdateAgent
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user