mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
chore(agent-v2): sync daily changes (#38298)
Signed-off-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: 盐粒 Yanli <mail@yanli.one> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: zyssyz123 <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: 林玮 (Jade Lin) <linw1995@icloud.com>
This commit is contained in:
co-authored by
盐粒 Yanli
Joel
zyssyz123
autofix-ci[bot]
盐粒 Yanli
林玮
parent
f8d47616c1
commit
1b3e8e9943
@@ -91,33 +91,31 @@ class AgentIdPath(BaseModel):
|
||||
class AgentAppCreatePayload(BaseModel):
|
||||
name: str = Field(..., min_length=1, description="Agent name")
|
||||
description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400)
|
||||
role: str = Field(..., min_length=1, description="Agent role", max_length=255)
|
||||
role: str | None = Field(default=None, description="Agent role", max_length=255)
|
||||
icon_type: IconType | None = Field(default=None, description="Icon type")
|
||||
icon: str | None = Field(default=None, description="Icon")
|
||||
icon_background: str | None = Field(default=None, description="Icon background color")
|
||||
|
||||
@field_validator("role")
|
||||
@classmethod
|
||||
def validate_role(cls, value: str) -> str:
|
||||
role = value.strip()
|
||||
if not role:
|
||||
raise ValueError("Agent role is required.")
|
||||
return role
|
||||
def validate_role(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.strip()
|
||||
|
||||
|
||||
# Keep agent-app roster DTOs agent-specific instead of reusing the shared
|
||||
# /apps response/request models. The roster surface needs Agent-only fields such
|
||||
# as `role`, while the generic console/apps contracts must stay unchanged.
|
||||
class AgentAppUpdatePayload(GenericUpdateAppPayload):
|
||||
role: str = Field(..., min_length=1, description="Agent role", max_length=255)
|
||||
role: str | None = Field(default=None, description="Agent role", max_length=255)
|
||||
|
||||
@field_validator("role")
|
||||
@classmethod
|
||||
def validate_role(cls, value: str) -> str:
|
||||
role = value.strip()
|
||||
if not role:
|
||||
raise ValueError("Agent role is required.")
|
||||
return role
|
||||
def validate_role(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.strip()
|
||||
|
||||
|
||||
class AgentAppCopyPayload(BaseModel):
|
||||
@@ -133,10 +131,7 @@ class AgentAppCopyPayload(BaseModel):
|
||||
def validate_role(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
role = value.strip()
|
||||
if not role:
|
||||
raise ValueError("Agent role is required when provided.")
|
||||
return role
|
||||
return value.strip()
|
||||
|
||||
|
||||
class AgentApiStatusPayload(BaseModel):
|
||||
@@ -531,6 +526,7 @@ class AgentAppListApi(Resource):
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
mode="agent",
|
||||
sort_by=args.sort_by,
|
||||
name=args.name,
|
||||
tag_ids=args.tag_ids,
|
||||
creator_ids=args.creator_ids,
|
||||
@@ -565,7 +561,7 @@ class AgentAppListApi(Resource):
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
mode="agent",
|
||||
agent_role=args.role,
|
||||
agent_role=args.role or "",
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from typing import Any, Literal
|
||||
from collections.abc import Generator, Iterator, Mapping
|
||||
from typing import Any, Literal, Protocol, runtime_checkable
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
@@ -60,6 +60,11 @@ from services.errors.llm import InvokeRateLimitError
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class _ClosableStream(Protocol):
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
def _resolve_debugger_chat_streaming(
|
||||
*, app_mode: AppMode, response_mode: str, response_mode_provided: bool = True
|
||||
) -> bool:
|
||||
@@ -118,15 +123,12 @@ edit workspace files, run validation or debugging commands, make exploratory che
|
||||
Use only the current Build chat message history to identify changes that need to be persisted. Do not inspect, test, or
|
||||
validate old config unless the message history already shows that the old config is invalid.
|
||||
|
||||
Persist only the build-draft config resources that need to change, using the Agent config CLI usage provided in the
|
||||
runtime prompt:
|
||||
Only update the build-draft config note when the current Build chat contains durable context that later runs need.
|
||||
Do not create, update, delete, inspect, or fill gaps in other Agent config resources, including config files, config
|
||||
skills, config env, tools, models, knowledge, or prompt settings.
|
||||
|
||||
- config files for reusable artifacts that should be available later,
|
||||
- config skills for reusable procedures or tools that should be available later,
|
||||
- config env when environment keys or values need to be recorded,
|
||||
- config note for concise durable context when useful.
|
||||
|
||||
When updating the config note, record only durable context needed by later runs, such as:
|
||||
When updating the config note with the Agent config CLI usage provided in the runtime prompt, record only durable
|
||||
context needed by later runs, such as:
|
||||
|
||||
- what you installed or configured outside the workspace for this agent,
|
||||
- where those external updates live, including CLI tools, packages, and persistent $HOME paths,
|
||||
@@ -437,7 +439,6 @@ def _drain_streaming_generate_response(response: RateLimitGenerator | Generator[
|
||||
changes the HTTP boundary: it drains the SSE stream server-side and returns
|
||||
success after the generated build-chat message reaches ``message_end``.
|
||||
"""
|
||||
close = getattr(response, "close", None)
|
||||
try:
|
||||
for chunk in response:
|
||||
for raw_event in chunk.split("\n\n"):
|
||||
@@ -467,8 +468,8 @@ def _drain_streaming_generate_response(response: RateLimitGenerator | Generator[
|
||||
if payload_event == "error":
|
||||
raise CompletionRequestError(str(payload.get("message") or "Build chat finalization failed."))
|
||||
finally:
|
||||
if callable(close):
|
||||
close()
|
||||
if isinstance(response, _ClosableStream):
|
||||
response.close()
|
||||
|
||||
raise CompletionRequestError("Build chat finalization did not complete.")
|
||||
|
||||
@@ -531,6 +532,8 @@ def _generate_chat_message_response(
|
||||
args=args,
|
||||
streaming=streaming,
|
||||
)
|
||||
if AppMode.value_of(app_model.mode) == AppMode.AGENT and streaming:
|
||||
response = _raise_agent_stream_error_before_response(response)
|
||||
return helper.compact_generate_response(response)
|
||||
|
||||
|
||||
@@ -543,3 +546,67 @@ def _stop_chat_message(*, current_user_id: str, app_model: App, task_id: str):
|
||||
)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
|
||||
def _raise_agent_stream_error_before_response(response):
|
||||
"""Surface immediate Agent App stream errors as HTTP errors before SSE starts.
|
||||
|
||||
The shared streaming helper always returns HTTP 200 once the SSE response is
|
||||
created. Agent v2 configuration errors, such as an invalid model API key,
|
||||
can be the first real stream event after the initial ping; pre-reading that
|
||||
first non-ping event lets the console API return the existing 400 error
|
||||
contract instead of a successful HTTP response carrying only an SSE error.
|
||||
"""
|
||||
if isinstance(response, Mapping):
|
||||
return response
|
||||
|
||||
buffered: list[str] = []
|
||||
iterator = iter(response)
|
||||
while True:
|
||||
try:
|
||||
chunk = next(iterator)
|
||||
except StopIteration:
|
||||
return iter(buffered)
|
||||
|
||||
if not isinstance(chunk, str):
|
||||
return _prepend_stream_chunks(buffered, chunk, iterator)
|
||||
|
||||
if _is_sse_ping(chunk):
|
||||
buffered.append(chunk)
|
||||
continue
|
||||
|
||||
error_payload = _extract_sse_error_payload(chunk)
|
||||
if error_payload is not None:
|
||||
if isinstance(response, _ClosableStream):
|
||||
response.close()
|
||||
message = error_payload.get("message")
|
||||
raise CompletionRequestError(str(message or "Agent App chat failed."))
|
||||
|
||||
return _prepend_stream_chunks(buffered, chunk, iterator)
|
||||
|
||||
|
||||
def _prepend_stream_chunks(buffered: list[Any], first: Any, iterator: Iterator[Any]) -> Generator[Any, None, None]:
|
||||
yield from buffered
|
||||
yield first
|
||||
yield from iterator
|
||||
|
||||
|
||||
def _is_sse_ping(chunk: str) -> bool:
|
||||
return chunk.strip() == "event: ping"
|
||||
|
||||
|
||||
def _extract_sse_error_payload(chunk: str) -> dict[str, Any] | None:
|
||||
for raw_event in chunk.split("\n\n"):
|
||||
data_lines: list[str] = []
|
||||
for line in raw_event.splitlines():
|
||||
if line.startswith("data: "):
|
||||
data_lines.append(line.removeprefix("data: "))
|
||||
if not data_lines:
|
||||
continue
|
||||
try:
|
||||
payload = json.loads("\n".join(data_lines))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(payload, dict) and payload.get("event") == "error":
|
||||
return payload
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user