Refactor the Action/Function components, and refactor the basic architecture of Agent Task/Todo.

This commit is contained in:
Liu Zhicong
2023-12-17 18:23:40 -08:00
parent 3d00095650
commit 29594c0319
41 changed files with 2687 additions and 1108 deletions
+1
View File
@@ -1,3 +1,4 @@
# pylint:disable=E0402
import json
import logging
import shlex
+1 -1
View File
@@ -1,4 +1,4 @@
# pylint:disable=E0402
from abc import ABC, abstractmethod
from typing import List, Optional
import datetime
+135 -55
View File
@@ -1,11 +1,23 @@
# pylint:disable=E0402
from abc import ABC, abstractmethod
from typing import Dict,Coroutine,Callable,List
class ParameterDefine:
def __init__(self) -> None:
self.name = None
self.type = None
self.description = None
def __init__(self,name:str,desc:str) -> None:
self.name:str = name
self.type:str = "string"
self.enum:List[str] = None
self.description = desc
self.is_required = False
@classmethod
def create_parameters(cls,json_obj:dict) -> Dict[str,'ParameterDefine']:
result = {}
for k,v in json_obj.items():
param = ParameterDefine(k,v)
result[k] = param
return result
class AIFunction:
@@ -23,32 +35,125 @@ class AIFunction:
"""
pass
def get_detail_description(self) -> str:
"""
return a detailed description of what the function does
"""
parameters = self.get_parameters()
parameters_str = ""
for k,v in parameters.items():
if len(v.description) <= 0:
parameters_str +=f"{k},"
else:
if v.description == k:
parameters_str += f"{k},"
else:
if v.is_required:
parameters_str += f"{k}: {v.description},"
else:
parameters_str += f"{k} (Optional): {v.description},"
if len(parameters_str) > 0:
return f"{self.get_description} Parameters: {parameters_str}"
return f"f{self.get_description()}, no parameters"
@abstractmethod
def get_parameters(self) -> Dict:
def get_parameters(self) -> Dict[str,ParameterDefine]:
pass
def get_openai_parameters(self) -> Dict:
"""
Return the list of parameters to execute this function in the form of
JSON schema as specified in the OpenAI documentation:
https://platform.openai.com/docs/api-reference/chat/create#chat/create-parameters
str = run_code(code:str)
parameters = {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code which needs to be executed"
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
}
}
},
{
"type": "function",
"function": {
"name": "get_n_day_weather_forecast",
"description": "Get an N-day weather forecast",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
"num_days": {
"type": "integer",
"description": "The number of days to forecast",
}
},
"required": ["location", "format", "num_days"]
},
}
},
]
"""
pass
parameters = self.get_parameters()
if parameters is not None:
result = {}
result["type"] = "object"
required = []
parm_defines = {}
for parm_name,parm in parameters.items():
parm_item = {}
parm_item["type"] = parm.type
parm_item["description"] = parm.description
if parm.enum is not None:
parm_item["enum"] = parm.enum
parm_defines[parm_name] = parm_item
if parm.is_required:
required.append(parm_name)
result["properties"] = parm_defines
result["required"] = required
return result
return {"type": "object", "properties": {}}
@abstractmethod
async def execute(self, **kwargs) -> str:
"""
Execute the function and return a JSON serializable dict.
Execute the function and return a JSON serializable dict by LLM
The parameters are passed in the form of kwargs
[{'id': 'call_fLsKR5vGllhbWxvpqsDT3jBj',
'type': 'function',
'function': {'name': 'get_n_day_weather_forecast',
'arguments': '{"location": "San Francisco, CA", "format": "celsius", "num_days": 4}'}},
{'id': 'call_CchlsGE8OE03QmeyFbg7pkDz',
'type': 'function',
'function': {'name': 'get_n_day_weather_forecast',
'arguments': '{"location": "Glasgow", "format": "celsius", "num_days": 4}'}}
]
"""
pass
@@ -70,10 +175,8 @@ class AIFunction:
def is_ready_only(self) -> bool:
pass
#def load_from_config(self,config:dict) -> bool:
# pass
class ActionItem:
#TODO need to be upgrade
class ActionNode:
def __init__(self,name:str,args:List[str]) -> None:
self.name:str= name
self.args:List[str]= args
@@ -90,9 +193,9 @@ class ActionItem:
pass
@classmethod
def from_json(cls,json_obj:dict) -> 'ActionItem':
def from_json(cls,json_obj:dict) -> 'ActionNode':
args = json_obj.get("args",[])
r = ActionItem(json_obj["name"],args)
r = ActionNode(json_obj["name"],args)
if json_obj.get("body"):
r.body = json_obj["body"]
r.parms = json_obj
@@ -100,23 +203,12 @@ class ActionItem:
return r
# call chain is a combination of ai_function,group of ai_function.
class CallChain:
def __init__(self) -> None:
pass
def load_from_config(self,config:dict) -> bool:
pass
async def execute(self):
pass
class SimpleAIFunction(AIFunction):
def __init__(self,func_id:str,description:str,func_handler:Coroutine,parameters:Dict = None) -> None:
def __init__(self,func_id:str,description:str,func_handler:Coroutine,parameters:Dict[str,ParameterDefine] = None) -> None:
self.func_id = func_id
self.description = description
self.func_handler = func_handler
self.parameters = parameters
self.parameters:Dict[str,ParameterDefine] = parameters
def get_name(self) -> str:
return self.func_id
@@ -124,24 +216,12 @@ class SimpleAIFunction(AIFunction):
def get_description(self) -> str:
return self.description
def get_parameters(self) -> Dict:
if self.parameters is not None:
result = {}
result["type"] = "object"
parm_defines = {}
for parm,desc in self.parameters.items():
parm_item = {}
parm_item["type"] = "string"
parm_item["description"] = desc
parm_defines[parm] = parm_item
result["properties"] = parm_defines
return result
return {"type": "object", "properties": {}}
def get_parameters(self) -> Dict[str,ParameterDefine]:
return self.parameters
async def execute(self,**kwargs) -> str:
if self.func_handler is None:
return "error: function not implemented"
return f"error: function {self.func_id} not implemented"
return await self.func_handler(**kwargs)
@@ -154,7 +234,7 @@ class SimpleAIFunction(AIFunction):
def is_ready_only(self) -> bool:
return False
class AIOperation:
class AIAction:
@abstractmethod
def get_name(self) -> str:
"""
@@ -178,7 +258,7 @@ class AIOperation:
"""
pass
class SimpleAIOperation(AIOperation):
class SimpleAIAction(AIAction):
def __init__(self,op:str,description:str,func_handler:Coroutine) -> None:
self.op = op
self.description = description
@@ -197,7 +277,7 @@ class SimpleAIOperation(AIOperation):
return await self.func_handler(params)
class AIFunctionOperation(AIOperation):
class AIFunction2Action(AIAction):
def __init__(self, func: AIFunction) -> None:
self.func = func
super().__init__()
@@ -208,7 +288,7 @@ class AIFunctionOperation(AIOperation):
@abstractmethod
def get_description(self) -> str:
return self.func.get_description()
return self.func.get_detail_description()
@abstractmethod
async def execute(self, params: dict) -> str:
+18 -17
View File
@@ -1,13 +1,13 @@
# pylint:disable=E0402
import copy
from enum import Enum
import json
import shlex
import uuid
import time
from typing import List, Union
from .ai_function import *
from .agent_msg import *
from typing import List, Union,Dict
from .ai_function import AIFunction,ActionNode
from .agent_msg import AgentMsg
from ..knowledge import ObjectID
from ..storage.storage import AIStorage
@@ -33,13 +33,15 @@ class ComputeTaskState(Enum):
class ComputeTaskType(Enum):
NONE = "None"
LLM_COMPLETION = "llm_completion"
TEXT_EMBEDDING ="text_embedding"
IMAGE_EMBEDDING ="image_embedding"
TEXT_2_IMAGE = "text_2_image"
IMAGE_2_TEXT = "image_2_text"
IMAGE_2_IMAGE = "image_2_image"
VOICE_2_TEXT = "voice_2_text"
TEXT_2_VOICE = "text_2_voice"
TEXT_EMBEDDING ="text_embedding"
IMAGE_EMBEDDING ="image_embedding"
# class Function(TypedDict, total=False):
# name: Required[str]
@@ -155,11 +157,8 @@ class LLMResult:
self.compute_error_str = None
self.resp : str = "" # llm say:
self.raw_result = None # raw result from compute kernel
self.inner_functions : List[AIFunction] = []
self.action_list : List[ActionItem] = [] # op_list is a optimize design for saving token
#self.post_msgs : List[AgentMsg] = [] # move to op_list
# self.send_msgs : List[AgentMsg] = [] # move to op_list
#self.inner_functions : List[AIFunction] = []
self.action_list : List[ActionNode] = [] # op_list is a optimize design for saving token
@classmethod
@@ -185,9 +184,11 @@ class LLMResult:
r.resp = llm_json.get("resp")
r.raw_result = llm_json
action_list = llm_json.get("actions")
for action in action_list:
action_item = ActionItem.from_json(action)
r.action_list.append(action_item)
if action_list:
for action in action_list:
action_item = ActionNode.from_json(action)
if action_item:
r.action_list.append(action_item)
return r
@@ -215,7 +216,7 @@ class LLMResult:
lines = llm_result_str.splitlines()
is_need_wait = False
def check_args(action_item:ActionItem):
def check_args(action_item:ActionNode):
match action_item.name:
case "post_msg":# /post_msg $target_id
if len(action_item.args) != 1:
@@ -232,7 +233,7 @@ class LLMResult:
return False
current_action : ActionItem = None
current_action : ActionNode = None
for line in lines:
if line.startswith("##/"):
if current_action:
@@ -242,7 +243,7 @@ class LLMResult:
r.action_list.append(current_action)
action_name,action_args = LLMResult.parse_action(line[3:])
current_action = ActionItem(action_name,action_args)
current_action = ActionNode(action_name,action_args)
else:
if current_action:
current_action.append_body(line + "\n")