feat: agent log

This commit is contained in:
Yeuoly 2024-03-29 19:23:48 +08:00
parent f43faa125b
commit 11b428a73f
No known key found for this signature in database
GPG Key ID: A66E7E320FB19F61
13 changed files with 275 additions and 19 deletions

View File

@ -8,7 +8,7 @@ api = ExternalApi(bp)
from . import admin, apikey, extension, feature, setup, version, ping
# Import app controllers
from .app import (advanced_prompt_template, annotation, app, audio, completion, conversation, generator, message,
model_config, site, statistic, workflow, workflow_run, workflow_app_log, workflow_statistic)
model_config, site, statistic, workflow, workflow_run, workflow_app_log, workflow_statistic, agent)
# Import auth controllers
from .auth import activate, data_source_oauth, login, oauth
# Import billing controllers

View File

@ -0,0 +1,32 @@
from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from libs.helper import uuid_value
from libs.login import login_required
from models.model import AppMode
from services.agent_service import AgentService
class AgentLogApi(Resource):
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.AGENT_CHAT])
def get(self, app_model):
"""Get agent logs"""
parser = reqparse.RequestParser()
parser.add_argument('message_id', type=uuid_value, required=True, location='args')
parser.add_argument('conversation_id', type=uuid_value, required=True, location='args')
args = parser.parse_args()
return AgentService.get_agent_logs(
app_model,
args['conversation_id'],
args['message_id']
)
api.add_resource(AgentLogApi, '/apps/<uuid:app_id>/agent/logs')

View File

@ -11,6 +11,7 @@ class ToolProviderType(Enum):
Enum class for tool provider
"""
BUILT_IN = "built-in"
DATASET_RETRIEVAL = "dataset-retrieval"
APP_BASED = "app-based"
API_BASED = "api-based"
@ -161,6 +162,8 @@ class ToolIdentity(BaseModel):
author: str = Field(..., description="The author of the tool")
name: str = Field(..., description="The name of the tool")
label: I18nObject = Field(..., description="The label of the tool")
provider: str = Field(..., description="The provider of the tool")
icon: Optional[str] = None
class ToolCredentialsOption(BaseModel):
value: str = Field(..., description="The value of the option")
@ -334,23 +337,25 @@ class ToolInvokeMeta(BaseModel):
"""
time_cost: float = Field(..., description="The time cost of the tool invoke")
error: Optional[str] = None
tool_config: Optional[dict] = None
@classmethod
def empty(cls) -> 'ToolInvokeMeta':
"""
Get an empty instance of ToolInvokeMeta
"""
return cls(time_cost=0.0, error=None)
return cls(time_cost=0.0, error=None, tool_config={})
@classmethod
def error_instance(cls, error: str) -> 'ToolInvokeMeta':
"""
Get an instance of ToolInvokeMeta with error
"""
return cls(time_cost=0.0, error=error)
return cls(time_cost=0.0, error=error, tool_config={})
def to_dict(self) -> dict:
return {
'time_cost': self.time_cost,
'error': self.error,
'tool_config': self.tool_config,
}

View File

@ -16,6 +16,8 @@ from models.tools import ApiToolProvider
class ApiBasedToolProviderController(ToolProviderController):
provider_id: str
@staticmethod
def from_db(db_provider: ApiToolProvider, auth_type: ApiProviderAuthType) -> 'ApiBasedToolProviderController':
credentials_schema = {
@ -89,9 +91,10 @@ class ApiBasedToolProviderController(ToolProviderController):
'en_US': db_provider.description,
'zh_Hans': db_provider.description
},
'icon': db_provider.icon
'icon': db_provider.icon,
},
'credentials_schema': credentials_schema
'credentials_schema': credentials_schema,
'provider_id': db_provider.id,
})
@property
@ -120,7 +123,8 @@ class ApiBasedToolProviderController(ToolProviderController):
'en_US': tool_bundle.operation_id,
'zh_Hans': tool_bundle.operation_id
},
'icon': tool_bundle.icon if tool_bundle.icon else ''
'icon': self.identity.icon,
'provider': self.provider_id,
},
'description': {
'human': {

View File

@ -68,6 +68,7 @@ class BuiltinToolProviderController(ToolProviderController):
script_path=path.join(path.dirname(path.realpath(__file__)),
'builtin', provider, 'tools', f'{tool_name}.py'),
parent_type=BuiltinTool)
tool["identity"]["provider"] = provider
tools.append(assistant_tool_class(**tool))
self.tools = tools

View File

@ -8,7 +8,8 @@ import requests
import core.helper.ssrf_proxy as ssrf_proxy
from core.tools.entities.tool_bundle import ApiBasedToolBundle
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolProviderType
from core.tools.entities.user_entities import UserToolProvider
from core.tools.errors import ToolInvokeError, ToolParameterValidationError, ToolProviderCredentialValidationError
from core.tools.tool.tool import Tool
@ -34,7 +35,7 @@ class ApiTool(Tool):
api_bundle=self.api_bundle.copy() if self.api_bundle else None,
runtime=Tool.Runtime(**meta)
)
def validate_credentials(self, credentials: dict[str, Any], parameters: dict[str, Any], format_only: bool = False) -> str:
"""
validate the credentials for Api tool
@ -49,6 +50,9 @@ class ApiTool(Tool):
# validate response
return self.validate_and_parse_response(response)
def tool_provider_type(self) -> ToolProviderType:
return UserToolProvider.ProviderType.API
def assembling_request(self, parameters: dict[str, Any]) -> dict[str, Any]:
headers = {}
credentials = self.runtime.credentials or {}

View File

@ -1,6 +1,8 @@
from core.model_runtime.entities.llm_entities import LLMResult
from core.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.entities.user_entities import UserToolProvider
from core.tools.model.tool_model_manager import ToolModelManager
from core.tools.tool.tool import Tool
from core.tools.utils.web_reader_tool import get_url
@ -40,6 +42,9 @@ class BuiltinTool(Tool):
prompt_messages=prompt_messages,
)
def tool_provider_type(self) -> ToolProviderType:
return UserToolProvider.ProviderType.BUILTIN
def get_max_tokens(self) -> int:
"""
get max tokens

View File

@ -7,7 +7,13 @@ from core.app.entities.app_invoke_entities import InvokeFrom
from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolDescription, ToolIdentity, ToolInvokeMessage, ToolParameter
from core.tools.entities.tool_entities import (
ToolDescription,
ToolIdentity,
ToolInvokeMessage,
ToolParameter,
ToolProviderType,
)
from core.tools.tool.tool import Tool
@ -53,7 +59,7 @@ class DatasetRetrieverTool(Tool):
for langchain_tool in langchain_tools:
tool = DatasetRetrieverTool(
langchain_tool=langchain_tool,
identity=ToolIdentity(author='', name=langchain_tool.name, label=I18nObject(en_US='', zh_Hans='')),
identity=ToolIdentity(provider='', author='', name=langchain_tool.name, label=I18nObject(en_US='', zh_Hans='')),
parameters=[],
is_team_authorization=True,
description=ToolDescription(
@ -77,6 +83,9 @@ class DatasetRetrieverTool(Tool):
required=True,
default=''),
]
def tool_provider_type(self) -> ToolProviderType:
return ToolProviderType.DATASET_RETRIEVAL
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
"""

View File

@ -11,7 +11,7 @@ from core.model_runtime.entities.message_entities import (
UserPromptMessage,
)
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.tools.entities.tool_entities import ModelToolPropertyKey, ToolInvokeMessage
from core.tools.entities.tool_entities import ModelToolPropertyKey, ToolInvokeMessage, ToolProviderType
from core.tools.tool.tool import Tool
VISION_PROMPT = """## Image Recognition Task
@ -79,6 +79,9 @@ class ModelTool(Tool):
"""
pass
def tool_provider_type(self) -> ToolProviderType:
return ToolProviderType.BUILT_IN
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
"""
"""

View File

@ -9,6 +9,7 @@ from core.tools.entities.tool_entities import (
ToolIdentity,
ToolInvokeMessage,
ToolParameter,
ToolProviderType,
ToolRuntimeImageVariable,
ToolRuntimeVariable,
ToolRuntimeVariablePool,
@ -59,6 +60,14 @@ class Tool(BaseModel, ABC):
runtime=Tool.Runtime(**meta),
)
@abstractmethod
def tool_provider_type(self) -> ToolProviderType:
"""
get the tool provider type
:return: the tool provider type
"""
def load_variables(self, variables: ToolRuntimeVariablePool):
"""
load variables from database

View File

@ -1,3 +1,4 @@
from copy import deepcopy
from datetime import datetime, timezone
from typing import Union
@ -55,11 +56,7 @@ class ToolEngine:
tool_inputs=tool_parameters
)
try:
meta, response = ToolEngine._invoke(tool, tool_parameters, user_id)
except ToolEngineInvokeError as e:
meta = e.meta
meta, response = ToolEngine._invoke(tool, tool_parameters, user_id)
response = ToolFileMessageTransformer.transform_tool_invoke_messages(
messages=response,
user_id=user_id,
@ -104,11 +101,16 @@ class ToolEngine:
except ToolInvokeError as e:
error_response = f"tool invoke error: {e}"
agent_tool_callback.on_tool_error(e)
except ToolEngineInvokeError as e:
meta = e.args[0]
error_response = f"tool invoke error: {meta.error}"
agent_tool_callback.on_tool_error(e)
return error_response, [], meta
except Exception as e:
error_response = f"unknown error: {e}"
agent_tool_callback.on_tool_error(e)
return error_response, [], meta
return error_response, [], ToolInvokeMeta.error_instance(error_response)
@staticmethod
def workflow_invoke(tool: Tool, tool_parameters: dict,
@ -146,12 +148,18 @@ class ToolEngine:
Invoke the tool with the given arguments.
"""
started_at = datetime.now(timezone.utc)
meta = ToolInvokeMeta(time_cost=0.0, error=None)
meta = ToolInvokeMeta(time_cost=0.0, error=None, tool_config={
'tool_name': tool.identity.name,
'tool_provider': tool.identity.provider,
'tool_provider_type': tool.tool_provider_type().value,
'tool_parameters': deepcopy(tool.runtime.runtime_parameters),
'tool_icon': tool.identity.icon
})
try:
response = tool.invoke(user_id, tool_parameters)
except Exception as e:
meta.error = str(e)
raise ToolEngineInvokeError(meta=meta)
raise ToolEngineInvokeError(meta)
finally:
ended_at = datetime.now(timezone.utc)
meta.time_cost = (ended_at - started_at).total_seconds()

View File

@ -1163,6 +1163,10 @@ class MessageAgentThought(db.Model):
else:
return []
@property
def tools(self) -> list[str]:
return self.tool.split(";") if self.tool else []
@property
def tool_labels(self) -> dict:
try:
@ -1182,6 +1186,55 @@ class MessageAgentThought(db.Model):
return {}
except Exception as e:
return {}
@property
def tool_inputs_dict(self) -> dict:
tools = self.tools
try:
if self.tool_input:
data = json.loads(self.tool_input)
result = {}
for tool in tools:
if tool in data:
result[tool] = data[tool]
else:
if len(tools) == 1:
result[tool] = data
else:
result[tool] = {}
return result
else:
return {
tool: {} for tool in tools
}
except Exception as e:
return {}
@property
def tool_outputs_dict(self) -> dict:
tools = self.tools
try:
if self.observation:
data = json.loads(self.observation)
result = {}
for tool in tools:
if tool in data:
result[tool] = data[tool]
else:
if len(tools) == 1:
result[tool] = data
else:
result[tool] = {}
return result
else:
return {
tool: {} for tool in tools
}
except Exception as e:
if self.observation:
return {
tool: self.observation for tool in tools
}
class DatasetRetrieverResource(db.Model):
__tablename__ = 'dataset_retriever_resources'

View File

@ -0,0 +1,123 @@
from core.app.app_config.easy_ui_based_app.agent.manager import AgentConfigManager
from extensions.ext_database import db
from models.account import Account
from models.model import App, Conversation, EndUser, Message, MessageAgentThought
from services.tools_transform_service import ToolTransformService
class AgentService:
@classmethod
def get_agent_logs(cls, app_model: App,
conversation_id: str,
message_id: str) -> dict:
"""
Service to get agent logs
"""
conversation: Conversation = db.session.query(Conversation).filter(
Conversation.id == conversation_id,
Conversation.app_id == app_model.id,
).first()
if not conversation:
raise ValueError(f"Conversation not found: {conversation_id}")
message: Message = db.session.query(Message).filter(
Message.id == message_id,
Message.conversation_id == conversation_id,
).first()
if not message:
raise ValueError(f"Message not found: {message_id}")
agent_thoughts: list[MessageAgentThought] = message.agent_thoughts
if conversation.from_end_user_id:
# only select name field
executor = db.session.query(EndUser, EndUser.name).filter(
EndUser.id == conversation.from_end_user_id
).first()
else:
executor = db.session.query(Account, Account.name).filter(
Account.id == conversation.from_account_id
).first()
if executor:
executor = executor.name
else:
executor = 'Unknown'
result = {
'meta': {
'status': 'success',
'executor': executor,
'start_time': message.created_at.isoformat(),
'elapsed_time': message.provider_response_latency,
'total_tokens': message.answer_tokens + message.message_tokens,
'agent_mode': app_model.app_model_config.agent_mode_dict.get('strategy', 'react'),
'iterations': len(agent_thoughts),
},
'iterations': [],
'files': message.files,
}
agent_config = AgentConfigManager.convert(app_model.app_model_config.to_dict())
agent_tools = agent_config.tools
def find_agent_tool(tool_name: str):
for agent_tool in agent_tools:
if agent_tool.tool_name == tool_name:
return agent_tool
for agent_thought in agent_thoughts:
tools = agent_thought.tools
tool_labels = agent_thought.tool_labels
tool_meta = agent_thought.tool_meta
tool_inputs = agent_thought.tool_inputs_dict
tool_outputs = agent_thought.tool_outputs_dict
tool_calls = []
for tool in tools:
tool_name = tool
tool_label = tool_labels.get(tool_name, tool_name)
tool_input = tool_inputs.get(tool_name, {})
tool_output = tool_outputs.get(tool_name, {})
tool_meta_data = tool_meta.get(tool_name, {})
tool_config = tool_meta_data.get('tool_config', {})
tool_icon = ToolTransformService.get_tool_provider_icon_url(
provider_type=tool_config.get('tool_provider_type', ''),
provider_name=tool_config.get('tool_provider', ''),
icon=tool_config.get('tool_icon', '')
)
if not tool_icon:
tool_entity = find_agent_tool(tool_name)
if tool_entity:
tool_icon = ToolTransformService.get_tool_provider_icon_url(
provider_type=tool_entity.provider_type,
provider_name=tool_entity.provider_id,
icon=''
)
tool_calls.append({
'status': 'success' if not tool_meta_data.get('error') else 'error',
'error': tool_meta_data.get('error'),
'time_cost': tool_meta_data.get('time_cost', 0),
'tool_name': tool_name,
'tool_label': tool_label,
'tool_input': tool_input,
'tool_output': tool_output,
'tool_parameters': tool_meta_data.get('tool_parameters', {}),
'tool_icon': tool_icon,
})
result['iterations'].append({
'tokens': agent_thought.tokens,
'tool_calls': tool_calls,
'tool_raw': {
'inputs': agent_thought.tool_input,
'outputs': agent_thought.observation,
},
'thought': agent_thought.thought,
'created_at': agent_thought.created_at.isoformat(),
'files': agent_thought.files,
})
return result