TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

936

こんにちは(=゚ω゚)ノ KINTOテクノロジーズで採用担当をしている たけの ひかる @t_hikarutaaaan です! KTC歴はもう少しで3年ですが、人事としてはまだ1年目のぴよぴよです。 今回はそんなぴよぴよ人事の私が 初めてイベントの PM をして実践したこと、学んだことを共有したいと思います! イベント概要 イベント名: プロダクトヒストリーカンファレンス 主催:株式会社 YOUTRUST 実施日:2024 年 11 月 30 日(土)10:00-20:00 場所:TODA HALL & CONFERENCE TOKYO @ card はじまり 開催日から3ヶ月切ったタイミングでKTCがゴールドスポンサーとして協賛することが決定! 「よし、決裁とれたぜ!!」って安心し… それと同時に「あれ? なにからやるんだ…?」ってなっていた私… そんな私に 神の手 が °˖✧◝(⁰▿⁰)◜✧˖° 技術広報Gのマネージャーである きんちゃん が他イベントでのSlackチャンネルを共有してくださり、 「こうゆう動きをしたらいいんだよ!」とアドバイスをくださいました…( ;∀;) 実践で吸収するタイプの私にとって、一番わかりやすい情報を提供してくれました。感謝です。 次にしたこと 「よし、ほなSlackチャンネル見ていこか」 … (ㆆωㆆ)ジー 「うーん、なんかわからんけど、まずは関係者集めてMTGや!!!」 MTG セット して 社内の Confluence を漁る それっぽい 議事録 を発見 コピー! ! ![Confluence のキャプチャ](/assets/blog/authors/takeno/prhs/prhs1.png =600x) 過去資料をコピー! なんだかんだで 社内の Confluence と過去のイベント関連の Slack チャンネル を漁ることで大体のことが解決できました! ! 改めて振り返ってみても、 ナレッジを溜めやすい環境が整っているんだな~ と感心と感謝の気持ちが沢山です…タスカッタ… いざ本番当日 前日にブースの準備は終えていたので 09:30 に会場入り 。 ![ブース準備の様子](/assets/blog/authors/takeno/prhs/prhs2.png =600x) 簡単なセッティングを終えたブース 少し落ち着いたところで開場時間が近づき…と思ったら… 「協賛企業みんなで円陣を組みましょう!!」 と Σ(・ω・ノ)ノ! しっかり両サイドの方と肩を組ませて頂きました。 さて、イベントスタート! 徐々に人も増えてきて、 お昼時にはブースに人だかり! ![ブースの様子](/assets/blog/authors/takeno/prhs/prhs3.png =600x) たくさんの方が興味を持ってくださいました! 本当にずーーーーーっと喋ってました! ! ブースでアンケートを実施していたのですが、予想より KTC のことを知っている方が多くて とても嬉しかったです。 一番多かった反応: 「色んなイベントでお見かけしました!!」 コツコツと沢山のイベントに参加してきた社員みんなの力を再認識しました! ! ![おなじみKTCのくもびぃ](/assets/blog/authors/takeno/prhs/prhs4.jpg =600x) KTCのマスコット「くもびぃ」も活躍中 景山さん登壇 今回は 取締役副社長の景山均 と 株式会社Luup CTO 岡田直道氏 によるクロストークを実施! 「テクノロジーの活用でモビリティの新常識をつくる 2 社が描く未来」をテーマに 創業・設立時の想い 各フェーズにおける開発組織の変遷 制約の多いプロダクト開発に取り組む難しさと面白さ などなど、両社の挑戦についてざっくばらんにお話いただきました。 ![登壇の様子](/assets/blog/authors/takeno/prhs/prhs5.jpg =600x) クロストーク 学んだこと 運営MTGはアジェンダ決めて、必要なタイミングで実施が良い 週次固定の定例スタイルも試してみると良いかも? くもびぃ人気 やっぱり可愛いは最強だった くもびぃは KINTO 公式マスコットキャラクターです @ card 社内にナレッジが溜まっている環境が強すぎる Slack のオープンチャンネル文化のおかげで探しやすい 経験値高い社員が集まりすぎ 後夜祭を予定していてよかった! イベント当日だけで繋がりを終わらせず、次に繋げやすい。 後夜祭には25~30名の方に参加頂きました! ![後夜祭の様子](/assets/blog/authors/takeno/prhs/prhs6.jpg =600x) 後夜祭には他社の方も参加してくださいました X Mile さん @xmile_product 、 アスエネ さん @asuene_inc_ 、 note さん @note_corp 、ありがとうございました!! 次のイベントに活かしたいこと ブースに来た人に “どんな体験をしてほしいのか” をもっと考えたい ブースのセッティングやノベルティのアレンジに活かしたい 実際にお話した方限定で選考ステップをスキップできるような“express card”を用意したい キャリアSNSであるYOUTRUSTさんのイベントでもあることから転職意欲がある人も多かった 優秀な方を確実に選考に乗ってもらえるようなフローを作っても良かったかも KPI を立てたい 振り返って ビビりな私は 「何が起きるかわかんない…!」 とドキドキしながらスタートしましたが、 結果、 「なんとかなったーーー!」 と大きな問題なく終了できたのは、 様々な方面から先回りして「これ決めたほうがいいんじゃない?」「これどうなってる?」と誘導してくれたり 準備だけでなく当日も臨機応変に対応してくれた運営メンバーのおかげでした!! 本当にありがとうございました!!! ![集合写真](/assets/blog/authors/takeno/prhs/prhs7.jpg =600x) みんなでくもびぃ着用!
アバター
この記事は 技術広報カレンダー2024 の23日目の記事です🎅🎄 はじめに こんにちは!リナ( @chimrindayo )です。 KINTOテクノロジーズで、エンジニアとして モビリティマーケット の開発運用と技術広報を兼務しています。 さて、KINTOテクノロジーズは2022年7月にテックブログを開設し、執筆者及び読者のみなさんのおかげで今日までテックブログが運営できています。いつもありがとうございます! 中でもアドベントカレンダーは、テックブログの一大イベントです🎄 そんな一大イベントで、今年はついに シリーズ4まで計100記事 を投稿できました👏(すごい) こうして100記事リリースできるようになるまで、技術広報のメンバーはさまざまな工夫に日々取り組んできました。今回はアドベントカレンダーを100記事リリースできるまでの軌跡と工夫の一部をご紹介したいと思います。 KINTOテクノロジーズ アドベントカレンダーの軌跡 KINTOテクノロジーズのアドベントカレンダーは、有志のメンバーによってテックブログ開設前の2021年に開始しました。そして翌年、2022年4月にテックブログ運用プロジェクト(現:技術広報グループ)という有志のチームが発足し、2022年7月にテックブログを開設しています。 テックブログ開設以降は毎年欠かさずにアドベントカレンダーを投稿し、25記事ずつ投稿数を増やしています⤴️ 年 URL 投稿数 2021 KINTO Technologies - トヨタ車のサブスク「KINTO」開発中! Advent Calendar 2021 24 2022 4月:テックブログ運用チーム発足 7月:テックブログ開設🎉 - 2022 KINTOテクノロジーズ Advent Calendar 2022 KINTOテクノロジーズ グループ紹介 Advent Calendar 2022 50 2023 KINTOテクノロジーズ Advent Calendar 2023 75 2024 KINTOテクノロジーズ Advent Calendar 2024 100 では私たちが記事を増やすためにどんな工夫に取り組んできたのか、各年ごとにふりかえっていきます。 テックブログ開設初期〜継続期で各フェーズに合わせて工夫を凝らしているため、これからテックブログを開設する or したい方、現在テックブログを運用しているが執筆者が増えなくて悩んでいる方のご参考になれば嬉しいです🙌 2022年 アドベントカレンダーの工夫 まずはじめに、テックブログ開設初期の工夫点です。 ふりかえってみると開設初期は、テックブログを執筆してもらうために 各個人にアプローチ をしていたのが大きな特徴だと思います。では具体的な内容をご紹介します。 執筆者への感謝のスタンスをルールにする まずはじめにやったことが「執筆者への感謝」をレビュアーのスタンスとしてルールにすることです。 このルールは 中西さん を中心に有志のメンバーで決めていきました。 そして、レビュアーになり得るマネージャーやリーダーに全社会議や1on1などの場で依頼したり、テックブログのPRテンプレートに書くことで執筆者に感謝するマインドを伝え続けています。 テックブログのPRテンプレートから抜粋 今でも執筆に感謝するということは、技術広報チーム全員が大切にしています。 全てのマネージャーに記事を書いてもらう 次にやったことは、各部署のマネージャーに記事を書いてもらうことです。 テックブログ開設初期は、「テックブログって何?どうしてやるの?」と思っている社内のメンバーも少なくありませんでした。よって、まずは各グループをリードしているマネージャー1人1人にテックブログの必要性を理解してもらうこと、そして全社にテックブログを広めるためには上位レイヤーの人から執筆する必要があると考え、マネージャーのみなさんと30分ほどのミーティングをしたうえで実現したのが「KINTOテクノロジーズ グループ紹介」です。 https://qiita.com/advent-calendar/2022/kinto-technologies-introduction 「社員・オフィスの様子をみんなに知ってもらう」をテーマに、各グループのマネージャーおよびリーダがアドベントカレンダーを執筆しました。 クリスマスには副社長の景山さんが「2022年振り返り&2023年展望」と言う記事で締めくくっています🎅 2022年以降クリスマスは景山さんの記事が毎年恒例になり、採用候補者の方や社員がほぼ全員目を通しています。 KINTOテクノロジーズが どのような組織で この1年どんなことをやってきたか 今後1年どんなことをやっていくのか がこの記事に凝縮されていて、組織の道しるべとなるような記事です。 景山さんが年に1度力を入れて執筆しています。 振り返り&展望の記事はこちら👇 https://blog.kinto-technologies.com/posts/2022-12-25-LookBack2022/ 記事の構成を一緒に考える 主に記事の執筆意欲はあるが技術記事を執筆した経験がなく、何を書いたらいいか相談したい人向けに30分程度で記事の構成を一緒に考えるミーティングを実施しています。 技術広報のメンバーが執筆者にインタビューする形式で 今までどんな業務をしていたのか 各プロジェクトの失敗/成功談と改善策 技術スタック などをヒアリングして、技術広報のメンバーがインタビュー内容を聞きながらその場で記事の構成を組み立てていきます。一通りインタビューが終わったら、作成した記事の構成をその場で執筆者の方に見ていただきます。すると、ほとんどの人に「思ったより簡単書けるかも!」と言っていただけます。 2022年のテックブログを開設した当時は、この取り組みを執筆者全員に行っていました。 (今考えたら力技すぎる...!ですが全員とコミュニケーションを取るこの力技こそが、私たちKINTOテクノロジーズの技術広報の強みであると考えています。よくこの話を他社の技術広報の方に共有すると「素晴らしい取り組み」と評価をいただけることが多いです。) 全員に行っていた理由はいくつかあります。 主な理由は、 テックブログの必要性をみんなに理解してもらいたい テックブログを書くことのハードルを下げたい 誰が何の業務を担当しているのか知りたい 気軽になんでも相談してもらえる関係を築く などです。 しかし、最も大事だと考えているのが、みなさんの 業務に価値があると再認識 してもらうことです。 実際に当時みなさんと対面でミーティングをした時に「書いてもいいけど、自分の業務は普通のことだからブログにするほどじゃないよ」と言う人が多くいました。 いやいや普通でいいんです! 絶対に同じように悩んでいる人が世界に1人か2人いて、その人に届けばいいんです。 誰か1人に届けば、誰か1人が面白いと思えば、十分にあなたの業務をテックブログ発信する価値があります。 だから一緒に執筆してみましょう!! というのを執筆者1人1人に伝え続けました。 この考えは現在でも技術広報チームが大事にしており、先日リリースされたブログでもご紹介しています👇 https://blog.kinto-technologies.com/posts/2024-12-11-gekishin/ このように記事の構成を一緒に考えるだけでなく、執筆者がこれから書こうとしている記事の内容に自信を持ってもらうことが重要であると考えています。 2023年 アドベントカレンダーの工夫 テックブログの開設初期は、1人1人と会話することで「まずはテックブログを書いてもらう」ための工夫を中心に取り組んできました。 次に2023年はいかにテックブログを 継続 して書いてもらうかに着目し、さらに執筆者を増やすための取り組みを実践しています。 インプットを支援する 2023年まず技術広報のメンバーで実施したことは、 Udemy Business の導入支援です。 なぜ導入を支援したかというと、そもそもテックブログへのアウトプットは知識やスキルのインプットがなければできないと考えているためです。したがって、Udemyのアカウント登録条件を「テックブログを書くこと」にしています。 実際にUdemyを受講して記事を書いているメンバーがいます👇 https://blog.kinto-technologies.com/posts/2024-08-30-udemy-kotlin-coroutines-and-flow/ またその後2024年にUdemyの導入支援以外に技術広報で知識のインプットからアウトプットまで包括的にサポートできるように「学びの道の駅」というチームが発足しています。 学びの道の駅チーム発足の経緯はこちら👇 https://blog.kinto-technologies.com/posts/2024-04-23_%E5%AD%A6%E3%81%B3%E3%81%AE%E9%81%93%E3%81%AE%E9%A7%85%E3%81%AF%E3%81%98%E3%82%81%E3%81%BE%E3%81%97%E3%81%9F/ 入社エントリを書いてもらう そして2023年10月から入社エントリを開始しました。 これから入社する人に会社の雰囲気をわかってもらえたら嬉しい テックブログへのアウトプットへの抵抗がなくなってほしい 入社同期の横の繋がりを作れたら嬉しい という思いから、入社エントリの記入をお願いしています。 入社時に誰もがテックブログを書くことで、テックブログの書き方や書くこと自体のハードルを下げること、そしてテックブログを書くことが当たり前になることを願っています。 https://blog.kinto-technologies.com/posts/2024-01-01-newcomers-introduction/ また、この入社エントリは入社メンバー1人1人に執筆してもらうとして技術広報メンバーで企画を進めていたのですが、10月入社の Ryommさん が「共同執筆したい!」と提案してくれたのをきっかけに1記事を共同で執筆する流れができました。 入社エントリの例のように技術広報グループのメンバーだけで企画を決めて進行するだけでなく、社員全員でアウトプットするカルチャーを創りあげています。 チーム単位で執筆を依頼する 次に、アドベントカレンダーをプロジェクト・部署単位のチームで執筆してもらう企画を実践しました。 5グループ・5名ずつ、計25日分執筆しています。 新車サブスクサイトのリニューアル アジャイル開発 KINTO FACTORY QA Diversity & Inclusion テックブログを日頃業務をともにしているチームのメンバーみんなで書くことによって、執筆者がテックブログのレビュー依頼や執筆の相談がチーム内でよりやりやすくだろうと考えたためです。 そしてチームのメンバーと一緒に、また共通のトピックで記事を執筆したことは、テックブログは読み手である社内のメンバーにとっては身近なコンテンツになるため、テックブログを読むきっかけになったり、SNSでブログを共有するきっかけになると考えました。 実際の企画はこちらのブログで紹介しています👇 https://blog.kinto-technologies.com/posts/2023-11-20-advent-calendar/#2.-%E8%A8%98%E4%BA%8B%E3%83%AA%E3%83%AC%E3%83%BC 2024年 アドベントカレンダーの工夫 最後に今年実践した工夫を紹介します。2024年はさらなる テックブログの拡張と品質向上 をテーマに取り組んできました。 テックブログの勉強会を開催する 2024年8月に技術広報のメンバーである p2skさん による社内向けのテックブログの勉強会を実施しました。 「テックブログの振り返りと発信力向上のためのベストプラクティス」というテーマで、KINTOテクノロジーズのテックブログ全記事を読んだ上で、Goodポイントや記事をよりよくするためのアイディアを共有しています。 社内の勉強会参加者からは「モチベーションが上がった!」「記事が書いてみたくなった」「アドベントカレンダー頑張ろう!」などの声があがり大変好評でした。 勉強会の内容は、非常に濃く本記事では割愛します。 企画運営を公募する 例年は技術広報のメンバーでアドベントカレンダーを企画・リードしていたのですが、今年は有志のメンバーを募集しました。狙いは、技術広報グループ以外の新しいメンバーに運営に入ってもらうことで、技術広報が今までアプローチできていなかったメンバーにリーチすることです。 ![](/assets/blog/authors/rina.k/100article/member.png =600x) 最大5名ほど集まったら嬉しいなと思っていたのですが、手を挙げてくれたのは1名でした。 その手を挙げてくれた1名が naka_shimaさん です。 naka_shimaさんが挙手してくれたことがきっかけで後述の部署単位でのシリーズの追加に繋がったと思います👏 部署単位でシリーズを埋める 2024年はグループ(部署)単位でシリーズを埋めました。 シリーズというのは、1~25日のアドベントカレンダー全日程 計25記事投稿することを指しています。 対象となったグループは、運営として挙手してくれたnaka_shimaさんが所属するモバイルグループと技術広報グループです。 モバイルグループ 技術広報グループ まずモバイルグループが選出された理由は、今年最も積極的に情報発信を行なっていたグループだからです。iOSDC2024やDroid Kaigiなどのスポンサーイベントの企画運営はもちろん、日頃のテックブログの発信も自発的に行なっていたのが特に目立っていたグループです。そこで「もしかしたら、1~25日全部モバイルグループで埋められるんじゃない・・・?」という無茶振りのもと実現したのが、モバイルグループで1シリーズです🎉 次に技術広報グループで1シリーズ実施することになった理由は、情報発信をリードするチーム自身が積極的にテックブログでのアウトプットを行う姿を見せることが重要だと考えたからです。 こうして ランダムテーマ(有志のメンバーが自由に執筆できる) モバイルグループ 技術広報グループ 翻訳(ローカライゼーションチームによる英日翻訳記事) の計100本をリリースすることができたのです。 今後の展望 テックブログを開設した2022年から2024年現在に至るまでの3年間の工夫をアドベントカレンダーを通してご紹介しました。みなさんのご協力とこれまでの工夫が2024年のアドベントカレンダー100本リリースに繋がっていると思います! 2025年は 執筆のしやすさ 執筆のモチベーション維持 を中心に工夫に取り組んでいきたいと思います。 2024年もありがとうございました☺️
アバター
こんにちは! KINTOテクノロジーズ(以下、KTC)の生成AI活用PJTで生成AIエバンジェリストをしている和田( @cognac_n )です。 さて、KTCでは、様々なシーンでの生成AI活用が進んでいます。例えば @ card @ card @ card :::details その他、生成AI関連のKTCテックブログ @ card @ card @ card @ card @ card ::: エンジニアも非エンジニアも、自分のロールや業務に合わせた生成AI活用をしていますね。生成AI活用PJTはこのように「だれもが当たり前に生成AIを活用している企業」を目指して活動をしてきました。 今回はその取り組みについてご紹介します。 1. はじめに、生成AI活用PJTの紹介 社内の生成AI活用を促進するため、2024年1月に立ち上げられた組織です。 生成AI活用PJTは現在、主に3つの機能を持っています。 PJTが持つ、3つの重点機能 生成AI活用PJTの機能 これらは独立した機能ではなく 良いアイデアを生み出す 実現の目処をつける 実装してデリバリーする 事例展開 という、生成AIがあらゆるシーンで活用されるためのサイクルを加速させることを目的としています。 生成AI活用PJTの機能とサイクル 今回はその中でも、 教育・研修 を中心に取り組みの紹介をしていきます。 2. 教育・研修体系の基礎となる考え方 「だれもが当たり前に生成AIを活用している企業」を具体化するために、3つの考え方を採用しています。 生成AIは特定のスペシャリストだけのものではありません 各自の役割に応じた「最適な活用レベル」があります 基礎から専門まで、段階的な学習を重視します 研修体系の基礎となる考え方 KTCでは複数の講師が様々なトピックについて研修をしています。また、受講者にはエンジニアも非エンジニアも、多様な業務に関わる人が含まれます。そのような状況下でもブレのない高品質な研修を提供するために、基礎的な考え方を共通認識として持つに至りました。 最初からこの考え方が固まっていたわけではなく、社内からのフィードバックをもとに改善を繰り返す中で自然と生まれた考え方です。 3. 実施している研修体系 3つの考え方をベースとして、段階的かつ体系的な研修プログラムを展開しています。 研修名 想定受講者 内容 初級 全社員 生成AIやプロンプトエンジニアリングの基礎知識。全ての基本となる最初の一歩 事例紹介 全社員 社内外の活用事例紹介。良い事例を取り込み、自らアレンジする力を身につける 事務生産性向上 各部から選抜(アンバサダー制) 生成AIをツールとして使いこなし、業務価値を生み出す。生成AIの活用を前提とした業務プロセスの改革を目指す。社内の生成AI活用推進者・伝道師となる ジェネラリスト システム開発関係者 生成AIを用いたシステム開発の勘所を学ぶ。技術の目利きや、価値の創出/検証力を身につける エンジニアリング 実装を行うエンジニア 生成AIを用いたシステム開発の実装力を身につける。価値を実現するための具体的知識と経験を得る それぞれの道のりにおいて、目指すべき生成AI活用のレベルを独自に定義しています。 4. 生まれ始めている価値 エンジニアの変化 既存システムへの生成AI機能の追加提案 生成AIを活用した新規サービスの企画提案 生成AIを活用した業務効率化ツールの自主的な開発 GitHub Copilotなどの生成AIツールの高度な活用 非エンジニアの変化 日常業務における積極的な生成AI活用 生成AIの支援によるテクニカルコミュニケーションの向上 簡易的なツール開発への挑戦 冒頭にブログを紹介した通り、研修を受けた社員が、それぞれの役割や業務の中で生成AIの価値を発揮し始めています。日常業務、コミュニケーション、システム開発・・・ありとあらゆるシーンで生成AIによる効率化、価値の向上が行われています。 我々に届く相談も「何ができるかわからない」ようなものから「やってみた!さらに良くするにはどうしたらいい?」「きっとこんなことができると思うので協力してほしい」と変化してきています。生成AIリテラシーが身についているからこそ恐れずに「まず試す」ことができ、「こんなことができそう(そしてそれは価値がある)」というセンスが身につきます。 5. 今後の展望 生成AI技術は日進月歩で進化しています。「当たり前の活用」が実現し始めているKTC/KINTOですが、この「当たり前」の水準にゴールはありません。 「だれもが当たり前に生成AIを活用している企業を目指して」 これからも取り組みを続けていきます! We Are Hiring! KINTOテクノロジーズでは、事業における生成AIの活用を推進する仲間を探しています。まずは気軽にカジュアル面談からの対応も可能です。少しでも興味のある方は以下のリンク、または XのDM などからご連絡ください。お待ちしております!! @ card ここまでお読みいただき、ありがとうございました!
アバター
Introduction Hello. I am Okita from the Mobile App Development Group. I'm involved in developing a smartphone app for a specific service project. Although I'm working with the Tokyo team members, I'm primarily based at the Osaka Tech Lab. So, let me introduce you to the Osaka Tech Lab! History of Osaka Tech Lab Born in April 2022. The Osaka Tech Lab was established with the aim of strengthening our capabilities as an IT engineering company and broadening our reach, including recruiting talented engineers. Believe it or not...I started it by myself! I had the whole floor to myself at the start. Our company consists of four locations: Nagoya Office Muromachi Office Jinbocho Office Osaka Tech Lab "Why is only the Osaka location called Osaka Tech Lab?" I’m often asked this question. The reason is that 'Osaka Tech Lab' was perceived as sounding more stylish than 'Osaka Branch' or 'Osaka Office,' and it was thought to appeal to a wider audience. In July 2022. Our first Osaka recruitment brought in four people. And from there, we've steadily grown. As of May 2023, we've expanded to 16 members! Where is the Osaka Tech Lab? The closest station is Shinsaibashi Station! Osaka Tech Lab Just a 1-minute walk from Exit 3 of Shinsaibashi Station on the Osaka Metro Midosuji Line and Nagahori Tsurumi-ryokuchi Line. *Non-smoking facility (Indoor smoking area available). What's the Atmosphere Like at the Osaka Tech Lab? Let me give you a glimpse with some photos. Entrance Bookshelf On the top shelf, you'll find a collection of miniature cars featuring the KINTO logo. There are also cactuses. Free Space The area offers both sofa and table seating. We use this space to enjoy meals, take a quick break, or use it as a corner to focus on our tasks. Snack Area This space brings together team members working on different tasks , designed to encourage casual conversations during a quick break. It’s been a big hit! Clock Our discussions led to the purchase of this clock, which we all selected together. It went something like this. “It’s nice to check the time at a glance, isn’t it?” "Oh, I was thinking the same!" "Let’s ask if we can get one installed." "I like something stylish." "Let’s make sure it’s earthquake-proof!'" This is the kind of casual conversation we have, whether in person or on Slack. We Also Have Seasonal Decorations During the Christmas season, we even set up a Christmas tree. What Kind of Team Members Work at Osaka Tech Lab? There are a total of 16 team members, representing various groups from the following departments: Project Promotion Group Owned Media & Incubation Development Group Platform Group Common Service Development Group Analysis Group Corporate IT Group Human Resources Group Mobile App Development Group There are veterans who provide steady guidance, sometimes leading, sometimes mentoring others, as well as juniors who actively contribute a variety of fresh ideas. The balance of age groups feels just right. What Makes Osaka Tech Lab So Special Arguably the people and the unknown future. The people It's what makes it a comfortable place to be. You can say it has a warm, at-home atmosphere. Even visitors on business trips often compliment us like, Even visitors on business trips often compliment us, saying, "This is my first time here, but Osaka Tech Lab has such a great atmosphere!" I've thought about why it feels so comfortable here, and I believe it's because we have team members that respects each other, even with different roles. Everyone has their own opinions, but they’re also open to listening to others and engaging in discussions to improve things. It might seem like a given, but isn’t it actually quite rare? I think this is the strength of Osaka Tech Lab! The unknown future It’s not every day that you get the chance to help set up an office! I decided to join the company because I wanted to have such an experience. Whenever I raise my hand and say, "I want to try this," the response is always "Go ahead, it’s all yours!" Of course, it is not a free-for-all, but as long as I set clear goals and outline the steps, I'm encouraged to take on new challenges. Here's one example: We are organizing information-sharing meetings at The Osaka Tech Lab with the following objectives: To enhance communication among Osaka Tech Lab members To understand what our colleagues are working on and strengthen cross-functional connections To share team and individual experiences and inspire new initiatives Here's what we're doing: A group introduction An LT (Lightning Talk) A discussion to make Osaka Tech Lab better In January 2023, as the members had grown closer, we hosted the third session, inviting managers from various groups who are usually based at other locations. Talk, talk and talk more: Osaka Tech Lab's first step in shaping the future We listened to the managers’ expectations for Osaka Tech Lab, and each team member shared their thoughts. Looking back, I think this was a significant first step. At first, these sessions started with just 10 members. But as people invited others, it expanded to include other locations. Now, the number of participants from other locations exceeds the members of Osaka Tech Lab itself. While we occasionally meet members from other locations during business trips, these information-sharing sessions have also created connections with members we normally wouldn't interact with. Osaka Tech Lab's Ambition Each of us is currently working on Tokyo-based projects, spending our days in a fast-paced, stimulating environment. One day... Someday... We hope to launch, develop, and operate a service entirely from Osaka Tech Lab. It seems many of us secretly share this dream. I'm excited for the unseen future that lies ahead! Conclusion So, what do you think? Why not join us in building the Osaka Tech Lab together toward an exciting, unknown future? We look forward to your application! KINTO Technologies Recruitment TOP page Wantedly
アバター
1. はじめに こんにちは、共通サービス開発グループの鳥居( @yu_torii )です。主にバックエンド/フロントエンド領域を担当しています。 KINTO 会員プラットフォーム開発チームでフロントエンドエンジニアを務めながら、社内での生成 AI 活用にも携わっています。 本記事では、Slack 上で LLM を活用する社内チャットボットの RAG や Slack リアクションを活用した翻訳機能を紹介します。 社内向け生成AIツールは、社内での生成 AI 活用を目指して開発された Slack チャットボットです。 フロントエンド開発を省略して素早く社内に展開し、社員が普段使う Slack 上で生成 AI を自然に利用できる環境を目標としています。 頼れる存在であるように、また社内の業務効率の向上や情報共有の円滑化のための頼れるパートナーでありたいという願いが込められています。 ちなみに、バナーのキャラクターはクリエイティブグループの方が KINTO テクノジーズ全体会議(KTC 超本部会)に際してサプライズで描いてくれたものです。ありがとうございます! この取り組みは、生成 AI 活用プロジェクトを推進する和田さん( @cognac_n )との協力により進められました。RAG パイプラインの導入、ローカル開発環境の整備、Slack 絵文字リアクションを活用した翻訳・要約機能など、多角的な改善を行い、社内での生成 AI 利活用を推進しています。 :::details 和田さんの記事はこちら @ card @ card @ card ::: なお、RAG や生成 AI 技術そのものの詳細解説は省き、実装や機能追加の過程を中心にまとめています。 また、社内向け生成AIツールの LLM は Azure OpenAI を利用しています。 この記事で得られること Slack Botによる生成AIを活用したチャット機能の利用方法 Slack と LLM を組み合わせ、絵文字リアクションや自然なメッセージ投稿で翻訳・要約を呼び出すチャットボット実装例を紹介 HTMLサニタイズを含むConfluenceデータ取得の工夫 Confluence ドキュメントを Go で取得し、HTML サニタイズにより要約・Embedding に適したテキストを整える方法 FAISS とS3を使った簡易的なRAGパイプライン導入 FAISS インデックスと S3 を活用した簡易 RAG パイプライン導入の手順と注意点を解説 応答速度はあまり早くないものの、低コストで簡易的な RAG を組み込む際の実装ステップや注意点をまとめます。 :::message 本記事は、執筆時点での実装状況をもとにしています。改善点が多分に含まれている点をご了承ください。 社内向けのツールであるため、プロダクションレベルのパフォーマンスを必要とした実装ではありません。 AWS SAM でのデプロイや Step Functions のパイプライン構築、GitHub Actions による CI/CD など、開発環境の整備については、本記事では詳細を省略しています。 ::: :::details 目次(展開) 1. はじめに この記事で得られること 2. 全体アーキテクチャの概要 3. Slack Botによる生成AIを活用したチャット機能 「チャット機能」と「リアクション機能」 チャット機能:利用シナリオ リアクション機能:利用シナリオ この構成のメリット 3.5 実際の利用例 3章まとめ 4. 実装方針と内部設計 全体的な処理フロー 条件分岐による機能判定 サニタイズの役割 RAG利用をConfluence参照に限定 拡張性・保守性への考慮 5. コード例と設定ファイルの紹介 5.1 [Go] Slackイベント受信と解析 5.2 [Go] HTMLテキストのサニタイズ 5.3 [Python] LLM問い合わせの具体例 5.4 [Python] RAG検索呼び出し例 5.5 [Python] EmbeddingとFAISSインデックス化 ::: 2. 全体アーキテクチャの概要 ここでは、社内向け生成AIツールが生成 AI による回答を返すまでの流れを、「ユーザとの生成 AI チャット機能」と「Confluence ドキュメントをインデックス化する処理」という 2 つの視点で整理します。 ユーザとの生成AIチャット機能の処理 Slackで社内向け生成AIツールを呼び出す 社内向け生成AIツールの呼び出し方は、チャットでの呼び出しと、リアクション絵文字での呼び出しの 2 通りがあります。 チャットでの呼び出しは、チャンネルでメンションを付けて投稿するか、ダイレクトメッセージで直接投稿することで、生成 AI による回答をリクエストできます。 リアクションでの呼び出しは、翻訳・要約用の絵文字リアクションを付けることで、そのメッセージの翻訳や要約をリクエストできます。 Go Slack Bot Lambda Slack イベントを受け取る Bot は Go で実装し、Lambda 上で動作します。 ユーザからの質問やリアクションを受け取り、リクエストの種類に応じて処理を振り分けます。 LLM へのリクエストや、Embedding 済みデータの参照など Azure OpenAI を利用する場合は、ここでリクエストを生成し、Python Lambda へ送信します。 Python LambdaでのLLMへのリクエスト・RAG参照 Python Lambda は機能ごとに分割し、LLM 問い合わせや RAG 参照などを担当します。 Go Lambda からのリクエストを受け取り、LLM への問い合わせや RAG による回答生成を行います。 Slackへの回答返送 生成された回答は、Go Slack Bot Lambda を経由して Slack へ返送します。 絵文字リアクションで呼び出す翻訳・要約機能も、このフローに組み込まれています。 :::details アーキテクチャ図(ユーザからの呼び出しに対応する処理) flowchart LR subgraph "ユーザからの呼び出しに対応する処理" A["User(Slack)"] -->|"質問・絵文字"| B["Go Slack Bot(Lambda)"] B -->|"リクエスト"| C["Python Lambda RAG/LLM"] C -->|"回答"| B B -->|"回答"| A end ::: Confluence ドキュメントをインデックス化する処理 RAG パイプラインを利用するには、Confluence ドキュメントを要約・Embedding しやすい状態に整える必要があります。これらの前処理は StepFunctions でワークフロー化し、定期的かつ自動的に実行しています。 ドキュメント取得・HTMLサニタイズ(Go実装) Go で実装した Lambda が Confluence API からドキュメントを取得し、HTML タグを整理してテキストを扱いやすい状態にします。 サニタイズ済みテキストは JSON として出力します。 要約処理(Go + Python Lambda呼び出し) 要約処理の目的は、テキストを Embedding や RAG で扱いやすい範囲に絞ることです。 Go 実装の Lambda が Azure OpenAI Chat API のリクエスト処理を行う Python Lambda を呼び出し、テキストを短く整え、再び JSON 化します。 FAISSインデックス化とS3保存(Indexer Lambda) 要約後のテキストを Embedding し、FAISS インデックスを生成する Indexer Lambda がインデックスやメタ情報を S3 へ格納します。 これにより、問い合わせ発生時にはインデックス済みデータを即座に参照でき、RAG パイプラインがスムーズに稼働します。 :::details アーキテクチャ図(Confluence ドキュメントのインデックス化フロー) flowchart LR subgraph "インデックス化準備処理 StepFunctions" D["Go Lambda(ドキュメント取得/HTMLサニタイズ)"] D --> E["Go Lambda(要約/Python呼出)"] E --> F["Indexer Lambda(Embedding/FAISS/S3)"] end ::: こうした事前処理と問い合わせ時の処理が組み合わさることで、社内向け生成AIツールは Slack 上の簡易な操作で社内特有のナレッジを反映した生成 AI 回答を返せるようになっています。 以降の章では、これらの要素についてさらに詳細を見ていきます。 3. Slack Botによる生成AIを活用したチャット機能 前章でアーキテクチャの全体像を示しましたが、本章では、ユーザが Slack 上で自然な操作を行うだけで得られる機能について、その使い方や有用性に焦点を当てます。 ここでは「何ができるか」「どんなシナリオで役立つか」という利用イメージを示し、実装詳細は次章以降で体系的に紹介します。 「チャット機能」と「リアクション機能」 社内向け生成AIツールは、Slack 上での自然な操作を原動力に、多様な生成 AI 機能を提供します。 チャット機能 : 質問を投稿したり、ファイルや画像、外部リンクを貼ったり、特定の形式でメッセージを送ることで、LLM への問い合わせや(特定の場合)RAG 検索が行われ、必要な回答が得られます。 Slack という日常的なツールに機能を統合することで、ユーザは新たな環境やコマンドを学ぶ必要なく、生成 AI を自然に日常業務へ取り込むことができます。 リアクション機能 : 特定の絵文字リアクションをメッセージに付与するだけで翻訳や社内向け生成AIツール発言の削除などをトリガーでき、コマンドレスでの追加操作が可能になります。 チャット機能:利用シナリオ 基本的な質問・回答 普通に質問を投稿するだけで、LLM を通じて回答を取得できます。 利用シーン例: 「このプロジェクトの手順は?」と尋ねれば即座に回答が得られ、スレッド内の履歴や発言者情報を考慮して文脈に沿った応答が可能です。 ファイル・画像・外部リンク参照 ファイルを貼り付けて「要約して」と依頼すれば、ファイル内容を要約。 画像を添付すれば文字起こしや画像内容を踏まえた回答が可能。 外部リンクを貼れば、そのページ内容を整理して LLM 回答に生かせます。 利用シーン例: 会議メモが入ったテキストファイルを「要約して」で短いサマリを得る、画像内文字情報を抜き出す、外部記事リンクを要約するといった、手間を省く活用ができます。 Confluenceページ参照(RAG活用) :confluence: index:Index名 で、社内でのルールや申請方法等のドキュメントが含まれた Confluence ページを RAG 検索。 利用シーン例: 社内申請手順や内部ルールが Confluence にまとまっている場合、必要な情報を即座に引き出し、一般的な回答以上に社内事情に即したアドバイスが得られます。 利用シーン例: プロジェクト独自の手順書や設定値を検索して、その内容を回答に反映。検索しづらい情報を簡単に引き出せます。 リアクション機能:利用シナリオ 特定の絵文字リアクションを付けることで、さらに直感的な機能呼び出しが可能です。 翻訳 : 翻訳用絵文字を付ければ、そのメッセージを指定言語へ翻訳可能です。言語の壁を低くし、コミュニケーションを円滑化します。 削除 不要になった社内向け生成AIツール発言を絵文字 1 つで簡易に削除し、チャンネルを整理できます。 この構成のメリット 自然な操作で活用可能 : ユーザは慣れた Slack 操作(メッセージ投稿、絵文字付与など)だけで生成 AI 機能を使え、学習コストを低減できます。 日常ツールへの統合で生成AI定着 : Slack という普段使うコミュニケーション基盤に機能を溶け込ませることで、生成 AI を身近に利用することが可能です。またリアクション機能は文字入力をすることなく、翻訳や削除などの操作ができるため、手軽に利用できます。 拡張に対応しやすい土台 : 今後、新たなモデルや追加機能を組み込みたくなった場合も、既存のフロー(質問投稿、絵文字操作)に条件を増やすだけで拡張可能です。 3.5 実際の利用例 ここで、社内向け生成AIツールが Slack 上でどのように使われているか、実際のシナリオをいくつか紹介します。 例1:画像の認識 メッセージに画像を添付すると、社内向け生成AIツールは画像内のテキストを認識し、その内容を回答します。 ![画像の認識](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-context.png =500x) 画像の認識 例2:Confluenceドキュメントに基づく回答 :confluence: という絵文字を使うことで、Confluence ドキュメントを参照した回答を得ることができます。 ![Confluence ドキュメントに基づく回答](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-confluence-rag.png =500x) Confluenceドキュメントに基づく回答 ご覧の通り、社内向け生成AIツールはワークフローから呼び出すことも可能で、プロンプトストアのような利用も可能です。 例3:翻訳リアクションでの英訳依頼 ユーザが英訳したいメッセージに翻訳リアクションを付与 英語話者に共有したい日本語メッセージに :ai_tool_translate_to_english: といったリアクション絵文字を付けると、そのメッセージのテキストが自動で英訳されます。 ![英語翻訳リアクション](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-translation.png =500x) 英語翻訳リアクション 翻訳内容を過信しないように、自動翻訳であることを複数言語で通知することで、誤解を防ぎます。 また、機能を普及させるために、利用方法も記載しています。 英訳の他に複数言語の翻訳機能を提供しています。 3章まとめ 本章では、「どんな操作で何ができるか」というユーザ視点の使い方に注目しました。 ここで得た利用イメージを基に、次の章以降で実装内容を体系的に示し、Go 製 Bot や Python Lambda の連携方法、Slack イベントの処理や RAG 検索のしくみ、ファイル抽出やサニタイズ処理などの詳細を紹介します。 4. 実装方針と内部設計 この章では、社内向け生成AIツールが Slack のメッセージやリアクションを通じて多様な生成 AI 機能を提供するための実装方針と内部設計について説明します。 ここで紹介するのは、あくまで全体的な考え方や役割分担、拡張性への配慮に関する部分です。具体的なコードや設定ファイルは次章以降に記載します。 全体的な処理フロー Slackイベント受信(Go Lambda) Slack で行われるメッセージ投稿、ファイル添付、画像挿入、外部リンク記載、絵文字リアクションなどのイベントは、Slack API 経由で Go 実装の AWS Lambda へ通知されます。 Go 側はこれらのイベントを解析し、ユーザが求める処理(通常のチャット、翻訳、Confluence 参照など)を判定します。 Go側でのテキスト処理・サニタイズ 外部リンクやファイルからテキストを取得し、コンテキストとしてプロンプトに追加します。 外部リンクを参照する場合は table 、 ol 、 ul など意味のあるタグは残し、不要なタグだけを除去してトークンを節約します。 Python側でのLLM問い合わせ・RAG検索 必要な場合、Go 側は LLM 問い合わせ用の Python Lambda(LLM 用)か、RAG 検索用の Python Lambda(Confluence 参照用)を呼び出します。 例えば、 :confluence: が含まれていれば RAG 検索用 Lambda を呼び出し、index が指定されていなければデフォルトインデックスで検索します。 そうでなければ通常は LLM 用 Lambda へテキストを渡し、LLM への問い合わせを行います。 回答返却とSlack表示 Python 側の Lambda が生成した回答を Go 側に戻し、Go 側が Slack へ投稿します。 これにより、ユーザはコマンドを暗記する必要なく、絵文字やキーワード、ファイル添付など日常的な操作だけで高度な機能を利用できます。 条件分岐による機能判定 特定の絵文字( :confluence: など)やキーワード、ファイルや画像の有無、外部リンク存在などにより処理を振り分けます。 新機能を追加する場合は、Go 側で新たな条件を追加して、必要なら対応する Python 側の Lambda(LLM 用、RAG 用など)を呼び出すロジックを増やすことで実現できます。 サニタイズの役割 Go 側でサニタイズし、不要な HTML タグを除去することで、モデルへ渡すテキストを効率的に扱い、トークン消費を抑えます。 table、ol、ul など意味あるタグは残して情報構造を保持し、モデルにとって有用な文脈を損ねないようにしています。 RAG利用をConfluence参照に限定 RAG 検索は :confluence: 指定時のみ利用します。 これにより通常の要約や翻訳、Q&A は LLM 直接問い合わせで済み、RAG 関連ロジックは Confluence 参照時だけ発動します。 Confluence ドキュメントの Embedding 生成や FAISS インデックス更新は StepFunctions で定期的に実行し、問い合わせ時には常に最新インデックスを利用できます。 拡張性・保守性への考慮 絵文字やキーワード、ファイル・画像の有無による条件分岐は、機能を増やす際の変更箇所を少なく保ち、保守性を高めます。 Go 側でテキスト整形やサニタイズ、Python 側で LLM 問い合わせ・RAG 検索を行う役割分担も、コードの見通しを良くし、将来的なモデル切り替えや処理追加を容易にします。 次章では、これらの方針を踏まえた具体的なコードスニペットや設定ファイル例を紹介します。 5. コード例と設定ファイルの紹介 この章では、第 4 章で説明した実装方針や設計上の考え方に基づいて、実装例を要点を絞って紹介します。 本章は次のセクションで構成します。 5.1 Slack イベント受信と解析(Go 側) Slack の Events API を用いてメッセージや絵文字リアクションなどのイベントを受信・解析する方法を示します。 5.2 Go 側でサニタイズ例 外部リンク参照した際の HTML サニタイズ処理の例を示します。 5.3 Python 側 LLM 問い合わせの具体例 LLM(LLM)への問い合わせを行う Lambda のコード例を提示します。 5.4 Python 側 RAG 検索呼び出し例 Confluence 参照など RAG を使う場合の検索呼び出し例を紹介します。 5.5 Python 側 Embedding と FAISS インデックス化 Confluence ドキュメントを定期的に Embedding し、FAISS インデックスを更新する Lambda のコード例を示します。 5.1 [Go] Slackイベント受信と解析 このセクションでは、Slack の Events API を利用して、AWS Lambda 上で Go コードでイベントを受信・解析する基本的な流れを説明します。 Slack 側での設定(OAuth & Permissions、イベント購読)や、 chat.postMessage メソッド利用時に必要なスコープ( chat:write など)の確認方法にも触れ、読者が実装を始めるまでに必要な準備を明確にします。 Slack側での設定手順 App作成とApp IDの確認 : https://api.slack.com/apps で新規 App を作成します。 作成後、Basic Information ページ( https://api.slack.com/apps/APP_ID/general 、 APP_ID は App 固有の ID)で App ID( A で始まる文字列)を確認します。 この App ID は Slack App の識別子であり、後述の OAuth & Permissions や Event Subscriptions ページの URL アクセスに用いることができます。 OAuth & Permissionsでスコープ付与 : 「OAuth & Permissions」ページ( https://api.slack.com/apps/APP_ID/oauth )にアクセスし、Bot Token Scopes に必要なスコープを追加します。 例えば、チャンネルへメッセージ投稿に chat.postMessage メソッドが必要である場合、 https://api.slack.com/methods/chat.postMessage で「Required scopes」を確認すると chat:write が必要であるとわかります。 スコープ付与後、「reinstall your app」をクリックし、ワークスペースに再インストールすると、変更が反映されます。 ![Required scopes の確認](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image.png =500x) Required scopesの確認 ![Scope の設定](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-1.png =500x) Scopeの設定 Events API有効化とイベント購読 : 「Event Subscriptions」ページ( https://api.slack.com/apps/APP_ID/event-subscriptions )で Events API を有効化し、「Request URL」に後述の AWS Lambda エンドポイントを設定します。 message.channels や reaction_added など、購読するイベントを追加します。これにより、対象イベント発生時に Slack が指定 URL へ通知を送るようになります。 ![Event Subscriptions の説明](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-3.png =500x) AWS Lambda側でのイベント受信・解析 Slack 側での設定が完了すると、購読対象のイベントが発生するたびに Slack は API Gateway 経由で Lambda へ POST リクエストを送ります。 ステップ1:Slackイベントの解析 slack-go/slackevents パッケージを用いて、受信した JSON を EventsAPIEvent へ変換します。 これにより、URL 検証や CallbackEvent などイベントタイプを判別しやすくなります。 func parseSlackEvent(body string) (*slackevents.EventsAPIEvent, error) { event, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { return nil, fmt.Errorf("Slackイベントの解析に失敗しました: %w", err) } return &event, nil } @ card ステップ2:URL検証要求対応 初回に Slack は type=url_verification のイベントを送ってきます。この中の challenge をそのまま返すことで Slack は URL 有効性を確認し、その後イベントを通知してくれるようになります。 func handleURLVerification(body string) (events.APIGatewayProxyResponse, error) { var r struct { Challenge string `json:"challenge"` } if err := json.Unmarshal([]byte(body), &r); err != nil { return createErrorResponse(400, err) } return events.APIGatewayProxyResponse{ StatusCode: 200, Body: r.Challenge, }, nil } @ card ステップ3:署名検証・再試行リクエスト無視 Slack はリクエスト署名を付与し、正当性を検証できます(実装は省略)。 また、障害時などに再試行リクエストが送られる場合があり、 X-Slack-Retry-Num ヘッダで再試行を判定して同じイベントを二重処理しないようにできます。 func verifySlackRequest(body string, headers http.Header) error { // 署名検証処理(省略) return nil } func isSlackRetry(headers http.Header) bool { return headers.Get("X-Slack-Retry-Num") != "" } func createIgnoredRetryResponse() (events.APIGatewayProxyResponse, error) { responseBody, _ := json.Marshal(map[string]string{"message": "Slackの再試行を無視します"}) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, nil } ステップ4:CallbackEvent処理 CallbackEvent は実際のメッセージ投稿やリアクション追加などが含まれます。ここで、 :confluence: が含まれるか、ファイルがあるか、翻訳用絵文字が付いているかなどを判定し、5.2 以降で示すテキスト処理や Python Lambda 呼び出しへ進みます。 // handleCallbackEvent は、コールバックイベントを処理します。(5.1での説明対象) func handleCallbackEvent(ctx context.Context, isOrchestrator bool, event *slackevents.EventsAPIEvent) (events.APIGatewayProxyResponse, error) { innerEvent := event.InnerEvent switch innerEvent.Data.(type) { case *slackevents.AppMentionEvent: // AppMentionEventの場合の処理(5.2で詳細説明) case *slackevents.MessageEvent: // MessageEventの場合の処理(5.2で詳細説明) case *slackevents.ReactionAddedEvent: // ReactionAddedEventの場合の処理(5.2で詳細説明) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: http.StatusOK}, nil } ハンドラ全体のコード例 これらのステップを組み合わせて AWS Lambda ハンドラを定義します。 :::details ハンドラ全体のコード例 func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { event, err := parseSlackEvent(request.Body) if err != nil { return createErrorResponse(400, err) } if event.Type == slackevents.URLVerification { return handleURLVerification(request.Body) } headers := convertToHTTPHeader(request.Headers) err = verifySlackRequest(request.Body, headers) if err != nil { return createErrorResponse(http.StatusUnauthorized, fmt.Errorf("リクエストの検証に失敗しました: %w", err)) } if isSlackRetry(headers) { return createIgnoredRetryResponse() } if event.Type == slackevents.CallbackEvent { return handleCallbackEvent(ctx, event) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: 200}, nil } func convertToHTTPHeader(headers map[string]string) http.Header { httpHeaders := http.Header{} for key, value := range headers { httpHeaders.Set(key, value) } return httpHeaders } func createErrorResponse(statusCode int, err error) (events.APIGatewayProxyResponse, error) { responseBody, _ := json.Marshal(map[string]string{"error": err.Error()}) return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, err } ::: 5.1のまとめ このセクションで、Slack App の App ID や OAuth & Permissions でのスコープ付与方法、Event Subscriptions でのイベント購読設定を説明し、Slack イベントの受信・解析プロセス(URL Verification 対応、署名検証、再試行リクエスト無視、CallbackEvent 処理)を示しました。 次の 5.2 以降では、CallbackEvent 処理の具体例や、Go 側でのテキスト処理、Python Lambda への問い合わせ方法などを紹介します。 5.2 [Go] HTMLテキストのサニタイズ 外部リンク参照内容のサニタイズ 外部リンクから取得した HTML テキストには、 script や style など回答に不要なタグが含まれる場合があります。そのまま LLM に渡すと、トークン数が増えてモデルコストが上昇し、回答精度が下がる可能性があります。次のコードでは、 bluemonday パッケージを用いて基本的なサニタイズを行ったうえで、 table や ol 、 ul など重要なタグを残しつつ不要なタグを除去し、読みやすい形に整形します。 addNewlinesForTags 関数を活用し、特定のタグ後に改行を挿入してテキストを整えることで、モデルへの問い合わせ時に必要な情報のみを最適なフォーマットで渡すことが可能になります。 @ card func sanitizeContent(htmlContent string) string { // 基本的なサニタイズ ugcPolicy := bluemonday.UGCPolicy() sanitized := ugcPolicy.Sanitize(htmlContent) // カスタムポリシーで特定タグを許可 customPolicy := bluemonday.NewPolicy() customPolicy.AllowLists() customPolicy.AllowTables() customPolicy.AllowAttrs("href").OnElements("a") // タグごとに改行を追加して可読性向上 formattedContent := addNewlinesForTags(sanitized, "p") // カスタムポリシー適用後の最終サニタイズ finalContent := customPolicy.Sanitize(formattedContent) return finalContent } func addNewlinesForTags(htmlStr string, tags ...string) string { for _, tag := range tags { closeTag := fmt.Sprintf("</%s>", tag) htmlStr = strings.ReplaceAll(htmlStr, closeTag, closeTag+"\n") } return htmlStr } この処理により、モデルへの入力は不要なタグが取り除かれたテキストのみとなり、回答精度とコスト効率が向上します。必要な構造(テーブルや箇条書き)は保持しつつ、特定タグの終了後に改行を挿入することで、モデルが情報を理解しやすい形でコンテキストを提供できます。 5.3 [Python] LLM問い合わせの具体例 以下は、Python で LLM(たとえば Azure OpenAI)へ問い合わせる処理の例です。 OpenAIClientFactory を用いてモデルやエンドポイントを動的に切り替えられるため、複数の Lambda ハンドラ間で共通のクライアント生成処理を再利用できます。 クライアント生成処理 OpenAIClientFactory は api_type や model に応じて Azure OpenAI または OpenAI 用のクライアントを動的に生成します。 環境変数や秘密情報により API キー、エンドポイントを取得するため、モデル変更や設定変更時もコード修正が最小限で済みます。 import openai from shared.secrets import get_secret class OpenAIClientFactory: @staticmethod def create_client(region="eastus2", model="gpt-4o") -> openai.OpenAI: secret = get_secret() api_type = secret.get("openai_api_type", "azure") if api_type == "azure": return openai.AzureOpenAI( api_key=secret.get(f"azure_openai_api_key_{region}"), azure_endpoint=secret.get(f"azure_openai_endpoint_{region}"), api_version=secret.get( f"azure_openai_api_version_{region}", "2024-07-01-preview" ), ) elif api_type == "openai": return openai.OpenAI(api_key=secret.get("openai_api_key")) raise ValueError(f"Invalid api_type: {api_type}") LLM問い合わせ処理 chatCompletionHandler 関数では、HTTP リクエストとして受け取った JSON から messages や model 、 temperature などを取得し、 OpenAIClientFactory で生成したクライアントを用いて LLM に問い合わせます。 レスポンスは JSON 形式で返し、エラー発生時は共通のエラーレスポンス生成関数によって整形されたレスポンスを返します。 import json from typing import Any, Dict, List import openai from openai.types.chat import ChatCompletionMessageParam from shared.openai_client import OpenAIClientFactory def chatCompletionHandler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: request_body = json.loads(event["body"]) messages: List[ChatCompletionMessageParam] = request_body.get("messages", []) model = request_body.get("model", "gpt-4o") client = OpenAIClientFactory.create_client(model=model) temperature = request_body.get("temperature", 0.7) max_tokens = request_body.get("max_tokens", 4000) response_format = request_body.get("response_format", None) completion = client.chat.completions.create( model=model, stream=False, messages=messages, max_tokens=max_tokens, frequency_penalty=0, presence_penalty=0, temperature=temperature, response_format=response_format, ) return { "statusCode": 200, "body": json.dumps(completion.to_dict()), "headers": { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST", "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", }, } この仕組みにより、異なる Lambda ハンドラでも同様の手順で LLM 問い合わせが可能となり、モデルやエンドポイント変更にも柔軟に対応できる構成となっています。 5.4 [Python] RAG検索呼び出し例 このセクションでは、Python で RAG(Retrieval Augmented Generation)検索を行うための手順を示します。 Confluence ドキュメントなど社内のナレッジをベクトル化し、FAISS インデックスを用いて類似文書検索を行うことで、LLM 回答に特化した情報を組み込むことが可能です。 ここで注目すべき点は faiss ライブラリの取り扱いです。 faiss は非常にサイズが大きく、Lambda Layer の容量制限を超える可能性があるため、通常は EFS を利用するか、Lambda をコンテナ化する必要があります。 今回はその手間を省くため、 setup_faiss 関数により S3 から faiss 関連パッケージをダウンロード・展開し、 sys.path に動的に追加することで faiss を利用可能にしています。 FAISS とは FAISS(Facebook AI Similarity Search)は、Meta(Facebook)製の近似最近傍探索ライブラリであり、類似の画像やテキストを検索するためのインデックスを作成するツールです。 @ card setup_faiss 関数によるFAISSセットアップ FAISS パッケージを Lambda 環境で利用するために、 setup_faiss 関数では次の手順を行います。 ローカル/CI環境でfaissパッケージをビルド・アーカイブ 開発者が GitHub Actions などの CI 環境で faiss-cpu パッケージをインストールし、必要なバイナリを tar.gz 形式でまとめます。 S3へアップロード CI でビルド・アーカイブした faiss_package.tar.gz を S3 にアップロードします。 本番やステージング環境に合わせて、適切なバケットやパスへ格納することで、Lambda 実行時に S3 から動的に取得可能です。 Lambda実行時に setup_faiss で動的ロード Lambda 実行環境上では、 setup_faiss 関数が起動時に S3 から faiss_package.tar.gz をダウンロード・展開し、 sys.path に追加します。 これにより、Lambda コード内で import faiss が可能となり、Embedding 処理で作成したベクトルを高速に検索できます。 GitHub Actions でfaissパッケージをS3へアップロードする例 以下は、 faiss-cpu をインストールし、Lambda で利用できるようにパッケージ化した上で、S3 にアップロードする GitHub Actions の例です。 ここでは、GitHub Actions の Secret や Environment Variables 機能を利用することで、AWS 認証情報や S3 バケット名などをハードコーディングせずに管理しています。 @ card name: Build and Upload FAISS on: workflow_dispatch: inputs: environment: description: デプロイ環境 type: environment default: dev jobs: build-and-upload-faiss: environment: ${{ inputs.environment }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" # 必要なパッケージのインストール(faiss-cpu) - name: Install faiss-cpu run: | set -e echo "Installing faiss-cpu..." pip install faiss-cpu --no-deps # faiss のバイナリをアーカイブ - name: Archive faiss binaries run: | mkdir -p faiss_package pip install --target=faiss_package faiss-cpu tar -czvf faiss_package.tar.gz faiss_package # AWS認証情報の設定(環境に合わせてSecretsやRoleを指定) - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.CICD_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.CICD_AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 # S3 にアップロード - name: Upload faiss binaries to S3 run: | echo "Uploading faiss_package.tar.gz to S3..." aws s3 cp faiss_package.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/lambda/faiss_package.tar.gz echo "Upload complete." 上記の例では、 faiss_package.tar.gz が S3 に lambda/faiss_package.tar.gz というキーでアップロードされます。 Lambda側での動的ロード処理 ( setup_faiss 関数) setup_faiss 関数は、Lambda 実行時に S3 から faiss_package.tar.gz をダウンロードし、 /tmp ディレクトリに展開、 sys.path にパッケージパスを追加します。こうして Lambda 内で import faiss が可能になり、FAISS インデックス検索を実行できるようになります。 # setup_faiss例:S3上のfaissパッケージをダウンロードし、sys.pathに追加 import os import sys import tarfile from shared.logger import getLogger from shared.s3_client import S3Client logger = getLogger(__name__) def setup_faiss(s3_client: S3Client, s3_bucket: str) -> None: try: import faiss logger.info("faiss が既にインポートされています。") except ImportError: logger.info("faiss が見つかりません。S3からダウンロードします。") faiss_package_key = "lambda/faiss_package.tar.gz" faiss_package_path = "/tmp/faiss_package.tar.gz" faiss_extract_path = "/tmp/faiss_package" # S3からパッケージをダウンロードして展開 s3_client.download_file(bucket_name=s3_bucket, key=faiss_package_key, file_path=faiss_package_path) with tarfile.open(faiss_package_path, "r:gz") as tar: for member in tar.getmembers(): member.name = os.path.relpath(member.name, start=member.name.split("/")[0]) tar.extract(member, faiss_extract_path) sys.path.insert(0, faiss_extract_path) import faiss logger.info("faiss のインポートに成功しました。") EmbeddingsとFAISSインデックスを用いたRAG検索 search_data 関数では、S3 から取得した FAISS インデックスをローカルでロードし、クエリに合致する文書を検索します。 get_embeddings 関数によって生成される Embeddings クライアント(Azure OpenAI または OpenAI)を用いて文書をベクトル化しており、 faiss を活用した高速検索が可能です。 from typing import Any, Dict, List, Optional from langchain_community.vectorstores import FAISS from langchain_core.documents.base import Document from langchain_core.vectorstores.base import VectorStoreRetriever from shared.secrets import get_secret from shared.logger import getLogger from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings logger = getLogger(__name__) def get_embeddings(secrets: Dict[str, str]): api_type: str = secrets.get("openai_api_type", "azure") if api_type == "azure": return AzureOpenAIEmbeddings( openai_api_key=secrets.get("azure_openai_api_key_eastus2"), azure_endpoint=secrets.get("azure_openai_endpoint_eastus2"), model="text-embedding-3-large", api_version=secrets.get("azure_openai_api_version_eastus2", "2023-07-01-preview"), ) elif api_type == "openai": return OpenAIEmbeddings( openai_api_key=secrets.get("openai_api_key"), model="text-embedding-3-large", ) else: logger.error("無効なAPIタイプが指定されています。") raise ValueError("Invalid api_type") def search_data( query: str, index_folder_path: str, search_type: str = "similarity", score_threshold: Optional[float] = None, k: Optional[int] = None, fetch_k: Optional[int] = None, lambda_mult: Optional[float] = None, ) -> List[Dict]: secrets: Dict[str, str] = get_secret() embeddings = get_embeddings(secrets) db: FAISS = FAISS.load_local( folder_path=index_folder_path, embeddings=embeddings, allow_dangerous_deserialization=True, ) search_kwargs = {"k": k} if search_type == "similarity_score_threshold" and score_threshold is not None: search_kwargs["score_threshold"] = score_threshold elif search_type == "mmr": search_kwargs["fetch_k"] = fetch_k or k * 4 if lambda_mult is not None: search_kwargs["lambda_mult"] = lambda_mult retriever: VectorStoreRetriever = db.as_retriever( search_type=search_type, search_kwargs=search_kwargs, ) results: List[Document] = retriever.invoke(input=query) return [{"content": doc.page_content, "metadata": doc.metadata} for doc in results] 非同期ダウンロードとLambdaハンドラ async_handler 内で setup_faiss を実行し、 download_files で S3 から FAISS インデックスファイルを取得します。その後 search_data で RAG 検索を実行し、結果を JSON で返します。 import asyncio import json import os from shared.s3_client import S3Client from shared.logger import getLogger from shared.token_verifier import with_token_verification logger = getLogger(__name__) RESULT_NUM = 5 @with_token_verification async def async_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: env = os.getenv("ENV") s3_client = S3Client() s3_bucket = "bucket-name" setup_faiss(s3_client, s3_bucket) request_body_str = event.get("body", "{}") request_body = json.loads(request_body_str) query = request_body.get("query") index_path = request_body.get("index_path") local_index_dir = "/tmp/index_faiss" await download_files(s3_client, s3_bucket, index_path, local_index_dir) results = search_data( query, local_index_dir, search_type=request_body.get("search_type", "similarity"), score_threshold=request_body.get("score_threshold"), k=request_body.get("k", RESULT_NUM), fetch_k=request_body.get("fetch_k"), lambda_mult=request_body.get("lambda_mult"), ) return create_response(200, results) def retrieverHandler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: return asyncio.run(async_handler(event, context)) def create_response(status_code: int, body: Any) -> Dict[str, Any]: return { "statusCode": status_code, "body": json.dumps(body, ensure_ascii=False), "headers": { "Content-Type": "application/json", }, } async def download_files(s3_client: S3Client, bucket: str, key: str, file_path: str) -> None: loop = asyncio.get_running_loop() await loop.run_in_executor(None, download_files_from_s3, s3_client, bucket, key, file_path) def download_files_from_s3(s3_client: S3Client, s3_bucket: str, prefix: str, local_dir: str) -> None: keys = s3_client.list_objects(bucket_name=s3_bucket, prefix=prefix) if not keys: logger.info(f"'{prefix}'内にファイルが存在しません。") return for key in keys: relative_path = os.path.relpath(key, prefix) local_file_path = os.path.join(local_dir, relative_path) os.makedirs(os.path.dirname(local_file_path), exist_ok=True) s3_client.download_file(bucket_name=s3_bucket, key=key, file_path=local_file_path) 5.4のまとめ setup_faiss による faiss 動的ロードで Lambda レイヤー容量問題を回避 非同期 I/O と S3 利用により、コンテナ化や EFS 接続なしで FAISS インデックスをロード search_data で Embedding 済みインデックスを検索し、RAG が迅速な類似文書提供を実現 これにより、RAG 検索を用いた高速なナレッジ検索が可能となり、LLM 回答に特化した情報提供が実現できます。 5.5 [Python] EmbeddingとFAISSインデックス化 このセクションでは、Confluence ドキュメントなどの社内ドキュメントを Embedding し、FAISS インデックスを作成・更新する定期バッチ処理の例を示します。 RAG パイプラインで参照するインデックスは、生成 AI が社内特有の知識を回答に反映するための重要な鍵です。そのため、定期的なドキュメント更新時には Embedding と FAISS インデックスを再構築し、最新情報を常に参照できるようにします。 処理の概要 S3 から JSON フォーマットのドキュメントを取得 取得したドキュメントを Embedding(OpenAI や Azure OpenAI の Embeddings API を利用) Embedding 済みのテキスト群を FAISS インデックス化 作成した FAISS インデックスを S3 へアップロード これらの手順を Lambda バッチ処理や Step Functions ワークフローで定期的に実行することで、問い合わせ時には常に最新インデックスを用いた RAG 検索が行えます。 ステップ1:JSONドキュメントの読み込み S3 上の JSON ファイル(Confluence ページ等を要約済みのもの)をダウンロード・パースし、 Document オブジェクトのリストに変換します。 import json from typing import Any, Dict, List from langchain_core.documents.base import Document from shared.logger import getLogger logger = getLogger(__name__) def load_json(file_path: str) -> List[Document]: """ JSONファイルを読み込み、Documentオブジェクトのリストを返します。 JSONは [{"title": "...", "content": "...", "id": "...", "url": "..."}] のような形式を想定。 """ with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, list): raise ValueError("JSONトップレベルがリストではありません。") documents = [] for record in data: if not isinstance(record, dict): logger.warning(f"スキップされたレコード(辞書でない): {record}") continue title = record.get("title", "") content = record.get("content", "") metadata = { "id": record.get("id"), "title": title, "url": record.get("url"), } # タイトルとコンテンツをまとめたテキストとしてDocument化 doc = Document(page_content=f"Title: {title}\nContent: {content}", metadata=metadata) documents.append(doc) logger.info(f"{len(documents)} 件のドキュメントをロードしました。") return documents ステップ2:EmbeddingとFAISSインデックス化 vectorize_and_save 関数では、 get_embeddings で取得した Embeddings クライアントでドキュメントを Embedding し、 FAISS インデックスを作成します。その後、ローカルにインデックスを保存します。 import os from langchain_community.vectorstores import FAISS from langchain_core.text_splitter import RecursiveCharacterTextSplitter from shared.logger import getLogger logger = getLogger(__name__) def vectorize_and_save(documents: List[Document], output_dir: str, embeddings) -> None: """ ドキュメントをEmbeddingし、FAISSインデックスを作成してローカルに保存します。 """ # テキスト分割器を使用してドキュメントを小さなチャンクに分割 text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=128) split_docs = text_splitter.split_documents(documents) logger.info(f"{len(split_docs)} 件の分割済みドキュメント") # Embeddingsでベクトル化し、FAISSインデックス構築 db: FAISS = FAISS.from_documents(split_docs, embeddings) logger.info("ベクトルDBの構築が完了しました。") os.makedirs(output_dir, exist_ok=True) db.save_local(output_dir) logger.info(f"ベクトルDBを {output_dir} に保存しました。") ステップ3:インデックスをS3へアップロード ローカルで作成した FAISS インデックスを S3 にアップロードすることで、RAG 検索用 Lambda から容易に取得可能になります。 from shared.s3_client import S3Client from shared.logger import getLogger logger = getLogger(__name__) def upload_faiss_to_s3(s3_client: S3Client, s3_bucket: str, local_index_dir: str, index_s3_path: str) -> None: """ FAISSインデックスをS3にアップロードします。 """ index_files = ["index.faiss", "index.pkl"] for file_name in index_files: local_file_path = os.path.join(local_index_dir, file_name) s3_index_key = os.path.join(index_s3_path, file_name) s3_client.upload_file(local_file_path, s3_bucket, s3_index_key) logger.info(f"FAISSインデックスファイルを s3://{s3_bucket}/{s3_index_key} にアップロードしました。") ステップ4:全体フローをLambdaで実行 index_to_s3 関数は全体フローをまとめています。S3 から JSON をダウンロード、Embedding と FAISS インデックス作成、そしてインデックスを S3 へアップロードします。この処理を Step Functions などのワークフローで定期的に実行し、常に最新のインデックスを用意します。 import os from shared.faiss import setup_faiss from shared.logger import getLogger from shared.s3_client import S3Client from shared.secrets import get_secret logger = getLogger(__name__) def index_to_s3(json_s3_key: str, index_s3_path: str) -> Dict[str, Any]: """ S3からJSONファイルをダウンロードし、EmbeddingとFAISSインデックス作成を行い、インデックスをS3に保存します。 """ env = os.getenv("ENV") if env is None: error_msg = "ENV 環境変数が設定されていません。" logger.error(error_msg) return {"status": "error", "message": error_msg} try: s3_client = S3Client() s3_bucket = "bucket-name" local_json_path = "/tmp/json_file.json" local_index_dir = "/tmp/index" # 必要なら faiss のセットアップ(S3からダウンロード) setup_faiss(s3_client, s3_bucket) # JSONファイルをS3からダウンロード s3_client.download_file(s3_bucket, json_s3_key, local_json_path) documents = load_json(local_json_path) # Embeddingsクライアント取得 secrets = get_secret() embeddings = get_embeddings(secrets) # ベクトル化とFAISSインデックス作成 vectorize_and_save(documents, local_index_dir, embeddings) # インデックスファイルをS3へアップロード upload_faiss_to_s3(s3_client, s3_bucket, local_index_dir, index_s3_path) return { "status": "success", "message": "FAISSインデックスを作成し、S3にアップロードしました。", "output": { "bucket": s3_bucket, "index_key": index_s3_path, }, } except Exception as e: logger.error(f"インデックス作成処理中にエラー発生: {e}") return {"status": "error", "message": str(e)} 5.5のまとめ load_json で JSON ファイルを読み込み、 vectorize_and_save で Embedding と FAISS インデックス作成 upload_faiss_to_s3 でローカルインデックスを S3 へアップロード index_to_s3 で全体フローをまとめ、定期バッチ処理で最新インデックスを作成・更新 これにより、社内ドキュメントを Embedding し、RAG 検索用の FAISS インデックスを作成・更新するバッチ処理を実現できます。 6. まとめ 本記事では、Slack 上で LLM を活用する社内チャットボットの開発背景や技術的実装ポイント、RAG パイプラインの導入手順、Confluence ドキュメントのサニタイズや Embedding/FAISS インデックスによる検索基盤の整備、さらには翻訳・要約などの機能拡張について紹介しました。 こうした仕組みにより、Slack 内で自然な操作で生成 AI を利用でき、社員は新たなツールやコマンドを学ぶことなく高度な情報活用が可能になります。 7. 今後の展望 私たちは社内向け生成AIツールをさらに進化させるため次の改善・拡張に積極的に取り組みます。 Azure 環境での構築強化 Azure Functions や Azure CosmosDB などの Azure サービスとの連携を本格化し RAG パイプラインのパフォーマンスや拡張性を抜本的に向上させます。 Azure Cosmos DBベクトル検索の導入 Azure Cosmos DB for NoSQL 上でベクトル検索機能を実用化しより高度な検索を提供します。 @ card AI Document Intelligenceの活用 AI Document Intelligence を積極的に取り込み RAG のナレッジ範囲を拡大させより多彩な情報活用を実現します。 @ card モデルの多様化・高度化 GPT-4o のみならず GPT-o1 や Google Gemini など最新で多様なモデルへの対応を推進し常に最先端のモデルを統合します。 Web UIの実装 Slack 依存の表現上の制約を解消するため Web UI を構築し多彩なインタラクションや新機能を柔軟に展開します。 プロンプト管理の拡充 既存のプロンプトをテンプレート化し用途別に容易な再利用を実現します。またプロンプト共有機能を充実させ社内全体での生成 AI 活用を一層促進します。 マルチエージェント化の実現 要約や翻訳、 RAG 検索など特定機能に特化した専門エージェントを配置し、エージェントビルダー機能で自由に組み合わせることでより柔軟で高度な情報活用を可能にします。 RAG精度の評価・改善 テストセットを構築し回答の自動評価を実施して精度を定量的に把握し継続的な品質向上につなげます。 ユーザーフィードバックを起点とした改善 利用実態やフィードバックを反映し対話フローの最適化やプロンプトチューニング外部サービス連携強化など実運用を通じて常に社内向け生成AIツールの利便性と有用性を高めていきます。 私たちはこれらの取り組みによって社内向け生成AIツールを持続的に進化させ、より多様なニーズに応えられる力強い社内支援ツールへ成長させます。
アバター
こんにちは、人事グループ組織人事チームのHOKAです。 このBlogは 10X innovation culture program 内製化における苦悩と挑戦【前編】 の続きです。 8月:Google Cloud Next Tokyo'24に登壇して気づいたこと Google Cloud Next Tokyo'24とは、グーグル・クラウド・ジャパン合同会社が2024年8月1日(木)~2日(金)に開催した旗艦イベントで、ビジネスリーダー、イノベーター、エンジニアのためのクラウドカンファレンスです。生成AIやセキュリティをはじめとするビジネスに欠かせないテーマをもとに、基調講演やライブセッション、ハンズオンなどさまざまなプログラムが用意されていました。  詳細はこちら→ https://cloudonair.withgoogle.com/events/next-tokyo-24 KTCからは開発支援部 部長のきっしーとDBREグループ マネージャーのあわっちが「10X Innovation Culture Program 体験ワークショップ」に登壇しました。ワークショップ参加者の皆様に10X Innovation Culture Programを導入している事例をお話する機会をいただいたのです。 本番までには何度も打ち合わせをし、Google オフィスでリハーサルをし、ランチもご一緒させていただき、Googleの方々と接する時間が増えました。 そして迎えた本番。 パシフィコ横浜の広い会場における非日常感が「今日は学ぶぞ」という気持ちを醸成させてくれます。 初めに、Googleの方から10X についてご案内しました。なぜGoogleが10X innovation culture programを社内外で推進しているか、その背景や理由、結果を「生きた言葉」で話してくださり、会場にいたKTCのメンバーは「10XはやはりKTCにも必要だ!!!」と士気が上がりました。 続いて、きっしーとあわっちが登壇しました。 きっしーとあわっちからは下記3つについてお話しました。 カルチャー変革に取り組んだ背景、なぜ 10X Innovation Culture Program を実施することになったのか 10X Innovation Culture Program を実際にやってみて、実施する上でのポイント 今後の展開について 特に、10X Innovation Culture Program を実施しながら感じている 「Google だからできる」という思い込みを捨て、深く考えずにまずはやってみることが大事 ボトムアップから社内自立化の実現に向けた社内ファシリテーターの育成が重要 カルチャーは一足飛びではなく、何度も気づき、体験して浸透していくもの ということをKey Messageとして会場で伝えました。 質問タイムではたくさんの方がKTCに質問してくださり、「10Xは素晴らしい。しかし、導入するのは難しくないのか?」という疑問を投げかけてくださいました。まだ10X innovation culture programによる大きな効果は見えないものの、私たちの取り組みが少しでも参考になっていれば幸いです。 Google Cloud Next Tokyo’24に参加して、私は7月に自分たちで手作りした10X innovation culture programと圧倒的な違いを感じました。それは オフィスとは別の会場で参加者の聞く体制の準備が整う Googleの社員によって語られる「10X」は、とても心に響く ディスカッションメインの場と思っていたが、もっとインプットメインでも良いのかもしれない。 その上で、「10Xの考えを日々の業務に取り入れたい」と変わるのかもしれない。 ということでした。上記を仮説として、秋に開催する10X innovation culture programで検証していくことが定まりました。 9月:10Xファシリテーター研修を受けてみた まず、私たちは10Xの理解およびディスカッションをスムーズにさせるファシリテーターの育成から着手することにしました。私たちが立てた仮説 Googleの社員によって語られる「10X」は、すごく浸透する ディスカッションメインの場と思っていたが、もっとインプットメイン を紐解くと、10Xを運営する側が「10Xとは何か」のインプットが少ないから、相手に伝わらないと思ったからです。 思い切って、Googleの方に相談したところ、ファシリテーター研修を実施していただける運びとなりました。 ファシリテーター研修を実施するにあたり、メンバー選出で悩みましたが、そもそも10X innovation culture programはリーダーシッププログラムなので、マネージャーに受講いただくことにしました。 ファシリテーター研修当日 今回の研修のメイン担当であるGoogleのKotaさんから、研修の冒頭で 「めちゃくちゃスパルタです。1回言ったことを覚えて、話せるようにする研修です。 Googleで大切にされている言葉 Steal with pride(プライドを持って盗もう) Feedback is a gift!(フィードバックは贈り物) を体感する構成となっております。 Googleの社員よりも10Xについて濃密な時間を過ごします。」 というメッセージをいただきました。 一体、何が始まるのか?という空気感で、研修は幕を開けました。 【研修前半】インプット Kotaさのメッセージにあった通り、初めにGoogleの方から10Xの6つの要素をプレゼンテーションし、KTC社員が理解する「インプットの時間」がありました。 例えば、ひとつ目の要素「DEI」について。 Googleでは Unconscious Bias @ Work という無意識の偏見に目を向ける研修を社員の50%が受講済。 週に一度の意識調査: Googlegeist を行い、ダイバーシティの受け入れや公平性に関する成果を測定している。 といった具体的な施策と、それによる効果についてお話いただきました。 同様に、10Xの6つの要素ひとつひとつに、Googleにおける具体的事例と、プレゼンターご自身が社員として感じていることやエピソードを交えて話してくださいました。 【研修後半】アウトプット Googleの方のプレゼンテーションを聞いた後、KTCのメンバーは6人1グループに分かれました。 そして、1人1要素を暗記し、自分のエピソードを交えてプレゼンテーションをしました。 プレゼンテーションを聞く人は聞いているだけではなく、1人ずつプレゼンテーションの良かったところや改善点をフィードバックしていきました。Googleの方からもフィードバックをいただきました。 このプロセスは、冒頭にKotaさんが仰ったとおり「スパルタ」かもしれませんが、Googleのやり方を「Steal with pride」し、「Feedback is a gift」する時間でした。  また、フィードバックの内容も 「もっと全社でビジョン共有した方が良いね」 「目標設定、どうなってんでしたっけ?」 「もっと事例を言えるようになりたい。経営者との接点を増やそう!」 など、どんどんアイデアが生まれて来ました。 10Xをインプットしたことで、「KTCの今」と照らし合わせることができるようになった瞬間に立ち会うことができました。 実際に、ファシリテーター研修を受けたKTCのメンバーから「今まで10Xを素晴らしいものとは思いつつも、どこか他人事だったが、研修を通して短時間で暗記し、自分のエピソードと絡め、他者にプレゼンテーションすることで一気に自分のものにすることが出来ました」というコメントが多数寄せられました。 また、その場でGoogleの方からフィードバックをもらえたのも貴重な機会だったと思います。 10月:3回目の10X innovation culture programに向けて準備中 ファシリテーター研修を終え、3回目の10X innovation culture program準備中(10月)にこのBlogを書きました。 3回目の10X innovation culture programは2日程に分かれます。新しいやり方です。 また、参加者のうち初めて参加するメンバーは47%を占めています。 前回(7月)より前のめりで参加してもらえるか? 9月のファシリテーター研修の成果は出るか? また改めてTechBlogに投稿いたします。 10X innovation culture program 内製化における苦悩と挑戦【後編】に続く ※2025年1月公開予定
アバター
1. はじめに こんにちは、共通サービス開発グループの鳥居( @yu_torii )です。主にバックエンド/フロントエンド領域を担当しています。 KINTO 会員プラットフォーム開発チームでフロントエンドエンジニアを務めながら、社内での生成 AI 活用にも携わっています。 本記事では、Slack 上で LLM を活用する社内チャットボット「しぇるぱ」の RAG や Slack リアクションを活用した翻訳機能を紹介します。 しぇるぱは、社内での生成 AI 活用を目指して開発された Slack チャットボットです。 フロントエンド開発を省略して素早く社内に展開し、社員が普段使う Slack 上で生成 AI を自然に利用できる環境を目標としています。 「しぇるぱ」という名前はエベレスト登山のガイドとして知られるシェルパ族に由来し、彼らが登山者を支える頼れる存在であるように、しぇるぱもまた社内の業務効率の向上や情報共有の円滑化のための頼れるパートナーでありたいという願いが込められています。 ちなみに、バナーの「しぇるぱ」さんはクリエイティブグループの方が KINTO テクノジーズ全体会議(KTC 超本部会)に際してサプライズで描いてくれたものです。ありがとうございます! この取り組みは、生成 AI 活用プロジェクトを推進する和田さん( @cognac_n )との協力により進められました。RAG パイプラインの導入、ローカル開発環境の整備、Slack 絵文字リアクションを活用した翻訳・要約機能など、多角的な改善を行い、社内での生成 AI 利活用を推進しています。 :::details 和田さんの記事はこちら @ card @ card @ card ::: なお、RAG や生成 AI 技術そのものの詳細解説は省き、実装や機能追加の過程を中心にまとめています。 また、しぇるぱの LLM は Azure OpenAI を利用しています。 この記事で得られること Slack Botによる生成AIを活用したチャット機能の利用方法 Slack と LLM を組み合わせ、絵文字リアクションや自然なメッセージ投稿で翻訳・要約を呼び出すチャットボット実装例を紹介 HTMLサニタイズを含むConfluenceデータ取得の工夫 Confluence ドキュメントを Go で取得し、HTML サニタイズにより要約・Embedding に適したテキストを整える方法 FAISS とS3を使った簡易的なRAGパイプライン導入 FAISS インデックスと S3 を活用した簡易 RAG パイプライン導入の手順と注意点を解説 応答速度はあまり早くないものの、低コストで簡易的な RAG を組み込む際の実装ステップや注意点をまとめます。 :::message 本記事は、執筆時点での実装状況をもとにしています。改善点が多分に含まれている点をご了承ください。 社内向けのツールであるため、プロダクションレベルのパフォーマンスを必要とした実装ではありません。 AWS SAM でのデプロイや Step Functions のパイプライン構築、GitHub Actions による CI/CD など、開発環境の整備については、本記事では詳細を省略しています。 ::: :::details 目次(展開) 1. はじめに この記事で得られること 2. 全体アーキテクチャの概要 3. Slack Botによる生成AIを活用したチャット機能 「チャット機能」と「リアクション機能」 チャット機能:利用シナリオ リアクション機能:利用シナリオ この構成のメリット 3.5 実際の利用例 3章まとめ 4. 実装方針と内部設計 全体的な処理フロー 条件分岐による機能判定 サニタイズの役割 RAG利用をConfluence参照に限定 拡張性・保守性への考慮 5. コード例と設定ファイルの紹介 5.1 [Go] Slackイベント受信と解析 5.2 [Go] HTMLテキストのサニタイズ 5.3 [Python] LLM問い合わせの具体例 5.4 [Python] RAG検索呼び出し例 5.5 [Python] EmbeddingとFAISSインデックス化 ::: 2. 全体アーキテクチャの概要 ここでは、しぇるぱが生成 AI による回答を返すまでの流れを、「ユーザとの生成 AI チャット機能」と「Confluence ドキュメントをインデックス化する処理」という 2 つの視点で整理します。 ユーザとの生成AIチャット機能の処理 Slackでしぇるぱを呼び出す しぇるぱの呼び出し方は、チャットでの呼び出しと、リアクション絵文字での呼び出しの 2 通りがあります。 チャットでの呼び出しは、チャンネルでメンションを付けて投稿するか、ダイレクトメッセージで直接投稿することで、生成 AI による回答をリクエストできます。 リアクションでの呼び出しは、翻訳・要約用の絵文字リアクションを付けることで、そのメッセージの翻訳や要約をリクエストできます。 Go Slack Bot Lambda Slack イベントを受け取る Bot は Go で実装し、Lambda 上で動作します。 ユーザからの質問やリアクションを受け取り、リクエストの種類に応じて処理を振り分けます。 LLM へのリクエストや、Embedding 済みデータの参照など Azure OpenAI を利用する場合は、ここでリクエストを生成し、Python Lambda へ送信します。 Python LambdaでのLLMへのリクエスト・RAG参照 Python Lambda は機能ごとに分割し、LLM 問い合わせや RAG 参照などを担当します。 Go Lambda からのリクエストを受け取り、LLM への問い合わせや RAG による回答生成を行います。 Slackへの回答返送 生成された回答は、Go Slack Bot Lambda を経由して Slack へ返送します。 絵文字リアクションで呼び出す翻訳・要約機能も、このフローに組み込まれています。 :::details アーキテクチャ図(ユーザからの呼び出しに対応する処理) flowchart LR subgraph "ユーザからの呼び出しに対応する処理" A["User(Slack)"] -->|"質問・絵文字"| B["Go Slack Bot(Lambda)"] B -->|"リクエスト"| C["Python Lambda RAG/LLM"] C -->|"回答"| B B -->|"回答"| A end ::: Confluence ドキュメントをインデックス化する処理 RAG パイプラインを利用するには、Confluence ドキュメントを要約・Embedding しやすい状態に整える必要があります。これらの前処理は StepFunctions でワークフロー化し、定期的かつ自動的に実行しています。 ドキュメント取得・HTMLサニタイズ(Go実装) Go で実装した Lambda が Confluence API からドキュメントを取得し、HTML タグを整理してテキストを扱いやすい状態にします。 サニタイズ済みテキストは JSON として出力します。 要約処理(Go + Python Lambda呼び出し) 要約処理の目的は、テキストを Embedding や RAG で扱いやすい範囲に絞ることです。 Go 実装の Lambda が Azure OpenAI Chat API のリクエスト処理を行う Python Lambda を呼び出し、テキストを短く整え、再び JSON 化します。 FAISSインデックス化とS3保存(Indexer Lambda) 要約後のテキストを Embedding し、FAISS インデックスを生成する Indexer Lambda がインデックスやメタ情報を S3 へ格納します。 これにより、問い合わせ発生時にはインデックス済みデータを即座に参照でき、RAG パイプラインがスムーズに稼働します。 :::details アーキテクチャ図(Confluence ドキュメントのインデックス化フロー) flowchart LR subgraph "インデックス化準備処理 StepFunctions" D["Go Lambda(ドキュメント取得/HTMLサニタイズ)"] D --> E["Go Lambda(要約/Python呼出)"] E --> F["Indexer Lambda(Embedding/FAISS/S3)"] end ::: こうした事前処理と問い合わせ時の処理が組み合わさることで、しぇるぱは Slack 上の簡易な操作で社内特有のナレッジを反映した生成 AI 回答を返せるようになっています。 以降の章では、これらの要素についてさらに詳細を見ていきます。 3. Slack Botによる生成AIを活用したチャット機能 前章でアーキテクチャの全体像を示しましたが、本章では、ユーザが Slack 上で自然な操作を行うだけで得られる機能について、その使い方や有用性に焦点を当てます。 ここでは「何ができるか」「どんなシナリオで役立つか」という利用イメージを示し、実装詳細は次章以降で体系的に紹介します。 「チャット機能」と「リアクション機能」 しぇるぱは、Slack 上での自然な操作を原動力に、多様な生成 AI 機能を提供します。 チャット機能 : 質問を投稿したり、ファイルや画像、外部リンクを貼ったり、特定の形式でメッセージを送ることで、LLM への問い合わせや(特定の場合)RAG 検索が行われ、必要な回答が得られます。 Slack という日常的なツールに機能を統合することで、ユーザは新たな環境やコマンドを学ぶ必要なく、生成 AI を自然に日常業務へ取り込むことができます。 リアクション機能 : 特定の絵文字リアクションをメッセージに付与するだけで翻訳やしぇるぱ発言の削除などをトリガーでき、コマンドレスでの追加操作が可能になります。 チャット機能:利用シナリオ 基本的な質問・回答 普通に質問を投稿するだけで、LLM を通じて回答を取得できます。 利用シーン例: 「このプロジェクトの手順は?」と尋ねれば即座に回答が得られ、スレッド内の履歴や発言者情報を考慮して文脈に沿った応答が可能です。 ファイル・画像・外部リンク参照 ファイルを貼り付けて「要約して」と依頼すれば、ファイル内容を要約。 画像を添付すれば文字起こしや画像内容を踏まえた回答が可能。 外部リンクを貼れば、そのページ内容を整理して LLM 回答に生かせます。 利用シーン例: 会議メモが入ったテキストファイルを「要約して」で短いサマリを得る、画像内文字情報を抜き出す、外部記事リンクを要約するといった、手間を省く活用ができます。 Confluenceページ参照(RAG活用) :confluence: index:Index名 で、社内でのルールや申請方法等のドキュメントが含まれた Confluence ページを RAG 検索。 利用シーン例: 社内申請手順や内部ルールが Confluence にまとまっている場合、必要な情報を即座に引き出し、一般的な回答以上に社内事情に即したアドバイスが得られます。 利用シーン例: プロジェクト独自の手順書や設定値を検索して、その内容を回答に反映。検索しづらい情報を簡単に引き出せます。 リアクション機能:利用シナリオ 特定の絵文字リアクションを付けることで、さらに直感的な機能呼び出しが可能です。 翻訳 : 翻訳用絵文字を付ければ、そのメッセージを指定言語へ翻訳可能です。言語の壁を低くし、コミュニケーションを円滑化します。 削除 不要になったしぇるぱ発言を絵文字 1 つで簡易に削除し、チャンネルを整理できます。 この構成のメリット 自然な操作で活用可能 : ユーザは慣れた Slack 操作(メッセージ投稿、絵文字付与など)だけで生成 AI 機能を使え、学習コストを低減できます。 日常ツールへの統合で生成AI定着 : Slack という普段使うコミュニケーション基盤に機能を溶け込ませることで、生成 AI を身近に利用することが可能です。またリアクション機能は文字入力をすることなく、翻訳や削除などの操作ができるため、手軽に利用できます。 拡張に対応しやすい土台 : 今後、新たなモデルや追加機能を組み込みたくなった場合も、既存のフロー(質問投稿、絵文字操作)に条件を増やすだけで拡張可能です。 3.5 実際の利用例 ここで、しぇるぱが Slack 上でどのように使われているか、実際のシナリオをいくつか紹介します。 例1:画像の認識 メッセージに画像を添付すると、しぇるぱは画像内のテキストを認識し、その内容を回答します。 ![画像の認識](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-context.png =500x) 画像の認識 例2:Confluenceドキュメントに基づく回答 :confluence: という絵文字を使うことで、Confluence ドキュメントを参照した回答を得ることができます。 ![Confluence ドキュメントに基づく回答](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-confluence-rag.png =500x) Confluenceドキュメントに基づく回答 ご覧の通り、しぇるぱはワークフローから呼び出すことも可能で、プロンプトストアのような利用も可能です。 例3:翻訳リアクションでの英訳依頼 ユーザが英訳したいメッセージに翻訳リアクションを付与 英語話者に共有したい日本語メッセージに :sherpa_translate_to_english: といったリアクション絵文字を付けると、そのメッセージのテキストが自動で英訳されます。 ![英語翻訳リアクション](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-translation.png =500x) 英語翻訳リアクション 翻訳内容を過信しないように、自動翻訳であることを複数言語で通知することで、誤解を防ぎます。 また、機能を普及させるために、利用方法も記載しています。 英訳の他に複数言語の翻訳機能を提供しています。 3章まとめ 本章では、「どんな操作で何ができるか」というユーザ視点の使い方に注目しました。 ここで得た利用イメージを基に、次の章以降で実装内容を体系的に示し、Go 製 Bot や Python Lambda の連携方法、Slack イベントの処理や RAG 検索のしくみ、ファイル抽出やサニタイズ処理などの詳細を紹介します。 4. 実装方針と内部設計 この章では、しぇるぱが Slack のメッセージやリアクションを通じて多様な生成 AI 機能を提供するための実装方針と内部設計について説明します。 ここで紹介するのは、あくまで全体的な考え方や役割分担、拡張性への配慮に関する部分です。具体的なコードや設定ファイルは次章以降に記載します。 全体的な処理フロー Slackイベント受信(Go Lambda) Slack で行われるメッセージ投稿、ファイル添付、画像挿入、外部リンク記載、絵文字リアクションなどのイベントは、Slack API 経由で Go 実装の AWS Lambda へ通知されます。 Go 側はこれらのイベントを解析し、ユーザが求める処理(通常のチャット、翻訳、Confluence 参照など)を判定します。 Go側でのテキスト処理・サニタイズ 外部リンクやファイルからテキストを取得し、コンテキストとしてプロンプトに追加します。 外部リンクを参照する場合は table 、 ol 、 ul など意味のあるタグは残し、不要なタグだけを除去してトークンを節約します。 Python側でのLLM問い合わせ・RAG検索 必要な場合、Go 側は LLM 問い合わせ用の Python Lambda(LLM 用)か、RAG 検索用の Python Lambda(Confluence 参照用)を呼び出します。 例えば、 :confluence: が含まれていれば RAG 検索用 Lambda を呼び出し、index が指定されていなければデフォルトインデックスで検索します。 そうでなければ通常は LLM 用 Lambda へテキストを渡し、LLM への問い合わせを行います。 回答返却とSlack表示 Python 側の Lambda が生成した回答を Go 側に戻し、Go 側が Slack へ投稿します。 これにより、ユーザはコマンドを暗記する必要なく、絵文字やキーワード、ファイル添付など日常的な操作だけで高度な機能を利用できます。 条件分岐による機能判定 特定の絵文字( :confluence: など)やキーワード、ファイルや画像の有無、外部リンク存在などにより処理を振り分けます。 新機能を追加する場合は、Go 側で新たな条件を追加して、必要なら対応する Python 側の Lambda(LLM 用、RAG 用など)を呼び出すロジックを増やすことで実現できます。 サニタイズの役割 Go 側でサニタイズし、不要な HTML タグを除去することで、モデルへ渡すテキストを効率的に扱い、トークン消費を抑えます。 table、ol、ul など意味あるタグは残して情報構造を保持し、モデルにとって有用な文脈を損ねないようにしています。 RAG利用をConfluence参照に限定 RAG 検索は :confluence: 指定時のみ利用します。 これにより通常の要約や翻訳、Q&A は LLM 直接問い合わせで済み、RAG 関連ロジックは Confluence 参照時だけ発動します。 Confluence ドキュメントの Embedding 生成や FAISS インデックス更新は StepFunctions で定期的に実行し、問い合わせ時には常に最新インデックスを利用できます。 拡張性・保守性への考慮 絵文字やキーワード、ファイル・画像の有無による条件分岐は、機能を増やす際の変更箇所を少なく保ち、保守性を高めます。 Go 側でテキスト整形やサニタイズ、Python 側で LLM 問い合わせ・RAG 検索を行う役割分担も、コードの見通しを良くし、将来的なモデル切り替えや処理追加を容易にします。 次章では、これらの方針を踏まえた具体的なコードスニペットや設定ファイル例を紹介します。 5. コード例と設定ファイルの紹介 この章では、第 4 章で説明した実装方針や設計上の考え方に基づいて、実装例を要点を絞って紹介します。 本章は次のセクションで構成します。 5.1 Slack イベント受信と解析(Go 側) Slack の Events API を用いてメッセージや絵文字リアクションなどのイベントを受信・解析する方法を示します。 5.2 Go 側でサニタイズ例 外部リンク参照した際の HTML サニタイズ処理の例を示します。 5.3 Python 側 LLM 問い合わせの具体例 LLM(LLM)への問い合わせを行う Lambda のコード例を提示します。 5.4 Python 側 RAG 検索呼び出し例 Confluence 参照など RAG を使う場合の検索呼び出し例を紹介します。 5.5 Python 側 Embedding と FAISS インデックス化 Confluence ドキュメントを定期的に Embedding し、FAISS インデックスを更新する Lambda のコード例を示します。 5.1 [Go] Slackイベント受信と解析 このセクションでは、Slack の Events API を利用して、AWS Lambda 上で Go コードでイベントを受信・解析する基本的な流れを説明します。 Slack 側での設定(OAuth & Permissions、イベント購読)や、 chat.postMessage メソッド利用時に必要なスコープ( chat:write など)の確認方法にも触れ、読者が実装を始めるまでに必要な準備を明確にします。 Slack側での設定手順 App作成とApp IDの確認 : https://api.slack.com/apps で新規 App を作成します。 作成後、Basic Information ページ( https://api.slack.com/apps/APP_ID/general 、 APP_ID は App 固有の ID)で App ID( A で始まる文字列)を確認します。 この App ID は Slack App の識別子であり、後述の OAuth & Permissions や Event Subscriptions ページの URL アクセスに用いることができます。 OAuth & Permissionsでスコープ付与 : 「OAuth & Permissions」ページ( https://api.slack.com/apps/APP_ID/oauth )にアクセスし、Bot Token Scopes に必要なスコープを追加します。 例えば、チャンネルへメッセージ投稿に chat.postMessage メソッドが必要である場合、 https://api.slack.com/methods/chat.postMessage で「Required scopes」を確認すると chat:write が必要であるとわかります。 スコープ付与後、「reinstall your app」をクリックし、ワークスペースに再インストールすると、変更が反映されます。 ![Required scopes の確認](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image.png =500x) Required scopesの確認 ![Scope の設定](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-1.png =500x) Scopeの設定 Events API有効化とイベント購読 : 「Event Subscriptions」ページ( https://api.slack.com/apps/APP_ID/event-subscriptions )で Events API を有効化し、「Request URL」に後述の AWS Lambda エンドポイントを設定します。 message.channels や reaction_added など、購読するイベントを追加します。これにより、対象イベント発生時に Slack が指定 URL へ通知を送るようになります。 ![Event Subscriptions の説明](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-3.png =500x) AWS Lambda側でのイベント受信・解析 Slack 側での設定が完了すると、購読対象のイベントが発生するたびに Slack は API Gateway 経由で Lambda へ POST リクエストを送ります。 ステップ1:Slackイベントの解析 slack-go/slackevents パッケージを用いて、受信した JSON を EventsAPIEvent へ変換します。 これにより、URL 検証や CallbackEvent などイベントタイプを判別しやすくなります。 func parseSlackEvent(body string) (*slackevents.EventsAPIEvent, error) { event, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { return nil, fmt.Errorf("Slackイベントの解析に失敗しました: %w", err) } return &event, nil } @ card ステップ2:URL検証要求対応 初回に Slack は type=url_verification のイベントを送ってきます。この中の challenge をそのまま返すことで Slack は URL 有効性を確認し、その後イベントを通知してくれるようになります。 func handleURLVerification(body string) (events.APIGatewayProxyResponse, error) { var r struct { Challenge string `json:"challenge"` } if err := json.Unmarshal([]byte(body), &r); err != nil { return createErrorResponse(400, err) } return events.APIGatewayProxyResponse{ StatusCode: 200, Body: r.Challenge, }, nil } @ card ステップ3:署名検証・再試行リクエスト無視 Slack はリクエスト署名を付与し、正当性を検証できます(実装は省略)。 また、障害時などに再試行リクエストが送られる場合があり、 X-Slack-Retry-Num ヘッダで再試行を判定して同じイベントを二重処理しないようにできます。 func verifySlackRequest(body string, headers http.Header) error { // 署名検証処理(省略) return nil } func isSlackRetry(headers http.Header) bool { return headers.Get("X-Slack-Retry-Num") != "" } func createIgnoredRetryResponse() (events.APIGatewayProxyResponse, error) { responseBody, _ := json.Marshal(map[string]string{"message": "Slackの再試行を無視します"}) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, nil } ステップ4:CallbackEvent処理 CallbackEvent は実際のメッセージ投稿やリアクション追加などが含まれます。ここで、 :confluence: が含まれるか、ファイルがあるか、翻訳用絵文字が付いているかなどを判定し、5.2 以降で示すテキスト処理や Python Lambda 呼び出しへ進みます。 // handleCallbackEvent は、コールバックイベントを処理します。(5.1での説明対象) func handleCallbackEvent(ctx context.Context, isOrchestrator bool, event *slackevents.EventsAPIEvent) (events.APIGatewayProxyResponse, error) { innerEvent := event.InnerEvent switch innerEvent.Data.(type) { case *slackevents.AppMentionEvent: // AppMentionEventの場合の処理(5.2で詳細説明) case *slackevents.MessageEvent: // MessageEventの場合の処理(5.2で詳細説明) case *slackevents.ReactionAddedEvent: // ReactionAddedEventの場合の処理(5.2で詳細説明) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: http.StatusOK}, nil } ハンドラ全体のコード例 これらのステップを組み合わせて AWS Lambda ハンドラを定義します。 :::details ハンドラ全体のコード例 func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { event, err := parseSlackEvent(request.Body) if err != nil { return createErrorResponse(400, err) } if event.Type == slackevents.URLVerification { return handleURLVerification(request.Body) } headers := convertToHTTPHeader(request.Headers) err = verifySlackRequest(request.Body, headers) if err != nil { return createErrorResponse(http.StatusUnauthorized, fmt.Errorf("リクエストの検証に失敗しました: %w", err)) } if isSlackRetry(headers) { return createIgnoredRetryResponse() } if event.Type == slackevents.CallbackEvent { return handleCallbackEvent(ctx, event) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: 200}, nil } func convertToHTTPHeader(headers map[string]string) http.Header { httpHeaders := http.Header{} for key, value := range headers { httpHeaders.Set(key, value) } return httpHeaders } func createErrorResponse(statusCode int, err error) (events.APIGatewayProxyResponse, error) { responseBody, _ := json.Marshal(map[string]string{"error": err.Error()}) return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, err } ::: 5.1のまとめ このセクションで、Slack App の App ID や OAuth & Permissions でのスコープ付与方法、Event Subscriptions でのイベント購読設定を説明し、Slack イベントの受信・解析プロセス(URL Verification 対応、署名検証、再試行リクエスト無視、CallbackEvent 処理)を示しました。 次の 5.2 以降では、CallbackEvent 処理の具体例や、Go 側でのテキスト処理、Python Lambda への問い合わせ方法などを紹介します。 5.2 [Go] HTMLテキストのサニタイズ 外部リンク参照内容のサニタイズ 外部リンクから取得した HTML テキストには、 script や style など回答に不要なタグが含まれる場合があります。そのまま LLM に渡すと、トークン数が増えてモデルコストが上昇し、回答精度が下がる可能性があります。次のコードでは、 bluemonday パッケージを用いて基本的なサニタイズを行ったうえで、 table や ol 、 ul など重要なタグを残しつつ不要なタグを除去し、読みやすい形に整形します。 addNewlinesForTags 関数を活用し、特定のタグ後に改行を挿入してテキストを整えることで、モデルへの問い合わせ時に必要な情報のみを最適なフォーマットで渡すことが可能になります。 @ card func sanitizeContent(htmlContent string) string { // 基本的なサニタイズ ugcPolicy := bluemonday.UGCPolicy() sanitized := ugcPolicy.Sanitize(htmlContent) // カスタムポリシーで特定タグを許可 customPolicy := bluemonday.NewPolicy() customPolicy.AllowLists() customPolicy.AllowTables() customPolicy.AllowAttrs("href").OnElements("a") // タグごとに改行を追加して可読性向上 formattedContent := addNewlinesForTags(sanitized, "p") // カスタムポリシー適用後の最終サニタイズ finalContent := customPolicy.Sanitize(formattedContent) return finalContent } func addNewlinesForTags(htmlStr string, tags ...string) string { for _, tag := range tags { closeTag := fmt.Sprintf("</%s>", tag) htmlStr = strings.ReplaceAll(htmlStr, closeTag, closeTag+"\n") } return htmlStr } この処理により、モデルへの入力は不要なタグが取り除かれたテキストのみとなり、回答精度とコスト効率が向上します。必要な構造(テーブルや箇条書き)は保持しつつ、特定タグの終了後に改行を挿入することで、モデルが情報を理解しやすい形でコンテキストを提供できます。 5.3 [Python] LLM問い合わせの具体例 以下は、Python で LLM(たとえば Azure OpenAI)へ問い合わせる処理の例です。 OpenAIClientFactory を用いてモデルやエンドポイントを動的に切り替えられるため、複数の Lambda ハンドラ間で共通のクライアント生成処理を再利用できます。 クライアント生成処理 OpenAIClientFactory は api_type や model に応じて Azure OpenAI または OpenAI 用のクライアントを動的に生成します。 環境変数や秘密情報により API キー、エンドポイントを取得するため、モデル変更や設定変更時もコード修正が最小限で済みます。 import openai from shared.secrets import get_secret class OpenAIClientFactory: @staticmethod def create_client(region="eastus2", model="gpt-4o") -> openai.OpenAI: secret = get_secret() api_type = secret.get("openai_api_type", "azure") if api_type == "azure": return openai.AzureOpenAI( api_key=secret.get(f"azure_openai_api_key_{region}"), azure_endpoint=secret.get(f"azure_openai_endpoint_{region}"), api_version=secret.get( f"azure_openai_api_version_{region}", "2024-07-01-preview" ), ) elif api_type == "openai": return openai.OpenAI(api_key=secret.get("openai_api_key")) raise ValueError(f"Invalid api_type: {api_type}") LLM問い合わせ処理 chatCompletionHandler 関数では、HTTP リクエストとして受け取った JSON から messages や model 、 temperature などを取得し、 OpenAIClientFactory で生成したクライアントを用いて LLM に問い合わせます。 レスポンスは JSON 形式で返し、エラー発生時は共通のエラーレスポンス生成関数によって整形されたレスポンスを返します。 import json from typing import Any, Dict, List import openai from openai.types.chat import ChatCompletionMessageParam from shared.openai_client import OpenAIClientFactory def chatCompletionHandler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: request_body = json.loads(event["body"]) messages: List[ChatCompletionMessageParam] = request_body.get("messages", []) model = request_body.get("model", "gpt-4o") client = OpenAIClientFactory.create_client(model=model) temperature = request_body.get("temperature", 0.7) max_tokens = request_body.get("max_tokens", 4000) response_format = request_body.get("response_format", None) completion = client.chat.completions.create( model=model, stream=False, messages=messages, max_tokens=max_tokens, frequency_penalty=0, presence_penalty=0, temperature=temperature, response_format=response_format, ) return { "statusCode": 200, "body": json.dumps(completion.to_dict()), "headers": { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST", "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", }, } この仕組みにより、異なる Lambda ハンドラでも同様の手順で LLM 問い合わせが可能となり、モデルやエンドポイント変更にも柔軟に対応できる構成となっています。 5.4 [Python] RAG検索呼び出し例 このセクションでは、Python で RAG(Retrieval Augmented Generation)検索を行うための手順を示します。 Confluence ドキュメントなど社内のナレッジをベクトル化し、FAISS インデックスを用いて類似文書検索を行うことで、LLM 回答に特化した情報を組み込むことが可能です。 ここで注目すべき点は faiss ライブラリの取り扱いです。 faiss は非常にサイズが大きく、Lambda Layer の容量制限を超える可能性があるため、通常は EFS を利用するか、Lambda をコンテナ化する必要があります。 今回はその手間を省くため、 setup_faiss 関数により S3 から faiss 関連パッケージをダウンロード・展開し、 sys.path に動的に追加することで faiss を利用可能にしています。 FAISS とは FAISS(Facebook AI Similarity Search)は、Meta(Facebook)製の近似最近傍探索ライブラリであり、類似の画像やテキストを検索するためのインデックスを作成するツールです。 @ card setup_faiss 関数によるFAISSセットアップ FAISS パッケージを Lambda 環境で利用するために、 setup_faiss 関数では次の手順を行います。 ローカル/CI環境でfaissパッケージをビルド・アーカイブ 開発者が GitHub Actions などの CI 環境で faiss-cpu パッケージをインストールし、必要なバイナリを tar.gz 形式でまとめます。 S3へアップロード CI でビルド・アーカイブした faiss_package.tar.gz を S3 にアップロードします。 本番やステージング環境に合わせて、適切なバケットやパスへ格納することで、Lambda 実行時に S3 から動的に取得可能です。 Lambda実行時に setup_faiss で動的ロード Lambda 実行環境上では、 setup_faiss 関数が起動時に S3 から faiss_package.tar.gz をダウンロード・展開し、 sys.path に追加します。 これにより、Lambda コード内で import faiss が可能となり、Embedding 処理で作成したベクトルを高速に検索できます。 GitHub Actions でfaissパッケージをS3へアップロードする例 以下は、 faiss-cpu をインストールし、Lambda で利用できるようにパッケージ化した上で、S3 にアップロードする GitHub Actions の例です。 ここでは、GitHub Actions の Secret や Environment Variables 機能を利用することで、AWS 認証情報や S3 バケット名などをハードコーディングせずに管理しています。 @ card name: Build and Upload FAISS on: workflow_dispatch: inputs: environment: description: デプロイ環境 type: environment default: dev jobs: build-and-upload-faiss: environment: ${{ inputs.environment }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" # 必要なパッケージのインストール(faiss-cpu) - name: Install faiss-cpu run: | set -e echo "Installing faiss-cpu..." pip install faiss-cpu --no-deps # faiss のバイナリをアーカイブ - name: Archive faiss binaries run: | mkdir -p faiss_package pip install --target=faiss_package faiss-cpu tar -czvf faiss_package.tar.gz faiss_package # AWS認証情報の設定(環境に合わせてSecretsやRoleを指定) - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.CICD_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.CICD_AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 # S3 にアップロード - name: Upload faiss binaries to S3 run: | echo "Uploading faiss_package.tar.gz to S3..." aws s3 cp faiss_package.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/lambda/faiss_package.tar.gz echo "Upload complete." 上記の例では、 faiss_package.tar.gz が S3 に lambda/faiss_package.tar.gz というキーでアップロードされます。 Lambda側での動的ロード処理 ( setup_faiss 関数) setup_faiss 関数は、Lambda 実行時に S3 から faiss_package.tar.gz をダウンロードし、 /tmp ディレクトリに展開、 sys.path にパッケージパスを追加します。こうして Lambda 内で import faiss が可能になり、FAISS インデックス検索を実行できるようになります。 # setup_faiss例:S3上のfaissパッケージをダウンロードし、sys.pathに追加 import os import sys import tarfile from shared.logger import getLogger from shared.s3_client import S3Client logger = getLogger(__name__) def setup_faiss(s3_client: S3Client, s3_bucket: str) -> None: try: import faiss logger.info("faiss が既にインポートされています。") except ImportError: logger.info("faiss が見つかりません。S3からダウンロードします。") faiss_package_key = "lambda/faiss_package.tar.gz" faiss_package_path = "/tmp/faiss_package.tar.gz" faiss_extract_path = "/tmp/faiss_package" # S3からパッケージをダウンロードして展開 s3_client.download_file(bucket_name=s3_bucket, key=faiss_package_key, file_path=faiss_package_path) with tarfile.open(faiss_package_path, "r:gz") as tar: for member in tar.getmembers(): member.name = os.path.relpath(member.name, start=member.name.split("/")[0]) tar.extract(member, faiss_extract_path) sys.path.insert(0, faiss_extract_path) import faiss logger.info("faiss のインポートに成功しました。") EmbeddingsとFAISSインデックスを用いたRAG検索 search_data 関数では、S3 から取得した FAISS インデックスをローカルでロードし、クエリに合致する文書を検索します。 get_embeddings 関数によって生成される Embeddings クライアント(Azure OpenAI または OpenAI)を用いて文書をベクトル化しており、 faiss を活用した高速検索が可能です。 from typing import Any, Dict, List, Optional from langchain_community.vectorstores import FAISS from langchain_core.documents.base import Document from langchain_core.vectorstores.base import VectorStoreRetriever from shared.secrets import get_secret from shared.logger import getLogger from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings logger = getLogger(__name__) def get_embeddings(secrets: Dict[str, str]): api_type: str = secrets.get("openai_api_type", "azure") if api_type == "azure": return AzureOpenAIEmbeddings( openai_api_key=secrets.get("azure_openai_api_key_eastus2"), azure_endpoint=secrets.get("azure_openai_endpoint_eastus2"), model="text-embedding-3-large", api_version=secrets.get("azure_openai_api_version_eastus2", "2023-07-01-preview"), ) elif api_type == "openai": return OpenAIEmbeddings( openai_api_key=secrets.get("openai_api_key"), model="text-embedding-3-large", ) else: logger.error("無効なAPIタイプが指定されています。") raise ValueError("Invalid api_type") def search_data( query: str, index_folder_path: str, search_type: str = "similarity", score_threshold: Optional[float] = None, k: Optional[int] = None, fetch_k: Optional[int] = None, lambda_mult: Optional[float] = None, ) -> List[Dict]: secrets: Dict[str, str] = get_secret() embeddings = get_embeddings(secrets) db: FAISS = FAISS.load_local( folder_path=index_folder_path, embeddings=embeddings, allow_dangerous_deserialization=True, ) search_kwargs = {"k": k} if search_type == "similarity_score_threshold" and score_threshold is not None: search_kwargs["score_threshold"] = score_threshold elif search_type == "mmr": search_kwargs["fetch_k"] = fetch_k or k * 4 if lambda_mult is not None: search_kwargs["lambda_mult"] = lambda_mult retriever: VectorStoreRetriever = db.as_retriever( search_type=search_type, search_kwargs=search_kwargs, ) results: List[Document] = retriever.invoke(input=query) return [{"content": doc.page_content, "metadata": doc.metadata} for doc in results] 非同期ダウンロードとLambdaハンドラ async_handler 内で setup_faiss を実行し、 download_files で S3 から FAISS インデックスファイルを取得します。その後 search_data で RAG 検索を実行し、結果を JSON で返します。 import asyncio import json import os from shared.s3_client import S3Client from shared.logger import getLogger from shared.token_verifier import with_token_verification logger = getLogger(__name__) RESULT_NUM = 5 @with_token_verification async def async_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: env = os.getenv("ENV") s3_client = S3Client() s3_bucket = "bucket-name" setup_faiss(s3_client, s3_bucket) request_body_str = event.get("body", "{}") request_body = json.loads(request_body_str) query = request_body.get("query") index_path = request_body.get("index_path") local_index_dir = "/tmp/index_faiss" await download_files(s3_client, s3_bucket, index_path, local_index_dir) results = search_data( query, local_index_dir, search_type=request_body.get("search_type", "similarity"), score_threshold=request_body.get("score_threshold"), k=request_body.get("k", RESULT_NUM), fetch_k=request_body.get("fetch_k"), lambda_mult=request_body.get("lambda_mult"), ) return create_response(200, results) def retrieverHandler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: return asyncio.run(async_handler(event, context)) def create_response(status_code: int, body: Any) -> Dict[str, Any]: return { "statusCode": status_code, "body": json.dumps(body, ensure_ascii=False), "headers": { "Content-Type": "application/json", }, } async def download_files(s3_client: S3Client, bucket: str, key: str, file_path: str) -> None: loop = asyncio.get_running_loop() await loop.run_in_executor(None, download_files_from_s3, s3_client, bucket, key, file_path) def download_files_from_s3(s3_client: S3Client, s3_bucket: str, prefix: str, local_dir: str) -> None: keys = s3_client.list_objects(bucket_name=s3_bucket, prefix=prefix) if not keys: logger.info(f"'{prefix}'内にファイルが存在しません。") return for key in keys: relative_path = os.path.relpath(key, prefix) local_file_path = os.path.join(local_dir, relative_path) os.makedirs(os.path.dirname(local_file_path), exist_ok=True) s3_client.download_file(bucket_name=s3_bucket, key=key, file_path=local_file_path) 5.4のまとめ setup_faiss による faiss 動的ロードで Lambda レイヤー容量問題を回避 非同期 I/O と S3 利用により、コンテナ化や EFS 接続なしで FAISS インデックスをロード search_data で Embedding 済みインデックスを検索し、RAG が迅速な類似文書提供を実現 これにより、RAG 検索を用いた高速なナレッジ検索が可能となり、LLM 回答に特化した情報提供が実現できます。 5.5 [Python] EmbeddingとFAISSインデックス化 このセクションでは、Confluence ドキュメントなどの社内ドキュメントを Embedding し、FAISS インデックスを作成・更新する定期バッチ処理の例を示します。 RAG パイプラインで参照するインデックスは、生成 AI が社内特有の知識を回答に反映するための重要な鍵です。そのため、定期的なドキュメント更新時には Embedding と FAISS インデックスを再構築し、最新情報を常に参照できるようにします。 処理の概要 S3 から JSON フォーマットのドキュメントを取得 取得したドキュメントを Embedding(OpenAI や Azure OpenAI の Embeddings API を利用) Embedding 済みのテキスト群を FAISS インデックス化 作成した FAISS インデックスを S3 へアップロード これらの手順を Lambda バッチ処理や Step Functions ワークフローで定期的に実行することで、問い合わせ時には常に最新インデックスを用いた RAG 検索が行えます。 ステップ1:JSONドキュメントの読み込み S3 上の JSON ファイル(Confluence ページ等を要約済みのもの)をダウンロード・パースし、 Document オブジェクトのリストに変換します。 import json from typing import Any, Dict, List from langchain_core.documents.base import Document from shared.logger import getLogger logger = getLogger(__name__) def load_json(file_path: str) -> List[Document]: """ JSONファイルを読み込み、Documentオブジェクトのリストを返します。 JSONは [{"title": "...", "content": "...", "id": "...", "url": "..."}] のような形式を想定。 """ with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, list): raise ValueError("JSONトップレベルがリストではありません。") documents = [] for record in data: if not isinstance(record, dict): logger.warning(f"スキップされたレコード(辞書でない): {record}") continue title = record.get("title", "") content = record.get("content", "") metadata = { "id": record.get("id"), "title": title, "url": record.get("url"), } # タイトルとコンテンツをまとめたテキストとしてDocument化 doc = Document(page_content=f"Title: {title}\nContent: {content}", metadata=metadata) documents.append(doc) logger.info(f"{len(documents)} 件のドキュメントをロードしました。") return documents ステップ2:EmbeddingとFAISSインデックス化 vectorize_and_save 関数では、 get_embeddings で取得した Embeddings クライアントでドキュメントを Embedding し、 FAISS インデックスを作成します。その後、ローカルにインデックスを保存します。 import os from langchain_community.vectorstores import FAISS from langchain_core.text_splitter import RecursiveCharacterTextSplitter from shared.logger import getLogger logger = getLogger(__name__) def vectorize_and_save(documents: List[Document], output_dir: str, embeddings) -> None: """ ドキュメントをEmbeddingし、FAISSインデックスを作成してローカルに保存します。 """ # テキスト分割器を使用してドキュメントを小さなチャンクに分割 text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=128) split_docs = text_splitter.split_documents(documents) logger.info(f"{len(split_docs)} 件の分割済みドキュメント") # Embeddingsでベクトル化し、FAISSインデックス構築 db: FAISS = FAISS.from_documents(split_docs, embeddings) logger.info("ベクトルDBの構築が完了しました。") os.makedirs(output_dir, exist_ok=True) db.save_local(output_dir) logger.info(f"ベクトルDBを {output_dir} に保存しました。") ステップ3:インデックスをS3へアップロード ローカルで作成した FAISS インデックスを S3 にアップロードすることで、RAG 検索用 Lambda から容易に取得可能になります。 from shared.s3_client import S3Client from shared.logger import getLogger logger = getLogger(__name__) def upload_faiss_to_s3(s3_client: S3Client, s3_bucket: str, local_index_dir: str, index_s3_path: str) -> None: """ FAISSインデックスをS3にアップロードします。 """ index_files = ["index.faiss", "index.pkl"] for file_name in index_files: local_file_path = os.path.join(local_index_dir, file_name) s3_index_key = os.path.join(index_s3_path, file_name) s3_client.upload_file(local_file_path, s3_bucket, s3_index_key) logger.info(f"FAISSインデックスファイルを s3://{s3_bucket}/{s3_index_key} にアップロードしました。") ステップ4:全体フローをLambdaで実行 index_to_s3 関数は全体フローをまとめています。S3 から JSON をダウンロード、Embedding と FAISS インデックス作成、そしてインデックスを S3 へアップロードします。この処理を Step Functions などのワークフローで定期的に実行し、常に最新のインデックスを用意します。 import os from shared.faiss import setup_faiss from shared.logger import getLogger from shared.s3_client import S3Client from shared.secrets import get_secret logger = getLogger(__name__) def index_to_s3(json_s3_key: str, index_s3_path: str) -> Dict[str, Any]: """ S3からJSONファイルをダウンロードし、EmbeddingとFAISSインデックス作成を行い、インデックスをS3に保存します。 """ env = os.getenv("ENV") if env is None: error_msg = "ENV 環境変数が設定されていません。" logger.error(error_msg) return {"status": "error", "message": error_msg} try: s3_client = S3Client() s3_bucket = "bucket-name" local_json_path = "/tmp/json_file.json" local_index_dir = "/tmp/index" # 必要なら faiss のセットアップ(S3からダウンロード) setup_faiss(s3_client, s3_bucket) # JSONファイルをS3からダウンロード s3_client.download_file(s3_bucket, json_s3_key, local_json_path) documents = load_json(local_json_path) # Embeddingsクライアント取得 secrets = get_secret() embeddings = get_embeddings(secrets) # ベクトル化とFAISSインデックス作成 vectorize_and_save(documents, local_index_dir, embeddings) # インデックスファイルをS3へアップロード upload_faiss_to_s3(s3_client, s3_bucket, local_index_dir, index_s3_path) return { "status": "success", "message": "FAISSインデックスを作成し、S3にアップロードしました。", "output": { "bucket": s3_bucket, "index_key": index_s3_path, }, } except Exception as e: logger.error(f"インデックス作成処理中にエラー発生: {e}") return {"status": "error", "message": str(e)} 5.5のまとめ load_json で JSON ファイルを読み込み、 vectorize_and_save で Embedding と FAISS インデックス作成 upload_faiss_to_s3 でローカルインデックスを S3 へアップロード index_to_s3 で全体フローをまとめ、定期バッチ処理で最新インデックスを作成・更新 これにより、社内ドキュメントを Embedding し、RAG 検索用の FAISS インデックスを作成・更新するバッチ処理を実現できます。 6. まとめ 本記事では、Slack 上で LLM を活用する社内チャットボットしぇるぱの開発背景や技術的実装ポイント、RAG パイプラインの導入手順、Confluence ドキュメントのサニタイズや Embedding/FAISS インデックスによる検索基盤の整備、さらには翻訳・要約などの機能拡張について紹介しました。 こうした仕組みにより、Slack 内で自然な操作で生成 AI を利用でき、社員は新たなツールやコマンドを学ぶことなく高度な情報活用が可能になります。 7. 今後の展望 私たちはしぇるぱをさらに進化させるため次の改善・拡張に積極的に取り組みます。 Azure 環境での構築強化 Azure Functions や Azure CosmosDB などの Azure サービスとの連携を本格化し RAG パイプラインのパフォーマンスや拡張性を抜本的に向上させます。 Azure Cosmos DBベクトル検索の導入 Azure Cosmos DB for NoSQL 上でベクトル検索機能を実用化しより高度な検索を提供します。 @ card AI Document Intelligenceの活用 AI Document Intelligence を積極的に取り込み RAG のナレッジ範囲を拡大させより多彩な情報活用を実現します。 @ card モデルの多様化・高度化 GPT-4o のみならず GPT-o1 や Google Gemini など最新で多様なモデルへの対応を推進し常に最先端のモデルを統合します。 Web UIの実装 Slack 依存の表現上の制約を解消するため Web UI を構築し多彩なインタラクションや新機能を柔軟に展開します。 プロンプト管理の拡充 既存のプロンプトをテンプレート化し用途別に容易な再利用を実現します。またプロンプト共有機能を充実させ社内全体での生成 AI 活用を一層促進します。 マルチエージェント化の実現 要約や翻訳、 RAG 検索など特定機能に特化した専門エージェントを配置し、エージェントビルダー機能で自由に組み合わせることでより柔軟で高度な情報活用を可能にします。 RAG精度の評価・改善 テストセットを構築し回答の自動評価を実施して精度を定量的に把握し継続的な品質向上につなげます。 ユーザーフィードバックを起点とした改善 利用実態やフィードバックを反映し対話フローの最適化やプロンプトチューニング外部サービス連携強化など実運用を通じて常にしぇるぱの利便性と有用性を高めていきます。 私たちはこれらの取り組みによってしぇるぱを持続的に進化させ、より多様なニーズに応えられる力強い社内支援ツールへ成長させます。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の23日目の記事です🎅🎄 1. はじめに こんにちは、KINTO FACTORY でバックエンドエンジニアをしている西田です。 今回はAWSコスト削減をテーマにお話をしたいと思います。 2. コスト削減に取り組んだきっかけ KINTO テクノロジーズでは AWS のサービス利用料を Amazon QuickSight を使って可視化しており、プロダクト単位で利用料を確認することができます。 KINTO FACTORY をローンチしてしばらく経った頃、何気なくプロジェクトのコストを確認してみたところ、なんと社内全プロダクトの中で2番目にコストがかかっていることが分かりました。まだサービスを開始して間もない段階でここまでのコストが発生しているのは想定外でした。この「えっ!?」という発見をきっかけに、アプリケーションチームを主体に私たちのコスト削減への取り組みが始まります。 3. 具体的にやったこと それでは、私たちが実際に取り組んだコスト削減施策について詳しくご紹介していきます。 ECS Fargate のインスタンス数と起動タイプの見直し コストの内訳を眺めてみると、ECS Fargateの利用料が断トツ1位でした。KINTO FACTORYのアプリケーションがECS Fargate上で動いているため当然といえば当然なのですが、このコストを何とか最適化できないか検討することにしました。 まず最初に気づいたのが、「あれ?開発環境のインスタンス数、本番と同じになってない?」という点。開発環境ってそんなにガッツリしたスペックはいらないはず。そこで、開発環境のインスタンス数を必要最低限まで減らすことにしました。 さらに調べてみると、Fargate の起動タイプには通常とは別の Fargate Spot というものがあることがわかりました。Fargate Spot は AWS 上にある未使用のリソースを利用する仕組みで、通常の Fargate に比べて最大70%の割引が適用されます。使わない手はないですね。 Fargate Spot を使用すると、割り込み許容のある Amazon ECS タスクを、Fargate 料金と比較して割引料金で実行できます。Fargate Spot は、予備のコンピュートキャパシティーでタスクを実行します。AWS がキャパシティを戻す必要がある場合、タスクは中断され、2 分間の警告が表示されます。 -- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/fargate-capacity-providers.html ただ、ドキュメントにもある通り Fargate Spot は予備のコンピュートキャパシティーでタスクを実行している仕組みとなるため、AWS 側のリソース調整でタスクが途中で中断される可能性があります。本番環境への適用は難しいので中断されても問題ない開発環境を対象に設定を変更しました。 開発環境の起動・停止の自動化 本番環境だけでなく開発環境も24/365稼働していたため、深夜帯や休日といった利用しない期間は開発環境を停止させるようにしました。 構成としては Step Functions を使って、Application、DB の起動停止を自動化しました。 前段に EventBridge を使い、cron で指定した日時に Step Functions に対して起動・停止のトリガーを送信します。 ポイントとしては DB の起動には時間がかかり起動直後に ECS を起動するとコネクションエラーになってしまうため、DB のステータスを確認してから ECS を起動するようにしています。 参考のため、以下にStep Functionsのワークフローを記述するサンプルコード(YAMLファイル)を示します。 "DB起動": { "Type": "Task", "Parameters": { "DbClusterIdentifier": "${db_cluster_identifier}" }, "Resource": "arn:aws:states:::aws-sdk:rds:startDBCluster", "Next": "Wait" }, "Wait": { "Type": "Wait", "Seconds": 300, "Next": "起動後のDBの状態取得" }, "起動後のDBの状態取得": { "Type": "Task", "Parameters": { "DbClusterIdentifier": "${db_cluster_identifier}" }, "Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters", "Next": "起動完了しているかチェック" }, "起動完了しているかチェック": { "Type": "Choice", "Choices": [ { "Variable": "$.DbClusters[0].Status", "StringEquals": "available", "Next": "各サービスの起動" } ], "Default": "Wait" }, サーバーレスアーキテクチャへの移行 次に、ある機能のバッチ処理を見直していた時に処理自体は1分もかからないのにECSを使った常駐サービスで稼働させていることがわかりました。 こちらもコスト的に効率が悪いと考え、Lambdaを用いたサーバーレスアーキテクチャへ移行しました。 Lambda はリクエスト数と実行時間に応じて課金されるため、短時間で終わる処理や常時起動不要な処理を実行するには最適なサービスです。 4. コスト削減の結果 結果として、コスト削減に取り組んですぐに効果が現れ、ピーク時と比較すると 65% も削減となりました。まさかの半分以下です。 これだけ削減できる余地があったのは正直驚きでした。。。 5. さいごに 今回は私たちのコスト削減への取り組みについてお話しました。 実施したことはどれも簡単で特別なものではなかったですが、大きな成果を出すことができました。 また、取り組んでみて分かったことがあります。コスト削減って、単純に「お金を節約できた!」というだけじゃないんですよね。 無駄なリソースを見直すことで、システム全体の見える化ができた アーキテクチャを見直すきっかけになった 「本当にこのリソースいる?」を考える習慣が身についた その先に得られるメリットも大きいので、改めて取り組みをして良かったなと感じます。 もし似たような課題をお持ちの方の参考になれば嬉しいです。
アバター
はじめに Flutterのマルチパッケージプロジェクトでは、アセット管理、特にローカルJSONファイルの読み込みが課題となることがあります。 通常のシングルパッケージプロジェクトとは異なるアプローチが必要となり、開発者を悩ませることがあります。 この記事では、Flutterのマルチパッケージプロジェクトにおいて、ローカルのJSONファイルを効果的に読み込む方法について詳しく解説します。 この記事は KINTOテクノロジーズアドベントカレンダー2024 の23日目の記事です🎅🎄 今回用意したテストプロジェクト 今回、調査のため、マルチパッケージで管理する簡潔なプロジェクトを用意しました。 このプロジェクトは、以下のような構成になっています。 🎯 Dart SDK: 3.5.4 🪽 Flutter SDK: 3.24.5 🧰 melos: 6.2.0 ├── .github ├── .vscode ├── app/ │ ├── android/ │ ├── ios/ │ ├── lib/ │ │ └── main.dart │ └── pubspec.yaml ├── packages/ │ ├── features/ │ │ ├── assets/ │ │ │ └── sample.json │ │ ├── lib/ │ │ │ └── package1.dart │ │ └── pubspec.yaml │ ├── .../ ├── analysis_options.yaml ├── melos.yaml ├── pubspec.yaml └── README.md Assetにあるファイルを読み込む 一般的なシングルパッケージでのAssetの読み込みは、以下のようにすればできるという説明はよく見かけます。 flutter: assets: - assets/ # アセットフォルダを指定 import 'package:flutter/services.dart' show rootBundle; Future<String> loadAsset() async { return await rootBundle.loadString('assets/sample.json'); } 公式も同じ様な説明をしています。 https://docs.flutter.dev/ui/assets/assets-and-images 実際にAssetからJSONファイルを読み込んでTextにString表示するだけのWidgetを作ってみました。 import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class LocalAssetPage extends StatefulWidget { const LocalAssetPage({super.key}); @override LocalAssetPageState createState() => LocalAssetPageState(); } class LocalAssetPageState extends State<LocalAssetPage> { String _jsonContent = ''; @override void initState() { super.initState(); _loadJson(); } Future<void> _loadJson() async { final response = await rootBundle.loadString('assets/sample.json'); final data = await json.decode(response); setState(() { _jsonContent = json.encode(data); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Local Asset Page'), ), body: Center( child: _jsonContent.isEmpty ? const CircularProgressIndicator() : Text(_jsonContent), ), ); } } しかし、マルチパッケージでのAssetの読み込みは、この方法ではうまくいきません。 🤨 flutter_genを使ってAssetを読み込む 大抵の場合、マルチパッケージでのAssetの読み込みは、flutter_genを使って解決できます。 flutter_genは、AssetやLocalization等のパスからコード生成をして、タイプセーフにアセットの読み込みが実現できるツールで、マルチパッケージのAssetの読み込みもサポートしています。 https://github.com/FlutterGen/flutter_gen flutter_genでマルチパッケージのAssetを読み込むには、以下の設定が必要になります。 flutter_gen: assets: outputs: package_parameter_enabled: true この設定を入れて、flutter_genを実行すると、マルチパッケージのAssetを読み込むためのコードが生成されます。 そこで生成されたコードを使ってタイプセーフにAssetを読み込むことができます。 上記の例をflutter_genを使って書き換えると、以下のようになります。 import 'package:{YOUR_PACKAGE_NAME}/gen/assets.gen.dart'; Future<String> loadAsset() async { return await rootBundle.loadString(Assets.sample); } 実際に以下の様なコードを書くことで、マルチパッケージのAssetを読み込むことができます。 import 'dart:convert'; + import 'package:feature_flutter_gen_sample/gen/assets.gen.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class FlutterGenSamplePage extends StatefulWidget { const FlutterGenSamplePage({super.key}); @override FlutterGenSamplePageState createState() => FlutterGenSamplePageState(); } class FlutterGenSamplePageState extends State<FlutterGenSamplePage> { String _jsonContent = ''; @override void initState() { super.initState(); _loadJson(); } Future<void> _loadJson() async { + final response = await rootBundle.loadString(Assets.sample); - final response = await rootBundle.loadString('assets/sample.json'); final data = await json.decode(response); setState(() { _jsonContent = data.toString(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('FlutterGen Sample'), ), body: Center( child: _jsonContent.isNotEmpty ? Text(_jsonContent) : const CircularProgressIndicator(), ), ); } } ファイルのパスが構造化されるので、とてもキレイですね! チーム開発のAsset管理はこれで安心です。 ですが、なるべくツールに頼らない方法を取りたい場合も多々ありますよね? 次はその方法についてもお話します。 flutter_genを使わないでAssetを読み込む flutter_genを使わずにマルチパッケージのAssetを読み込む方法ももちろんあります。 その場合は、以下のルールでパスを指定することでAssetを読み込むことができます。 :::message packages/ {パッケージ名} / {フォルダパス} /ファイル名 ::: パッケージ名 は、そのassetが格納されているPackageのpubspec.yamlのnameに指定した名前になります。 フォルダパス は、そのPackageのpubspec.yamlのassetsに指定したパスになります。 name: local_asset ... flutter: assets: - assets/ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class LocalAssetPage extends StatefulWidget { const LocalAssetPage({super.key}); @override LocalAssetPageState createState() => LocalAssetPageState(); } class LocalAssetPageState extends State<LocalAssetPage> { String _jsonContent = ''; @override void initState() { super.initState(); _loadJson(); } Future<void> _loadJson() async { + final response = await rootBundle.loadString('packages/local_asset/assets/sample.json'); - final response = await rootBundle.loadString('assets/sample.json'); final data = await json.decode(response); setState(() { _jsonContent = json.encode(data); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Local Asset Page'), ), body: Center( child: _jsonContent.isEmpty ? const CircularProgressIndicator() : Text(_jsonContent), ), ); } } このようにして、flutter_genを使わずにマルチパッケージのAssetを読み込むことができます。 小規模プロジェクトや、個人開発ではこの方法でも十分に対応できそうです。 このパスの作成ルールが理解できておらず、ファイルの相対パスを指定したり、パスの設定を色々工夫してみたりしましたが、 そもそもエラーでビルドできなかったりととても苦戦しました... pubspec.yamlで指定するパスについて ローカルでアセットを管理する時に、pubspec.yamlで指定するパスについても注意が必要です。 assetsのパスは、pubspec.yamlからの相対パスで指定しますが、 /assets と /assets/{サブフォルダ} の扱いにも注意が必要です。 以下の様にJSONファイルをサブフォルダに移動した場合、 ├── packages/ │ ├── features/ │ │ ├── assets/ │ │ │ └── jsons/ │ │ │ └── sample.json <<< HERE │ │ ├── lib/ │ │ │ └── package1.dart │ │ └── pubspec.yaml │ ├── .../ 僕はてっきり /assets と指定すれば、 /assets/{サブフォルダ} 以下のファイルも読み込めると思っていましたが、 loadStringのパスを packages/local_asset/assets/jsons/sample.json に変更しても読み込めなくなります。 サブフォルダに移動した場合は、以下の様にサブフォルダを明示的に指定する必要があります。 flutter: assets: - /assets/jsons/ これで、 packages/local_asset/assets/jsons/sample.json をロードできる様になりました。 サブフォルダで細かくアセットを管理するのであれば、サブフォルダの指定もpubspec.yamlに追加するのを忘れない様にしないといけません。 ちなみに、ここまで  assets  フォルダで話をしていましたが、このフォルダ名も変更できます。 pubspec.yamlと実際のパス構成が合っていれば、 assets フォルダ以外でも問題なく読み込めます。 まとめ 今回はFlutterのマルチパッケージの中でローカルのJSONを読み込む方法についてお話しました。 普段はiOSの開発がメインなので、簡単にできるだろうと思っていたのですが、意外と苦労しました。 マルチパッケージ下での開発手法はまだあまり情報がない様なので、今後もこういった情報があれば共有していきたいと思います。
アバター
Introduction Hello! I am a team member of the KINTO Technologies study session organizing staff. The other day, we held a case study presentation and roundtable discussion session specializing in the field of corporate IT, under the title “ KINTO Technologies MeetUp! Sharing Four Case Studies for Information Systems People by Information Systems People .” Before the study session was held, a previous article on this topic talked about the steps from planning to launching by the organizing staff. In this article, by way of a sequel to that one, I would like to tell you about the various things the organizing staff did in the run-up to holding it. Following the previous session, we hope this will serve as a helpful reference for those looking to start and promote study sessions from scratch within their own organization. The previous article mainly covered the following: Planning the study session Calling for organizing staff members and speakers Discussing the matters still pending Dividing up roles and starting up agile progress management Now, I would like to delve deeper into things like what kinds of roles there were specifically, and what kinds of activities each of them entailed. Preliminary Reporting to Management This study session will be conducted as a "company activity" rather than "by individuals or volunteers". To get the appropriate support from the company, it is extremely important to report to management and get their understanding and advice regarding the activities. Therefore, this time we have decided to report in advance to the management team in the following manner. Reporting to the vice president (who is also the CIO and CISO): First of all, asking about the “holding of the event” itself, and asking for support Reporting to the president: Asking about the objectives, KPIs, equipment to be used, and budget Of course, when it comes to reporting, it is necessary to "set up a place for the report and make preparations in advance." However, by adopting the following format, we were able to carry it out relatively inexpensively. Reporting to the vice president Timing of implementation: Utilize the regularly scheduled departmental report meetings to provide updates. Report materials: We used the original plan document virtually as is, and provided details about the budget at a later date. Reporting to the president Timing of implementation: Report during the regular recruitment team briefing session Report materials: We summarized the plan document in three slides, and gave a simple report based on three points: an overview, the objectives, and the budget. As a result, we received positive responses in both cases, such as "Let's give it a try." Venue Arrangements At KINTO's Muromachi office, there is a photogenic spot called "KINTO TOKYO Junction (commonly known as the Junction)" that is always used for company introductions and external announcements. There is no reason not to use this photogenic spot when holding your first study session. Venue arrangements Junction bears the name of KINTO but is in fact owned by the parent company, Toyota Financial Services. For official use, permission from the parent company is required. However, a request can be made with just an email, and generally approval is given. We got the green light to use it for this event without any problems, too. Building arrangements The venue is a building with a security gate, so we needed to set up a reception desk outside the security gate to welcome outside guests. We notified the building management in advance and asked if we could place a desk there. This was also approved without any problems. Also, when we got permission from the building, we discovered that the automatic doors leading to the entrance were locked outside of business hours (after 6pm) (they can be unlocked from the inside, but an ID card is needed to enter from the outside). We asked the building management how to deal with this, and they agreed to install a small sign. Consultation is vital, right? Venue layout We were given permission to use the facilities, so all that was left was to decide how to lay out the venue! We had initially planned to use several large monitors for the presentation, but then we discovered a projector screen and decided we wanted to use a projector! However, we lacked the all-important projector for that, so we decided to procure one in a hurry, with a view to using it in future as well. (Everyone acted as one team wonderfully for this, too.) Wanting the layout to make the projector the main feature and encourage as many people as possible to take part as much as possible, this is what we came up with: Arrangements for the desks and chairs We used the desks and chairs that were originally there at Junction, but they were not enough on their own... So, we gathered up desks and chairs that could be used within the company. We mainly borrowed desks and chairs from the break space and were able to secure the required number of desks and chairs. On the day On the day, all the operation staff readily sprang into action, and we managed to get everything set up very quickly! The tidying up was also done really quickly, and this gave me a strong “one team” feeling, too. Everyone was amazing! Finally Inviting guests from outside to Junction was also a first for a company event, too. Throughout it all, everyone pulled together as one team to make the company look as good as possible to as many people as possible. We plan to carry on holding more study sessions as an organization in the future, too. To that end, we intend to use the results of this one to make them even smoother to enjoy! Snack Arrangements We decided to hold a social gathering as part of the study session And when it comes to social gatherings, snacks are a must. Right, time to prepare those! Snacks! The classic choice for a study session is pizza. It has been the most frequently provided food at the study sessions I've attended, leaving a strong impression on me We really wanted to make an impact right from the get-go, but since it was a first for everyone, we could not do anything over-the-top. So, we sensibly opted for pizza. Calories are right and just. And if there is pizza, there has to be cola. We were thinking that having these two would do the trick, but we were told from on high to provide some other drinks besides, so we prepared some alcoholic ones as well. How much should we provide? — Pizza — We had never paid any mind to how much there had been at the other study sessions we had taken part in up to then. How much would do...? The L-size pizza we were thinking of offering is 12 slices, and generally serves three to four people. This time, it's treated as a "light meal," so we decided to treat it as 5 people per slice. Even in the study sessions I've attended, people usually eat about 2 slices. We also took that personal experience into account. In the end, we arranged 10 pizzas for 50 people, thinking of a maximum of 40 participants plus the 10 operation staff. How much should we provide? — Drinks — For this, too, we also based the calculation on personal experience. Figuring that we usually drank around two or three cans per event ourselves...we decided to get three cans per person. The breakdown we decided on was to provide alcoholic drinks and soda in the ratio 1:1. Assuming 40 participants, that meant arranging 120 drinks. However...plastic bottles would be handier for the soda. So, we switched to those, and also ended up ordering 48 (two boxes of 24), because it was a convenient quantity in terms of placing an order. We prepared a total of 60 alcoholic drinks. Together with the 48 bottles of soda, this gave a grand total of 108 drinks. When should we arrange them by? We arranged the pizza the day before and the drinks three days before. We took delivery of all of them without any trouble. What were the results like? Pizza: None left over Alcohol: Eight left over → 52 consumed Soda: 21 left over → 27 consumed There were around 20 participants on the day, and the results were as above. This includes the food and drinks consumed by the staff on the day as well. In terms of the pizza, I feel there was not enough (because the operation staff did not get to have any of it). This is because the format of the social gathering was a "roundtable discussion" with KTC staff, and the time spent listening to each other was long, which meant more time spent eating. That is our analysis. Also, in terms of the drinks, we think the alcohol was just right, but our impression is that there was too much soda. Some people said they wanted tea, so we felt it would have been better to provide that instead of soda. In conclusion This was our first time providing snacks at a study session. So, while there were many things we were not sure about, I think we somehow managed to provide them without incurring any major dissatisfaction. We hope to provide them in a better way the next time we hold the session! Producing a Novelty Item HOS-PI-TAL-I-TY KINTO Technologies is a new company that was established in April 2021. Also, the format for holding this event was offline, which means people needed to come all the way to the venue at our company. There would be snacks and drinks like the ones mentioned above as well, but to make the effort even more worthwhile, we wanted to prepare something for everyone to: take home with them and remember the company by. To that end, we decided to produce a novelty item. How to make it happen? The members of the organizing staff discussed the matter together, and the unanimous result was, “Let’s make a novelty item!” And so, a team was launched to produce one. 1. What to make? The first thing we did was to decide what kind of novelty item to make. Of course, there were budgetary constraints as well, and when batting ideas around, we also pictured the kinds of people we were anticipating would attend. ![Material used to consider the novelty item](/assets/blog/authors/tomori/ノベルティ検討資料.png =400x) Part of the materials we used to bat ideas around As a result, we decided to make a sticker this time! Lessons learned: It's important to ground your endless thoughts in a practical place. 2. How to make it? We had decided to make the item a sticker. We had a track record of making stickers for other in-house events, so we consulted with the design department, who have the know-how, about the purpose of the event, and with the corporate engineer about the expected attributes and interests of the visitors, and this is what we came up with! Ta-da! Based on these two patterns, we discussed which one would yield the most value as a novelty item for this event. As a result, we decided to go with the white-background, corporate-colors version! How it turned out Then, we steadily proceeded with placing the order and settling the payment, and awaited the delivery... Ta-da! (For the second time.) Production was completed without a hitch in time for the event! We hope that this novelty created with a one-team spirit spanning various departments will go on to be used in many places in the future as well, and help people know about KINTO Technologies. Promotion and Measurement Summary We thought about how to measure the results of this study session and how to announce the event itself. With the aim of gathering material for reporting to the company and indicators for doing kaizen (continuous improvement), we promoted the event as much as possible while measuring the influx into it and the impact of the event itself. We will share what we actually did, what we thought, and the results of our implementation. Background The first thing we thought about was reporting the results to the company. We had said that the study session would raise awareness of KTC, so for reporting its results to the company, we needed to gather data to show how well it had done so (the number of participants being a prime example). And since we wanted to hold the study session itself a second and third time as well, we figured we would need to have indicators for doing kaizen . Above all, since this was the first study session we held for outside the company, we honestly had no idea how many people would attend. To do our utmost to avoid winding up with zero participants, we tried out as many announcement methods as we could. In the end, we roughly settled on collecting the following indicators. Which channels had there been influx from, and how much? How much had the study session raised awareness of KTC? What we did and thought In order to visually represent what indicators we wanted, first, we wrote down the structure of the influx. Since we decided to accept entries to the event via connpass, we measured the influx into that. We also decided to measure how well the study session and roundtable discussion had helped raise awareness. Since it was an info sys event, we decided to announce it on the info sys Slack. In addition, since KTC has a Tech Blog and X (formerly Twitter) account, we asked the team operating those to announce it as well. We also used the in-house Slack to call on all employees to spread the word about it via social media. As results of the study session and roundtable discussion, we decided to prepare an X hashtag and pick up the number of posts and impressions that had it. Also, we decided to run a survey asking people if they had already known about KTC, measure the percentage who had not, multiply that by the total number of impressions, and use the result as a half-forceful way to measure the reach among people who had not known about the company before. Finally, after the event, we decided to measure how the influx and following on our company’s X account, corporate website, and Tech Blog changed, and how they changed from before the event to after it. Results Many people cooperated with spreading the word, and a total of 56 people applied to take part, so the capacity of 40 was filled up. On the day, 31 people actually came! The influx per channel for connpass was as follows: Up to the day of the event, there were 61 posts on X, and a total of 28,608 impressions. Of these, 376 were accessed, so 1.31% of the people who had viewed us on X had come to our connpass. Our survey revealed that 39% of the event participants had not originally known about KTC. If we apply this to the 1,356 visitors to connpass, that means that 39% = about 530 people did not know about KTC before, so we succeeded in making that many people aware of KTC!! (A bit of a stretch) Comparing before and after the event, the number of followers of our in-house X account increased slightly. The number of pageviews and unique users on our corporate website and tech blog were slightly higher than usual on the day of the event. However, we were able to see that it definitely had an effect, even if only a little, so we will continue to measure and improve from the second time onwards! Announcement website preparation Summary We opted for connpass as the medium for announcing the event, and prepared an information page about it. We learned about what kinds of events are popular from the top-ranking ones, and implemented our findings for our own event. connpass itself is a service that lets you to create events extremely easily, so creating the page was no struggle at all. This enabled us to put all the more thought into setting the target and deciding what message to convey. Thoughts Seeing as it was going to be the first event we had ever held, we were terrified of getting zero participants... So, we resolved to create the best page that we could at the time. What we did and thought First, we submitted an internal application to use an external service. If it is the first time a business is doing business with/using the service, it is necessary for the relevant department to check, but I cleared this without any problems. Then, we decided on a management method for making the application. After briefly defining the purpose of management, the objects to be managed, and the purpose of use, we designated the system and account administrators, and then simply determined the specific account management method. Once our application was approved and we could actually start working with connpass, first of all, we identified and checked out all the configurable items. This is because we wanted to leave it as a guide for holding future events, and also because we wanted to see if there were any irreversible settings. As a result of checking the items within the scope of an offline event with no participation fee, we found that the following two were the only ones that were unchangeable or restricted. Group: Unchangeable. Participation details and participation slots: Participation slots can be deleted only while there are no applicants . Most of the items were changeable. However, if the date, time, or venue were to change, participants would be confused, so I think it's better to add, but not change or delete. In addition to checking the specifications for the event information page, we also analyzed the popular events. We looked at the event rankings on connpass and picked out around 20 events that had the same attributes as our own and were extremely popular for some reason. We looked at them one by one, extracted and abstracted the elements that we felt were important, and applied them to our event information page. Now that we knew the specifications and points to consider, we once again solidified the basic design. Before we could move on to the detailed design of the "Event Information Page," we first clarified the purpose of the study session and verbalized the following: 1) the target users, 2) the message, and 3) the basic rules for communication. (1) The target users We set the target to be corporate engineers who are actively into study sessions. We provisionally set the needs “Want to know about case studies from other companies” and “Want to connect with outside info sys people,” and created wording for the event information page in relation to those. (2) Message We opted for a message along the lines of “Let’s talk candidly about how other companies do info sys!” Wanting to spread study session culture is the theme of KINTO Technologies MeetUp!, and we want to convey our own ideas and so on, and hear other people’s as well. In order to achieve that, we wanted to provide the framework of a roundtable discussion, and create a forum where people would be able to talk to each other free from the distinction of internal versus external. (3) Basic rules for communication We made it a rule that the event should be a forum for input and output as means of studying. We firmly suppressed any thoughts like “Well, since we have gotten them to come, it would be a shame not to advertise KTC’s business, recruitment, and what makes it so great...,” and thoroughly stayed true to the concept of “a study session for info sys people by info sys people.” When it came time to open the event to the public, from a technical standpoint, we decided to start with a low capacity and then increase it if necessary. It seems that there is a lot of last minute demand for tickets that are at the very limit. It's not embarrassing if there are few applications (important) Results This is how it turned out: The first KINTO Technologies MeetUp! event’s connpass page Our takeaways from looking at popular pages were the following: People are more likely to attend if it is clear who the event is aimed at and what they will gain from participating. People are more likely to come an event if it seems safe. Famous companies and people, etc. Having images and videos that show the atmosphere of past events might also be good. Hot topics (for example, ChatGPT) seem to be a draw for people. We also used notification emails to remind people just before the day. If there is a gap between the announcement and the event itself, we recommend giving people a well-timed reminder about it. This will also serve to keep the participants and operation people motivated. Coordinating the Session Overview Here's a comprehensive summary of the arrangements for the day. Specifically, these included creating a timetable for the event day, planning the roundtable discussion and deciding the venue layout, compiling the presentation materials, and managing the event’s progress. Although it was our first event, all of our staff proactively looked for work, and as a result, we were able to complete the event without any major issues and have a great time! The key point It was our first-ever event Actions/Things to consider Creating a timetable for the event day Planning the roundtable discussion and deciding the venue layout Compiling the presentation materials Managing the event’s progress Execution result We were able to complete the event without any major issues. As it was our first event, and we had no idea what would happen, all the staff were on edge with adrenaline. I think the event was a success precisely because each and every staff member actively looked for jobs that were not even someone’s role and tasks that were not even tasks! Preparation of the Case Study Presentation Materials by the Speakers Case study presentations were going to be the main feature of this study session. Since the speakers-to-be had varying levels of experience of taking the stage at study sessions, after broadly deciding on the process for preparing the materials, we met up periodically to make sure no one is left behind as we proceeded with the work. Making the slide templates uniform Based on the judgment that the presentation materials should look organized, we decided to use uniform templates. Slide templates for in-house use already existed, so we were able to agree on this matter smoothly. Setting deadlines that would serve as milestones Setting a deadline that was right before the last minute would lead to uneven progress from person to person, so instead, we split up the process of creating the materials based on the following milestones, and used these to keep everyone on schedule. Initial short-term deadline: We set a rough deadline of “in two weeks’ time,” and checked up on everyone then. Show-each-other meeting: We held a meeting to check each other’s progress on creating the materials, stipulating that while no one would be told off for falling behind, they would certainly be urged to get a move on. Review by the vice president: We set a deadline for the finished versions, with a view to getting the go-ahead from the vice president in advance. Pre-rehearsal: We held a rehearsal among the presenters a week before the event, to get the presentations even more polished. As a result, in the final pre-rehearsal, everyone had virtually finished their preparations in terms of getting their materials complete and staying withing the allotted time, and we could now look forward to the event day with peace of mind. Lastly Thanks to the organizing staff working so proactively, we were able to make it to the event day like this without a hitch. I think this is a case study that truly embodies the philosophy of “ One Team, One Player ” that we champion as our working stance. Thank you to everyone who took part, and to everyone who got involved on the operation side as well! In addition to this Tech Blog article, at a later date, we are planning to release one that talks about the event from the operation staff’s perspective and one about the case study presentations by the speakers. Please do check those out as well!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の22日目の記事です🎅🎄 はじめまして! KINTOテクノロジーズでUnlimited(Android)アプリを開発しているkikumidoと申します。 弊アプリでは、 GraphQL クライアントとして Apollo Kotlin を使用しています。 Apollo Kotlin v4では、パフォーマンスの向上や新機能の追加など、多くの改善が行われています。 これらの利点を活かすため、私たちはv3からv4へのアップグレードを決定しました。 本記事では、今年の7月にリリースされたApollo Kotlin v4への移行作業の流れと、その過程で遭遇した課題について詳しく説明します。 当初はスムーズな移行を期待していましたが、予想外の例外に直面し、解決策を見出すのに苦労する場面もありました。 この記事が、これからバージョンアップを検討している方々にとって、有益な情報源となれば幸いです。 v3からv4への移行 公式サイト に従い対応していきます。 尚、 Android Studioにプラグインをインストールすることで、半自動でバージョンアップすることも出来ます。 必要な対応を確認しながら移行作業を実施したかったので、私はプラグインを使わずに愚直に手動で対応しました。 一方、プラグインでどこまで自動でできるのかにも興味があったので、手動の場合との作業方法の比較を後半に記載します。 1.対応必須箇所 v3からv4への移行で、弊アプリでAndroid Studio上でエラーを無くす為に必須だった対応は5つあります。 以下に対応内容をひとつずつ記載していきます。 :::message alert アプリの実装状況により対応内容が異なる可能性は大いにあります あくまでも弊アプリの対応内容であることはご留意ください ::: 1.1.ライブラリのバージョンアップ 何はともあれ、まずは ライブラリ のバージョンアップをします。 apollographql = "3.8.5" apollo-runtime = { module = "com.apollographql.apollo3:apollo-runtime", version.ref = "apollographql" } apollographql-apollo = { id = "com.apollographql.apollo3", version.ref = "apollographql" } ↓ // 記事執筆時はv4.1.0がリリースされていますが、移行当時の最新である4.0.1で記載しています apollographql = "4.0.1" apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollographql" } apollographql-apollo = { id = "com.apollographql.apollo", version.ref = "apollographql" } ふむふむ。 com.apollographql.apollo3 が com.apollographql.apollo4 になったのではなく、「3」が消えましたね。 1.2.gradleファイルの修正 v3の時は不要でしたが、v4ではserviceで囲う必要があります。 apollo { packageName.set("com.example.xxx.xxx.xxx") ・・・ } ↓ apollo { service("service") { packageName.set("com.example.xxx.xxx.xxx") ・・・ } } 1.3.importの変更 先述の「3」が消えた時に予想できましたが、importのパッケージ名を変更する必要があります。 「3」を消すだけです。 import com.apollographql.apollo3.* ↓ import com.apollographql.apollo.* 1.4.例外処理を修正 execute() でフェッチエラーがスローされないように変更になりましたので対応します。 プロジェクトごとに対応方針が異なると思いますが、私たちは一旦既存処理への影響を最小限に抑える対応を実施することにしました。 v3の時にフェッチエラーが発生し ApolloException をスローしていたのと同じ条件の時に、 DefaultApolloException をスローするように修正する方法です。 共通処理を修正することにより、共通処理を呼び出している側では実装を修正することなく、既存と同じ挙動にできました。 apolloClient.query(query).execute() apolloClient.mutation(mutation).execute() ↓ execute(apolloClient.query(query)) execute(apolloClient.mutation(mutation)) private suspend inline fun <D : Operation.Data> execute(apolloCall: ApolloCall<D>): ApolloResponse<D> { val response = apolloCall.execute() if (response.data == null) { response.exception?.let { // response.dataがnullかつresponse.exceptionがnullでない場合はフェッチエラー throw DefaultApolloException(it.message, it.cause) } } return response } 既存実装やv4対応の方針により一気に移行することが難しい場合は、v3の挙動を変えずに移行する為のヘルパーとして executeV3() が準備されていますので、そちらに一旦置き換えることも可能です。 apolloClient.query(query).executeV3() apolloClient.mutation(mutation).executeV3() 機能毎など、徐々にv4への移行を進めたい場合はこちらを使うとよさそうです。 :::message alert executeV3() は非推奨メソッドなのでいずれは対応が必要になるはずですのでご注意ください ::: 1.5.ApolloExceptionをDefaultApolloExceptionに修正 ApolloException が sealed class になったため、インスタンスを生成していた箇所を DefaultApolloException に置き換えます。 Android Studio上でエラーを無くすために必要な対応は以上となります。 2.ビルド エラーも無くなったので・・・・ それでは、待ちに待ったビルドを実行してみましょう! ダララララララララララララララララララララララララララララララララ(ドラムロール)・・・ジャン! はい!出ました! ビルドエラー・・・! ・・・まあ、そんなにすんなり行くとは思っていませんでしたが・・・地味にショック・・・ さて、気を取り直してエラーログを確認していきます。 2.1.エラーログを確認 見慣れないエラーログが大量に吐かれていました。 要するに「KSP[^1]でAssistedInjectProcessingStep[^2]の時に必要なClassがないよ」ということらしい。 [^1]:弊アプリでは、 KSP を使用しています [^2]:弊アプリでは、依存関係インジェクションライブラリとして Hilt を使用しています If type 'error.NonExistentClass' is a generated type, check above for compilation errors that may have prevented the type from being generated. Otherwise, ensure that type 'error.NonExistentClass' is on your classpath. e: [ksp] AssistedInjectProcessingStep was unable to process 'XXXXXViewModel(java.lang.String,long,com.xx.xx.XXXXXRepository)' because 'error.NonExistentClass' could not be resolved. もちろんソース上は存在するクラスですし、本対応前は問題なくビルドが成功していました。 v4移行で必要な対応が漏れていないか?など色々調べましたがなかなかわからず・・・ 色々調査した結果、下記3種類の方法でビルドを成功させることができました。 A. Hilt を KSP から kapt に戻す B. build.gradle.ktsに追記する:パターン1 androidComponents { onVariants(selector().all()) { variant -> afterEvaluate { val variantName = variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } val generateTask = project.tasks.findByName("generateServiceApolloSources") val kspTask = project.tasks.findByName("ksp${variantName}Kotlin") as? org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool<*> kspTask?.run { generateTask?.let { setSource(it.outputs) } } } } } C. build.gradle.ktsに追記する:パターン2 apollo { service("service") { packageName.set("com.example.xxx.xxx.xxx") // 追記start outputDirConnection { connectToAndroidSourceSet("main") } // 追記end ・・・ } } ちなみに Hilt の AssistedInject を使用している(定義方法によっては問題ない場合もあるようです) Hilt に KSP を使用している 先述の「1.2.gradleファイルの修正」で service で囲う 上記の条件が揃っていればv3でも再現しますので厳密には「v4対応」とは言えませんが、同時に対応したので記載しておきます。 弊アプリでは最初「B」で対応していましたが、ApolloExtensionの service の不具合[^3]で「C」で対応できることがわかった為、「C」を採用しました。 [^3]: Issues → 記事執筆時最新のv4.1.0でも再現しました 以上でビルドが成功し、アプリが起動・既存と同じ挙動をするようv4へ移行することが出来ました! 3.非推奨箇所の対応 続いて、非推奨でワーニングとなった以下の2つに対応します。 3.1.ApolloResponse.Builderを修正 @Deprecated("Use 2 params constructor instead", ReplaceWith("Builder(operation = operation, requestUuid = requestUuid).data(data = data)")) とのことなので、その通りに修正します。 ApolloResponse.Builder(operation(), UUID_CONST, data).build() ↓ ApolloResponse.Builder<D>(operation(), UUID_CONST).data(data).build() data が外に出ましたね。 Builderパターンに則った形式に変更されたようです。 3.2.Errorインスタンス生成処理をBuilderに修正 Errorインスタンス生成でコンストラクタを使用していた箇所を、Builderを使用するように修正します。 Error("occurred Error", null, null, mapOf("errorCode" to responseCode), null) ↓ Error.Builder(message = "occurred Error") .putExtension("errorCode", responseCode) .build() 以上でワーニングを消すことも出来ました。 4.プラグインでの移行 Android StudioでApolloのプラグインを使用することによりある程度自動で移行してくれます。 実行方法はとても簡単で、 Android StudioでApolloのプラグインをインストールし Tools > Apollo > Migrate to Apollo Kotlin 4... をタップするだけです。簡単ですね。 ![Migrate to Apollo Kotlin 4](/assets/blog/authors/kikumido/plugin.png =500x) では実行結果を確認します。 先述の 1.1.ライブラリのバージョンアップ 1.2.gradleファイルの修正 1.3.importの変更 は自動で移行してくれました。 1.4.例外処理を修正 は executeV3() に置き換えることしか行われないので、v4として適切な対応は手動で行う必要があります。 1.5.ApolloExceptionをDefaultApolloExceptionに修正 は移行してくれませんでしたので、対応は手動で行う必要があります。 文字通り、機械的にできる箇所は自動で移行してくれるので、 プラグインを使用して移行した上で、必要な箇所のみ手動で対応する方法もありだと思います。 ちなみに このプラグインは未使用フィールドをグレー表示してくれたり、移行時以外も活躍してくれるので、インストールしておくと良いと思います。 5.まとめ Apollo Kotlinをv3からv4へ移行した時の話は以上になります。 実際に対応した時から時間が経過してしまったので「すでに対応済み」という方も多くいらっしゃると思いますが、何かしらお役に立つものがあると嬉しいです。 最後までお読みいただきありがとうございました。 6. 関連リンク https://www.apollographql.com/docs/kotlin/migration/4.0 https://www.apollographql.com/docs/kotlin/testing/android-studio-plugin https://developer.android.com/build/migrate-to-ksp?hl=ja
アバター
はじめに KINTOテクノロジーズ株式会社(略して、KTC)でプロダクトマネージャーをやっている、super_spa3(中の人は、ナカノヒロフミ)です。 今回は、KTC初の海外カンファレンスとして、WWDC24に参加してきましたので、参加からしばらく経ってしまいましたが、その間のアップデートも含めてテックブログにまとめていこうと思います。 あれ?iOSとどういう関わりが?と思いますよね。 実は、ナカノ、社会人キャリアをスタートしたときにiOS開発にアサインされ(それまでは趣味でAndroidをかじってました)、2社目のKTCにはiOSエンジニアで入社、とあるきっかけがあって、現在はプロダクトマネージャーをしています。 仕事だと、アプリのリリースでApp Store Connectをよく使ったり、個人でもApple Developer Programに登録して開発をしています。 そんなナカノが関わっているアプリは、KINTO かんたん申し込みアプリという新車のサブスクKINTOをアプリでサクサク見積りから審査までができるアプリです。 https://kinto-jp.com/entry_app/ WWDC24参加のきっかけ WWDCへの参加は毎回抽選という形で3月あたりにAppleからApple Developer Programに登録しているメンバーにお知らせメールが送られてきます。 そのお知らせ先のリンクから応募を押して、抽選で当たったメンバーが現地のイベントに参加できるというわけです。 ただ、ナカノも前職のときに何度か応募をしてみましたが、なかなか当たらず、今回もそうだろうなと思い、あまり期待せず応募を押しました。 抽選結果当日、メールの受信箱を開くと、"Great News, Hirofumi!"という内容のメールがあり、内容を読んでみると抽選に当たった趣旨のメールでした。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Accepted.png =750x) 間違いなく何かいい知らせが伝わる当選メール 所属するモバイルアプリ開発グループ内で当選したのはナカノだけだったらしく、小寺社長に打診したところ、無事、承認され参加できることが決まりました! 前夜祭 at Apple Infinite Loop Campus メインイベント前日にAppleの旧社屋であるApple Infinite Loop One Campusでグッズの受け取りとネームバッジをもらいました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Apple_Infinite_Loop_Entrance.jpg =750x) Infinite Loop Oneのエントランス、すごくミニマリスティックでおしゃれ ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Badge.jpg =750x) 期間中のネームバッジ、TOYOTA FINANCIAL SERVICE CORPORATIONでアプリを出しています 毎年SWAGの内容が違うようで、今年はレジャーシートが入っていました。コロナが明けて自由に移動ができるようになったのでピクニック用によさそうです。 青空コーディングとか、どうでしょう。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Swag.jpg =750x) WWDC24のSwagとApple Parkでもらえた追加のピン ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Pins.jpg =750x) Apple Vision Proのバッジがかっこいい! 会場に入って、みんなWWDC24のモニュメントの前で記念撮影、中庭でのイベントだったため、ちょっとTBSのアナザースカイみたいなポーズをしてみました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Board.jpg =750x) 立体的なWWDC24のボード、この後ここで写真を撮りました ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Apple_Infinite_Loop_Garden.jpg =750x) ここが私のアナザースカイ、Apple Infinite Loop Campusです(笑) 食べ物と飲み物を持って、会場に来ているAppleエコシステム内で開発している人たちと交流しました。 今回一人で行ったため、話し相手を探すために、空いている席があったら「相席いいですか?」と聞いて、お互いどういうものを開発しているのか話しながら交流を深めました。 メインイベント at Apple Park メインイベントの朝は早いです。朝7時にチェックインだったのですが、早めに乗り込みに行くという人たちを事前に聞いていたので、自分も予定より結構早く7時前にApple Park横のApple Park Visitor Center(Apple Storeにカフェ、ジオラマ、テラスが併設されている)で整列をしました。 日本人は朝早いのが得意なのか、先頭には結構な人数の日本人のグループが今か今かと待ちわびていました。8時入場近くになって、WWDCコールが発生し、みんなテンション高く「ダーブダーブ、ディーシー!」と叫んでました。(もちろん、しっかりテンション高く叫びました) 8時になってゲートオープン、Keynoteの会場まで行き、前から6列目の席を確保することが出来ました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Keynote_Stage.jpg =750x) めっちゃ近い! ここからは、10時までの間は自由時間のため、朝食とApple Parkの散策、日本で深夜遅くにKeynoteを見るために準備している東京のメンバーとZoomをしました。そのときのツイートがこちら https://twitter.com/ioco95/status/1800239213851574355 ステージ右側の席だったため、裏手にTim Cook、Craig Federighiがスタンバイするところを見られました。今年のCraigはめっちゃアクロバティックに動いていましたね。 今回のKeynoteではApple Intelligenceが大きな目玉となりましたが、個人的には、Math Notesによる書いた数式を計算してくれる機能や、Apple Vision ProのvisionOS 2からMac仮想ディスプレイがウルトラワイドモニターのように広くなるところがワクワクしました。(まだ、Apple Vision Proは持っていませんが) お昼もCaffe Macsの両サイドにいろんな地域のご飯が選べて、何回も並んでいろんなランチを食べた人もいました。 午後は、Platform State of the UnionをApple Parkの中で見ました。 In-person labsでの出来事と散策 そして、いよいよ、In-person labsの時間です。この時間はエリア分けされたところにAppleのエンジニアやスタッフが質問や課題を見てくれる時間です。自分は、まず事前にDesign Labsの予約を入れていたので、3階のエリアに上がりました。 ![会場内にエリア分けされたIn-person labs](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Labs_Session.jpg =750x) Design Labsでは、自分が担当しているKINTO かんたん申し込みアプリについて、Apple社員の率直なフィードバックとデザイナーさんが課題に思う部分を聞くことができました。 なかなかApple社員の方からフィードバックをもらうということができないので、この機会はとても貴重でした。 1つ、Design Labsでの教訓として、もし、アプリが日本限定公開になっている場合は、TestFlightを使うなどして日本国外からでもアクセスができるようにしましょう。自分は事前予約フォーム内にAppStoreのURLを貼って応募したのですが、 Appleの中の人だから大丈夫だろうと思っていたら、日本限定になっていたのでアプリを事前に確認できなかった、といわれてしまったため、限りある時間の中でアプリを説明して課題と質問について回答してもらいました。(みなさんもお気をつけください) Design Labsが終わったら、社内のエンジニアからもらった質問の答えをもらいにいくつかのエリアを渡り歩き、合間にApple Park内(中庭とビル内)を散策しました。 ![Apple Parkの中庭を背にイエイ](/assets/blog/authors/hirofumi.nakano/WWDC24/Apple_Park_with_Rainbow.jpg =750x) 写真を撮りたそうにしている人がたくさんいるので、「写真撮りましょうか?」と声かけて、そのままお友達になりましょう OMT Conf参加 実は、WWDCの現地イベントは、日、月、火の3日間しかないんです。セッションは月曜日から金曜日にかけてですが、現地ではそうではありません。 そこで、Apple ParkがあるCupertino近くのホテルで開催された、One More Thing Conference(略して、OMTConf)にも火曜日から金曜日参加してきました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/OMTConf_Entrance.jpg =750x) よく聞くスポンサーさんがずらり ![](/assets/blog/authors/hirofumi.nakano/WWDC24/OMTConf_Badge.jpg =750x) ここでもバッジゲット。Pro Ticketと書いてあるが、実は無料のチケット OMTConfは、スピーカーセッション(Main Room)と各エキスパートにテーマ別に相談ができるセッション(Big TentとSmall Tent)に分かれた構成になっていました。 気になるテーマのスピーカーを聞いたり、相談したいテーマがある場合にテントの方に言って喋ったりしていました。 その他、木曜日の午後には、Paul Hudsonさんのワークショップがあり、元々はSwiftDataに関するワークショップを企画していたそうなのですが、WWDC24ではそこまでアップデートがなかったから急遽テーマを変えて、What's New in SwiftUIというテーマで3時間ほどワークショップを実施しました。 いつも、Hacking with Swiftをよく見るのですが、初めてのPaulさんのワークショップに参加して思ったのが、めっちゃ教えるのがうまいのと、質問に対する回答もめっちゃ速かったです。 Swift Social 2024とCore Coffeeに参加 WWDCやカンファレンスに参加する以外にコミュニティイベントにも2つほど参加しました。 1つは、Swift Social 2024。このイベントはSwiftコミュニティが主催したイベントで、サンノゼ市内のバーを貸し切って、誕生10周年のSwiftをお祝いしました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Swift_Social_Board.JPG =750x) 祝10周年! ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Swift_Social_Sign.JPG =750x) おしゃれなSWIFTロゴ看板 イベント内では、Swiftにまつわるクイズ大会(Kahoot)で誰が一番早く答えられるか勝負し、上位の人たちはSwiftのグッズをもらえました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Swift_Social_Play_Kahoot.jpg =750x) 結構いいところまでいったんですが、残念! 2つ目は、Core Coffeeです。Core CoffeeはWWDC期間中、毎日別の場所で開催がされており、自分が参加した回はApple Parkの隣にあるApple Park Visitor Center屋上にあるテラスでの開催で、Apple Parkに近いからか、何人かAppleの社員の方も参加されていました。 WWDC24で発表されたトピックでどれが気になる?という話題が盛り上がったりしました。そして、やはりみなホーム画面のTint色が果たしてどう影響するのか、好き嫌いが結構激しかったです。他、Apple社員の方のワークスタイルやどのプロダクトに関わっているのかも聞くことができ、結構身近に感じることが出来ました。 海外カンファレンスに参加するための基準整備中 WWDCに参加したあと、アメリカ西海岸で他にもいくつかのカンファレンスが開催されたため、KTCからも数名海外カンファレンスに参加してきたメンバーが増えました。 また、社内でも海外カンファレンスに参加するための基準の整備に取り組んでいます。 海外のカンファレンスに参加することで刺激を受け、持ち帰ってきて個人・チームのモチベーションにつながるきっかけになるといいなと思っています。 まとめ 今回初めて念願のWWDCに参加して、開発欲への刺激と仲間作りが個人的に印象に残りました。 実際、WWDCに参加する人はみな、Appleエコシステムの何かしらのアプリを開発していて、どんなアプリを開発しているのか、どういうポジションの人なのか、普段開発しながらどういうことを課題に感じているのかなどを知ることが出来ました。(もちろん、開発者だけではなく、プロダクトマネージャーなどの参加者もいました) また、メインの仕事がアプリ開発ではなく、趣味としてアプリ開発をしている人も何人か会うことができ、とても刺激をもらいました。 他、日本から参加した開発者の人たちと知り合ったり、期間中に話しかけた人ともその後日本に帰って顔馴染みになったりしました。 Appleエコシステムのプロダクト開発に関わる人であればぜひ参加を検討してみてください、世界中から集まる開発者の人たちと交流するのが楽しいと思います。
アバター
This is the Day-2 post of the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello! I am Hasegawa , an Android engineer at KTC! This article highlights common mistakes developers often make when working with the Application class in Android development and offers solutions to prevent them. What is the Application Class? The Application class in Android can be described using the official documentation as follows: "Base class for maintaining global application state. It is instantiated before any other class for your application/package is created." This means that it is responsible for maintaining a global state and is instantiated before any other class in the application. While implementation may vary depending on the project, the Application class is generally used for tasks such as initializing libraries used throughout the app or configuring Dependency Injection (DI). class MyApplication: Application() { override fun onCreate() { super.onCreate() // Library initialization // DI setup } } What would happen if you made an API call to fetch data from the server during the application startup? class MyApplication: Application() { override fun onCreate() { super.onCreate() // Library initialization // DI setup // API call } } At least in my experience, I’ve come across code like this more than once. Although it may not cause immediate issues, it could lead to problems down the line. In this article, I’ll discuss scenarios where this code might lead to issues. The Connection Between the Four Android App Components and the Application Class To understand why making API calls in the Application class can be problematic, it’s essential to have a good grasp of Android’s 4 app components . The diagram below shows these four components and the typical features associated with each. Activities are primarily responsible for managing the app's user interface and navigation between screens, making them the most commonly used component. Apps that provide notification functionality often rely on Services. Apps with widget functionality typically use Broadcast Receivers. Content Providers may be less commonly used, but they are useful for sharing your app’s data with other apps. It's important to note that the Application class is instantiated whenever any of these components are active. Notably, components other than Activities can run even if the user hasn't explicitly opened the app. For instance, in apps with widgets, the Application class may be instantiated when the device restarts, triggering the widget to be recreated. At this point, if the Application class includes API calls, they might be triggered even when the user hasn’t opened the app, potentially occurring at times unintended by the developer. In apps with notification functionality, the Application class might be instantiated when a push notification is received If push notifications are sent to multiple users simultaneously, API calls could be triggered at the same time, potentially causing a situation similar to a DDoS attack. This issue might only surface later, such as when the user base expands. Hence, it’s crucial to recognize this risk and include it in your knowledge base. What If You Want to Make an API Call at Startup? There are many possible solutions, so there is no single “correct” answer. However, here’s one example: Data should be fetched only when and where it is necessary, and in the precise amount needed. Hence, it’s best to retrieve data within the context of each of the four components. If the fetched data needs to be reused across components, you can persist it locally, store it in the Application class, or use dependency injection (DI) to store it in a class with a Singleton lifecycle scope. Conclusion Thank you for taking the time to read this article. Today we explored the Application class lifecycle, highlighting key implementation considerations to keep in mind. Specifically, I examined the use of API calls in the Application class as an example. Another frequent issue developers encounter is sending events —such as app startup events—, directly from the Application class. As mentioned earlier, the instantiation of the Application class does not always align with the moment a user explicitly launches the app. If the event is triggered by the user launching the app, it should be managed within the Activity. Even in apps with multiple activities, it's crucial to identify the correct entry point for the app. I hope this article proves helpful to your development journey. *The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
アバター
こんにちは、人事グループ組織人事チームのHOKAです。 本日は、 2024年3月に10X innovation culture programを受けた その後のお話をいたします。 はじめに KINTOテクノロジーズ(以下、KTC)では、イノベーションを生み出す組織環境づくりに挑戦しています。現在はまだその「過程」であり、試行錯誤をしていますが、これから 10X innovation culture program を導入しようと考えている方や、何かイノベーションを起こす組織を創りたいと考えている方に読んでもらえたら幸いです。 7月:自分たちで10X innovation culture programやってみた 3月にGoogleオフィスで10X innovation culture programを受ける前から、決意していたことがあります。 それは、10X innovation culture programを自社で実施していくということ。 「3か月ごとに実施しよう。アセスメント結果を見よう。」 「2回目からは自分たちで10X を実施しよう。」と誓った2024年3月の私たち。 夏の開催に向け、再びあわっちとHOKAは動き始めました。 まず最初に行ったのが、ファシリテーターを決めること。 今回はGoogleの方はいないので、自分たちで担当しなくてはなりません。 Slackで声をかけたところ、4人のメンバーがファシリテーターに立候補してくれました。 10X innovation culture programは、大きく分けて下記3つのコンテンツを実施します。そして、参加者が自分だったらどうするか?KTCだったらどれが良いかを考え、実行していきます。 【1】10Xの動画を視聴し、10Xとは何かを理解する「事前MTG」 【2】アセスメント結果から「現在地」を知る 【3】10Xの要素をテーマにディスカッションする  これらを実施するだけです。 それなのに、すごく難しかった!!のです。 難しかったポイント 【時間配分】当日のプログラムは2時間必要なのか、3時間必要なのかさえ分からない 【コンテンツ】初めて参加する人と2回目に参加する人、2パターンいるがどうやって進めたら良いのか 【品質】「10Xとは」を語るシーンが重要なのだが、いざKTCメンバーが話すと、ただ読み上げているだけになってしまう   等々、基本の「き」から手探り状態でした。あわっちとファシリテーターの4人と一緒に、3月の研修をふまえて「たぶん、こうだよね」と話し合いながら何とか実施したというのが本音です。 コンテンツごとに振り返り 【1】動画を視聴し10Xとは何かを理解する「事前MTG」 前回と同様にzoomで実施。ファシリテーターが「10Xとは」を説明し、その後の10Xの動画を再生。最後にアセスメント回答をしました。 なぜか3月ほど盛り上がっていない。 もしかしたら、「なぜ10XをKTCが学ぶのか」が足りないのかも? と運営側では感じていました。 【2】アセスメントの結果から現在地を知る そして、2024年7月2日、10X innovation culture programを室町オフィスで開催しました。 Osaka Tech Lab.(大阪オフィス)や、名古屋からも参加し、総勢39名。初めて参加した人も、2回目に参加する人もいました。 事前にみんなが回答したアセスメントの結果を見て、前回よりも上がった/下がったを報告したが、なんだか反応が薄い。 前回(3月)から今回(7月)まで何かに取り組んだ訳ではないから、数値に思い入れが持てないのかも? アセスメント結果を共有されているだけでは、まだ良い・悪いは判断できないのかも? と運営側では仮説を立てました。 【3】10Xの要素をテーマにディスカッションする 続いて、「この3か月間の振り返り」をディスカッションしました。 全く覚えていない人がいたり、初めて参加した人は「動画を見ただけだから、何をしたら良いか分からない」という人も。前回、Googleオフィスで行った時には、スムーズにディスカッションが進んでいただけに、運営側としては想定外の事態でした。(今、思えば当たり前なんですけど) その後、10Xの要素の1つ「自主性」についてディスカッションを実施。前回と同様にグループに分かれて課題から書き出し、解決策をディスカッションしていただきました。 3か月間、特に10Xを意識した行動してない人がいる なぜか前回よりもディスカッションしづらい雰囲気がある という発見がありました。 実施後アンケート 前回 に比べ、3や2の数が増え、1が付いた項目もありました。 推進のモチベーションも下がっています。 また、アンケートコメントにおいても、参加者の方から以下のアドバイスをいただきました。 全体の感想 運営お疲れ様でした!楽しい会になりました。 今のところ特になし ぜひ他の組織でも行ってほしいです。他の人の考えが知れるだけでもとてもいい活動 前回の内容に関するフィードバック 前回の内容を簡単に説明して欲しかった。前回参加できていないかたが置いてきぼりの時間があった。 前回の課題の記憶取り戻しに時間を要したので、事前に振り返りの時間を作れたらと思った。 前回のほうがディスカッションが盛り上がった気がした、場を積極的に回す人がいなかったし(自発性の欠如)事前準備をきちんとしておらず、入り込むまでに時間がかかった。 時間・スケジュールに関するフィードバック 時間的には短かったよね。もっとグループワークしてディスカッションする時間があったほうがよかったかも。 タイムスケジュールがタイトすぎる(大体その通りいかないので余裕持たないと。分単位は危険すぎる) 確保時間の問題かな?(目的はこれだったと思うので、流れ的にネクストアクションとまではいっていなかった気がする) グループディスカッションに関するフィードバック グループディスカッションのメンバー選定について、チームを横断したメンバーではなく、実際の業務で組んでいるチームメンバーでやっても良いかなと 前回、今回のように他チームのメンバーと協議することは刺激的であり、新鮮です。ですが業務に即した結果が出にくいということも感じています。そこで同じチームメンバー同士で改めて10Xについて会話することも良いかなと思いました。 ワークショップの進行・運営に関するフィードバック アイスブレイク(自己紹介)などの時間が少なすぎ。ワークショップを始めるにあたって、知らない人同士の環境を緩めるには非常に重要。自己紹介も終わらなかった。 ワークショップの質問が抽象的で回答の出し合いに困っていた おそらく、このワークショップのゴール設定がなされていなかった可能性がある(運営側が) 参加する方も今回これを持ち帰ってねという握りをはじめにしておいた方がいいとおもう 内容の理解・フォローアップに関するフィードバック 初参加でしたが内容としては既習者向けの感があり、全体としてぼんやりとした内容という印象を受けました。 内容を絞ってコンパクトにするか、尺を拡大してしっかりした説明を加えたほうが良いのではと感じました。 その他 配布された紙(回答コメント)がなんのためか分からない デスク毎にリーダーを決めてもらうことをはじめにした方がよさそう 自発性とチーム内の話し合い また前回を振り返った時にアクションが少なかったので、定期的にチームでも話し合いの時間をもうけたい 内発的動機に興味あります。自立性と組み合わせれるとより一層のスピード感を持てると思います。 さらに、10X innovation culture program実施後にファシリテーターメンバーで集まって、振り返りを実施。アイデアがたくさん集まりました。 さて、どこから手をつけるべきか? そんな中、Googleの方から Google Cloud Next Tokyo'24 で登壇する機会をいただきました。 <<中編へ続く>>
アバター
This article is the entry for day [21] in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hey there! I’m Viacheslav, an iOS engineer, and today’s article is part of the Advent Calendar 2024 event at KINTO Technologies. This year, I had the opportunity to work on a new feature in our Unlimited app called "これなにガイド" (Kore Nani Gaido, meaning "What's This Guide"). Kore Nani Gaido is an augmented reality (AR) manual that allows users to "scan" their car's dashboard and displays virtual markers over a car's buttons, switches, and other elements. By selecting a marker corresponding to a specific button, users can access a detailed manual page for that control. Today, I’d like to share a short and simple solution to one of the challenges I encountered while working on this feature: accurately capturing the coordinates of a physical object recognized by Vision framework on the screen and converting them into 3D coordinates within an AR scene. What initially seemed like a trivial task turned out to be more complex than expected. After exploring several approaches and performing a lot of manual calculations, I finally arrived at a solution that is both straightforward and surprisingly concise. I wish I’d known about it when I started, as there is relatively little information available about ARKit and CoreML integration. So, let’s add to that knowledge base! A Couple of Preconditions Before we get to the actual code, let's clearly define the environment we’ll be working with: ARSCNView This is a view that displays the video feed from the device's camera, capturing the real-world environment and allowing 3D objects to "blend in" for an AR experience. ARSCNView is part of Apple's ARKit , built on top of SceneKit , which handles rendering 3D objects in an AR scene. Core ML Object Detection Model Before we can determine the coordinates of an object, we first need to recognize it within the video feed frames provided by the device's camera. Vision framework utilizes Core ML Object detection models for that purpose. For this article, I’ll assume you already have a model ready to use. If not, there are many pre-trained models available for download, such as YOLOv3-Tiny , which you can find here . And that’s all you need for a bare-bones solution! We’ll capture video frames from ARSCNView , use the Core ML model to detect the object’s position within the ARSCNView viewport, and apply a technique called "hit-testing" to determine the object’s coordinates in 3D AR space. Capturing the Coordinates of a Recognized Object in ARSCNView When performing recognition requests with Vision, a typical setup you might have is described below. You initialize a Core ML model and a VNCoreMLRequest to handle recognition using that model. let vnModel = try! VNCoreMLModel(for: myCoreMLModel) let vnRequest = VNCoreMLRequest(model: vnModel) { [weak self] request, _ in guard let observations = request.results else { return } // Observations handling } request.imageCropAndScaleOption = .centerCrop You then keep a reference to vnRequest in a suitable place, ready to be performed with the next set of arguments. The argument types depend on where you’re capturing video feed frames from. In our scenario, we are passing frames from an ARSCNView , which should be captured in the session(_:didUpdate:) method of ARSessionDelegate . This delegate method is called whenever a new frame is available for ARSCNView to display. func session(_ session: ARSession, didUpdate frame: ARFrame) { guard let vnRequest else { return } // 1 let options: [VNImageOption: Any] = [.cameraIntrinsics: frame.camera.intrinsics] // 2 let requestHandler = VNImageRequestHandler( cvPixelBuffer: frame.capturedImage, // 3 orientation: .downMirrored, // 4 options: options ) try? requestHandler.perform([vnRequest]) // 5 } Breaking Down the Code: Reference to VNCoreMLRequest : Once we receive a new frame, we are ready to perform the request we have initialized earlier. Camera Intrinsics : frame.camera.intrinsics provides camera calibration data to help Vision interpret the geometric properties of the scene. Image Input : VNImageRequestHandler accepts raw image data as a CVPixelBuffer , obtained from the AR frame. Image Orientation : The .downMirrored orientation accounts for the inverted coordinate system of the camera feed compared to Vision's default orientation. Performing the Request : The prepared request is executed using the request handler. Once you start passing frames to Vision, object detection results are returned as arrays of VNRecognizedObjectObservation objects in the VNCoreMLRequest completion handler. While you might filter these results by confidence level or perform other processing, today we’ll focus on extracting the coordinates of a specific recognized object. Extracting Bounding Box Coordinates At first, this might seem straightforward since VNRecognizedObjectObservation has a boundingBox property — a CGRect enclosing the recognized object. However, there are a few complications: The boundingBox is in a normalized coordinate system relative to the input image of the object recognition model (meaning that the coordinates' values are between 0 and 1) which also has inverted Y-axis. The sizes and aspect ratios of the camera feed, Core ML model input, and the ARSCNView viewport differ from each other. This means that a series of coordinate conversions and rescaling steps are required to transform the boundingBox into the ARSCNView viewport's coordinate system. Doing those conversions manually might be cumbersome and mistake-prone. Fortunately, there’s an embarrassingly simple way to handle this using CGAffineTransform . Here’s how: let sceneView: ARSCNView func getDetectionCenter(from observation: VNRecognizedObjectObservation) -> CGRect? { guard let currentFrame = sceneView.session.currentFrame else { return nil } let viewportSize = sceneView.frame.size // 1 let fromCameraImageToViewTransform = currentFrame.displayTransform(for: .portrait, viewportSize: viewportSize) let viewNormalizedBoundingBox = observation.boundingBox.applying(fromCameraImageToViewTransform) // 2 let scaleTransform = CGAffineTransform(scaleX: viewportSize.width, y: viewportSize.height) let viewBoundingBox = viewNormalizedBoundingBox.applying(scaleTransform) return viewBoundingBox } Explanation: Transforming to View Coordinates : Using displayTransform(for:viewportSize:) the detected bounding box is converted from the normalized coordinate system of the input image to a normalized coordinate system of ARSCNView . Scaling to Pixel Dimensions : The normalized bounding box is scaled to match the size of the ARSCNView viewport, resulting in a bounding box in screen pixel dimensions. And that’s it! You now have the bounding box in the coordinate system of the ARSCNView viewport. Getting the Third Coordinate I promised that we will get the coordinates of the recognized object within the 3D coordinate space of the AR scene. To do that, we are going to utilize a technique called "hit-testing." It allows us to measure the distance to the closest physical object at an arbitrary point in the viewport. You can imagine this technique as casting a straight ray from your device to the first intersection with a physical object at the selected point of the viewport and then measuring the length of that ray. This functionality is a part of ARKit and is really easy to use. Here is how we can find the 3D coordinates of the perceivable center of the object we detected earlier. func performHitTestInCenter(of boundingBox: CGRect) -> SCNVector3? { let center = CGPoint(x: boundingBox.midX, y: boundingBox.midY) // 1 return performHitTest(at: center) } func performHitTest(at location: CGPoint) -> SCNVector3? { guard let query = sceneView.raycastQuery( // 2 from: location, allowing: .estimatedPlane, // 3 alignment: .any // 4 ) else { return nil } guard let result = sceneView.session.raycast(query).first else { return nil } // 5 let translation = result.worldTransform.columns.3 // 6 return .init(x: translation.x, y: translation.y, z: translation.z) } Explanation: Here, we calculate the center of the bounding box from the previous step, as we need a single point to perform the hit-testing. Creates a raycast query starting from the given 2D point. Allows hit-testing to consider non-planar surfaces or planes about which ARKit can only estimate. Enables hit-testing for both horizontal and vertical surfaces (default is horizontal only). Executes the raycast query using the AR session. Returns nil if there’s no intersection. Each ARRaycastResult contains a worldTransform , which is a 4x4 matrix representing the 3D transformation of the detected point in world space. The columns.3 contains the translation vector, which specifies the 3D position of the intersection. This translation is returned as an SCNVector3 , which ARKit/SceneKit uses to represent 3D positions. Done! You now have the 3D coordinates of an object detected by Vision. Feel free to use them for your purposes. :) Conclusion In the Unlimited app, we use these 3D coordinates to display AR manual markers in a car. Of course, there are many additional techniques we employ to make the user experience smoother and the marker positions more stable, but this is one of the core techniques. That said, this same method can be used for any other purpose you can think of. I hope you find it helpful. Finally, here’s a tiny sneak peek into our testing process and how our AR manual markers are displayed after object detection. That's it for today—thanks for reading! Wishing you the Merriest Christmas and the Happiest New Year!
アバター
Introduction This is a report from the organizers of the second "KINTO Technologies MeetUp!" Our Event page: [Second] KINTO Technologies MeetUp! - connpass Previous related articles: Preparations for our first "KINTO Technologies MeetUp!" | KINTO Tech Blog | KINTO Tech Blog (kinto-technologies.com) KINTO Technologies MeetUp! (Organizers’ edition) | KINTO Tech Blog | KINTO Tech Blog (kinto-technologies.com) The first event was held exclusively on-site, but this time we prepared to allow participation online as well. Building on the knowledge gained from our first event, we embraced the challenge of hosting in a new hybrid format. Here’s a look at the organizer’s experience this time around! Pre-Event Tasks Snack arrangement To ensure no one felt “a bit unsatisfied” with the food, we focused on enhancing the snack selection. During the latter half of our last event, we found that the pizza we had prepared wasn’t enough, and some participants commented that they felt “a bit unsatisfied.” Initially, we planned for pizza again and increased the budget to address this, but we eventually learned about Maisen’s mini burgers (thanks to a recommendation) and decided to offer those instead. Here are some of the great things about the mini burger: It is individually packaged and easy to grab. Therefore, there is no need to prepare paper plates. It is easy to give away if there are leftovers, reducing waste. It is designed to be served at room temperature, allowing more flexibility with delivery timing. Although we had a strong desire to serve hot pizza, we decided on the mini burgers after considering the benefits. Additionally we prepared individually wrapped snacks to make it easy for participants to enjoy at their convenience. As a result, many participants enjoyed the mini burgers, and we ended up with just a few leftovers, making it a successful choice overall! Novelty selection We aimed to provide novelties that would make participants think, "I want to take this home and actually use it.” With this goal in mind, we carefully selected items, believing that a good event combined with appealing novelties can bring back fond memories of the day just by looking at them. In choosing the novelties, we considered not only practicality but also whether the items would have an irresistible appeal. For distribution, we thought carefully about how to present the novelties in a way that encourage attendees to take them home. Although not all novelties were taken by participants, we hope that those who did take them will remember the event whenever they see the items. Event timetable design I was entrusted with designing the event timetable, which is an important factor that determines the progression and flow of the event. It was a significant responsibility, and I felt the pressure as I worked through it. My main focus during the planning was, “ Will the participants find the event engaging and enjoyable without feeling bored? ” In the timetable for the previous MeetUp, we missed incorporating break times and a group photo session with participants, which were later highlighted as areas for improvement. For this event, we addressed these points by allocating time for breaks and photos. Additionally, we included a Lightning Talk session to bring energy and keep the pace lively, something that was missing last time. Thankfully, the event proceeded smoothly, mostly according to the timetable, and the atmosphere was lively throughout—a welcome outcome. However, we made some last-minute adjustments to the timetable, and I realized in hindsight that more careful pre-event planning would have helped. Event documents preparation To prepare documents, not only did we organized presentations for speakers but also prepared various guides and resources for attendees, including event instructions, seating arrangements for discussion sessions, and pre-event handouts. These documents were aimed at ensuring the smooth progression of the Meetup. In the previous event, we realized that verbal directions to restrooms or having a QR code for Wi-Fi access would have been helpful, so this time we created documents to cover these needs. Promotion and Outreach Attracting attendees to corporate events can be challenging, and we also faced difficulties. Here are two approaches we found especially effective: Included the product names mentioned in the presentations within the promotional text. Posted in the IT Slack community. Included the product names mentioned in the presentations within the promotional text. Initially, we planned to announce the event on X twice. The first announcement was scheduled for when it went live on connpass, and the second just before the event. However, we soon realized that the key metrics we wanted to focus on were not growing as expected, which were: Views on the connpass event page The number of event participants To address these challenges, we considered increasing the opportunities for people to learn about the event by adding more posts on X. However, simply posting about the event might not catch people’s attention, and awareness of the event could remain low. So, we decided to drill down our thinking to create more effective post content. We asked ourselves, “what kind of post would catch my eye as a reader?” ↓ Would I be more interested if the post mentioned a product I care about? ↓ Would it feel more relatable if it included some of the challenges we faced? Through this simple drill-down, we concluded that the post should include not only the product names mentioned in the presentations but also what we achieved with them. Here’s an example of the posts we created: By posting such kind of content, we successfully increased both the views on the connpass event page and the number of participants, addressing the challenges we had been facing. We couldn't be happier with the results!! IT Slack community posts On the day of the event, we asked some of the on-site attendees how they had heard about the event. All of them answered, “on the IT Slack.” Its reach is truly impressive! As someone who regularly checks the Slack channel #share-event as well, I highly recommend posting announcements there when hosting events related to IT or corporate IT. This time, our event aimed to connect with other corporate IT teams, and the warmth of the IT Slack community was truly heartwarming. To those who participated, whether you found us through connpass or X, thank you, and we look forward to your continued support! Disruptive participants prevention In the past, we experienced a situation at another KTC-hosted event where a person seemed to attend solely for the food. There was an atmosphere that could have escalated into a disturbance, as the person drank excessively and reacted negatively when approached. Through this experience, I decided to firmly establish a clear protocols and preventative measures. After researching how others handle such situations, we adopted the following approaches: Identify and prevent disruptive individuals before they can cause issues. If a disruptive individual does enter (or tries to get in), ensure minimal impact on legitimate participants, and act swiftly to manage the situation. Thoughts on handling troublemakers at networking events | wakatono (note.com) We prepared with the approach of preventing suspicious individuals from entering while also establishing grounds and methods for their removal if necessary. After consulting with the building management team at KINTO Technologies’ Muromachi Office in COREDO Muromachi 2, which was our venue, they assured us that they could intervene if needed, even restraining individuals if the situation escalated. We shared their contact information with the event staff to ensure everyone was prepared to respond if necessary. Additionally, we updated the connpass event page to include a clear policy prohibiting disruptive behavior and informing participants that they may be asked to leave if such actions occur. Fortunately, no such incidents happened this time. However, having these measures in place gave us peace of mind and allowed us to focus fully on running the event smoothly. Event Day Tasks Venue set up We followed the same basic process as in the previous event, but this time we also needed to account for the space required for the streaming equipment. Its placement was determined by the length of our cables, and we also made sure that participants still had a comfortable viewing area while adressing those constraints. While we prepared the basic layout in advance, adjustments were made on the day to accommodate the number of attendees. Although there was some last-minute scrambling, we successfully created a space where participants could relax and enjoy the event. Unfortunately, we weren't able to update the seating layout for the discussion session in time, which caused some confusion during the event. Equipment setup, connections, and control With limited equipment on hand, we carefully planned how to deploy it effectively. Hosting the event in a hybrid format required extra attention to various details. Thankfully, there were no major issues, and we were able to deliver a seamless experience for our online attendees as well. Overall, it turned out to be a successful event! Camera setup Since the camera feed would be projected onto a small wipe, we adjusted the framing to ensure that the speaker's face was clearly visible. The camera had to be connected to the “switcher” used for stream control, so we positioned it near the operation desk (commonly referred as the "ops desk") while ensuring it faced the speaker directly. The camera was basically fixed in place! Minimal adjustments were needed when speakers took turns to correct any misalignment caused by differences in their heights. Audio setup We conducted pre-event checks for microphones, volume levels in the venue, and audio levels for online participants. Since this was a hybrid event, these checks were absolutely essential! We made sure to prevent any issues, such as audio distortion or volume levels that were either too low or too high for both online and in-person attendees. Additionally, during presentations, we continuously monitored the online audio to respond immediately in case of any audio dropouts or sudden distortion. Streaming For this event, we decided on a relatively simple streaming setup, drawing from previous successful experiences. By keeping the setup straightforward, we minimized the risk of unexpected issues during the broadcast. We projected the same video feed onto a screen so that the presenters could see how their slides and wipe were displayed. To ensure the speaker’s slides and the wipe didn’t overlap, we manually adjusted the position of the wipe, moving it around to different corners as needed. Each speaker was able to present using their own familiar laptop, and we managed the visuals without any disruptions. This made the process smooth and hassle-free! Microphones, document projection, the wipe-everything was checked and ready to go! Reception, guidance, and exit support Preparation Since we had the same team as the previous event, we already had a good amount of knowledge. However, we documented tasks in Confluence, assigned roles to each member, and prepared accordingly. By deciding everyone’s roles for the event day in advance, we avoided unnecessary chaos during the event. On the day of the event As soon as the registration closed, we started preparing the building entry passes. They looked too plain on their own, so we added a custom cover for a personalized touch. This small effort, which we’ve also used for other events, really makes a difference. Reception start Since the event took place during rush hour, many non-participants were coming and going. To ensure that attendees could recognize the reception area, we called out to anyone who looked like they might be attending the event (Though sometimes we mistakenly approached unrelated people, haha). This time, we didn’t assign staff to guide attendees from the reception area to the 16th floor venue. Instead, whenever participants headed to the venue, we notified the operations team via Slack with updates like “X number of people are coming!” This was initially done spontaneously but turned out to be very well-received by the team waiting at the venue. In the end, it was a great idea, and I’m glad we did it! Exit support To be honest, we didn’t have detailed discussions about this part as a team, so whoever was available handled it on the spot. Personally, I really enjoyed chatting with attendees in the elevator as they left. Hearing comments like "That was fun!" "Please hold another event again" or "KINTO Technologies seems like such a fun company!" Left me feeling thrilled. Being able to hear this feedback live felt like a special reward! ♬ Moderation and hosting We managed to get through most of it with energy and quick adaptability! About 80% of the moderation was handled this way. Here, I’ll share the remaining 20% of the things we consciously focused on. Adding comments before each presentation In an in-person-only event, transitions like speaker changes are visible and don’t stand out much. However, in a hybrid event, online attendees can only see what’s shown on camera, which creates awkward silences during transitions. To address this, I added short introductions about the upcoming presentation or summarized the previous one, much like the opening remarks before classic Showa-era song performances. This helped create a smoother transition. Separate remarks for in-person and online audiences only at the beginning and end Time constraints were the main reason, but I avoided making separate comments for the in-person and online audiences during the event. For online attendees, hearing jokes or commentary that only resonates with the in-person audience can make them feel left out and reduce their sense of immersion. I kept this in mind while moderating. Using a microphone sparingly after the online broadcast ended This was the opposite approach compared to the one mentioned above. I wanted to maximize the in-person experience! It’s similar to how bands or idol groups sometimes speak directly to the audience without a mic at the end of a concert. It gives you that feeling of being on a live session. Yes, like that moment. I was initially hesitant about speaking in front of the audience, but I’m relieved that I was able to fulfill my role. To anyone reading this, I encourage you to give it a try as well. Snack preparation and distribution Last time was our first external event, and organizing refreshments was a bit of a challenge. But after hosting several other events over the past few months, we’ve gotten the hang of it! This time, we prepared Maisen’s cutlet sandwiches🐷 Everything looks delicious...Kin-chan Which should I take...Take-chan Compared to pizza, it's easier to eat. We adopted a casual style, encouraging participants to grab a sandwich after checking in and enjoy the meetup! while eating. Since the event was held around dinnertime (pretty much), I think this was a welcome addiction! Even though we ordered plenty, the fillet cutlet sandwiches were the first to disappear ( ..)φ [takes notes] We will do our best to adjust the quantities next time. For drinks, beer was the most popular. We had three types available, but after attending another company’s event that offered about ten varieties, I was impressed! Next time, we’ll try expanding our selection. Kaizen, kaizen! Post-Event Tasks Retrospective When it comes to wrapping up team activities, our go-to process is doing Retrospectives. What is a Retrospective Retrospectives are sessions: to look back on what went well, what didn’t go so well, and what happened during an activity or project, and from there, to identify and develop concrete action plans for the future. At KINTO Technologies, retrospectives are deeply embedded in our everyday practices. Across all departments, they are commonly referred to as “furikaeri” or simply “retro,” and they often come up in casual conversations. The event retrospective After the MeetUp concluded, we held a retrospective specifically for the organizing team. While there are no strict rules or fixed formats for these sessions, we often use the KPT framework (Keep/Problem/Try) to guide our discussions, which is what we used this time as well. In event operations, there are always things that could have gone better or areas for improvement. The KPT framework helps us identify these “seeds of kaizen” and turn them into actionable steps for future events. Retrospective preparation When there are many participants and everyone’s time is limited, proper preparation is key to make the most out of retrospective sessions. This time, we followed these steps: Created a Wiki where everyone can freely contribute (we used Confluence as our Wiki tool) Prepared a page with the KPT framework (for this retrospective, we created additional categories such as "Preparation," "Promotion and Outreach," and "On-site Setup" to make it easier to organize inputs). Announced on Slack, asking participants to write down their observations before the retrospective day. The facilitator reviewed the pages right before the retrospective, processes the contributions, and planned the flow of the discussion (for example, deciding what to prioritize and how much time to allocate for each topic). The retrospective: From icebreaker to ground rules And then, the retrospective took place! If we had more time, we would have started with an icebreaker, asking participants to share their best moments of the event to liven up the atmosphere. However, due to time constraints, the retrospective kicked off with a positive summary from the event leader. After the Icebreaker, we explained the “magic rules” (ground rules) to make it easier for everyone to share their thoughts and ensure the best retrospective experience. The following rules were read aloud to all participants: Be active in the conversation! No matter how trivial it may seem, don't hesitate to share it! Don't monopolize the conversation! Do not interrupt others while they’re speaking! Remember that even those who are quiet have something valuable to contribute Trust that everyone did their best Focus on finding causes, not assigning blame Frame it as “the problem versus us” Appreciate the opportunity to learn from mistakes and celebrate our growth! After reading the ground rules, the main retrospective began. The retrospective: KPT implementation Thanks to the contributions made by participants beforehand, we were able to start the session with a clear sense of direction. We began by reviewing the submitted entries one by one. An essential part of the process is having the person who wrote each entry read it aloud. While the facilitator could do this, it’s important for the group to understand the intent and emotions behind the words. That’s why we prioritize self-reading as much as possible. Process: The author reads their entry aloud. The facilitator reflects on the content, organizes it, and opens the floor for comments, additions, and feedback. Consolidate the input and update the Wiki to keep everything organized. For any proposed actions, achieve consensus and add them as a “Try” item. We followed this flow and kept the discussion lively and engaging for as long as time allows. At the end, we celebrated the completion of the event by finalizing the Wiki, enriched with action items (“Try”). A retrospective built together Reflecting on the retrospective Through this retrospective, we were able to create actionable items for the next similar event, leaving a legacy for our future selves to build upon. At the same time, we believe the retrospective served as a space to improve the “quality of relationships” and the “quality of thinking” among participants. By repeating these collaborative events, we aim to create a “positive cycle of success”. Better actions will lead to better results, ultimately strengthening the organization as a whole. Closing thoughts Thanks to the autonomy and dedication of each team member, we were able to host another enjoyable event with no major issues, just like last time! Now that we have established a track record of holding offline/online hybrid events, we plan to leverage this knowledge for future opportunities. This time, we replicated the approach from the first event to achieve the same results with less effort. However, it made me reflect on balancing "standardization" with "creativity." If a task is merely "something to do" without requiring creativity, it's better to standardize it through manuals (as we did this time). However, with each event held by KINTO Technologies, whether it’s managing the reception or arranging light meals, I’ve seen firsthand how creativity and ingenuity make these processes more attractive and efficient. Rather than attempting to overly systematize everything, perhaps it’s more important to create an environment where individuals are empowered to grow through their own autonomy and ingenuity. We also incorporated online participation to reach as many people as possible. However, I found it incredibly valuable to share challenges with other companies’ IT teams during in-person social gatherings. As we move forward, I would like to continue hosting events while carefully considering whether to stick with the hybrid format or focus solely on on-site events.
アバター
This article is the entry for day 21 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello everyone! My name is DL, and I’m part of the Business Development Group team in the Group Core System Division at KINTO Technologies (KTC) . Currently, we’re collaborating with KINTO divisions in Latin America to implement an in-house system for KINTO One, a car leasing business with additional services like insurance. As a Business Analyst (BA), I focus on analyzing KINTO's operations and crafting IT solutions to streamline workflows and enhance efficiency. Before business analysis became part of the global team, system designs often fell short to meet KINTO’s global business expectations for enterprise-level solutions that addressed their daily operational intricacies. This is where a BA becomes indispensable in capturing the daily operational processes and addressing pain points, making Business Analysis a strategic pillar for adding value to global projects. Today, I want to invite you into the often-misunderstood world of the BA. You might think, “Isn’t that just a numbers person?” or “Aren’t they the ones writing down what others say?” But being a BA is far more—it’s about problem-solving, strategy, and communication. At its core, a BA bridges the technical and business worlds. Whether improving efficiency, streamlining operations, or adopting technology, the BA steps in to listen, analyze, and design practical, impactful solutions. By engaging stakeholders from executives to end-users, the BA becomes the crucial translator, turning business needs into effective technical solutions. Story 1 - The Power of Listening: A Business Analyst (BA)’s Role One critical skill for a BA is listening—not just to words, but to the underlying needs. In one project, I tackled a financial company’s inefficient reporting process. Business units worldwide submitted quarterly reports via email, creating a bottleneck with over 100 daily emails. Files were manually downloaded, consolidated, and checked for errors—a time-consuming, error-prone process. Witnessing this firsthand, I worked with stakeholders to identify pain points. Collaborating with IT, we developed a secure LAN directory structure for direct submissions. Automated batch and VBA scripts streamlined checking, copying, and aggregating data, reducing manual effort by 70% and enabling more valuable analysis. The solution’s success led to its adoption across the department. Story 2 - A Lesson in Overlooking Stakeholders Afterwards, a SaaS product was introduced without the BA and users’ full review. It was selected for its cost savings and dashboards, but it couldn’t support existing automated workflows, forcing teams back to manual processes... Despite complaints, the decision was final, leaving users frustrated and dissatisfied. This experience underscored the importance of involving a BA early to align solutions with real user needs. The Story Continues… Although Story 2 might initially seem like a letdown, it offers an important lesson: whether we’re introducing process changes or developing new products, the key to success lies in listening to and truly understanding the needs of the end users. This brings us to the critical role a BA plays in KTC’s global project. As mentioned earlier, the global team is working with KINTO divisions in Latin America to implement an in-house system for KINTO One business (a car leasing business with additional services, e.g., insurance). Developing a global product/system means addressing diverse specifications across countries and languages. The challenge lies in creating a system that is flexible enough to accommodate each country’s varying needs. So, how do we approach this? Business users in each country are experts in their own processes and the processes are different country by country. Engineers are tasked with the challenge of building a flexible system that can accommodate all the differences. This is where the BA steps in, bridging the gap between business users and engineers. As a BA, we take a systematic approach: Gathering Business Processes: We carefully document the processes unique to each country. As the BA, this step is highly critical. The BA basically needs to become the expert of the business processes and fully understand every detailed step to ensure no steps are missed. To accomplish this step, our BAs in KTC have visited and been on-site at the businesses in Latin America, which is essential for grasping the nuances of each process. Moreover, the key in this step is to understand the businesses’ current pain points. Why? Because the system KTC develops should address these pain points to add tangible value to the business users’ daily workflows. By addressing the pain points, the business users can see a decrease in manual work, increase in productivity and effectiveness. And this can be measured in terms of hours of work reduced, reduction in lead time, and general welfare of the business users. Conducting Gap Analysis: This analysis highlights the differences and commonalities across these processes for each country. As the BA, there are two critical considerations in this step: Order of Operations: If the countries have similar order of operation, then that’s great news! However, if one country has a reverse order of operation, then this becomes much more complex when designing a system that can accommodate both. For example, to perform a repair on a vehicle, in case 1: the repair is approved first as a KINTO service, and then the execution of service is carried out. However, in case 2: the execution of service is carried out first, then the service is verified if it’s included as a KINTO service. Such differences in the order of operation adds complexity to the system design. External Systems Integration: Another critical consideration in this step is to note the external systems/platforms that are currently being used by the different countries. For example, one country may be using SAP as their accounting system, another country may be using another type of accounting system. This adds complexity to the integration needed for the global product. Collaborating on Solutions: Using the gap analysis, the BA works closely with the engineers to design the flexibility needed to meet the requirements of all countries involved. In this step, the BA is now trying to solve a complex puzzle with the help of the engineers. As the BA, this step is highly critical because the BA basically represents the business users of both countries and needs to design a system that can accommodate both. As an analogy, maybe one country is like a sedan, and the other country is like a van, so the BA works with the engineers to come up with an SUV?... lol... something along those lines. This step involves looking at the details of every step of the process and making sure that every step can accommodate both countries. Then after that, making sure that all the steps can be stitched together to create a cohesive and flexible system. It’s a lot of fun to solve such complex problems! The result is a collaborative effort that ensures the global product not only aligns with business needs but also has the right balance of system flexibility. This demonstrates just how essential the role of a BA is in the success of a global project. The Impact of a Business Analyst (BA) What does it mean to be a BA? It means being a problem solver, a communicator, and a strategist—having the curiosity to ask questions, the patience to listen, and the persistence to find answers. Ultimately, it’s about making a real difference in how organizations run, how people work, and how decisions are made. BAs may not always be in the spotlight, but our work shapes outcomes, saves resources, and makes lives easier. We are the bridge that turns ideas into reality, translating needs into solutions. If there’s one takeaway, it’s this: Never underestimate the power of listening. Whether you’re a BA, a stakeholder, or someone striving to make a difference, real progress comes from listening, translating, and connecting. Thank you!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の21日目の記事です🎅🎄 はじめに こんにちは。KINTO FACTORY開発グループのうえはら( @penpen_77777 )です。 2024年の7月に入社し、KINTO FACTORYのバックエンドの開発を担当しています。 今回は12/8に開催されたISUCON14にKINTO Technologies社員で参加してきたので、その内容や結果を共有したいと思います。 ISUCON14とは ISUCONとは、LINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです。 優勝すると賞金100万円を獲得できます! 今年は12/8(日)の10:00〜18:00で開催されました。 私(うえはら)は昨年のISUCON13からパフォーマンスチューニングの知識習得はもちろん、エンジニアとしての腕試し・スキル向上を目的に参加しています。 :::message ISUCONは、LINEヤフー株式会社の商標または登録商標です。 https://isucon.net/ ::: チーム「ktc-isucon-bu」 社内Slackでうえはらがメンバーを募集し、チーム「ktc-isucon-bu」を結成しました。 メンバは以下の通りです。 うえはら( @penpen_77777 ) ISUCON13にも参加しており、今年で2回目 古谷 ISUCON初参加 西田 ISUCON初参加 ISUCONでは初期実装で使われている言語がいくつかありますが、今回はGo言語を選択しました。 また、初参加のメンバーが多いチームのためランキング上位を狙うというよりもスコアが10000点を超えることを目標にしました。 参加に向けての事前準備 ISUCONでは競技本番での改善タスクに集中できるように、典型的な作業は自動化もしくは容易に実行できるようにするのが重要です。 今回は、以下の準備を行いました。 環境構築・デプロイコマンドの整備 ドキュメント生成ツールの整備 計測ツールの整備 個人練習/チーム練習 環境構築・デプロイコマンドの整備 go-taskはターミナルでタスクを実行するのに特化したツール、いわゆるタスクランナーです。 https://taskfile.dev/ 従来だとmakeコマンドをタスクランナーとして使われることが多いと思いますが、個人的にはgo-taskの方がより便利に感じたので今回はgo-taskを使用しました。 (makeと異なりインストールが必要なのが難点ですが、そこを除けば業務でも十分活用できるかなと思います) 例えば以下のようなtaskfile.yamlを作成し、 setup タスクと deploy タスクを定義します。 version: '3' tasks: setup: cmds: # 環境構築用コマンドを記述 - echo "setup" deploy: cmds: # デプロイ用コマンドを記述 - echo "deploy" 作成後、以下のように task タスク名 でコマンドを叩くことでタスクを実行できます。 # 環境構築タスクを実行 $ task setup setup # デプロイタスクを実行 $ task deploy deploy また、タスク実行時にコマンドライン引数を渡すこともでき、受け取った引数をタスク内に埋め込むこともできます。 version: '3' tasks: setup: cmds: - echo "setup {{ .CLI_ARGS }}" deploy: cmds: - echo "deploy {{ .CLI_ARGS }}" # サーバisu1に対してsetupタスクを実行 $ task setup -- isu1 setup isu1 # サーバisu2に対してdeployタスクを実行 $ task deploy -- isu2 deploy isu2 上のコードは簡単な例ではありますが、実際使用する際にはコマンドライン引数を用いて反映先のサーバを自由に切り替えられるように作っておくことで チームでの分担作業が捗ります。 その他、go-taskを使うメリットは以下の通りです。 サブディレクトリで作業していても、親ディレクトリに存在するtaskfile.yamlを検出してタスクを実行できる ディレクトリの位置を気にせず、タスクを実行できる タスクからタスクを呼び出すことができるため、定義したタスクの再利用性を高められる deploy タスクを実行する前に setup タスクを実行するなど、タスクを組み合わせて実行できる ISUCONで使用するツールを全てtaskfile.yamlに記述しておくことで、ツールの使用方法(必要なオプションなど)を知らなくても task コマンドを叩くだけで実行できる 事前に taskfile.yaml のテンプレートを作成しておき、本番では一部の変数を書き換えるだけであらゆる問題がきても柔軟に対応できるようにしておきました。 ドキュメント生成ツールの整備 以下の2つのツールを使用しました。 https://github.com/k1LoW/tbls DBの中身を読み取り、ER図とスキーマの説明が入ったドキュメント(markdown)を生成 データベースに繋ぎに行かなくてもスキーマ定義を確認できるため、DBの構造を理解するのに便利 CI/CDパイプラインでドキュメントを自動生成することで、DB構造の変更を追いやすくなる 詳しくは以下の記事を参照ください https://devblog.thebase.in/entry/auto_generated_er_graph_by_tbls_and_github_actions https://github.com/mazrean/isucrud アプリケーションコードを読み取り、DBテーブルとの関係を可視化・ドキュメント(markdown)を生成 関数が多くなってくると図がわかりにくくなるのが欠点だったのですが、webブラウザ上でインタラクティブに見たい箇所を絞り込めるようになったのでとても使いやすくなった 計測ツールの整備 以下のツールを使用しました。 https://github.com/kaz/pprotein アクセスログ、スロークエリログ、プロファイルデータを収集し、可視化するツール ベンチマークを叩くとwebhookによって自動的にデータ集計を始めてくれるようになるので便利 データ収集時点でのコミットハッシュも記録してくれるので、どのコミットがスコアアップに寄与したかがわかりやすい^[pproteinでうまくGitコミットハッシュが取得できない現象に遭遇し、fork・コードを修正してPRを出しました。 https://github.com/kaz/pprotein/pull/37] 個人練習/チーム練習 練習に関して、初参加のメンバーがいきなりISUCONの過去問を解こうとしても難易度の高さから挫折してしまう可能性があります。 まずはパフォーマンスチューニングの雰囲気を感じ取ってもらうため、達人が教えるWebパフォーマンスチューニングという本を読んでもらいました。 https://gihyo.jp/book/2022/978-4-297-12846-3 書籍を読み進めつつ、以下を練習問題として週末チームで集まり解いていきました。 https://github.com/catatsuy/private-isu (書籍でもprivate-isuを使ってどのようにチューニングしていけば良いか紹介されています) 大体10~20万点くらいまでスコアを取れるようになってきたところで、昨年のISUCON13の問題に変えて解いていきました。 ここまできたら本番を意識した練習にスライドしても特に問題なく進められるようになったかなと思います。 今回のお題 https://www.youtube.com/watch?v=UFlcAUvWvrY ライドチェアサービス「ISURIDE」の改善 チェアオーナー(椅子の所有者)が椅子を提供 ユーザはアプリから椅子が配車され、その椅子を使って目的地まで移動できる スコアは移動距離やライド完了率などから計算される ユーザ満足度を高めるように改善を進める必要がある https://github.com/isucon/isucon14 結果 結果は以下の通りです。目標の1万点を超えることができました! 135位 / 831チーム中 12514点 できたこと デプロイスクリプト等の準備(うえはら 10:00~10:30) 当日マニュアル(古谷 10:00) 動作しているプロセスを確認しておおよその構成を理解する(古谷 10:00) アプリケーションマニュアルを読む(西田 10:00) MySQLにログインし、テーブルサイズを調べる インデックスを追加(西田 11:21) -- chair_locations CREATE INDEX idx_chair_id ON chair_locations(chair_id); CREATE INDEX idx_chair_id_created_at ON chair_locations(chair_id, created_at); -- chairs CREATE INDEX idx_owner_id ON chairs(owner_id); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_chair_sent_at_created_at ON ride_statuses (ride_id, chair_sent_at, created_at); pproteinの仕込み (うえはら 11:48) nginxの設定を修正するのに手こずり、1時間以上かかってしまう インデックス追加(西田 11:54) CREATE INDEX idx_ride_statuses_created_at_ride_id_chair_sent_at ON ride_statuses (created_at, ride_id, chair_sent_at); -- rides CREATE INDEX idx_chair_id_updated_at ON rides (chair_id, updated_at DESC); 動的パラメータを有効化・jsonパッケージを高速化(うえはら 12:38) // import文を修正 "encoding/json" → "github.com/goccy/go-json" // DB接続時の設定でInterpolateParamsをtrueに dbConfig.InterpolateParams = true インデックス追加(うえはら 13:05) -- chairs CREATE INDEX idx_chairs_access_token ON chairs(access_token); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_app_sent_at_created_at ON ride_statuses (ride_id, app_sent_at, created_at); -- rides CREATE INDEX idx_rides ON rides (user_id, created_at); -- coupons CREATE INDEX idx_coupons_used_by ON coupons(used_by); ユーザ・ステータスキャッシュ(古谷 14:30) usersとride_statusesテーブルをインメモリキャッシュ トランザクション範囲の修正(西田 14:48, 15:09) notificationのポーリング間隔調整(古谷 16:27) appGetNotificationとchairGetNotificationで返しているRetryAfterMsを30 msから300 msに変更 椅子とユーザのマッチング間隔を短くした(西田 17:18, 17:28) ISUCON_MATCHING_INTERVALを0.5 sから0.1 sに短くした スコアが2倍近く上がった(一番効いた施策) binログ停止(古谷 17:24) MySQLの設定ファイル(/etc/mysql/mysql.conf.d/mysqld.cnf)でbinログの出力を停止 disable-log-bin=1 innodb_flush_log_at_trx_commit=0 ログ出力を無効化(うえはら 17:43) nginx、mysqlのログ出力を停止 アプリケーション側のログ出力停止 ownerGetChairsの改善(西田 17:43) distanceをメモ化するようにした コネクション数を調整(うえはら 17:49) db.SetMaxIdleConns(50) db.SetMaxOpenConns(50) 時間が足りない or スコアがあがらずできなかった施策 マッチング処理の修正(うえはら 13:00~16:00) 実装したが椅子が配車されないエラーの解消を乗り越えられず、断念… chairsのアクセストークンをキャッシュ(古谷 15:36) 叩かれるクエリ数は大幅に削減できた(30000→100)が、うまくスコアが上がらなかった 別の箇所にボトルネックが移ったのかもしれない nginxパラメータチューニング(古谷 17:39) ベンチマークのエラーでうまく動かず… サーバ分割(うえはら 16:00 ~ 17:00) nginx+goとMySQLの2台構成に変更しようとしたら、ベンチマークでデータ不整合のエラーがおきうまく動かすことができなかった 2台ともchairとrideのマッチングを行うサービスが動いていたため、データ不整合が起きたことに競技終了後気づいた…(止めておけば…) 良かった点 1万点を超えるという目標を達成できた isucon2回目1名+初参加2名のチームで135位、12514点というのは結果としては上々なのではないかと感じる 全員で改善タスクを進められた 何も手をつけられないということがなく、全員が何かしらの改善を進められた サーバ上への環境構築、デプロイスクリプトの仕込みがほぼ問題なく進められた デプロイ面で特に問題が生じることはなかった ただpproteinの仕込みでnginxの設定がうまくいかず1時間ほど手こずってしまったので、手順書に注意書きを書いておきたい ツールを作っておくと快適に改善を進められるので今後もツール面を充実させていきたい 反省点 あまりアプリケーションの中身を理解できずに改善を進めてしまった アプリケーションの仕様理解が重要な今回の問題だと、機械的にスロークエリログやアプリケーションログを元に高速化してもスコアアップに繋がりにくく、手を出せる箇所が少なかった アプリケーション理解が深まるような何らかの仕組みづくりをしたい 普段の業務でも仕様理解は重要なので、今後も意識していきたい マッチング処理をバグらせてしまっていつまでも実装できなかった テーブル構造を変えずにクエリだけで実装しようとして辛くなってしまったので、テーブルに都合の良いカラム追加してアプリケーションコードを単純にして実装しやすくするのが大事だなと思った SSE(Server-Sent Events)について全く知らなかったので勉強しておきたい ユーザの椅子の状態をリアルタイムに更新するのに使える技術として紹介されていた これからの業務で生かそうと思う点 技術的な知識習得だけでなく目の前のアプリケーション理解は何よりも大事 タスクランナー(go-task)やドキュメント自動生成ツール(tblsなど)を使って業務効率化を図っていきたい SSE(Server-Sent Events)についてプロダクトに組み込めるようなところがあれば使ってみたい まとめ ISUCON14に参加し、チーム「ktc-isucon-bu」として12514点、全チーム831チームのうち135位という結果を残すことができました。 反省点はありますが、ISUCON初参加のメンバーがいる中でスコア10000点を超えるという目標を達成できた点は良かったです。 この反省を糧に、次回のISUCON15ではさらなる高みを目指していきたいと思います。 初ISUCONにもかかわらず参加いただいたチームメンバーの古谷さん、西田さん、また今回参加までには至らずとも興味・関心をお寄せいただいた社員の皆さんありがとうございました! 最後にこの素晴らしいイベントを開催して下さった運営の皆さんに感謝いたします。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の21日目の記事です🎅🎄 はじめに こんにちは。KINTO FACTORY開発グループのうえはら( @penpen_77777 )です。 2024年の7月に入社し、KINTO FACTORYのバックエンドの開発を担当しています。 今回は12/8に開催されたISUCON14にKINTO Technologies社員で参加してきたので、その内容や結果を共有したいと思います。 ISUCON14とは ISUCONとは、LINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです。 優勝すると賞金100万円を獲得できます! 今年は12/8(日)の10:00〜18:00で開催されました。 私(うえはら)は昨年のISUCON13からパフォーマンスチューニングの知識習得はもちろん、エンジニアとしての腕試し・スキル向上を目的に参加しています。 :::message ISUCONは、LINEヤフー株式会社の商標または登録商標です。 https://isucon.net/ ::: チーム「ktc-isucon-bu」 社内Slackでうえはらがメンバーを募集し、チーム「ktc-isucon-bu」を結成しました。 メンバは以下の通りです。 うえはら( @penpen_77777 ) ISUCON13にも参加しており、今年で2回目 古谷 ISUCON初参加 西田 ISUCON初参加 ISUCONでは初期実装で使われている言語がいくつかありますが、今回はGo言語を選択しました。 また、初参加のメンバーが多いチームのためランキング上位を狙うというよりもスコアが10000点を超えることを目標にしました。 参加に向けての事前準備 ISUCONでは競技本番での改善タスクに集中できるように、典型的な作業は自動化もしくは容易に実行できるようにするのが重要です。 今回は、以下の準備を行いました。 環境構築・デプロイコマンドの整備 ドキュメント生成ツールの整備 計測ツールの整備 個人練習/チーム練習 環境構築・デプロイコマンドの整備 go-taskはターミナルでタスクを実行するのに特化したツール、いわゆるタスクランナーです。 https://taskfile.dev/ 従来だとmakeコマンドをタスクランナーとして使われることが多いと思いますが、個人的にはgo-taskの方がより便利に感じたので今回はgo-taskを使用しました。 (makeと異なりインストールが必要なのが難点ですが、そこを除けば業務でも十分活用できるかなと思います) 例えば以下のようなtaskfile.yamlを作成し、 setup タスクと deploy タスクを定義します。 version: '3' tasks: setup: cmds: # 環境構築用コマンドを記述 - echo "setup" deploy: cmds: # デプロイ用コマンドを記述 - echo "deploy" 作成後、以下のように task タスク名 でコマンドを叩くことでタスクを実行できます。 # 環境構築タスクを実行 $ task setup setup # デプロイタスクを実行 $ task deploy deploy また、タスク実行時にコマンドライン引数を渡すこともでき、受け取った引数をタスク内に埋め込むこともできます。 version: '3' tasks: setup: cmds: - echo "setup {{ .CLI_ARGS }}" deploy: cmds: - echo "deploy {{ .CLI_ARGS }}" # サーバisu1に対してsetupタスクを実行 $ task setup -- isu1 setup isu1 # サーバisu2に対してdeployタスクを実行 $ task deploy -- isu2 deploy isu2 上のコードは簡単な例ではありますが、実際使用する際にはコマンドライン引数を用いて反映先のサーバを自由に切り替えられるように作っておくことで チームでの分担作業が捗ります。 その他、go-taskを使うメリットは以下の通りです。 サブディレクトリで作業していても、親ディレクトリに存在するtaskfile.yamlを検出してタスクを実行できる ディレクトリの位置を気にせず、タスクを実行できる タスクからタスクを呼び出すことができるため、定義したタスクの再利用性を高められる deploy タスクを実行する前に setup タスクを実行するなど、タスクを組み合わせて実行できる ISUCONで使用するツールを全てtaskfile.yamlに記述しておくことで、ツールの使用方法(必要なオプションなど)を知らなくても task コマンドを叩くだけで実行できる 事前に taskfile.yaml のテンプレートを作成しておき、本番では一部の変数を書き換えるだけであらゆる問題がきても柔軟に対応できるようにしておきました。 ドキュメント生成ツールの整備 以下の2つのツールを使用しました。 https://github.com/k1LoW/tbls DBの中身を読み取り、ER図とスキーマの説明が入ったドキュメント(markdown)を生成 データベースに繋ぎに行かなくてもスキーマ定義を確認できるため、DBの構造を理解するのに便利 CI/CDパイプラインでドキュメントを自動生成することで、DB構造の変更を追いやすくなる 詳しくは以下の記事を参照ください https://devblog.thebase.in/entry/auto_generated_er_graph_by_tbls_and_github_actions https://github.com/mazrean/isucrud アプリケーションコードを読み取り、DBテーブルとの関係を可視化・ドキュメント(markdown)を生成 関数が多くなってくると図がわかりにくくなるのが欠点だったのですが、webブラウザ上でインタラクティブに見たい箇所を絞り込めるようになったのでとても使いやすくなった 計測ツールの整備 以下のツールを使用しました。 https://github.com/kaz/pprotein アクセスログ、スロークエリログ、プロファイルデータを収集し、可視化するツール ベンチマークを叩くとwebhookによって自動的にデータ集計を始めてくれるようになるので便利 データ収集時点でのコミットハッシュも記録してくれるので、どのコミットがスコアアップに寄与したかがわかりやすい^[pproteinでうまくGitコミットハッシュが取得できない現象に遭遇し、fork・コードを修正してPRを出しました。 https://github.com/kaz/pprotein/pull/37] 個人練習/チーム練習 練習に関して、初参加のメンバーがいきなりISUCONの過去問を解こうとしても難易度の高さから挫折してしまう可能性があります。 まずはパフォーマンスチューニングの雰囲気を感じ取ってもらうため、達人が教えるWebパフォーマンスチューニングという本を読んでもらいました。 https://gihyo.jp/book/2022/978-4-297-12846-3 書籍を読み進めつつ、以下を練習問題として週末チームで集まり解いていきました。 https://github.com/catatsuy/private-isu (書籍でもprivate-isuを使ってどのようにチューニングしていけば良いか紹介されています) 大体10~20万点くらいまでスコアを取れるようになってきたところで、昨年のISUCON13の問題に変えて解いていきました。 ここまできたら本番を意識した練習にスライドしても特に問題なく進められるようになったかなと思います。 今回のお題 https://www.youtube.com/watch?v=UFlcAUvWvrY ライドチェアサービス「ISURIDE」の改善 チェアオーナー(椅子の所有者)が椅子を提供 ユーザはアプリから椅子が配車され、その椅子を使って目的地まで移動できる スコアは移動距離やライド完了率などから計算される ユーザ満足度を高めるように改善を進める必要がある https://github.com/isucon/isucon14 結果 結果は以下の通りです。目標の1万点を超えることができました! 135位 / 831チーム中 12514点 できたこと デプロイスクリプト等の準備(うえはら 10:00~10:30) 当日マニュアル(古谷 10:00) 動作しているプロセスを確認しておおよその構成を理解する(古谷 10:00) アプリケーションマニュアルを読む(西田 10:00) MySQLにログインし、テーブルサイズを調べる インデックスを追加(西田 11:21) -- chair_locations CREATE INDEX idx_chair_id ON chair_locations(chair_id); CREATE INDEX idx_chair_id_created_at ON chair_locations(chair_id, created_at); -- chairs CREATE INDEX idx_owner_id ON chairs(owner_id); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_chair_sent_at_created_at ON ride_statuses (ride_id, chair_sent_at, created_at); pproteinの仕込み (うえはら 11:48) nginxの設定を修正するのに手こずり、1時間以上かかってしまう インデックス追加(西田 11:54) CREATE INDEX idx_ride_statuses_created_at_ride_id_chair_sent_at ON ride_statuses (created_at, ride_id, chair_sent_at); -- rides CREATE INDEX idx_chair_id_updated_at ON rides (chair_id, updated_at DESC); 動的パラメータを有効化・jsonパッケージを高速化(うえはら 12:38) // import文を修正 "encoding/json" → "github.com/goccy/go-json" // DB接続時の設定でInterpolateParamsをtrueに dbConfig.InterpolateParams = true インデックス追加(うえはら 13:05) -- chairs CREATE INDEX idx_chairs_access_token ON chairs(access_token); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_app_sent_at_created_at ON ride_statuses (ride_id, app_sent_at, created_at); -- rides CREATE INDEX idx_rides ON rides (user_id, created_at); -- coupons CREATE INDEX idx_coupons_used_by ON coupons(used_by); ユーザ・ステータスキャッシュ(古谷 14:30) usersとride_statusesテーブルをインメモリキャッシュ トランザクション範囲の修正(西田 14:48, 15:09) notificationのポーリング間隔調整(古谷 16:27) appGetNotificationとchairGetNotificationで返しているRetryAfterMsを30 msから300 msに変更 椅子とユーザのマッチング間隔を短くした(西田 17:18, 17:28) ISUCON_MATCHING_INTERVALを0.5 sから0.1 sに短くした スコアが2倍近く上がった(一番効いた施策) binログ停止(古谷 17:24) MySQLの設定ファイル(/etc/mysql/mysql.conf.d/mysqld.cnf)でbinログの出力を停止 disable-log-bin=1 innodb_flush_log_at_trx_commit=0 ログ出力を無効化(うえはら 17:43) nginx、mysqlのログ出力を停止 アプリケーション側のログ出力停止 ownerGetChairsの改善(西田 17:43) distanceをメモ化するようにした コネクション数を調整(うえはら 17:49) db.SetMaxIdleConns(50) db.SetMaxOpenConns(50) 時間が足りない or スコアがあがらずできなかった施策 マッチング処理の修正(うえはら 13:00~16:00) 実装したが椅子が配車されないエラーの解消を乗り越えられず、断念… chairsのアクセストークンをキャッシュ(古谷 15:36) 叩かれるクエリ数は大幅に削減できた(30000→100)が、うまくスコアが上がらなかった 別の箇所にボトルネックが移ったのかもしれない nginxパラメータチューニング(古谷 17:39) ベンチマークのエラーでうまく動かず… サーバ分割(うえはら 16:00 ~ 17:00) nginx+goとMySQLの2台構成に変更しようとしたら、ベンチマークでデータ不整合のエラーがおきうまく動かすことができなかった 2台ともchairとrideのマッチングを行うサービスが動いていたため、データ不整合が起きたことに競技終了後気づいた…(止めておけば…) 良かった点 1万点を超えるという目標を達成できた isucon2回目1名+初参加2名のチームで135位、12514点というのは結果としては上々なのではないかと感じる 全員で改善タスクを進められた 何も手をつけられないということがなく、全員が何かしらの改善を進められた サーバ上への環境構築、デプロイスクリプトの仕込みがほぼ問題なく進められた デプロイ面で特に問題が生じることはなかった ただpproteinの仕込みでnginxの設定がうまくいかず1時間ほど手こずってしまったので、手順書に注意書きを書いておきたい ツールを作っておくと快適に改善を進められるので今後もツール面を充実させていきたい 反省点 あまりアプリケーションの中身を理解できずに改善を進めてしまった アプリケーションの仕様理解が重要な今回の問題だと、機械的にスロークエリログやアプリケーションログを元に高速化してもスコアアップに繋がりにくく、手を出せる箇所が少なかった アプリケーション理解が深まるような何らかの仕組みづくりをしたい 普段の業務でも仕様理解は重要なので、今後も意識していきたい マッチング処理をバグらせてしまっていつまでも実装できなかった テーブル構造を変えずにクエリだけで実装しようとして辛くなってしまったので、テーブルに都合の良いカラム追加してアプリケーションコードを単純にして実装しやすくするのが大事だなと思った SSE(Server-Sent Events)について全く知らなかったので勉強しておきたい ユーザの椅子の状態をリアルタイムに更新するのに使える技術として紹介されていた これからの業務で生かそうと思う点 技術的な知識習得だけでなく目の前のアプリケーション理解は何よりも大事 タスクランナー(go-task)やドキュメント自動生成ツール(tblsなど)を使って業務効率化を図っていきたい SSE(Server-Sent Events)についてプロダクトに組み込めるようなところがあれば使ってみたい まとめ ISUCON14に参加し、チーム「ktc-isucon-bu」として12514点、全チーム831チームのうち135位という結果を残すことができました。 反省点はありますが、ISUCON初参加のメンバーがいる中でスコア10000点を超えるという目標を達成できた点は良かったです。 この反省を糧に、次回のISUCON15ではさらなる高みを目指していきたいと思います。 初ISUCONにもかかわらず参加いただいたチームメンバーの古谷さん、西田さん、また今回参加までには至らずとも興味・関心をお寄せいただいた社員の皆さんありがとうございました! 最後にこの素晴らしいイベントを開催して下さった運営の皆さんに感謝いたします。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の20日目の記事です🎅🎄 はじめに はじめまして、KINTOテクノロジーズ( KTC )でモバイルアプリ(Flutter)の開発を担当しているHand-Tomiです。 最近、KTCでは「Flutterチーム」を立ち上げ、アプリケーション開発を進めています。その中で、導入して特に効果的だと感じた手法をいくつかご紹介したいと思います。 今回は、「GitHub Actions」と「Firebase Hosting」を活用して、コードレビュー時に便利なWebプレビューを実現する方法について解説します。 皆さんの参考になれば幸いです。 🎯 目標 この記事の目的は、プルリクに /preview というコメントを追加することで、デバッグ用のWebページリンクが自動でコメントとして投稿される仕組みを実現することです。 ![preview_comment_and_link](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview_comment_and_link.png =600x) 上記のリンクをクリックすると、以下のようにFlutterプロジェクトのアプリケーションが表示されます。 ![preview](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview.png =400x) 🔍 Webプレビューを使う理由 コードレビューする際、動作を確認するにはソースコードをクローンし、設定やビルドを行う必要がありますが、この方法は時間がかかります。一方、 Webプレビュー を設定しておくと、簡単かつ迅速に動作確認を行うことが可能です。 以降では、この仕組みを実現する方法をstep-by-stepでご紹介します。 Firebaseのセットアップ 🌐 Firebaseプロジェクトを作成 まだFirebaseプロジェクトがない場合、Firebaseコンソールから新しいプロジェクトを作成してください。 プロジェクト名は「sample」としました。 他の機能を使用する予定がない場合は無効にしておきます(有効にしておいても問題ありません)。 しばらく待つと プロジェクトの作成が完了しました! ⚙️ Firebase CLIのセットアップ FlutterプロジェクトにFirebase Hostingを設定する予定です。Firebase CLIを使用すれば簡単に設定できるので、Terminalでセットアップしてみましょう。 1. Firebase CLIのインストール Firebase CLIのインストール方法はいくつかありますが、npmがインストールされているMacOSでは、以下のコマンドを使用することで簡単にインストールできます。 npm install -g firebase-tools 他の環境でのインストール方法は、 こちら を参照してください。 2. Firebase CLIへのログイン 以下のコマンドを実行して、CLI上でFirebaseにログインしてください。 firebase login 🔧 Firebase Hostingのセットアップ 準備が整ったので、FlutterプロジェクトにFirebase Hostingを設定してみましょう。 1. webframeworksの有効化 FlutterアプリケーションをFirebase Hostingにデプロイするためには、実験的な機能である webframeworks を有効にする必要があります。 firebase experiments:enable webframeworks 2. Firebase Hostingの初期化 Flutterプロジェクトのルートディレクトリで、以下のコマンドを実行してFirebaseをセットアップしましょう。 firebase init hosting 上記のコマンドを実行すると、以下のような質問が表示されます。 # Firebaseプロジェクトは先ほど作成したsampleプロジェクトを選択します。 ? Please select an option: Use an existing project ? Select a default Firebase project for this directory: sample-1234 (sample) # こちらは「Yes」で問題ありません。 ? Detected an existing Flutter Web codebase in the current directory, should we use this? Yes # リージョン選択の質問です。デフォルトの「us-central1 (Iowa)」を選択しました。 ? In which region would you like to host server-side content, if applicable? us-central1 (Iowa) # 自分で作成する予定なので「No」にしました。 ? Set up automatic builds and deploys with GitHub? No i Writing configuration info to firebase.json... i Writing project information to .firebaserc... ✔ Firebase initialization complete! 質問に回答すると、 firebase.json ファイルが生成されます。 :::message alert FlutterプロジェクトにWebプラットフォームが含まれていない場合、エラーが発生することがあります。その場合は、以下のコマンドを実行してWebプラットフォームを追加してください。 flutter create . --platform web ::: 3. デプロイ 試しにデプロイしてみましょう。 firebase deploy 上記のコマンドを実行すると、以下のようにHosting URLが表示されます。 ... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/sample-1234/overview Hosting URL: https://sample-1234.web.app このURLを開くと、Flutterプロジェクトが正常に表示されることを確認できます。 GitHub Actionsの作成 次に、プルリクエストに /preview とコメントすると実行されるYAMLファイルを作成しましょう。 🔑 Firebaseサービスアカウントキーの準備 GitHub Actionsを通じてFirebaseにデプロイするためには、Firebaseサービスアカウントのキーが必要です。簡単にキーを取得する方法は、以下のコマンドを使用することです。 firebase init hosting:github 上記のコマンドを入力すると、以下のような質問が表示されます。ソースコードがあるリポジトリを user/repository の形式で指定してください。 # Github repositoryを入力してください。 `user/repository`のように記載してください。 ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) Hand-Tomi/sample すると、Firebaseが自動的にGiGitHubポジトリのSecretsにサービスアカウントキーを設定し、以下のようにSecretsの定数名を教えてくれます(例: FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 )。この定数名は保存しておきましょう。 ✔ Created service account github-action-1234 with Firebase Hosting admin permissions. ✔ Uploaded service account JSON to GitHub as secret FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234. i You can manage your secrets at https://github.com/Hand-Tomi/sample/settings/secrets. 続いて、以下のような質問が出てきますが、必要なものは揃っているので Control+C (Windowsの場合は Ctrl+C )で終了します。 ? Set up the workflow to run a build script before every deploy? ✍️ GitHub ActionsのYAMLファイル作成 いよいよGitHub ActionsのYAMLファイルを作成しましょう。 Flutterプロジェクトのルートにある .github/workflows ディレクトリにYAMLファイルを作成し、以下のコードを入力します。 name: Command Execute Deploy Web on: issue_comment: types: [created] jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} name: Deploy Web runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true steps: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.issue.number }}/head fetch-depth: 0 - name: Set Up Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Install Dependencies run: flutter pub get - id: deploy-web name: Deploy to Firebase Hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} expires: 7d channelId: "issue_number_${{ github.event.issue.number }}" env: FIREBASE_CLI_EXPERIMENTS: webframeworks - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | ✅ Previewがデプロイされました。 - **リンク** : ${{ steps.deploy-web.outputs.details_url }} firebaseServiceAccount には、事前に作成しておいたSecretsの定数名を指定してください(例: FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 )。 firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} その後、該当リポジトリにマージし、プルリクエストに /preview とコメントを残すと、自動的にActionsが実行され、GitHub Actionsがリンクをコメントしてくれます。 💡 参考:GitHub ActionsのYAMLコードの説明 上記のYAMLコードについて解説します。 実行タイミング on: issue_comment: types: [created] issue_comment を使用すると、コメントが作成されたときに自動的にWorkflowが実行されます。このコメントはプルリクエストだけでなく、Issueにコメントされた場合も含まれます。 今回の記事ではプルリクエストのコメントに限定したいので、以下のように jobs の if に github.event.issue.pull_request を入れて、プルリクエストのみ実行するようにします。 jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} また、 issue_comment を使う場合、Checkoutする場所を変更する必要があります。 issue_comment は現在のプルリクエストの最新コミット情報を持っていないため、そのままCheckoutするとデフォルトブランチの最新コミットにCheckoutされてしまいます。 そのため、以下のように actions/checkout に ref を指定する必要があります(参考: https://github.com/actions/checkout/issues/331#issuecomment-1438220926 )。 - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.issue.number }}/head fetch-depth: 0 コメントメッセージ確認 コメントのメッセージが /preview かどうかを確認します。以下のように github.event.comment.body を確認し、メッセージが /preview の場合のみdeploy-webジョブ内の処理を実行します。 jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} 同時実行の防止 /preview を残した後、すぐに再度 /preview をコメントすると、同時実行になる可能性があります。 concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true この場合、GitHub Actionsは concurrency を通じて同時実行を防止します。 重要なのは、 jobs の下に配置することです。 jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} name: Deploy Web runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true 上記のように jobs の下に配置しないと、 if で github.event.comment.body == '/preview' を確認せずに concurrency が実行され、 /preview コメント後すぐに /preview 以外のコメントを残した場合、Actionが実行されなくなります。 デプロイ 以下のステップはFirebase Hostingにデプロイするものです。 - id: deploy-web name: Deploy to Firebase Hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} expires: 7d channelId: "issue_number_${{ github.event.issue.number }}" env: FIREBASE_CLI_EXPERIMENTS: webframeworks firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} : Firebaseのサービスアカウントの認証キーです。以前に取得しておいたものを入力してください。 expires: 7d : 有効期限です。このように設定するとプレビューサイトは7日後に無効になります。 channelId: "issue_number_${{ github.event.issue.number }}" : Firebase Previewのチャンネル名です。 live 以外の channelId を指定すると、Firebase Previewにデプロイされ、有効期限を設定できます。 FIREBASE_CLI_EXPERIMENTS: webframeworks : Firebase CLIの実験的な機能である webframeworks を使用します。Flutter Webの場合は必須です。 リンクコメント peter-evans/create-or-update-comment を使用して、リンクのコメントを残しました。これを使うと、簡単にリアクションを残したり、コメントを追加・編集できます。 - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | ✅ Previewがデプロイされました。 - **リンク** : ${{ steps.deploy-web.outputs.details_url }} if: success() : 成功したときのみこのステップが実行されます。 token: ${{ secrets.GITHUB_TOKEN }} : コメントを残すためには GITHUB_TOKEN が必要です。別途の設定は必要ありません。 issue-number: ${{ github.event.issue.number }} : どのIssueにコメントするかの指定です。 issue_comment で実行したWorkflowの場合、 github.event.issue.number でIssue番号を確認できます。 steps.deploy-web.outputs.details_url : 上記のデプロイステップから取得したURLを表示します。 他の情報を載せたい場合は、 こちら を参照してください。 おわりに 今回ご紹介した手法により、コードレビュー時の動作確認がこれまでよりも迅速かつ簡単に行えるようになります。チーム全体の開発効率が向上し、より良いプロダクトの提供につながるでしょう。 ただし、デバッグ用のWebプレビューを導入する際には、セキュリティ面への配慮が必要であり、OS特有の差異をどのように解決するかも検討する必要があります。OSの機能を多用する場合、メリットよりもデメリットが大きいかもしれません。 しかし、OSの機能をあまり使用しないプロジェクトでは、デメリットよりもメリットが多いため、ぜひ皆さんの開発環境にも導入してみてください。 また、flutterチームのメンバーが執筆した他の記事もぜひご覧ください! Flutter開発: CustomPaintとPathでQRコードの枠線をデザインする ここまで読んでいただき、ありがとうございました。
アバター