Bootstrap

《LLM入门教程》大模型教程笔记12:一、面向开发者的提示工程——8.聊天机器人(订餐聊天机器人代码)

代码

(代码不是很优化,仅供演示)

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入门教程》订餐聊天机器人

效果:

在这里插入图片描述

;