commit 922fa0e77ae46479396fd3976f7d4f2d0655f1f4 Author: kosukesuenaga Date: Mon Nov 17 14:21:29 2025 +0900 init diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a9be4e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +handle-company-webhook/ + +terraform.* +.terraform* + +IAM/ + +test/ + +venv/ +__pycache__/ +*.csv + +request.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..81f02b4 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Sales Tool プロジェクト + +## 概要 + +このリポジトリは、Sales Tool と呼ばれるシステムのバックエンドおよびインフラ構成を管理しています。主に以下のコンポーネントで構成されています。 + +- API ゲートウェイ(OpenAPI 定義) +- Google Cloud Functions(各種ファンクション) +- ワークフロー定義(Cloud Workflow) +- Terraform を使ったインフラ構成 +- テスト用スクリプト・データ + +--- + +## フォルダ構成 + +``` +. +├── requirements.txt +├── api-gateway +│ ├── openapi_dev.yaml # 開発環境用 OpenAPI 定義 +│ └── openapi.yaml # 本番環境用 OpenAPI 定義 +├── functions # Cloud Functions 実装 +│ ├── add-company +│ │ ├── _scripts +│ │ │ └── deploy_dev.sh # デプロイスクリプト(開発環境) +│ │ └── source +│ │ ├── main.py +│ │ └── requirements.txt +│ ├── create-meeting-log-to-hubspot +│ │ └── source +│ │ └── main.py +│ ├── create-minutes +│ │ └── source +│ │ ├── main.py +│ │ └── requirements.txt +│ └── trigger-minutes-workflow-from-miitel +│ └── source +│ ├── main.py +│ └── requirements.txt +├── test # テスト用スクリプトとサンプルデータ +│ ├── main.py +│ ├── mst_company.csv +│ ├── request.json +│ ├── requirements.txt +│ └── __pycache__ +│ └── main.cpython-38.pyc +├── terraform # インフラ(Terraform) +│ └── dev +│ ├── main.tf +│ ├── terraform.tfstate +│ └── terraform.tfstate.backup +└── workflows # Cloud Workflow 定義 + └── workflow-create-minutes + └── main.yaml +``` + +--- + +## ディレクトリ詳細 + +- **api-gateway**: OpenAPI 仕様ファイルを管理し、API ゲートウェイで使用。 +- **functions**: 各種 Google Cloud Functions のソースコードと依存定義。 +- **test**: ローカルでの動作確認やユニットテストに使用するスクリプト・データ。 +- **terraform**: Google Cloud 上のリソース(Storage バケットなど)の構築をコード化。 +- **workflows**: Cloud Workflow の実行定義ファイル(`.yaml`)。 + +--- + +## ローカル実行方法(概要) + +1. Python の依存インストール: `pip install -r requirements.txt` +2. Functions のデプロイ: 各フォルダ内の `deploy_dev.sh` を実行 +3. Terraform 初期化・適用: + ```bash + cd terraform/dev && terraform init && terraform apply -auto-approve + ``` +4. ワークフロー実行: Cloud Console または `gcloud workflows` CLI + +詳細は各ディレクトリの README やコメントを参照してください。 diff --git a/_test/test_dev.sh b/_test/test_dev.sh new file mode 100755 index 0000000..e17dc07 --- /dev/null +++ b/_test/test_dev.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# APIエンドポイントURL +API_URL="https://sales-tool-gw-dev-ex1cujb.an.gateway.dev/trigger-minutes-workflow-from-miitel" + +# APIキー(ヘッダーに付与する場合) +API_KEY="AIzaSyBVJOtvJTB4noAfUGEyMhCRqsF5yfypENc" + +# リクエストボディ +JSON_FILE="request.json" + +# curlコマンド実行 +curl -X POST "$API_URL" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $API_KEY" \ + -d @"$JSON_FILE" diff --git a/api-gateway/create_api_dev.sh b/api-gateway/create_api_dev.sh new file mode 100755 index 0000000..1e97276 --- /dev/null +++ b/api-gateway/create_api_dev.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# 使用するAPI名 +API_NAME="sales-tool-api" + +# プロジェクトを設定 +gcloud auth application-default set-quota-project dmiru-dev +gcloud config set project $PROJECT_ID + +# API Gatewayを作成 +gcloud api-gateway apis create sales-tool-api \ + + +echo "API Gateway '${API_NAME}' が作成され、有効化されました。" \ No newline at end of file diff --git a/api-gateway/deploy_dev.sh b/api-gateway/deploy_dev.sh new file mode 100755 index 0000000..b7ad6a2 --- /dev/null +++ b/api-gateway/deploy_dev.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# 環境変数 +API_NAME="sales-tool-api" +API_CONFIG_NAME="sales-tool-gw-dev-conf-20250619" +GATEWAY_NAME="sales-tool-gw-dev" +OPENAPI_SPEC="openapi_dev.yaml" +PROJECT_ID="datacom-poc" +SERVICE_ACCOUNT="api-gateway-mpos@datacom-poc.iam.gserviceaccount.com" +LOCATION="asia-northeast1" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# API GatewayのAPI Configを作成 +echo "Creating API Config..." +gcloud api-gateway api-configs create $API_CONFIG_NAME \ + --api=$API_NAME \ + --openapi-spec=$OPENAPI_SPEC \ + --project=$PROJECT_ID \ + --backend-auth-service-account=$SERVICE_ACCOUNT + +# API GatewayのGatewayを作成 +echo "Creating Gateway..." +gcloud api-gateway gateways create $GATEWAY_NAME \ + --api=$API_NAME \ + --api-config=$API_CONFIG_NAME \ + --location=$LOCATION \ + --project=$PROJECT_ID + +echo "API Gateway deployment completed successfully." \ No newline at end of file diff --git a/api-gateway/openapi.yaml b/api-gateway/openapi.yaml new file mode 100755 index 0000000..c056fc8 --- /dev/null +++ b/api-gateway/openapi.yaml @@ -0,0 +1,77 @@ +swagger: '2.0' +info: + title: crate-minutes-api + description: 'Meeting Minutes Generator Web-API' + version: '1.0.0' +schemes: + - 'https' +host: 'crate-minutes-gw-a8slsa47.an.gateway.dev' +x-google-endpoints: + - name: 'crate-minutes-gw-a8slsa47.an.gateway.dev' + allowCors: True +paths: + /create-minutes: + post: + description: '商談レポート作成' + operationId: 'create-minutes' + x-google-backend: + address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/create-minutes + path_translation: CONSTANT_ADDRESS + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + description: JSON payload + required: false + schema: + type: object + additionalProperties: true + responses: + 200: + description: 'OK' + schema: + type: object + additionalProperties: true + 401: + description: 'Auth Error' + schema: + type: object + properties: + error: + type: string + 500: + description: 'Error' + schema: + type: object + properties: + error: + type: string + security: + - APIKeyHeader: [] + options: + summary: 'CORS support' + operationId: 'create-minutes-options' + x-google-backend: + address: https://asia-northeast1-rational-timing-443808-u0.cloudfunctions.net/create-minutes + path_translation: CONSTANT_ADDRESS + responses: + 204: + description: 'CORS preflight' + headers: + Access-Control-Allow-Origin: + type: string + default: '*' + Access-Control-Allow-Methods: + type: string + default: 'GET, POST, OPTIONS' + Access-Control-Allow-Headers: + type: string + default: 'Content-Type, x-api-key' +securityDefinitions: + APIKeyHeader: + type: apiKey + name: x-api-key + in: header diff --git a/api-gateway/openapi_dev.yaml b/api-gateway/openapi_dev.yaml new file mode 100755 index 0000000..ad2a432 --- /dev/null +++ b/api-gateway/openapi_dev.yaml @@ -0,0 +1,73 @@ +swagger: '2.0' +info: + title: crate-minutes-api + description: 'Meeting Minutes Generator Web-API' + version: '1.0.0' +schemes: + - 'https' +paths: + /trigger-minutes-workflow-from-miitel: + post: + description: 'ワークフロー呼び出し処理' + operationId: 'trigger-minutes-workflow-from-miitel' + x-google-backend: + address: https://asia-northeast1-datacom-poc.cloudfunctions.net/mrt-trigger-minutes-workflow-from-miitel + path_translation: CONSTANT_ADDRESS + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + description: JSON payload + required: false + schema: + type: object + additionalProperties: true + responses: + 200: + description: 'OK' + schema: + type: object + additionalProperties: true + 401: + description: 'Auth Error' + schema: + type: object + properties: + error: + type: string + 500: + description: 'Error' + schema: + type: object + properties: + error: + type: string + security: + - APIKeyHeader: [] + options: + summary: 'CORS support' + operationId: 'trigger-minutes-workflow-from-miitel-options' + x-google-backend: + address: https://asia-northeast1-datacom-poc.cloudfunctions.net/mrttrigger-minutes-workflow-from-miitel + path_translation: CONSTANT_ADDRESS + responses: + 204: + description: 'CORS preflight' + headers: + Access-Control-Allow-Origin: + type: string + default: '*' + Access-Control-Allow-Methods: + type: string + default: 'GET, POST, OPTIONS' + Access-Control-Allow-Headers: + type: string + default: 'Content-Type, x-api-key' +securityDefinitions: + APIKeyHeader: + type: apiKey + name: x-api-key + in: header diff --git a/cloudbuild_dev.yaml b/cloudbuild_dev.yaml new file mode 100755 index 0000000..757cb07 --- /dev/null +++ b/cloudbuild_dev.yaml @@ -0,0 +1,196 @@ +substitutions: + _ENV: 'dev' + _CF_SERVICE_ACCOUNT: 'mrt-cloudfunctions-sa-devtest' + _CW_SERVICE_ACCOUNT: 'mrt-cloudworkflows-sa-devtest' + +options: + logging: CLOUD_LOGGING_ONLY + +steps: + # 会社一覧取得 + - id: 'gcloud functions deploy mrt-export-companies-to-gcs' + name: gcr.io/cloud-builders/gcloud + dir: 'functions/export-companies-to-gcs' + args: [ + 'functions', + 'deploy', + 'mrt-export-companies-to-gcs', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # 担当者一覧取得 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/export-owners-to-gcs' + args: [ + 'functions', + 'deploy', + 'mrt-export-owners-to-gcs', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # スプレッドシート作成 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/create-log-sheet' + args: [ + 'functions', + 'deploy', + 'mrt-create-log-sheet', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # ワークフロー呼び出し関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/trigger-minutes-workflow-from-miitel' + args: [ + 'functions', + 'deploy', + 'mrt-trigger-minutes-workflow-from-miitel', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # 議事録作成関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/generate-meeting-minutes' + args: [ + 'functions', + 'deploy', + 'mrt-generate-meeting-minutes', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--cpu=0.5', + '--memory=1Gi', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--timeout=10m', + '--quiet', + ] + waitFor: ['-'] + + # 議事録をドライブへアップロードする関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/upload-minutes-to-drive' + args: [ + 'functions', + 'deploy', + 'mrt-upload-minutes-to-drive', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--cpu=0.5', + '--memory=1Gi', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # Hubspot連携関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/create-hubspot-meeting-log' + args: [ + 'functions', + 'deploy', + 'mrt-create-hubspot-meeting-log', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # スプレッドシートへ記録 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/append-log-to-sheet' + args: [ + 'functions', + 'deploy', + 'mrt-append-log-to-sheet', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_dev', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # ワークフロー + - name: gcr.io/cloud-builders/gcloud + dir: 'workflows/workflow-create-minutes' + args: + [ + 'workflows', + 'deploy', + 'mrt-workflow-create-minutes', + '--location=asia-northeast1', + '--source=main.yaml', + '--service-account=$_CW_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--quiet', + ] diff --git a/cloudbuild_prod.yaml b/cloudbuild_prod.yaml new file mode 100755 index 0000000..87f56d4 --- /dev/null +++ b/cloudbuild_prod.yaml @@ -0,0 +1,193 @@ +substitutions: + _ENV: 'prod' + _CF_SERVICE_ACCOUNT: 'mrt-cloudfunctions-sa' + _CW_SERVICE_ACCOUNT: 'mrt-cloudworkflows-sa' + +steps: + # 会社一覧取得 + - id: 'gcloud functions deploy mrt-export-companies-to-gcs' + name: gcr.io/cloud-builders/gcloud + dir: 'functions/export-companies-to-gcs' + args: [ + 'functions', + 'deploy', + 'mrt-export-companies-to-gcs', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # 担当者一覧取得 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/export-owners-to-gcs' + args: [ + 'functions', + 'deploy', + 'mrt-export-owners-to-gcs', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # スプレッドシート作成 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/create-log-sheet' + args: [ + 'functions', + 'deploy', + 'mrt-create-log-sheet', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # ワークフロー呼び出し関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/trigger-minutes-workflow-from-miitel' + args: [ + 'functions', + 'deploy', + 'mrt-trigger-minutes-workflow-from-miitel', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # 議事録作成関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/generate-meeting-minutes' + args: [ + 'functions', + 'deploy', + 'mrt-generate-meeting-minutes', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--cpu=0.5', + '--memory=1Gi', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--timeout=10m', + '--quiet', + ] + waitFor: ['-'] + + # 議事録をドライブへアップロードする関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/upload-minutes-to-drive' + args: [ + 'functions', + 'deploy', + 'mrt-upload-minutes-to-drive', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--cpu=0.5', + '--memory=1Gi', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # Hubspot連携関数 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/create-hubspot-meeting-log' + args: [ + 'functions', + 'deploy', + 'mrt-create-hubspot-meeting-log', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # スプレッドシートへ記録 + - name: gcr.io/cloud-builders/gcloud + dir: 'functions/append-log-to-sheet' + args: [ + 'functions', + 'deploy', + 'mrt-append-log-to-sheet', + '--gen2', + '--runtime=python312', + '--region=asia-northeast1', + '--source=./source', # dir で切り替えているので「.」 + '--entry-point=handle_request', # 変更する場合はここ + '--trigger-http', + '--no-allow-unauthenticated', + '--service-account=$_CF_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--env-vars-file=.env_prod', + '--project=$PROJECT_ID', + '--quiet', + ] + waitFor: ['-'] + + # ワークフロー + - name: gcr.io/cloud-builders/gcloud + dir: 'workflows/workflow-create-minutes' + args: + [ + 'workflows', + 'deploy', + 'mrt-workflow-create-minutes', + '--location=asia-northeast1', + '--source=main.yaml', + '--service-account=$_CW_SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com', + '--quiet', + ] diff --git a/functions/append-log-to-sheet/.env_debug b/functions/append-log-to-sheet/.env_debug new file mode 100755 index 0000000..a672143 --- /dev/null +++ b/functions/append-log-to-sheet/.env_debug @@ -0,0 +1,5 @@ +KEY_PATH=projects/32472615575/secrets/sa-access-google-drive-key +LOG_FOLDER_ID=1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX +MEETING_FOLDER_ID=1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw +HUBSPOT_COMPANY_URL=https://app-na2.hubspot.com/contacts/242960467/record/0-2 +MODE=dev diff --git a/functions/append-log-to-sheet/.env_dev b/functions/append-log-to-sheet/.env_dev new file mode 100755 index 0000000..b9a7bd3 --- /dev/null +++ b/functions/append-log-to-sheet/.env_dev @@ -0,0 +1,5 @@ +KEY_PATH: projects/32472615575/secrets/sa-access-google-drive-key +LOG_FOLDER_ID: 1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX +MEETING_FOLDER_ID: 1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw +HUBSPOT_COMPANY_URL: https://app-na2.hubspot.com/contacts/242960467/record/0-2 +MODE: dev diff --git a/functions/append-log-to-sheet/.env_prod b/functions/append-log-to-sheet/.env_prod new file mode 100755 index 0000000..d7650d0 --- /dev/null +++ b/functions/append-log-to-sheet/.env_prod @@ -0,0 +1,5 @@ +KEY_PATH: projects/570987459910/secrets/sa-create-minutes-key +LOG_FOLDER_ID: 1arL6AxpvA7N6Umg4wdrdAcRWBdKc-Jfb +MEETING_FOLDER_ID: 0AGT_1dSq66qYUk9PVA +HUBSPOT_COMPANY_URL: https://app.hubspot.com/contacts/22400567/record/0-2 +MODE: production diff --git a/functions/append-log-to-sheet/_scripts/deploy_dev.sh b/functions/append-log-to-sheet/_scripts/deploy_dev.sh new file mode 100755 index 0000000..deee31d --- /dev/null +++ b/functions/append-log-to-sheet/_scripts/deploy_dev.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# デプロイする関数名 +FUNCTION_NAME="mrt-append-log-to-sheet" + +# 関数のエントリポイント +ENTRY_POINT="handle_request" + +# ランタイム +RUNTIME="python312" + +# リージョン +REGION="asia-northeast1" + +# 環境変数ファイル +ENV_VARS_FILE=".env_dev" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# デプロイコマンド +gcloud functions deploy $FUNCTION_NAME \ + --gen2 \ + --region $REGION \ + --runtime $RUNTIME \ + --source=./source \ + --trigger-http \ + --no-allow-unauthenticated \ + --entry-point $ENTRY_POINT \ + --env-vars-file $ENV_VARS_FILE \ No newline at end of file diff --git a/functions/append-log-to-sheet/source/main.py b/functions/append-log-to-sheet/source/main.py new file mode 100755 index 0000000..33c5599 --- /dev/null +++ b/functions/append-log-to-sheet/source/main.py @@ -0,0 +1,267 @@ +import functions_framework +from google.cloud import secretmanager +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +import json +import os +from datetime import datetime, timezone, timedelta + + +sm_client = secretmanager.SecretManagerServiceClient() + + +SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"] +HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"] + +@functions_framework.http +def handle_request(request): + # POSTリクエストの処理 + if request.method != 'POST': + return ('', 405, {'Allow': 'POST', 'Content-Type': 'application/json'}) # メソッドがPOSTでない場合は405エラーを返す + + """Shows basic usage of the Drive Activity API. + + Prints information about the last 10 events that occured the user's Drive. + """ + try: + request_json = request.get_json() + print(request_json) + title = request_json['title'] # 会議タイトル + document_id = request_json['document_id'] # 議事録ファイルのID + matched_company_id = request_json['matched_company_id'] # マッチした会社ID + matched_company_name = request_json['matched_company_name'] # マッチした会社名 + host_name = request_json['host_name'] # ホストユーザー名 + video_url = request_json['video_url'] # 会議履歴URL + starts_at = request_json['starts_at'] # 開始日時 + + log_folder_id = os.getenv("LOG_FOLDER_ID") # 共有ドライブID + meeting_folder_id = os.getenv("MEETING_FOLDER_ID") # ミーティングフォルダID + hubspot_company_url = os.getenv("HUBSPOT_COMPANY_URL") # HubSpotの会社情報URL + mode = os.getenv("MODE") # モード(devまたはprod) + + service_account_info = get_service_account_info() + # 認証 + credentials = get_credentials(service_account_info) + + # APIクライアントの構築 + drive_service = build("drive", "v3", credentials=credentials) + sheet_service = build("sheets", "v4", credentials=credentials) + + + # 現在日時をJSTに変換 + jst_now = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=9))) + # JSTの現在日時を文字列に変換 + ym_str = jst_now.strftime("%Y%m") + y_str = jst_now.strftime("%Y") + + + # 年別のフォルダを検索 + target_folder = get_directory_files_dev(drive_service, log_folder_id, y_str) if mode == "dev" else get_directory_files_prod(drive_service, meeting_folder_id, log_folder_id, y_str) + print("target_folder", target_folder) + + year_folder_id = None + if not target_folder: + # フォルダが存在しない場合は新規作成 + year_folder_id = create_new_folder(drive_service, log_folder_id, y_str) + else: + # フォルダが存在する場合はそのIDを使用 + year_folder_id = target_folder[0]['id'] + print("年別のフォルダID:", year_folder_id) + + # スプレッドシートを検索 + target_files = get_directory_files_dev(drive_service, year_folder_id, ym_str) if mode == "dev" else get_directory_files_prod(drive_service, meeting_folder_id, year_folder_id, ym_str) + print("スプレッドシート", target_files) + + if not target_files: + print('not found') + + # スプレッドシートを作成 + spreadsheet_id = create_new_spreadsheet(drive_service, year_folder_id, ym_str) + print("スプレッドシートID:", spreadsheet_id) + # 注意事項追加 + append_log_to_sheet(sheet_service, spreadsheet_id, ["※シート名変更厳禁"]) + # ヘッダーを追加 + append_log_to_sheet(sheet_service, spreadsheet_id, HEADER_VALUES) + + else: + print('found') + # ファイルIDを取得 + spreadsheet_id = target_files[0]['id'] + + documnet_url = f"https://docs.google.com/document/d/{document_id}/edit" if document_id else "" + hubspot_url = f"{hubspot_company_url}/{matched_company_id}" if matched_company_id else "" + # テストログを追加 + row_data = [jst_now.strftime("%Y-%m-%d %H:%M:%S"), + convert_to_jst_ymd(starts_at), + title, + matched_company_name, + host_name, + video_url, + documnet_url, + hubspot_url + ] + append_log_to_sheet(sheet_service, spreadsheet_id, row_data) + print("ログを追加しました:", row_data) + + return (json.dumps({"status": "success"}, ensure_ascii=False), 200, {"Content-Type": "application/json"}) + + except HttpError as error: + # TODO(developer) - Handleerrors from drive activity API. + print(f"An error occurred: {error}") + + +# +# SecretManagerから秘密鍵を取得 +# +def get_service_account_info(): + key_path = os.getenv('KEY_PATH') + "/versions/1" + # 秘密鍵取得 + response = sm_client.access_secret_version(name=key_path) + # 秘密鍵の値をデコード + secret_key = response.payload.data.decode("UTF-8") + return json.loads(secret_key) + +# Google Drive認証 +def get_credentials(service_account_info): + credentials = service_account.Credentials.from_service_account_info( + service_account_info, + scopes=SCOPES + ) + return credentials + + +# 開発用マイドライブからのファイルを取得 +def get_directory_files_dev(service,shared_folder_id, filename): + """ + 対象のディレクトリ配下からファイル名で検索した結果を配列で返す + :param filename: ファイル名 + :param directory_id: ディレクトリID + :param pages_max: 最大ページ探索数 + :return: ファイルリスト + """ + items = [] + page = 0 + pages_max = 10 # 最大ページ数 + while True: + page += 1 + if page == pages_max: + break + results = service.files().list( + corpora="user", + includeItemsFromAllDrives=True, + includeTeamDriveItems=True, + q=f"'{shared_folder_id}' in parents and name = '{filename}' and trashed = false", + supportsAllDrives=True, + pageSize=10, + fields="nextPageToken, files(id, name)").execute() + items += results.get("files", []) + + page_token = results.get('nextPageToken', None) + if page_token is None: + break + return items + +# 本番用共有ドライブからのファイルを取得 +def get_directory_files_prod(service,shared_folder_id,sub_folder_id,filename): + """ + 対象のディレクトリ配下からファイル名で検索した結果を配列で返す + :param filename: ファイル名 + :param directory_id: ディレクトリID + :param pages_max: 最大ページ探索数 + :return: ファイルリスト + """ + items = [] + page = 0 + pages_max = 10 # 最大ページ数 + while True: + page += 1 + if page == pages_max: + break + results = service.files().list( + corpora="drive", + driveId=shared_folder_id, + includeItemsFromAllDrives=True, + includeTeamDriveItems=True, + q=f"'{sub_folder_id}' in parents and name = '{filename}' and trashed = false", + supportsAllDrives=True, + pageSize=10, + fields="nextPageToken, files(id, name, parents)").execute() + items += results.get("files", []) + + page_token = results.get('nextPageToken', None) + if page_token is None: + break + return items + +def create_new_folder(service, sub_folder_id, title): + """ + Google Drive APIを使用して新しいフォルダを作成する + :param service: Google Drive APIのサービスオブジェクト + :param title: フォルダのタイトル + :return: 作成したフォルダのID + """ + file_metadata = { + "name": title, + "parents": [sub_folder_id], # 共有ドライブのIDを指定 + "mimeType": "application/vnd.google-apps.folder", + } + + result = service.files().create(body=file_metadata, fields="id", supportsAllDrives=True).execute() + return result.get('id') + + +def create_new_spreadsheet(service,folder_id,title): + """ + Google Sheets APIを使用して新しいスプレッドシートを作成する + :param service: Google Sheets APIのサービスオブジェクト + :param title: スプレッドシートのタイトル + :return: 作成したスプレッドシートのID + """ + file_metadata = { + 'name': title, + 'parents': [folder_id], # 作成したフォルダのIDを指定 + 'mimeType': 'application/vnd.google-apps.spreadsheet', + } + result = ( + service.files() + .create(body=file_metadata, fields="id", supportsAllDrives=True) + .execute() + ) + return result.get("id") + + +def append_log_to_sheet(service, spreadsheet_id, row_data): + """ + Google Sheets APIを使用してスプレッドシートにログを追加する + :param service: Google Sheets APIのサービスオブジェクト + :param spreadsheet_id: スプレッドシートのID + :param row_data: 追加するログデータ(リスト形式) + """ + body = { + 'values': [row_data] + } + + # スプレッドシートにログを追加 + result = service.spreadsheets().values().append( + spreadsheetId=spreadsheet_id, + range='Sheet1', + valueInputOption="USER_ENTERED", + insertDataOption='INSERT_ROWS', + body=body, + ).execute() + print(f"{result.get('updates').get('updatedCells')} cells appended.") + + + + +def convert_to_jst_ymd(starts_at): + """ + 開始日時をYYYY年MM月DD日形式に変換する + :param starts_at: 開始日時の文字列 + :return: YYYY年MM月DD日形式の文字列 + """ + # 開始日時をUTCからJSTに変換 + dt = datetime.fromisoformat(starts_at.replace("Z", "+00:00")).astimezone(timezone(timedelta(hours=9))) + # YYYY年MM月DD日形式に変換 + return dt.strftime("%Y年%m月%d日") \ No newline at end of file diff --git a/functions/append-log-to-sheet/source/requirements.txt b/functions/append-log-to-sheet/source/requirements.txt new file mode 100755 index 0000000..e809a11 --- /dev/null +++ b/functions/append-log-to-sheet/source/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +google-cloud-secret-manager +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib \ No newline at end of file diff --git a/functions/create-hubspot-meeting-log/.env_debug b/functions/create-hubspot-meeting-log/.env_debug new file mode 100755 index 0000000..2cd13fd --- /dev/null +++ b/functions/create-hubspot-meeting-log/.env_debug @@ -0,0 +1,5 @@ +PROJECT_ID=datacom-poc +LOCATION=asia-northeast1 +BUCKET=meeting-report-data +KEY_PATH=projects/32472615575/secrets/mrt-hubspot-accesstoken +MODE=dev \ No newline at end of file diff --git a/functions/create-hubspot-meeting-log/.env_dev b/functions/create-hubspot-meeting-log/.env_dev new file mode 100755 index 0000000..eb8efeb --- /dev/null +++ b/functions/create-hubspot-meeting-log/.env_dev @@ -0,0 +1,5 @@ +PROJECT_ID: datacom-poc +LOCATION: asia-northeast1 +BUCKET: meeting-report-data +KEY_PATH: projects/32472615575/secrets/mrt-hubspot-accesstoken +MODE: dev \ No newline at end of file diff --git a/functions/create-hubspot-meeting-log/.env_prod b/functions/create-hubspot-meeting-log/.env_prod new file mode 100755 index 0000000..e23fb91 --- /dev/null +++ b/functions/create-hubspot-meeting-log/.env_prod @@ -0,0 +1,5 @@ +PROJECT_ID: rational-timing-443808-u0 +LOCATION: asia-northeast1 +BUCKET: meeting-data +KEY_PATH: projects/570987459910/secrets/mrt-hubspot-accesstoken +MODE: prod \ No newline at end of file diff --git a/functions/create-hubspot-meeting-log/_scripts/deploy_dev.sh b/functions/create-hubspot-meeting-log/_scripts/deploy_dev.sh new file mode 100755 index 0000000..02e2e95 --- /dev/null +++ b/functions/create-hubspot-meeting-log/_scripts/deploy_dev.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# デプロイする関数名 +FUNCTION_NAME="mrt-create-hubspot-meeting-log" + +# 関数のエントリポイント +ENTRY_POINT="handle_request" + +# ランタイム +RUNTIME="python312" + +# リージョン +REGION="asia-northeast1" + +# 環境変数ファイル +ENV_VARS_FILE=".env_dev" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# デプロイコマンド +gcloud functions deploy $FUNCTION_NAME \ + --gen2 \ + --region $REGION \ + --runtime $RUNTIME \ + --source=./source \ + --trigger-http \ + --no-allow-unauthenticated \ + --entry-point $ENTRY_POINT \ + --env-vars-file $ENV_VARS_FILE \ No newline at end of file diff --git a/functions/create-hubspot-meeting-log/source/main.py b/functions/create-hubspot-meeting-log/source/main.py new file mode 100755 index 0000000..a6b204a --- /dev/null +++ b/functions/create-hubspot-meeting-log/source/main.py @@ -0,0 +1,200 @@ +import functions_framework +from google.cloud import storage, secretmanager +import os +import hubspot +from hubspot.crm.objects.meetings import SimplePublicObjectInputForCreate, ApiException +import requests +import csv +import io +import re +import jaconv +from rapidfuzz import process, fuzz +import json + +CUTOFF = 80 # Fuzzy 閾値 (0-100) +LEGAL_SUFFIX = r'(株式会社|(株)|\(株\)|有限会社|合同会社|Inc\.?|Corp\.?|Co\.?Ltd\.?)' + +cs_client = storage.Client(project=os.getenv("PROJECT_ID")) +sm_client = secretmanager.SecretManagerServiceClient() + +@functions_framework.http +def handle_request(request): + try: + request_json = request.get_json() + print(request_json) + + mode = os.getenv("MODE") # モード(devまたはprod) + title = request_json['title'] + host_id = request_json['host_id'] if mode == 'prod' else 'ksuenaga@datacom.jp' # ホストユーザーID(開発環境では固定値を使用) + starts_at = request_json['starts_at'] + ends_at = request_json['ends_at'] + minutes = request_json['minutes'] + + # タイトルから【】を削除 + title = title.replace("【", "").replace("】", "") + # タイトルから企業名を抽出 + company_name = title.split("様")[0].strip() # "様" で分割して企業名を取得 + print("抽出した企業名:", company_name) + + # 会社名から会社IDを取得 + matched_company_id, matched_company_name = search_company(company_name) + + # マッチしたときだけ処理を行う + if matched_company_id: + # ユーザーIDを取得 + by_email = load_owners() + user_id = None + if host_id in by_email: + user_id = by_email[host_id]['id'] + print("取得したユーザーID:", user_id) + + # 改行コードを
タグに変換 + minutes_html = minutes.replace("\n", "
") + # ミーティングログを作成 + create_meeting_log(matched_company_id, title, user_id, starts_at, ends_at, minutes_html) + + + response_data = { + "matched_company_id": matched_company_id, # マッチした会社ID + "matched_company_name": matched_company_name, # マッチした会社名 + } + return (json.dumps(response_data, ensure_ascii=False), 200, {"Content-Type": "application/json"}) + except ApiException as e: + print("Exception when calling basic_api->create: %s\n" % e) + + +def normalize(name: str) -> str: + """表記ゆれ吸収用の正規化""" + n = jaconv.z2h(name, kana=False, digit=True, ascii=True).lower() + n = re.sub(LEGAL_SUFFIX, '', n) + return re.sub(r'[\s\-・・,,、\.]', '', n) + + +# GCSから会社一覧取得 +def load_componies(): + """ + 毎回 Cloud Storage から CSV を読み込む。 + *応答速度を気にしない* 前提なのでキャッシュしなくても OK。 + """ + + blob = cs_client.bucket(os.getenv("BUCKET")).blob('master/mst_company.csv') + raw = blob.download_as_bytes() # bytes + + recs, by_norm = [], {} + with io.StringIO(raw.decode("utf-8")) as f: + reader = csv.DictReader(f) + for row in reader: + row["norm_name"] = normalize(row["company_name"]) + recs.append(row) + by_norm[row["norm_name"]] = row # 完全一致用ハッシュ + + return recs, by_norm # (list[dict], dict) + + +# GCSから担当者一覧取得 +def load_owners(): + """ + GCS から担当者一覧 CSV を読み込み、 + email -> row 辞書 のマッピングを返す + """ + + blob = cs_client.bucket(os.getenv("BUCKET")).blob('master/mst_owner.csv') + raw = blob.download_as_bytes() # bytes + + by_email = {} + with io.StringIO(raw.decode("utf-8")) as f: + reader = csv.DictReader(f) + for row in reader: + # row に "email" と "user_id" フィールドがある前提 + email = row["email"].strip().lower() + by_email[email] = row + + return by_email + + + +def fuzzy_candidates(norm: str, recs): + """ + norm : 正規化済み検索語 + recs : 会社レコード list[dict] (norm_name 含む) + 戻り値 : list[(score:int, idx:int)] + """ + top = 2 # 上位 2 件を取得 + matches = process.extract( + norm, + [r["norm_name"] for r in recs], + scorer=fuzz.WRatio, + score_cutoff=CUTOFF, + limit=top + ) + print("ファジーマッチ結果:", matches) + if len(matches) == 0: + return None # マッチなしの場合は None を返す + elif len(matches) == 1: + return recs[matches[0][2]] # 上位 1 件のみの場合はそのレコードを返す + else: + if(matches[0][1] == matches[1][1]): + return None # 上位 2 件のスコアが同じ場合は None を返す + return recs[matches[0][2]] # 上位 1 件のみの場合はそのレコードを返す + + +def search_company(company_name): + # -------------------- マスタ読み込み -------------------- + recs, by_norm = load_componies() + norm_company_name = normalize(company_name) + print("正規化した企業名:", norm_company_name) + + matched_company_id = None + matched_company_name = None + # -------------------- 完全一致 -------------------- + if norm_company_name in by_norm: + matched_company_id = by_norm[norm_company_name]["company_id"] + matched_company_name = by_norm[norm_company_name]["company_name"] + + # -------------------- ファジーマッチ複数 -------------------- + else : + result = fuzzy_candidates(norm_company_name, recs) + if result: + matched_company_id = result["company_id"] + matched_company_name = result["company_name"] + + print("マッチした会社ID:", matched_company_id) + print("マッチした会社名:", matched_company_name) + return matched_company_id, matched_company_name + + +def create_meeting_log(company_id ,title, user_id, starts_at, ends_at, minutes): + """ + HubSpot API を使ってミーティングログを作成する。 + """ + access_key = get_access_key() # Secret Manager からアクセストークンを取得 + hs_client = hubspot.Client.create(access_token=access_key) + + properties = { + "hs_timestamp": starts_at, + "hs_meeting_title": title, + "hubspot_owner_id": user_id, + "hs_meeting_body": minutes, + "hs_meeting_start_time": starts_at, + "hs_meeting_end_time": ends_at, + + } + + simple_public_object_input_for_create = SimplePublicObjectInputForCreate( + associations=[{"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":188}],"to":{"id":company_id}}], + properties=properties + ) + + api_response = hs_client.crm.objects.meetings.basic_api.create(simple_public_object_input_for_create=simple_public_object_input_for_create) + print(api_response) + +# +# SecretManagerからアクセストークンを取得 +# +def get_access_key(): + key_path = os.getenv('KEY_PATH') + "/versions/1" + # アクセストークン取得 + response = sm_client.access_secret_version(name=key_path) + # アクセストークンをデコード + access_token = response.payload.data.decode("UTF-8") + return access_token diff --git a/functions/create-hubspot-meeting-log/source/requirements.txt b/functions/create-hubspot-meeting-log/source/requirements.txt new file mode 100755 index 0000000..db94e68 --- /dev/null +++ b/functions/create-hubspot-meeting-log/source/requirements.txt @@ -0,0 +1,8 @@ +functions-framework==3.* +Flask +google-cloud-storage +google-cloud-workflows +google-cloud-secret-manager +hubspot-api-client +rapidfuzz +jaconv \ No newline at end of file diff --git a/functions/create-log-sheet/.env_debug b/functions/create-log-sheet/.env_debug new file mode 100755 index 0000000..a672143 --- /dev/null +++ b/functions/create-log-sheet/.env_debug @@ -0,0 +1,5 @@ +KEY_PATH=projects/32472615575/secrets/sa-access-google-drive-key +LOG_FOLDER_ID=1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX +MEETING_FOLDER_ID=1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw +HUBSPOT_COMPANY_URL=https://app-na2.hubspot.com/contacts/242960467/record/0-2 +MODE=dev diff --git a/functions/create-log-sheet/.env_dev b/functions/create-log-sheet/.env_dev new file mode 100755 index 0000000..b9a7bd3 --- /dev/null +++ b/functions/create-log-sheet/.env_dev @@ -0,0 +1,5 @@ +KEY_PATH: projects/32472615575/secrets/sa-access-google-drive-key +LOG_FOLDER_ID: 1IZToaM9K9OJXrgV05aLO5k2ZCXpdlJzX +MEETING_FOLDER_ID: 1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw +HUBSPOT_COMPANY_URL: https://app-na2.hubspot.com/contacts/242960467/record/0-2 +MODE: dev diff --git a/functions/create-log-sheet/.env_prod b/functions/create-log-sheet/.env_prod new file mode 100755 index 0000000..d7650d0 --- /dev/null +++ b/functions/create-log-sheet/.env_prod @@ -0,0 +1,5 @@ +KEY_PATH: projects/570987459910/secrets/sa-create-minutes-key +LOG_FOLDER_ID: 1arL6AxpvA7N6Umg4wdrdAcRWBdKc-Jfb +MEETING_FOLDER_ID: 0AGT_1dSq66qYUk9PVA +HUBSPOT_COMPANY_URL: https://app.hubspot.com/contacts/22400567/record/0-2 +MODE: production diff --git a/functions/create-log-sheet/_scripts/deploy_dev.sh b/functions/create-log-sheet/_scripts/deploy_dev.sh new file mode 100755 index 0000000..d9c2d22 --- /dev/null +++ b/functions/create-log-sheet/_scripts/deploy_dev.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# デプロイする関数名 +FUNCTION_NAME="mrt-create-log-sheet" + +# 関数のエントリポイント +ENTRY_POINT="handle_request" + +# ランタイム +RUNTIME="python312" + +# リージョン +REGION="asia-northeast1" + +# 環境変数ファイル +ENV_VARS_FILE=".env_dev" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# デプロイコマンド +gcloud functions deploy $FUNCTION_NAME \ + --gen2 \ + --region $REGION \ + --runtime $RUNTIME \ + --source=./source \ + --trigger-http \ + --no-allow-unauthenticated \ + --entry-point $ENTRY_POINT \ + --env-vars-file $ENV_VARS_FILE \ No newline at end of file diff --git a/functions/create-log-sheet/source/main.py b/functions/create-log-sheet/source/main.py new file mode 100755 index 0000000..53b192e --- /dev/null +++ b/functions/create-log-sheet/source/main.py @@ -0,0 +1,218 @@ +import functions_framework +from google.cloud import secretmanager +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +import json +import os +from datetime import datetime, timezone, timedelta + + +sm_client = secretmanager.SecretManagerServiceClient() + + +SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"] +HEADER_VALUES = ["タイムスタンプ","商談日", "タイトル", "登録先企業","担当者", "ミーティングURL", "議事録URL", "HubSpot会社概要URL"] + +@functions_framework.http +def handle_request(request): + # POSTリクエストの処理 + if request.method != 'POST': + return ('', 405, {'Allow': 'POST', 'Content-Type': 'application/json'}) # メソッドがPOSTでない場合は405エラーを返す + + """Shows basic usage of the Drive Activity API. + + Prints information about the last 10 events that occured the user's Drive. + """ + try: + log_folder_id = os.getenv("LOG_FOLDER_ID") # 共有ドライブID + meeting_folder_id = os.getenv("MEETING_FOLDER_ID") # ミーティングフォルダID + mode = os.getenv("MODE") # モード(devまたはprod) + + service_account_info = get_service_account_info() + # 認証 + credentials = get_credentials(service_account_info) + + # APIクライアントの構築 + drive_service = build("drive", "v3", credentials=credentials) + sheet_service = build("sheets", "v4", credentials=credentials) + + + # 現在日時をJSTに変換 + jst_now = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=9))) + # JSTの現在日時を文字列に変換 + ym_str = jst_now.strftime("%Y%m") + y_str = jst_now.strftime("%Y") + + + # 年別のフォルダを検索 + target_folder = get_directory_files_dev(drive_service, log_folder_id, y_str) if mode == "dev" else get_directory_files_prod(drive_service, meeting_folder_id, log_folder_id, y_str) + print("target_folder", target_folder) + + year_folder_id = None + if not target_folder: + # フォルダが存在しない場合は新規作成 + year_folder_id = create_new_folder(drive_service, log_folder_id, y_str) + else: + # フォルダが存在する場合はそのIDを使用 + year_folder_id = target_folder[0]['id'] + print("年別のフォルダID:", year_folder_id) + + # スプレッドシートを作成 + spreadsheet_id = create_new_spreadsheet(drive_service, year_folder_id, ym_str) + print("スプレッドシートID:", spreadsheet_id) + # 注意事項追加 + append_log_to_sheet(sheet_service, spreadsheet_id, ["※シート名変更厳禁"]) + # ヘッダーを追加 + append_log_to_sheet(sheet_service, spreadsheet_id, HEADER_VALUES) + + + + return (json.dumps({"status": "success"}, ensure_ascii=False), 200, {"Content-Type": "application/json"}) + + except HttpError as error: + # TODO(developer) - Handleerrors from drive activity API. + print(f"An error occurred: {error}") + + +# +# SecretManagerから秘密鍵を取得 +# +def get_service_account_info(): + key_path = os.getenv('KEY_PATH') + "/versions/1" + # 秘密鍵取得 + response = sm_client.access_secret_version(name=key_path) + # 秘密鍵の値をデコード + secret_key = response.payload.data.decode("UTF-8") + return json.loads(secret_key) + +# Google Drive認証 +def get_credentials(service_account_info): + credentials = service_account.Credentials.from_service_account_info( + service_account_info, + scopes=SCOPES + ) + return credentials + + +# 開発用マイドライブからのファイルを取得 +def get_directory_files_dev(service,shared_folder_id, filename): + """ + 対象のディレクトリ配下からファイル名で検索した結果を配列で返す + :param filename: ファイル名 + :param directory_id: ディレクトリID + :param pages_max: 最大ページ探索数 + :return: ファイルリスト + """ + items = [] + page = 0 + pages_max = 10 # 最大ページ数 + while True: + page += 1 + if page == pages_max: + break + results = service.files().list( + corpora="user", + includeItemsFromAllDrives=True, + includeTeamDriveItems=True, + q=f"'{shared_folder_id}' in parents and name = '{filename}' and trashed = false", + supportsAllDrives=True, + pageSize=10, + fields="nextPageToken, files(id, name)").execute() + items += results.get("files", []) + + page_token = results.get('nextPageToken', None) + if page_token is None: + break + return items + +# 本番用共有ドライブからのファイルを取得 +def get_directory_files_prod(service,shared_folder_id,sub_folder_id,filename): + """ + 対象のディレクトリ配下からファイル名で検索した結果を配列で返す + :param filename: ファイル名 + :param directory_id: ディレクトリID + :param pages_max: 最大ページ探索数 + :return: ファイルリスト + """ + items = [] + page = 0 + pages_max = 10 # 最大ページ数 + while True: + page += 1 + if page == pages_max: + break + results = service.files().list( + corpora="drive", + driveId=shared_folder_id, + includeItemsFromAllDrives=True, + includeTeamDriveItems=True, + q=f"'{sub_folder_id}' in parents and name = '{filename}' and trashed = false", + supportsAllDrives=True, + pageSize=10, + fields="nextPageToken, files(id, name, parents)").execute() + items += results.get("files", []) + + page_token = results.get('nextPageToken', None) + if page_token is None: + break + return items + +def create_new_folder(service, sub_folder_id, title): + """ + Google Drive APIを使用して新しいフォルダを作成する + :param service: Google Drive APIのサービスオブジェクト + :param title: フォルダのタイトル + :return: 作成したフォルダのID + """ + file_metadata = { + "name": title, + "parents": [sub_folder_id], # 共有ドライブのIDを指定 + "mimeType": "application/vnd.google-apps.folder", + } + + result = service.files().create(body=file_metadata, fields="id", supportsAllDrives=True).execute() + return result.get('id') + + +def create_new_spreadsheet(service,folder_id,title): + """ + Google Sheets APIを使用して新しいスプレッドシートを作成する + :param service: Google Sheets APIのサービスオブジェクト + :param title: スプレッドシートのタイトル + :return: 作成したスプレッドシートのID + """ + file_metadata = { + 'name': title, + 'parents': [folder_id], # 作成したフォルダのIDを指定 + 'mimeType': 'application/vnd.google-apps.spreadsheet', + } + result = ( + service.files() + .create(body=file_metadata, fields="id", supportsAllDrives=True) + .execute() + ) + return result.get("id") + + +def append_log_to_sheet(service, spreadsheet_id, row_data): + """ + Google Sheets APIを使用してスプレッドシートにログを追加する + :param service: Google Sheets APIのサービスオブジェクト + :param spreadsheet_id: スプレッドシートのID + :param row_data: 追加するログデータ(リスト形式) + """ + body = { + 'values': [row_data] + } + + # スプレッドシートにログを追加 + result = service.spreadsheets().values().append( + spreadsheetId=spreadsheet_id, + range='Sheet1', + valueInputOption="USER_ENTERED", + insertDataOption='INSERT_ROWS', + body=body, + ).execute() + print(f"{result.get('updates').get('updatedCells')} cells appended.") + diff --git a/functions/create-log-sheet/source/requirements.txt b/functions/create-log-sheet/source/requirements.txt new file mode 100755 index 0000000..e809a11 --- /dev/null +++ b/functions/create-log-sheet/source/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +google-cloud-secret-manager +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib \ No newline at end of file diff --git a/functions/export-companies-to-gcs/.env_debug b/functions/export-companies-to-gcs/.env_debug new file mode 100755 index 0000000..2da85c5 --- /dev/null +++ b/functions/export-companies-to-gcs/.env_debug @@ -0,0 +1,5 @@ +PROJECT_ID=datacom-poc +LOCATION=asia-northeast1 +BUCKET=meeting-report-data +OBJECT=master/mst_company.csv +KEY_PATH=projects/32472615575/secrets/mrt-hubspot-accesstoken \ No newline at end of file diff --git a/functions/export-companies-to-gcs/.env_dev b/functions/export-companies-to-gcs/.env_dev new file mode 100755 index 0000000..93c6a3e --- /dev/null +++ b/functions/export-companies-to-gcs/.env_dev @@ -0,0 +1,5 @@ +PROJECT_ID: datacom-poc +LOCATION: asia-northeast1 +BUCKET: meeting-report-data +OBJECT: master/mst_company.csv +KEY_PATH: projects/32472615575/secrets/mrt-hubspot-accesstoken diff --git a/functions/export-companies-to-gcs/.env_prod b/functions/export-companies-to-gcs/.env_prod new file mode 100755 index 0000000..0ad8e08 --- /dev/null +++ b/functions/export-companies-to-gcs/.env_prod @@ -0,0 +1,5 @@ +PROJECT_ID: rational-timing-443808-u0 +LOCATION: asia-northeast1 +BUCKET: meeting-data +OBJECT: master/mst_company.csv +KEY_PATH: projects/570987459910/secrets/mrt-hubspot-accesstoken diff --git a/functions/export-companies-to-gcs/source/main.py b/functions/export-companies-to-gcs/source/main.py new file mode 100755 index 0000000..cb434c1 --- /dev/null +++ b/functions/export-companies-to-gcs/source/main.py @@ -0,0 +1,87 @@ +import functions_framework +from google.cloud import storage, secretmanager +import os +import hubspot +from hubspot.crm.objects.meetings import ApiException +import csv +import io +import json + +cs_client = storage.Client() +sm_client = secretmanager.SecretManagerServiceClient() + +@functions_framework.http +def handle_request(request): + try: + # 会社一覧取得 + companies = fetch_all_companies() + # メモリ上で CSV を生成 + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer) + # ヘッダー行 + writer.writerow(["company_id", "company_name"]) + # 各行を書き込み + for row in companies: + company_id = row['properties']['hs_object_id'] + company_name = row['properties']['name'] + writer.writerow([company_id, company_name]) + + # Cloud Storage にアップロード + upload_to_gcs(csv_buffer) + return 'success', 200 + except ApiException as e: + print("Exception when calling basic_api->create: %s\n" % e) + return (json.dumps("", ensure_ascii=False), 500, {"Content-Type": "application/json"}) + +def fetch_all_companies(): + """ + Companies API の get_page をページネーション付きで呼び出し、 + 全オブジェクトをリストで返す。 + """ + access_key = get_access_key() # Secret Manager からアクセストークンを取得 + hs_client = hubspot.Client.create(access_token=access_key) + + all_companies = [] + after = None + limit = 100 # 1 回あたりの取得件数(最大 100) + + while True: + # get_page の基本呼び出し + response = hs_client.crm.companies.basic_api.get_page( + limit=limit, + archived=False, + after=after + ) + + # レスポンスから companies の配列を追加 + if response.results: + all_companies.extend([c.to_dict() for c in response.results]) + + # 次ページがない場合はループ終了 + paging = response.paging + if not paging or not paging.next or not paging.next.after: + break + + # next.after をセットして次ループへ + after = paging.next.after + + return all_companies + +def upload_to_gcs(data): + """ + メモリ上の CSV データを Cloud Storage にアップロード + """ + bucket = cs_client.bucket(os.getenv("BUCKET")) + blob = bucket.blob(os.getenv("OBJECT")) + blob.upload_from_string(data.getvalue(), content_type='text/csv') + +# +# SecretManagerからアクセストークンを取得 +# +def get_access_key(): + key_path = os.getenv('KEY_PATH') + "/versions/1" + # アクセストークン取得 + response = sm_client.access_secret_version(name=key_path) + # アクセストークンをデコード + access_token = response.payload.data.decode("UTF-8") + return access_token diff --git a/functions/export-companies-to-gcs/source/requirements.txt b/functions/export-companies-to-gcs/source/requirements.txt new file mode 100755 index 0000000..60c7aa0 --- /dev/null +++ b/functions/export-companies-to-gcs/source/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +Flask +google-cloud-storage +google-cloud-secret-manager +hubspot-api-client \ No newline at end of file diff --git a/functions/export-owners-to-gcs/.env_debug b/functions/export-owners-to-gcs/.env_debug new file mode 100755 index 0000000..d59e97c --- /dev/null +++ b/functions/export-owners-to-gcs/.env_debug @@ -0,0 +1,5 @@ +PROJECT_ID=datacom-poc +LOCATION=asia-northeast1 +BUCKET=meeting-report-data +OBJECT=master/mst_owner.csv +KEY_PATH=projects/32472615575/secrets/mrt-hubspot-accesstoken \ No newline at end of file diff --git a/functions/export-owners-to-gcs/.env_dev b/functions/export-owners-to-gcs/.env_dev new file mode 100755 index 0000000..624c47e --- /dev/null +++ b/functions/export-owners-to-gcs/.env_dev @@ -0,0 +1,5 @@ +PROJECT_ID: datacom-poc +LOCATION: asia-northeast1 +BUCKET: meeting-report-data +OBJECT: master/mst_owner.csv +KEY_PATH: projects/32472615575/secrets/mrt-hubspot-accesstoken \ No newline at end of file diff --git a/functions/export-owners-to-gcs/.env_prod b/functions/export-owners-to-gcs/.env_prod new file mode 100755 index 0000000..470780f --- /dev/null +++ b/functions/export-owners-to-gcs/.env_prod @@ -0,0 +1,5 @@ +PROJECT_ID: rational-timing-443808-u0 +LOCATION: asia-northeast1 +BUCKET: meeting-data +OBJECT: master/mst_owner.csv +KEY_PATH: projects/570987459910/secrets/mrt-hubspot-accesstoken diff --git a/functions/export-owners-to-gcs/source/main.py b/functions/export-owners-to-gcs/source/main.py new file mode 100755 index 0000000..9d822b5 --- /dev/null +++ b/functions/export-owners-to-gcs/source/main.py @@ -0,0 +1,90 @@ +import functions_framework +from google.cloud import storage, secretmanager +import os +import hubspot +from hubspot.crm.objects.meetings import ApiException +import csv +import io +import json + +cs_client = storage.Client() +sm_client = secretmanager.SecretManagerServiceClient() + + +@functions_framework.http +def handle_request(request): + try: + # 会社一覧取得 + owners = fetch_all_owners() + # メモリ上で CSV を生成 + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer) + # ヘッダー行 + writer.writerow(["id", "email"]) + # 各行を書き込み + for row in owners: + user_id = row['id'] + email = row['email'] + writer.writerow([user_id, email]) + + # Cloud Storage にアップロード + upload_to_gcs(csv_buffer) + return (json.dumps('', ensure_ascii=False), 200, {"Content-Type": "application/json"}) + except ApiException as e: + print("Exception when calling basic_api->create: %s\n" % e) + + + return (json.dumps("", ensure_ascii=False), 200, {"Content-Type": "application/json"}) + +def fetch_all_owners(): + """ + Companies API の get_page をページネーション付きで呼び出し、 + 全オブジェクトをリストで返す。 + """ + access_key = get_access_key() # Secret Manager からアクセストークンを取得 + hs_client = hubspot.Client.create(access_token=access_key) + + all_owners = [] + after = None + limit = 100 # 1 回あたりの取得件数(最大 100) + + while True: + # get_page の基本呼び出し + response = hs_client.crm.owners.owners_api.get_page( + limit=limit, + archived=False, + after=after + ) + + # レスポンスから companies の配列を追加 + if response.results: + all_owners.extend([c.to_dict() for c in response.results]) + + # 次ページがない場合はループ終了 + paging = response.paging + if not paging or not paging.next or not paging.next.after: + break + + # next.after をセットして次ループへ + after = paging.next.after + + return all_owners + +def upload_to_gcs(data): + """ + メモリ上の CSV データを Cloud Storage にアップロード + """ + bucket = cs_client.bucket(os.getenv("BUCKET")) + blob = bucket.blob(os.getenv("OBJECT")) + blob.upload_from_string(data.getvalue(), content_type='text/csv') + +# +# SecretManagerからアクセストークンを取得 +# +def get_access_key(): + key_path = os.getenv('KEY_PATH') + "/versions/1" + # アクセストークン取得 + response = sm_client.access_secret_version(name=key_path) + # アクセストークンをデコード + access_token = response.payload.data.decode("UTF-8") + return access_token diff --git a/functions/export-owners-to-gcs/source/requirements.txt b/functions/export-owners-to-gcs/source/requirements.txt new file mode 100755 index 0000000..60c7aa0 --- /dev/null +++ b/functions/export-owners-to-gcs/source/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +Flask +google-cloud-storage +google-cloud-secret-manager +hubspot-api-client \ No newline at end of file diff --git a/functions/generate-meeting-minutes/.env_debug b/functions/generate-meeting-minutes/.env_debug new file mode 100755 index 0000000..15e4399 --- /dev/null +++ b/functions/generate-meeting-minutes/.env_debug @@ -0,0 +1,3 @@ +MIITEL_URL=https://datacom.miitel.jp/ +PROJECT_ID=datacom-poc +MODEL_ID=gemini-2.5-flash diff --git a/functions/generate-meeting-minutes/.env_dev b/functions/generate-meeting-minutes/.env_dev new file mode 100755 index 0000000..0335b9f --- /dev/null +++ b/functions/generate-meeting-minutes/.env_dev @@ -0,0 +1,3 @@ +MIITEL_URL: https://datacom.miitel.jp/ +PROJECT_ID: datacom-poc +MODEL_ID: gemini-2.5-flash \ No newline at end of file diff --git a/functions/generate-meeting-minutes/.env_prod b/functions/generate-meeting-minutes/.env_prod new file mode 100755 index 0000000..88be3ae --- /dev/null +++ b/functions/generate-meeting-minutes/.env_prod @@ -0,0 +1,3 @@ +MIITEL_URL: https://datacom.miitel.jp/ +PROJECT_ID: rational-timing-443808-u0 +MODEL_ID: gemini-2.5-flash diff --git a/functions/generate-meeting-minutes/_scripts/deploy_dev.sh b/functions/generate-meeting-minutes/_scripts/deploy_dev.sh new file mode 100755 index 0000000..d91fb99 --- /dev/null +++ b/functions/generate-meeting-minutes/_scripts/deploy_dev.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# デプロイする関数名 +FUNCTION_NAME="mrt-generate-meeting-minutes" + +# 関数のエントリポイント +ENTRY_POINT="handle_request" + +# ランタイム +RUNTIME="python312" + +# リージョン +REGION="asia-northeast1" + +# 環境変数ファイル +ENV_VARS_FILE=".env_dev" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# デプロイコマンド +gcloud functions deploy $FUNCTION_NAME \ + --gen2 \ + --region $REGION \ + --runtime $RUNTIME \ + --source=./source \ + --trigger-http \ + --cpu=0.5 \ + --memory=1Gi \ + --no-allow-unauthenticated \ + --entry-point $ENTRY_POINT \ + --env-vars-file $ENV_VARS_FILE \ No newline at end of file diff --git a/functions/generate-meeting-minutes/source/main.py b/functions/generate-meeting-minutes/source/main.py new file mode 100755 index 0000000..5f88379 --- /dev/null +++ b/functions/generate-meeting-minutes/source/main.py @@ -0,0 +1,132 @@ +import functions_framework +import vertexai +from vertexai.generative_models import GenerativeModel, ChatSession +from google.cloud import storage +from google.cloud import secretmanager +import json +import requests +import os +from datetime import datetime, timezone, timedelta +import gzip + + +# Storage クライアントを作成 +storage_client = storage.Client() +sm_client = secretmanager.SecretManagerServiceClient() + +@functions_framework.http +def handle_request(request): + # POSTリクエストの処理 + if request.method != 'POST': + return ({'error': 'Method not allowed'}, 405, {'Content-Type': 'application/json'}) + try: + request_json = request.get_json() + print(request_json) + + project_id = os.getenv("PROJECT_ID") + miitel_url = os.getenv("MIITEL_URL") + + video_info = request_json["video"] + + access_permission = video_info["access_permission"] + video_id = video_info["id"] # 会議履歴ID + host_name = video_info["host"]["user_name"] # ホストユーザー名 + host_id = video_info["host"]["login_id"] # ホストユーザーID + starts_at = video_info["starts_at"] # 開始日時 + ends_at = video_info["ends_at"] # 終了日時 + + video_url = miitel_url + "app/video/" + video_id # 会議履歴URL + title = video_info["title"] # 会議タイトル + print("会議タイトル",title) + + # 閲覧制限のない会議のみ生成 + if access_permission != "EVERYONE": + return (json.dumps({"status": "end"}, ensure_ascii=False), 200, {"Content-Type": "application/json"}) + + # 社外ミーティングのみ議事録作成 + if "様" not in title or "社内" in title: + return (json.dumps({"status": "end"}, ensure_ascii=False), 200, {"Content-Type": "application/json"}) + + # 議事録ファイル名 + jst_date_str = generate_jst_date(starts_at) # 開始日時をJSTに変換 + file_name = f"{jst_date_str} {title} {host_name}" + print(file_name) + # 議事録作成 + speech_recognition = video_info["speech_recognition"]["raw"] # 文字起こしデータ + minutes_text = create_minutes(project_id,speech_recognition) + print("議事録作成完了") + + # テキスト内容をセット + minutes = f"会議履歴URL:{video_url}\n" + minutes += f"担当者:{host_name}\n\n" + minutes += minutes_text + + response_data = { + "status": "next", # ステータス + "title": title, # 会議タイトル + "host_id": host_id, # ホストユーザーID + "host_name": host_name, # ホストユーザー名 + "video_url": video_url, # 会議履歴URL + "starts_at": starts_at, # 開始日時 + "ends_at": ends_at, # 終了日時 + "file_name": file_name, # 議事録ファイル名 + "minutes": minutes, # 議事録内容 + } + + return (json.dumps(response_data, ensure_ascii=False), 200, {"Content-Type": "application/json"}) + except Exception as e: + # エラー + error_response = { + "error": str(e) #エラー内容 + } + print(str(e)) + return json.dumps(error_response), 500, {'Content-Type': 'application/json'} #エラー + + +def generate_jst_date(starts_at): + + # UTCの文字列をdatetimeオブジェクトに変換 + utc_datetime = datetime.fromisoformat(starts_at) + + # JSTへの変換 + jst_timezone = timezone(timedelta(hours=9)) # JSTはUTC+9 + jst_datetime = utc_datetime.astimezone(jst_timezone) + + # yyyy-MM-dd形式にフォーマット + jst_date_str = jst_datetime.strftime("%Y年%m月%d日") + return jst_date_str + + +def create_minutes(project_id,speech_recognition): + location = "us-central1" + model_id = os.getenv("MODEL_ID") + # print("モデルID:", model_id) + + vertexai.init(project=project_id, location=location) + model = GenerativeModel(model_id) + # print("モデル初期化完了") + + prompt = f""" +あなたは議事録作成のプロフェッショナルです。以下の「文字起こし結果」は営業マンが録音した商談の文字起こしです。以下の制約条件に従い、最高の商談報告の議事録を作成してください。 + +制約条件: +1. 文字起こし結果にはAIによる書き起こしミスがある可能性を考慮してください。 +2. 冒頭に主要な「決定事項」と「アクションアイテム」をまとめてください。 +3. 議論のポイントを議題ごとに要約してください。 +4. 見出しや箇条書きを用いて、情報が探しやすい構造で簡潔かつ明瞭に記述してください。 +5. 要約は500文字以内に収めてください。 +6. 箇条書き形式で簡潔にまとめてください。 +7. マークダウン記法は使わず、各項目を「■」や「・」等を使って見やすくしてください。 + +文字起こし結果: +{speech_recognition} + """ + + + # print("-------------プロンプト-------------") + # print(prompt[:1000]) + # print("-------------議事録作成-------------") + response = model.generate_content(prompt) + # print(response.text) + return response.text + diff --git a/functions/generate-meeting-minutes/source/requirements.txt b/functions/generate-meeting-minutes/source/requirements.txt new file mode 100755 index 0000000..fab01d8 --- /dev/null +++ b/functions/generate-meeting-minutes/source/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +google-cloud-storage +google-cloud-aiplatform +google-cloud-secret-manager +pydrive2 \ No newline at end of file diff --git a/functions/trigger-minutes-workflow-from-miitel/.env_debug b/functions/trigger-minutes-workflow-from-miitel/.env_debug new file mode 100755 index 0000000..195ea91 --- /dev/null +++ b/functions/trigger-minutes-workflow-from-miitel/.env_debug @@ -0,0 +1,4 @@ +PROJECT_ID=datacom-poc +LOCATION=asia-northeast1 +BUCKET=meeting-report-data +WORKFLOW=mrt-workflow-create-minutes diff --git a/functions/trigger-minutes-workflow-from-miitel/.env_dev b/functions/trigger-minutes-workflow-from-miitel/.env_dev new file mode 100755 index 0000000..e0baea9 --- /dev/null +++ b/functions/trigger-minutes-workflow-from-miitel/.env_dev @@ -0,0 +1,4 @@ +PROJECT_ID: datacom-poc +LOCATION: asia-northeast1 +BUCKET: meeting-report-data +WORKFLOW: mrt-workflow-create-minutes diff --git a/functions/trigger-minutes-workflow-from-miitel/.env_prod b/functions/trigger-minutes-workflow-from-miitel/.env_prod new file mode 100755 index 0000000..1141bab --- /dev/null +++ b/functions/trigger-minutes-workflow-from-miitel/.env_prod @@ -0,0 +1,4 @@ +PROJECT_ID: rational-timing-443808-u0 +LOCATION: asia-northeast1 +BUCKET: meeting-data +WORKFLOW: mrt-workflow-create-minutes diff --git a/functions/trigger-minutes-workflow-from-miitel/_scripts/deploy_dev.sh b/functions/trigger-minutes-workflow-from-miitel/_scripts/deploy_dev.sh new file mode 100755 index 0000000..022d3a0 --- /dev/null +++ b/functions/trigger-minutes-workflow-from-miitel/_scripts/deploy_dev.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# デプロイする関数名 +FUNCTION_NAME="mrt-trigger-minutes-workflow-from-miitel" + +# 関数のエントリポイント +ENTRY_POINT="handle_request" + +# ランタイム +RUNTIME="python312" + +# リージョン +REGION="asia-northeast1" + +# 環境変数ファイル +ENV_VARS_FILE=".env_dev" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# デプロイコマンド +gcloud functions deploy $FUNCTION_NAME \ + --gen2 \ + --region $REGION \ + --runtime $RUNTIME \ + --source=./source \ + --trigger-http \ + --no-allow-unauthenticated \ + --entry-point $ENTRY_POINT \ + --env-vars-file $ENV_VARS_FILE \ No newline at end of file diff --git a/functions/trigger-minutes-workflow-from-miitel/source/main.py b/functions/trigger-minutes-workflow-from-miitel/source/main.py new file mode 100755 index 0000000..08bf7d7 --- /dev/null +++ b/functions/trigger-minutes-workflow-from-miitel/source/main.py @@ -0,0 +1,75 @@ +import functions_framework +from google.cloud import storage +from google.cloud.workflows import executions_v1 +from google.cloud.workflows.executions_v1.types import Execution +import json +import os +import gzip + + +# Storage クライアントを作成 +cs_client = storage.Client() +wf_client = executions_v1.ExecutionsClient() + +@functions_framework.http +def handle_request(request): + # POSTリクエストの処理 + if request.method != 'POST': + # 他のメソッドに対するエラーレスポンス + return ({'error': 'Method not allowed'}, 405) + + try: + request_json = request.get_json() + print(request_json) + + + if "challenge" in request_json: + # MiiTelのチャレンジリクエストに対する応答 + return (request_json["challenge"], 200, {'Content-Type':'text/plain'}) + + project_id = os.getenv("PROJECT_ID") + bucket_name = os.getenv("BUCKET") # 共有ドライブID + location = os.getenv("LOCATION") # ワークフローのロケーション + workflow = os.getenv("WORKFLOW") # ワークフロー名 + + # デバッグ用に保存 + save_to_gcs(bucket_name,request_json) + + # ワークフロー呼び出し + argument = json.dumps({"video": request_json["video"]}) + execution = Execution(argument=argument) + parent = f"projects/{project_id}/locations/{location}/workflows/{workflow}" + print(parent) + response = wf_client.create_execution(request={"parent": parent, "execution": execution}) + print(f"Workflow execution started: {response.name}") + + return (json.dumps({}), 200, {'Content-Type': 'application/json'}) + except Exception as e: + # エラー + error_response = { + "error": str(e) #エラー内容 + } + print(str(e)) + return json.dumps(error_response), 500, {'Content-Type': 'application/json'} #エラー + + + + +def save_to_gcs(bucket_name,request_json): + file_name = request_json["video"]["id"] + ".json.gz" + + bucket = cs_client.bucket(bucket_name) + + # GCS バケットのブロブを取得 + blob = bucket.blob(f"request_log/{file_name}") + + + # JSONを文字列に変換 + json_string = json.dumps(request_json) + + # Gzip圧縮 + compressed_data = gzip.compress(json_string.encode('utf-8')) + + # 圧縮されたデータをアップロード + blob.upload_from_string(compressed_data, content_type='application/gzip') + diff --git a/functions/trigger-minutes-workflow-from-miitel/source/requirements.txt b/functions/trigger-minutes-workflow-from-miitel/source/requirements.txt new file mode 100755 index 0000000..1f7ed5c --- /dev/null +++ b/functions/trigger-minutes-workflow-from-miitel/source/requirements.txt @@ -0,0 +1,4 @@ +functions-framework==3.* +Flask +google-cloud-storage +google-cloud-workflows \ No newline at end of file diff --git a/functions/upload-minutes-to-drive/.env_debug b/functions/upload-minutes-to-drive/.env_debug new file mode 100755 index 0000000..4331911 --- /dev/null +++ b/functions/upload-minutes-to-drive/.env_debug @@ -0,0 +1,2 @@ +KEY_PATH=projects/570987459910/secrets/sa-create-minutes-key +FOLDER_ID=0AGT_1dSq66qYUk9PVA diff --git a/functions/upload-minutes-to-drive/.env_dev b/functions/upload-minutes-to-drive/.env_dev new file mode 100755 index 0000000..0fd6eab --- /dev/null +++ b/functions/upload-minutes-to-drive/.env_dev @@ -0,0 +1,2 @@ +KEY_PATH: projects/32472615575/secrets/sa-access-google-drive-key +FOLDER_ID: 1cCDJKusfrlDrJe2yHCR8pCHJXRqX-4Hw diff --git a/functions/upload-minutes-to-drive/.env_prod b/functions/upload-minutes-to-drive/.env_prod new file mode 100755 index 0000000..2b50f81 --- /dev/null +++ b/functions/upload-minutes-to-drive/.env_prod @@ -0,0 +1,2 @@ +KEY_PATH: projects/570987459910/secrets/sa-create-minutes-key +FOLDER_ID: 0AGT_1dSq66qYUk9PVA diff --git a/functions/upload-minutes-to-drive/_scripts/deploy_dev.sh b/functions/upload-minutes-to-drive/_scripts/deploy_dev.sh new file mode 100755 index 0000000..f4dbcee --- /dev/null +++ b/functions/upload-minutes-to-drive/_scripts/deploy_dev.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# プロジェクトIDを設定 +PROJECT_ID="datacom-poc" + +# デプロイする関数名 +FUNCTION_NAME="mrt-create-minutes" + +# 関数のエントリポイント +ENTRY_POINT="handle_request" + +# ランタイム +RUNTIME="python312" + +# リージョン +REGION="asia-northeast1" + +# 環境変数ファイル +ENV_VARS_FILE=".env_dev" + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + +# デプロイコマンド +gcloud functions deploy $FUNCTION_NAME \ + --gen2 \ + --region $REGION \ + --runtime $RUNTIME \ + --source=./source \ + --trigger-http \ + --no-allow-unauthenticated \ + --entry-point $ENTRY_POINT \ + --env-vars-file $ENV_VARS_FILE \ No newline at end of file diff --git a/functions/upload-minutes-to-drive/source/main.py b/functions/upload-minutes-to-drive/source/main.py new file mode 100755 index 0000000..46b6b64 --- /dev/null +++ b/functions/upload-minutes-to-drive/source/main.py @@ -0,0 +1,128 @@ +import functions_framework +from google.cloud import secretmanager +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +import json +import os + +SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"] + +sm_client = secretmanager.SecretManagerServiceClient() + +@functions_framework.http +def handle_request(request): + # POSTリクエストの処理 + if request.method != 'POST': + # 他のメソッドに対するエラーレスポンス + return ({'error': 'Method not allowed'}, 405) + + try: + request_json = request.get_json() + print(request_json) + + folder_id = os.getenv("FOLDER_ID") # 共有ドライブID + + file_name = request_json["file_name"] # 会議タイトル + minutes = request_json["minutes"] # 議事録 + + + # Secret Manager からサービスアカウントJSON文字列を取得 + service_account_info = get_service_account_info() + # 認証 + credentials = get_credentials(service_account_info) + + # APIクライアントの構築 + drive_service = build("drive", "v3", credentials=credentials) + docs_service = build("docs", "v1", credentials=credentials) + + # ファイル作成 + document_id = create_new_document(drive_service, folder_id, file_name) + print(f"Created document with ID: {document_id}") + + # テキスト内容をセット + append_minutes_to_doc(docs_service, document_id, minutes) + + response_data = { + "document_id": document_id, # 作成したドキュメントのID + } + + return json.dumps(response_data) , 200, {"Content-Type": "application/json"} + except Exception as e: + # エラー + error_response = { + "error": str(e) #エラー内容 + } + print(str(e)) + return json.dumps(error_response), 500, {'Content-Type': 'application/json'} #エラー + + + +# +# SecretManagerから秘密鍵を取得 +# +def get_service_account_info(): + key_path = os.getenv('KEY_PATH') + "/versions/1" + # 秘密鍵取得 + response = sm_client.access_secret_version(name=key_path) + # 秘密鍵の値をデコード + secret_key = response.payload.data.decode("UTF-8") + return json.loads(secret_key) + +# Google Drive認証 +def get_credentials(service_account_info): + credentials = service_account.Credentials.from_service_account_info( + service_account_info, + scopes=SCOPES + ) + return credentials + + +def create_new_document(service,folder_id,title): + """ + Google Sheets APIを使用して新しいスプレッドシートを作成する + :param service: Google Sheets APIのサービスオブジェクト + :param title: スプレッドシートのタイトル + :return: 作成したスプレッドシートのID + """ + file_metadata = { + 'name': title, + 'parents': [folder_id], # 作成したフォルダのIDを指定 + 'mimeType': 'application/vnd.google-apps.document', + } + result = ( + service.files() + .create(body=file_metadata, fields="id", supportsAllDrives=True) + .execute() + ) + return result.get("id") + + +def append_minutes_to_doc(service, document_id, minutes): + """ + Google Sheets APIを使用してスプレッドシートにログを追加する + :param service: Google Sheets APIのサービスオブジェクト + :param spreadsheet_id: スプレッドシートのID + :param row_data: 追加するログデータ(リスト形式) + """ + requests = [ + { + 'insertText': { + 'location': { + 'index': 1, + }, + 'text': minutes + } + }, + ] + + body = { + 'requests': requests + } + + # スプレッドシートにログを追加 + result = service.documents().batchUpdate( + documentId=document_id, + body=body, + ).execute() + return result diff --git a/functions/upload-minutes-to-drive/source/requirements.txt b/functions/upload-minutes-to-drive/source/requirements.txt new file mode 100755 index 0000000..e809a11 --- /dev/null +++ b/functions/upload-minutes-to-drive/source/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +google-cloud-secret-manager +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..8ddbd3d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +functions-framework==3.* +requests==2.26.0 +google-cloud-storage +google-cloud-bigquery +hubspot diff --git a/terraform/dev/initial/main.tf b/terraform/dev/initial/main.tf new file mode 100755 index 0000000..a502c79 --- /dev/null +++ b/terraform/dev/initial/main.tf @@ -0,0 +1,78 @@ +variable "project_id" { + type = string + default = "datacom-poc" +} + +variable "project_number" { + type = string + default = "32472615575" +} + +variable "region" { + type = string + default = "asia-northeast1" +} + + +# Cloud Functionsサービスアカウント +resource "google_service_account" "cf_sa" { + project = var.project_id + account_id = "mrt-cloudfunctions-sa-devtest" + display_name = "Cloud Functions SA" +} + +# 権限をSAに付与 +resource "google_project_iam_member" "cf_sa_role" { + for_each = toset(["roles/storage.objectAdmin","roles/workflows.invoker", "roles/secretmanager.secretAccessor", "roles/aiplatform.user"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.cf_sa.email}" +} + + +# Cloud Workflows用サービスアカウント +resource "google_service_account" "workflows_sa" { + project = var.project_id + account_id = "mrt-cloudworkflows-sa-devtest" + display_name = "Cloud Workflows SA" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "wf_cf_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.workflows_sa.email}" +} + + +# API Gateway用サービスアカウント +resource "google_service_account" "gateway_sa" { + project = var.project_id + account_id = "mrt-apigateway-sa-devtest" + display_name = "Cloud Functions 起動用サービスアカウント" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "gateway_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.gateway_sa.email}" +} + + +# cloud build用サービスアカウント +resource "google_service_account" "cloudbuild_sa" { + project = var.project_id + account_id = "mrt-cloudbuild-sa-devtest" + display_name = "Cloud Build 用サービスアカウント" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "cloudbuild_role" { + for_each = toset(["roles/cloudbuild.builds.builder","roles/storage.objectAdmin", "roles/artifactregistry.writer", "roles/developerconnect.readTokenAccessor", "roles/cloudfunctions.developer","roles/workflows.admin", "roles/iam.serviceAccountUser"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.cloudbuild_sa.email}" +} \ No newline at end of file diff --git a/terraform/dev/scheduler/main.tf b/terraform/dev/scheduler/main.tf new file mode 100755 index 0000000..1cfc697 --- /dev/null +++ b/terraform/dev/scheduler/main.tf @@ -0,0 +1,51 @@ +variable "project_id" { + type = string + default = "datacom-poc" +} + +variable "region" { + type = string + default = "asia-northeast1" +} + +variable "function_name" { + type = string + default = "mrt-create-log-sheet" +} + + +# Scheduler実行用サービスアカウント +resource "google_service_account" "cf_scheduler_sa" { + project = var.project_id + account_id = "mrt-scheduler-sa-devtest" + display_name = "Cloud Functions 起動用サービスアカウント" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "scheduler_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}" +} + + +# 毎月1日0時に Function を実行する Scheduler ジョブ +resource "google_cloud_scheduler_job" "monthly_cf_trigger" { + project = var.project_id + name = "monthly-cf-trigger" + description = "Invoke Cloud Function on the 1st of each month at 00:00" + region = var.region + schedule = "0 0 1 * *" + time_zone = "Asia/Tokyo" + + http_target { + uri = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}" + http_method = "POST" + oidc_token { + service_account_email = google_service_account.cf_scheduler_sa.email + audience = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}" + } + } +} + diff --git a/terraform/prod/initial/main.tf b/terraform/prod/initial/main.tf new file mode 100755 index 0000000..6d5566d --- /dev/null +++ b/terraform/prod/initial/main.tf @@ -0,0 +1,78 @@ +variable "project_id" { + type = string + default = "rational-timing-443808-u0" +} + +variable "project_number" { + type = string + default = "32472615575" +} + +variable "region" { + type = string + default = "asia-northeast1" +} + + +# Cloud Functionsサービスアカウント +resource "google_service_account" "cf_sa" { + project = var.project_id + account_id = "mrt-cloudfunctions-sa" + display_name = "Cloud Functions SA" +} + +# 権限をSAに付与 +resource "google_project_iam_member" "cf_sa_role" { + for_each = toset(["roles/storage.objectAdmin","roles/workflows.invoker", "roles/secretmanager.secretAccessor", "roles/aiplatform.user"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.cf_sa.email}" +} + + +# Cloud Workflows用サービスアカウント +resource "google_service_account" "workflows_sa" { + project = var.project_id + account_id = "mrt-cloudworkflows-sa" + display_name = "Cloud Workflows SA" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "wf_cf_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.workflows_sa.email}" +} + + +# API Gateway用サービスアカウント +resource "google_service_account" "gateway_sa" { + project = var.project_id + account_id = "mrt-apigateway-sa" + display_name = "Cloud Functions 起動用サービスアカウント" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "gateway_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.gateway_sa.email}" +} + + +# cloud build用サービスアカウント +resource "google_service_account" "cloudbuild_sa" { + project = var.project_id + account_id = "mrt-cloudbuild-sa" + display_name = "Cloud Build 用サービスアカウント" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "cloudbuild_role" { + for_each = toset(["roles/cloudbuild.builds.builder","roles/storage.objectAdmin", "roles/artifactregistry.writer", "roles/developerconnect.readTokenAccessor", "roles/cloudfunctions.developer","roles/workflows.admin", "roles/iam.serviceAccountUser"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.cloudbuild_sa.email}" +} \ No newline at end of file diff --git a/terraform/prod/scheduler/main.tf b/terraform/prod/scheduler/main.tf new file mode 100755 index 0000000..e7441a4 --- /dev/null +++ b/terraform/prod/scheduler/main.tf @@ -0,0 +1,51 @@ +variable "project_id" { + type = string + default = "rational-timing-443808-u0" +} + +variable "region" { + type = string + default = "asia-northeast1" +} + +variable "function_name" { + type = string + default = "mrt-create-log-sheet" +} + + +# Scheduler実行用サービスアカウント +resource "google_service_account" "cf_scheduler_sa" { + project = var.project_id + account_id = "mrt-scheduler-sa" + display_name = "Cloud Functions 起動用サービスアカウント" +} + +# 権限を SA に付与 +resource "google_project_iam_member" "scheduler_role" { + for_each = toset(["roles/cloudfunctions.invoker","roles/run.invoker"]) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.cf_scheduler_sa.email}" +} + + +# 毎月1日0時に Function を実行する Scheduler ジョブ +resource "google_cloud_scheduler_job" "monthly_cf_trigger" { + project = var.project_id + name = "monthly-cf-trigger" + description = "Invoke Cloud Function on the 1st of each month at 00:00" + region = var.region + schedule = "0 0 1 * *" + time_zone = "Asia/Tokyo" + + http_target { + uri = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}" + http_method = "POST" + oidc_token { + service_account_email = google_service_account.cf_scheduler_sa.email + audience = "https://${var.region}-${var.project_id}.cloudfunctions.net/${var.function_name}" + } + } +} + diff --git a/workflows/workflow-create-minutes/_scripts/deploy_dev.sh b/workflows/workflow-create-minutes/_scripts/deploy_dev.sh new file mode 100755 index 0000000..15b0428 --- /dev/null +++ b/workflows/workflow-create-minutes/_scripts/deploy_dev.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# 環境変数 +PROJECT_ID="datacom-poc" +WORKFLOW_NAME="mrt-workflow-create-minutes" + + +gcloud auth application-default set-quota-project $PROJECT_ID +gcloud config set project $PROJECT_ID + + +gcloud workflows deploy $WORKFLOW_NAME \ + --source=main.yaml \ + --location=asia-northeast1 \ No newline at end of file diff --git a/workflows/workflow-create-minutes/main.yaml b/workflows/workflow-create-minutes/main.yaml new file mode 100755 index 0000000..dedd621 --- /dev/null +++ b/workflows/workflow-create-minutes/main.yaml @@ -0,0 +1,71 @@ +main: + params: [input] + steps: + - initialize: + assign: + - project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")} + - create_hubspot_meeting_log_result: {} + - upload_minutes_to_drive_result: {} + - generate_meeting_minutes: + call: http.post + args: + url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-generate-meeting-minutes"} + body: + video: ${input.video} + auth: + type: OIDC + result: generate_meeting_minutes_result + - conditinal_switch: + switch: + - condition: ${generate_meeting_minutes_result.body.status != "end"} + steps: + - parallel_execute: + parallel: + shared: + [ + create_hubspot_meeting_log_result, + upload_minutes_to_drive_result, + ] + branches: + - create_hubspot_meeting_log_branch: + steps: + - create_hubspot_meeting_log: + call: http.post + args: + url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-create-hubspot-meeting-log"} + body: + title: ${generate_meeting_minutes_result.body.title} + host_id: ${generate_meeting_minutes_result.body.host_id} + starts_at: ${generate_meeting_minutes_result.body.starts_at} + ends_at: ${generate_meeting_minutes_result.body.ends_at} + minutes: ${generate_meeting_minutes_result.body.minutes} + auth: + type: OIDC + result: create_hubspot_meeting_log_result + - upload_minutes_to_drive_branch: + steps: + - upload-minutes-to-drive: + call: http.post + args: + url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-upload-minutes-to-drive"} + body: + file_name: ${generate_meeting_minutes_result.body.file_name} + minutes: ${generate_meeting_minutes_result.body.minutes} + auth: + type: OIDC + result: upload_minutes_to_drive_result + - append_log_to_sheet: + call: http.post + args: + url: ${"https://asia-northeast1-" + project_id + ".cloudfunctions.net/mrt-append-log-to-sheet"} + body: + title: ${generate_meeting_minutes_result.body.title} + host_name: ${generate_meeting_minutes_result.body.host_name} + video_url: ${generate_meeting_minutes_result.body.video_url} + starts_at: ${generate_meeting_minutes_result.body.starts_at} + matched_company_id: ${create_hubspot_meeting_log_result.body.matched_company_id} + matched_company_name: ${create_hubspot_meeting_log_result.body.matched_company_name} + document_id: ${upload_minutes_to_drive_result.body.document_id} + auth: + type: OIDC + result: append_log_to_sheet_result