2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ServiceNow Lens for MobileとLens in Virtual Agent使ってみた

Posted at

待望(?)のServiceNow Lens for Mobile

ありがたいことに、以下の記事が投稿以降結構読まれていまして、

なにかLensに関するアップデートで記事出せるといいなーと思っていたところ、
良い情報を見つけて検証したのでその記録です。

上記記事投稿当初から頂いたXのリプライでもMobile対応してるのかどうか、とか、
社内の会話でも画像の送信ならMobileから送る需要ありそうだよね、みたいな話があり、たまに情報を探ってました。

ということで、最新の公式の情報ソースのまとめと、
さわりの部分だけ検証して動作確認レベルまでは実施してみたので紹介します。
(2025年10月6日時点の情報です)

LensのMobileに関する公式情報まとめ

  1. ServiceNow Lens for mobile
    おそらく「ServiceNow Lens Mobile」などでググると一番上に出てくるこの記事です。
    こちらに関しては2025年8月にLens(PC)の検証をしている時からありましたが、「Mobileでできる」という感じのニュアンスと、キャプチャからしてVirtual AgentのChatでできるんだろうな~(できるようになるんだろうな~)という感じの状態でした。(設定手順や方法などは無い)

  2. Enabling ServiceNow Lens in Virtual Agent
    そんな中ここ最近で発見したのがこの記事です。1)の記事の左側のメニューにあり、ふと目に入って見つけました。
    たぶん、8月時点では無かった気がするんですが…見落としかも。

この後の検証で少し触れますが、「Lensの分析機能をMobile(Virtual Agent)で使う」だけであればこの2.のdocsの通りに設定していけば利用可能になります。特に込み入った設定は無く、画像を送ってその分析情報をレスポンスする、というものを実現できます。

【そのまま】Lens in Virtual Agent(+Mobile)で使ってみた

まずはそのまま編です。
上記2.のdocs通りに設定した結果、docsの途中で躓くこともなくスムーズに設定できました。

※前提として、インスタンス側にServiceNow Lensプラグインの導入やユーザーの権限設定が終わっているものとしています。詳しくは冒頭にも記載のコチラの記事を先にご確認ください。

docs通りの設定完了後の動作検証の様子

  1. 「ServiceNow Lens」トピックを選択
    image.png

  2. Lensの標準機能として画像のアップロードを促されます
    image.png

  3. Provide instructions for this image…のメッセージに対して、どうしてほしいかというプロンプトを投げます。(docsと同じように"Extract the issue and suggest next steps"としてみました)
    image.png

4)指示通りに画像の状況に対する問題と次のステップを出力してくれました
image.png

5)標準のServiceNow Lensトピックはここで終了です。
image.png

これをこのまま次の作業や仕事につなげるとしたら、分析結果をコピペしてインシデントやリクエストを起票する形になると思います。
image.png

【チャレンジ】Lens in Virtual Agent(+Mobile)で使ってみた

さて、チャレンジ編です。そのまま編だけで機能の妥当性は確認できましたが、実用するにはもう一歩踏み込んで機能の価値を出したいところです。

考え方としては、
・Lensは画像を送るだけである程度高精度の分析結果を示してくれるはず

・画像を送るときにMobileの需要があるよね

・画像が送れること自体にMobileの需要があるわけではなく、
 Mobileで出先やPCが使えない環境でも仕事が完結するから価値なのである

・つまり、分析結果がMobile画面に出力されるだけでは恩恵を受けづらい

・じゃあ、分析結果をインシデントに起票してあげたらいかがか????

という感じです。

基本的な処理はそのまま編で使ったトピックの処理を使いつつ、
出力を変数にしてインシデントレコードを作成するトピックブロックを作成しました。

<検証で改造したTopic Blockのフロー(動作確認完了時点)>
※赤枠が追加で作成したり、コードを修正したりした箇所です。
image.png

もしものために、あらかじめ用意されたトピックやトピックブロックを直接編集せず、コピーして検証しましょう

そのまま編で紹介したトピックを見ると、実際の処理は「ServiceNow Lens - Topic Block」内にあることがわかります。
image.png
こちらのTopic Blockと大元のTopicをコピーしてロジックを追加、編集しましょう。(アプリケーションはGlobalです)

変更を加えたポイントは以下です。
(1)Lensの出力や添付ファイルをインシデントに渡すためInvoke Lens Service with attachment Id(元からあるScript action utility)のスクリプトを編集
(2)Lensの出力を受取り、インシデントを起票するCreate Incident from LensのScript action utilityを新規作成

今回動作検証するためにコードを作成しましたが、「手早く」「ざっくり」「動作が確認できれば良い」という理由から、すべて生成AI任せでコード生成し、詳細なレビューを実施しておりません。万が一流用する際は自己責任でお願いします。
(デバッグの痕跡とかも残したままです)

微粒子レベルの参考までにコードも残しておきます(長いので折りたたみ表示)

(1)Invoke Lens Service with attachment Id
(function execute() {
  try {
    // 1) 添付画像URLから media_id を抽出
    var mediaURL = (vaInputs.attached_image || '').toString();
    var mediaArray = mediaURL.split('/cs/media/');
    if (mediaArray.length !== 2) throw new Error(AILensMessages.VA_TRY_AGAIN);

    var mediaId = mediaArray[1];

    // 2) sys_cs_media から sys_attachment_id  sys_id を取得
    var mediaGR = new GlideRecord('sys_cs_media'); // NOTE: admin only read (SKIP_ACL comment)
    mediaGR.addQuery('media_id', mediaId);
    mediaGR.setLimit(1);
    mediaGR.query();

    var mediaSysId = '';
    var mediaAttachmentId = '';
    if (mediaGR.next()) {
      mediaSysId        = String(mediaGR.getUniqueValue());         // sys_cs_media  sys_id
      mediaAttachmentId = String(mediaGR.getValue('sys_attachment_id')); // そこに紐づく sys_attachment.sys_id
    }
    if (!mediaAttachmentId) throw new Error(AILensMessages.VA_TRY_AGAIN);

    // 3) ユーザープロンプトLensへ渡すのみ
    var userPrompt = 'User prompt: ' + (vaInputs.user_instruction || '').toString();
    var parentContext = (vaInputs.parent_topic_context || '').toString();
    if (parentContext.length > 0) userPrompt += ', Additional context: ' + parentContext;

    // 4) Lens 呼び出し
    var response = new sn_app_lens_core.AILensActionService()
      .invokeLens('842bfc8e37066210b97528c734924baf', [mediaAttachmentId], userPrompt, [], {}, true);

    // 5) レスポンス整形JSON/プレーン両対応
    if (response.status === 'success') {
      try {
        var llmResponse = (response.lensResponse || '')
          .replace(/^```(json)?/i, '')
          .replace(/```$/, '');
        response.llmResponse = JSON.parse(llmResponse);
      } catch (ignore) {
        response.llmResponse = response.lensResponse;
      }
    }

    // 6) 後工程用に コピー元の親 を確定して保持
    //    - まずは sys_cs_media を優先
    vaVars.mediaSysId = mediaSysId;

    //    - さらにLens 側で作られた sys_attachment の親を解決して保持汎用フォールバック用
    //      response.request.attachmentIds[0] または mediaAttachmentId のどちらかで親を調べる
    var candidateAttId = '';
    if (response && response.request && response.request.attachmentIds && response.request.attachmentIds.length > 0) {
      candidateAttId = String(response.request.attachmentIds[0]);
    }
    if (!candidateAttId) candidateAttId = mediaAttachmentId;

    var sourceParentTable = '';
    var sourceParentSysId = '';
    if (candidateAttId) {
      var attGR = new GlideRecord('sys_attachment');
      if (attGR.get(candidateAttId)) {
        sourceParentTable = String(attGR.getValue('table_name'));   // : lens_transaction / sys_cs_conversation_task
        sourceParentSysId = String(attGR.getValue('table_sys_id')); // その親レコードの sys_id
      }
    }

    // 7) 共有変数へ保存
    vaVars.sourceParentTable = sourceParentTable; // : 'lens_transaction'
    vaVars.sourceParentSysId = sourceParentSysId; // : '4b844e5f...'
    vaVars.attachmentId      = candidateAttId;    // 念のため保持
    vaVars.lensResponse      = JSON.stringify(response);

  } catch (e) {
    vaVars.lensResponse = JSON.stringify({ status: 'error', error: { message: e.message } });
  } finally {
    vaVars.firstTimePrompt = false;
  }
})();

(2)Create Incident from Lens
(function execute() {
  try {
    // 1) Lensレスポンスを取得検証
    var raw = vaVars.lensResponse || '';
    if (!raw) throw new Error('Lens response is empty.');
    var resp = JSON.parse(raw);
    if (resp.status !== 'success' || !resp.llmResponse) throw new Error('Lens response not ready.');

    // 2) LLM出力をオブジェクト化文字列ならJSONパースを試行
    var lens = resp.llmResponse;
    if (typeof lens === 'string') {
      try {
        var trimmed = lens.replace(/^```(json)?/i, '').replace(/```$/, '');
        lens = JSON.parse(trimmed);
      } catch (ignore) { /* 文字列のまま利用 */ }
    }

    // 3) ヘルパー
    function toText(v) {
      if (v === null || v === undefined) return '';
      if (typeof v === 'string') return v;
      try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }
    }
    function firstNonEmpty(obj, paths) {
      for (var i = 0; i < paths.length; i++) {
        var p = paths[i];
        try {
          var parts = p.split('.');
          var cur = obj;
          for (var j = 0; j < parts.length; j++) { if (cur == null) break; cur = cur[parts[j]]; }
          var t = toText(cur).trim();
          if (t) return t;
        } catch (e) {}
      }
      return '';
    }
    function buildShortFrom(text, fallback) {
      var t = (text || '').replace(/\s+/g, ' ').trim();
      if (!t) return fallback || 'Lens analysis result';
      var m = t.match(/stop code[:\s]+([A-Z0-9_]+)/i);                // : CRITICAL_PROCESS_DIED
      if (m && m[1]) return 'Blue screen error: ' + m[1].toUpperCase();
      var cutIdx = t.search(/[。\.]\s/);
      var one = cutIdx >= 0 ? t.substring(0, cutIdx) : t;
      return one.substring(0, 160);
    }

    // 4) Short/Description 抽出
    var primaryText = (typeof lens === 'string')
      ? lens.trim()
      : (firstNonEmpty(lens, [
          'summary','analysis','content','description','text','message',
          'details','extracted_text','result','answer','output.text'
        ]) || toText(lens).trim());

    var titleCandidate = (typeof lens === 'object' && lens) ? (lens.title || lens.caption || '') : '';
    var shortDesc = titleCandidate ? toText(titleCandidate) : buildShortFrom(primaryText, 'Lens analysis result');

    // 5) インシデント作成
    var inc = new GlideRecord('incident');
    inc.initialize();
    inc.caller_id         = gs.getUserID();
    inc.short_description = shortDesc;
    inc.description       = primaryText || shortDesc;

    if (typeof lens === 'object' && lens) {
      if (lens.category)    inc.category    = lens.category;
      if (lens.subcategory) inc.subcategory = lens.subcategory;
    }

    var incSysId = inc.insert();
    if (!incSysId) throw new Error('Failed to create incident.');

   // 6) 添付コピー
  (function () {
      var attId = (vaVars.attachmentId || '').toString();
      if (!attId && resp && resp.request && resp.request.attachmentIds && resp.request.attachmentIds.length > 0) {
        attId = String(resp.request.attachmentIds[0]);
      }
      if (!attId) return; // 何も無ければ終了
       try {
        var sa  = new GlideSysAttachment();
        var aGr = new GlideRecord('sys_attachment');
         if (!aGr.get(attId)) return;
          var bytes = sa.getBytes(attId);
         if (!bytes || bytes.length === 0) return;
          var name = aGr.getValue('file_name')   || 'lens_image';
          var mime = aGr.getValue('content_type')|| 'application/octet-stream';
          var incGr = new GlideRecord('incident');
         if (incGr.get(incSysId)) sa.write(incGr, name, mime, new Packages.java.io.ByteArrayInputStream(bytes));
       } catch (ignore) {}
    })(); 

    // 7) 呼び出し元へ返却
    inc.get(incSysId);
    vaVars.createdIncidentSysId   = incSysId;
    vaVars.createdIncidentNumber  = inc.number.toString();
    vaVars.createdIncidentUrl     = gs.getProperty('glide.servlet.uri') + inc.getLink(true);
    vaVars.createdIncidentAttachCount = String(after);   // テスト確認用不要なら削除
    vaVars.createIncidentStatus   = 'success';

  } catch (e) {
    vaVars.createIncidentStatus = 'error';
    vaVars.createIncidentError  = e.message;
  }
})();

ここまで準備できたら、TopicとTopick BlockをPublishして準備完了です。

コピーしたTopicを選択しAgent Chatを開始します。
image.png

分析結果が出た後、User inputに追加した処理が出るので、Create Incidentを選択して送信します。
image.png

image.png

本当はこの後も少しTopic Blockに処理を増やして作成したインシデントレコードのリンクを返すなどしたいのですが、とりあえずレコードができる確認が先にしたかったので今回は割愛させてください)

ということでいつの間にかCreateされた感が強いですが、無事レコードが作成できました
(Chatにアップした画像もレコードの添付ファイルとなるようにしています)
image.png

まとめ・感想

どうにかしてLens for Mobileを実用するにはこんな感じか?という動きを作れたと思います。
最初の設定は簡単だったんですが、やはり実際に業務で使うとなるともう一声欲しい感があり、とりあえず無理やり処理をつなげて体感だけでもすることができました。

Virtual Agentに詳しい方であれば、処理のつなぎ方などもう少し工夫してスマートにできるのかな?と考えたりしてます。(OOTBでCreate Incidentのトピックあるからそこに処理飛ばせるよーとかである程度そういった部分の処理も流用出来たり?(想像で言ってます))

画像を(スマホで)サクッと撮ってその場で分析結果が出る。ここまでは生成AIやAI-OCRと一緒ですが、ServiceNowインスタンス内で業務の情報に対してそれができるし、なにより項目マッピングできるというのがLensの良い部分かなと思ってるので、Mobile(スマホ)で撮って送信するという感覚やスピード感をにもそのメリットを載せられると良いなーと。引き続き情報ウォッチしてネタにしていこうと思います。

それでは!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?