Skip to content

Commit

Permalink
Merge pull request baidubce#613 from userpj/master
Browse files Browse the repository at this point in the history
Python ToolCall新增函数、注解解析
  • Loading branch information
wolvever authored Nov 25, 2024
2 parents 1943db9 + 61c1022 commit a512b59
Show file tree
Hide file tree
Showing 13 changed files with 2,548 additions and 217 deletions.
507 changes: 459 additions & 48 deletions cookbooks/end2end_application/agent/tool_call.ipynb

Large diffs are not rendered by default.

410 changes: 269 additions & 141 deletions docs/BasisModule/Platform/Application/appbuilder_client.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ def get_default_header():

from appbuilder.utils.logger_util import logger

from appbuilder.core.manifest.manifest_decorator import manifest, manifest_parameter
from appbuilder.core.manifest.models import Manifest

from appbuilder.core.utils import get_model_list

from appbuilder.core.console.appbuilder_client.appbuilder_client import AppBuilderClient
Expand Down
15 changes: 8 additions & 7 deletions python/core/console/appbuilder_client/appbuilder_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
import json
import uuid
import queue
from typing import Optional
from typing import Optional,Union
from appbuilder.core.component import Message, Component
from appbuilder.core.manifest.models import Manifest
from appbuilder.core.console.appbuilder_client import data_class
from appbuilder.core._exception import AppBuilderServerException
from appbuilder.utils.sse_util import SSEClient
Expand Down Expand Up @@ -250,7 +251,7 @@ def run(self, conversation_id: str,
query: str = "",
file_ids: list = [],
stream: bool = False,
tools: list[data_class.Tool] = None,
tools: list[Union[data_class.Tool,Manifest]]= None,
tool_outputs: list[data_class.ToolOutput] = None,
tool_choice: data_class.ToolChoice = None,
end_user_id: str = None,
Expand All @@ -264,7 +265,7 @@ def run(self, conversation_id: str,
conversation_id (str): 唯一会话ID,如需开始新的会话,请使用self.create_conversation创建新的会话
file_ids(list[str]): 文件ID列表
stream (bool): 为True时,流式返回,需要将message.content.answer拼接起来才是完整的回答;为False时,对应非流式返回
tools(list[data_class.Tools]): 一个Tools组成的列表,其中每个Tools对应一个工具的配置, 默认为None
tools(list[Union[data_class.Tool,Manifest]]): 一个Tool或Manifest组成的列表,其中每个Tool(Manifest)对应一个工具的配置, 默认为None
tool_outputs(list[data_class.ToolOutput]): 工具输出列表,格式为list[ToolOutput], ToolOutputd内容为本地的工具执行结果,以自然语言/json dump str描述,默认为None
tool_choice(data_class.ToolChoice): 控制大模型使用组件的方式,默认为None
end_user_id (str): 用户ID,用于区分不同用户
Expand Down Expand Up @@ -319,7 +320,7 @@ def run_with_handler(self,
conversation_id: str,
query: str = "",
file_ids: list = [],
tools: list[data_class.Tool] = None,
tools: list[Union[data_class.Tool,Manifest]] = None,
stream: bool = False,
event_handler=None,
action=None,
Expand All @@ -330,10 +331,10 @@ def run_with_handler(self,
conversation_id (str): 唯一会话ID,如需开始新的会话,请使用self.create_conversation创建新的会话
query (str): 查询字符串
file_ids (list): 文件ID列表
tools(list[data_class.Tools], 可选): 一个Tools组成的列表,其中每个Tools对应一个工具的配置, 默认为None
tools(list[Union[data_class.Tool,Manifest]], 可选): 一个Tool或Manifest组成的列表,其中每个Tool(Manifest)对应一个工具的配置, 默认为None
stream (bool): 是否流式响应
event_handler (EventHandler): 事件处理器
action(dataclass.Action) 对话时要进行的特殊操作。如回复工作流agent中“信息收集节点“的消息。
action(data_class.Action) 对话时要进行的特殊操作。如回复工作流agent中“信息收集节点“的消息。
kwargs: 其他参数
Expand Down Expand Up @@ -369,7 +370,7 @@ def run_multiple_dialog_with_handler(self,
conversation_id (str): 唯一会话ID,如需开始新的会话,请使用self.create_conversation创建新的会话
queries (iter): 查询字符串可迭代对象
file_ids (iter): 文件ID列表
tools(iter, 可选): 一个Tools组成的列表,其中每个Tools对应一个工具的配置, 默认为None
tools(iter, 可选): 一个Tool或Manifest组成的列表,其中每个Tool(Manifest)对应一个工具的配置, 默认为None
stream (bool): 是否流式响应
event_handler (EventHandler): 事件处理器
actions(iter) 对话时要进行的特殊操作。如回复工作流agent中“信息收集节点“的消息。
Expand Down
3 changes: 2 additions & 1 deletion python/core/console/appbuilder_client/data_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pydantic import Field
from typing import Union
from typing import Optional
from appbuilder.core.manifest.models import Manifest


class Function(BaseModel):
Expand Down Expand Up @@ -112,7 +113,7 @@ class AppBuilderClientRequest(BaseModel):
conversation_id: str
file_ids: Optional[list[str]] = None
app_id: str
tools: Optional[list[Tool]] = None
tools: Optional[list[Union[Tool,Manifest]]] = None
tool_outputs: Optional[list[ToolOutput]] = None
tool_choice: Optional[ToolChoice] = None
end_user_id: Optional[str] = None
Expand Down
13 changes: 13 additions & 0 deletions python/core/manifest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2024 Baidu, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
224 changes: 224 additions & 0 deletions python/core/manifest/manifest_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Copyright (c) 2024 Baidu, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
from typing import Any, Dict, Optional

from pydantic.v1 import BaseModel as PydanticBaseModel # noqa: F403 # type: ignore
from pydantic.v1 import create_model

from appbuilder.core.manifest.manifest_signature import get_signature
from appbuilder.core.manifest.models import Manifest, PropertyModel, ParametersModel

def manifest(
*,
description: Optional[str] = None,
name: Optional[str] = None,
):
"""
Decorator for functions. Use some information to extract meta about function.
Priority as follows:
1. User provided value via arguments
2. Function signature + annotations
Args:
description (str, optional): The functionality of the function, general user guideline.
name (str, optional): The name of the function.
"""

def decorator(func):
"""Decorator for function."""

# Get meta from signature
sig_params, sig_returns = get_signature(func=func)

# Get meta from decorator
dec_params_list = (
func.__ab_manifest_parameters__
if hasattr(func, "__ab_manifest_parameters__")
else []
)
dec_params = {item.name: item for item in dec_params_list}

properties = {}
required_fields = []
for param in sig_params:
dec_param = dec_params.get(param["name"])
param_type = param.get("type_") or getattr(dec_param, "type", None)

param_info = {
"name": param["name"], # 参数名称
"type": param_type,
"description": getattr(
dec_params.get(param["name"]), "description", None
), # 描述
"required": param.get(
"required", getattr(dec_params.get(param["name"]), "required", True)
), # 是否必需
}

# 构造 PropertyModel
properties[param["name"]] = PropertyModel(
name=param_info["name"],
type=param_info["type"],
description=param_info["description"],
required=param_info["required"],
)

# 记录必需参数
if param_info["required"]:
required_fields.append(param["name"])

# 确定函数的最终名称和描述
final_name = name or func.__name__
final_desc = description or func.__doc__

parameters_model = ParametersModel(
type="object",
properties={
k: v.model_dump(exclude_none=False) for k, v in properties.items()
},
required=required_fields,
)

view = Manifest(
type="function",
function={
"name": final_name,
"description": final_desc,
"parameters": parameters_model.model_dump(),
},
)

# Attach view to function.
func.__ab_manifest__ = view

# Compatible to semantic kernel 0.9 ~ 1.6 according to this url: https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/functions/kernel_function_decorator.py
func.__kernel_function__ = True
func.__kernel_function_description__ = final_desc
func.__kernel_function_name__ = final_name

return func

return decorator


def manifest_parameter(
*,
name: str,
description: str = None,
type: str = None,
required: bool = True,
):
"""
Decorator for function parameters.
Args:
name -- The name of the parameter
description -- The description of the parameter
default_value -- The default value of the parameter
type -- The type of the parameter, used for function calling
required -- Whether the parameter is required
example -- The example of the parameter
"""

def decorator(func):
"""Decorator for function parameter."""

new_view = PropertyModel(
name=name,
type=type,
description=description,
required=required,
)
# Update parameter view lists for function_parameter decorator.
# This will be merged into ManifestView if function decorator runs last like:
# @manifest
# @manifest_parameter
# @manifest_parameter
if hasattr(func, "__ab_manifest_parameters__"):
current_views = func.__ab_manifest_parameters__
else:
current_views = []
current_views.append(new_view)
func.__ab_manifest_parameters__ = current_views

if hasattr(func, "__ab_manifest__"):
# 获取现有的 parameters
parameters_dict = func.__ab_manifest__.function.get("parameters", {})
parameters_model = ParametersModel(**parameters_dict)

# 更新 properties
existing_property = parameters_model.properties.get(new_view.name)
if existing_property:
merged_property = PropertyModel.merge(existing_property, new_view)
parameters_model.properties[new_view.name] = merged_property
else:
parameters_model.properties[new_view.name] = new_view

# 更新 required 字段
if new_view.required:
if new_view.name not in parameters_model.required:
parameters_model.required.append(new_view.name)
else:
if new_view.name in parameters_model.required:
parameters_model.required.remove(new_view.name)

# 更新 func.__ab_manifest__.function["parameters"]
func.__ab_manifest__.function["parameters"] = parameters_model.model_dump()

# Compatible to semantic kernel 0.9 ~ 1.6 according to this url: https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/functions/kernel_function_decorator.py
if hasattr(func, "__kernel_function_parameters__"):
item = {
"name": name,
"description": description,
"type": type,
"required": required,
}

new_list = _update_list(
item,
func.__kernel_function_parameters__,
lambda item, new_item: item["name"] == new_item["name"],
lambda item, new_item: _merge_dict(item, new_item),
)
func.__kernel_function_parameters__ = new_list

return func

return decorator


def _merge_dict(current_dict, new_dict):
result = current_dict.copy()
for k, v in new_dict.items():
if v:
result[k] = v
return result


# Replace the parameter with the same name and keep order.
# Since there are few parameters for each function, keep use list for simplicity
def _update_list(new_item, list, condition, replacer):
new_list = []
replaced = False
for item in list:
if condition(item, new_item):
replaced = True
merged_item = replacer(item, new_item)
new_list.append(merged_item)
# Missing parameter append to the end.
if not replaced:
new_list.append(new_item)
return new_list
Loading

0 comments on commit a512b59

Please sign in to comment.