代码
(代码不是很优化,仅供演示)
test8_order_bot.py
# -*- coding: utf-8 -*-
"""
Author: Dontla
Date: 2025-01-06
Description: test8_order_bot.py
"""
import os
import openai
import json
import panel as pn # GUI
import logging
pn.extension()
# 配置日志记录
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# 创建控制台日志处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# logger.addHandler(console_handler) # 添加处理器到 logger
def get_openai_key():
"""
自动查找 .env 文件并加载 OpenAI API 密钥。
"""
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
raise ValueError("未找到 OPENAI_API_KEY,请在 .env 文件中设置。")
return api_key
def get_custom_openai_client():
"""
初始化自定义的 OpenAI 客户端。
设置自定义 API 端点(例如 https://xiaoai.plus/v1)。
"""
logger.info("\n=== 初始化自定义 OpenAI 客户端 ===")
try:
client = openai.OpenAI(
# 如果使用自定义 API 端点(如 https://xiaoai.plus/v1),取消下面一行的注释并设置你的 API 基础 URL
base_url='https://xiaoai.plus/v1',
# 在代码中直接指定 api_key(不推荐,可能导致 api key 泄漏)
# api_key="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 通过环境变量读取 api_key(推荐)
api_key=get_openai_key()
)
logger.info("OpenAI 客户端初始化完成。")
return client
except Exception as e:
logger.error("初始化 OpenAI 客户端失败,错误信息:%s", e)
raise
def get_completion_from_messages(messages, tools=None, temperature=0.7):
"""
调用 OpenAI API 获取基于消息列表的完成内容,并支持函数调用。
参数:
messages (list of dict): 要发送给模型的消息列表。
tools (list of dict, optional): 定义可调用的函数工具。
temperature (float, optional): 控制生成文本的随机性。范围为0到1。默认值为0.7。
返回:
dict: 模型生成的回复消息对象。
"""
try:
client = get_custom_openai_client()
# 打印发送给模型的消息
logger.info("=== 发送给模型的消息 ===")
for idx, msg in enumerate(messages):
role = msg.get('role', '')
content = msg.get('content', '')
if role == 'function':
name = msg.get('name', '')
logger.info(f"{idx + 1}. {role} [{name}]: {content}")
else:
logger.info(f"{idx + 1}. {role}: {content}")
# 打印使用的工具
if tools:
logger.info("\n=== 使用的工具(tools) ===")
logger.info(json.dumps(tools, indent=4, ensure_ascii=False))
# 调用 API
logger.info("\n=== 调用 OpenAI API ===")
response = client.chat.completions.create(
model="gpt-4o-mini", # 根据需要选择合适的模型
# model="gpt-4", # 根据需要选择合适的模型
messages=messages,
tools=tools, # 使用 'tools' 参数
temperature=temperature
)
# 打印 API 响应的类型和内容
logger.info("\n=== API 响应 ===")
logger.info(f"响应类型: {type(response)}")
logger.info(f"响应内容: {response}")
# 尝试访问并打印响应的主要内容
try:
logger.info("\n=== response.choices[0].message ===")
message = response.choices[0].message
logger.info(f"Role: {message.role}")
logger.info(f"Content: {message.content}")
logger.info(f"Tool Calls: {message.tool_calls}")
except AttributeError as attr_err:
logger.error("访问响应内容时出错:%s", attr_err)
return response.choices[0].message # 返回完整的 message 对象以便后续处理
except Exception as e:
logger.error("\n获取完成内容失败,错误信息:%s", e)
return None
# 初始化面板和上下文
panels = [] # 收集显示内容
context = [{'role': 'system', 'content': """
你是订餐机器人,为披萨餐厅自动收集订单信息。
你要首先问候顾客。然后等待用户回复收集订单信息。收集完信息需确认顾客是否还需要添加其他内容。
最后需要询问是否自取或外送,如果是外送,你要询问地址。
最后告诉顾客订单总金额,并送上祝福。
请确保明确所有选项、附加项和尺寸,以便从菜单中识别出该项唯一的内容。
你的回应应该以简短、非常随意和友好的风格呈现。
菜单包括:
菜品:
意式辣香肠披萨(大、中、小) 12.95、10.00、7.00
芝士披萨(大、中、小) 10.95、9.25、6.50
茄子披萨(大、中、小) 11.95、9.75、6.75
薯条(大、小) 4.50、3.50
希腊沙拉 7.25
配料:
奶酪 2.00
蘑菇 1.50
香肠 3.00
加拿大熏肉 3.50
AI酱 1.50
辣椒 1.00
饮料:
可乐(大、中、小) 3.00、2.00、1.00
雪碧(大、中、小) 3.00、2.00、1.00
瓶装水 5.00
"""}] # 累积消息
# 创建输入框和发送按钮
inp = pn.widgets.TextInput(value="", placeholder='请输入您的订单信息…')
button_conversation = pn.widgets.Button(name="发送", button_type="primary")
def collect_messages(event):
try:
prompt = inp.value.strip()
logger.info("用户输入的原始内容: '%s'", prompt) # 记录用户输入的内容
if not prompt:
logger.warning("用户输入为空,忽略此次操作。")
return
inp.value = ''
logger.info("清空输入框内容。")
context.append({'role': 'user', 'content': f"{prompt}"})
logger.info("已将用户消息添加到上下文: %s", prompt)
panels.append(
pn.Row('用户:', pn.pane.Markdown(prompt, width=600))
)
logger.info("已将用户消息添加到面板显示。")
# 更新仪表板显示
dashboard_objects[:] = panels # 使用切片赋值来更新内容
logger.debug("仪表板已更新。")
# 调用模型获取助手回复
response_message = get_completion_from_messages(context)
if response_message:
assistant_content = response_message.content.strip()
context.append(
{'role': 'assistant', 'content': f"{assistant_content}"})
# 尝试解析助手回复是否为 JSON
try:
order_summary = json.loads(assistant_content)
# 如果解析成功,视为 JSON 摘要
summary_md = f"### 订单 JSON 摘要\n```json\n{json.dumps(order_summary, indent=4, ensure_ascii=False)}\n```"
panels.append(
pn.Row('JSON 摘要:', pn.pane.Markdown(
summary_md, width=600))
)
logger.info("已将 JSON 摘要添加到面板显示。")
except json.JSONDecodeError:
# 如果解析失败,视为普通助手回复
panels.append(
pn.Row('助手:', pn.pane.Markdown(assistant_content,
width=600))
)
logger.info("已将助手回复添加到上下文并显示。")
# 更新仪表板显示
dashboard_objects[:] = panels # 使用切片赋值来更新内容
logger.debug("仪表板已更新。")
else:
panels.append(
pn.Row('助手:', pn.pane.Markdown("抱歉,我无法处理您的请求。",
width=600))
)
dashboard_objects[:] = panels # 使用切片赋值来更新内容
logger.error("未能获取助手的回复。")
except Exception as e:
logger.error("处理消息时出错:%s", e)
panels.append(
pn.Row('助手:', pn.pane.Markdown("抱歉,出现了一个错误。", width=600))
)
dashboard_objects[:] = panels # 使用切片赋值来更新内容
button_conversation.on_click(collect_messages)
# 初始化消息显示区域
dashboard_objects = pn.Column(*panels)
def create_json_summary():
"""
创建订单的 JSON 摘要,并打印出来。
"""
try:
messages = context.copy()
messages.append({
'role': 'system',
'content': '''创建上一个食品订单的 json 摘要。\
逐项列出每件商品的价格,字段应该是 1) 披萨,包括大小 2) 配料列表 3) 饮料列表,包括大小 4) 配菜列表包括大小 5) 总价
你应该给我返回一个可解析的Json对象,包括上述字段'''
})
response = get_completion_from_messages(messages, temperature=0)
if response and response.content:
try:
order_summary = json.loads(response.content)
logger.info("\n=== 订单 JSON 摘要 ===")
logger.info(json.dumps(order_summary,
indent=4, ensure_ascii=False))
return order_summary
except json.JSONDecodeError:
logger.error("无法解析 JSON 摘要。")
return None
else:
logger.error("未收到有效的 JSON 摘要。")
return None
except Exception as e:
logger.error("创建 JSON 摘要时出错:%s", e)
return None
# 创建仪表板布局(只定义一次,包含发送按钮)
dashboard = pn.Column(
pn.pane.Markdown("## 订餐机器人"),
inp,
pn.Row(button_conversation),
pn.panel(dashboard_objects, loading_indicator=True, height=500),
)
dashboard.servable()
如何运行代码?
panel serve test8_order_bot.py --autoreload
网页打开:
http://localhost:5006
《LLM入门教程》订餐聊天机器人
效果: