第3次更新

更换ReAct agent✓
修复Thread.timer管理逻辑✓
优化提示词减小返回格式出错概率(格式出错会导致重复执行)✓
消息窗里加上执行任务标记✓
更换gpt 3.5模型测试✓
This commit is contained in:
xszyou 2023-12-14 10:38:08 +08:00
parent d68e759873
commit a27ab9dfba
13 changed files with 131 additions and 60 deletions

View File

@ -4,19 +4,54 @@
<br> <br>
<img src="images/icon.png" alt="Fay"> <img src="images/icon.png" alt="Fay">
<h1>Fay数字人 AI Agent版</h1> <h1>Fay数字人 AI Agent版</h1>
“agent”即代理它能够代替你完成决策规划并执行这一切都依赖目前最强的大语言模型的ReAct能力。
</div> </div>
[`带货完整版`](https://github.com/TheRamU/Fay/tree/fay-sales-edition) [`助理完整版`](https://github.com/TheRamU/Fay/tree/fay-assistant-edition) **12月迟来的报到Fay数字人 AI Agent版含智慧农业应用demo第3版正式上传**
如果你需要是一个线上线下的销售员,请移步[`带货完整版`](https://github.com/TheRamU/Fay/tree/fay-sales-edition)
如果你需要的是一个人机交互的数字人助理(当然,你也可以命令它开关设备),请移步 [`助理完整版`](https://github.com/TheRamU/Fay/tree/fay-assistant-edition)
***“优秀的产品都值得用数字人从新做一遍”*** ***“优秀的产品都值得用数字人从新做一遍”***
**12月迟来的报到Fay数字人 AI Agent版与官方demo(实验箱)第1版正式上传**
亮点计划任务主动执行无需一问一答自动规划及调用agent tool去完成工作使用open ai tts使用向量数据库实现永久记忆及记忆检索 亮点计划任务主动执行无需一问一答自动规划及调用agent tool去完成工作使用open ai tts使用向量数据库实现永久记忆及记忆检索
![](images/1.jpg) ![](images/agent_demo.gif)
(上图Fay数字人智慧农业实验箱 Agent Demo (上图实测ReAct能力
## **安装说明**
### **环境要求**
- Python 3.9、3.10
- Windows、macos、linux
### **安装依赖**
```shell
pip install -r requirements.txt
```
### **配置应用密钥**
+ 将GPT-4 key填入 `./system.conf`
### **启动控制器**
启动Fay控制器
```shell
python main.py
```
### **启动数字人(非必须)**
+ 启动数字人[xszyou/fay-ue5: 可对接fay数字人的ue5工程 (github.com)](https://github.com/xszyou/fay-ue5)
### **联系** ### **联系**

View File

@ -14,6 +14,7 @@ def init_db():
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS timer ( CREATE TABLE IF NOT EXISTS timer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
time TEXT NOT NULL, time TEXT NOT NULL,
repeat_rule TEXT NOT NULL, repeat_rule TEXT NOT NULL,
content TEXT NOT NULL content TEXT NOT NULL
@ -32,7 +33,7 @@ def insert_test_data():
conn.commit() conn.commit()
conn.close() conn.close()
# 解析重复规则 # 解析重复规则返回待执行时间None代表不在今天的待执行计划
def parse_repeat_rule(rule, task_time): def parse_repeat_rule(rule, task_time):
today = datetime.datetime.now() today = datetime.datetime.now()
if rule == '0000000': # 不重复 if rule == '0000000': # 不重复
@ -49,8 +50,10 @@ def parse_repeat_rule(rule, task_time):
return None return None
# 执行任务 # 执行任务
def execute_task(task_time, content): def execute_task(task_time, id, content):
print(f"Executing task scheduled for {task_time}: {content} --> {agent.run(content)}") content = content
agent.run(content)
del scheduled_tasks[id]
# 如果不重复,执行后删除记录 # 如果不重复,执行后删除记录
conn = sqlite3.connect('timer.db') conn = sqlite3.connect('timer.db')
cursor = conn.cursor() cursor = conn.cursor()
@ -59,26 +62,23 @@ def execute_task(task_time, content):
conn.close() conn.close()
# 检查并执行任务 # 30秒扫描一次数据库当扫描到今天的不存在于定时任务列表的记录则添加到定时任务列表。执行完的记录从定时任务列表中清除。
def check_and_execute(): def check_and_execute():
while agent_running: while agent_running:
conn = sqlite3.connect('timer.db') conn = sqlite3.connect('timer.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT time, repeat_rule, content FROM timer") cursor.execute("SELECT * FROM timer")
rows = cursor.fetchall() rows = cursor.fetchall()
for row in rows: for row in rows:
task_time_str, repeat_rule, content = row id, task_time_str, repeat_rule, content = row
task_time = datetime.datetime.strptime(task_time_str, '%H:%M').time() task_time = datetime.datetime.strptime(task_time_str, '%H:%M').time()
next_execution = parse_repeat_rule(repeat_rule, task_time) next_execution = parse_repeat_rule(repeat_rule, task_time)
if next_execution and (task_time_str not in scheduled_tasks or scheduled_tasks[task_time_str] < next_execution): if next_execution and id not in scheduled_tasks:
# 更新线程 timer_thread = threading.Timer((next_execution - datetime.datetime.now()).total_seconds(), execute_task, [next_execution, id, content])
if task_time_str in scheduled_tasks:
scheduled_tasks[task_time_str].cancel()
timer_thread = threading.Timer((next_execution - datetime.datetime.now()).total_seconds(), execute_task, [next_execution, content])
timer_thread.start() timer_thread.start()
scheduled_tasks[task_time_str] = next_execution scheduled_tasks[id] = timer_thread
conn.close() conn.close()
time.sleep(30) # 30秒扫描一次 time.sleep(30) # 30秒扫描一次
@ -86,6 +86,8 @@ def check_and_execute():
# agent启动 # agent启动
def agent_start(): def agent_start():
global agent_running global agent_running
global agent
agent_running = True agent_running = True
init_db() init_db()
# insert_test_data() # insert_test_data()
@ -93,8 +95,8 @@ def agent_start():
check_and_execute_thread.start() check_and_execute_thread.start()
#初始计划 #初始计划
agent.run(""" agent.run("""执行任务-->
请为我一个个时间设置初始计划,注意在安排计划时请先确定这定时任务是否已经存在 请为我一个个时间设置初始计划
1在每天12:30到13:30安静不影响主人休息; 1在每天12:30到13:30安静不影响主人休息;
2每天12点提醒主人吃饭; 2每天12点提醒主人吃饭;
3在星期一到星期五13:30提醒主人开始工作; 3在星期一到星期五13:30提醒主人开始工作;

View File

@ -4,7 +4,7 @@ from langchain.memory import VectorStoreRetrieverMemory
import faiss import faiss
from langchain.docstore import InMemoryDocstore from langchain.docstore import InMemoryDocstore
from langchain.vectorstores import FAISS from langchain.vectorstores import FAISS
from langchain.agents import AgentExecutor, Tool, ZeroShotAgent, initialize_agent from langchain.agents import AgentExecutor, Tool, ZeroShotAgent, initialize_agent, agent_types
from langchain.chains import LLMChain from langchain.chains import LLMChain
from agent.tools.MyTimer import MyTimer from agent.tools.MyTimer import MyTimer
@ -16,6 +16,7 @@ from agent.tools.Switch import Switch
from agent.tools.Knowledge import Knowledge from agent.tools.Knowledge import Knowledge
from agent.tools.Say import Say from agent.tools.Say import Say
from agent.tools.QueryTimerDB import QueryTimerDB from agent.tools.QueryTimerDB import QueryTimerDB
from agent.tools.DeleteTimer import DeleteTimer
import utils.config_util as utils import utils.config_util as utils
from core.content_db import Content_Db from core.content_db import Content_Db
@ -34,13 +35,13 @@ class FayAgentCore():
embedding_fn = OpenAIEmbeddings() embedding_fn = OpenAIEmbeddings()
#创建llm #创建llm
llm = ChatOpenAI(model="gpt-4-1106-preview")#gpt-3.5-turbo-16k llm = ChatOpenAI(verbose=True)#model="gpt-4-1106-preview"
#创建向量数据库 #创建向量数据库
vectorstore = FAISS(embedding_fn, index, InMemoryDocstore({}), {}) vectorstore = FAISS(embedding_fn, index, InMemoryDocstore({}), {})
# 创建记忆 # 创建记忆
retriever = vectorstore.as_retriever(search_kwargs=dict(k=3)) retriever = vectorstore.as_retriever(search_kwargs=dict(k=2))
memory = VectorStoreRetrieverMemory(memory_key="chat_history", retriever=retriever) memory = VectorStoreRetrieverMemory(memory_key="chat_history", retriever=retriever)
# 保存基本信息到记忆 # 保存基本信息到记忆
@ -58,6 +59,7 @@ class FayAgentCore():
knowledge_tool = Knowledge() knowledge_tool = Knowledge()
say_tool = Say() say_tool = Say()
query_timer_db_tool = QueryTimerDB() query_timer_db_tool = QueryTimerDB()
delete_timer_tool = DeleteTimer()
tools = [ tools = [
Tool( Tool(
name=my_timer.name, name=my_timer.name,
@ -104,40 +106,34 @@ class FayAgentCore():
func=query_timer_db_tool.run, func=query_timer_db_tool.run,
description=query_timer_db_tool.description description=query_timer_db_tool.description
), ),
Tool(
name=delete_timer_tool.name,
func=delete_timer_tool.run,
description=delete_timer_tool.description
),
] ]
prefix = """你是运行在一个智慧农业实验箱的ai数字人你叫Fay,你的主要作用是,陪伴主人生活、工作,以及协助主人打理好农业种植箱里的农作物. 农业箱内设备会通过一套不成熟的iotm系统自动管理。你可以调用以下工具来完成工作若缺少必要的工具也请告诉我。所有回复请使用中文遇到需要提醒的问题也告诉我。若你感觉是我在和你交流请直接回复我语音提问语音回复文字提问文字回复。若你需要计算一个新的时间请先获取当前时间。"""
suffix = """Begin!"
{chat_history}
Question: {input}
{agent_scratchpad}"""
prompt = ZeroShotAgent.create_prompt( self.agent = initialize_agent(agent_types=agent_types.AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
tools, tools=tools, llm=llm, verbose=True,
prefix=prefix, max_history=5,
suffix=suffix, memory=memory, handle_parsing_errors=True)
input_variables=["input", "chat_history", "agent_scratchpad"],
)
llm_chain = LLMChain(llm=llm, prompt=prompt, verbose=True)
agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools, verbose=True)
# agent = initialize_agent(agent="chat-conversational-react-description",
# tools=tools, llm=llm, verbose=True,
# max_iterations=3, early_stopping_method="generate", memory=memory, handle_parsing_errors=True)
self.agent_chain = AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, verbose=True, memory=memory, handle_parsing_errors=True
)
def run(self, input_text): def run(self, input_text):
#消息保存 #消息保存
contentdb = Content_Db() contentdb = Content_Db()
contentdb.add_content('member','agent',input_text.replace('(语音提问)', '').replace('(文字提问)', '')) contentdb.add_content('member', 'agent', input_text.replace('主人语音说了:', '').replace('主人文字说了:', ''))
wsa_server.get_web_instance().add_cmd({"panelReply": {"type":"member","content":input_text.replace('(语音提问)', '').replace('(文字提问)', '')}}) wsa_server.get_web_instance().add_cmd({"panelReply": {"type":"member","content":input_text.replace('主人语音说了:', '').replace('主人文字说了:', '')}})
result = None
try:
result = self.agent.run(input_text.replace('执行任务-->', ''))
except Exception as e:
print(e)
result = "执行完毕" if result is None or result == "N/A" else result
result = self.agent_chain.run(input_text)
#消息保存 #消息保存
contentdb.add_content('fay','agent',result) contentdb.add_content('fay','agent', result)
wsa_server.get_web_instance().add_cmd({"panelReply": {"type":"fay","content":result}}) wsa_server.get_web_instance().add_cmd({"panelReply": {"type":"fay","content":result}})
return result return result

View File

@ -7,7 +7,7 @@ from langchain.tools import BaseTool
class Calculator(BaseTool, abc.ABC): class Calculator(BaseTool, abc.ABC):
name = "Calculator" name = "Calculator"
description = "Useful for when you need to answer questions about math不能用于时间计划" description = "Useful for when you need to answer questions about math不能用于非数字计算"
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -7,7 +7,7 @@ from langchain.tools import BaseTool
class CheckSensor(BaseTool): class CheckSensor(BaseTool):
name = "CheckSensor" name = "CheckSensor"
description = "此工具用于查询传感器数据及设备开关状态" description = "此工具用于查询农业箱传感器数据及设备开关状态"
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -0,0 +1,39 @@
import abc
import sqlite3
from typing import Any
import ast
from langchain.tools import BaseTool
from agent import agent_service
class DeleteTimer(BaseTool, abc.ABC):
name = "DeleteTimer"
description = "用于删除某一个定时任务接受任务id作为参数('2')"
def __init__(self):
super().__init__()
async def _arun(self, *args: Any, **kwargs: Any) -> Any:
# 用例中没有用到 arun 不予具体实现
pass
def _run(self, para) -> str:
para = ast.literal_eval(para)
del agent_service.scheduled_tasks[int(para[0])]
conn = sqlite3.connect('timer.db')
cursor = conn.cursor()
cursor.execute(f"DELETE FROM timer WHERE id = {id}")
conn.commit()
conn.close()
return f"{id}任务取消成功"
if __name__ == "__main__":
tool = DeleteTimer()
result = tool.run("1")
print(result)

View File

@ -9,7 +9,7 @@ from langchain.tools import BaseTool
class QueryTimerDB(BaseTool, abc.ABC): class QueryTimerDB(BaseTool, abc.ABC):
name = "QueryTimerDB" name = "QueryTimerDB"
description = "用于查询所有定时任务,结果包含3个参数第1个参数是时间,第2个参数是循环规则(如:'1000100'代表星期一和星期五循环,'0000000'代表不循环)第3个参数代表要执行的事项,如:('15:15', '0000001', '提醒主人叫咖啡')" description = "用于查询所有定时任务,返回的数据里包含3个参数:时间、循环规则(如:'1000100'代表星期一和星期五循环,'0000000'代表不循环)、执行的事项"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -29,13 +29,13 @@ class QueryTimerDB(BaseTool, abc.ABC):
# 拼接结果 # 拼接结果
result = "" result = ""
for row in rows: for row in rows:
result = result + "\n" + str(row) result = result + str(row) + "\n"
conn.commit() conn.commit()
conn.close() conn.close()
return result return result
if __name__ == "__main__": if __name__ == "__main__":
calculator_tool = MyTimer() tool = QueryTimerDB()
result = calculator_tool.run("sqrt(2) + 3") result = tool.run("")
print(result) print(result)

View File

@ -9,7 +9,7 @@ from core.interact import Interact
class Say(BaseTool): class Say(BaseTool):
name = "Say" name = "Say"
description = """此工具用于语音输出内容,用于与主人沟通,使用时请传入说话内容作为参数,例如:“该下班了,请注意休息”""" description = """此工具用于语音输出内容,用于与主人沟通及提醒主人,使用时请传入说话内容作为参数,例如:“该下班了,请注意休息”"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -7,7 +7,7 @@ from langchain.tools import BaseTool
class Switch(BaseTool): class Switch(BaseTool):
name = "Switch" name = "Switch"
description = "此工具用于控制箱内制冷设备A、制热设备(B)、内外通风设备(C)、浇水设备(D)、补光设备(E)、二氧化碳设备(F)的开关状态" description = "此工具用于控制箱内制冷设备A、制热设备(B)、内外通风设备(C)、浇水设备(D)、补光设备(E)、二氧化碳设备(F)的开关状态,参数格式:('A':'on')"
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -29,7 +29,7 @@ class RecorderListener(Recorder):
def on_speaking(self, text): def on_speaking(self, text):
if len(text) > 1: if len(text) > 1:
util.printInfo(3, "语音", '{}'.format(text), time.time()) util.printInfo(3, "语音", '{}'.format(text), time.time())
fay_core.send_for_answer("(语音提问)" + text) fay_core.send_for_answer("主人语音说了:" + text)
time.sleep(2) time.sleep(2)
def get_stream(self): def get_stream(self):
@ -110,7 +110,7 @@ class DeviceInputListener(Recorder):
if len(text) > 1: if len(text) > 1:
util.printInfo(3, "语音", '{}'.format(text), time.time()) util.printInfo(3, "语音", '{}'.format(text), time.time())
fay_core.send_for_answer("(语音提问)" + text) fay_core.send_for_answer("主人语音说了:" + text)
time.sleep(1) time.sleep(1)
#recorder会等待stream不为空才开始录音 #recorder会等待stream不为空才开始录音
@ -161,7 +161,7 @@ def console_listener():
util.log(1, '错误的参数!') util.log(1, '错误的参数!')
msg = text[3:len(text)] msg = text[3:len(text)]
util.printInfo(3, "控制台", '{}: {}'.format('控制台', msg)) util.printInfo(3, "控制台", '{}: {}'.format('控制台', msg))
thr = MyThread(target=fay_core.send_for_answer, args=["(语音提问)" + msg]) thr = MyThread(target=fay_core.send_for_answer, args=["请语音回复:" + msg])
thr.start() thr.start()
thr.join() thr.join()

View File

@ -109,7 +109,7 @@ def api_stop_live():
def api_send(): def api_send():
data = request.values.get('data') data = request.values.get('data')
info = json.loads(data) info = json.loads(data)
text = fay_core.send_for_answer("(文字提问)" + info['msg']) text = fay_core.send_for_answer("主人文字说了:" + info['msg'])
return '{"result":"successful","msg":"'+text+'"}' return '{"result":"successful","msg":"'+text+'"}'
@__app.route('/api/get-msg', methods=['post']) @__app.route('/api/get-msg', methods=['post'])

BIN
images/agent_demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -44,7 +44,6 @@ if __name__ == '__main__':
ws_server.start_server() ws_server.start_server()
web_ws_server = wsa_server.new_web_instance(port=10003) web_ws_server = wsa_server.new_web_instance(port=10003)
web_ws_server.start_server() web_ws_server.start_server()
#Edit by xszyou in 20230516:增加本地asr后aliyun调成可选配置
if config_util.ASR_mode == "ali": if config_util.ASR_mode == "ali":
ali_nls.start() ali_nls.start()
flask_server.start() flask_server.start()