mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-12 22:20:33 +00:00
fix(gateway): name the agent problem when run admission rejects agent_name (#8087)
* fix(gateway): name the agent problem when run admission rejects agent_name Since #5238, run admission loads the named custom agent's config for every run, with or without a knowledge scope, and folded every failure into "knowledge_scope assistant configuration could not be resolved". Messaging a chat whose custom agent was deleted, or passing a malformed agent_name, pointed callers at the knowledge base instead of the agent. The 422 status is unchanged; the detail now says which problem it is: - Agent '<name>' not found - Invalid agent_name '<value>': must contain only letters, digits, and hyphens after normalization. (shared with custom_agent_name(), which gains a field= argument; existing callers keep the default) - Invalid agent_name: expected a string, got <type>. - Agent '<name>' configuration could not be loaded * docs(changelog): reference #8087 for the run admission agent_name detail * fix(gateway): bound the agent name echoed in run admission errors Address review on #8087: admission now echoes the caller's raw agent_name, which start_run merges from config/context without a size check, so an oversized value was reflected verbatim into the 422 detail and any log line that records it. Cap the echoed name at 200 characters in the invalid-name message (shared with assistant_id validation) and in the not-found / unreadable-config messages. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
eb97327ddf
commit
53c5195e5d
@ -390,6 +390,13 @@ This section accumulates work toward the **2.3.0** milestone
|
||||
there. Retrying in a completed, failed, running or paused batch is
|
||||
unchanged. ([#7792])
|
||||
|
||||
- **gateway:** A run that names a missing or invalid custom agent now says so.
|
||||
Messaging a chat whose custom agent was deleted, or passing a malformed
|
||||
`agent_name`, returned the 422 detail "knowledge_scope assistant configuration
|
||||
could not be resolved" even when no knowledge scope was involved. The 422 now
|
||||
reports `Agent '<name>' not found`, an invalid `agent_name`, or an agent
|
||||
config that could not be loaded. ([#8087])
|
||||
|
||||
- **agents:** `PUT /api/agents/{name}` with `"description": null` now resets the
|
||||
description to empty. It used to store the null, which the agent config
|
||||
rejects, so the agent then failed every load: the update, GET and repair
|
||||
@ -16393,6 +16400,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#8071]: https://github.com/bytedance/deer-flow/issues/8071
|
||||
[#8072]: https://github.com/bytedance/deer-flow/pull/8072
|
||||
[#8079]: https://github.com/bytedance/deer-flow/pull/8079
|
||||
[#8087]: https://github.com/bytedance/deer-flow/pull/8087
|
||||
[#8117]: https://github.com/bytedance/deer-flow/pull/8117
|
||||
[#8118]: https://github.com/bytedance/deer-flow/pull/8118
|
||||
[#8126]: https://github.com/bytedance/deer-flow/pull/8126
|
||||
|
||||
@ -153,6 +153,11 @@
|
||||
为这些条目显示重试按钮。在已完成、失败、运行中或已暂停批次中重试的行为不变。
|
||||
([#7792])
|
||||
|
||||
- **网关:** 运行指定的自定义智能体不存在或名称无效时,现在会明确说明原因。此前向一个
|
||||
自定义智能体已被删除的对话发消息,或传入格式错误的 `agent_name`,即使与知识库范围无关,
|
||||
也会返回 422 和 "knowledge_scope assistant configuration could not be resolved"。现在
|
||||
422 会提示 `Agent '<name>' not found`、`agent_name` 无效,或智能体配置无法加载。([#8087])
|
||||
|
||||
- **智能体:** `PUT /api/agents/{name}` 传入 `"description": null` 时现在会把描述重置为
|
||||
空。此前会把 null 原样保存,而智能体配置不接受该值,导致之后每次加载都失败:更新本身、
|
||||
GET 和修复性更新都返回 500,文件后端会把它从智能体列表中漏掉,数据库后端会让该用户的
|
||||
@ -12889,6 +12894,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#8043]: https://github.com/bytedance/deer-flow/pull/8043
|
||||
[#8046]: https://github.com/bytedance/deer-flow/pull/8046
|
||||
[#8064]: https://github.com/bytedance/deer-flow/pull/8064
|
||||
[#8087]: https://github.com/bytedance/deer-flow/pull/8087
|
||||
[#8118]: https://github.com/bytedance/deer-flow/pull/8118
|
||||
[#8126]: https://github.com/bytedance/deer-flow/pull/8126
|
||||
[#8139]: https://github.com/bytedance/deer-flow/pull/8139
|
||||
|
||||
@ -1453,11 +1453,17 @@ def _clamp_recursion_limit(value: Any, max_limit: int, default_limit: int) -> in
|
||||
return min(value, max_limit)
|
||||
|
||||
|
||||
def custom_agent_name(assistant_id: str | None) -> str | None:
|
||||
# Error details echo caller-supplied agent names; bound them so an oversized
|
||||
# value cannot bloat the response or any log line that records the detail.
|
||||
_MAX_ECHOED_AGENT_NAME_CHARS = 200
|
||||
|
||||
|
||||
def custom_agent_name(assistant_id: str | None, *, field: str = "assistant_id") -> str | None:
|
||||
"""Return the custom agent *assistant_id* selects, or ``None`` for the lead agent.
|
||||
|
||||
Custom names are normalized (trimmed, lowercased, ``_`` to ``-``) and must
|
||||
then contain only letters, digits and hyphens.
|
||||
then contain only letters, digits and hyphens. *field* names the request
|
||||
field in the error message.
|
||||
|
||||
Raises:
|
||||
ValueError: *assistant_id* cannot name an agent.
|
||||
@ -1466,7 +1472,7 @@ def custom_agent_name(assistant_id: str | None) -> str | None:
|
||||
return None
|
||||
normalized = assistant_id.strip().lower().replace("_", "-")
|
||||
if not normalized or not re.fullmatch(r"[a-z0-9-]+", normalized):
|
||||
raise ValueError(f"Invalid assistant_id {assistant_id!r}: must contain only letters, digits, and hyphens after normalization.")
|
||||
raise ValueError(f"Invalid {field} {assistant_id!r:.{_MAX_ECHOED_AGENT_NAME_CHARS}}: must contain only letters, digits, and hyphens after normalization.")
|
||||
return normalized
|
||||
|
||||
|
||||
@ -2199,20 +2205,30 @@ async def _load_scope_agent_config(
|
||||
) -> Any | None:
|
||||
if not assistant_id or assistant_id == _DEFAULT_ASSISTANT_ID:
|
||||
return None
|
||||
# The caller's config/context ``agent_name`` arrives here unvalidated, and
|
||||
# every run that names a custom agent passes through, with or without a
|
||||
# knowledge scope, so each rejection says what is wrong with the agent.
|
||||
if not isinstance(assistant_id, str):
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Invalid agent_name: expected a string, got {type(assistant_id).__name__}.",
|
||||
)
|
||||
try:
|
||||
normalized = custom_agent_name(assistant_id, field="agent_name")
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
try:
|
||||
# The caller's config/context ``agent_name`` arrives here unvalidated.
|
||||
if not isinstance(assistant_id, str):
|
||||
raise ValueError("agent_name must be a string")
|
||||
normalized = assistant_id.strip().lower().replace("_", "-")
|
||||
return await asyncio.to_thread(
|
||||
load_agent_config,
|
||||
normalized,
|
||||
user_id=user_id,
|
||||
)
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=422, detail=f"Agent '{normalized:.{_MAX_ECHOED_AGENT_NAME_CHARS}}' not found") from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="knowledge_scope assistant configuration could not be resolved",
|
||||
detail=f"Agent '{normalized:.{_MAX_ECHOED_AGENT_NAME_CHARS}}' configuration could not be loaded",
|
||||
) from exc
|
||||
|
||||
|
||||
|
||||
@ -210,3 +210,103 @@ def test_free_form_runtime_scope_fields_are_scrubbed() -> None:
|
||||
strip_internal_context_keys(config)
|
||||
|
||||
assert config == {"context": {}, "configurable": {}}
|
||||
|
||||
|
||||
# -- agent config loading during run admission ------------------------------
|
||||
# Every run that names a custom agent loads its config here, with or without a
|
||||
# knowledge scope, so the 422 detail must say what is wrong with the agent.
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_scope_agent_lookup_reports_a_missing_agent(monkeypatch) -> None:
|
||||
from app.gateway import services
|
||||
|
||||
def _missing(name, *, user_id=None):
|
||||
raise FileNotFoundError(name)
|
||||
|
||||
monkeypatch.setattr(services, "load_agent_config", _missing)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await services._load_scope_agent_config(assistant_id="Ghost_Agent", user_id="u1")
|
||||
|
||||
assert excinfo.value.status_code == 422
|
||||
assert excinfo.value.detail == "Agent 'ghost-agent' not found"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_scope_agent_lookup_reports_an_unreadable_agent_config(monkeypatch) -> None:
|
||||
from app.gateway import services
|
||||
|
||||
def _corrupt(name, *, user_id=None):
|
||||
raise ValueError("bad yaml")
|
||||
|
||||
monkeypatch.setattr(services, "load_agent_config", _corrupt)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await services._load_scope_agent_config(assistant_id="broken-agent", user_id="u1")
|
||||
|
||||
assert excinfo.value.status_code == 422
|
||||
assert excinfo.value.detail == "Agent 'broken-agent' configuration could not be loaded"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("agent_name", "detail"),
|
||||
[
|
||||
("Bad Name!", "Invalid agent_name 'Bad Name!': must contain only letters, digits, and hyphens after normalization."),
|
||||
(" ", "Invalid agent_name ' ': must contain only letters, digits, and hyphens after normalization."),
|
||||
(1, "Invalid agent_name: expected a string, got int."),
|
||||
],
|
||||
)
|
||||
async def test_scope_agent_lookup_rejects_an_invalid_agent_name_without_loading(monkeypatch, agent_name, detail) -> None:
|
||||
from app.gateway import services
|
||||
|
||||
def _unexpected(name, *, user_id=None):
|
||||
raise AssertionError("an invalid agent name must not reach the agent store")
|
||||
|
||||
monkeypatch.setattr(services, "load_agent_config", _unexpected)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await services._load_scope_agent_config(assistant_id=agent_name, user_id="u1")
|
||||
|
||||
assert excinfo.value.status_code == 422
|
||||
assert excinfo.value.detail == detail
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_scope_agent_lookup_bounds_an_oversized_invalid_agent_name(monkeypatch) -> None:
|
||||
"""Admission echoes the caller's raw ``agent_name``; a huge value must not bloat the 422 or logs."""
|
||||
from app.gateway import services
|
||||
|
||||
def _unexpected(name, *, user_id=None):
|
||||
raise AssertionError("an invalid agent name must not reach the agent store")
|
||||
|
||||
monkeypatch.setattr(services, "load_agent_config", _unexpected)
|
||||
oversized = "Bad Name!" + "x" * 100_000
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await services._load_scope_agent_config(assistant_id=oversized, user_id="u1")
|
||||
|
||||
assert excinfo.value.status_code == 422
|
||||
assert excinfo.value.detail.startswith("Invalid agent_name 'Bad Name!xxx")
|
||||
assert excinfo.value.detail.endswith(": must contain only letters, digits, and hyphens after normalization.")
|
||||
assert len(excinfo.value.detail) < 400
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_scope_agent_lookup_bounds_an_oversized_missing_agent_name(monkeypatch) -> None:
|
||||
"""A syntactically valid name has no length cap either, so the not-found detail is bounded too."""
|
||||
from app.gateway import services
|
||||
|
||||
def _missing(name, *, user_id=None):
|
||||
raise FileNotFoundError(name)
|
||||
|
||||
monkeypatch.setattr(services, "load_agent_config", _missing)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await services._load_scope_agent_config(assistant_id="a" * 100_000, user_id="u1")
|
||||
|
||||
assert excinfo.value.status_code == 422
|
||||
assert excinfo.value.detail.startswith("Agent 'aaa")
|
||||
assert excinfo.value.detail.endswith("' not found")
|
||||
assert len(excinfo.value.detail) < 400
|
||||
|
||||
@ -569,7 +569,7 @@ def test_stream_run_completes_and_persists_runtime_state(isolated_app):
|
||||
|
||||
|
||||
_CONFIG_SECTION_ERROR = "invalid_run_config_section"
|
||||
_UNRESOLVED_AGENT_DETAIL = "knowledge_scope assistant configuration could not be resolved"
|
||||
_NON_STRING_AGENT_DETAIL = "Invalid agent_name: expected a string, got int."
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -592,9 +592,10 @@ _UNRESOLVED_AGENT_DETAIL = "knowledge_scope assistant configuration could not be
|
||||
"Invalid assistant_id ' ': must contain only letters, digits, and hyphens after normalization.",
|
||||
id="assistant-id-blank",
|
||||
),
|
||||
pytest.param({"config": {"configurable": {"agent_name": 1}}}, 422, _UNRESOLVED_AGENT_DETAIL, id="configurable-agent-name"),
|
||||
pytest.param({"config": {"context": {"agent_name": 1}}}, 422, _UNRESOLVED_AGENT_DETAIL, id="config-context-agent-name"),
|
||||
pytest.param({"context": {"agent_name": 1}}, 422, _UNRESOLVED_AGENT_DETAIL, id="context-agent-name"),
|
||||
pytest.param({"config": {"configurable": {"agent_name": 1}}}, 422, _NON_STRING_AGENT_DETAIL, id="configurable-agent-name"),
|
||||
pytest.param({"config": {"context": {"agent_name": 1}}}, 422, _NON_STRING_AGENT_DETAIL, id="config-context-agent-name"),
|
||||
pytest.param({"context": {"agent_name": 1}}, 422, _NON_STRING_AGENT_DETAIL, id="context-agent-name"),
|
||||
pytest.param({"context": {"agent_name": "ghost-agent"}}, 422, "Agent 'ghost-agent' not found", id="context-agent-missing"),
|
||||
pytest.param(
|
||||
{"input": {"messages": [{"role": "user", "content": "hi"}, {"role": "ai", "content": "", "tool_calls": "x"}]}},
|
||||
400,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user