|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from utils import run_society |
|
import os |
|
import gradio as gr |
|
import time |
|
import json |
|
import logging |
|
import datetime |
|
from typing import Tuple |
|
import importlib |
|
from dotenv import load_dotenv, set_key, find_dotenv, unset_key |
|
import threading |
|
import queue |
|
import re |
|
|
|
os.environ["PYTHONIOENCODING"] = "utf-8" |
|
|
|
|
|
|
|
def setup_logging(): |
|
"""配置日志系统,将日志输出到文件和内存队列以及控制台""" |
|
|
|
logs_dir = os.path.join(os.path.dirname(__file__), "logs") |
|
os.makedirs(logs_dir, exist_ok=True) |
|
|
|
|
|
current_date = datetime.datetime.now().strftime("%Y-%m-%d") |
|
log_file = os.path.join(logs_dir, f"gradio_log_{current_date}.txt") |
|
|
|
|
|
root_logger = logging.getLogger() |
|
|
|
|
|
for handler in root_logger.handlers[:]: |
|
root_logger.removeHandler(handler) |
|
|
|
root_logger.setLevel(logging.INFO) |
|
|
|
|
|
file_handler = logging.FileHandler(log_file, encoding="utf-8", mode="a") |
|
file_handler.setLevel(logging.INFO) |
|
|
|
|
|
console_handler = logging.StreamHandler() |
|
console_handler.setLevel(logging.INFO) |
|
|
|
|
|
formatter = logging.Formatter( |
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s" |
|
) |
|
file_handler.setFormatter(formatter) |
|
console_handler.setFormatter(formatter) |
|
|
|
|
|
root_logger.addHandler(file_handler) |
|
root_logger.addHandler(console_handler) |
|
|
|
logging.info("日志系统已初始化,日志文件: %s", log_file) |
|
return log_file |
|
|
|
|
|
|
|
LOG_FILE = None |
|
LOG_QUEUE: queue.Queue = queue.Queue() |
|
STOP_LOG_THREAD = threading.Event() |
|
CURRENT_PROCESS = None |
|
STOP_REQUESTED = threading.Event() |
|
|
|
|
|
|
|
def log_reader_thread(log_file): |
|
"""后台线程,持续读取日志文件并将新行添加到队列中""" |
|
try: |
|
with open(log_file, "r", encoding="utf-8") as f: |
|
|
|
f.seek(0, 2) |
|
|
|
while not STOP_LOG_THREAD.is_set(): |
|
line = f.readline() |
|
if line: |
|
LOG_QUEUE.put(line) |
|
else: |
|
|
|
time.sleep(0.1) |
|
except Exception as e: |
|
logging.error(f"日志读取线程出错: {str(e)}") |
|
|
|
|
|
def get_latest_logs(max_lines=100, queue_source=None): |
|
"""从队列中获取最新的日志行,如果队列为空则直接从文件读取 |
|
|
|
Args: |
|
max_lines: 最大返回行数 |
|
queue_source: 指定使用哪个队列,默认为LOG_QUEUE |
|
|
|
Returns: |
|
str: 日志内容 |
|
""" |
|
logs = [] |
|
log_queue = queue_source if queue_source else LOG_QUEUE |
|
|
|
|
|
temp_queue = queue.Queue() |
|
temp_logs = [] |
|
|
|
try: |
|
|
|
while not log_queue.empty() and len(temp_logs) < max_lines: |
|
log = log_queue.get_nowait() |
|
temp_logs.append(log) |
|
temp_queue.put(log) |
|
except queue.Empty: |
|
pass |
|
|
|
|
|
logs = temp_logs |
|
|
|
|
|
if len(logs) < max_lines and LOG_FILE and os.path.exists(LOG_FILE): |
|
try: |
|
with open(LOG_FILE, "r", encoding="utf-8") as f: |
|
all_lines = f.readlines() |
|
|
|
remaining_lines = max_lines - len(logs) |
|
file_logs = ( |
|
all_lines[-remaining_lines:] |
|
if len(all_lines) > remaining_lines |
|
else all_lines |
|
) |
|
|
|
|
|
logs = file_logs + logs |
|
except Exception as e: |
|
error_msg = f"读取日志文件出错: {str(e)}" |
|
logging.error(error_msg) |
|
if not logs: |
|
logs = [error_msg] |
|
|
|
|
|
if not logs: |
|
return "初始化运行中..." |
|
|
|
|
|
filtered_logs = [] |
|
for log in logs: |
|
if "camel.agents.chat_agent - INFO" in log: |
|
filtered_logs.append(log) |
|
|
|
|
|
if not filtered_logs: |
|
return "暂无对话记录。" |
|
|
|
|
|
simplified_logs = [] |
|
|
|
|
|
processed_messages = set() |
|
|
|
def process_message(role, content): |
|
|
|
msg_id = f"{role}:{content}" |
|
if msg_id in processed_messages: |
|
return None |
|
|
|
processed_messages.add(msg_id) |
|
content = content.replace("\\n", "\n") |
|
lines = [line.strip() for line in content.split("\n")] |
|
content = "\n".join(lines) |
|
|
|
role_emoji = "🙋" if role.lower() == "user" else "🤖" |
|
return f"""### {role_emoji} {role.title()} Agent |
|
|
|
{content}""" |
|
|
|
for log in filtered_logs: |
|
formatted_messages = [] |
|
|
|
messages_match = re.search( |
|
r"Model (.*?), index (\d+), processed these messages: (\[.*\])", log |
|
) |
|
|
|
if messages_match: |
|
try: |
|
messages = json.loads(messages_match.group(3)) |
|
for msg in messages: |
|
if msg.get("role") in ["user", "assistant"]: |
|
formatted_msg = process_message( |
|
msg.get("role"), msg.get("content", "") |
|
) |
|
if formatted_msg: |
|
formatted_messages.append(formatted_msg) |
|
except json.JSONDecodeError: |
|
pass |
|
|
|
|
|
if not formatted_messages: |
|
user_pattern = re.compile(r"\{'role': 'user', 'content': '(.*?)'\}") |
|
assistant_pattern = re.compile( |
|
r"\{'role': 'assistant', 'content': '(.*?)'\}" |
|
) |
|
|
|
for content in user_pattern.findall(log): |
|
formatted_msg = process_message("user", content) |
|
if formatted_msg: |
|
formatted_messages.append(formatted_msg) |
|
|
|
for content in assistant_pattern.findall(log): |
|
formatted_msg = process_message("assistant", content) |
|
if formatted_msg: |
|
formatted_messages.append(formatted_msg) |
|
|
|
if formatted_messages: |
|
simplified_logs.append("\n\n".join(formatted_messages)) |
|
|
|
|
|
formatted_logs = [] |
|
for i, log in enumerate(simplified_logs): |
|
|
|
log = log.strip() |
|
|
|
formatted_logs.append(log) |
|
|
|
|
|
if not log.endswith("\n"): |
|
formatted_logs.append("\n") |
|
|
|
return "\n".join(formatted_logs) |
|
|
|
|
|
|
|
MODULE_DESCRIPTIONS = { |
|
"run": "默认模式:使用OpenAI模型的默认的智能体协作模式,适合大多数任务。", |
|
"run_mini": "使用使用OpenAI模型最小化配置处理任务", |
|
"run_deepseek_zh": "使用deepseek模型处理中文任务", |
|
"run_openai_compatible_model": "使用openai兼容模型处理任务", |
|
"run_ollama": "使用本地ollama模型处理任务", |
|
"run_qwen_mini_zh": "使用qwen模型最小化配置处理任务", |
|
"run_qwen_zh": "使用qwen模型处理任务", |
|
"run_azure_openai": "使用azure openai模型处理任务", |
|
"run_groq": "使用groq模型处理任务", |
|
} |
|
|
|
|
|
|
|
DEFAULT_ENV_TEMPLATE = """#=========================================== |
|
# MODEL & API |
|
# (See https://docs.camel-ai.org/key_modules/models.html#) |
|
#=========================================== |
|
|
|
# OPENAI API (https://platform.openai.com/api-keys) |
|
OPENAI_API_KEY='Your_Key' |
|
# OPENAI_API_BASE_URL="" |
|
|
|
# Azure OpenAI API |
|
# AZURE_OPENAI_BASE_URL="" |
|
# AZURE_API_VERSION="" |
|
# AZURE_OPENAI_API_KEY="" |
|
# AZURE_DEPLOYMENT_NAME="" |
|
|
|
|
|
# Qwen API (https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key) |
|
QWEN_API_KEY='Your_Key' |
|
|
|
# DeepSeek API (https://platform.deepseek.com/api_keys) |
|
DEEPSEEK_API_KEY='Your_Key' |
|
|
|
#=========================================== |
|
# Tools & Services API |
|
#=========================================== |
|
|
|
# Google Search API (https://coda.io/@jon-dallas/google-image-search-pack-example/search-engine-id-and-google-api-key-3) |
|
GOOGLE_API_KEY='Your_Key' |
|
SEARCH_ENGINE_ID='Your_ID' |
|
|
|
# Chunkr API (https://chunkr.ai/) |
|
CHUNKR_API_KEY='Your_Key' |
|
|
|
# Firecrawl API (https://www.firecrawl.dev/) |
|
FIRECRAWL_API_KEY='Your_Key' |
|
#FIRECRAWL_API_URL="https://api.firecrawl.dev" |
|
""" |
|
|
|
|
|
def validate_input(question: str) -> bool: |
|
"""验证用户输入是否有效 |
|
|
|
Args: |
|
question: 用户问题 |
|
|
|
Returns: |
|
bool: 输入是否有效 |
|
""" |
|
|
|
if not question or question.strip() == "": |
|
return False |
|
return True |
|
|
|
|
|
def run_owl(question: str, example_module: str) -> Tuple[str, str, str]: |
|
"""运行OWL系统并返回结果 |
|
|
|
Args: |
|
question: 用户问题 |
|
example_module: 要导入的示例模块名(如 "run_terminal_zh" 或 "run_deep") |
|
|
|
Returns: |
|
Tuple[...]: 回答、令牌计数、状态 |
|
""" |
|
global CURRENT_PROCESS |
|
|
|
|
|
if not validate_input(question): |
|
logging.warning("用户提交了无效的输入") |
|
return ("请输入有效的问题", "0", "❌ 错误: 输入问题无效") |
|
|
|
try: |
|
|
|
load_dotenv(find_dotenv(), override=True) |
|
logging.info(f"处理问题: '{question}', 使用模块: {example_module}") |
|
|
|
|
|
if example_module not in MODULE_DESCRIPTIONS: |
|
logging.error(f"用户选择了不支持的模块: {example_module}") |
|
return ( |
|
f"所选模块 '{example_module}' 不受支持", |
|
"0", |
|
"❌ 错误: 不支持的模块", |
|
) |
|
|
|
|
|
module_path = f"examples.{example_module}" |
|
try: |
|
logging.info(f"正在导入模块: {module_path}") |
|
module = importlib.import_module(module_path) |
|
except ImportError as ie: |
|
logging.error(f"无法导入模块 {module_path}: {str(ie)}") |
|
return ( |
|
f"无法导入模块: {module_path}", |
|
"0", |
|
f"❌ 错误: 模块 {example_module} 不存在或无法加载 - {str(ie)}", |
|
) |
|
except Exception as e: |
|
logging.error(f"导入模块 {module_path} 时发生错误: {str(e)}") |
|
return (f"导入模块时发生错误: {module_path}", "0", f"❌ 错误: {str(e)}") |
|
|
|
|
|
if not hasattr(module, "construct_society"): |
|
logging.error(f"模块 {module_path} 中未找到 construct_society 函数") |
|
return ( |
|
f"模块 {module_path} 中未找到 construct_society 函数", |
|
"0", |
|
"❌ 错误: 模块接口不兼容", |
|
) |
|
|
|
|
|
try: |
|
logging.info("正在构建社会模拟...") |
|
society = module.construct_society(question) |
|
|
|
except Exception as e: |
|
logging.error(f"构建社会模拟时发生错误: {str(e)}") |
|
return ( |
|
f"构建社会模拟时发生错误: {str(e)}", |
|
"0", |
|
f"❌ 错误: 构建失败 - {str(e)}", |
|
) |
|
|
|
|
|
try: |
|
logging.info("正在运行社会模拟...") |
|
answer, chat_history, token_info = run_society(society) |
|
logging.info("社会模拟运行完成") |
|
except Exception as e: |
|
logging.error(f"运行社会模拟时发生错误: {str(e)}") |
|
return ( |
|
f"运行社会模拟时发生错误: {str(e)}", |
|
"0", |
|
f"❌ 错误: 运行失败 - {str(e)}", |
|
) |
|
|
|
|
|
if not isinstance(token_info, dict): |
|
token_info = {} |
|
|
|
completion_tokens = token_info.get("completion_token_count", 0) |
|
prompt_tokens = token_info.get("prompt_token_count", 0) |
|
total_tokens = completion_tokens + prompt_tokens |
|
|
|
logging.info( |
|
f"处理完成,令牌使用: 完成={completion_tokens}, 提示={prompt_tokens}, 总计={total_tokens}" |
|
) |
|
|
|
return ( |
|
answer, |
|
f"完成令牌: {completion_tokens:,} | 提示令牌: {prompt_tokens:,} | 总计: {total_tokens:,}", |
|
"✅ 成功完成", |
|
) |
|
|
|
except Exception as e: |
|
logging.error(f"处理问题时发生未捕获的错误: {str(e)}") |
|
return (f"发生错误: {str(e)}", "0", f"❌ 错误: {str(e)}") |
|
|
|
|
|
def update_module_description(module_name: str) -> str: |
|
"""返回所选模块的描述""" |
|
return MODULE_DESCRIPTIONS.get(module_name, "无可用描述") |
|
|
|
|
|
|
|
WEB_FRONTEND_ENV_VARS: dict[str, str] = {} |
|
|
|
|
|
def init_env_file(): |
|
"""初始化.env文件如果不存在""" |
|
dotenv_path = find_dotenv() |
|
if not dotenv_path: |
|
with open(".env", "w") as f: |
|
f.write(DEFAULT_ENV_TEMPLATE) |
|
dotenv_path = find_dotenv() |
|
return dotenv_path |
|
|
|
|
|
def load_env_vars(): |
|
"""加载环境变量并返回字典格式 |
|
|
|
Returns: |
|
dict: 环境变量字典,每个值为一个包含值和来源的元组 (value, source) |
|
""" |
|
dotenv_path = init_env_file() |
|
load_dotenv(dotenv_path, override=True) |
|
|
|
|
|
env_file_vars = {} |
|
with open(dotenv_path, "r") as f: |
|
for line in f: |
|
line = line.strip() |
|
if line and not line.startswith("#"): |
|
if "=" in line: |
|
key, value = line.split("=", 1) |
|
env_file_vars[key.strip()] = value.strip().strip("\"'") |
|
|
|
|
|
system_env_vars = { |
|
k: v |
|
for k, v in os.environ.items() |
|
if k not in env_file_vars and k not in WEB_FRONTEND_ENV_VARS |
|
} |
|
|
|
|
|
env_vars = {} |
|
|
|
|
|
for key, value in system_env_vars.items(): |
|
env_vars[key] = (value, "系统") |
|
|
|
|
|
for key, value in env_file_vars.items(): |
|
env_vars[key] = (value, ".env文件") |
|
|
|
|
|
for key, value in WEB_FRONTEND_ENV_VARS.items(): |
|
env_vars[key] = (value, "前端配置") |
|
|
|
os.environ[key] = value |
|
|
|
return env_vars |
|
|
|
|
|
def save_env_vars(env_vars): |
|
"""保存环境变量到.env文件 |
|
|
|
Args: |
|
env_vars: 字典,键为环境变量名,值可以是字符串或(值,来源)元组 |
|
""" |
|
try: |
|
dotenv_path = init_env_file() |
|
|
|
|
|
for key, value_data in env_vars.items(): |
|
if key and key.strip(): |
|
|
|
if isinstance(value_data, tuple): |
|
value = value_data[0] |
|
else: |
|
value = value_data |
|
|
|
set_key(dotenv_path, key.strip(), value.strip()) |
|
|
|
|
|
load_dotenv(dotenv_path, override=True) |
|
|
|
return True, "环境变量已成功保存!" |
|
except Exception as e: |
|
return False, f"保存环境变量时出错: {str(e)}" |
|
|
|
|
|
def add_env_var(key, value, from_frontend=True): |
|
"""添加或更新单个环境变量 |
|
|
|
Args: |
|
key: 环境变量名 |
|
value: 环境变量值 |
|
from_frontend: 是否来自前端配置,默认为True |
|
""" |
|
try: |
|
if not key or not key.strip(): |
|
return False, "变量名不能为空" |
|
|
|
key = key.strip() |
|
value = value.strip() |
|
|
|
|
|
if from_frontend: |
|
WEB_FRONTEND_ENV_VARS[key] = value |
|
|
|
os.environ[key] = value |
|
|
|
|
|
dotenv_path = init_env_file() |
|
set_key(dotenv_path, key, value) |
|
load_dotenv(dotenv_path, override=True) |
|
|
|
return True, f"环境变量 {key} 已成功添加/更新!" |
|
except Exception as e: |
|
return False, f"添加环境变量时出错: {str(e)}" |
|
|
|
|
|
def delete_env_var(key): |
|
"""删除环境变量""" |
|
try: |
|
if not key or not key.strip(): |
|
return False, "变量名不能为空" |
|
|
|
key = key.strip() |
|
|
|
|
|
dotenv_path = init_env_file() |
|
unset_key(dotenv_path, key) |
|
|
|
|
|
if key in WEB_FRONTEND_ENV_VARS: |
|
del WEB_FRONTEND_ENV_VARS[key] |
|
|
|
|
|
if key in os.environ: |
|
del os.environ[key] |
|
|
|
return True, f"环境变量 {key} 已成功删除!" |
|
except Exception as e: |
|
return False, f"删除环境变量时出错: {str(e)}" |
|
|
|
|
|
def is_api_related(key: str) -> bool: |
|
"""判断环境变量是否与API相关 |
|
|
|
Args: |
|
key: 环境变量名 |
|
|
|
Returns: |
|
bool: 是否与API相关 |
|
""" |
|
|
|
api_keywords = [ |
|
"api", |
|
"key", |
|
"token", |
|
"secret", |
|
"password", |
|
"openai", |
|
"qwen", |
|
"deepseek", |
|
"google", |
|
"search", |
|
"hf", |
|
"hugging", |
|
"chunkr", |
|
"firecrawl", |
|
] |
|
|
|
|
|
return any(keyword in key.lower() for keyword in api_keywords) |
|
|
|
|
|
def get_api_guide(key: str) -> str: |
|
"""根据环境变量名返回对应的API获取指南 |
|
|
|
Args: |
|
key: 环境变量名 |
|
|
|
Returns: |
|
str: API获取指南链接或说明 |
|
""" |
|
key_lower = key.lower() |
|
if "openai" in key_lower: |
|
return "https://platform.openai.com/api-keys" |
|
elif "qwen" in key_lower or "dashscope" in key_lower: |
|
return "https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key" |
|
elif "deepseek" in key_lower: |
|
return "https://platform.deepseek.com/api_keys" |
|
elif "google" in key_lower: |
|
return "https://coda.io/@jon-dallas/google-image-search-pack-example/search-engine-id-and-google-api-key-3" |
|
elif "search_engine_id" in key_lower: |
|
return "https://coda.io/@jon-dallas/google-image-search-pack-example/search-engine-id-and-google-api-key-3" |
|
elif "chunkr" in key_lower: |
|
return "https://chunkr.ai/" |
|
elif "firecrawl" in key_lower: |
|
return "https://www.firecrawl.dev/" |
|
else: |
|
return "" |
|
|
|
|
|
def update_env_table(): |
|
"""更新环境变量表格显示,只显示API相关的环境变量""" |
|
env_vars = load_env_vars() |
|
|
|
api_env_vars = {k: v for k, v in env_vars.items() if is_api_related(k)} |
|
|
|
|
|
result = [] |
|
for k, v in api_env_vars.items(): |
|
guide = get_api_guide(k) |
|
|
|
guide_link = ( |
|
f"<a href='{guide}' target='_blank' class='guide-link'>🔗 获取</a>" |
|
if guide |
|
else "" |
|
) |
|
result.append([k, v[0], guide_link]) |
|
return result |
|
|
|
|
|
def save_env_table_changes(data): |
|
"""保存环境变量表格的更改 |
|
|
|
Args: |
|
data: Dataframe数据,可能是pandas DataFrame对象 |
|
|
|
Returns: |
|
str: 操作状态信息,包含HTML格式的状态消息 |
|
""" |
|
try: |
|
logging.info(f"开始处理环境变量表格数据,类型: {type(data)}") |
|
|
|
|
|
current_env_vars = load_env_vars() |
|
processed_keys = set() |
|
|
|
|
|
import pandas as pd |
|
|
|
if isinstance(data, pd.DataFrame): |
|
|
|
columns = data.columns.tolist() |
|
logging.info(f"DataFrame列名: {columns}") |
|
|
|
|
|
for index, row in data.iterrows(): |
|
|
|
if len(columns) >= 3: |
|
|
|
key = row[0] if isinstance(row, pd.Series) else row.iloc[0] |
|
value = row[1] if isinstance(row, pd.Series) else row.iloc[1] |
|
|
|
|
|
if key and str(key).strip(): |
|
logging.info(f"处理环境变量: {key} = {value}") |
|
add_env_var(key, str(value)) |
|
processed_keys.add(key) |
|
|
|
elif isinstance(data, dict): |
|
logging.info(f"字典格式数据的键: {list(data.keys())}") |
|
|
|
if "data" in data: |
|
rows = data["data"] |
|
elif "values" in data: |
|
rows = data["values"] |
|
elif "value" in data: |
|
rows = data["value"] |
|
else: |
|
|
|
rows = [] |
|
for key, value in data.items(): |
|
if key not in ["headers", "types", "columns"]: |
|
rows.append([key, value]) |
|
|
|
if isinstance(rows, list): |
|
for row in rows: |
|
if isinstance(row, list) and len(row) >= 2: |
|
key, value = row[0], row[1] |
|
if key and str(key).strip(): |
|
add_env_var(key, str(value)) |
|
processed_keys.add(key) |
|
elif isinstance(data, list): |
|
|
|
for row in data: |
|
if isinstance(row, list) and len(row) >= 2: |
|
key, value = row[0], row[1] |
|
if key and str(key).strip(): |
|
add_env_var(key, str(value)) |
|
processed_keys.add(key) |
|
else: |
|
logging.error(f"未知的数据格式: {type(data)}") |
|
return f"❌ 保存失败: 未知的数据格式 {type(data)}" |
|
|
|
|
|
api_related_keys = {k for k in current_env_vars.keys() if is_api_related(k)} |
|
keys_to_delete = api_related_keys - processed_keys |
|
|
|
|
|
for key in keys_to_delete: |
|
logging.info(f"删除环境变量: {key}") |
|
delete_env_var(key) |
|
|
|
return "✅ 环境变量已成功保存" |
|
except Exception as e: |
|
import traceback |
|
|
|
error_details = traceback.format_exc() |
|
logging.error(f"保存环境变量时出错: {str(e)}\n{error_details}") |
|
return f"❌ 保存失败: {str(e)}" |
|
|
|
|
|
def get_env_var_value(key): |
|
"""获取环境变量的实际值 |
|
|
|
优先级:前端配置 > .env文件 > 系统环境变量 |
|
""" |
|
|
|
if key in WEB_FRONTEND_ENV_VARS: |
|
return WEB_FRONTEND_ENV_VARS[key] |
|
|
|
|
|
return os.environ.get(key, "") |
|
|
|
|
|
def create_ui(): |
|
"""创建增强版Gradio界面""" |
|
|
|
def clear_log_file(): |
|
"""清空日志文件内容""" |
|
try: |
|
if LOG_FILE and os.path.exists(LOG_FILE): |
|
|
|
open(LOG_FILE, "w").close() |
|
logging.info("日志文件已清空") |
|
|
|
while not LOG_QUEUE.empty(): |
|
try: |
|
LOG_QUEUE.get_nowait() |
|
except queue.Empty: |
|
break |
|
return "" |
|
else: |
|
return "" |
|
except Exception as e: |
|
logging.error(f"清空日志文件时出错: {str(e)}") |
|
return "" |
|
|
|
|
|
def process_with_live_logs(question, module_name): |
|
"""处理问题并实时更新日志""" |
|
global CURRENT_PROCESS |
|
|
|
|
|
clear_log_file() |
|
|
|
|
|
result_queue = queue.Queue() |
|
|
|
def process_in_background(): |
|
try: |
|
result = run_owl(question, module_name) |
|
result_queue.put(result) |
|
except Exception as e: |
|
result_queue.put((f"发生错误: {str(e)}", "0", f"❌ 错误: {str(e)}")) |
|
|
|
|
|
bg_thread = threading.Thread(target=process_in_background) |
|
CURRENT_PROCESS = bg_thread |
|
bg_thread.start() |
|
|
|
|
|
while bg_thread.is_alive(): |
|
|
|
logs2 = get_latest_logs(100, LOG_QUEUE) |
|
|
|
|
|
yield ( |
|
"0", |
|
"<span class='status-indicator status-running'></span> 处理中...", |
|
logs2, |
|
) |
|
|
|
time.sleep(1) |
|
|
|
|
|
if not result_queue.empty(): |
|
result = result_queue.get() |
|
answer, token_count, status = result |
|
|
|
|
|
logs2 = get_latest_logs(100, LOG_QUEUE) |
|
|
|
|
|
if "错误" in status: |
|
status_with_indicator = ( |
|
f"<span class='status-indicator status-error'></span> {status}" |
|
) |
|
else: |
|
status_with_indicator = ( |
|
f"<span class='status-indicator status-success'></span> {status}" |
|
) |
|
|
|
yield token_count, status_with_indicator, logs2 |
|
else: |
|
logs2 = get_latest_logs(100, LOG_QUEUE) |
|
yield ( |
|
"0", |
|
"<span class='status-indicator status-error'></span> 已终止", |
|
logs2, |
|
) |
|
|
|
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as app: |
|
gr.Markdown( |
|
""" |
|
# 🦉 OWL 多智能体协作系统 |
|
|
|
基于CAMEL框架开发的先进多智能体协作系统,旨在通过智能体协作解决复杂问题。 |
|
|
|
可以通过修改本地脚本自定义模型和工具。 |
|
|
|
本网页应用目前处于测试阶段,仅供演示和测试使用,尚未推荐用于生产环境。 |
|
""" |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<style> |
|
/* 聊天容器样式 */ |
|
.chat-container .chatbot { |
|
height: 500px; |
|
overflow-y: auto; |
|
border-radius: 10px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
|
|
/* 改进标签页样式 */ |
|
.tabs .tab-nav { |
|
background-color: #f5f5f5; |
|
border-radius: 8px 8px 0 0; |
|
padding: 5px; |
|
} |
|
|
|
.tabs .tab-nav button { |
|
border-radius: 5px; |
|
margin: 0 3px; |
|
padding: 8px 15px; |
|
font-weight: 500; |
|
} |
|
|
|
.tabs .tab-nav button.selected { |
|
background-color: #2c7be5; |
|
color: white; |
|
} |
|
|
|
/* 状态指示器样式 */ |
|
.status-indicator { |
|
display: inline-block; |
|
width: 10px; |
|
height: 10px; |
|
border-radius: 50%; |
|
margin-right: 5px; |
|
} |
|
|
|
.status-running { |
|
background-color: #ffc107; |
|
animation: pulse 1.5s infinite; |
|
} |
|
|
|
.status-success { |
|
background-color: #28a745; |
|
} |
|
|
|
.status-error { |
|
background-color: #dc3545; |
|
} |
|
|
|
/* 日志显示区域样式 */ |
|
.log-display textarea { |
|
height: 400px !important; |
|
max-height: 400px !important; |
|
overflow-y: auto !important; |
|
font-family: monospace; |
|
font-size: 0.9em; |
|
white-space: pre-wrap; |
|
line-height: 1.4; |
|
} |
|
|
|
.log-display { |
|
border-radius: 10px; |
|
padding: 15px; |
|
margin-bottom: 20px; |
|
min-height: 50vh; |
|
max-height: 75vh; |
|
} |
|
|
|
/* 环境变量管理样式 */ |
|
.env-manager-container { |
|
border-radius: 10px; |
|
padding: 15px; |
|
background-color: #f9f9f9; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.env-controls, .api-help-container { |
|
border-radius: 8px; |
|
padding: 15px; |
|
background-color: white; |
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); |
|
height: 100%; |
|
} |
|
|
|
.env-add-group, .env-delete-group { |
|
margin-top: 20px; |
|
padding: 15px; |
|
border-radius: 8px; |
|
background-color: #f5f8ff; |
|
border: 1px solid #e0e8ff; |
|
} |
|
|
|
.env-delete-group { |
|
background-color: #fff5f5; |
|
border: 1px solid #ffe0e0; |
|
} |
|
|
|
.env-buttons { |
|
justify-content: flex-start; |
|
gap: 10px; |
|
margin-top: 10px; |
|
} |
|
|
|
.env-button { |
|
min-width: 100px; |
|
} |
|
|
|
.delete-button { |
|
background-color: #dc3545; |
|
color: white; |
|
} |
|
|
|
.env-table { |
|
margin-bottom: 15px; |
|
} |
|
|
|
/* 改进环境变量表格样式 */ |
|
.env-table table { |
|
border-collapse: separate; |
|
border-spacing: 0; |
|
width: 100%; |
|
border-radius: 8px; |
|
overflow: hidden; |
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05); |
|
} |
|
|
|
.env-table th { |
|
background-color: #f0f7ff; |
|
padding: 12px 15px; |
|
text-align: left; |
|
font-weight: 600; |
|
color: #2c7be5; |
|
border-bottom: 2px solid #e0e8ff; |
|
} |
|
|
|
.env-table td { |
|
padding: 10px 15px; |
|
border-bottom: 1px solid #f0f0f0; |
|
} |
|
|
|
.env-table tr:hover td { |
|
background-color: #f9fbff; |
|
} |
|
|
|
.env-table tr:last-child td { |
|
border-bottom: none; |
|
} |
|
|
|
/* 状态图标样式 */ |
|
.status-icon-cell { |
|
text-align: center; |
|
font-size: 1.2em; |
|
} |
|
|
|
/* 链接样式 */ |
|
.guide-link { |
|
color: #2c7be5; |
|
text-decoration: none; |
|
cursor: pointer; |
|
font-weight: 500; |
|
} |
|
|
|
.guide-link:hover { |
|
text-decoration: underline; |
|
} |
|
|
|
.env-status { |
|
margin-top: 15px; |
|
font-weight: 500; |
|
padding: 10px; |
|
border-radius: 6px; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.env-status-success { |
|
background-color: #d4edda; |
|
color: #155724; |
|
border: 1px solid #c3e6cb; |
|
} |
|
|
|
.env-status-error { |
|
background-color: #f8d7da; |
|
color: #721c24; |
|
border: 1px solid #f5c6cb; |
|
} |
|
|
|
.api-help-accordion { |
|
margin-bottom: 8px; |
|
border-radius: 6px; |
|
overflow: hidden; |
|
} |
|
|
|
|
|
@keyframes pulse { |
|
0% { opacity: 1; } |
|
50% { opacity: 0.5; } |
|
100% { opacity: 1; } |
|
} |
|
</style> |
|
""") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=0.5): |
|
question_input = gr.Textbox( |
|
lines=5, |
|
placeholder="请输入您的问题...", |
|
label="问题", |
|
elem_id="question_input", |
|
show_copy_button=True, |
|
value="打开百度搜索,总结一下camel-ai的camel框架的github star、fork数目等,并把数字用plot包写成python文件保存到本地,并运行生成的python文件。", |
|
) |
|
|
|
|
|
|
|
module_dropdown = gr.Dropdown( |
|
choices=list(MODULE_DESCRIPTIONS.keys()), |
|
value="run_qwen_zh", |
|
label="选择功能模块", |
|
interactive=True, |
|
) |
|
|
|
|
|
module_description = gr.Textbox( |
|
value=MODULE_DESCRIPTIONS["run_qwen_zh"], |
|
label="模块描述", |
|
interactive=False, |
|
elem_classes="module-info", |
|
) |
|
|
|
with gr.Row(): |
|
run_button = gr.Button( |
|
"运行", variant="primary", elem_classes="primary" |
|
) |
|
|
|
status_output = gr.HTML( |
|
value="<span class='status-indicator status-success'></span> 已就绪", |
|
label="状态", |
|
) |
|
token_count_output = gr.Textbox( |
|
label="令牌计数", interactive=False, elem_classes="token-count" |
|
) |
|
|
|
|
|
examples = [ |
|
"打开百度搜索,总结一下camel-ai的camel框架的github star、fork数目等,并把数字用plot包写成python文件保存到本地,并运行生成的python文件。", |
|
"浏览亚马逊并找出一款对程序员有吸引力的产品。请提供产品名称和价格", |
|
"写一个hello world的python文件,保存到本地", |
|
] |
|
|
|
gr.Examples(examples=examples, inputs=question_input) |
|
|
|
gr.HTML(""" |
|
<div class="footer" id="about"> |
|
<h3>关于 OWL 多智能体协作系统</h3> |
|
<p>OWL 是一个基于CAMEL框架开发的先进多智能体协作系统,旨在通过智能体协作解决复杂问题。</p> |
|
<p>© 2025 CAMEL-AI.org. 基于Apache License 2.0开源协议</p> |
|
<p><a href="https://github.com/camel-ai/owl" target="_blank">GitHub</a></p> |
|
</div> |
|
""") |
|
|
|
with gr.Tabs(): |
|
with gr.TabItem("对话记录"): |
|
|
|
with gr.Box(): |
|
log_display2 = gr.Markdown( |
|
value="暂无对话记录。", |
|
elem_classes="log-display", |
|
) |
|
|
|
with gr.Row(): |
|
refresh_logs_button2 = gr.Button("刷新记录") |
|
auto_refresh_checkbox2 = gr.Checkbox( |
|
label="自动刷新", value=True, interactive=True |
|
) |
|
clear_logs_button2 = gr.Button("清空记录", variant="secondary") |
|
|
|
with gr.TabItem("环境变量管理", id="env-settings"): |
|
with gr.Box(elem_classes="env-manager-container"): |
|
gr.Markdown(""" |
|
## 环境变量管理 |
|
|
|
在此处设置模型API密钥和其他服务凭证。这些信息将保存在本地的`.env`文件中,确保您的API密钥安全存储且不会上传到网络。正确设置API密钥对于OWL系统的功能至关重要, 可以按找工具需求灵活配置环境变量。 |
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
|
with gr.Column(scale=3): |
|
with gr.Box(elem_classes="env-controls"): |
|
|
|
gr.Markdown(""" |
|
<div style="background-color: #e7f3fe; border-left: 6px solid #2196F3; padding: 10px; margin: 15px 0; border-radius: 4px;"> |
|
<strong>提示:</strong> 请确保运行cp .env_template .env创建本地.env文件,根据运行模块灵活配置所需环境变量 |
|
</div> |
|
""") |
|
|
|
|
|
env_table = gr.Dataframe( |
|
headers=["变量名", "值", "获取指南"], |
|
datatype=[ |
|
"str", |
|
"str", |
|
"html", |
|
], |
|
row_count=10, |
|
col_count=(3, "fixed"), |
|
value=update_env_table, |
|
label="API密钥和环境变量", |
|
interactive=True, |
|
elem_classes="env-table", |
|
) |
|
|
|
|
|
gr.Markdown( |
|
""" |
|
<div style="background-color: #fff3cd; border-left: 6px solid #ffc107; padding: 10px; margin: 15px 0; border-radius: 4px;"> |
|
<strong>操作指南</strong>: |
|
<ul style="margin-top: 8px; margin-bottom: 8px;"> |
|
<li><strong>编辑变量</strong>: 直接点击表格中的"值"单元格进行编辑</li> |
|
<li><strong>添加变量</strong>: 在空白行中输入新的变量名和值</li> |
|
<li><strong>删除变量</strong>: 清空变量名即可删除该行</li> |
|
<li><strong>获取API密钥</strong>: 点击"获取指南"列中的链接获取相应API密钥</li> |
|
</ul> |
|
</div> |
|
""", |
|
elem_classes="env-instructions", |
|
) |
|
|
|
|
|
with gr.Row(elem_classes="env-buttons"): |
|
save_env_button = gr.Button( |
|
"💾 保存更改", |
|
variant="primary", |
|
elem_classes="env-button", |
|
) |
|
refresh_button = gr.Button( |
|
"🔄 刷新列表", elem_classes="env-button" |
|
) |
|
|
|
|
|
env_status = gr.HTML( |
|
label="操作状态", |
|
value="", |
|
elem_classes="env-status", |
|
) |
|
|
|
|
|
save_env_button.click( |
|
fn=save_env_table_changes, |
|
inputs=[env_table], |
|
outputs=[env_status], |
|
).then(fn=update_env_table, outputs=[env_table]) |
|
|
|
refresh_button.click(fn=update_env_table, outputs=[env_table]) |
|
|
|
|
|
run_button.click( |
|
fn=process_with_live_logs, |
|
inputs=[question_input, module_dropdown], |
|
outputs=[token_count_output, status_output, log_display2], |
|
) |
|
|
|
|
|
module_dropdown.change( |
|
fn=update_module_description, |
|
inputs=module_dropdown, |
|
outputs=module_description, |
|
) |
|
|
|
|
|
refresh_logs_button2.click( |
|
fn=lambda: get_latest_logs(100, LOG_QUEUE), outputs=[log_display2] |
|
) |
|
|
|
clear_logs_button2.click(fn=clear_log_file, outputs=[log_display2]) |
|
|
|
|
|
def toggle_auto_refresh(enabled): |
|
if enabled: |
|
return gr.update(every=3) |
|
else: |
|
return gr.update(every=0) |
|
|
|
auto_refresh_checkbox2.change( |
|
fn=toggle_auto_refresh, |
|
inputs=[auto_refresh_checkbox2], |
|
outputs=[log_display2], |
|
) |
|
|
|
|
|
|
|
return app |
|
|
|
|
|
|
|
def main(): |
|
try: |
|
|
|
global LOG_FILE |
|
LOG_FILE = setup_logging() |
|
logging.info("OWL Web应用程序启动") |
|
|
|
|
|
log_thread = threading.Thread( |
|
target=log_reader_thread, args=(LOG_FILE,), daemon=True |
|
) |
|
log_thread.start() |
|
logging.info("日志读取线程已启动") |
|
|
|
|
|
init_env_file() |
|
app = create_ui() |
|
|
|
app.queue() |
|
app.launch(share=False) |
|
except Exception as e: |
|
logging.error(f"启动应用程序时发生错误: {str(e)}") |
|
print(f"启动应用程序时发生错误: {str(e)}") |
|
import traceback |
|
|
|
traceback.print_exc() |
|
|
|
finally: |
|
|
|
STOP_LOG_THREAD.set() |
|
STOP_REQUESTED.set() |
|
logging.info("应用程序关闭") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|