File size: 45,661 Bytes
3808745 538cdc0 a11b407 538cdc0 d4ada08 9dffbe6 3808745 538cdc0 d4ada08 9dffbe6 3808745 538cdc0 a1520e7 9dffbe6 d2960de 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 d2960de 3808745 9dffbe6 3808745 9dffbe6 d2960de 3808745 9dffbe6 d2960de 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 27f82ca 538cdc0 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 d2960de 9dffbe6 3808745 9dffbe6 d2960de 3808745 9dffbe6 d2960de 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 9be7074 3808745 d2960de 3808745 48678c8 3808745 d2960de 3808745 d2960de 3808745 48678c8 538cdc0 3808745 d4ada08 3808745 3cbd9b0 3808745 97a6fba 35edd4a d4ada08 9be7074 538cdc0 9be7074 d4ada08 538cdc0 9be7074 d4ada08 9be7074 538cdc0 d4ada08 9be7074 35edd4a d4ada08 35edd4a 9be7074 a1520e7 d4ada08 9be7074 538cdc0 d4ada08 9be7074 d4ada08 538cdc0 d2960de a1520e7 3808745 a1520e7 3808745 a1520e7 3808745 d2960de 538cdc0 3808745 538cdc0 3808745 538cdc0 d2960de 538cdc0 d2960de 3808745 a1520e7 9dffbe6 9be7074 3808745 538cdc0 d4ada08 9dffbe6 3808745 a1520e7 9dffbe6 a1520e7 3808745 a1520e7 3808745 538cdc0 9be7074 a1520e7 9dffbe6 a1520e7 9dffbe6 a1520e7 3808745 a1520e7 9dffbe6 3808745 538cdc0 9dffbe6 a1520e7 3808745 a1520e7 3808745 538cdc0 a1520e7 9dffbe6 a1520e7 d2960de a1520e7 9dffbe6 a1520e7 3808745 a1520e7 3808745 a1520e7 9dffbe6 a1520e7 9dffbe6 a1520e7 9dffbe6 a1520e7 3808745 a1520e7 d2960de a1520e7 3808745 a1520e7 3808745 538cdc0 3808745 538cdc0 3808745 538cdc0 9dffbe6 3808745 538cdc0 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 d4ada08 3808745 9be7074 3808745 d4ada08 3808745 d4ada08 3808745 9be7074 3808745 9be7074 3808745 d4ada08 538cdc0 3808745 1c4eee7 3808745 1c4eee7 3808745 1c4eee7 3808745 1c4eee7 3808745 051b4e3 d2960de 3808745 9be7074 051b4e3 3808745 051b4e3 d2960de 3808745 051b4e3 27f82ca 051b4e3 3808745 d2960de 051b4e3 3808745 d2960de 3808745 051b4e3 3808745 27f82ca d2960de 3808745 6893a36 3808745 6893a36 3808745 27f82ca 3808745 d4ada08 3808745 7eda262 499ce9d 9be7074 499ce9d 9be7074 7eda262 3808745 6893a36 d2960de 6893a36 9a22c3f 48678c8 5816c60 3808745 d2960de 6893a36 3808745 48678c8 3808745 9be7074 3808745 538cdc0 3808745 48678c8 3808745 48678c8 d824351 48678c8 3808745 27f82ca 3808745 d4ada08 3808745 d2960de 3808745 d4ada08 3808745 d4ada08 9be7074 3808745 d4ada08 3808745 9be7074 3808745 d4ada08 3808745 d4ada08 3808745 538cdc0 3808745 538cdc0 a1520e7 9dffbe6 3808745 9dffbe6 3808745 9dffbe6 3808745 d4ada08 a1520e7 3808745 0f5a716 a1520e7 9dffbe6 a1520e7 3808745 a1520e7 3808745 9dffbe6 27f82ca 9dffbe6 538cdc0 3808745 538cdc0 3808745 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 |
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Import from the correct module path
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 # For regular expression operations
os.environ["PYTHONIOENCODING"] = "utf-8"
# 配置日志系统
def setup_logging():
"""配置日志系统,将日志输出到文件和内存队列以及控制台"""
# 创建logs目录(如果不存在)
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 "初始化运行中..."
# 过滤日志,只保留 camel.agents.chat_agent - INFO 的日志
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
# 如果JSON解析失败或没有找到消息数组,尝试直接提取对话内容
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)
# Dictionary containing module descriptions
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}")
# 检查模块是否在MODULE_DESCRIPTIONS中
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)}")
# 检查是否包含construct_society函数
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文件读取环境变量
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, "系统")
# 添加.env文件环境变量(中等优先级)
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
# 同时更新.env文件
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()
# 从.env文件中删除
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相关的关键词
api_keywords = [
"api",
"key",
"token",
"secret",
"password",
"openai",
"qwen",
"deepseek",
"google",
"search",
"hf",
"hugging",
"chunkr",
"firecrawl",
]
# 检查是否包含API相关关键词(不区分大小写)
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相关的环境变量
api_env_vars = {k: v for k, v in env_vars.items() if is_api_related(k)}
# 转换为列表格式,以符合Gradio Dataframe的要求
# 格式: [变量名, 变量值, 获取指南链接]
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() # 记录已处理的键,用于检测删除的变量
# 处理pandas DataFrame对象
import pandas as pd
if isinstance(data, pd.DataFrame):
# 获取列名信息
columns = data.columns.tolist()
logging.info(f"DataFrame列名: {columns}")
# 遍历DataFrame的每一行
for index, row in data.iterrows():
# 使用列名访问数据
if len(columns) >= 3:
# 获取变量名和值 (第0列是变量名,第1列是值)
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]
# 检查系统环境变量(包括从.env加载的)
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框架开发的先进多智能体协作系统,旨在通过智能体协作解决复杂问题。
可以通过修改本地脚本自定义模型和工具。
本网页应用目前处于测试阶段,仅供演示和测试使用,尚未推荐用于生产环境。
"""
)
# 添加自定义CSS
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_DESCRIPTIONS中定义的模块
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",
], # 将最后一列设置为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("日志读取线程已启动")
# 初始化.env文件(如果不存在)
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()
|