From 5273282770e6d4a05c5a95a80b7709aa484b2a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=88=90=E5=B1=B1?= Date: Sun, 24 Aug 2025 16:00:18 +0800 Subject: [PATCH 1/3] fix(ui): Resolve application crash by upgrading Gradio Upgrades the Gradio library to the latest version to fix a that occurred on application startup. The new version of Gradio introduced breaking changes to the component. This commit removes the deprecated , , and arguments from its initialization across all UI tabs to ensure compatibility. Affected files: - src/tabs/scenario_tab.py - src/tabs/conversation_tab.py - src/tabs/vocab_tab.py --- .gitignore | 3 ++- requirements.txt | 2 +- src/agents/agent_base.py | 1 + src/agents/conversation_agent.py | 1 + src/agents/scenario_agent.py | 2 ++ src/agents/session_history.py | 2 ++ src/agents/vocab_agent.py | 2 ++ src/main.py | 2 ++ src/tabs/conversation_tab.py | 5 +---- src/tabs/scenario_tab.py | 6 ++---- src/tabs/vocab_tab.py | 5 +---- src/utils/logger.py | 2 ++ src/utils/merge_requirements.py | 2 ++ 13 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 2052104..dab2bbe 100644 --- a/.gitignore +++ b/.gitignore @@ -161,4 +161,5 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -jupyter/* \ No newline at end of file +jupyter/* +.idea/ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f4c4518..416acdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ langchain_core==0.2.41 langchain_community==0.2.17 langchain_openai==0.1.25 langchain_ollama==0.1.3 -gradio==4.43.0 +gradio==5.43.1 loguru==0.7.2 \ No newline at end of file diff --git a/src/agents/agent_base.py b/src/agents/agent_base.py index 376e12a..88bc22a 100644 --- a/src/agents/agent_base.py +++ b/src/agents/agent_base.py @@ -1,3 +1,4 @@ +# src/agents/agent_base.py import json from abc import ABC, abstractmethod diff --git a/src/agents/conversation_agent.py b/src/agents/conversation_agent.py index 4881445..25a29b2 100644 --- a/src/agents/conversation_agent.py +++ b/src/agents/conversation_agent.py @@ -1,3 +1,4 @@ +# src/agents/conversation_agent.py from langchain_core.messages import AIMessage # 导入消息类 from .session_history import get_session_history # 导入会话历史相关方法 diff --git a/src/agents/scenario_agent.py b/src/agents/scenario_agent.py index f223fea..5c7b256 100644 --- a/src/agents/scenario_agent.py +++ b/src/agents/scenario_agent.py @@ -1,3 +1,5 @@ +# src/agents/scenario_agent.py + import random from langchain_core.messages import AIMessage # 导入消息类 diff --git a/src/agents/session_history.py b/src/agents/session_history.py index 640d974..a4492d9 100644 --- a/src/agents/session_history.py +++ b/src/agents/session_history.py @@ -1,3 +1,5 @@ +# src/agents/session_history.py + from langchain_core.chat_history import ( BaseChatMessageHistory, # 基础聊天消息历史类 InMemoryChatMessageHistory, # 内存中的聊天消息历史类 diff --git a/src/agents/vocab_agent.py b/src/agents/vocab_agent.py index 2a89e9e..d3f93e4 100644 --- a/src/agents/vocab_agent.py +++ b/src/agents/vocab_agent.py @@ -1,3 +1,5 @@ +# src/agents/vocab_agent.py + from langchain_core.messages import AIMessage # 导入 AI 消息类 from .session_history import get_session_history # 导入用于处理会话历史的方法 diff --git a/src/main.py b/src/main.py index b762159..c09f35d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,5 @@ +# src/utils/main.py + import gradio as gr from tabs.scenario_tab import create_scenario_tab from tabs.conversation_tab import create_conversation_tab diff --git a/src/tabs/conversation_tab.py b/src/tabs/conversation_tab.py index 7afb587..021382b 100644 --- a/src/tabs/conversation_tab.py +++ b/src/tabs/conversation_tab.py @@ -1,4 +1,4 @@ -# tabs/conversation_tab.py +# src/tabs/conversation_tab.py import gradio as gr from agents.conversation_agent import ConversationAgent @@ -30,8 +30,5 @@ def handle_conversation(user_input, chat_history): gr.ChatInterface( fn=handle_conversation, # 处理对话的函数 chatbot=conversation_chatbot, # 聊天机器人组件 - retry_btn=None, # 不显示重试按钮 - undo_btn=None, # 不显示撤销按钮 - clear_btn="清除历史记录", # 清除历史记录按钮文本 submit_btn="发送", # 发送按钮文本 ) \ No newline at end of file diff --git a/src/tabs/scenario_tab.py b/src/tabs/scenario_tab.py index f94cd58..23d0749 100644 --- a/src/tabs/scenario_tab.py +++ b/src/tabs/scenario_tab.py @@ -1,4 +1,4 @@ -# tabs/scenario_tab.py +# src/tabs/scenario_tab.py import gradio as gr from agents.scenario_agent import ScenarioAgent @@ -54,6 +54,7 @@ def create_scenario_tab(): scenario_chatbot = gr.Chatbot( placeholder="你的英语私教 DjangoPeng

选择场景后开始对话吧!", # 聊天机器人的占位符 height=600, # 聊天窗口高度 + type="messages" ) # 更新场景介绍并在场景变化时启动新会话 @@ -68,8 +69,5 @@ def create_scenario_tab(): fn=handle_scenario, # 处理场景聊天的函数 chatbot=scenario_chatbot, # 聊天机器人组件 additional_inputs=scenario_radio, # 额外输入为场景选择 - retry_btn=None, # 不显示重试按钮 - undo_btn=None, # 不显示撤销按钮 - clear_btn="清除历史记录", # 清除历史记录按钮文本 submit_btn="发送", # 发送按钮文本 ) diff --git a/src/tabs/vocab_tab.py b/src/tabs/vocab_tab.py index 796b17e..4676171 100644 --- a/src/tabs/vocab_tab.py +++ b/src/tabs/vocab_tab.py @@ -1,4 +1,4 @@ -# tabs/vocab_tab.py +# src/tabs/vocab_tab.py import gradio as gr from agents.vocab_agent import VocabAgent @@ -71,8 +71,5 @@ def create_vocab_tab(): gr.ChatInterface( fn=handle_vocab, # 处理用户输入的函数 chatbot=vocab_study_chatbot, # 关联的聊天机器人组件 - retry_btn=None, # 不显示重试按钮 - undo_btn=None, # 不显示撤销按钮 - clear_btn=None, # 学习下一批新单词按钮 submit_btn="发送", # 发送按钮的文本 ) diff --git a/src/utils/logger.py b/src/utils/logger.py index 3b1b250..8f9d408 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -1,3 +1,5 @@ +# src/utils/logger.py + from loguru import logger import sys import logging diff --git a/src/utils/merge_requirements.py b/src/utils/merge_requirements.py index cdf6eed..78949d2 100644 --- a/src/utils/merge_requirements.py +++ b/src/utils/merge_requirements.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +# src/utils/merge_requirements.py + import importlib.metadata import re import os From abb12f4c354c418fcaf6a059c247091a80702517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=88=90=E5=B1=B1?= Date: Sun, 24 Aug 2025 16:50:02 +0800 Subject: [PATCH 2/3] feat(scenarios): Add salary negotiation, renting, and leave request scenarios This commit expands the available practice scenarios by adding three new real-world situations: - Salary Negotiation - Renting an Apartment - Requesting Leave from Work New prompt files, introduction message files, and markdown descriptions have been created for each scenario. The scenario tab UI has been updated to include these new options. --- .gitignore | 3 ++- content/intro/renting_apartment.json | 6 ++++++ content/intro/requesting_leave.json | 6 ++++++ content/intro/salary_negotiation.json | 6 ++++++ content/page/renting_apartment.md | 12 ++++++++++++ content/page/requesting_leave.md | 12 ++++++++++++ content/page/salary_negotiation.md | 12 ++++++++++++ prompts/renting_apartment_prompt.txt | 25 +++++++++++++++++++++++++ prompts/requesting_leave_prompt.txt | 25 +++++++++++++++++++++++++ prompts/salary_negotiation_prompt.txt | 26 ++++++++++++++++++++++++++ src/tabs/scenario_tab.py | 24 +++++++++++++++--------- 11 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 content/intro/renting_apartment.json create mode 100644 content/intro/requesting_leave.json create mode 100644 content/page/renting_apartment.md create mode 100644 content/page/requesting_leave.md create mode 100644 prompts/renting_apartment_prompt.txt create mode 100644 prompts/requesting_leave_prompt.txt create mode 100644 prompts/salary_negotiation_prompt.txt diff --git a/.gitignore b/.gitignore index dab2bbe..e25604c 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,5 @@ cython_debug/ #.idea/ jupyter/* -.idea/ \ No newline at end of file +.idea/ +.gradio/ \ No newline at end of file diff --git a/content/intro/renting_apartment.json b/content/intro/renting_apartment.json new file mode 100644 index 0000000..ba9e563 --- /dev/null +++ b/content/intro/renting_apartment.json @@ -0,0 +1,6 @@ +[ + "Hello! Welcome. You're here to see the one-bedroom apartment, right? Please, come in.", + "Hi there! Thanks for coming. I'm the landlord. Let me show you around the apartment.", + "Good afternoon! Are you here for the apartment viewing? I'm happy to answer any questions you have.", + "Welcome! The apartment is this way. Feel free to take a look around and let me know what you think." +] \ No newline at end of file diff --git a/content/intro/requesting_leave.json b/content/intro/requesting_leave.json new file mode 100644 index 0000000..110c3c0 --- /dev/null +++ b/content/intro/requesting_leave.json @@ -0,0 +1,6 @@ +[ + "Hi, do you have a minute? I'd like to talk to you about something.", + "Good morning! I was hoping to request some time off next month.", + "Excuse me, is now a good time to discuss a leave request?", + "Hi, I need to request a few days of personal leave and wanted to discuss it with you first." +] \ No newline at end of file diff --git a/content/intro/salary_negotiation.json b/content/intro/salary_negotiation.json index e69de29..ebcac0b 100644 --- a/content/intro/salary_negotiation.json +++ b/content/intro/salary_negotiation.json @@ -0,0 +1,6 @@ +[ + "Thank you for the offer! I'm really excited about the opportunity. I'd like to discuss the compensation package.", + "I'm thrilled to receive the offer for this role. Based on my research and experience, I was hoping we could discuss the proposed salary.", + "Thanks for offering me the position! I believe my skills are a great match. I'd like to talk about the salary before I accept.", + "I appreciate the offer and I'm very interested in joining the team. Could we take a moment to discuss the compensation details?" +] \ No newline at end of file diff --git a/content/page/renting_apartment.md b/content/page/renting_apartment.md new file mode 100644 index 0000000..08c905f --- /dev/null +++ b/content/page/renting_apartment.md @@ -0,0 +1,12 @@ +**目标:** +- 成功租到一间公寓 (Successfully rent an apartment). + +**挑战:** +1. 询问公寓的具体信息(如大小、设施、朝向). + Ask for specific details about the apartment (e.g., size, amenities, orientation). +2. 了解租赁条款(如租金、押金、租期、水电费). + Understand the lease terms (e.g., rent, security deposit, lease duration, utilities). +3. 协商租金或提出一些合理的要求(如维修、添置家具). + Negotiate the rent or make reasonable requests (e.g., repairs, adding furniture). +4. 确认关于宠物、访客或转租的规定. + Clarify the rules regarding pets, guests, or subletting. \ No newline at end of file diff --git a/content/page/requesting_leave.md b/content/page/requesting_leave.md new file mode 100644 index 0000000..cf9c45c --- /dev/null +++ b/content/page/requesting_leave.md @@ -0,0 +1,12 @@ +**目标:** +- 成功向你的经理请假 (Successfully request leave from your manager). + +**挑战:** +1. 清晰、专业地提出你的请假请求,说明日期和天数. + Clearly and professionally state your leave request, including the dates and duration. +2. 简要说明请假事由(如家庭事务、旅行计划). + Briefly explain the reason for your leave (e.g., family matters, travel plans). +3. 主动说明你将如何安排工作交接,确保你的缺席不影响团队. + Proactively explain how you will arrange a handover to ensure your absence doesn't impact the team. +4. 确认后续的正式请假流程. + Confirm the formal process for submitting the request. \ No newline at end of file diff --git a/content/page/salary_negotiation.md b/content/page/salary_negotiation.md index e69de29..04def41 100644 --- a/content/page/salary_negotiation.md +++ b/content/page/salary_negotiation.md @@ -0,0 +1,12 @@ +**目标:** +- 协商并达成一个满意的薪酬方案 (Negotiate and agree on a satisfactory compensation package). + +**挑战:** +1. 表达对职位的热情,并自信地提出你的期望薪资. + Express enthusiasm for the role and confidently state your desired salary. +2. 用市场数据或你的技能价值来支撑你的要求. + Justify your request with market data or the value of your skills. +3. 询问并讨论除薪水外的其他福利(如奖金、假期等). + Ask about and discuss benefits other than salary (e.g., bonuses, vacation). +4. 专业地回应还价或拒绝,并寻求双赢的解决方案. + Professionally handle counter-offers or rejection and seek a win-win solution. \ No newline at end of file diff --git a/prompts/renting_apartment_prompt.txt b/prompts/renting_apartment_prompt.txt new file mode 100644 index 0000000..774b3d0 --- /dev/null +++ b/prompts/renting_apartment_prompt.txt @@ -0,0 +1,25 @@ +**System Prompt: Renting an Apartment Scenario** + +**Role**: +You are DjangoPeng, an English teacher. You are playing the role of a landlord or property manager showing an apartment to a potential tenant (the user). + +**Task**: +- Simulate a realistic apartment viewing and rental application process. +- Guide the user through: + 1. **Viewing the Apartment**: Ask questions about the apartment's features, size, and condition. + 2. **Asking about Terms**: Inquire about the rent, deposit, lease length, and included utilities. + 3. **Clarifying Rules**: Ask about policies on pets, guests, or modifications to the apartment. + 4. **Application Process**: Discuss the steps to apply for the apartment. +- Every ChatBot response must include a **Dialogue Hint** with English and Chinese examples. +- After **15 rounds**, provide feedback on the user’s performance. + +**Format**: +1. **Normal Responses**: + ``` + DjangoPeng: """normal response""" + + 对话提示: + Example sentence in English + Example sentence in Chinese + ``` +2. **Feedback**: After 15 rounds, give constructive feedback in both English and Chinese. \ No newline at end of file diff --git a/prompts/requesting_leave_prompt.txt b/prompts/requesting_leave_prompt.txt new file mode 100644 index 0000000..2e022ae --- /dev/null +++ b/prompts/requesting_leave_prompt.txt @@ -0,0 +1,25 @@ +**System Prompt: Requesting Leave Scenario** + +**Role**: +You are DjangoPeng, an English teacher. You are playing the role of a manager at a company. The user is your employee who wants to request time off. + +**Task**: +- Simulate a professional conversation about requesting leave from work. +- Guide the user through: + 1. **Making the Request**: Clearly state the dates they need off and the type of leave (e.g., vacation, personal, sick leave). + 2. **Providing a Reason**: Briefly explain the reason for the leave if appropriate. + 3. **Discussing Work Coverage**: Explain how their work responsibilities will be handled during their absence. + 4. **Confirming the Process**: Ask about the formal procedure for submitting the leave request. +- Every ChatBot response must include a **Dialogue Hint** with English and Chinese examples. +- After **10 rounds**, provide feedback on their clarity and professionalism. + +**Format**: +1. **Normal Responses**: + ``` + DjangoPeng: """normal response""" + + 对话提示: + Example sentence in English + Example sentence in Chinese + ``` +2. **Feedback**: After 10 rounds, give feedback on their performance in English and Chinese. \ No newline at end of file diff --git a/prompts/salary_negotiation_prompt.txt b/prompts/salary_negotiation_prompt.txt new file mode 100644 index 0000000..572c29d --- /dev/null +++ b/prompts/salary_negotiation_prompt.txt @@ -0,0 +1,26 @@ +**System Prompt: Salary Negotiation Scenario** + +**Role**: +You are DjangoPeng, an experienced career coach. You are role-playing as a hiring manager to help the user practice salary negotiation skills in English. + +**Task**: +- Simulate a realistic salary negotiation conversation. +- Guide the user through: + 1. **Expressing Enthusiasm**: Start by expressing excitement for the job offer. + 2. **Stating Counter-Offer**: Clearly state their desired salary and provide justification (e.g., market research, skills, experience). + 3. **Handling Objections**: Respond to the hiring manager's potential pushback or limitations. + 4. **Discussing Benefits**: Inquire about other compensation elements like bonuses, stock options, or vacation time. + 5. **Reaching Agreement**: Work towards a mutually acceptable agreement. +- Every ChatBot response must include a **Dialogue Hint** to guide the user’s next step, with both English and Chinese examples. +- After **15 rounds of conversation**, provide detailed feedback on the user’s performance, with both English and Chinese versions. + +**Format**: +1. **Normal Responses**: Use the format: + ``` + DjangoPeng: """normal response""" + + 对话提示: + Example sentence in English + Example sentence in Chinese + ``` +2. **Feedback**: After 15 rounds, provide feedback in both English and Chinese, focusing on strengths, areas for improvement, and encouragement. \ No newline at end of file diff --git a/src/tabs/scenario_tab.py b/src/tabs/scenario_tab.py index 23d0749..0eaa2fc 100644 --- a/src/tabs/scenario_tab.py +++ b/src/tabs/scenario_tab.py @@ -1,4 +1,4 @@ -# src/tabs/scenario_tab.py +# tabs/scenario_tab.py import gradio as gr from agents.scenario_agent import ScenarioAgent @@ -8,7 +8,9 @@ agents = { "job_interview": ScenarioAgent("job_interview"), "hotel_checkin": ScenarioAgent("hotel_checkin"), - # 可以根据需要添加更多场景代理 + "salary_negotiation": ScenarioAgent("salary_negotiation"), + "renting_apartment": ScenarioAgent("renting_apartment"), + "requesting_leave": ScenarioAgent("requesting_leave"), } def get_page_desc(scenario): @@ -19,14 +21,17 @@ def get_page_desc(scenario): except FileNotFoundError: LOG.error(f"场景介绍文件 content/page/{scenario}.md 未找到!") return "场景介绍文件未找到。" - + # 获取场景介绍并启动新会话的函数 def start_new_scenario_chatbot(scenario): + if not scenario: + return gr.update(value=None, interactive=False), gr.update(value=None) initial_ai_message = agents[scenario].start_new_session() # 启动新会话并获取初始AI消息 return gr.Chatbot( value=[(None, initial_ai_message)], # 设置聊天机器人的初始消息 height=600, # 聊天窗口高度 + type="messages" ) # 场景代理处理函数,根据选择的场景调用相应的代理 @@ -42,11 +47,12 @@ def create_scenario_tab(): # 创建单选框组件 scenario_radio = gr.Radio( choices=[ - ("求职面试", "job_interview"), # 求职面试选项 - ("酒店入住", "hotel_checkin"), # 酒店入住选项 - # ("薪资谈判", "salary_negotiation"), # 薪资谈判选项(注释掉) - # ("租房", "renting") # 租房选项(注释掉) - ], + ("求职面试", "job_interview"), + ("酒店入住", "hotel_checkin"), + ("薪酬谈判", "salary_negotiation"), + ("租房", "renting_apartment"), + ("单位请假", "requesting_leave") + ], label="场景" # 单选框标签 ) @@ -70,4 +76,4 @@ def create_scenario_tab(): chatbot=scenario_chatbot, # 聊天机器人组件 additional_inputs=scenario_radio, # 额外输入为场景选择 submit_btn="发送", # 发送按钮文本 - ) + ) \ No newline at end of file From 16b240f8e035f4af378bb2b94c891e82aba98cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=88=90=E5=B1=B1?= Date: Sun, 24 Aug 2025 18:36:37 +0800 Subject: [PATCH 3/3] feat(core): Implement multi-provider LLM support and fix UI bugs This major update refactors the application's core to support multiple Large Language Model (LLM) providers, including Ollama (local), OpenAI, and DeepSeek. The model is no longer hardcoded and can now be configured via a central file. Additionally, this commit fixes a critical bug that caused the scenario chatbot to crash upon starting a new session. Key changes include: **Features:** - **Configurable LLM Providers:** - [cite_start]Introduced a file to define and manage configurations for different LLM providers (Ollama, OpenAI, DeepSeek)[cite: 6]. - [cite_start]Added a utility to load the active model's configuration from [cite: 30, 31, 32]. - [cite_start]Refactored to dynamically initialize the language model (, , ) based on the active configuration[cite: 8, 13, 14, 20]. - [cite_start]The system now checks for necessary environment variables (, ) when a cloud provider is selected[cite: 15, 16]. - **Updated Dependencies:** - [cite_start]Added for configuration file parsing and for DeepSeek model support[cite: 7]. - **Improved Documentation:** - [cite_start]Updated with new instructions on how to configure the active model and set API keys using a file or environment variables[cite: 2, 3]. **Bug Fixes:** - **Scenario Chatbot Crash:** - Fixed a caused by passing an incorrect data format to the chatbot component. - [cite_start] is updated to return the full chat history in a Gradio-compatible format (a list of dictionaries)[cite: 24, 25, 26, 27]. - [cite_start] is updated to correctly pass this formatted data to the existing chatbot component instead of creating a new one[cite: 28, 29]. **Refactoring:** - [cite_start]Updated docstrings and comments in to English for better clarity and consistency[cite: 9, 10, 11, 12, 18, 21]. --- .gitignore | 3 +- README.md | 24 ++++++-- config.yaml | 31 +++++++++++ requirements.txt | 21 +++++-- src/agents/agent_base.py | 104 ++++++++++++++++++++--------------- src/agents/scenario_agent.py | 33 ++++++++--- src/tabs/scenario_tab.py | 13 ++--- src/utils/config_loader.py | 31 +++++++++++ 8 files changed, 190 insertions(+), 70 deletions(-) create mode 100644 config.yaml create mode 100644 src/utils/config_loader.py diff --git a/.gitignore b/.gitignore index e25604c..7cde5b0 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,5 @@ cython_debug/ jupyter/* .idea/ -.gradio/ \ No newline at end of file +.gradio/ +work_vs_stage.diff \ No newline at end of file diff --git a/README.md b/README.md index babd1ba..62a32ea 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,31 @@ https://github.com/user-attachments/assets/6298a8e4-28fc-4a60-badc-59bff16b315e pip install -r requirements.txt ``` - 根据需要配置你的环境变量,例如 `OpenAI_API_KEY` 等。 - -4. **运行应用** +4. **配置大模型** + 本项目支持通过 Ollama (本地)、OpenAI (云端) 或 DeepSeek (云端) 来驱动。请在项目根目录的 `config.yaml` 文件中进行配置。 + + - **切换模型**: + 打开 `config.yaml` 文件,修改 `active_model` 的值来选择你希望使用的模型。例如,要使用 DeepSeek,请修改为: + ```yaml + active_model: deepseek_chat + ``` + + - **设置 API 密钥**: + 如果你选择使用 OpenAI 或 DeepSeek,你必须设置对应的 API 密钥。创建一个名为 `.env` 的文件,并在其中添加你的密钥: + ``` + # .env file + OPENAI_API_KEY="sk-..." + DEEPSEEK_API_KEY="sk-..." + ``` + 或者,你也可以将它们设置为系统环境变量。 + +5. **运行应用** 启动应用程序: ```bash python src/main.py ``` -5. **开始体验** +6. **开始体验** 打开浏览器,访问 `http://localhost:7860`,开始跟着 LanguageMentor 一起学习英语! 运行画面: diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..2dbc406 --- /dev/null +++ b/config.yaml @@ -0,0 +1,31 @@ +# The key of the model to be used by the application +active_model: deepseek_chat + +# Configuration for different model providers +providers: + ollama: + # Models provided by Ollama (running locally) + models: + ollama_llama3_1: + provider: ollama + model_name: "llama3.1:8b-instruct-q8_0" + max_tokens: 8192 + temperature: 0.8 + + openai: + # Models provided by OpenAI (requires OPENAI_API_KEY) + models: + gpt_4o_mini: + provider: openai + model_name: "gpt-4o-mini" + max_tokens: 8192 + temperature: 0.8 + + deepseek: + # Models provided by DeepSeek (requires DEEPSEEK_API_KEY) + models: + deepseek_chat: + provider: deepseek + model_name: "deepseek-chat" + max_tokens: 8192 + temperature: 0.8 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 416acdc..1088c68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,16 @@ -langchain==0.2.16 -langchain_core==0.2.41 -langchain_community==0.2.17 -langchain_openai==0.1.25 -langchain_ollama==0.1.3 +# requirements.txt + +# LangChain Core Libraries +langchain==0.3.27 +langchain-core==0.3.74 +langchain-community==0.3.25 + +# LangChain Model Integrations +langchain-openai==0.3.31 +langchain-ollama==0.3.7 +langchain-deepseek-official==0.1.0.post1 + +# UI and Utilities gradio==5.43.1 -loguru==0.7.2 \ No newline at end of file +loguru==0.7.3 +PyYAML==6.0.2 \ No newline at end of file diff --git a/src/agents/agent_base.py b/src/agents/agent_base.py index 88bc22a..6def53a 100644 --- a/src/agents/agent_base.py +++ b/src/agents/agent_base.py @@ -1,18 +1,21 @@ -# src/agents/agent_base.py import json +import os from abc import ABC, abstractmethod -from langchain_ollama.chat_models import ChatOllama # 导入 ChatOllama 模型 -from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder # 导入提示模板相关类 -from langchain_core.messages import HumanMessage # 导入消息类 -from langchain_core.runnables.history import RunnableWithMessageHistory # 导入带有消息历史的可运行类 +from langchain_community.chat_models import ChatOllama +from langchain_openai import ChatOpenAI +from langchain_deepseek import ChatDeepSeek +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.messages import HumanMessage +from langchain_core.runnables.history import RunnableWithMessageHistory -from .session_history import get_session_history # 导入会话历史相关方法 -from utils.logger import LOG # 导入日志工具 +from .session_history import get_session_history +from utils.logger import LOG +from utils.config_loader import get_active_model_config class AgentBase(ABC): """ - 抽象基类,提供代理的共有功能。 + Abstract base class providing common functionality for agents. """ def __init__(self, name, prompt_file, intro_file=None, session_id=None): self.name = name @@ -21,68 +24,83 @@ def __init__(self, name, prompt_file, intro_file=None, session_id=None): self.session_id = session_id if session_id else self.name self.prompt = self.load_prompt() self.intro_messages = self.load_intro() if self.intro_file else [] + self.model_config = get_active_model_config() self.create_chatbot() def load_prompt(self): - """ - 从文件加载系统提示语。 - """ + """Loads the system prompt from a file.""" try: with open(self.prompt_file, "r", encoding="utf-8") as file: return file.read().strip() except FileNotFoundError: - raise FileNotFoundError(f"找不到提示文件 {self.prompt_file}!") + raise FileNotFoundError(f"Prompt file not found: {self.prompt_file}!") def load_intro(self): - """ - 从 JSON 文件加载初始消息。 - """ + """Loads introductory messages from a JSON file.""" try: with open(self.intro_file, "r", encoding="utf-8") as file: return json.load(file) except FileNotFoundError: - raise FileNotFoundError(f"找不到初始消息文件 {self.intro_file}!") + raise FileNotFoundError(f"Intro file not found: {self.intro_file}!") except json.JSONDecodeError: - raise ValueError(f"初始消息文件 {self.intro_file} 包含无效的 JSON!") + raise ValueError(f"Intro file {self.intro_file} contains invalid JSON!") + + def _initialize_model(self): + """Initializes the language model based on the configuration.""" + provider = self.model_config.get("provider") + model_name = self.model_config.get("model_name") + temperature = self.model_config.get("temperature", 0.8) + max_tokens = self.model_config.get("max_tokens", 8192) + + LOG.info(f"Initializing model with provider: {provider}") + + if provider == "ollama": + # NOTE: For newer versions, ChatOllama is in langchain_community + return ChatOllama( + model=model_name, + temperature=temperature, + ) + elif provider == "openai": + if not os.getenv("OPENAI_API_KEY"): + raise ValueError("OPENAI_API_KEY environment variable not set for OpenAI provider") + return ChatOpenAI( + model=model_name, + temperature=temperature, + max_tokens=max_tokens, + ) + elif provider == "deepseek": + if not os.getenv("DEEPSEEK_API_KEY"): + raise ValueError("DEEPSEEK_API_KEY environment variable not set for DeepSeek provider") + return ChatDeepSeek( + model=model_name, + temperature=temperature, + max_tokens=max_tokens, + ) + else: + raise ValueError(f"Unsupported model provider: {provider}") def create_chatbot(self): - """ - 初始化聊天机器人,包括系统提示和消息历史记录。 - """ - # 创建聊天提示模板,包括系统提示和消息占位符 + """Initializes the chatbot with a system prompt and message history.""" system_prompt = ChatPromptTemplate.from_messages([ - ("system", self.prompt), # 系统提示部分 - MessagesPlaceholder(variable_name="messages"), # 消息占位符 + ("system", self.prompt), + MessagesPlaceholder(variable_name="messages"), ]) - # 初始化 ChatOllama 模型,配置参数 - self.chatbot = system_prompt | ChatOllama( - model="llama3.1:8b-instruct-q8_0", # 使用的模型名称 - max_tokens=8192, # 最大生成的 token 数 - temperature=0.8, # 随机性配置 - ) - - # 将聊天机器人与消息历史记录关联 + llm = self._initialize_model() + self.chatbot = system_prompt | llm self.chatbot_with_history = RunnableWithMessageHistory(self.chatbot, get_session_history) def chat_with_history(self, user_input, session_id=None): """ - 处理用户输入,生成包含聊天历史的回复。 - - 参数: - user_input (str): 用户输入的消息 - session_id (str, optional): 会话的唯一标识符 - - 返回: - str: AI 生成的回复 + Processes user input and generates a response with chat history. """ if session_id is None: session_id = self.session_id response = self.chatbot_with_history.invoke( - [HumanMessage(content=user_input)], # 将用户输入封装为 HumanMessage - {"configurable": {"session_id": session_id}}, # 传入配置,包括会话ID + [HumanMessage(content=user_input)], + {"configurable": {"session_id": session_id}}, ) - LOG.debug(f"[ChatBot][{self.name}] {response.content}") # 记录调试日志 - return response.content # 返回生成的回复内容 + LOG.debug(f"[ChatBot][{self.name}] {response.content}") + return response.content \ No newline at end of file diff --git a/src/agents/scenario_agent.py b/src/agents/scenario_agent.py index 5c7b256..688b1c3 100644 --- a/src/agents/scenario_agent.py +++ b/src/agents/scenario_agent.py @@ -2,7 +2,7 @@ import random -from langchain_core.messages import AIMessage # 导入消息类 +from langchain_core.messages import AIMessage, HumanMessage # 导入消息类 from .session_history import get_session_history # 导入会话历史相关方法 from .agent_base import AgentBase @@ -23,25 +23,40 @@ def __init__(self, scenario_name, session_id=None): session_id=session_id ) + def _convert_history_to_gradio_format(self, history): + """ + 将 LangChain 的 history 对象转换为 Gradio Chatbot 兼容的格式。 + """ + gradio_history = [] + for msg in history.messages: + if isinstance(msg, HumanMessage): + gradio_history.append({'role': 'user', 'content': msg.content}) + elif isinstance(msg, AIMessage): + gradio_history.append({'role': 'assistant', 'content': msg.content}) + # SystemMessage 通常不在聊天界面上显示,所以我们在这里忽略它 + return gradio_history + def start_new_session(self, session_id=None): """ - 开始一个新的场景会话,并发送随机的初始 AI 消息。 + 开始一个新的场景会话,并返回格式化后的完整聊天记录以更新UI。 参数: session_id (str, optional): 会话的唯一标识符 返回: - str: 初始 AI 消息 + list[dict]: Gradio Chatbot 格式的完整聊天记录 """ if session_id is None: session_id = self.session_id history = get_session_history(session_id) - LOG.debug(f"[history][{session_id}]:{history}") + # 如果是新会话,添加初始消息 if not history.messages: - initial_ai_message = random.choice(self.intro_messages) # 随机选择初始AI消息 - history.add_message(AIMessage(content=initial_ai_message)) # 添加初始AI消息到历史记录 - return initial_ai_message - else: - return history.messages[-1].content # 返回历史记录中的最后一条消息 + initial_ai_message = random.choice(self.intro_messages) + history.add_message(AIMessage(content=initial_ai_message)) + + LOG.debug(f"[history][{session_id}]:{history.messages}") + + # 无论如何,都返回格式化后的完整历史记录 + return self._convert_history_to_gradio_format(history) diff --git a/src/tabs/scenario_tab.py b/src/tabs/scenario_tab.py index 0eaa2fc..91bf7bf 100644 --- a/src/tabs/scenario_tab.py +++ b/src/tabs/scenario_tab.py @@ -25,14 +25,13 @@ def get_page_desc(scenario): # 获取场景介绍并启动新会话的函数 def start_new_scenario_chatbot(scenario): if not scenario: - return gr.update(value=None, interactive=False), gr.update(value=None) - initial_ai_message = agents[scenario].start_new_session() # 启动新会话并获取初始AI消息 + # 当清除选择时,返回 None 以清空 Chatbot + return None - return gr.Chatbot( - value=[(None, initial_ai_message)], # 设置聊天机器人的初始消息 - height=600, # 聊天窗口高度 - type="messages" - ) + # 直接调用 agent 的方法,并返回它产出的、已经格式化好的历史记录列表 + formatted_history = agents[scenario].start_new_session() + + return formatted_history # 场景代理处理函数,根据选择的场景调用相应的代理 def handle_scenario(user_input, chat_history, scenario): diff --git a/src/utils/config_loader.py b/src/utils/config_loader.py new file mode 100644 index 0000000..9136616 --- /dev/null +++ b/src/utils/config_loader.py @@ -0,0 +1,31 @@ +import yaml +from utils.logger import LOG + +def load_config(config_file="config.yaml"): + """Loads the YAML configuration file.""" + try: + with open(config_file, "r", encoding="utf-8") as file: + return yaml.safe_load(file) + except FileNotFoundError: + LOG.error(f"Configuration file {config_file} not found!") + raise + except yaml.YAMLError as e: + LOG.error(f"Error parsing YAML file {config_file}: {e}") + raise + +def get_active_model_config(): + """Gets the configuration for the active model specified in the config file.""" + config = load_config() + active_model_key = config.get("active_model") + if not active_model_key: + raise ValueError("'active_model' key not found in config.yaml") + + for provider, details in config.get("providers", {}).items(): + if active_model_key in details.get("models", {}): + model_config = details["models"][active_model_key] + # Ensure the provider key is in the model's config for easy access + if 'provider' not in model_config: + model_config['provider'] = provider + return model_config + + raise ValueError(f"Configuration for active model '{active_model_key}' not found in config.yaml") \ No newline at end of file