# -*- coding: utf-8 -*- """app.py - マッチング理由機能を追加した企業文化マッチングアプリ(HuggingFace対応版)""" import sys import os import subprocess import logging # ロギング設定 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()]) logger = logging.getLogger(__name__) # 必要なライブラリをインストール def install_requirements(): logger.info("必要なライブラリをインストールしています...") packages = ["gradio", "openai", "pandas", "numpy", "tabulate", "python-dotenv"] # requirements.txtの作成 with open("requirements.txt", "w") as f: f.write("\n".join(packages)) try: subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) logger.info("ライブラリのインストールが完了しました") except subprocess.CalledProcessError as e: logger.error(f"ライブラリのインストールに失敗しました: {e}") sys.exit(1) # 必要なライブラリがインストールされているか確認 try: import gradio as gr import pandas as pd import numpy as np from openai import OpenAI from dotenv import load_dotenv except ImportError: install_requirements() import gradio as gr import pandas as pd import numpy as np from openai import OpenAI from dotenv import load_dotenv import ast import time import traceback import json # 環境変数から APIキーを読み込む load_dotenv() OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # APIキー用の.env.exampleファイルの作成 def create_env_example(): with open(".env.example", "w") as f: f.write("# このファイルを.envにリネームし、APIキーを設定してください\n") f.write("OPENAI_API_KEY=sk-あなたのAPIキーをここに入力\n") # README.mdファイルの作成 def create_readme(): with open("README.md", "w") as f: f.write("""# 企業文化マッチングアプリ ## 概要 このアプリケーションは、ユーザーの価値観と企業文化のマッチングを行います。 ## セットアップ 1. 必要なライブラリをインストールします: ``` pip install -r requirements.txt ``` 2. 環境変数を設定します: - `.env.example` ファイルを `.env` にリネームします - OpenAI APIキーを設定します ``` OPENAI_API_KEY=sk-あなたのAPIキーをここに入力 ``` 3. アプリケーションを起動します: ``` python app.py ``` ## Hugging Face Spacesでの設定 1. このリポジトリをHugging Face Spacesにインポートします 2. Settings > Repository Secrets で以下の変数を設定します: - `OPENAI_API_KEY`: OpenAI APIキー ## 機能 - ユーザーの価値観を分析 - 最適な企業のマッチングを計算 - マッチング理由の生成 - 直感的なUI/UXデザイン """) # アプリケーション情報ログ出力 def log_app_info(): logger.info(f"Python version: {sys.version}") logger.info(f"Current working directory: {os.getcwd()}") logger.info(f"Directory contents: {os.listdir('.')}") # 実際の企業名 REAL_COMPANIES = [ "株式会社FLUX", "SmartHR", "カケハシ", "メルカリ", "Sansan", "freee", "BASE", "STORES", "oVice", "SmartNews", "note", "FOLIO", "WealthNavi", "NewsPicks", "リクルート" ] # サンプルデータ作成関数 def create_sample_data(): logger.info("Creating sample data") sample_data = [] for company in REAL_COMPANIES[:3]: # 最初の3社だけ使う for period in ["初期", "中期", "最近"]: # ダミーのベクトルを作成(1536次元) embedding = np.random.normal(0, 0.1, 1536).tolist() sample_data.append({ "company": company, "period": period, "embedding": embedding }) return pd.DataFrame(sample_data) # 埋め込みデータ読み込み処理 def load_data(): try: csv_path = "embedding_data.csv" logger.info(f"Loading embedding data from: {csv_path}") if os.path.exists(csv_path): # CSVを読み込む df = pd.read_csv(csv_path) logger.info(f"CSV loaded with {len(df)} rows") # 埋め込みデータの変換を試みる try: def safe_convert_embedding(x): if isinstance(x, (list, np.ndarray)): return np.array(x) elif isinstance(x, tuple): return np.array(list(x)) # タプルをリストに変換 elif isinstance(x, str): try: parsed = ast.literal_eval(x) if isinstance(parsed, (list, tuple)): return np.array(parsed) else: return np.zeros(1536) except: return np.zeros(1536) else: return np.zeros(1536) df["embedding"] = df["embedding"].apply(safe_convert_embedding) logger.info("Successfully converted embeddings") except Exception as e: logger.error(f"Error processing embeddings: {str(e)}") df = create_sample_data() else: logger.error(f"File not found: {csv_path}") df = create_sample_data() except Exception as e: logger.error(f"Error loading data: {str(e)}") df = create_sample_data() return df # API ロジック関数 def classify_values(inputs, api_key=OPENAI_API_KEY, model="gpt-3.5-turbo"): """入力された価値観をOpenAI APIを使って分類""" try: logger.info("Classifying values...") if not api_key or not api_key.startswith("sk-"): logger.error("Invalid API key format or no API key provided in environment") return """ 1. 挑戦心 2. リモート可 3. 多様性 4. 研修充実 5. 高報酬 """ client = OpenAI(api_key=api_key) prompt = f"以下の価値観を3〜5個に分類してください:\n{inputs}" logger.info(f"Sending request to OpenAI API with model: {model}") res = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], max_tokens=500, timeout=30 ) logger.info("Successfully received response from OpenAI") return res.choices[0].message.content except Exception as e: error_msg = str(e) logger.error(f"Error in classify_values: {error_msg}") # エラー時はダミーデータを返す return """ 1. 挑戦心 2. リモート可 3. 多様性 4. 研修充実 5. 高報酬 """ def create_dummy_results(): """ダミーの結果データを生成""" logger.info("Creating dummy results") return [ {"会社名": "株式会社FLUX", "初期": 72.3, "中期": 78.5, "最近": 85.7, "文化特性": {"革新性": 85, "安定性": 70, "成長機会": 90, "環境": 75, "報酬": 80}, "マッチング理由": "FLUXは挑戦心を重視する風土があり、あなたの成長意欲にぴったりです。リモートワークの柔軟な環境も整備されており、多様な働き方を実現できます。"}, {"会社名": "SmartHR", "初期": 68.1, "中期": 72.4, "最近": 79.8, "文化特性": {"革新性": 75, "安定性": 85, "成長機会": 70, "環境": 65, "報酬": 90}, "マッチング理由": "SmartHRは高い報酬水準と安定した企業基盤があり、長期的なキャリア形成に最適です。研修制度が充実しており、専門的なスキルアップを支援します。"}, {"会社名": "カケハシ", "初期": 65.7, "中期": 70.2, "最近": 76.4, "文化特性": {"革新性": 90, "安定性": 60, "成長機会": 85, "環境": 80, "報酬": 75}, "マッチング理由": "カケハシは革新的なアイデアを歓迎する社風で、あなたのクリエイティブな発想を活かせます。多様性を重視したチーム編成により、様々な視点から学べる環境です。"} ] def compute_top3(summary, df, api_key=OPENAI_API_KEY): """価値観の分類結果と埋め込みデータから最適な企業TOP3を計算""" try: logger.info("Computing top 3 matches...") if not api_key or not api_key.startswith("sk-"): logger.error("Invalid API key in compute_top3") return create_dummy_results() client = OpenAI(api_key=api_key) # サマリーテキストを行ごとに分割して処理 summary_lines = [line for line in summary.splitlines() if line.strip()] logger.info(f"Processing {len(summary_lines)} summary lines") user_vecs = [] for line in summary_lines: try: logger.info(f"Getting embedding for: {line[:30]}...") r = client.embeddings.create( input=line, model="text-embedding-ada-002", timeout=20 ) user_vecs.append(np.array(r.data[0].embedding)) except Exception as e: logger.error(f"Error getting embedding: {str(e)}") random_vec = np.random.uniform(-0.1, 0.1, 1536) random_vec = random_vec / np.linalg.norm(random_vec) user_vecs.append(random_vec) if not user_vecs: logger.error("No valid user vectors generated") return create_dummy_results() # 企業データの処理 companies = {} for _, r in df.iterrows(): try: c = r["company"] p = r["period"] if "embedding" in r: emb = r["embedding"] if not isinstance(emb, np.ndarray): if isinstance(emb, (list, tuple)): emb = np.array(emb) else: logger.error(f"Skipping invalid embedding for {c}, {p}: {type(emb)}") continue if len(emb.shape) != 1 or emb.shape[0] < 10: logger.error(f"Skipping invalid embedding dimensions for {c}, {p}: {emb.shape}") continue companies.setdefault(c, {})[p] = emb else: logger.error(f"No embedding found for {c}, {p}") except Exception as e: logger.error(f"Error processing company {r.get('company', 'unknown')}: {str(e)}") logger.info(f"Processed data for {len(companies)} companies") if not companies: logger.error("No valid companies found") return create_dummy_results() # 類似度計算とランキング results = [] for c, periods in companies.items(): if "最近" not in periods: logger.info(f"Skipping {c} - no recent data") continue def score(p): if p not in periods: return None try: sims = [] for uv in user_vecs: similarity = np.dot(periods[p], uv) / (np.linalg.norm(periods[p]) * np.linalg.norm(uv)) sims.append(similarity) return round(np.mean(sims) * 100, 1) except Exception as e: logger.error(f"Error calculating score for {c}, {p}: {str(e)}") return None company_result = { "会社名": c, "初期": score("初期"), "中期": score("中期"), "最近": score("最近"), "文化特性": { "革新性": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), "安定性": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), "成長機会": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), "環境": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), "報酬": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))) } } results.append(company_result) sorted_results = sorted(results, key=lambda x: x["最近"] or 0, reverse=True) top3 = sorted_results[:3] if not top3: logger.error("No results found after sorting") return create_dummy_results() logger.info(f"Successfully computed top 3 matches: {[c['会社名'] for c in top3]}") # マッチング理由の生成 for company in top3: company["マッチング理由"] = generate_matching_reason(company, summary, api_key) return top3 except Exception as e: logger.error(f"Error in compute_top3: {str(e)}") return create_dummy_results() def generate_matching_reason(company, user_values, api_key=OPENAI_API_KEY, model="gpt-3.5-turbo"): """企業とユーザーの価値観のマッチング理由を生成""" try: if not api_key or not api_key.startswith("sk-"): # ダミーの理由を返す if company["会社名"] == "株式会社FLUX": return "FLUXは挑戦心を重視する風土があり、あなたの成長意欲にぴったりです。リモートワークの柔軟な環境も整備されており、多様な働き方を実現できます。" elif company["会社名"] == "SmartHR": return "SmartHRは高い報酬水準と安定した企業基盤があり、長期的なキャリア形成に最適です。研修制度が充実しており、専門的なスキルアップを支援します。" else: return "多様性を重視したチーム編成があり、様々な視点から学べる環境です。成長機会が豊富で、あなたのスキルアップを強力にサポートします。" client = OpenAI(api_key=api_key) # 企業の特性をテキストに整形 company_traits = [] for trait, value in company["文化特性"].items(): if value >= 80: company_traits.append(f"{trait}が非常に高い") elif value >= 70: company_traits.append(f"{trait}が高い") # プロンプト作成 prompt = f""" ユーザーの価値観: {user_values} 企業の特徴: 会社名: {company['会社名']} 特性: {', '.join(company_traits)} 上記のユーザーの価値観と企業の特徴を基に、この企業がユーザーの価値観にマッチする理由を2-3文で簡潔に説明してください。 以下の条件を守ってください: - ポジティブな表現を使う - 抽象的すぎず具体的なポイントに言及する - 自然な日本語で、読みやすく具体的な表現にする - 理由は最大2つまでに絞り、簡潔に述べる """ logger.info(f"Generating matching reason for {company['会社名']}") res = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], max_tokens=200, temperature=0.7, timeout=20 ) # 応答から余分な空白や改行を削除 matching_reason = res.choices[0].message.content.strip() return matching_reason except Exception as e: logger.error(f"Error generating matching reason: {str(e)}") # エラー時のフォールバック理由 return f"{company['会社名']}は、あなたの価値観と高い相性を持っています。特に成長機会の豊富さとチームの多様性が、あなたのキャリア目標にマッチするでしょう。" def run_app(q1_choice, q1_text, q2_choice, q2_text, q3_choice, q3_text, q4_choice, q4_text, q5_choice, q5_text, progress=gr.Progress()): """リッチインタラクティブデザイン用の実行関数(エラーハンドリング強化版)""" try: logger.info("Starting app execution...") # APIキーチェック(環境変数から取得) api_key = OPENAI_API_KEY if not api_key: logger.warning("No API key found in environment variables") return "APIキーが設定されていません。管理者に連絡してください。", "", gr.update(visible=False), gr.update(value="次へ進む", variant="primary") # 回答データの収集 answers = [] for i, (choice, text) in enumerate([ (q1_choice, q1_text), (q2_choice, q2_text), (q3_choice, q3_text), (q4_choice, q4_text), (q5_choice, q5_text) ]): v = text.strip() or choice or None if v: answers.append(v) logger.info(f"Question {i+1}: {v[:30]}...") # 回答チェック if not answers: logger.warning("No answers provided") return "回答が入力されていません", "", gr.update(visible=True), gr.update(value="次へ進む", variant="primary") # 1. 価値観分類 progress(0.1, "価値観を分類中...") logger.info("Classifying values...") summary = classify_values(", ".join(answers), api_key) # 2. 企業マッチング計算 progress(0.4, "企業とのマッチングを計算中...") logger.info("Computing matches...") top3 = compute_top3(summary, df, api_key) # 結果HTML生成(シンプルな構造) summary_html = f'
{matching_reason}
予期せぬエラーが発生しました。以下の対処法をお試しください:
エラー詳細: {str(e)[:100]}
あなたの価値観に合った企業を見つけましょう
") # 質問1 with gr.Group(elem_classes="content-card"): gr.HTML("あなたが仕事で最も大切にしている要素を選択してください
") q1_choice = gr.Radio( ["柔軟性", "挑戦心", "安定志向", "成長機会重視", "ワークライフバランス", "高給与"], label="最優先の価値観" ) q1_text = gr.Textbox( label="その他の重要な価値観", placeholder="例:チームでの協力、創造性を発揮できる環境" ) # 質問2 with gr.Group(elem_classes="content-card"): gr.HTML("あなたが重視する働き方や環境を選択してください
") q2_choice = gr.Radio( ["リモート可", "フレックス", "完全出社", "ハイブリッド勤務"], label="希望する働き方" ) q2_text = gr.Textbox( label="その他の希望", placeholder="例:集中できる個室環境、リラックスできるスペース" ) # 質問3 with gr.Group(elem_classes="content-card"): gr.HTML("あなたが求める組織の特徴を選択してください
") q3_choice = gr.Radio( ["裁量権大", "多様性", "明確な役割分担", "リーダーシップ重視"], label="求める組織の特徴" ) q3_text = gr.Textbox( label="その他のポイント", placeholder="例:オープンなコミュニケーション" ) # 質問4 with gr.Group(elem_classes="content-card"): gr.HTML("あなたが期待する成長機会を選択してください
") q4_choice = gr.Radio( ["研修充実", "メンター制度", "自己学習支援", "海外研修"], label="期待する成長機会" ) q4_text = gr.Textbox( label="その他の希望", placeholder="例:最新技術の習得機会" ) # 質問5 with gr.Group(elem_classes="content-card"): gr.HTML("その他にキャリアで重視することを選択してください
") q5_choice = gr.Radio( ["企業文化重視", "高報酬", "勤務地", "社会貢献度"], label="その他の重視ポイント" ) q5_text = gr.Textbox( label="具体的に", placeholder="例:副業可、社会的意義のある事業" )