TalentX Tech Blog

Tech Blog

カスタマーサクセスからの調査依頼を効率化!Ask Devin×GAS×Slackによる自動調査の構築

こんにちは。MyTalent開発の穴原です。

SaaSを運用している中でCS(カスタマーサクセス)から調査依頼を受けることがあります。

本記事では調査工数を減らすためにAsk Devinを活用した取り組みを紹介します。

Ask Devinとは

Ask DevinはDevinが提供する機能の一つです。 devin.ai Devinとは違い、プログラムを作成したりgithub上のプルリクエストをレビューしてもらうことはできません。 名前の通り、質問を行うとリポジトリのプログラムを基に回答を行ってくれます。

Ask Devinは2025年9月時点ではBeta版となっており、利用にACUを消費しません。

回答には参照したプログラム箇所も記載してくれるため、回答内容が正しいかエンジニアが判断しやすく、調査に適した機能です。

TalentXの調査依頼フロー

TalentXではCSからの調査依頼をSlackのワークフロー経由で受けています。 SlackワークフローはGoogle スプレッドシートと連携をしており、調査依頼を管理しています。

このGoogle スプレッドシート上のGASにて調査依頼の内容をAsk Devinに伝え、一次調査を行ってもらおうと思います。

処理のフロー

以下のフローでAsk Devinに調査依頼を行い、GASにてSlackに調査結果を報告します。

  1. Slackワークフローにて調査依頼を投稿
  2. 依頼内容をGoogle スプレッドシート連携機能にて記載
  3. Google スプレッドシートに変更が行われたことをトリガーにGASの起動
  4. GASよりAsk Devinに調査内容を伝える
  5. Ask Devinより返却された内容をGASでSlackに投稿

一連の処理を作成

Slack ワークフローの作成、Google スプレッドシート連携

Slackワークフローにて以下を作成してください。

  • 調査依頼を入力してもらうフォーム表示
  • フォームに入力された内容をGoogle スプレッドシートに記載

参考
自動化 : シンプルなフォームを使って情報を収集する
ワークフロービルダーで Google スプレッドシートを使用する

GASの作成

Ask Devinに質問内容をリクエストする

Ask DevinはAPIが提供されていません。 代わりにDeepWiki MCPを利用することでAsk Devinに対してリクエストを行うことが可能です。

公式のドキュメントは以下になります。
https://docs.devin.ai/work-with-devin/deepwiki-mcp

今回はプライベートリポジトリが対象のため、Devin APIを利用するAPI KEYを取得します。 API KEYの取得方法は以下のドキュメントを参照ください。
https://docs.devin.ai/api-reference/overview#get-an-api-key

MCPは生成 AIを対象にしたプロトコルですが、httpによる接続のためGASからリクエストを送ることができます。

実装

以下の処理をGASにて実装します。

  1. Google スプレッドシートの最終行より調査依頼内容を取得。
  2. MCPサーバーに接続し、初期化メッセージを送信。メッセージIDを取得。
  3. Ask Devinを指定して調査依頼内容をリクエスト送信。
  4. Ask Devinからの返却値をパース。
  5. Slackに通知。

プログラムは以下の様になります。

/**
 * Google スプレッドシートの変更時に自動実行される関数
 * トリガー設定: 「変更時」に設定
 */
function onSpreadsheetChange(e) {
  const sheet = SpreadsheetApp.getActiveSheet();
  const lastRow = sheet.getLastRow();
  
  // 設定値を取得
  const scriptProperties = PropertiesService.getScriptProperties();
  const repository = scriptProperties.getProperty('REPOSITORY_NAME');
  const apiKey = scriptProperties.getProperty('DEVIN_API_KEY');
  const webhookUrl = scriptProperties.getProperty('SLACK_WEBHOOK_URL');
  const channel = scriptProperties.getProperty('SLACK_CHANNEL');

  // 最終行のA列から質問を取得
  const question = sheet.getRange(lastRow, 1).getValue();
  
  if (!question) {
    return;
  }
  
  // MCPを初期化
  const sessionId = initializeMCP(apiKey);
  // DevinのMCPに質問を送信
  const result = askDevin(
    sessionId,
    repository,
    question,
    apiKey
  );
  
  // Slackに結果を送信
  sendToSlack(question, result, webhookUrl, channel);
}

/**
 * MCP接続を初期化
 */
function initializeMCP(apiKey) {
  const url = 'https://mcp.devin.ai/mcp';
  
  const payload = {
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-03-26",
      "clientInfo": {
        "name": "gas-client",
        "version": "1.0.0"
      },
      "capabilities": {}
    },
    "id": 1
  };
  
  const options = {
    'method': 'POST',
    'headers': {
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream',
      'Authorization': `Bearer ${apiKey}`
    },
    'payload': JSON.stringify(payload)
  };
  
  const response = UrlFetchApp.fetch(url, options);
  const headers = response.getAllHeaders();
  const sessionId = headers['Mcp-Session-Id'] || headers['mcp-session-id'];

  return sessionId;
}

/**
 * Ask Devinに質問を送信する
 */
function askDevin(sessionId, repoName, question, apiKey) {
  const url = 'https://mcp.devin.ai/mcp';
  
  const payload = {
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "ask_question",
      "arguments": {
        "repoName": repoName,
        "question": question
      }
    },
    "id": 2
  };
  
  const options = {
    'method': 'POST',
    'headers': {
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream',
      'Mcp-Session-Id': sessionId,
      'Authorization': `Bearer ${apiKey}`
    },
    'payload': JSON.stringify(payload)
  };
  
  const response = UrlFetchApp.fetch(url, options);
  const responseText = response.getContentText();
  return parseSSEResponse(responseText);
}

/**
 * SSEのレスポンスをパース
 */
function parseSSEResponse(responseText) {
  const lines = responseText.split('\n');
  
  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = line.substring(6);
      
      if (data !== 'ping' && data !== '') {
        const jsonData = JSON.parse(data);
        if (jsonData.result) {
          return jsonData.result.content[0].text;
        }
      }
    }
  }
  
  return "";
}

/**
 * 質問と回答をフォーマットしてSlackチャンネルに投稿
 */
function sendToSlack(question, answer, webhookUrl, channel) {
  // slackの文字数制限対策で回答が2500文字超えていたら省略
  if (answer.length > 2500) {
    const lines = answer.trim().split('\n');
    const lastLine = lines[lines.length - 1];
    // 最終行には回答全文が確認できるURLが記載されているため残す
    answer = answer.substring(0, 2500) + '...\n' + lastLine;
  }

  // Slackに送信するメッセージを作成
  const payload = {
    channel: channel,
    text: "Ask Devinからの回答",
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: "🤖 Ask Devin Response"
        }
      },
      {
        type: "section",
        fields: [
          {
            type: "mrkdwn",
            text: "*質問:*\n" + question
          }
        ]
      },
      {
        type: "divider"
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: "*回答:*\n" + answer
        }
      }
    ]
  };
  
  const options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  };
  
  UrlFetchApp.fetch(webhookUrl, options);
}

/**
 * MCPサーバーから利用可能なツール一覧を取得する
 * Ask Devinへのリクエスト失敗する時等にツール情報を取得してデバッグに活用
 */
function getMCPToolsList() {
  const url = 'https://mcp.devin.ai/mcp';
  const apiKey = PropertiesService.getScriptProperties().getProperty('DEVIN_API_KEY');
  
  // セッションIDを取得
  const sessionId = initializeMCP(apiKey);
  
  const payload = {
    "jsonrpc": "2.0",
    "method": "tools/list",
    "params": {},
    "id": 3
  };
  
  const options = {
    'method': 'POST',
    'headers': {
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream',
      'Mcp-Session-Id': sessionId,
      'Authorization': `Bearer ${apiKey}`
    },
    'payload': JSON.stringify(payload)
  };
  
  const response = UrlFetchApp.fetch(url, options);
  const responseText = response.getContentText();
  
  // レスポンスをパース
  const lines = responseText.split('\n');
  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = line.substring(6);
      if (data !== 'ping' && data !== '') {
        const jsonData = JSON.parse(data);
        if (jsonData.result && jsonData.result.tools) {
          return jsonData.result.tools;
        }
      }
    }
  }
  return null;
}

Ask Devinへのリクエストが失敗する場合

AIがMCPを利用する際にはツール一覧の取得を行い、Ask Devinを利用するためのリクエスト内容をAIが判断してリクエストを作成します。

GASではツールの一覧を取得し、内容を理解してAsk Devinを利用するためのリクエスト作成が難しいためツール一覧の取得は省略しています。

Ask Devinへのリクエストが失敗した場合、利用するためのリクエスト内容に変更が入った可能性があります。
getMCPToolsList 関数を利用し、Ask Devinを利用するためのリクエスト方法を理解し、プログラムの修正を行う必要があります。

Google スプレッドシートへの記録をトリガーに起動させる

GASの左メニュー「トリガー」より、新規トリガーを作成してください。
イベントの種類を「変更時」に設定して保存してください。

実際に調査をお願いしてみた。

簡単な調査内容として、「MyTalentにタレントは最大何人登録できますか?」と質問してみます。

Ask Devinが起動し、最大人数の制限は確認できないことを報告してくれました。 ※他にも様々な情報を返却してくれたのですが、ほぼモザイクとなる情報になるため省略しています。

まとめ

Slack経由の調査依頼時にAsk Devinを活用する取り組みを紹介しました。

調査内容によってはCSとコミュニケーションを行いながら理解を深める必要もあるため、常にAsk Devinだけで解決するかというと難しいです。
ただ、Ask Devinで解決しなくても参考となるプログラム部分や考えを投稿してくれることで調査の助けとなることが多く、調査完了までの工数削減に役立ってくれます。

TalentXでは「仕様質問」「不具合報告」「データ抽出依頼」毎にSlackワークフローを分けています。使用するワークフローに合わせて回答精度が上がるようにAsk Devinに伝えるプロンプトをカスタマイズしています。

Slackで調査依頼の連絡を頂く方は多いと思います。調査工数を削減するには有効な施策だと思いますので使ってみてください。

最後に

現在、TalentXでは一緒に働く仲間を募集しております。

talentx.brandmedia.i-myrefer.jp

カジュアル面談も行っておりますので、ぜひご応募ください!

i-myrefer.jp