import os import json import gradio as gr from fastapi import FastAPI from starlette.staticfiles import StaticFiles # ---- 遅延インポート ---- def _lazy_imports_for_main(): from modules.utils import ensure_dirs # noqa: F401 from modules.rag_indexer import index_files_and_urls from modules.workflow import run_full_workflow ensure_dirs() return index_files_and_urls, run_full_workflow # ---- ユーティリティ ---- EXPORT_DIR = "/tmp/agent_studio/exports" os.makedirs(EXPORT_DIR, exist_ok=True) # 静的配信用に事前作成 def _link_or_note(path_str): """ 生成されたファイルパスから /files の相対URLを返す。 存在しなければ理由を文字列で返す。 """ if not path_str: return "(まだ生成されていません)" fname = os.path.basename(path_str) full = os.path.join(EXPORT_DIR, fname) if os.path.exists(full): # Hugging Face Space 上では相対リンクでOK return f"/files/{fname}" # modules 側が別ディレクトリで作った場合もあるので存在チェック if os.path.exists(path_str): # もし別パスにあるなら公開用にコピーしてリンク化 try: import shutil dst = os.path.join(EXPORT_DIR, os.path.basename(path_str)) if path_str != dst: shutil.copy2(path_str, dst) return f"/files/{os.path.basename(path_str)}" except Exception as e: return f"(生成済みだが公開に失敗: {e})" return "(パスが無効です)" # ---- UI コールバック ---- def ui_company_score_and_proposal( company_name, company_website, lead_email, objective, urls_text, files, # gr.File(multiple=True) は使わないが、値は受ける ): """ 戻り値順: score_json(str), contexts_text(str), proposal_text(str), docx_link(str), pptx_link(str), next_actions(str), index_report(str) """ index_files_and_urls, run_full_workflow = _lazy_imports_for_main() # URL & ファイルパス抽出(SpaceのUI都合で来ることがあるため防御的に扱う) urls = [u.strip() for u in (urls_text or "").splitlines() if u.strip()] file_paths = [] if files: # ここには通常来ないが、互換のため残す(path or tempfileオブジェクト) if isinstance(files, list): file_paths = [getattr(f, "name", None) or str(f) for f in files if f] else: file_paths = [getattr(files, "name", None) or str(files)] file_paths = [p for p in file_paths if p] # インデックス更新(失敗しても継続) try: index_report = index_files_and_urls(file_paths=file_paths, urls=urls) except Exception as e: index_report = f"Index error:\n\n{e}" # メイン処理(提案ドラフトなど) result = run_full_workflow( company_name=company_name or "(未入力)", company_website=company_website or "", lead_email=lead_email or "", objective=objective or "", temperature=0.3, ) score_json = json.dumps(result.get("score", {}), ensure_ascii=False, indent=2) contexts_text = "\n\n---\n\n".join(result.get("top_contexts") or []) proposal_text = result.get("proposal_markdown", "") next_actions = result.get("next_actions", "") # 生成物を /files で配信できる相対リンクに変換(テキストで返す) docx_path = result.get("exports", {}).get("docx_path", "") pptx_path = result.get("exports", {}).get("pptx_path", "") docx_link = _link_or_note(docx_path) pptx_link = _link_or_note(pptx_path) return ( score_json, contexts_text, proposal_text, docx_link, pptx_link, next_actions, index_report, ) # ---- Gradio UI(すべて Textbox / Button のみ)---- with gr.Blocks(title="営業自動化 Agent Studio", analytics_enabled=False, theme=gr.themes.Soft()) as demo: gr.Markdown("## 営業自動化 Agent Studio(Space内完結・リンクでダウンロード)") with gr.Row(): with gr.Column(): company_name = gr.Textbox(label="企業名", value="トヨタ自動車") company_website = gr.Textbox(label="企業Webサイト", value="https://toyota.jp/index.html") lead_email = gr.Textbox(label="リードのメールアドレス(任意)", value="") objective = gr.Textbox(label="提案目的(任意)", value="商談化のための初回提案") urls_text = gr.Textbox( label="クロールするURL(1行1件、任意)", lines=4, placeholder="https://example.com\nhttps://example.com/blog/post1", ) # NOTICE: ファイル型コンポーネントはスキーマ問題回避のため配置しない dummy_files = gr.Textbox(visible=False) # 互換のためダミー run_btn = gr.Button("ワークフロー実行", variant="primary") with gr.Column(): score_json = gr.Textbox(label="✅ 企業スコア(JSON)", lines=10, interactive=False) contexts_text = gr.Textbox(label="🧠 抽出コンテキスト(上位)", lines=8, interactive=False) proposal_text = gr.Textbox(label="✍️ 提案ドラフト(Markdownテキスト)", lines=16) # ここはテキストでリンクを表示(/files/xxx) docx_link = gr.Textbox(label="📎 DOCX ダウンロードリンク", interactive=False) pptx_link = gr.Textbox(label="📎 PPTX ダウンロードリンク", interactive=False) next_actions = gr.Textbox(label="🤖 次アクション提案", lines=4, interactive=False) index_report = gr.Textbox(label="🧩 インデックス更新ログ", lines=4, interactive=False) run_btn.click( fn=ui_company_score_and_proposal, inputs=[company_name, company_website, lead_email, objective, urls_text, dummy_files], outputs=[score_json, contexts_text, proposal_text, docx_link, pptx_link, next_actions, index_report], ) # ---- FastAPI / 静的ファイル公開(/files -> /tmp/agent_studio/exports)---- app = FastAPI() # 生成物を静的公開。相対リンク /files/xxx でダウンロード可能。 app.mount("/files", StaticFiles(directory=EXPORT_DIR), name="files") # Gradio をルートにマウント app = gr.mount_gradio_app(app, demo.queue(), path="/") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "7860")))