diff --git a/owl/webapp_jp.py b/owl/webapp_jp.py new file mode 100644 index 0000000..893dd81 --- /dev/null +++ b/owl/webapp_jp.py @@ -0,0 +1,1313 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Apache License 2.0(「ライセンス」)に基づいてライセンスされています。 +# あなたはライセンスに準拠している場合を除き、このファイルを使用できません。 +# ライセンスのコピーは以下から入手できます。 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 適用される法律で要求されているか、書面で合意されていない限り、 +# ライセンスに基づいて配布されるソフトウェアは「現状のまま」で、 +# 明示的または黙示的ないかなる種類の保証や条件もなく配布されます。 +# ライセンスに基づく特定の言語での権限と制限については、ライセンスを参照してください。 +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# 正しいモジュールパスからインポート +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ディレクトリを作成(存在しない場合) + 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) + + # Add handlers to root logger + 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): + """キューから最新のログ行を取得するか、キューが空の場合はファイルから直接読み取る + + 引数: + max_lines: 返す最大行数 + queue_source: 使用するキューを指定、デフォルトはLOG_QUEUE + + 戻り値: + 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 "まだ会話記録はありません。" + + # Process log content, extract the latest user and assistant messages + simplified_logs = [] + + # Use a set to track messages that have already been processed, to avoid duplicates + processed_messages = set() + + def process_message(role, content): + # Create a unique identifier to track messages + 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 "🤖" + role_ja = "ユーザー" if role.lower() == "user" else "アシスタント" + return f"""### {role_emoji} {role_ja}エージェント + +{content}""" + + for log in filtered_logs: + formatted_messages = [] + # Try to extract message array + 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 JSON parsing fails or no message array is found, try to extract conversation content directly + 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)) + + # Format log output, ensure appropriate separation between each conversation record + formatted_logs = [] + for i, log in enumerate(simplified_logs): + # Remove excess whitespace characters from beginning and end + log = log.strip() + + formatted_logs.append(log) + + # Ensure each conversation record ends with a newline + 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 = """#=========================================== +# モデル & API +# (参照: https://docs.camel-ai.org/key_modules/models.html#) +#=========================================== + +# OPENAI API (https://platform.openai.com/api-keys) +OPENAI_API_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='あなたのキー' + +# DeepSeek API (https://platform.deepseek.com/api_keys) +DEEPSEEK_API_KEY='あなたのキー' + +#=========================================== +# ツール & サービス 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='あなたのキー' +SEARCH_ENGINE_ID='あなたのID' + +# Chunkr API (https://chunkr.ai/) +CHUNKR_API_KEY='あなたのキー' + +# Firecrawl API (https://www.firecrawl.dev/) +FIRECRAWL_API_KEY='あなたのキー' +#FIRECRAWL_API_URL="https://api.firecrawl.dev" +""" + + +def validate_input(question: str) -> bool: + """ユーザー入力が有効かどうかを検証 + + 引数: + question: ユーザーの質問 + + 戻り値: + bool: 入力が有効かどうか + """ + # 入力が空またはスペースのみかどうかをチェック + if not question or question.strip() == "": + return False + return True + + +def run_owl(question: str, example_module: str) -> Tuple[str, str, str]: + """OWLシステムを実行して結果を返す + + 引数: + question: ユーザーの質問 + example_module: インポートする例モジュール名(例:"run_terminal_zh"や"run_deep") + + 戻り値: + Tuple[...]: 回答、トークン数、ステータス + """ + global CURRENT_PROCESS + + # Validate input + if not validate_input(question): + logging.warning("ユーザーが無効な入力を送信しました") + return ( + "有効な質問を入力してください", + "0", + "❌ エラー: 無効な入力質問", + ) + + try: + # Ensure environment variables are loaded + load_dotenv(find_dotenv(), override=True) + logging.info( + f"質問を処理中: '{question}', モジュール使用: {example_module}" + ) + + # Check if the module is in MODULE_DESCRIPTIONS + if example_module not in MODULE_DESCRIPTIONS: + logging.error(f"ユーザーがサポートされていないモジュールを選択しました: {example_module}") + return ( + f"選択されたモジュール '{example_module}' はサポートされていません", + "0", + "❌ エラー: サポートされていないモジュール", + ) + + # Dynamically import target module + 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)}", + ) + + # Check if it contains the construct_society function + if not hasattr(module, "construct_society"): + logging.error( + f"construct_society 関数がモジュール {module_path} に見つかりません" + ) + return ( + f"construct_society 関数がモジュール {module_path} に見つかりません", + "0", + "❌ エラー: モジュールインターフェースが互換性がありません", + ) + + # Build society simulation + 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)}", + ) + + # Run society simulation + 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)}", + ) + + # Safely get token count + 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(): + """環境変数を読み込み、辞書形式で返す + + 戻り値: + 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ファイルに保存 + + 引数: + env_vars: 辞書、キーは環境変数名、値は文字列または(value, source)タプル + """ + try: + dotenv_path = init_env_file() + + # Save each environment variable + for key, value_data in env_vars.items(): + if key and key.strip(): # Ensure key is not empty + # Handle case where value might be a tuple + if isinstance(value_data, tuple): + value = value_data[0] + else: + value = value_data + + set_key(dotenv_path, key.strip(), value.strip()) + + # Reload environment variables to ensure they take effect + 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): + """単一の環境変数を追加または更新 + + 引数: + key: 環境変数名 + value: 環境変数値 + from_frontend: フロントエンド設定からかどうか、デフォルトはTrue + """ + try: + if not key or not key.strip(): + return False, "変数名は空にできません" + + key = key.strip() + value = value.strip() + + # If from frontend, add to frontend environment variable dictionary + if from_frontend: + WEB_FRONTEND_ENV_VARS[key] = value + # Directly update system environment variables + os.environ[key] = value + + # Also update .env file + 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() + + # Delete from .env file + dotenv_path = init_env_file() + unset_key(dotenv_path, key) + + # Delete from frontend environment variable dictionary + if key in WEB_FRONTEND_ENV_VARS: + del WEB_FRONTEND_ENV_VARS[key] + + # Also delete from current process environment + 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関連かどうかを判断 + + 引数: + key: 環境変数名 + + 戻り値: + 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ガイドを返す + + 引数: + key: 環境変数名 + + 戻り値: + 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() + # Filter out API-related environment variables + api_env_vars = {k: v for k, v in env_vars.items() if is_api_related(k)} + # Convert to list format to meet Gradio Dataframe requirements + # Format: [Variable name, Variable value, Guide link] + result = [] + for k, v in api_env_vars.items(): + guide = get_api_guide(k) + # If there's a guide link, create a clickable link + guide_link = ( + f"🔗 取得" + if guide + else "" + ) + result.append([k, v[0], guide_link]) + return result + + +def save_env_table_changes(data): + """環境変数テーブルへの変更を保存 + + 引数: + data: データフレームデータ、おそらくpandas DataFrameオブジェクト + + 戻り値: + str: 操作ステータス情報、HTML形式のステータスメッセージを含む + """ + try: + logging.info( + f"環境変数テーブルデータの処理を開始します、タイプ: {type(data)}" + ) + + # Get all current environment variables + current_env_vars = load_env_vars() + processed_keys = set() # Record processed keys to detect deleted variables + + # Process pandas DataFrame object + import pandas as pd + + if isinstance(data, pd.DataFrame): + # Get column name information + columns = data.columns.tolist() + logging.info(f"DataFrameの列名: {columns}") + + # Iterate through each row of the DataFrame + for index, row in data.iterrows(): + # Use column names to access data + if len(columns) >= 3: + # Get variable name and value (column 0 is name, column 1 is value) + key = row[0] if isinstance(row, pd.Series) else row.iloc[0] + value = row[1] if isinstance(row, pd.Series) else row.iloc[1] + + # Check if it's an empty row or deleted variable + if ( + key and str(key).strip() + ): # If key name is not empty, add or update + logging.info( + f"環境変数の処理: {key} = {value}" + ) + add_env_var(key, str(value)) + processed_keys.add(key) + # Process other formats + elif isinstance(data, dict): + logging.info(f"辞書形式データのキー: {list(data.keys())}") + # If dictionary format, try different keys + if "data" in data: + rows = data["data"] + elif "values" in data: + rows = data["values"] + elif "value" in data: + rows = data["value"] + else: + # Try using dictionary directly as row data + 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)}" + + # Process deleted variables - check if there are variables in current environment not appearing in the table + api_related_keys = {k for k in current_env_vars.keys() if is_api_related(k)} + keys_to_delete = api_related_keys - processed_keys + + # Delete variables no longer in the table + 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ファイル > システム環境変数 + """ + # Check frontend configured environment variables + if key in WEB_FRONTEND_ENV_VARS: + return WEB_FRONTEND_ENV_VARS[key] + + # Check system environment variables (including those loaded from .env) + return os.environ.get(key, "") + + +def create_ui(): + """拡張されたGradioインターフェースを作成""" + + def clear_log_file(): + """ログファイルの内容をクリア""" + try: + if LOG_FILE and os.path.exists(LOG_FILE): + # Clear log file content instead of deleting the file + open(LOG_FILE, "w").close() + logging.info("ログファイルがクリアされました") + # Clear log queue + 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 + 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() + + # 処理が完了するのを待つ間、1秒ごとにログを更新 + while bg_thread.is_alive(): + # 会話記録表示を更新 + logs2 = get_latest_logs(100, LOG_QUEUE) + + # Always update status + yield ( + "0", + " 処理中...", + logs2, + ) + + time.sleep(1) + + # Processing complete, get results + if not result_queue.empty(): + result = result_queue.get() + answer, token_count, status = result + + # Final update of conversation record + logs2 = get_latest_logs(100, LOG_QUEUE) + + # Set different indicators based on status + if "エラー" in status: + status_with_indicator = ( + f" {status}" + ) + else: + status_with_indicator = ( + f" {status}" + ) + + yield token_count, status_with_indicator, logs2 + else: + logs2 = get_latest_logs(100, LOG_QUEUE) + yield ( + "0", + " 終了しました", + logs2, + ) + + with gr.Blocks(title="OWL", theme=gr.themes.Soft(primary_hue="blue")) as app: + gr.Markdown( + """ + # 🦉 OWL マルチエージェント協力システム + + CAMELフレームワークをベースに開発された高度なマルチエージェント協力システムで、エージェント協力を通じて複雑な問題を解決するように設計されています。 + + モデルやツールはローカルスクリプトを変更することでカスタマイズできます。 + + このウェブアプリは現在ベータ開発中です。デモンストレーションとテスト目的のみで提供されており、本番環境での使用はまだ推奨されていません。 + """ + ) + + # Add custom CSS + gr.HTML(""" + + """) + + 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="Googleで検索して、camel-aiのcamelフレームワークのGitHubスター数、フォーク数などを要約し、その数値をplotパッケージを使ってPythonファイルに書き込み、ローカルに保存して、生成したPythonファイルを実行してください。", + ) + + # Enhanced module selection dropdown + # Only includes modules defined in MODULE_DESCRIPTIONS + module_dropdown = gr.Dropdown( + choices=list(MODULE_DESCRIPTIONS.keys()), + value="run", + label="機能モジュールを選択", + interactive=True, + ) + + # Module description text box + module_description = gr.Textbox( + value=MODULE_DESCRIPTIONS["run"], + label="モジュールの説明", + interactive=False, + elem_classes="module-info", + ) + + with gr.Row(): + run_button = gr.Button( + "実行", variant="primary", elem_classes="primary" + ) + + status_output = gr.HTML( + value=" 準備完了", + label="ステータス", + ) + token_count_output = gr.Textbox( + label="トークン数", interactive=False, elem_classes="token-count" + ) + + # Example questions + examples = [ + "Googleで検索して、camel-aiのcamelフレームワークのGitHubスター数、フォーク数などを要約し、その数値をplotパッケージを使ってPythonファイルに書き込み、ローカルに保存して、生成したPythonファイルを実行してください。", + "Amazonを閲覧して、プログラマーに魅力的な商品を見つけてください。商品名と価格を提供してください", + "Hello worldを表示するPythonファイルを作成し、ローカルに保存してください", + ] + + gr.Examples(examples=examples, inputs=question_input) + + gr.HTML(""" + + """) + + with gr.Tabs(): # Set conversation record as the default selected tab + with gr.TabItem("会話記録"): + # Add conversation record display area + 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システムの機能にとって非常に重要です。環境変数はツールの要件に応じて柔軟に設定できます。 + """) + + # Main content divided into two-column layout + with gr.Row(): + # Left column: Environment variable management controls + with gr.Column(scale=3): + with gr.Box(elem_classes="env-controls"): + # Environment variable table - set to interactive for direct editing + gr.Markdown(""" +
+ ヒント: cp .env_template .env を実行してローカルの.envファイルを作成し、実行モジュールに応じて必要な環境変数を柔軟に設定してください +
+ """) + + # Enhanced environment variable table, supporting adding and deleting rows + env_table = gr.Dataframe( + headers=[ + "変数名", + "値", + "取得ガイド", + ], + datatype=[ + "str", + "str", + "html", + ], # Set the last column as HTML type to support links + row_count=10, # Increase row count to allow adding new variables + col_count=(3, "fixed"), + value=update_env_table, + label="APIキーと環境変数", + interactive=True, # Set as interactive, allowing direct editing + elem_classes="env-table", + ) + + # Operation instructions + gr.Markdown( + """ +
+ 操作ガイド: + +
+ """, + elem_classes="env-instructions", + ) + + # Environment variable operation buttons + 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" + ) + + # Status display + 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]) + + # Set up event handling + run_button.click( + fn=process_with_live_logs, + inputs=[question_input, module_dropdown], + outputs=[token_count_output, status_output, log_display2], + ) + + # Module selection updates description + module_dropdown.change( + fn=update_module_description, + inputs=module_dropdown, + outputs=module_description, + ) + + # Conversation record related event handling + 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]) + + # Auto refresh control + 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], + ) + + # No longer automatically refresh logs by default + + 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, favicon_path="../assets/owl-favicon.ico") + 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()