Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
731524c
refactor(api): serve Agent logs through an application service
41tair Oct 8, 2026
87f667b
Merge updated stack base into refactor/api-76-10-4
41tair Oct 8, 2026
8fea94f
test(agent): type log transport and database-backed presentation cont…
41tair Oct 9, 2026
d15d6ef
docs(api): regenerate agent log endpoint descriptions
41tair Oct 9, 2026
2e821a2
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 9, 2026
1bb3c25
Merge updated stack base into refactor/api-76-10-4
41tair Oct 9, 2026
6ac8ae2
Merge updated refactor/api-76-10-3
41tair Oct 15, 2026
566ec92
fix(workflow): address review findings in #43688
41tair Oct 15, 2026
e211974
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 19, 2026
acf23be
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 19, 2026
fa21392
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 19, 2026
fd6f613
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 19, 2026
566a324
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 20, 2026
056775f
Merge workflow migration compatibility from updated parent
41tair Oct 20, 2026
a68b947
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 26, 2026
acfcf2c
Use relocated helpers in Agent logs tests
41tair Oct 26, 2026
bf58520
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
da189eb
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
31207a5
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
003ad49
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
1415021
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
47b38d9
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
33d3ac1
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
def75c1
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
a813844
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
1442132
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
dd390e9
Sync validated refactor/api-76-10-3 fixes
41tair Oct 26, 2026
f07667a
Sync validated refactor/api-76-10-3 fixes
41tair Oct 27, 2026
b150f60
Sync validated refactor/api-76-10-3 fixes
41tair Oct 27, 2026
a1f967e
Sync validated refactor/api-76-10-3 fixes
41tair Oct 27, 2026
553ab2d
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 27, 2026
71112dd
Merge updated refactor/api-76-10-3 into refactor/api-76-10-4
41tair Oct 31, 2026
c1dcbca
Regenerate API error contracts for migrated Agent logs
41tair Oct 31, 2026
1301c06
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
3c9a511
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
d5df4d7
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
54abad5
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
a6d87a3
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
9175bee
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
9e24420
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
e8fd395
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
c33da4d
Sync validated refactor/api-76-10-3 fixes
41tair Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions api/controllers/console/app/agent.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
from typing import Any
from uuid import UUID

from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session

from controllers.common.rbac import PlainApp, RBACCheck
from controllers.common.schema import query_params_from_model, register_response_schema_models
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model
from controllers.console.app.error import AppNotFoundError
from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import (
RBACPermission,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
from extensions.ext_application_services import application_services
from fields.base import ResponseModel
from libs.helper import uuid_value
from libs.login import login_required
from models.model import App, AppMode
from services.agent_service import AgentService
from libs.helper import dump_response, uuid_value
from machinery.context import RequestContext
from services.agent.log_contracts import AgentLogAppNotFoundError


class AgentLogQuery(BaseModel):
Expand Down Expand Up @@ -49,8 +46,8 @@ class AgentToolCallResponse(ResponseModel):
time_cost: float | int
tool_name: str
tool_label: str | dict[str, str] = Field(description="Tool name or its i18n labels keyed by locale")
tool_input: dict[str, Any]
tool_output: dict[str, Any]
tool_input: Any
tool_output: Any
tool_parameters: dict[str, Any]
tool_icon: str | dict[str, str] | None = Field(default=None, description="Icon URL or emoji icon")

Expand Down Expand Up @@ -81,14 +78,18 @@ class AgentLogApi(Resource):
@console_ns.doc(params=query_params_from_model(AgentLogQuery))
@console_ns.response(200, "Agent logs retrieved successfully", console_ns.models[AgentLogResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@setup_required
@login_required
@account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@with_session(write=False)
@get_app_model(mode=[AppMode.AGENT_CHAT])
@console_account_admission(rbac_checks=[RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())])
@model_validate(AgentLogQuery)
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
def get(self, req_data: AgentLogQuery, request_context: RequestContext, app_id: UUID):
"""Get agent logs."""

return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
try:
result = application_services().agent_apps.logs.get(
request_context,
app_id=str(app_id),
conversation_id=req_data.conversation_id,
message_id=req_data.message_id,
)
except AgentLogAppNotFoundError as error:
raise AppNotFoundError() from error
return dump_response(AgentLogResponse, result)
Comment thread
41tair marked this conversation as resolved.
Resolved
13 changes: 13 additions & 0 deletions api/extensions/application_services/agent.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
"""Composition of the Agent App console use cases."""

from dataclasses import dataclass
from functools import partial

from sqlalchemy.orm import Session, sessionmaker

from configs import dify_config
from core.app.file_access import DatabaseFileAccessController
from repositories.app.agent_app_repository import AgentAppRepository
from repositories.app.agent_sandbox_repository import AgentSandboxRepository
from repositories.app.generation_repository import AppGenerationRepository
from repositories.tools.provider_repository import ToolProviderRepository
from services.agent.log_file_gateway import AgentLogFileGateway
from services.agent.log_service import AgentLogService
from services.app.agent_app_feature_gateway import AgentAppFeatureValidator
from services.app.agent_app_sandbox_service import AgentAppSandboxService
from services.app.agent_app_service import AgentAppAccessService, AgentAppFeatureConfigService
from services.app.agent_sandbox_file_gateway import AgentSandboxFileGateway, create_sandbox_client
from services.file_request_service import FileRequestService
from services.tools.tool_manager import ToolManager


@dataclass(frozen=True, slots=True)
class AgentAppServices:
logs: AgentLogService
access: AgentAppAccessService
features: AgentAppFeatureConfigService
sandbox: AgentAppSandboxService
Expand All @@ -24,6 +32,11 @@ class AgentAppServices:
def build_agent_app_services(*, database_client: sessionmaker[Session]) -> AgentAppServices:
apps = AgentAppRepository(session_factory=database_client)
return AgentAppServices(
logs=AgentLogService(
records=AppGenerationRepository(database_client),
icons=partial(ToolManager.get_tool_icon, tool_providers=ToolProviderRepository(database_client)),
files=AgentLogFileGateway(sessions=database_client, access=DatabaseFileAccessController()),
),
access=AgentAppAccessService(references=apps),
features=AgentAppFeatureConfigService(features=apps, validator=AgentAppFeatureValidator()),
sandbox=AgentAppSandboxService(
Expand Down
5 changes: 2 additions & 3 deletions api/openapi/markdown/console-openapi.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions api/services/agent/log_file_gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Rebuild message attachments with the shared file factory and explicit sessions."""

from typing import Any

from sqlalchemy.orm import Session, sessionmaker

from core.app.file_access import FileAccessControllerProtocol
from factories.file_factory import build_from_mapping
from graphon.file import FileTransferMethod


class AgentLogFileGateway:
def __init__(self, *, sessions: sessionmaker[Session], access: FileAccessControllerProtocol) -> None:
self._sessions = sessions
self._access = access

def resolve(self, *, tenant_id: str, files: list[dict[str, Any]]) -> list[dict[str, Any]]:
result = []
for source in files:
mapping = dict(source)
if mapping["transfer_method"] == FileTransferMethod.TOOL_FILE:
reference = mapping["upload_file_id"]
if reference is None:
reference = mapping["url"].split("/")[-1].split(".")[0]
mapping["tool_file_id"] = reference
mapping["upload_file_id"] = reference
file = build_from_mapping(
mapping=mapping, tenant_id=tenant_id, access_controller=self._access, sessions=self._sessions
)
result.append(
{"belongs_to": source["belongs_to"], "upload_file_id": mapping["upload_file_id"], **file.to_dict()}
)
return result
96 changes: 96 additions & 0 deletions api/services/agent/log_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Present one Agent message log after its read transaction has closed."""

from datetime import UTC
from typing import Any

import pytz

from core.app.app_config.easy_ui_based_app.agent.manager import AgentConfigManager
from core.tools.entities.tool_entities import EmojiIconDict
from libs.datetime_utils import ensure_naive_utc
from machinery.context import RequestContext
from services.agent.log_contracts import AgentLogConfigurationError, AgentLogFiles, AgentLogIcons, AgentLogRecords


class AgentLogService:
def __init__(self, *, records: AgentLogRecords, icons: AgentLogIcons, files: AgentLogFiles) -> None:
self._records = records
self._icons = icons
self._files = files

def get(self, context: RequestContext, *, app_id: str, conversation_id: str, message_id: str) -> dict[str, Any]:
log = self._records.agent_log(
tenant_id=context.active_workspace_id,
account_id=context.account_id,
app_id=app_id,
conversation_id=conversation_id,
message_id=message_id,
)
agent_config = AgentConfigManager.convert(log.model_config)
if agent_config is None:
raise AgentLogConfigurationError("Agent config not found")
configured_tools = {tool.tool_name: tool for tool in agent_config.tools or []}
iterations = []
for thought in log.thoughts:
tool_calls = []
for name in thought.tools:
metadata = thought.metadata.get(name, {})
config = metadata.get("tool_config", {})
provider_type = config.get("tool_provider_type", "")
provider_id = config.get("tool_provider", "")
configured = configured_tools.get(name)
if not provider_type and configured is not None:
provider_type = configured.provider_type
provider_id = provider_id or configured.provider_id
icon: str | EmojiIconDict = ""
if provider_type and provider_type != "dataset-retrieval":
icon = self._icons(
tenant_id=context.active_workspace_id,
provider_type=provider_type,
provider_id=provider_id,
)
if not icon and configured is not None:
icon = self._icons(
tenant_id=context.active_workspace_id,
provider_type=configured.provider_type,
provider_id=configured.provider_id,
)
tool_calls.append(
{
"status": "success" if not metadata.get("error") else "error",
"error": metadata.get("error"),
"time_cost": metadata.get("time_cost", 0),
"tool_name": name,
"tool_label": thought.labels.get(name, name),
"tool_input": thought.inputs.get(name, {}),
"tool_output": thought.outputs.get(name, {}),
"tool_parameters": metadata.get("tool_parameters", {}),
"tool_icon": icon,
}
)
iterations.append(
{
"tokens": thought.tokens or 0,
"tool_calls": tool_calls,
"tool_raw": {"inputs": thought.raw_input, "outputs": thought.raw_output},
"thought": thought.thought,
"created_at": thought.created_at.isoformat(),
"files": thought.files,
}
)
return {
"meta": {
"status": "success",
"executor": log.executor,
"start_time": ensure_naive_utc(log.created_at)
.replace(tzinfo=UTC)
.astimezone(pytz.timezone(log.timezone))
.isoformat(),
"elapsed_time": log.elapsed_time,
"total_tokens": log.total_tokens,
"agent_mode": log.model_config["agent_mode"].get("strategy", "react"),
"iterations": len(iterations),
},
"iterations": iterations,
"files": self._files.resolve(tenant_id=context.active_workspace_id, files=log.files),
}
Loading
Loading