TECH PLAY

KINTOテクノロジーズ

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

936

Hello, this is HOKA from the Organizational Human Resources Team in the Human Resources Group. This blog is a continuation of Overcoming Challenges in the In-House Rollout of the 10X Innovation Culture Program [Part 1] August: Lessons learned from Speaking at Google Cloud Next Tokyo '24 Google Cloud Next Tokyo '24, held by Google Cloud Japan, LLC on August 1–2, 2024, is a flagship event bringing together business leaders, innovators, and engineers for a major cloud conference. The event featured a variety of programs, including keynote speeches, live sessions, and hands-on sessions, all centered around key business topics like generative AI and security.  For more details, click here -> https://cloudonair.withgoogle.com/events/next-tokyo-24 From our company, Kisshi, General Manager of the Development Support Division, and Awache, Manager of the Database Reliability Engineering Group, took the stage at the 10X Innovation Culture Program's experience workshop. The workshop was a great opportunity to highlight real-world examples of the 10X Innovation Culture Program in action. Leading up to the actual event, we had numerous meetings, rehearsed at the Google office, and had lunch together, giving us plenty of time to interact with people from Google. And then the real thing began. The extraordinary atmosphere of the spacious venue at Pacifico Yokohama naturally created a feeling of "let’s learn something new today" kind of mindset. First, a representative from Google introduced us to 10X. The speaker shared, in down-to-earth and relatable words, the background, reasons, and results behind Google’s promotion of the 10X Innovation Culture Program both inside and outside the company. Hearing this, the KINTO Technologies (KTC) members in the audience couldn’t help but think, "10X is exactly what we need too!" and the energy in the room instantly lifted. After that, Kisshi and Awache took the stage. Kisshi and Awache talked about the following three topics: The background to the cultural transformation initiative and why we decided to implement the 10X Innovation Culture Program Key takeaways and important points learned from actually implementing the program Future plans and next steps In particular, they shared the following key takeaways from their experience implementing the 10X Innovation Culture Program: It's important to discard the notion that "only Google can do it" and just give it a try without overthinking it. It is important to develop internal facilitators to achieve in-house independence from the bottom up. Culture is not something that is created in one leap, but rather something that is noticed and experienced many times before becoming ingrained. Many questions came up during the Q&A session. One of them was, “10X sounds amazing, but isn’t it difficult to actually implement?” While significant results from the 10X Innovation Culture Program have yet to emerge, we hope these efforts can provide at least a little inspiration. Having attended the Google Cloud Next Tokyo '24, I felt an overwhelming difference from the 10X Innovation Culture Program created and held internally in July. What stood out were: A dedicated venue, separate from the office, puts participants in the right mindset to truly listen. Hearing "10X" directly from Google employees carries real weight and resonates deeply. The event felt discussion-heavy, but perhaps a stronger focus on input and learning would be even more impactful. With that foundation, participants may naturally start thinking, "I want to bring 10X thinking into my daily work." Having these as hypotheses, plans are in place to put them to the test at the 10X Innovation Culture Program this fall. September: Participating in the 10X Facilitator Training First, we decided to start by training facilitators who can guide smooth understanding and discussion of 10X concepts. Our hypotheses were: "10X" when shared by Google employees, resonates strongly. What seemed like a discussion-focused setting turned out to need more input. The reason? Those running the 10x program didn’t have enough clarity or input themselves on what it really is, making it tough to convey to others. After reaching out to Google for advice, they kindly agreed to set up facilitation trainings. Selecting members for the facilitation training was a challenge. But since the 10X Innovation Culture Program is, at its core, a leadership program, managers were chosen to participate. Facilitator Training Day At the beginning of the training, Kota-san from Google, who is the main person in charge of this session, said, "It's going to be extremely intense." The goal is to hear something once, remember it, and be able to say it yourself. It’s designed to let you experience two phrases that Google lives by: Steal with pride Feedback is a gift! "You’re about to spend more time thinking about 10X than Google employees do", was what we were told. "What on earth is about to begin?" The training kicked off in that atmosphere. [First half of the training] Input Time Just as Kota-san mentioned, the first part of the session was all about learning. A Googler gave a presentation on the six key elements of 10X, and KTC employees had some solid "input time" to really understand each one. Take the first element, DEI, for example. At Google, 50% of employees have taken the training Unconscious Bias @ Work , which helps raise awareness of unconscious bias. On top of that, they run weekly Googlegeist to track how well they’re doing in areas like inclusiveness and equity. We got to hear not just about these concrete initiatives, but also the impact they’ve had. Similarly, for each of the six 10X elements, the presenter shared real examples from Google, along with their own personal thoughts and stories from working there. [Second half of the training] Output Time After hearing the presentation from Google, the KTC members split into groups of six. Each person took one of the six elements, memorized it, and gave a presentation that included their own personal story or experience. The listeners weren’t just passive after each presentation, everyone gave feedback on what worked well and what could be improved. We also received feedback from the Google team. The whole process might’ve felt a bit intense, just like Kota-san warned, but it was the perfect chance to "steal with pride" from Google’s approach and truly embrace the idea that "feedback is a gift."  And the feedback wasn’t just technical. Ideas started flowing, too: "We should be sharing the vision more across the whole company," "What’s the current status of our goal-setting process?", "I want to be able to give more concrete examples: let’s increase opportunities to talk directly with executives!" and so on. More and more ideas bubbled up. By learning about 10X, we got to witness that “aha!” moment when people started comparing it to where KTC stands today. In fact, after the training, a lot of KTC members shared feedback such as: "I always thought 10X was great, but it felt like something distant, like it was a distant thing. But by memorizing it, tying it to my own experiences and presenting it to others in such a short time, it suddenly became something I truly owned." And getting immediate feedback from the Google team on the spot was such a valuable opportunity. October: Preparing for the Third 10X Innovation Culture Program I wrote this blog in October, after completing the facilitator training and while gearing up for the third round of the 10X Innovation Culture Program. The third 10X iteration will be divided into two days. It's a brand-new approach for us. Additionally, 47% of participants this time are joining for the first time. Will they be even more eager and engaged than the group in July? Will all the effort we put into the September’s facilitator training pay off? I’ll share how it all turns out in a follow-up post on the TechBlog. To be continued in the 10X Innovation Culture Program: The Struggles and Challenges of Building It In-House [Part 2], coming January 2025.
アバター
Introduction Managing assets in a multi-package Flutter project can get tricky, especially when it comes to loading local JSON files. This requires a different approach than a normal single package project, leaving developers scratching their heads. In this article, we’ll break down how to load local JSON files effectively in a multi-package Flutter project. This article is the entry for day 23 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Test Project Setup For this study, we prepared a simple project managed with multiple packages. Here’s how it’s structured: 🎯 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 Load a File in Asset You’ll often see explanations showing that, in a typical single-package project, you can load assets like this: flutter: assets: - assets/ # Specify the assets folder import 'package:flutter/services.dart' show rootBundle; Future<String> loadAsset() async { return await rootBundle.loadString('assets/sample.json'); } The official explanation is pretty much the same. https://docs.flutter.dev/ui/assets/assets-and-images I built a simple Widget that simply reads a JSON file from an Asset and displays it as a String in a Text 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), ), ); } } But this method doesn’t work for loading assets across multiple packages. 🤨 Loading Assets with flutter_gen In most cases, loading Assets across multiple packages can be handled easily with flutter_gen. flutter_gen is a tool that generates code from Asset paths and localization files, enabling type-safe asset loading. It also natively supports loading Assets across multiple packages. https://github.com/FlutterGen/flutter_gen To load Assets from multiple packages with flutter_gen, the following settings are required. flutter_gen: assets: outputs: package_parameter_enabled: true With this setting, running flutter_gen will generate code for loading assets from multiple packages. You can then use this generated code to load Assets in a type-safe way. Here’s how the previous example looks when rewritten using flutter_gen: import 'package:{YOUR_PACKAGE_NAME}/gen/assets.gen.dart'; Future<String> loadAsset() async { return await rootBundle.loadString(Assets.sample); } Assets from multiple packages can be loaded with code like this: 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(), ), ); } } The file paths are structured, which makes things much neater! Asset management for team development is now a breeze. But sometimes, you might prefer not to rely on tools too much, right? Let’s look at how to handle that next. Loading Assets without flutter_gen Of course, you can also load Assets from multiple packages without flutter_gen. In that case, you can load Assets by specifying the path as follows. :::message packages/ {package name} / {folder path} /file name ::: Package name is the name specified in the pubspec.yaml of the package in which the asset is stored. Folder path is the path specified under assets in that Package's pubspec.yaml. 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), ), ); } } This way, you can load Assets from multiple packages without flutter_gen. For small projects or solo development, this approach should work just fine. Not gonna lie; without fully understanding these path rules, there was a lot of trial and error: trying relative paths, tweaking settings... And hitting build errors over and over. It was a struggle. About Paths Specified in pubspec.yaml When managing assets locally, pay close attention to the paths you specify in pubspec.yaml. Assets paths are set relative to pubspec.yaml, but watch out for differences between: /assets and /assets/{subfolder} . For example, if you move a JSON file into a subfolder like this: ├── packages/ │ ├── features/ │ │ ├── assets/ │ │ │ └── jsons/ │ │ │ └── sample.json <<< HERE │ │ ├── lib/ │ │ │ └── package1.dart │ │ └── pubspec.yaml │ ├── .../ I assumed that specifying /assets would also include files in /assets/{subfolder} , but even after changing the loadString path to packages/local_asset/assets/jsons/sample.json , it still wouldn't load. When you move files into a subfolder, you need to explicitly include that subfolder in pubspec.yaml, like this: flutter: assets: - /assets/jsons/ Now, you can load packages/local_asset/assets/jsons/sample.json . If you manage assets in subfolders, make sure to add those subfolders to pubspec.yaml. By the way, the examples so far have used assets folder, but you can also change the name of it. As long as the pubspec.yaml and actual path configuration match, assets can also be loaded without any problems from folders other than assets folder. Summary This time, we covered how to load local JSON files in a Flutter multi-package setup. Since my main focus is iOS development, I thought it would be easy, but it turned out to be more challenging than expected. There’s limited information available on working in multi-package environments, so I'd like to share more insights as I continue learning in the future.
アバター
KINTOテクノロジーズで my route(iOS) を開発しているRyommです。 CIクレジットの節約のため、 いくつか取り組んできたことを紹介します。 はじめに 弊プロジェクトにおいては、CIツールとしてBitriseを利用しています。 昨年は通常のユニットテストに加えて スナップショットテストを導入 したり、 SPMに移行 したりしました。 気付くとBitrise CIの1回あたりの実行時間が多いときは約25分ほどに膨れ上がり、多くの実装が行われた月は予算を超過してしまうこともしばしば発生するようになってしまいました。 Bitriseは契約分を超過すると高額になってしまうので、執筆時点の為替レートでは超過分のCI1回の実行に約400円ほど掛かっている計算です。たっっか! そういうわけで、クレジットが超過しそうになるとCIを動かさないためにPRのマージも必要最低限に抑える動きが生まれてしまっていました。 この状況を打破すべく取り組んできた、弊プロジェクトにおけるクレジット節約術を紹介します。 CLIツールのセットアップを見直す BitriseのBuild結果を見ると、どのステップにどのくらい時間が掛かったのか見ることができます。 BitriseのBuild結果 これを見ると、「Script Runner」にて12分も掛かっていることがわかります。 これは、swiftLintやLicensePlistのセットアップを行っているステップです。 以前執筆した記事 にて紹介しましたが、Build Phaseで実行するためにworkspaceとは独立して作成したパッケージにて、ライブラリを落として使えるようにしています。 ま〜たしかにこれは時間かかるよね〜というところなので、短縮していきます。 幸いここで使いたいライブラリはBuild Tool Pluginに対応しているため、そちらに移し替えていくことでこのステップを省くことができます。 元々 license_plist.yml や .swiftlint.yml などの設定は済んでいるため、ただプロジェクトの Package Dependencies にパッケージを追加し、ターゲットの Build Phase の Run Build Tool Plug-ins にプラグインを追加してあげればOKです。 Build Phaseの設定 LicensePlistはプラグインだと outputPath の場所の指定が効かないため、 README にあるように Settings.bundle の下にライセンスのファイルを移動するようBuildPhaseに含める必要があります。 また、パッケージはFrameworksでリンクしているパッケージではなく、アプリ本体に含める必要があります。 これで「Script Runner」のステップが丸々不要になったので、12分の短縮...そして消費クレジットも半分になりました!🎉 BitriseのBuild結果 さらにプロジェクト構成が単純化し、セットアップやバージョン更新の際に別途シェルを実行するような必要もなくなりました。 今回のケースでは全てBuild Tool Pluginに対応していたため構成ごと変更しましたが、別のアプローチとして nest も試しました。こちらは既存のCLIツールを別パッケージに分けて管理する構成のまま、CI時間を短縮させることができます。 tools ディレクトリ配下にあったCLIツールをインストールするためのパッケージをnestに置き換えます。 Project/ ├── Hoge.xcworkspace ├── Hoge.xcodeproj ├── Test/ │ └── ... ├── ... └── tools └── nextfile.yaml // ここを置き換える nest bootstrap nestfile.yaml を実行すると tools/.nest/bin 内にバイナリがインストールされていることが確認できるので、これを Build Phase で実行されるように設定します。 Build Phaseでswiftlintを設定 Build Tool Plugin に対応していなかったりする場合には有用かもしれません。 テストを見直す 弊プロジェクトでは、1つのテストターゲット内に全てのテストが詰め込まれていたため、常に全てのテストが実行されていました。 またその中でもスナップショットテストは全て実行すると1時間ほど掛かる非常に重いテストのため、リファレンス画像と比較するメソッドはCI上ではコメントアウトして実行されないようにしていました。しかし、比較前の非同期の描画処理の待機などは実行されてしまうため、失敗時はタイムアウトが積もり積もって長い時間待つこととなり、これもクレジットを食い潰す要因の一つとなっていました。 そこで、CI上では動かしていないスナップショットテストを別のテストターゲットへ切り離し、TestPlanを使って実行するテストをコントロールするようにしてみました。 まずは、時間のかかるスナップショット用のテストターゲットを作成します。 テストターゲットを作成 既存のテストターゲットを参考にターゲットの設定を行ったら、 Build Phases の Compile Sources もしくは各テストファイルの File inspector より Target Membership から、スナップショットテストを新規作成したターゲットへ移していきます。 このとき、移動したテストファイルが元のテストターゲット内のテストファイルと依存関係があるとビルドできなくなるため、都度依存を切り離していきます。 ターゲットを変更していく 次に、TestPlanを作成します。 TestPlanとは、実行するテストとテストの設定をまとめたものです。その際、実行するテストを指定できる範囲はテストターゲット単位です。 このためにテストターゲットを分離させました。 TestPlanはスキーマに紐づけることができ、弊アプリではスキーマとTestPlanが1対1になるようにしています。 そしてCI上で使用するためのスキーマ用のTestPlanにおいては、スナップショットテストを実行しないようにします。 TestPlanの設定 実際に動かしてみると、CI上では失敗しない限り大きくは実行時間は変わりません。しかし、ローカルにおけるテスト体験は大きく改善しました。ロジックのみの変更でもスナップショットテストが実行されてしまっていたのが、チェックを外すだけで実行しないようにできるため、大幅な時間短縮が実現しました。 元々CI上では動かしていなかったテストをそもそも実行しないようにした形ですが、クレジット利用状況とのバランスを調整した上でスナップショットテストも実行できるようにしていきたいと考えています。 おわりに ここに紹介したもの以外にも、型推論に時間が掛かっているコードを修正したり、使用していないアセットを削除することなどもビルド時間の短縮につながります。 これらの取り組みによってCI1回あたりの時間は平均約22分程度から12分程度まで短縮され、45%ほどのクレジット節約が実現しました。 今回はすぐにできる範囲としてビルド前後の時間短縮が主でしたが、次はビルド自体の時間ももっと短縮したいです。
アバター
1. Introduction Hello, this is Torii ( @yu_torii ) from the Common Service Development Group. I'm a fullstack software engineer, primarily working on both backend and frontend development. As part of the KINTO Member Platform Development Team, I focus on frontend engineering while also contributing to internal initiatives involving generative AI. This article introduces Sherpa, our internal chatbot powered by LLM and integrated into Slack. We’ll explore its RAG capabilities and its translation function, which works through Slack reactions. Sherpa is designed to facilitate generative AI adoption within the company by enabling employees to naturally interact with AI in their daily Slack conversations. The goal is to eliminate the need for frontend development and deploy it quickly within the company, enhancing efficiency and collaboration. The name Sherpa is inspired by the Sherpa people, known for guiding climbers on Mount Everest. Just as Sherpas are reliable supporters for mountaineers, Sherpa aims to be a dependable assistant for improving work efficiency and streamlining information sharing within the company. By the way, the Sherpa on the banner was generated by team members of the Creative Group as a surprise reveal for the KINTO Technologies General Meeting (KTC CHO All-Hands Meeting). A big thank you to them! This initiative was carried out in collaboration with Wada-san ( @cognac_n ), who is leading the Generative AI Development Project. We have been driving the adoption of generative AI within the company through various improvements, such as introducing a RAG pipeline, setting up a local development environment, and implementing a translation and summarization feature using Slack emoji reactions. :::details Click here for Wada-san's article @ card @ card @ card ::: Note that this article does not go into detail about RAG or generative AI technology itself. Instead, it focuses on the implementation process and feature enhancements. Additionally, Sherpa's LLM runs on Azure OpenAI. What You Will Gain from This Article How to use chat functions powered by generative AI in a Slack Bot This article introduces an implementation example of a chatbot that combines Slack and LLM, allowing users to trigger translations and summaries using emoji reactions or natural message posts. Techniques for retrieving Confluence data, including HTML sanitization Learn how to fetch Confluence documents using Go and sanitize HTML to prepare text for summarization and embedding. Implementing a simple RAG pipeline using FAISS and S3 This section explains the steps and considerations for setting up a simple RAG pipeline using the FAISS indexing and S3. While the response speed may not be very fast, this approach provides a cost-effective way to integrate a basic RAG system. :::message This article is based on the implementation status at the time of writing. Please note that there are still many areas for improvement. Since this is an internal tool, it is not designed for production-level performance. Additionally, this article does not cover details on setting up the development environment, such as deployment with AWS SAM, building a pipeline with Step Functions, or configuring CI/CD with GitHub Actions. ::: :::details Table of Contents (Expanded) 1. Introduction What You Will Gain from This Article 2. Overview of the Overall Architecture 3. AI-Powered Chat Function Using Slack Bot Chat Function and Reaction Function Chat Function: Usage Scenario Reaction Feature: Usage Scenario Benefits of This Configuration 3.5 Actual Use Cases Summary of Chapter 3 4. Implementation Policy and Internal Design Overall Process Flow Function Determination by Conditional Branching The Role of Sanitization Limiting RAG Usage to Confluence References Considerations for Scalability and Maintainability 5. Introduction to Code examples and Configuration Files 5.1 [Go] Receiving and Parsing Slack Events 5.2 [Go] HTML Text Sanitization 5.3 [Python] Example of an LLM Query 5.4 [Python] Example of a RAG Search Call 5.5 [Python] Embedding and FAISS Indexing ::: 2. Overview of the Overall Architecture Here, we will explain the process by which Sherpa returns answers using generative AI from two perspectives: - The generative AI chat function with users - The process of indexing Confluence documents. Processing Generative AI Chat Functions with Users Calling Sherpa on Slack There are two ways to call Sherpa: via chat or with a reaction emoji. Users can call Sherpa by mentioning it in a channel or sending a direct message to request a generative AI response. A reaction call allows you to request a translation or summary of a message by adding a translation/summary emoji reaction. Go Slack Bot Lambda The bot that handles Slack events is built with Go and runs on AWS Lambda. It receives questions and reactions, then determines processing based on the request type. When using Azure OpenAI for LLM queries or retrieving embedded data, this component generates requests and forwards them to Python Lambda. Request to LLM and RAG reference in Python Lambda Python Lambda is divided into functions responsible for LLM queries, RAG references, and related processes. It receives requests from Go Lambda, queries the LLM, and generates an answer using RAG. Returning an answer to Slack The generated answer is sent back to Slack via Go Slack Bot Lambda. The translation and summary functions that can be invoked via emoji reactions are also integrated into this flow. :::details Architecture diagram (processing user requests) flowchart LR subgraph "Processing user requests" A["User(Slack)"] -->|"Question・Emoji"| B["Go Slack Bot(Lambda)"] B -->|"Request"| C["Python Lambda RAG/LLM"] C -->|"Answer"| B B -->|"Answer"| A end ::: The Process of Indexing Confluence Documents To use the RAG pipeline, you need to prepare your Confluence documents in a way that makes them easy to summarize and embed. These preprocessing steps are structured into workflows using StepFunctions and are executed automatically on a scheduled basis. Document retrieval and HTML sanitization (Go implementation) A Lambda function implemented in Go retrieves documents from the Confluence API, cleans up HTML tags, and makes the text more manageable. The sanitized text is output as JSON. Summary processing (Go + Python Lambda invocation) Summarization aims to refine the text, making it easier to process with Embedding and RAG. The Go implementation of Lambda invokes a Python Lambda that processes requests to the Azure OpenAI Chat API, shortens the text, and converts it back to JSON. FAISS indexing and S3 storage (Indexer Lambda) The Indexer Lambda embeds the summarized text and generates a FAISS index, then stores the index and meta information in S3. This enables instant retrieval of indexed data upon a query, ensuring the RAG pipeline runs smoothly. :::details Architecture diagram (Confluence document indexing flow) flowchart LR subgraph "Indexing Preparation StepFunctions" D["Go Lambda (Document Retrieval/HTML Sanitization)"] D --> E["Go Lambda (summary/Python call)"] E --> F["Indexer Lambda(Embedding/FAISS/S3)"] end ::: By combining this pre-processing with request-time processing, Sherpa enables generative AI responses that incorporate company-specific knowledge with simple operations in Slack. In the following chapters, we'll dive deeper into these components. 3. AI-Powered Chat Function Using Slack Bot The previous chapter provided an overview of the architecture. In this chapter, we’ll focus on how users can make use of Sherpa’s features with simple, natural interactions in Slack, and how these features can benefit them. Here, we will illustrate what Sherpa can do and which scenarios it can be useful in, while the next chapters will systematically explain the implementation details. Chat Function and Reaction Function Sherpa offers a variety of generative AI capabilities, powered by natural interactions within Slack. Chat function : By posting a question, attaching files or images, including external links, or sending a message in a specific format, users can trigger LLM queries or, in certain cases, RAG searches to obtain relevant answers. By integrating AI into Slack, an everyday tool, users can seamlessly adopt generative AI without the need to learn new environments or commands. Reaction function : By simply adding specific emoji reactions to a message, users can trigger translations, delete messages, and perform other actions—enabling additional operations without requiring commands. Chat Function: Usage Scenario Basic Questions and Answers Simply posting a question allows users to receive AI-generated responses through LLM. Example scenario: Asking "What are the steps for this project?" provides an instant answer, taking into account thread history and speaker context for a more accurate response. File, image, and external link processing Attaching a file and asking "Summarize this" will generate a summary of the document. Uploading an image allows Sherpa to extract text and provide relevant answers. Sharing an external link enables the bot to analyze and summarize webpage content, incorporating it into the LLM-generated response. Example scenario: -Get a short summary of meeting notes from a text file. -Extract text information from an image. -Generate a concise summary of an external article. Confluence page lookup (RAG integration) :confluence: By using index:Index name , users can perform a RAG-based search on Confluence pages containing company documentation, such as internal rules and application procedures. Example scenario: If company policies and procedures are documented in Confluence, users can instantly access relevant information tailored to internal workflows. Example scenario: Easily retrieve project-specific settings and instructions. that would otherwise be difficult to search for. Reaction Feature: Usage Scenario Adding specific emoji reactions makes calling up functions even more intuitive. Translation : Adding a translation emoji automatically translates the message into the specified language, helping break down language barriers and improve communication. Message deletion Unnecessary Sherpa responses can be deleted instantly by adding a single emoji, keeping Slack channels organized. Benefits of This Configuration Seamless and intuitive AI usage : Users can use AI without needing new commands—they simply interact with Slack as they normally do (e.g., posting messages, adding emojis reactions), reducing learning costs. Embedding AI into everyday tool (Slack) : By integrating generative AI directly into Slack, AI can be naturally incorporated into daily workflows without friction. Additionally, reaction-based interactions enable users to perform actions like translation and deletion without typing commands, making AI even more accessible. Scalable and easily extendable : If new models or additional features need to be introduced, the existing flow (chat-based queries and emoji interactions) can be easily expanded by adding conditions. 3.5 Actual Use Cases Below are some real-world examples of how Sherpa is used within Slack. Example 1: Image Recognition When you attach an image to a message, Sherpa recognizes the text within the image and responds with its content. ![Image Recognition](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-context.png =500x) Image Recognition Example 2: Answers Based on Confluence Documentation By using :confluence: emoji, users can retrieve answers based on the Confluence documentation. ![Answer based on Confluence documentation](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-confluence-rag.png =500x) Answer based on Confluence documentation As shown above, Sherpa can be triggered from workflows, allowing it to function similarly to a prompt store. Example 3: Requesting English Translations via the Translation Reaction Add translation reactions to messages that users want to translate into English To share a Japanese message with an English speaker, simply add the :sherpa_translate_to_english: reaction emoji. This will automatically translate the message into English. ![English translation reaction](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-translation.png =500x) English translation reaction To prevent misunderstandings and ensure users do not overly rely on automatic translations, a notification in multiple languages clarifies that the translation was AI-generated. Additionally, we provide instructions on how to use the feature to encourage adoption. Besides English, Sherpa supports translation into multiple languages. Summary of Chapter 3 In this chapter, we focused on the user perspective—"what can be done with what operations." Building on the usage scenarios discussed here, the following chapters will systematically explain the implementation details. We will cover: - How to integrate Go-based bot with Python Lambdas. - How Slack events are processed and how RAG search works. - The details of the file extraction and sanitization processes. 4. Implementation Policy and Internal Design In this chapter, we will explain the implementation policy and internal design behind how Sherpa provides a variety of generative AI functions through Slack messages and reactions. This section focuses on the overall concept, role distribution, and scalability considerations. Specific code snippets and configuration files will be provided in the next chapter. Overall Process Flow Slack event reception (Go Lambda) Events occurring in Slack—such as message posts, file attachments, image uploads, external link insertions, and emoji reactions—are sent to an AWS Lambda function written in Go via the Slack API. The Go function then analyzes these events and determines the appropriate action based on user intent (e.g., normal chat, translation, Confluence reference). Text processing and sanitization on the Go side Text is extracted from external links or files and added to the prompt as context. When referencing external links, meaningful tags such as table , ol , ul are preserved while unnecessary tags are removed to optimize token usage. LLM queries and RAG searches on the Python side If necessary, the Go function invokes a Python Lambda for LLM queries or a separate Python Lambda for RAG searches (e.g., for Confluence references). For example, if :confluence: is included in the request, the Go function calls the RAG search Lambda. If no index is specified, it defaults to the primary index. Otherwise, the text is passed to the LLM Lambda for standard query processing. Reply and display in Slack The Python Lambda returns the generated response to the Go function, which then posts it back to Slack. This enables users to access advanced features through familiar Slack interactions—such as emojis, keywords, and file attachments—without needing to memorize commands. Function Determination by Conditional Branching Processing is routed based on specific emojis (e.g., :confluence: ), keywords, the presence of files or images, and whether an external link is included. To add new features, simply introduce new conditions on the Go side and, if necessary, extend the logic for invoking the corresponding Python Lambdas (e.g., for LLM or RAG tasks). The Role of Sanitization Sanitizing on the Go side removes unnecessary HTML tags to improve token efficiency and ensure clean input for the model. Key structural elements such as table, ol, and ul are retained to preserve the information structure and maintain useful context for the model. Limiting RAG Usage to Confluence References RAG search is only performed when explicitly specified with :confluence: . By default, summarization, translation, and Q&A tasks are handled via direct LLM queries, ensuring RAG logic is triggered only for Confluence references. Embedding generation for Confluence documents and FAISS index updates are handled periodically by StepFunctions, ensuring that the latest index is always available for queries. Considerations for Scalability and Maintainability Conditional branching based on emojis, keywords, or the presence of files/images minimizes the number of code modifications required when introducing new features, enhancing maintainability. The separation of concerns—where text formatting and sanitization are handled on the Go side, while LLM queries and RAG searches are managed on the Python side—improves code clarity and facilitates future model replacements or additional processing logic. In the next chapter, we will introduce specific code snippets and configuration examples based on these design principles. 5. Introduction to Code examples and Configuration Files This chapter introduces a brief implementation example based on the implementation policy and design concepts explained in Chapter 4. This chapter contains the following sections: 5.1 [Go] Receiving and parsing Slack events Explains how to use Slack's Events API to receive and process events such as messages and emoji reactions. 5.2 [Go] HTML text sanitization Provides an example of sanitizing HTML when referencing external links. 5.3 [Python] Example of an LLM query Shows how to query an LLM using a Python-based Lambda function. 5.4 [Python] Example of a RAG search call Demonstrates how to perform a RAG search call, such as for Confluence lookups. 5.5 [Python] Embedding and FAISS indexing. Provides an example of Lambda code that periodically embeds Confluence documents and updates the FAISS index. 5.1 [Go] Receiving and Parsing Slack Events This section explains the basic steps for using the Slack Events API to receive and analyze events with Go code on AWS Lambda. We will also cover the settings on the Slack side (OAuth & Permissions, event subscription) and how to check the scopes required when using the chat.postMessage method (such as chat:write ), to clarify the necessary preparations before implementation. Configuration Steps on Slack Side Create an app and check the App ID : Create a new app at https://api.slack.com/apps . Once created, find your App ID (a string starting with A ) on the Basic Information page ( https://api.slack.com/apps/APP_ID/general , where APP_ID is a unique ID for your app). This App ID identifies your Slack App and can be used to access the URLs for OAuth & Permissions and Event Subscriptions pages described below. Granting scopes via OAuth & Permissions : Visit the OAuth & Permissions page ( https://api.slack.com/apps/APP_ID/oauth ) and add the necessary scopes to Bot Token Scopes. For example, if the chat.postMessage method is needed to post messages to a channel, checking this page ( https://api.slack.com/methods/chat.postMessage ) under "Required scopes" will indicate that chat:write is required. After granting the scope, click "reinstall your app" to apply the changes in your workspace. Then, the changes will be reflected. ![Checking required scopes](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image.png =500x) Checking required scopes ![Setting scope](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-1.png =500x) Setting scope Enabling the Events API and subscribing to events : Enable the Events API on the "Event Subscriptions" page ( https://api.slack.com/apps/APP_ID/event-subscriptions ) and set the AWS Lambda endpoint described below in "Request URL". Add the events you want to subscribe to, such as message.channels or reaction_added . This allows Slack to send a notification to the specified URL whenever a subscribed event occurs. ![Event Subscriptions Explanation](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-3.png =500x) Event Reception and Analysis on AWS Lambda Once the configuration is complete on the Slack side, Slack will send a POST request to AWS Lambda via API Gateway whenever a subscribed event occurs. Step 1: Parsing Slack Events Use the slack-go/slackevents package to parse the received JSON into an EventsAPIEvent structure. This makes it easier to identify event types, such as URL validation and CallbackEvent. func parseSlackEvent(body string) (*slackevents.EventsAPIEvent, error) { event, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { return nil, fmt.Errorf("Failed to parse Slack event: %w", err) } return &event, nil } @ card Step 2: Handling URL Verification Requests When setting up the integration, Slack will initially send an event with type=url_verification . To verify the URL, simply return the challenge value as received. Once verified, Slack will continue sending event notifications. 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 Step 3: Verifying Signatures and Ignoring Retry Requests Slack includes a request signature that allows verification of authenticity (implementation omitted). Additionally, in case of a failure or outage, Slack may resend the request as a retry. The X-Slack-Retry-Num header can be used to identify retry attempts and prevent processing the same event multiple times. func verifySlackRequest(body string, headers http.Header) error { // Signature verification process (omitted) 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": "Ignoring Slack retry request"}) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, nil } Step 4: Handling CallbackEvent The CallbackEvent includes actions such as message postings and adding reactions. At this stage, the system checks whether :confluence: is included in the message, if a file is attached, or if a translation-related emoji is present. Based on this assessment, it proceeds to text processing and Python Lambda invocation, as described in section 5.2 and beyond. // handleCallbackEvent processes callback events (covered in Section 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: // Processing for AppMentionEvent (details explained in 5.2) case *slackevents.MessageEvent: // Processing for MessageEvent (details explained in 5.2) case *slackevents.ReactionAddedEvent: // Processing for ReactionAddedEvent (details explained in 5.2) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: http.StatusOK}, nil } Complete Handler Code Example These steps combine to define an AWS Lambda handler. :::details Complete code example of the handler 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("Failed to validate request: %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 } ::: Summary of 5.1 In this section, we explained how to obtain the Slack App's App ID, grant scopes using OAuth & Permissions, and configure event subscriptions in Event Subscriptions. We also covered the process of receiving and parsing Slack events, including handling URL verification, signature validation, ignoring retry requests, and processing CallbackEvents. From section 5.2 onwards, we will introduce specific examples of CallbackEvent processing, text handling in Go, and sending queries to Python Lambda. 5.2 [Go] HTML Text Sanitization Sanitizing External Link References HTML text retrieved from external links may contain unnecessary tags such as script and style , which are not needed for generating responses. Passing this directly to the LLM increases the token count, leading to higher model costs and potentially reducing response accuracy. The following code uses the bluemonday package for basic sanitization. It removes unnecessary tags while preserving important ones like table , ol , and ul , ensuring the text remains well-structured and readable. Additionally, the addNewlinesForTags function inserts line breaks after specific tags, improving text formatting. This helps optimize queries to the model by ensuring that only the necessary information is passed in a structured and efficient format. @ card func sanitizeContent(htmlContent string) string { // Basic sanitization ugcPolicy := bluemonday.UGCPolicy() sanitized := ugcPolicy.Sanitize(htmlContent) // Allow specific tags in a custom policy customPolicy := bluemonday.NewPolicy() customPolicy.AllowLists() customPolicy.AllowTables() customPolicy.AllowAttrs("href").OnElements("a") // Add line breaks after specific tags to improve readability formattedContent := addNewlinesForTags(sanitized, "p") // Apply final sanitization after enforcing the custom policy 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 } This process ensures that the model receives only text with unnecessary tags removed, improving response accuracy and cost efficiency. By preserving essential structures such as tables and bullet points while inserting line breaks after specific tags, the model can better interpret the provided context. 5.3 [Python] Example of an LLM Query Below is an example of how to query an LLM (e.g. Azure OpenAI) in Python. With OpenAIClientFactory , you can dynamically switch models and endpoints, enabling the reuse of a common client creation process across multiple Lambda handlers. Client Creation Process OpenAIClientFactory dynamically generates a client for either Azure OpenAI or OpenAI, depending on api_type and model . Since API keys and endpoints are retrieved from environment variables and secret management services, code modifications are minimized even when updating models or configurations. 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 Query Processing The chatCompletionHandler function extracts messages , model , temperature , and other parameters from the JSON received in the HTTP request. It then queries the LLM using the client generated by OpenAIClientFactory . Responses are returned in JSON format. If an error occurs, a properly formatted error response is generated using a common error handling function. 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", }, } This mechanism allows different Lambda handlers to make LLM queries using the same procedure, ensuring flexibility in adapting to models and endpoint changes. 5.4 [Python] Example of a RAG Search Call This section provides instructions on performing a Retrieval Augmented Generation (RAG) search in Python. By vectorizing internal knowledge, such as Confluence documents, and performing similarity searches using the FAISS index, it is possible to integrate highly relevant information into LLM responses. A key consideration is the handling of the faiss library. faiss is a large package and may exceed the capacity limits of the Lambda Layers. To work around this, it is common to use EFS or containerize the Lambda function. To simplify deployment, the setup_faiss function dynamically downloads and extracts faiss from S3, then adds it to sys.path , making faiss available at runtime. What is FAISS? FAISS (Facebook AI Similarity Search) is an approximate nearest neighbor search library developed by Meta (Facebook). It provides tools for creating indexes to efficiently search for similar images and text. @ card FAISS Setup Using the setup_faiss Function To use FAISS in the Lambda environment, the setup_faiss function performs the following steps: Build and archive the faiss package in a local/CI environment Developers install the faiss-cpu package in a CI environment such as GitHub Actions and package the necessary binaries into a tar.gz archive. Upload to S3 The archived faiss_package.tar.gz is uploaded to an S3 bucket. By storing the package in an appropriate bucket and path (e.g., for staging or production), the Lambda function can dynamically retrieve it during execution. Dynamic loading with setup_faiss when running Lambda In the Lambda execution environment, the setup_faiss function downloads and extracts faiss_package.tar.gz from S3 at startup and adds it to sys.path . This enables the Lambda function to run import faiss , allowing for efficient vector searches using embeddings. Example: Uploading the FAISS Package to S3 Using GitHub Actions The following GitHub Actions workflow demonstrates how to install faiss-cpu , package it for Lambda use, and upload it to S3. This setup uses GitHub Actions Secrets and Environment Variables to manage AWS credentials and S3 bucket names securely, avoiding hardcoded values. @ card name: Build and Upload FAISS on: workflow_dispatch: inputs: environment: description: Deployment Environment 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" # Install required packages (faiss-cpu) - name: Install faiss-cpu run: | set -e echo "Installing faiss-cpu..." pip install faiss-cpu --no-deps # Archive the faiss binary - name: Archive faiss binaries run: | mkdir -p faiss_package pip install --target=faiss_package faiss-cpu tar -czvf faiss_package.tar.gz faiss_package # Set AWS credentials (configure Secrets or Roles based on your environment) - 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 # Upload to 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." In the above example, faiss_package.tar.gz is uploaded to S3 with the key lambda/faiss_package.tar.gz . Dynamic Loading Process on Lambda side ( setup_faiss function) The setup_faiss function handles the dynamic loading of FAISS at runtime. It downloads faiss_package.tar.gz from S3, extracts it to the /tmp directory, and appends the package path to sys.path . This enables import faiss to be executed within Lambda, allowing FAISS index lookups to be performed. # setup_faiss example: Download the FAISS package from S3 and add it to 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 has already been imported.") except ImportError: logger.info("faiss not found. Downloading from S3.") faiss_package_key = "lambda/faiss_package.tar.gz" faiss_package_path = "/tmp/faiss_package.tar.gz" faiss_extract_path = "/tmp/faiss_package" # Download the package from S3 and extract it 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 was imported successfully.") RAG Search Using Embeddings and FAISS Indexes The search_data function loads the FAISS index retrieved from S3 locally and searches for documents that best match the query. Documents are vectorized using the Embeddings client (Azure OpenAI or OpenAI) generated by the get_embeddings function, enabling fast searches using 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("An invalid API type specified.") 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] Asynchronous Downloads and Lambda Handlers Within async_handler , setup_faiss is executed, and the FAISS index file is retrieved from S3 using download_files . Afterward, search_data performs a RAG search, and the results are returned in JSON format. 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"No file found in '{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) Summary of 5.4 Avoid Lambda layer capacity issues with setup_faiss faiss dynamic loading. Asynchronous I/O and S3 usage allow FAISS index to be loaded without containerization or EFS connectivity. search_data searches the embedded index, enabling RAG to quickly provide similar documents. This enables high-speed knowledge searches using RAG, providing LLM answers enriched with company-specific information. 5.5 [Python] Embedding and FAISS Indexing This section provides an example of periodic batch processing that embeds internal company documents (such as Confluence pages) and creates or updates the FAISS index. The index used in the RAG pipeline is essential for generative AI to incorporate company-specific knowledge into its responses. To maintain accuracy, we regularly update embeddings and rebuild the FAISS index, ensuring that the latest information is always accessible. Process Overview Retrieve JSON-formatted documents from S3. Generate embeddings for the retrieved documents (using the Embeddings API from OpenAI or Azure OpenAI). Index the embedded text using FAISS. Upload the FAISS index to S3. By executing these steps periodically via Lambda batch processing or a Step Functions workflow, RAG searches will always use the latest index when queried. Step 1: Loading a JSON document Download and parse a JSON file from S3 (e.g., summarized Confluence pages) and convert it into a list of Document objects. 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]: """ Reads a JSON file and returns a list of Document objects. The JSON format is expected to be: [{"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("The top-level JSON structure is not a list.") documents = [] for record in data: if not isinstance(record, dict): logger.warning(f"Skipped record (not a dictionary): {record}") continue title = record.get("title", "") content = record.get("content", "") metadata = { "id": record.get("id"), "title": title, "url": record.get("url"), } # Create a Document object combining the title and content doc = Document(page_content=f"Title: {title}\nContent: {content}", metadata=metadata) documents.append(doc) logger.info(f"Loaded {len(documents)} documents.") return documents Step 2: Embedding and FAISS indexing The vectorize_and_save function embeds the documents using the Embeddings client obtained from get_embeddings and creates a FAISS index. It then saves the index locally. 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: """ Embed the documents, create a FAISS index, and save it locally. """ # Split the document into smaller chunks using a text splitter text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=128) split_docs = text_splitter.split_documents(documents) logger.info(f"{len(split_docs)} split documents") # Vectorize using embeddings and build FAISS index db: FAISS = FAISS.from_documents(split_docs, embeddings) logger.info("Vector DB construction completed.") os.makedirs(output_dir, exist_ok=True) db.save_local(output_dir) logger.info(f"Vector DB saved to {output_dir}") Step 3: Uploading the Index to S3 By uploading the locally created FAISS index to S3, it can be easily accessed by the RAG search 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: """ Upload the FAISS index to 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 index file uploaded to s3://{s3_bucket}/{s3_index_key}") Step 4: Running the entire flow in Lambda The index_to_s3 function encapsulates the entire process. It downloads JSON from S3, generates embeddings, creates a FAISS index, and uploads the index to S3. This process can be executed periodically using a workflow such as Step Functions, ensuring that the index remains up to date. 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]: """ Download JSON from S3, generate embeddings, create a FAISS index, and upload the index to S3. """ env = os.getenv("ENV") if env is None: error_msg = "ENV environment variable not set." 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" # Set up faiss if necessary (download from S3) setup_faiss(s3_client, s3_bucket) # Download the JSON file from S3 s3_client.download_file(s3_bucket, json_s3_key, local_json_path) documents = load_json(local_json_path) # Get Embeddings client secrets = get_secret() embeddings = get_embeddings(secrets) # Vectorization and FAISS indexing vectorize_and_save(documents, local_index_dir, embeddings) # Upload the index file to S3 upload_faiss_to_s3(s3_client, s3_bucket, local_index_dir, index_s3_path) return { "status": "success", "message": "FAISS index created and uploaded to S3.”, "output": { "bucket": s3_bucket, "index_key": index_s3_path, }, } except Exception as e: logger.error(f"An error occurred during the indexing process: {e}") return {"status": "error", "message": str(e)} Summary of 5.5 load_json loads a JSON file, and vectorize_and_save generates embeddings and creates a FAISS index. upload_faiss_to_s3 uploads the local index to S3. index_to_s3 consolidates the entire process, ensuring that the latest index is created and updated through regular batch processing. This enables automated batch processing to embed internal documents and maintain FAISS indexes for RAG searches. 6. Summary In this article, we covered the development background and technical implementation of Sherpa, out internal chatbot powered by LLM and integrated into Slack. We also outlined the steps for implementing the RAG pipeline, sanitizing Confluence documents, building a search infrastructure using Embeddings and FAISS indexes, and extending functionality with features like translation and summarization. This system enables employees to seamlessly integrate generative AI into their Slack workflow, allowing them to access advanced information capabilities without needing to learn new tools or commands. 7. Future Outlook We will actively work on the following improvements and expansions to further enhance Sherpa. Strengthening Azure-based deployment We will fully integrate with Azure services such as Azure Functions and Azure CosmosDB, significantly improving the performance and scalability of the RAG pipeline. Introducing Azure Cosmos DB Vector Search We will implement vector search functionality on Azure Cosmos DB for NoSQL, enabling more advanced search capabilities. @ card Utilizing AI Document Intelligence By actively incorporating AI Document Intelligence, we aim to expand the knowledge scope of RAG and enhance information utilization across a broader range of use cases. @ card Diversification and sophistication of models We will continue integrating cutting-edge models by expanding support beyond GPT-4o to include GPT-o1, Google Gemini, and other state-of-the-art AI models. Implementing Web UI To overcome the expression and interaction limitations imposed by Slack, we will develop a Web UI, allowing for more diverse interactions and the flexible deployment of new features. Enhancing prompt management We will template existing prompts, making them easily reusable across different use cases. Additionally, we will enhance the prompt-sharing functionality to further promote the adoption of generative AI across the company. Realizing multi-agent capabilities By deploying specialized agents dedicated to tasks such as summarization, translation, and RAG search, and allowing flexible combinations through an Agent Builder, we will enable more advanced and adaptable information processing. Evaluating and improving RAG accuracy We will build test sets and conduct automated answer evaluations to quantitatively measure accuracy and continuously improve quality. Enhancements based on user feedback By incorporating real-world usage data and feedback, we will optimize dialogue flows, fine-tune prompts, and strengthen external service integrations, ensuring that Sherpa remains highly convenient and useful. Through these efforts, we will continue evolving Sherpa, growing it into a powerful internal support tool that meets a wide range of business needs.
アバター
1. Introduction Hello, this is Torii ( @yu_torii ) from the Common Service Development Group. I'm a fullstack software engineer, primarily working on both backend and frontend development. As part of the KINTO Member Platform Development Team, I focus on frontend engineering while also contributing to internal initiatives involving generative AI. This article introduces our internal generative AI tool, our internal chatbot powered by LLM and integrated into Slack. We’ll explore its RAG capabilities and its translation function, which works through Slack reactions. Internal generative AI tool is designed to facilitate generative AI adoption within the company by enabling employees to naturally interact with AI in their daily Slack conversations. The goal is to eliminate the need for frontend development and deploy it quickly within the company, enhancing efficiency and collaboration. Internal generative AI tool aims to be a dependable assistant for improving work efficiency and streamlining information sharing within the company. By the way, the character was generated by team members of the Creative Group as a surprise reveal for the KINTO Technologies General Meeting (KTC CHO All-Hands Meeting). A big thank you to them! This initiative was carried out in collaboration with Wada-san ( @cognac_n ), who is leading the Generative AI Development Project. We have been driving the adoption of generative AI within the company through various improvements, such as introducing a RAG pipeline, setting up a local development environment, and implementing a translation and summarization feature using Slack emoji reactions. :::details Click here for Wada-san's article @ card @ card @ card ::: Note that this article does not go into detail about RAG or generative AI technology itself. Instead, it focuses on the implementation process and feature enhancements. Additionally, Internal generative AI tool's LLM runs on Azure OpenAI. What You Will Gain from This Article How to use chat functions powered by generative AI in a Slack Bot This article introduces an implementation example of a chatbot that combines Slack and LLM, allowing users to trigger translations and summaries using emoji reactions or natural message posts. Techniques for retrieving Confluence data, including HTML sanitization Learn how to fetch Confluence documents using Go and sanitize HTML to prepare text for summarization and embedding. Implementing a simple RAG pipeline using FAISS and S3 This section explains the steps and considerations for setting up a simple RAG pipeline using the FAISS indexing and S3. While the response speed may not be very fast, this approach provides a cost-effective way to integrate a basic RAG system. :::message This article is based on the implementation status at the time of writing. Please note that there are still many areas for improvement. Since this is an internal tool, it is not designed for production-level performance. Additionally, this article does not cover details on setting up the development environment, such as deployment with AWS SAM, building a pipeline with Step Functions, or configuring CI/CD with GitHub Actions. ::: :::details Table of Contents (Expanded) 1. Introduction What You Will Gain from This Article 2. Overview of the Overall Architecture 3. AI-Powered Chat Function Using Slack Bot Chat Function and Reaction Function Chat Function: Usage Scenario Reaction Feature: Usage Scenario Benefits of This Configuration 3.5 Actual Use Cases Summary of Chapter 3 4. Implementation Policy and Internal Design Overall Process Flow Function Determination by Conditional Branching The Role of Sanitization Limiting RAG Usage to Confluence References Considerations for Scalability and Maintainability 5. Introduction to Code examples and Configuration Files 5.1 [Go] Receiving and Parsing Slack Events 5.2 [Go] HTML Text Sanitization 5.3 [Python] Example of an LLM Query 5.4 [Python] Example of a RAG Search Call 5.5 [Python] Embedding and FAISS Indexing ::: 2. Overview of the Overall Architecture Here, we will explain the process by which internal generative AI tool returns answers using generative AI from two perspectives: - The generative AI chat function with users - The process of indexing Confluence documents. Processing Generative AI Chat Functions with Users Calling internal generative AI tool on Slack There are two ways to call internal generative AI tool: via chat or with a reaction emoji. Users can call internal generative AI tool by mentioning it in a channel or sending a direct message to request a generative AI response. A reaction call allows you to request a translation or summary of a message by adding a translation/summary emoji reaction. Go Slack Bot Lambda The bot that handles Slack events is built with Go and runs on AWS Lambda. It receives questions and reactions, then determines processing based on the request type. When using Azure OpenAI for LLM queries or retrieving embedded data, this component generates requests and forwards them to Python Lambda. Request to LLM and RAG reference in Python Lambda Python Lambda is divided into functions responsible for LLM queries, RAG references, and related processes. It receives requests from Go Lambda, queries the LLM, and generates an answer using RAG. Returning an answer to Slack The generated answer is sent back to Slack via Go Slack Bot Lambda. The translation and summary functions that can be invoked via emoji reactions are also integrated into this flow. :::details Architecture diagram (processing user requests) flowchart LR subgraph "Processing user requests" A["User(Slack)"] -->|"Question・Emoji"| B["Go Slack Bot(Lambda)"] B -->|"Request"| C["Python Lambda RAG/LLM"] C -->|"Answer"| B B -->|"Answer"| A end ::: The Process of Indexing Confluence Documents To use the RAG pipeline, you need to prepare your Confluence documents in a way that makes them easy to summarize and embed. These preprocessing steps are structured into workflows using StepFunctions and are executed automatically on a scheduled basis. Document retrieval and HTML sanitization (Go implementation) A Lambda function implemented in Go retrieves documents from the Confluence API, cleans up HTML tags, and makes the text more manageable. The sanitized text is output as JSON. Summary processing (Go + Python Lambda invocation) Summarization aims to refine the text, making it easier to process with Embedding and RAG. The Go implementation of Lambda invokes a Python Lambda that processes requests to the Azure OpenAI Chat API, shortens the text, and converts it back to JSON. FAISS indexing and S3 storage (Indexer Lambda) The Indexer Lambda embeds the summarized text and generates a FAISS index, then stores the index and meta information in S3. This enables instant retrieval of indexed data upon a query, ensuring the RAG pipeline runs smoothly. :::details Architecture diagram (Confluence document indexing flow) flowchart LR subgraph "Indexing Preparation StepFunctions" D["Go Lambda (Document Retrieval/HTML Sanitization)"] D --> E["Go Lambda (summary/Python call)"] E --> F["Indexer Lambda(Embedding/FAISS/S3)"] end ::: By combining this pre-processing with request-time processing, internal generative AI tool enables generative AI responses that incorporate company-specific knowledge with simple operations in Slack. In the following chapters, we'll dive deeper into these components. 3. AI-Powered Chat Function Using Slack Bot The previous chapter provided an overview of the architecture. In this chapter, we’ll focus on how users can make use of internal generative AI tool’s features with simple, natural interactions in Slack, and how these features can benefit them. Here, we will illustrate what internal generative AI tool can do and which scenarios it can be useful in, while the next chapters will systematically explain the implementation details. Chat Function and Reaction Function internal generative AI tool offers a variety of generative AI capabilities, powered by natural interactions within Slack. Chat function : By posting a question, attaching files or images, including external links, or sending a message in a specific format, users can trigger LLM queries or, in certain cases, RAG searches to obtain relevant answers. By integrating AI into Slack, an everyday tool, users can seamlessly adopt generative AI without the need to learn new environments or commands. Reaction function : By simply adding specific emoji reactions to a message, users can trigger translations, delete messages, and perform other actions—enabling additional operations without requiring commands. Chat Function: Usage Scenario Basic Questions and Answers Simply posting a question allows users to receive AI-generated responses through LLM. Example scenario: Asking "What are the steps for this project?" provides an instant answer, taking into account thread history and speaker context for a more accurate response. File, image, and external link processing Attaching a file and asking "Summarize this" will generate a summary of the document. Uploading an image allows internal generative AI tool to extract text and provide relevant answers. Sharing an external link enables the bot to analyze and summarize webpage content, incorporating it into the LLM-generated response. Example scenario: -Get a short summary of meeting notes from a text file. -Extract text information from an image. -Generate a concise summary of an external article. Confluence page lookup (RAG integration) :confluence: By using index:Index name , users can perform a RAG-based search on Confluence pages containing company documentation, such as internal rules and application procedures. Example scenario: If company policies and procedures are documented in Confluence, users can instantly access relevant information tailored to internal workflows. Example scenario: Easily retrieve project-specific settings and instructions. that would otherwise be difficult to search for. Reaction Feature: Usage Scenario Adding specific emoji reactions makes calling up functions even more intuitive. Translation : Adding a translation emoji automatically translates the message into the specified language, helping break down language barriers and improve communication. Message deletion Unnecessary internal generative AI tool responses can be deleted instantly by adding a single emoji, keeping Slack channels organized. Benefits of This Configuration Seamless and intuitive AI usage : Users can use AI without needing new commands—they simply interact with Slack as they normally do (e.g., posting messages, adding emojis reactions), reducing learning costs. Embedding AI into everyday tool (Slack) : By integrating generative AI directly into Slack, AI can be naturally incorporated into daily workflows without friction. Additionally, reaction-based interactions enable users to perform actions like translation and deletion without typing commands, making AI even more accessible. Scalable and easily extendable : If new models or additional features need to be introduced, the existing flow (chat-based queries and emoji interactions) can be easily expanded by adding conditions. 3.5 Actual Use Cases Below are some real-world examples of how internal generative AI tool is used within Slack. Example 1: Image Recognition When you attach an image to a message, internal generative AI tool recognizes the text within the image and responds with its content. ![Image Recognition](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-context.png =500x) Image Recognition Example 2: Answers Based on Confluence Documentation By using :confluence: emoji, users can retrieve answers based on the Confluence documentation. ![Answer based on Confluence documentation](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-confluence-rag.png =500x) Answer based on Confluence documentation As shown above, internal generative AI tool can be triggered from workflows, allowing it to function similarly to a prompt store. Example 3: Requesting English Translations via the Translation Reaction Add translation reactions to messages that users want to translate into English To share a Japanese message with an English speaker, simply add the :ai_tool_translate_to_english: reaction emoji. This will automatically translate the message into English. ![English translation reaction](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-translation.png =500x) English translation reaction To prevent misunderstandings and ensure users do not overly rely on automatic translations, a notification in multiple languages clarifies that the translation was AI-generated. Additionally, we provide instructions on how to use the feature to encourage adoption. Besides English, internal generative AI tool supports translation into multiple languages. Summary of Chapter 3 In this chapter, we focused on the user perspective—"what can be done with what operations." Building on the usage scenarios discussed here, the following chapters will systematically explain the implementation details. We will cover: - How to integrate Go-based bot with Python Lambdas. - How Slack events are processed and how RAG search works. - The details of the file extraction and sanitization processes. 4. Implementation Policy and Internal Design In this chapter, we will explain the implementation policy and internal design behind how internal generative AI tool provides a variety of generative AI functions through Slack messages and reactions. This section focuses on the overall concept, role distribution, and scalability considerations. Specific code snippets and configuration files will be provided in the next chapter. Overall Process Flow Slack event reception (Go Lambda) Events occurring in Slack—such as message posts, file attachments, image uploads, external link insertions, and emoji reactions—are sent to an AWS Lambda function written in Go via the Slack API. The Go function then analyzes these events and determines the appropriate action based on user intent (e.g., normal chat, translation, Confluence reference). Text processing and sanitization on the Go side Text is extracted from external links or files and added to the prompt as context. When referencing external links, meaningful tags such as table , ol , ul are preserved while unnecessary tags are removed to optimize token usage. LLM queries and RAG searches on the Python side If necessary, the Go function invokes a Python Lambda for LLM queries or a separate Python Lambda for RAG searches (e.g., for Confluence references). For example, if :confluence: is included in the request, the Go function calls the RAG search Lambda. If no index is specified, it defaults to the primary index. Otherwise, the text is passed to the LLM Lambda for standard query processing. Reply and display in Slack The Python Lambda returns the generated response to the Go function, which then posts it back to Slack. This enables users to access advanced features through familiar Slack interactions—such as emojis, keywords, and file attachments—without needing to memorize commands. Function Determination by Conditional Branching Processing is routed based on specific emojis (e.g., :confluence: ), keywords, the presence of files or images, and whether an external link is included. To add new features, simply introduce new conditions on the Go side and, if necessary, extend the logic for invoking the corresponding Python Lambdas (e.g., for LLM or RAG tasks). The Role of Sanitization Sanitizing on the Go side removes unnecessary HTML tags to improve token efficiency and ensure clean input for the model. Key structural elements such as table, ol, and ul are retained to preserve the information structure and maintain useful context for the model. Limiting RAG Usage to Confluence References RAG search is only performed when explicitly specified with :confluence: . By default, summarization, translation, and Q&A tasks are handled via direct LLM queries, ensuring RAG logic is triggered only for Confluence references. Embedding generation for Confluence documents and FAISS index updates are handled periodically by StepFunctions, ensuring that the latest index is always available for queries. Considerations for Scalability and Maintainability Conditional branching based on emojis, keywords, or the presence of files/images minimizes the number of code modifications required when introducing new features, enhancing maintainability. The separation of concerns—where text formatting and sanitization are handled on the Go side, while LLM queries and RAG searches are managed on the Python side—improves code clarity and facilitates future model replacements or additional processing logic. In the next chapter, we will introduce specific code snippets and configuration examples based on these design principles. 5. Introduction to Code examples and Configuration Files This chapter introduces a brief implementation example based on the implementation policy and design concepts explained in Chapter 4. This chapter contains the following sections: 5.1 [Go] Receiving and parsing Slack events Explains how to use Slack's Events API to receive and process events such as messages and emoji reactions. 5.2 [Go] HTML text sanitization Provides an example of sanitizing HTML when referencing external links. 5.3 [Python] Example of an LLM query Shows how to query an LLM using a Python-based Lambda function. 5.4 [Python] Example of a RAG search call Demonstrates how to perform a RAG search call, such as for Confluence lookups. 5.5 [Python] Embedding and FAISS indexing. Provides an example of Lambda code that periodically embeds Confluence documents and updates the FAISS index. 5.1 [Go] Receiving and Parsing Slack Events This section explains the basic steps for using the Slack Events API to receive and analyze events with Go code on AWS Lambda. We will also cover the settings on the Slack side (OAuth & Permissions, event subscription) and how to check the scopes required when using the chat.postMessage method (such as chat:write ), to clarify the necessary preparations before implementation. Configuration Steps on Slack Side Create an app and check the App ID : Create a new app at https://api.slack.com/apps . Once created, find your App ID (a string starting with A ) on the Basic Information page ( https://api.slack.com/apps/APP_ID/general , where APP_ID is a unique ID for your app). This App ID identifies your Slack App and can be used to access the URLs for OAuth & Permissions and Event Subscriptions pages described below. Granting scopes via OAuth & Permissions : Visit the OAuth & Permissions page ( https://api.slack.com/apps/APP_ID/oauth ) and add the necessary scopes to Bot Token Scopes. For example, if the chat.postMessage method is needed to post messages to a channel, checking this page ( https://api.slack.com/methods/chat.postMessage ) under "Required scopes" will indicate that chat:write is required. After granting the scope, click "reinstall your app" to apply the changes in your workspace. Then, the changes will be reflected. ![Checking required scopes](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image.png =500x) Checking required scopes ![Setting scope](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-1.png =500x) Setting scope Enabling the Events API and subscribing to events : Enable the Events API on the "Event Subscriptions" page ( https://api.slack.com/apps/APP_ID/event-subscriptions ) and set the AWS Lambda endpoint described below in "Request URL". Add the events you want to subscribe to, such as message.channels or reaction_added . This allows Slack to send a notification to the specified URL whenever a subscribed event occurs. ![Event Subscriptions Explanation](/assets/blog/authors/torii/2024-12-23_ai_tool_slack/images/image-3.png =500x) Event Reception and Analysis on AWS Lambda Once the configuration is complete on the Slack side, Slack will send a POST request to AWS Lambda via API Gateway whenever a subscribed event occurs. Step 1: Parsing Slack Events Use the slack-go/slackevents package to parse the received JSON into an EventsAPIEvent structure. This makes it easier to identify event types, such as URL validation and CallbackEvent. func parseSlackEvent(body string) (*slackevents.EventsAPIEvent, error) { event, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { return nil, fmt.Errorf("Failed to parse Slack event: %w", err) } return &event, nil } @ card Step 2: Handling URL Verification Requests When setting up the integration, Slack will initially send an event with type=url_verification . To verify the URL, simply return the challenge value as received. Once verified, Slack will continue sending event notifications. 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 Step 3: Verifying Signatures and Ignoring Retry Requests Slack includes a request signature that allows verification of authenticity (implementation omitted). Additionally, in case of a failure or outage, Slack may resend the request as a retry. The X-Slack-Retry-Num header can be used to identify retry attempts and prevent processing the same event multiple times. func verifySlackRequest(body string, headers http.Header) error { // Signature verification process (omitted) 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": "Ignoring Slack retry request"}) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, nil } Step 4: Handling CallbackEvent The CallbackEvent includes actions such as message postings and adding reactions. At this stage, the system checks whether :confluence: is included in the message, if a file is attached, or if a translation-related emoji is present. Based on this assessment, it proceeds to text processing and Python Lambda invocation, as described in section 5.2 and beyond. // handleCallbackEvent processes callback events (covered in Section 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: // Processing for AppMentionEvent (details explained in 5.2) case *slackevents.MessageEvent: // Processing for MessageEvent (details explained in 5.2) case *slackevents.ReactionAddedEvent: // Processing for ReactionAddedEvent (details explained in 5.2) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: http.StatusOK}, nil } Complete Handler Code Example These steps combine to define an AWS Lambda handler. :::details Complete code example of the handler 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("Failed to validate request: %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 } ::: Summary of 5.1 In this section, we explained how to obtain the Slack App's App ID, grant scopes using OAuth & Permissions, and configure event subscriptions in Event Subscriptions. We also covered the process of receiving and parsing Slack events, including handling URL verification, signature validation, ignoring retry requests, and processing CallbackEvents. From section 5.2 onwards, we will introduce specific examples of CallbackEvent processing, text handling in Go, and sending queries to Python Lambda. 5.2 [Go] HTML Text Sanitization Sanitizing External Link References HTML text retrieved from external links may contain unnecessary tags such as script and style , which are not needed for generating responses. Passing this directly to the LLM increases the token count, leading to higher model costs and potentially reducing response accuracy. The following code uses the bluemonday package for basic sanitization. It removes unnecessary tags while preserving important ones like table , ol , and ul , ensuring the text remains well-structured and readable. Additionally, the addNewlinesForTags function inserts line breaks after specific tags, improving text formatting. This helps optimize queries to the model by ensuring that only the necessary information is passed in a structured and efficient format. @ card func sanitizeContent(htmlContent string) string { // Basic sanitization ugcPolicy := bluemonday.UGCPolicy() sanitized := ugcPolicy.Sanitize(htmlContent) // Allow specific tags in a custom policy customPolicy := bluemonday.NewPolicy() customPolicy.AllowLists() customPolicy.AllowTables() customPolicy.AllowAttrs("href").OnElements("a") // Add line breaks after specific tags to improve readability formattedContent := addNewlinesForTags(sanitized, "p") // Apply final sanitization after enforcing the custom policy 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 } This process ensures that the model receives only text with unnecessary tags removed, improving response accuracy and cost efficiency. By preserving essential structures such as tables and bullet points while inserting line breaks after specific tags, the model can better interpret the provided context. 5.3 [Python] Example of an LLM Query Below is an example of how to query an LLM (e.g. Azure OpenAI) in Python. With OpenAIClientFactory , you can dynamically switch models and endpoints, enabling the reuse of a common client creation process across multiple Lambda handlers. Client Creation Process OpenAIClientFactory dynamically generates a client for either Azure OpenAI or OpenAI, depending on api_type and model . Since API keys and endpoints are retrieved from environment variables and secret management services, code modifications are minimized even when updating models or configurations. 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 Query Processing The chatCompletionHandler function extracts messages , model , temperature , and other parameters from the JSON received in the HTTP request. It then queries the LLM using the client generated by OpenAIClientFactory . Responses are returned in JSON format. If an error occurs, a properly formatted error response is generated using a common error handling function. 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", }, } This mechanism allows different Lambda handlers to make LLM queries using the same procedure, ensuring flexibility in adapting to models and endpoint changes. 5.4 [Python] Example of a RAG Search Call This section provides instructions on performing a Retrieval Augmented Generation (RAG) search in Python. By vectorizing internal knowledge, such as Confluence documents, and performing similarity searches using the FAISS index, it is possible to integrate highly relevant information into LLM responses. A key consideration is the handling of the faiss library. faiss is a large package and may exceed the capacity limits of the Lambda Layers. To work around this, it is common to use EFS or containerize the Lambda function. To simplify deployment, the setup_faiss function dynamically downloads and extracts faiss from S3, then adds it to sys.path , making faiss available at runtime. What is FAISS? FAISS (Facebook AI Similarity Search) is an approximate nearest neighbor search library developed by Meta (Facebook). It provides tools for creating indexes to efficiently search for similar images and text. @ card FAISS Setup Using the setup_faiss Function To use FAISS in the Lambda environment, the setup_faiss function performs the following steps: Build and archive the faiss package in a local/CI environment Developers install the faiss-cpu package in a CI environment such as GitHub Actions and package the necessary binaries into a tar.gz archive. Upload to S3 The archived faiss_package.tar.gz is uploaded to an S3 bucket. By storing the package in an appropriate bucket and path (e.g., for staging or production), the Lambda function can dynamically retrieve it during execution. Dynamic loading with setup_faiss when running Lambda In the Lambda execution environment, the setup_faiss function downloads and extracts faiss_package.tar.gz from S3 at startup and adds it to sys.path . This enables the Lambda function to run import faiss , allowing for efficient vector searches using embeddings. Example: Uploading the FAISS Package to S3 Using GitHub Actions The following GitHub Actions workflow demonstrates how to install faiss-cpu , package it for Lambda use, and upload it to S3. This setup uses GitHub Actions Secrets and Environment Variables to manage AWS credentials and S3 bucket names securely, avoiding hardcoded values. @ card name: Build and Upload FAISS on: workflow_dispatch: inputs: environment: description: Deployment Environment 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" # Install required packages (faiss-cpu) - name: Install faiss-cpu run: | set -e echo "Installing faiss-cpu..." pip install faiss-cpu --no-deps # Archive the faiss binary - name: Archive faiss binaries run: | mkdir -p faiss_package pip install --target=faiss_package faiss-cpu tar -czvf faiss_package.tar.gz faiss_package # Set AWS credentials (configure Secrets or Roles based on your environment) - 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 # Upload to 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." In the above example, faiss_package.tar.gz is uploaded to S3 with the key lambda/faiss_package.tar.gz . Dynamic Loading Process on Lambda side ( setup_faiss function) The setup_faiss function handles the dynamic loading of FAISS at runtime. It downloads faiss_package.tar.gz from S3, extracts it to the /tmp directory, and appends the package path to sys.path . This enables import faiss to be executed within Lambda, allowing FAISS index lookups to be performed. # setup_faiss example: Download the FAISS package from S3 and add it to 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 has already been imported.") except ImportError: logger.info("faiss not found. Downloading from S3.") faiss_package_key = "lambda/faiss_package.tar.gz" faiss_package_path = "/tmp/faiss_package.tar.gz" faiss_extract_path = "/tmp/faiss_package" # Download the package from S3 and extract it 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 was imported successfully.") RAG Search Using Embeddings and FAISS Indexes The search_data function loads the FAISS index retrieved from S3 locally and searches for documents that best match the query. Documents are vectorized using the Embeddings client (Azure OpenAI or OpenAI) generated by the get_embeddings function, enabling fast searches using 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("An invalid API type specified.") 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] Asynchronous Downloads and Lambda Handlers Within async_handler , setup_faiss is executed, and the FAISS index file is retrieved from S3 using download_files . Afterward, search_data performs a RAG search, and the results are returned in JSON format. 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"No file found in '{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) Summary of 5.4 Avoid Lambda layer capacity issues with setup_faiss faiss dynamic loading. Asynchronous I/O and S3 usage allow FAISS index to be loaded without containerization or EFS connectivity. search_data searches the embedded index, enabling RAG to quickly provide similar documents. This enables high-speed knowledge searches using RAG, providing LLM answers enriched with company-specific information. 5.5 [Python] Embedding and FAISS Indexing This section provides an example of periodic batch processing that embeds internal company documents (such as Confluence pages) and creates or updates the FAISS index. The index used in the RAG pipeline is essential for generative AI to incorporate company-specific knowledge into its responses. To maintain accuracy, we regularly update embeddings and rebuild the FAISS index, ensuring that the latest information is always accessible. Process Overview Retrieve JSON-formatted documents from S3. Generate embeddings for the retrieved documents (using the Embeddings API from OpenAI or Azure OpenAI). Index the embedded text using FAISS. Upload the FAISS index to S3. By executing these steps periodically via Lambda batch processing or a Step Functions workflow, RAG searches will always use the latest index when queried. Step 1: Loading a JSON document Download and parse a JSON file from S3 (e.g., summarized Confluence pages) and convert it into a list of Document objects. 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]: """ Reads a JSON file and returns a list of Document objects. The JSON format is expected to be: [{"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("The top-level JSON structure is not a list.") documents = [] for record in data: if not isinstance(record, dict): logger.warning(f"Skipped record (not a dictionary): {record}") continue title = record.get("title", "") content = record.get("content", "") metadata = { "id": record.get("id"), "title": title, "url": record.get("url"), } # Create a Document object combining the title and content doc = Document(page_content=f"Title: {title}\nContent: {content}", metadata=metadata) documents.append(doc) logger.info(f"Loaded {len(documents)} documents.") return documents Step 2: Embedding and FAISS indexing The vectorize_and_save function embeds the documents using the Embeddings client obtained from get_embeddings and creates a FAISS index. It then saves the index locally. 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: """ Embed the documents, create a FAISS index, and save it locally. """ # Split the document into smaller chunks using a text splitter text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=128) split_docs = text_splitter.split_documents(documents) logger.info(f"{len(split_docs)} split documents") # Vectorize using embeddings and build FAISS index db: FAISS = FAISS.from_documents(split_docs, embeddings) logger.info("Vector DB construction completed.") os.makedirs(output_dir, exist_ok=True) db.save_local(output_dir) logger.info(f"Vector DB saved to {output_dir}") Step 3: Uploading the Index to S3 By uploading the locally created FAISS index to S3, it can be easily accessed by the RAG search 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: """ Upload the FAISS index to 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 index file uploaded to s3://{s3_bucket}/{s3_index_key}") Step 4: Running the entire flow in Lambda The index_to_s3 function encapsulates the entire process. It downloads JSON from S3, generates embeddings, creates a FAISS index, and uploads the index to S3. This process can be executed periodically using a workflow such as Step Functions, ensuring that the index remains up to date. 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]: """ Download JSON from S3, generate embeddings, create a FAISS index, and upload the index to S3. """ env = os.getenv("ENV") if env is None: error_msg = "ENV environment variable not set." 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" # Set up faiss if necessary (download from S3) setup_faiss(s3_client, s3_bucket) # Download the JSON file from S3 s3_client.download_file(s3_bucket, json_s3_key, local_json_path) documents = load_json(local_json_path) # Get Embeddings client secrets = get_secret() embeddings = get_embeddings(secrets) # Vectorization and FAISS indexing vectorize_and_save(documents, local_index_dir, embeddings) # Upload the index file to S3 upload_faiss_to_s3(s3_client, s3_bucket, local_index_dir, index_s3_path) return { "status": "success", "message": "FAISS index created and uploaded to S3.”, "output": { "bucket": s3_bucket, "index_key": index_s3_path, }, } except Exception as e: logger.error(f"An error occurred during the indexing process: {e}") return {"status": "error", "message": str(e)} Summary of 5.5 load_json loads a JSON file, and vectorize_and_save generates embeddings and creates a FAISS index. upload_faiss_to_s3 uploads the local index to S3. index_to_s3 consolidates the entire process, ensuring that the latest index is created and updated through regular batch processing. This enables automated batch processing to embed internal documents and maintain FAISS indexes for RAG searches. 6. Summary In this article, we covered the development background and technical implementation of internal generative AI tool, out internal chatbot powered by LLM and integrated into Slack. We also outlined the steps for implementing the RAG pipeline, sanitizing Confluence documents, building a search infrastructure using Embeddings and FAISS indexes, and extending functionality with features like translation and summarization. This system enables employees to seamlessly integrate generative AI into their Slack workflow, allowing them to access advanced information capabilities without needing to learn new tools or commands. 7. Future Outlook We will actively work on the following improvements and expansions to further enhance internal generative AI tool. Strengthening Azure-based deployment We will fully integrate with Azure services such as Azure Functions and Azure CosmosDB, significantly improving the performance and scalability of the RAG pipeline. Introducing Azure Cosmos DB Vector Search We will implement vector search functionality on Azure Cosmos DB for NoSQL, enabling more advanced search capabilities. @ card Utilizing AI Document Intelligence By actively incorporating AI Document Intelligence, we aim to expand the knowledge scope of RAG and enhance information utilization across a broader range of use cases. @ card Diversification and sophistication of models We will continue integrating cutting-edge models by expanding support beyond GPT-4o to include GPT-o1, Google Gemini, and other state-of-the-art AI models. Implementing Web UI To overcome the expression and interaction limitations imposed by Slack, we will develop a Web UI, allowing for more diverse interactions and the flexible deployment of new features. Enhancing prompt management We will template existing prompts, making them easily reusable across different use cases. Additionally, we will enhance the prompt-sharing functionality to further promote the adoption of generative AI across the company. Realizing multi-agent capabilities By deploying specialized agents dedicated to tasks such as summarization, translation, and RAG search, and allowing flexible combinations through an Agent Builder, we will enable more advanced and adaptable information processing. Evaluating and improving RAG accuracy We will build test sets and conduct automated answer evaluations to quantitatively measure accuracy and continuously improve quality. Enhancements based on user feedback By incorporating real-world usage data and feedback, we will optimize dialogue flows, fine-tune prompts, and strengthen external service integrations, ensuring that internal generative AI tool remains highly convenient and useful. Through these efforts, we will continue evolving internal generative AI tool, growing it into a powerful internal support tool that meets a wide range of business needs.
アバター
Hello, this is HOKA from the HR Group, Organizational Human Resources Team. Today, I’d like to share what happened following the 10X Innovation Culture Program in March 2024 . Introduction KINTO Technologies (KTC from now on) is challenging itself to create an organizational environment that fosters innovation. We are still in the process of trial and error, but I would be happy if this article were read by those considering the introduction of the 10X Innovation Culture Program or those who want to create an organization that fosters innovation. July: We Tried Implementing The 10X Innovation Culture Program Ourselves I made a resolution even before joining the 10X Innovation Culture Program at the Google offices in March. I decided to implement the 10X Innovation Culture Program within our company. It was in March 2024 when we made the following decision: We will rollout the program every three months and review the assessment results. From the second round onward, we will commit to run the 10X Innovation Culture Program independently. Awacchi and myself began preparing to run the program in the summer. The first step was to select facilitators. Since there would be no Google staff to support us this time, we needed to take on the role of facilitators in-house. When I reached out on Slack, four members stepped forward to volunteer. The 10X Innovation Culture Program broadly consisted of the three main components listed below. We asked ourselves: "What would I do if I were a participant?" and "Which approach would be best for KTC?" [1] A "pre-meeting" to watch a video and understand what the 10X Innovation Culture Program (from now on, the "10X") is [2] Understanding our current status from assessment results [3] Discussing the elements of 10X as the theme  These were the only things we had to do. And yet, it was so difficult!! Key Challenges: [Time Allocation] We were unsure whether the program should take 2 hours or 3. [Content] There were two groups of participants: first-time attendees and those joining for a second time. How should we move forward? [Quality] The section defining what 10X is important, but when KTC members presented, it came across as if they were simply reading from a script.   So, it was a trial-and-error process, starting from the very basics. Together with Awacchi and the four facilitators, despite our uncertainties, we managed to create the next program, discussing how things should unfold based on the March program. Reviewing Each Content [1] A pre-meeting to watch a video and understand what 10X is. As with the previous program, we held a pre-meeting via Zoom. A facilitator introduced the concept of 10X, followed by a recorded playback of the 10X content. Lastly, we conducted an assessment questionnaire. It seemed that this time, the enthusiasm wasn't as high as it had been in March. Perhaps we were lacking motivation, particularly in terms of why KTC was learning this. That was the impression of the organizing team. [2] Understanding our current status from the assessment results We held our in-house 10X Innovation Culture Program at the Muromachi office on July 2, 2024. A total of 39 people participated, including those who came from the Osaka Tech Lab (our Osaka office) and Nagoya. There were both first-time and second-time participants. We looked at the results of the assessment questionnaire that everyone had completed and reported whether each numerical value had gone up or down since the previous session, but the response seemed lukewarm. Maybe the participants did not feel attached to the reported numerical values because they haven't done anything special between the last program (March) and this one (July). Simply receiving the assessment results may not be sufficient for participants to determine whether the outcomes were good or bad. That was the hypothesis of the organizing team. [3] Discussing the elements of 10X as the theme Next, we discussed how to review the past three months. Some participants didn’t remember anything at all. Some first-time participants said, “Since I only watched the video, I don’t know what I’m supposed to do.” When the program was held at the Google offices the previous time, the discussions went smoothly, so this situation was unexpected for the organizing team (though looking back it seems obvious.) Afterward, we discussed Autonomy, one of the key elements of 10X. As in the previous time, participants were divided into groups to write down issues and discuss potential solutions, but this time we had the below discoveries: Some of them haven't been actively practicing 10X over the past three months. For some reason, the atmosphere this time seemed less conducive to active discussions compared to the previous session. Post-Feedback Survey Compared to last time , the number of items with ratings 3 and 2 increased, and there were also some items rated as 1. The motivation to promote this activities also decreased. In the questionnaire comments, we received the following advice from participants: Overall impression: Thank you to the organizing team for all your hard work. It was a fun event. No particular comments for the moment. I would love to see this implemented in other teams as well. It’s a great activity, even just for learning about other people's perspectives. We also saw feedback about the previous session held: I would have appreciated a brief recap of the content from the previous program. There were moments when those who missed the previous program felt left behind. It took me some time to refresh my memory on the issues from the previous program, so I wish there had been time for a review beforehand. I felt that the discussions were more lively last time. This time, no one was proactively steering the conversation (a lack of initiative), and there was no advance preparation, so it took some time to get into the groove. On time and schedule It was short in terms of time. It might have been better if we had more time for group work and discussions. The time schedule was too tight (since things don't always go as planned, there should have been some leeway in the program. It’s too risky to plan by the minute). Maybe the problem was time allocation (I think that was the goal, but overall, it didn’t seem like we were clear on the next action.) On group discussions When forming teams for group discussions, I thought it might have been better to select members from teams that actually work together, rather than mixing people from different teams. It was stimulating and refreshing to discuss with members from different teams, like in the previous program and this one. However, I also feel that it is difficult to achieve results that are directly applicable to our work. So, I thought it might be a good idea to have a conversation about 10X again among members from the same team. On facilitation and management of the workshop There was too little time for ice-breaker activities, including self-introductions. When starting a workshop, it is very important to ease the atmosphere among unfamiliar members. We didn’t even have the time to finish self-introductions. The questions in the workshop were too abstract, making it difficult for participants to come up with concrete answers. Maybe the organizing team overlooked setting goals for the workshop. It would have been better to set a goal at the beginning, such as telling the participants, "please take this back with you." On content understanding and follow-up It was my first time participating, so I felt that the contents seemed more suited for those who had prior knowledge, and overall, it gave an impression of being somewhat vague. It might have been better to either narrow down the content for a more compact format, or extend the duration to provide more thorough explanations. Others I don’t understand the purpose of the paper (for writing responses) that was distributed. It would have been better to assign a leader to each working team from the start. Proactivity and discussions within teams Looking back after the previous session, there were few actions taken, so I would like to allocate time for regular discussions within the team. I am interested in Intrinsic Motivation (another of the 10x key concepts). I believe that combining it with Autonomy can create an even greater sense of speed. After the 10X Innovation Culture Program the facilitator members gathered to conduct a retrospective session. A lot of ideas were gathered. Now, where should we start? Meanwhile, I was given the opportunity by Google to speak at Google Cloud Next Tokyo'24 . <<To be continued in Part 2>>
アバター
This article is part of the day 21 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello! I’m Uehara ( @penpen_77777 ) from the KINTO FACTORY Development Group. I joined the company in July 2024 and have been working on backend development for KINTO FACTORY. This time, KINTO Technologies employees participated in ISUCON14, which was held on December 8th, and would like to share the content and results. What is ISUCON14? ISUCON is a tuning competition organized by LINE Yahoo Japan Corporation. Participants are given a predefined web service and compete to optimize its performance as much as possible while adhering to specific regulations. The winning team receives a grand prize of 1 million yen! This year, the competition took place on Sunday, December 8th, from 10:00 AM to 6:00 PM. I (Uehara) have been participating in ISUCON since last year's ISUCON13, not only to gain knowledge about performance tuning but also to challenge myself as an engineer and improve my skills. :::message ISUCON is a trademark or registered trademark of LINE Yahoo Japan Corporation. https://isucon.net/ ::: Team "ktc-isucon-bu" I recruited members through the company's Slack and formed the team "ktc-isucon-bu." The team members are as follows. Uehara( @penpen_77777 ) Participated in ISUCON13 last year, making this their second time. Furuya Participating in ISUCON for the first time. Nishida Participating in ISUCON for the first time. ISUCON allows participants to choose from several programming languages for the initial implementation. This time, we chose Go. Since many of our team members were first-time participants, rather than aiming for a top ranking, we set a more achievable goal: scoring over 10,000 points. Preparation Before the Competition At ISUCON, it is crucial to automate or streamline repetitive tasks so that we can focus on optimizing during the actual competition. This time, we made the following preparations: Setting up environment provisioning and deployment commands Developing documentation generation tools Preparing measurement tools Conducting individual and team practice Setting up environment provisioning and deployment commands go-task is a task runner specialized for executing tasks in the terminal. https://taskfile.dev/ Traditionally, make is often used as a task runner, but I personally find go-task more convenient, so I chose to use it this time. (Unlike make, it requires installation, which is a minor drawback, but aside from that, I think it can be effectively utilized in real-world projects.) For example, create the following taskfile.yaml and define the setup task and deploy task. version: '3' tasks: setup: cmds: # Write commands for environment setup - echo "setup" deploy: cmds: # Write the deployment command - echo "deploy" After creating the file, you can execute a task by running task name as shown below: # Run the environment setup task $ task setup setup # Run the deploy task $ task deploy deploy Additionally, you can pass command-line arguments when running a task, embedding them into the task execution. version: '3' tasks: setup: cmds: - echo "setup {{ .CLI_ARGS }}" deploy: cmds: - echo "deploy {{ .CLI_ARGS }}" # Execute the setup task for server isu1 $ task setup -- isu1 setup isu1 # Execute the deploy task for server isu2 $ task deploy -- isu2 deploy isu2 The example above is a simple demonstration, but in real use, leveraging command-line arguments allows you to easily switch target servers, making it more efficient for teams to divide and manage tasks. Other benefits of using go-task include: Even if you are working in a subdirectory, you can find the taskfile.yaml in the parent directory and execute the task. You can execute tasks without worrying about the directory location. Tasks can be called from other tasks, increasing the reusability of the tasks you define. You can combine tasks, such as running a setup task before a deploy task. By describing all the tools used in ISUCON in taskfile.yaml, you can execute them just by typing the task command, even if you don't know how to use them (such as the required options). A template taskfile.yaml was prepared in advance so that variables could be modified easily during the competition, allowing for flexible responses to any issues that might arise. Developing documentation generation tools The following two tools were used: https://github.com/k1LoW/tbls Reads the contents of the database and generates documentation (Markdown) that includes ER diagram and schema descriptions. Allows checking the schema definition without connecting to the database, making it useful for understanding the database structure. Automatically generating documentation in the CI/CD pipeline makes it easier to track changes in the database structure. For more details, see the following article: https://devblog.thebase.in/entry/auto_generated_er_graph_by_tbls_and_github_actions https://github.com/mazrean/isucrud Analyzes application code, visualizes relationships with database tables, and generates documentation (Markdown). The drawback was that the diagrams became difficult to understand as the number of functions increased, but now it's much easier to use because you can interactively narrow down the parts you want to see on a web browser. Preparing measurement tools The following tools were used: https://github.com/kaz/pprotein Collects and visualizes access logs, slow query logs, and profile data. Automatically starts collecting data via webhook when a benchmark is run, making it convenient. Also records commit hashes at the time of data collection, making it easy to track which commits contributed to score improvements. ^[Encountered an issue where the Git commit hash could not be obtained properly with pprotein, so the repository was forked, the code was fixed, and a PR was submitted. https://github.com/kaz/pprotein/pull/37] Conducting individual and team practice For first-time participants, jumping straight into past ISUCON problems can be overwhelming and discouraging due to their high difficulty level. To help them get a feel for performance tuning, we first had them read the book Web Performance Tuning: Experts' Guide. https://gihyo.jp/book/2022/978-4-297-12846-3 As we progressed through the book, we gathered as a team on weekends to work through the following practice problems. https://github.com/catatsuy/private-isu (The book also provides guidance on how to optimize using private-isu) Once we were able to consistently score around 100,000 to 200,000 points, we shifted our focus to solving problems from ISUCON13. At this point, we had gained enough confidence to transition into competition-focused practice, allowing us to simulate real ISUCON conditions smoothly. This year’s Task https://www.youtube.com/watch?v=UFlcAUvWvrY The theme for ISUCON 14 was improving the ride chair service "ISURIDE" Chair owners provide chairs for transportation. Users request chairs via the app and travel to their destination. Scores are based on factors like distance traveled and ride completion rate. It is necessary to make improvements to increase user satisfaction. https://github.com/isucon/isucon14 Results The results are as follows. We surpassed our goal of 10,000 points! Final Ranking: 135th place out of 831 teams Final Scores: 12514 points What We Achieved Preparation of deployment scripts (Uehara 10:00-10:30) Manual for the day (Furuya 10:00) Checking running processes and understanding the general configuration (Furuya 10:00) Reading the application manual (Nishida 10:00) Logging into MySQL and checking table sizes Adding indexes (Nishida 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); Preparing pprotein (Uehara 11:48) I had a hard time fixing the nginx settings and it took me over an hour Adding more indexes (Nishida 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); Enabling dynamic parameters and speeding up json processing (Uehara 12:38) // Changed import statements "encoding/json" → "github.com/goccy/go-json" // Enabled InterpolateParams when connecting to the database dbConfig.InterpolateParams = true Adding More Indexes (Uehara 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); Implementing user status cache (Furuya 14:30) In-memory cache for users and ride_statuses tables Modifying transaction scope (Nishida 14:48, 15:09) Adjusting notification polling intervals (Furuya 16:27) Changed the RetryAfterMs returned in appGetNotification and chairGetNotification from 30 ms to 300 ms. Shortened chair-user matching interval (Nishida 17:18, 17:28) Shortened ISUCON_MATCHING_INTERVAL from 0.5 s to 0.1 s Score nearly doubled (most effective change) Stopped bin log (Furuya 17:24) Disabled bin logs in MySQL settings (/etc/mysql/mysql.conf.d/mysqld.cnf) disable-log-bin=1 innodb_flush_log_at_trx_commit=0 Disabled log output (Uehara 17:43) Stopped log output for nginx, MySQL, and application logs Improved ownerGetChairs (Nishida 17:43) Implemented memoization for distance Adjusting the number of connections (Uehara 17:49) db.SetMaxIdleConns(50) db.SetMaxOpenConns(50) Measures That Could Not Be Implemented Due to Time Constraints or Score Issues Fixed matching process (Uehara 13:00-16:00) Attempted improvements but failed to resolve chair dispatch errors and had to abandon it. Caching chair access tokens (Furuya 15:36) Significantly reduced queries from 30,000 to 100. However, the score did not increase. Likely due to a bottleneck elsewhere. nginx parameter tuning (Furuya 17:39) Unable to apply optimizations due to benchmark errors... Server split (Uehara 16:00-17:00) Attempted to split nginx+Go and MySQL, into two machines but encountered data inconsistency errors. Later realized that both servers were running the chair-matching service, which led to data inconsistencies... (If only we had stopped one...) Pros Achieved our goal of scoring over 10,000 points. Ranked 135th out of 831 teams with 12,514 points, a solid result for a team with one returning participant and two first-timers. Everyone contributed to improvement tasks, ensuring no idle time. Environment setup and deployment scripts worked smoothly. No major deployment issues, except minor struggles with nginx setup for pprotein. It took me about an hour to do so, so I would like to add a note to the instructions. Using automation tools significantly improved our workflow, and we plan to expand this approach. Points to Reflect on Lacked a deep understanding of the application before making optimizations. In this problem, understanding the application specifications was important. Mechanically optimizing based on slow query logs or application logs didn’t lead to a significant score increase, and there were few areas where we could make effective improvements. Need to develop a structured approach to understand application behavior before tuning. Understanding specifications is important in daily work as well, so I want to keep this in mind moving forward. Struggled with the matching process bug, which delayed implementation. Tried to optimize using only queries without modifying the table structure, which made implementation difficult. I realized that adding appropriate columns to the tables and simplifying the application code would have made implementation much smoother. I had no prior knowledge of SSE (Server-Sent Events), so I plan to study it. It was presented as a way to update the user’s chair status in real time. Lessons to Apply in Future Work Understanding the application is more critical than just focusing on technical optimizations. Use task runners (go-task) and auto-documentation tools (tbls, etc.) to improve workflow efficiency. Explore using SSE (Server-Sent Events) in product development. Summary We participated in ISUCON14 and achieved a score of 12514 as the team "ktc-isucon-bu", placing 135th out of 831 teams. Although there are things we need to improve on, we were pleased that we were able to achieve our goal of scoring over 10,000 points, even though some of our members were participating in ISUCON for the first time. We will use these lessons to improve further at ISUCON15. Special thanks to Furuya-san and Nishida-san for joining despite it being their first ISUCON. Also, a big thank you to all KINTO Technologies employees who showed interest, even if they couldn’t participate this time. Lastly, a huge shoutout to the ISUCON organizers for hosting this incredible event.
アバター
This article is the entry for day 20 in the KINTO Technologies Advent Calendar 2024 🎅🎄 How We Applied the Impact/Effort Matrix to Improve Cross-Team Interaction Hello! We are Maya and Kinoshita, team members of KINTO Technologies' Developer Relations (DevRel) Group. Introduction We previously held an internal event in Jimbocho to improve office communication and foster stronger ties between teams. Here’s the article from when we held the event ↓ https://blog.kinto-technologies.com/posts/2023-09-19-JimbochoISM/ For this event, we used an Impact/Effort Matrix during the planning phase to decide on the activities. This helped us organize ideas to make the event more engaging and clarify task priorities. As a result, we were able to smoothly run multiple events. In this article, we’ll share our approach to planning, running, and reflecting on events using the Impact/Effort Matrix. We hope this article will be useful for managing your project, for event planning and hope you enjoy the read till the end. About the Impact/Effort Matrix The Impact/Effort Matrix is a simple yet effective tool for efficiently determining the priority of projects and tasks. By categorizing tasks or ideas along two axes (Impact and Effort) this matrix can provide a clear, visual way to decide where to focus your team resources. Basic Structure The Impact/Effort Matrix consists of four quadrants: Quick Wins: Tasks that deliver high impact with minimal effort. They should be prioritized and executed quickly. Major Projects: Tasks that offer high impact but require significant effort. Resource planning is crucial and should be addressed strategically. Fill-ins: Tasks that have low impact and require minimal effort. These should be tackled when there’s extra capacity, but they’re lower priority. Parking Lot: Tasks that offer low impact and require a lot of effort. These should generally be avoided. For more details, you can check the Miro template description page . Why We Decided to Use the Impact/Effort Matrix After we held the first Jimbocho Information Sharing session, we received quite positive feedback from our collagues. However, not all participants felt a sense of unity, so we asked ourselves: how can we create more of a lively and energetic atmosphere? So we wanted to brainstorm this together. While discussing among the organizing team, we clarified our goals and selected the tasks to focus on and those to exclude. During this process, we explored appropriate methods to share task priorities with the team, and decided to use the Impact/Effort Matrix, which some team members had previous experience with. Percieved Benefits Clear prioritization: By placing all tasks relative to each other in the four quadrants, as discussions progress and sticky notes are arranged, the tasks that should be prioritized become visually apparent. Easily shareable, with high team alignment: Building on the previous point, the discussion about what should be prioritized serves as an opportunity to align everyone’s understanding. By progressing with the team’s consensus, commitment is high, and it becomes easier for everyone to take ownership and move forward together. Team resource optimization: By ensuring everyone agrees on the next actions, it helps prevent rework and ensures smoother progress. Challenges and Solutions As is often the case, the most challenging part for the team was decision-making. During the first round of implementing the Impact/Effort Matrix, we encountered many issues with differing opinions and a lack of shared understanding. It took a lot of discussions to organize the information and reach a consensus on how to use it for improving the next event. The granularity of the details was inconsistent, and the scope of what we wanted to accomplish was too broad, so we weren’t able to achieve the desired results. In the second round, we refined the granularity of the issues, clarified our objectives, and narrowed the scope, which allowed us to achieve more concrete outcomes. As a result, the team’s understanding deepened, and the process moved more smoothly. We were able to better define the direction for the event, and the second round was a success. We then created an action plan for the third Jimbocho Sharing Session and visualized the tasks on a Jira board. At this point, several new members joined the organizing team, many of whom were experiencing the Impact/Effort Matrix for the first time. Despite the presence of many newcomers, we were able to smoothly progress based on our prior experience, and successfully held the third Jimbocho Sharing Session. Thoughts on Using the Impact/Effort Matrix Even after understanding the Impact/Effort Matrix methodology, when we tried to apply it to our own situation, there were moments of doubt in the team about whether we were on the right track. However, by discussing the points that felt off or needed improvement and respecting each other's opinions, we were able to organize the various ideas that came up and clearly identify which tasks should be prioritized. During the process of making the Impact/Effort Matrix method work, there were disagreements, but by objectively evaluating each idea, we were able to move forward in a way that everyone could agree on. As preparations for the event progressed, the sense of unity within the team grew, and in the end, we received high praise from participants at the event. Conclusion In this article, we shared our approach to planning, executing, and reflecting on events with the Impact/Effort Matrix, using the Jimbocho internal event as an example. Initially, there were uncertainties and doubts, and we lacked confidence in whether we were doing things correctly. However, through repeated practice, we gradually became more comfortable with the process. Even when new team members joined midway, we were able to run the event smoothly. We hope this methodology will be helpful for the success of your own projects and events as it was for us. Furthermore, KINTO Technologies is looking for talents to join us! If this article has sparked even a bit of your interest, please feel free to reach out to us. We look forward to hearing from you! https://hrmos.co/pages/kinto-technologies/jobs
アバター
This article is the entry for day 22 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Nice to meet you! I am kikumido, and I am developing the Unlimited (Android) app at KINTO Technologies. For our app, we use Apollo Kotlin for the GraphQL client. Apollo Kotlin v4 introduces numerous enhancements, including better performance and new features. To take advantage of these benefits, we decided to upgrade from v3 to v4. In this article, I will explain in detail the process of migrating to Apollo Kotlin v4 (which was released this July), and the issues we encountered along the way. We had initially been hoping for a nice, smooth migration, but ran into some unexpected exceptions, and sometimes struggled to find solutions. I would be delighted if this article proves to be a helpful resource for those considering the upgrade themselves. Migrating From v3 to v4 We will proceed in accordance with the official website . You can also do the upgrade semi-automatically by installing a plugin into Android Studio. We wanted to check what the necessary steps were as we went along, so we did it manually without using the plugin. On the other hand, I was curious about how much the plugin can automate, so in the second half of this article, I will compare the manual approach with the automated process. 1. Things We Had to Do When migrating from v3 to v4, there were five things we had to do in Android Studio to eliminate errors in our app. I will go through them one by one below. :::message alert What you need to do will very likely vary depending on the implementation status of your own apps, so please be aware that this will only be what we did for ours. ::: 1.1. Upgrading the Library Anyway, first, we will update the library . apollographql = "3.8.5" apollo-runtime = { module = "com.apollographql.apollo3:apollo-runtime", version.ref = "apollographql" } apollographql-apollo = { id = "com.apollographql.apollo3", version.ref = "apollographql" } ↓ // At the time of writing this article, v4.1.0 has been released. However, I have based my explanation on 4.0.1, which was the latest version when we did the migration ourselves. apollographql = "4.0.1" apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollographql" } apollographql-apollo = { id = "com.apollographql.apollo", version.ref = "apollographql" } Hmm. Instead of turning into com.apollographql.apollo4 , com.apollographql.apollo3 lost its “3” instead. 1.2. Modifying the Gradle File This did not need to be done for v3, but wrapping in a service is required for v4. apollo { packageName.set("com.example.xxx.xxx.xxx") ・・・ } ↓ apollo { service("service") { packageName.set("com.example.xxx.xxx.xxx") ・・・ } } 1.3. Changing the Import We could see this coming when the “3” disappeared, but you need to change the import package name. Just delete the “3.” import com.apollographql.apollo3.* ↓ import com.apollographql.apollo.* 1.4. Modify the Exception Handling We changed things so that execute() will not throw a fetch error, so we will deal with that. I expect that the approach will vary by project, but for now, we have chosen to take measures to minimize the impact on existing processes. This method consists of modifying things so that DefaultApolloException will be thrown under the same conditions as when a fetch error arose and ApolloException was thrown with v3. By adjusting the common handling, we ensured that the behavior remained the same for the calling side without modifying its implementation. 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 { // Fetch error if response.data is null and response.exception is not null. throw DefaultApolloException(it.message, it.cause) } } return response } If it is difficult to migrate all in one go due to the existing implementation or the v4 support policy, executeV3() has been provided as a helper for migrating without changing the v3 behavior, so you can temporarily replace it with that. apolloClient.query(query).executeV3() apolloClient.mutation(mutation).executeV3() This approach is a good choice if you want to migrate to v4 gradually (for example, feature by feature). :::message alert Note that executeV3() is a deprecated method, so be aware that you’ll need to address it eventually. ::: 1.5. Modifying ApolloException to DefaultApolloException ApolloException has become a sealed class , so replace any places where instances were being generated with DefaultApolloException . The above are the steps required to eliminate errors in Android Studio. 2. Build Now that we’ve cleared all the errors...let’s try running the long-awaited build! Drum roll, aaand...ta-daaa! Yes! We got something! Build errors...! Although we didn’t expect everything to go perfectly smoothly, it was still quite a shock.... Okay, we will pull ourselves back together and check the error log. 2.1. Checking the Error Log A flood of unfamiliar error log entries suddenly appeared. In short, it seems that KSP[^1] is missing a class required for AssistedInjectProcessingStep.[^2] [^1]: For our app, we are using KSP . [^2]: For our app, we are using Hilt as a dependency injection library. If 'error.NonExistentClass' is a generated type, check for compilation errors above that may have prevented its generation. Otherwise, ensure that 'error.NonExistentClass' is included in 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. Of course, it is a class that exists in the source, and the build had been succeeding with no problems before we started doing this upgrade. Had we missed out something that was necessary for the v4 migration? We checked that and various other possibilities, but still couldn't pinpoint the issue... After much investigation, we managed to get the build to succeed by using the following three methods. A. Change Hilt back from KSP to kapt B. Add some code to build.gradle.kts: Pattern 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. Add some code to build.gradle.kts: Pattern 2 apollo { service("service") { packageName.set("com.example.xxx.xxx.xxx") // Start of additional code outputDirConnection { connectToAndroidSourceSet("main") } // End of additional code ・・・ } } By the way, we are using Hilt ’s AssistedInject (which is apparently sometimes not a problem depending on how it is defined); we are using KSP in Hilt ; and we wrap it in a service in “1.2. Modifying the Gradle file” above. If all the above conditions are met, the issue also occurs in v3, meaning it isn't strictly a 'v4-specific' support update. However, since we addressed it at the same time, I have included it here. For our app, we initially went with “B.” However, due to a bug in ApolloExtension’s service ,[^3] we subsequently found that we could do it using “C,” so we adopted that. [^3]: Issues → This was also reproduced with v4.1.0, the latest version at the time of writing. With the above changes, the build succeeded, allowing us to migrate to v4 while ensuring the app launched and functioned just as before! 3. Dealing with Deprecated Parts Next, we will address the following two things that caused deprecation warnings. 3.1. Modifying ApolloResponse.Builder @Deprecated("Use 2 params constructor instead", ReplaceWith("Builder(operation = operation, requestUuid = requestUuid).data(data = data)")) We received the above warning and made the necessary modifications accordingly ApolloResponse.Builder(operation(), UUID_CONST, data).build() ↓ ApolloResponse.Builder<D>(operation(), UUID_CONST).data(data).build() data has been moved outside, correct It seems to have been modified to follow the Builder pattern. 3.2. Modifying the Error Instance Creation Processing to Builder Update any instances where a constructor was used to create an Error instance to instead use the Builder pattern. Error("occurred Error", null, null, mapOf("errorCode" to responseCode), null) ↓ Error.Builder(message = "occurred Error") .putExtension("errorCode", responseCode) .build() Doing the above also successfully eliminated a warning. 4. Migrating Using the Plugin Using the Apollo plugin in Android Studio will automate the migration to some extent. It is very simple to do: just install the Apollo plugin in Android Studio, then tap Tools > Apollo > Migrate to Apollo Kotlin 4... . Easy, right? ![Migrate to Apollo Kotlin 4](/assets/blog/authors/kikumido/plugin.png =500x) Okay, let’s check the results of running it. The aforementioned 1.1. Upgrading the Library 1.2. Modifying the Gradle File 1.3. Changing the Import got done automatically. 1.4. Modifying the Exception Handling only consists of switching to executeV3() , so the appropriate action for v4 needs to be taken manually. 1.5. Modifying ApolloException to DefaultApolloException Was not completed, so it must be done manually. Essentially, it automates any migration tasks that can be handled mechanically. Therefore, another viable approach is to use the plugin for migration and then manually address only the necessary parts. By the way, this plugin not only assists with migration by graying out unused fields but is also useful for other tasks. I highly recommend installing it. 5. Summary That concludes the discussion of migrating Apollo Kotlin from v3 to v4. It's been a while since we actually completed this, so I imagine many people have already done it themselves. Still, I hope this article provides something useful. Thank you for reading all the way to the end. 6. Related links 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 テクノロジーズ セキュリティ・プライバシー グループのたなちゅーです!普段は、SIEM を活用したログ監視・分析や監視体制構築、SCoE グループ( SCoE グループとは? )の一部プロジェクトに参画してクラウド周りのセキュリティ業務に従事しています。自己紹介は こちら 。 本ブログでは、2025年3月26日に名古屋駅近くにある コラボスタイル さんのイベントスペースで開催された『 Sysdig Kraken Hunter ワークショップ 』の参加レポートをお届けします。 イベントスペースの様子 KINTOテクノロジーズの Sysdig Secure 活用 KINTO テクノロジーズでは、Sysdig Secure を主に、Cloud Security Posture Management(CSPM)と Cloud Detection Response(CDR)に利用しています。詳細については、こちらのブログにまとまっていますので、是非、ご覧ください。 KTC クラウドセキュリティエンジニアのとある一日 『Sysdig Kraken Hunter ワークショップ』とは? まずSysdigとは、ネットワークキャプチャツールで有名な Wireshark の共同開発者である Loris Degioanni 氏が創業した企業で、同社が開発したクラウドネイティブ脅威検知のオープンソース標準である Falco を軸に、クラウドやコンテナ向けのセキュリティソリューションを提供しています。弊社では、クラウド環境の権限設定やアカウント/リソース作成などのアクティビティを監視するために Sysdig Secure 使用しています。 『Sysdig Kraken Hunter ワークショップ』では、Amazon EKSのデモ環境に対して擬似的な攻撃を行い、Sysdigで検知や調査、対応などの一連の操作をモジュールに分けて体験するワークショップです。また、ワークショップ後の試験に合格すると、Kraken Hunter の認定バッジが付与されます。 本ブログでは、特に興味深かった3つのモジュールについて紹介します。 モジュール1:擬似攻撃とイベント調査 ここでは、実際に Amazon Elastic Kubernetes Service(Amazon EKS)のデモ環境へ擬似的な攻撃を行い、Sysdig Secureで検知や調査を行いました。 まず、提供されたドキュメントの手順に沿って、Amazon EKS のデモ環境へリモードコード実行(RCE)をシミュレートした以下のような擬似的な攻撃を行いました。 システム上の任意のファイルの内容を取得/書き込み/実行 システム上へファイルをダウンロード 擬似的な攻撃を行った後、ブラウザで Sysdig Secure のコンソールへアクセスし、攻撃を行った対象リソースの状態を見ると、攻撃に関連するイベントを検知していることを確認できます。 参照:sysdig-aws workshop-instructions-JP さらに調査を進めると、先ほどの攻撃を Sysdig Secure がリアルタイムで検知していることが確認できます。 参照:sysdig-aws workshop-instructions-JP このような流れで、擬似的な攻撃をして、Sysdig Secure のコンソールでどのように調査できるのか、攻撃を検知するのかを体験していきました。 自分で攻撃しながらどのように Sysdig Secure で調査できるのかを体験できるため、「Sysdig Secure でなにができるのか」を理解しながら進めることができたように感じました。 モジュール2:ホストとコンテナの脆弱性管理 このモジュールでは、ホストとコンテナの脆弱性管理に関する Sysdig Secure の機能を体験しました。弊社で開発しているプロダクトも、コンテナを使用してマイクロサービス化しているため、関心の高い内容です。 Sysdig Secure は脆弱性管理機能として、「ランタイム脆弱性スキャン」や「パイプライン脆弱性スキャン」、「レジストリ脆弱性スキャン」があるそうです。 「ランタイム脆弱性スキャン」では、過去15分以内に監視対象環境で実行されたすべてのコンテナとSysdig Secure の Agent がインストールされているすべてのホスト/ノードがリストされます。使用中の脆弱性の数と重要度に基づいて、自動的に重要度順にソートされて表示されるため、最も対応が必要なリソースを簡単に確認できます。 参照:sysdig-aws workshop-instructions-JP また、リストされたリソースをクリックしてドリルダウンすることで、脆弱性の詳細を確認できます。 参照:sysdig-aws workshop-instructions-JP 「パイプライン脆弱性スキャン」では、コンテナイメージがレジストリや実行環境に置かれる前に脆弱性をスキャンします。また、「レジストリ脆弱性スキャン」では、レジストリ内のイメージの脆弱性をスキャンします。このように、コンテナイメージの開発/運用フェーズごとに脆弱性の有無をチェックすることができます。 脆弱性を管理するセキュリティ製品はさまざま存在しますが、Sysdig Secure のコンソールは UI が洗練されており、直感的に操作できそうだなと思いました。 モジュール3:コンテナのポスチャー/コンプライアンスの管理 ここでは、クラウド環境のポスチャー/コンプライアンスを管理する Sysdig Secure の機能について体験しました。みなさんもよく見聞きしている通り、クラウド環境の設定ミスに起因するセキュリティインシデントは多数報告されているため、フルクラウド環境でプロダクトを開発している弊社にとっても人ごとではないと考えており、注目している機能の一つです。 ポスチャー/コンプライアンス管理機能として、Sysdig Secure では、自社環境が CIS、NIST、SOC 2、PCI DSS、ISO 27001 などの一般的な規格に準拠しているか確認できます。 参照:sysdig-aws workshop-instructions-JP また、規格に準拠していないリソースをリストし、どのように修正すべきかの手順を確認することもできます。すべてのシチュエーションで実用性の高い手順を確認できるかは未知数ですが、修正手順を調査する工数を削減できることは、管理者としてありがたい配慮だなと感じました。 参照:sysdig-aws workshop-instructions-JP Kraken Hunter 認定試験 Kraken Hunter 認定試験では、試験専用のWebページで30〜40問ほどの問題が出題されました。ワークショップ内で紹介された事柄が出題されるため、真面目に取り組んでいれば合格を狙えると思います。 ワークショップの冒頭に紹介された細かな内容があり苦戦しましたが、なんとか認定試験に合格することができました! 以下は合格者に付与される認定バッジです。 Kraken Hunter 認定バッジ 今後の Sysdig Secure の利用 弊社では Sysdig Secure を使い倒すべく、以下の利活用を検討・推進しています。 CSPM については、弊社ガバナンスルールに沿った独自ポリシールールを Rego で作成し、弊社ガバナンスに沿ったクラウドセキュリティの担保 CDR については、 Falco による独自ルールを作成し、弊社環境に則した脅威検知対象の拡大 コンテナワークロードのセキュリティ担保のために、CWP(Cloud workload protection)の検証と導入 まとめ 今回の Sysdig Kraken Hunter ワークショップでは、Amazon EKSのデモ環境に対して擬似的な攻撃を行い、Sysdig Secure で検知や調査、対応などの一連の操作を体験しました。 弊社では Sysdig Secure の一部の機能のみ使用しているため、今回のワークショップで紹介された機能は、初めてのものばかりで操作に不慣れな部分もありましたが、Sysdig Secure で何ができるのかを理解する良い機会となりました。 また、オフラインのワークショップに参加することで、他の企業が抱える課題や取り組みなど、生の声を聞くことができました。このような機会を作ってくださった運営のみなさまに感謝いたします。 さいごに 私の所属するセキュリティ・プライバシー グループやこのワークショップに一緒に参加した SCoE グループでは、一緒に働いてくれる仲間を募集しています。クラウドセキュリティの実務経験がある方も、経験はないけれど興味がある方も大歓迎です。お気軽にお問い合わせください。 詳しくは、 こちらをご確認ください 。
アバター
この記事は、 KINTOテクノロジーズ・アドベントカレンダー2024 の12日目の記事です🎅🎄 こんにちは。「KINTOかんたん申し込みアプリ」のAndroid開発チームメンバーです。今日は、私たちの既存のアプリに Kotlin Multiplatform (KMP) を実装するプロセス、その理由、そしてそれによってもたらされた変化と改善についてお話ししたいと思います。 私たちは去年からiOSとAndroidプラットフォーム間での開発効率を最大化する方法を探ってきました。 このプロセスの中で、KMPがチームの目に留まりました。この技術がどのように革新的に開発プロセスを改善したかを皆さんにお伝えしたいと思います。 目次 1. 既存のアプリにKMPを実装する理由 2. 既存のアプリへのKMPの統合 2.1 共有コードの配置決定 2.2 共有コードの整理 2.3 KMPモジュールの作成 2.4 マルチモジュール・アーキテクチャとアンブレラモジュール 2.5 CI:AndroidおよびiOSでの共有コードのテスト 3. KMPコードの配布 3.1 KMPコードの配布オプション 3.2 Swift Package Manager (SPM) 3.3 配布の自動化 4. AndroidおよびiOSの実装方法 4.1 機能の選択 5. KMPクロスプラットフォームモジュール実装における課題 6. 効果 7. 今後に向けて:今後の計画と課題 1. 既存のアプリにKMPを実装する理由 当時、私たちのチームはiOS開発リソースの不足に直面していました。 この課題に対処するために、AndroidチームはKotlin Multiplatform (KMP) を活用して、iOSとAndroidの両方のプラットフォームで共有されるビジネスロジックを作成することにしました。 このアプローチにより、オペレーティングシステム間でのコードの重複を削減し、Androidチームは専門知識を活用してiOS開発に対応できるようになりました。 この戦略は、人員の問題を軽減し、開発生産性を大幅に向上させる重要なソリューションとなり、KMPテクノロジーを既存のアプリに統合する決定的な理由となりました。 [背景の概要] iOS開発リソース不足への対処 Kotlinに関するAndroidチームの専門知識の活用 オペレーティングシステム間のコード重複の削減 開発生産性の向上とチーム連携の強化 ※ ビジネスロジックをKMPライブラリにモジュール化することで、オペレーティングシステム間での重複作業を排除しましょう。 2. 既存のアプリへのKMPの統合 私たちは、KMPコードを実装する前に、私たちの共有コードをどこに配置し、どのように整理するかについていくつかの戦略的決定を下しました。 2.1 共有コードの配置決定 現在運営していますモバイルアプリには、Androidリポジトリで作業するAndroidチームと、iOSリポジトリで作業するiOSチームという2つの別々の開発チームによる典型的な設定があります。 KMPを導入するときに最初に生じる疑問は、「共有コードをどこに配置すべきか」ということです。 オプション1:別のリポジトリ内の共有コード このオプションでは、共有コード用の新規リポジトリを作成し、AndroidリポジトリとiOSリポジトリの両方からアクセスできるようにします。このリポジトリ構造は次のようになります。 graph TB; subgraph Android Repository AndroidApp end subgraph iOS Repository iOSApp end subgraph KMP Repository KMP end KMP --> AndroidApp KMP --> iOSApp オプション2:Androidリポジトリ内の共有コード このオプションでは、共有コードをAndroidリポジトリに配置し、Androidチームが共有コードベースを管理できるようにします。このリポジトリ構造は次のようになります。 graph TB; subgraph Android Repository KMP --> AndroidApp end subgraph iOS Repository KMP --> iOSApp end オプション3:AndroidリポジトリとiOSリポジトリをモノレポに統合する このオプションでは、AndroidリポジトリとiOSリポジトリをモノレポに統合し、両チームが共有コードベースにアクセスできるようにします。このリポジトリ構造は次のようになります。 graph TB; subgraph One Repository KMP --> AndroidApp KMP --> iOSApp end [私たちの決定] 各オプションの長所と短所を検討した結果、共有コードをAndroidリポジトリに配置することに決定しました。この決定は以下の要因に基づいています: 既存のワークフローへの影響を最小限に抑える 共有コードベースの管理が容易になる 2.2 共有コードの整理 共有コードをどこに配置するかを決めたら、次はそれをどのように整理するかを決めました。 私たちの既存のAndroidアプリはマルチモジュール・アーキテクチャに従っているため、共有モジュールとプラットフォーム固有のモジュールを明確に区別したいと考えました。KMPモジュールを既存のAndroidモジュールと一緒に、Androidリポジトリ内の shared ディレクトリに配置することにしました。例: :app // Androidアプリのモジュール :domain // Android 固有のモジュール :shared:api // KMPモジュール :shared:validation // KMPモジュール 2.3 KMPモジュールの作成 KMP用のGradleモジュールは次を含みます。 1. build.gradle.kts ファイル 2. src サブフォルダー Androidモジュールの場合、 com.android.library プラグインを適用し、 android {} ブロックを含めます。 plugins { id("com.android.library") } android { // Android固有の設定 } KMPモジュールの場合、マルチプラットフォームプラグインを使用し、 kotlin {} ブロックを定義します。 plugins { kotlin("multiplatform") } kotlin { // KMPの設定 } この設定により、共有コードベースにおいてAndroid固有の要件とKMP固有の要件の両方に対応できるようになりました。 2.4 マルチモジュール・アーキテクチャとアンブレラモジュール 複数モジュールの制限 Androidでは、複雑なプロジェクトのためにコードを複数のモジュールに分割するのが標準です。ただし、KMPは現在、iOSに対して1つのモジュールのみの公開に対応しています。 例えば、共有コードベースに、 featureA , featureB , featureC という3つのモジュールがあるとします。各モジュールは data モジュールに依存し、 data モジュールは api モジュールに依存しています。 graph LR; api --> data --> featureA data --> featureB data --> featureC これら3つのモジュールをiOSに公開したい場合、理想的なシナリオでは、iOS開発者は次のように必要なモジュールのみをインポートします。 import featureA import featureB <swift code here> ただし、KMPの制限により、このアプローチではiOSアプリ内でコードが重複することになります。 以下のような構造が望ましいのですが、 graph LR; subgraph KMP api --> data --> featureA data --> featureB data --> featureC end featureA --> iOSApp featureB --> iOSApp featureC --> iOSApp 実際には、以下のような構造(重複あり)になってしまいます。 graph LR; subgraph KMP api --> data --> featureA end subgraph KMP1 api1(api copy) --> data1(data copy) --> featureB end subgraph KMP2 api2(api copy2) --> data2(data copy2) --> featureC end featureA --> iOSApp featureB --> iOSApp featureC --> iOSApp style api1 fill:#f88 style api2 fill:#f88 style data1 fill:#f88 style data2 fill:#f88 アンブレラモジュール この制限を克服するために、 アンブレラモジュール を導入しました。 アンブレラモジュールは、ソースコードを含まない「空の」モジュールであり、依存関係を管理するために使用されます。 graph LR; subgraph KMP subgraph Umbrella api --> data --> featureA data --> featureB data --> featureC end end Umbrella --> iOSApp style Umbrella fill:#8f88 こちらがbuild.gradle.ktsの例です: kotlin { val xcf = XCFramework() listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { baseName = "Umbrella" binaryOption("bundleId", "com.example.shared") export(project(":shared:featureA")) export(project(":shared:featureB")) export(project(":shared:featureC")) xcf.add(this) } } sourceSets { val commonMain by getting { dependencies { api(project(":shared:featureA")) api(project(":shared:featureB")) api(project(":shared:featureC")) } } } } アンブレラモジュールは、iOS開発者の統合プロセスを簡素化し、プラットフォーム間でシームレスかつ効率的な開発体験を確保します。 2.5 CI:AndroidおよびiOSでの共有コードのテスト 私たちは常にコードのテストを記述しており、共有コードも例外ではありません。プラットフォームの違いにより、一部の機能はiOSでは期待どおりに動作しない場合があります。互換性を確保するために、AndroidとiOSの両方でテストを実行します。AndroidはどのOSでもテストを実行できますが、iOSのテストはmacOSで実行する必要があります。 3.KMPコードの配布 KMPコードの記述が完了したら、次のステップはそれをiOSアプリに配布することです。 3.1 KMPコードの配布オプション KMPコードは、ソースコードまたはバイナリで配布できます。 ソースコードの配布 ソースコード配布では、iOS開発者はKMPコードを自らコンパイルする必要があります。このアプローチでは、Java VMやGradleなどのツールを含むKotlinビルド環境をセットアップする必要があります。 課題: すべてのiOS開発者は、KMPビルド環境を設定する必要があります。 これにより、KMPコードを iOSプロジェクトに導入する際の複雑さが増します。 バイナリ配布 より良いオプションはバイナリ配布です。私たちは、プリコンパイル済みのライブラリを提供することで、iOS開発者が追加のビルド環境を管理する必要性をなくし、共有コードの統合がはるかに簡単になるようにしています。 利点: iOS開発者のセットアップ作業を削減します。 環境間で一貫したビルドを確保します。 3.2 Swift Package Manager (SPM) iOSには主に2つの依存管理システムがあります。それは、CocoaPods と Swift Package Manager (SwiftPM)です。どちらを選択するかは、iOSチームの好みによります。幸いなことに、私たちのiOSチームは SwiftPMに完全移行しているため、SwiftPMのみに対応すれば済みます。 Swift Packageとは? Swift Packageは基本的に、次を含むGitリポジトリです。 Swiftソースコード Package.swiftマニフェストファイル Gitタグによるセマンティック・バージョニング SwiftPMによるバイナリ配布 SwiftPM 5.3以降、SwiftPMは binaryTarget に対応しており、ソースコードの代わりに予めコンパイルされたライブラリを配布できるようになっています。 バイナリ配布によるSwift Packageの作成 KMPコードをSwift Packageとして公開する方法を簡単に説明します。 KMPコードを .xcframework にコンパイルします。 .xcframework をzipファイルにパッケージ化し、そのチェックサムを計算します。 GitHubに新しいリリースページを作成し、リリースアセットの一部としてzipファイルをアップロードします。 リリースページからzipファイルのURLを取得します。 URLとチェックサムに基づいて Package.swift ファイルを生成します。 Package.swift ファイルをコミットし、gitタグを追加してリリースをマークします。 gitタグをリリースページに関連付け、GitHubリリースを正式に公開します。 詳細な手順については、[リモートSPMエクスポートに関するKMPドキュメント] を参照してください。( https://kotlinlang.org/docs/native-spm.html ) // swift-tools-version:5.10 import PackageDescription let packageName = "Umbrella" let package = Package( name: packageName, platforms: [ .iOS(.v13) ], products: [ .library( name: packageName, targets: [packageName]), ], targets: [ .binaryTarget( name: packageName, url: "https://url/to/some/remote/xcframework.zip", checksum:"The checksum of the ZIP archive that contains the XCFramework." ] ) 3.3 配布の自動化 手動での配布は時間がかかる場合があります。プロセスを円滑化するために、自動化用のGitHub Actionsワークフローを作成しました。 name:Publish KMP for iOS on: workflow_dispatch: inputs: release_version: description:'Semantic Version' required: true default:'1.0.0' env: DEVELOPER_DIR: /Applications/Xcode_15.3.app jobs: build: runs-on: macos-14 steps: - name:Checkout uses: actions/checkout@master - name: set up JDK 17 uses: actions/setup-java@v4 with: java-version:'17' distribution: 'zulu' - name:"Build and Publish" env: RELEASE_VERSION: ${{ github.event.inputs.release_version }} GH_TOKEN: ${{ github.token }} run: ./scripts/publish_iOS_Framework.sh $RELEASE_VERSION #!/bin/sh set -e MODULE_NAME="<your module name>" VERSION=$1 # Github リリース用のバージョン名 RELEASE_VERSION="$MODULE_NAME-$VERSION" # Gitタグ名 TAG="$VERSION" TMP_BRANCH="kmp_release_$VERSION" # VERSIONがsemver仕様であるか確認 if [[ ! $VERSION =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then echo "VERSION should be in semver format like 1.0.0" exit 1 fi ZIPFILE=./shared/$MODULE_NAME/build/XCFrameworks/release/$MODULE_NAME.xcframework.zip echo "Building $MODULE_NAME $VERSION" ./gradlew assembleKintoOneCoreReleaseXCFramework echo "creating zip file" pushd ./shared/$MODULE_NAME/build/XCFrameworks/release/ zip -r $MODULE_NAME.xcframework.zip $MODULE_NAME.xcframework popd # タグを取得 git fetch --tags # 直前のリリースタグを取得 PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -v ^version | head -n 1) echo "previous release tag: $PREVIOUS_RELEASE_TAG" # Github ドラフトリリースを作成 echo "creating github release $RELEASE_VERSION" gh release create $RELEASE_VERSION -d --generate-notes --notes-start-tag $PREVIOUS_RELEASE_TAG gh release upload $RELEASE_VERSION $ZIPFILE echo "retrieving asset api url" # Github リリースからアップロードされたzip ファイルのAsset APIのURLを取得 # 例: "https://api.github.com/repos/{username}/{repo}/releases/assets/132406451" ASSET_API_URL=$(gh release view $RELEASE_VERSION --json assets | jq -r '.assets[0].apiUrl') # URLの末尾に拡張子(.zip)を追加 ASSET_API_URL="${ASSET_API_URL}.zip" # Package.swiftを生成 ./scripts/generate_SPM_Manifest_File.sh $ZIPFILE $ASSET_API_URL # Package.swiftをコミットし、タグを追加 git checkout -b $TMP_BRANCH git add . git commit -m "release $VERSION" git tag -a $TAG -m "$MODULE_NAME $VERSION" git push origin $TAG # Githubリリースを更新し、新しいタグに置き換える gh release edit $RELEASE_VERSION --tag $TAG 4.AndroidおよびiOSの実装方法 このプロジェクトで私たちは、Kotlin Multiplatform (KMP) を使用して、既存のアプリに新規共通モジュールを導入しました。プラットフォーム固有の潜在的な問題を最小限に抑えるために、AndroidとiOSで確実に動作する機能を慎重に選択して実装しました。OSに依存しない機能を選択して本番環境での初期テストのために実装をシンプルに保つことによって、クロスプラットフォームモジュールの確立に重点を置きました。以下は、機能選択基準と実装プロセスの概要です。 4.1 機能の選択 KMPを本番環境に導入する際の潜在的な問題を特定するために、プラットフォーム固有の実装に依存せず、かつ最小限の依存関係で扱える機能を優先しました。機能選択の基準に次を含めました: OSに依存しない機能性 :本番環境で予期しない問題が発生するのを避けるために、OSに依存しない機能を選択し、通信、ストレージ、権限など特定のOSレベル制御を必要とする要素を除外しました。 追加ライブラリの最小化 :メンテナンスのリスクを軽減するために、追加ライブラリに依存せずにKotlin標準ライブラリのみで実装できる機能を選択します。 ライブラリの優先順位 :ライブラリを選択する際に、まず公式Kotlinライブラリ、次に公式Kotlinドキュメントで推奨されているライブラリ、最後にサードパーティのライブラリ、と優先順位をつけました。 これらの基準に基づいて、KMPで実装する最初のクロスプラットフォーム機能として 入力検証 を選択しました。そして、 全角/半角文字変換 機能を追加しました。 Android 入力検証の実施 デフォルトでは、Android実装にはライブラリ機能の不足やインターフェースの違いといった問題しかありませんが、それは大した問題ではありませんでした。 入力検証機能を、一般的なオブジェクト指向プログラミング (OOP) の原則に従って構造化し、再利用性と一貫性を重視しました。 1.共通インターフェースの定義 :両方のプラットフォーム間で入力を検証するための一貫した基盤を作るために、 Validator インターフェースと ValidationResult インターフェースを定義しました。 abstract class ValidationResult( /** * Informations about input and fail reason. */ val arguments:Map<String, Any?>, requiredKeys:Set<String> ) fun interface Validator<T, R :ValidationResult> { /** * @return validation result or `null` if the target is valid. */ operator fun invoke(target:T):R? } 2.入力タイプ別のバリデータの実装 :電子メールやパスワードの検証など、入力タイプごとにバリデータと結果クラスを分けて作成しました。 class IntRangeValidator( /** * min bound(inclusive). */ val min:Int, /** * Max bound(inclusive). */ val max:Int ) :Validator<String, IntRangeValidationResult> { companion object { const val PATTERN = "0|(-?[1-9][0-9]*)" val REGEX = PATTERN.toRegex() const val ARG_NUMBER = "number" const val ARG_RANGE = "range" const val ARG_PATTERN = "pattern" } val range = min..max override fun invoke(target:String):IntRangeValidationResult? { when { target.isEmpty() -> return RequiredIntRangeValidationResult() !target.matches(REGEX) -> return IllegalPatternIntRangeValidationResult(target, PATTERN) } return try { target.toInt(10).let { number -> if (number !in range) { OutOfRangeIntRangeValidationResult(target, range) } else { null } } } catch (e:NumberFormatException) { OutOfRangeIntRangeValidationResult(target, range) } } } 3.テストコードの作成 :プラットフォーム間でのモジュールの精度を検証するために、 kotlin-test パッケージを使用して広範なテストケースを実施し、AndroidとiOSの両方で安定した機能を確保しました。 import kotlin.random.Random import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull class IntRangeValidatorTest { private var min = 0 private var max = 0 private lateinit var validator:IntRangeValidator @BeforeTest fun setUp() { min = Random.nextInt() max = Random.nextInt(min + 1, Int.MAX_VALUE) validator = IntRangeValidator(min, max) } @AfterTest fun tearDown() { min = 0 max = 0 } @Test fun `invoke - decimal number string`() { val validator = IntRangeValidator(Int.MIN_VALUE, Int.MAX_VALUE) for (number in listOf( "0", "1", "111", "${Int.MAX_VALUE}", "-1", "${Int.MIN_VALUE}", "${Random.nextInt(Int.MAX_VALUE)}", "-${Random.nextInt(Int.MAX_VALUE - 1)}" )) { // 以下の場合 val result = validator(number) /// したがって、以下の通りになる assertNull(result) } } } 全角/半角文字変換の実装 入力検証に加えて、文字変換機能を実装し、アプリケーション要件に基づいて全角文字と半角文字を自動的に変換しました。 1.拡張可能なインターフェースの定義 :多様で複雑な文字変換に対応するため、インターフェースを定義し、あらゆる変換処理を継承できるようにしました。Kotlinには、これを実装するのに役立つ関数型インターフェース ( fun interface ) と演算子関数 ( operator fun ) の機能が備わっています。 fun interface TextConverter { operator fun invoke(input:String):String operator fun plus(other:TextConverter) = TextConverter { input -> other(this(input)) } } 2.変換用のマッピング定数の定義 :全角/半角文字とそれらの変換をリストアップした文字マッピングテーブルを作成し、定義済みマッピングを参照して変換できるようにしました。 open class SimpleTextConverter( val map:Map<String, String> ) :TextConverter { override operator fun invoke(input:String):String { var result = input for ((key, value) in map) { result = result.replace(key, value) } return result } } class RemoveLineSeparator( map:Map<String, String> = mapOf( "\n" to "", "\r" to "" ) ) :SimpleTextConverter(map) object HalfwidthDigitToFullwidthDigitConverter :SimpleTextConverter( mapOf( "0" to "0", "1" to "1", "2" to "2", "3" to "3", "4" to "4", "5" to "5", "6" to "6", "7" to "7", "8" to "8", "9" to "9" ) ) val NUMBER_CONVERTER = FullwidthDigitToHalfwidthDigitConverter + RemoveLineSeparator() 3.自動変換機能 :変換機能は、全角文字を半角文字に、またはその逆に自動変換するように設計されており、一貫性のある予測可能な入力体験を実現します。 これらのOS非依存性機能を選択し、それらをKMP で実装することで、AndroidとiOS間で確実に導入できる安定した再利用可能なモジュールを確立できました。 iOSへの統合 私たちのKMPコードはSwift Packageとして配布され、iOSチームはXcodeGenを使用してXcodeプロジェクトファイルを管理しました。iOSアプリへのKMPコードの統合は、 project.yml ファイルに4行のコードを追加するだけで簡単に実行できます。 packages: + Umbrella: + url: https://github.com/your-org/your-android-repository + minorVersion:1.0.0 targets: App: dependencies: + - package:Umbrella - package: ... ただし、私たちのコードはプライベートリポジトリに格納されているため、いくつかの追加設定が必要です。詳細については、以下をご覧ください: SwiftPMでのプライベートリポジトリの資格情報設定 5.KMPクロスプラットフォームモジュール実装における課題 KMP共通モジュールの開発中に、技術的な問題がいくつか発生しました。特に目立ったのは、基本的な機能、マルチバイト文字、符号化の処理に関する問題でした。以下は、これらの問題の概要と、それらをどのように解決したかについてです。 Kotlin標準ライブラリはUnicodeコードポイントに対応していません 入力検証において漢字やサロゲートペアなどのマルチバイト文字を正確に処理するために、Unicodeコードポイントベースの正規表現を実装することにしました。このアプローチにより、文字を単なる個々の文字として扱うのではなく、Unicode範囲内での文字の位置に基づいて文字を正確に一致させて検証できるようになりました。しかし、問題が発生しました。 Kotlinの String クラスは、Unicodeコードポイントの処理をネイティブに対応しておらず、この目的(特にサロゲートペア)のための公式ライブラリも提供していません。 そのため、コードポイントに基づいてマルチバイト文字を正確に処理するために、サードパーティのライブラリを使用しています。これにより、漢字などの複雑な文字を私たちの正規表現内でより正確に一致させることができるようになりました。 非UTFの文字における符号化には対応していません レガシーシステムとの互換性を維持するには、Shift-JIS (MS932) の符号化に対応する必要がありました。しかし、KMPはShift-JIS符号化において、ネイティブに対応していません。 レガシーシステムへのテキスト転送では、MS932で符号化が可能であるか否かを確認する必要があり、そのため、 ktor-client ライブラリを使用して符号化を処理することにしました。ただし、 ktor-client のiOSバージョンはUTFベースの符号化スキームしか対応していないため、MS932符号化を実施するのは困難です。 MS932符号化の制限により、漢字検証のためのコードポイントの使用を断念しました。代わりに、検証に必要な漢字のリスト全体を含む定数を宣言し、必要に応じて参照できるように、これらをUnicodeコードポイントに変換しました。 Unicodeコードポイントの問題 全角文字と半角文字の変換を実施する際に、特定の文字間のコードポイントの不一致が発生し、単純な加算/減算アプローチが効果的でなくなりました。 例えば、日本語の全角文字 ァ' ( U+30A1 ) と ア ( U+30A2 )は、コードポイントが1つ異なるだけです。対照的に、半角文字 ァ ( U+FF67 )と ア ( U+FF71 )は、コードポイントが10異なります。この不一致は、統一された変換アプローチが実現不可能であることを意味していました。 私たちは、すべての変換に対して定数マッピングテーブルを作成し、すべての全角文字と半角文字、およびそれぞれのマッピングを明確に定義することで、この問題を解決しました。このアプローチにより、変換操作においてさまざまな文字を正確に処理できるようになりました。 これらの問題に対処することで、私たちのKMP共通モジュールの安定性と完全性が向上し、AndroidとiOSの両方のプラットフォームで正確な機能を確保できました。 6.効果 技術的な効果: プロセスの一貫性 :KMPの実装により、iOSとAndroid間での動作の不一致が最小化され、QA時のエラー頻度が低下しました。 コードの再利用性 :Androidチームが検証したコードはiOSでも使用されるため、両方のプラットフォームにわたって開発効率が向上します。 OS連携と開発リソースの最適化: コミュニケーション負担の軽減 :KMPにより、Androidチームはほとんどのメンテナンスを単独で処理できるようになり、iOSチームはバージョン アップグレードと軽微なメンテナンスに集中できるようになります。 これは、開発リソースのより効率的な使用と、チーム間の連携強化をもたらします。 プロジェクト管理上の問題: 開発およびメンテナンスのコスト :初期設定には時間がかかりますが、その後は通常通り開発を続けることができます。ただし、Android固有のライブラリおよびJavaベースのライブラリの使用には制限があるため、開発コストが増加する可能性があります。 リソースの割り当て :Androidチームに重点を置いた開発プロセスは、繁忙期におけるリソース不足につながる可能性があります。 KMPで実装された機能は主にAndroid チームが管理しているため、iOSチームの理解度は比較的低く、バランスの取れたリソース配分とトレーニングが求められます。 7.今後に向けて:今後の計画と課題 継続的な教育とトレーニングを通じた将来の拡張計画の実施 現在、私たちのチームは、Kotlin Multiplatform(KMP)技術をより効果的に活用するために、社内教育およびトレーニングプログラムの開発と実施に取り組んでいます。このプログラムは技術的な詳細にとどまらず、チームワークやプロジェクト管理スキルの向上にも重点を置いています。これにより私たちは、技術力の向上だけでなく、プロジェクトの効果的な管理やチーム間の連携強化も目指しています。 今後の計画:KMPへの共通ロジックの移行 今後、私たちのチームは、より多くの共通ロジックをKMPへ移行する予定です。これにより、iOSアプリケーションとAndroidアプリケーション間でのコード再利用を最大化し、メンテナンスの複雑さを軽減することで、開発効率の向上を図ります。 移行へのキーロジック: APIクライアント: BFF、OCR ビジネスロジック: キャッシュ管理など ユーティリティ: テキストの書式設定(時間、使用料)、バージョン比較(利用規約)など ローカルストレージ: アプリ設定、認証トークンなど これらの計画を実行することで、クロスプラットフォーム開発の効率と連携が強化され、私たちのチームがプラットフォーム間での開発タスクをより効果的に行えるようになることが期待されます。 お読みいただきありがとうございました。これが、まだKMPテクノロジーを適用していないチームにとって、役立つ参考資料となることを願っています。
アバター
先日、同僚との何気ない会話の中で、こんな言葉をふいに投げかけられました。 「AIって、あっという間にここまで来たよね。あと5年もしたら、何ができるようになるか想像もつかないよ。」 自分はAIの専門家ではありません。ですが、たまたまその分野について少し調べたことがあるんです。 そして、その問いに対する答えは「もっと進化してすごくなる」という単純なものではありません。 というのも、AIの発展には、その技術の本質に根ざした壁がいくつか存在しているのです。 繰り返しますが、自分は専門家ではありません。 ただ、その壁とは何か、そしてそれをどう乗り越えられる可能性があるのか、それをお話ししたいと思います。 ボトルネックとしての人間 ご存知のとおり、大規模言語モデルをはじめとする生成AIは、学習のためにデータを必要とします。 しかも、その量は 膨大 です。 そのデータは、Webクローリングやスクレイピングを通じてインターネット上の公開情報から収集されたり、本やコードのリポジトリなどから集められたりします。 そして重要なのは、それらのコンテンツは基本的に 人間が作っている という点です。 ですが、私たち人間は、AIが消費できる速度で新しいデータを作るには、あまりにも遅すぎるのです。 Epoch AI研究所 のPablo Villalobosさんが執筆した 論文 によると、現在の傾向が続けば、 2026年から2032年の間 に、高品質で公開されている人間のテキストデータが枯渇するとされています。 つまり、それ以降は「データを増やしてスケールさせる」というアプローチが通用しなくなるかもしれません。なぜなら、学習に使える新しい人間由来のコンテンツが、もう十分に残っていないからです。 人間が生成した公開テキストの実効ストックおよびLLM学習におけるデータ消費量の予測 データの再利用(いわゆる マルチエポック学習 )は一定の効果がありますが、根本的な解決にはなりません。 さらに悪いことに、現在急増しているデータの中には、質の低いものが多く含まれています。例えば、スパム、SNSのコメント、極端に偏った情報、誤情報、違法なコンテンツなどです。 また、人間が生成するデータは、英語のような広く使われている言語に比べて、あまり普及していない言語では自然と蓄積のペースが遅くなることも指摘しておくべきかもしれません。 そのため、そういった言語においては「人間が作るデータ量」と「AIのデータ需要」とのギャップが、さらに深刻になる可能性があります。 では、どうすればよいのでしょうか? 提案されている解決策の例としては、以下のようなものがあります: 合成データ(つまりAIが生成したデータ)を学習に使う 分野によっては効果が見込めますが、「モデル崩壊」というリスクも伴います。この問題については次のセクションで詳しく説明します。 非公開データを活用する つまり、企業などが保有しているデータをAI学習に利用するということです。当然ながら、法的・倫理的に重大な問題が生じます。実際に、 New York Times のように、自社コンテンツのスクレイピングをAIベンダーに禁止する企業も出てきています。 効率性の向上 単に大きくするのではなく、 賢く学習させる というアプローチです。 実際、最近のモデルではその兆しが見え始めています。ChatGPTなどを使っていると、「推論(reasoning)」のような、ただの記憶の再生ではなく、複数のステップを論理的につなげる動きが見られるようになっています。 生成モデルの近親交配 先ほど触れたように、学習に使えるデータ量を増やす方法のひとつが、データを 生成 してしまうことです。 ですが、これには独自のリスクがついてきます。 こちらの論文 では、ケンブリッジ大学のZakhar Shumaylovさんが、「人間ではなく、過去のAIモデルが生成したデータで次世代のモデルを学習させると何が起こるのか?」を調査しています。 著者らは、 モデル崩壊 (Model Collapse)と呼ばれる危険なフィードバックループを指摘しています。 AI生成データでの学習が繰り返されると、モデルは現実世界の本来のデータ分布から徐々にズレていきます。 その結果、出力はより一般的に、単調に、そして歪んだものになっていくのです。特に、まれで繊細な特徴は失われやすくなります。 これは主に以下の2つの理由によって起こります: 有限なサンプルにより 統計的な誤差 が世代を重ねるごとに蓄積される 複雑な分布を完全には再現できないことで 機能的な誤差 が生じる モデル崩壊の視覚的イメージ 興味深いことに、人間が生成した元データを10%だけでも保持すれば、モデル崩壊をある程度抑える効果があります。 ですが、 完全に防ぐことはできません 。 人間による本物のデータを意図的に残さない限り、AIモデルはどんどん狭くて自己強化的な世界観に閉じこもっていくことになります。 まさに デジタルな近親交配 です。 さらに、イーストカロライナ大学のGabrielle Steinさんは、AI同士でデータを受け渡す形の「クロスモデル学習」でこの問題が回避できるのかを 検証しました 。 結論としては… あまり効果はなかったようです 。 彼女の研究では、100%、75%、50%、25%、0%といった異なる人間データの割合でトレーニングを実施しました。 その結果、以下のような傾向が見られました: 合成データの割合が増えるにつれて、言語的多様性が徐々に減少 特定の割合で急激に崩壊が進むような「転換点」は見られなかった わずかでも人間のデータを混ぜることで、劣化のスピードは抑えられた 彼女は、 全体の半分以上 を確実に人間が書いたと確認できるコンテンツで構成することが、モデル崩壊を初期段階で食い止める有効な対策だと提案しています。 インターネット上で目にするデータの多くがAIによって生成されたものであり、しかもAIの学習素材のほとんどがインターネットから集められていることを考えると、AIの未来はやや暗いものに見えてくるかもしれません。 AI生成コンテンツが学習データにますます入り込むことで、将来的にモデル崩壊を引き起こすリスクが高まっているのです。 しかし、状況を打開できるかもしれない新たな先進的アプローチも登場しつつあります。 次に来るものは? ここまで紹介してきた課題を解決するために、比較的新しいアプローチが最近少しずつ登場しています。 恒久的な解決策とはいかないまでも、「近親交配+データ不足による崩壊」をしばらくのあいだ 先延ばしにする ことはできるかもしれません。 すでにひとつ例として挙げたのが、AIの「推論(Reasoning)」です。 これは、ChatGPTのようなモデルが最終的な回答を出す前に、複数のステップで内部的に思考・判断を行うような動きです。 もうひとつ有望とされているのが、 検索拡張生成(Retrieval-Augmented Generation / RAG) という手法です。 簡単に言えば、AIモデルが学習済みデータだけでなく、 外部から提供されたドキュメント の情報も使って応答を生成するというアプローチです。 たとえば、LLMにPDFファイルを読み込ませたり、回答の前にインターネット検索させたりするようなケースが該当します。 とはいえ、察しがつくかもしれませんが、これも 本質的なデータ不足の問題を解決するわけではありません 。 なぜなら、モデルに与えられる新しい情報には限りがあるからです。 では、まだ本格的には実現されていないものの、有望とされるアプローチは何でしょうか? その一例が、 仮想現実環境+エンボディド・エージェント (Synthetic Reality + Embodied Agents)という方向性です。 これはAI開発において従来とはまったく異なる発想です。 静的なデータセットから受動的に学ぶのではなく、AIエージェントを動的な仮想環境に配置し、目標達成のために行動・探索・適応をさせるのです。 そこで得られるのは、結果を体験し、仮説を試し、戦略を立てる中で 自ら生成したデータ です。 それは、文脈があり、多様性があり、因果関係に基づいた、極めて質の高い情報です。 この方法であれば、無限に近いバリエーションを持つ環境の中で、 持続可能かつ自己更新型の学習 が可能になります。 人間が書いたテキストの枯渇に依存せず、AI自身の出力に閉じこもるリスクも避けられます。 ……とはいえ、現時点ではまだこの段階には到達していません。 つまり、退屈な作業をいろいろとAIに押し付けることには成功しましたが── 少なくとも今後しばらくの間は、私たち自身がそれなりに働く必要がありそうです。 それでは、また!
アバター
This article is the entry for day 20 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello, my name is Hand-Tomi and I am a mobile app (Flutter) developer at KINTO Technologies ( KTC ). Recently, KTC has launched the “Flutter Team”, and now is actively developing different applications. I would like to introduce some techniques we’ve implemented and found to be particularly effective. This time, I will explain how to use GitHub Actions and Firebase Hosting to provide convenient web previews during code reviews. I hope this article is helpful to you. 🎯Goal The goal of this article is to create a system where a debug web page link is automatically posted as a comment by adding the comment /preview to a pull request. ![preview_comment_and_link](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview_comment_and_link.png =600x) When you click the link above, the Flutter project application will be displayed as shown below. ![preview](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview.png =400x) 🔍Why Use Web Preview? When reviewing code, you need to clone the source code, configure it, and build it to check how it operates, but this process takes time. On the other hand, if you set up Web Preview , you can easily and quickly check how it operates. From here on, I will introduce how to implement this system step by step. Setting up Firebase 🌐Creating a Firebase Project If you don't have a Firebase project yet, create a new project from the console. The project name is "sample". If you don't plan to use other features, you can disable them (though there's no harm in leaving them enabled). Wait a while The project creation is complete! ⚙️ Setting up Firebase CLI I'm planning to set up Firebase Hosting for the Flutter project. Since you can easily set it up using the Firebase CLI, let's set it up in the Terminal. 1. Installing Firebase CLI There are several ways to install the Firebase CLI, but on macOS with npm installed, you can easily install it using the following command: npm install -g firebase-tools For installation on other environments, please see here . 2. Log in to Firebase CLI Run the following command to log in to Firebase on the CLI. firebase login 🔧 Setting up Firebase Hosting Now that we're ready, let's set up Firebase Hosting for the Flutter project. 1. Enabling webframeworks To deploy a Flutter application to Firebase Hosting, you need to enable the experimental feature webframeworks . firebase experiments:enable webframeworks 2. Initializing Firebase Hosting In the root directory of your Flutter project, run the following command to set up Firebase: firebase init hosting After running the above command, you will be asked the following questions: # For the Firebase project, select the sample project you created earlier. ? Please select an option: Use an existing project ? Select a default Firebase project for this directory: sample-1234 (sample) # It's fine to answer "Yes" here. ? Detected an existing Flutter Web codebase in the current directory, should we use this? Yes # It's a question about region selection. I chose the default "us-central1 (Iowa)." ? In which region would you like to host server-side content, if applicable? us-central1 (Iowa) # Since I plan to create it myself, I selected "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! Once you answer the questions, a firebase.json file will be generated. :::message alert If your Flutter project does not include the Web platform, you may encounter an error. In that case, run the following command to add a Web platform: flutter create . --platform web ::: 3. Deploy Let's try deploying it. firebase deploy When you run the above command, the Hosting URL will be displayed as shown below. ... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/sample-1234/overview Hosting URL: https://sample-1234.web.app If you open this URL, you can see that your Flutter project is displayed correctly. Creating GitHub Actions Next, let's create a YAML file that will be executed when you comment /preview on a pull request. 🔑 Preparing your Firebase Service Account Key To deploy to Firebase through GitHub Actions, you need a Firebase service account key. To easily obtain the key, use the following command: firebase init hosting:github After entering the above command, you will be asked the question below. Specify the repository containing the source code in the format user/repository . # Enter a GitHub repository. Write it as `user/repository`. ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) Hand-Tomi/sample Firebase will then automatically set the service account key in the Secrets of the GitHub repository and provide you with the Secrets constant name as shown below (e.g. FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 ). Save this constant name. ✔ 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. Next, you will be asked the following question, but since you have everything you need, press Control+C (or in the case of Windows, Ctrl+C ) to exit. ? Set up the workflow to run a build script before every deploy? ✍️ Create a YAML File for GitHub Actions Now it's time to create a YAML file for GitHub Actions. Create a YAML file in the .github/workflows directory at the root of the Flutter project and add the following code: 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 has been deployed. - **Link**: ${{ steps.deploy-web.outputs.details_url }} For firebaseServiceAccount , specify the Secrets’constant name you created beforehand (e.g. FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 ). firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} After that, when you merge into the relevant repository and leave a comment /preview in the pull request, Actions will be executed automatically, and GitHub Actions will comment a link. 💡Reference: Explanation of the YAML Code for GitHub Actions The explanation for the YAML code above is as follows: Execution Timing on: issue_comment: types: [created] When you use issue_comment , a workflow is automatically executed when a comment is created. This comment applies not only to pull requests but also when a comment is made on an Issue. In this article, I want to limit the scope to pull request comments, so I will include github.event.issue.pull_request in if of jobs , ensuring that only pull requests are executed. jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} Additionally, when using issue_comment , you need to change the checkout location. issue_comment does not have the latest commit information for the current pull request, so if you check it out as it is, it will check out to the latest commit on the default branch. Therefore, you need to specify ref in actions/checkout as follows (Reference: 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 Check Comment Message Check whether the comment message is /preview . Check github.event.comment.body as shown below, and only if the message is /preview , execute the process in the deploy-web job. jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} Prevention of Concurrent Execution If you leave /preview as is and immediately comment /preview again, concurrent execution may occur. concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true In this case, GitHub Actions prevents concurrent execution through concurrency . The important thing is to place it under 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 If you do not place it under jobs as shown above, concurrency will be executed without checking github.event.comment.body == '/preview' in if , and if you leave a comment other than /preview immediately after commenting /preview , the Action will not be executed. Deploy The following steps are for deploying to 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 }} : This is the Firebase service account authentication key. Enter the one you previously obtained. expires: 7d : This is the expiration date. If you set it like this, your preview site will expire after 7 days. channelId: "issue_number_${{ github.event.issue.number }}" : This is the name of the Firebase Preview channel. If you specify a channelId other than live , it will be deployed to Firebase Preview and you can set the expiration date. FIREBASE_CLI_EXPERIMENTS: webframeworks : This uses webframeworks , an experimental feature of Firebase CLI. It is indispensable for Flutter Web. Link Comments A comment was left for the link using peter-evans/create-or-update-comment . By using this, you can easily leave reactions and add or edit comments. - 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 has been deployed. - **Link**: ${{ steps.deploy-web.outputs.details_url }} if: success() : Only if successful will this step be executed. token: ${{ secrets.GITHUB_TOKEN }} : You need a GITHUB_TOKEN to leave a comment. No additional setup is required. issue-number: ${{ github.event.issue.number }} : This specifies which issue to comment on. When the workflow was executed via issue_comment , the issue number can be confirmed with github.event.issue.number . steps.deploy-web.outputs.details_url : This displays the URL obtained from the deployment step above. If you want to include other information, see here . Conclusion The techniques introduced in this article will enable faster and easier verification of operation during code reviews than before. This will improve the development efficiency of the entire team, leading to the delivery of better products. However, when introducing a web preview for debugging, it is essential to consider both security concerns and how to resolve OS-specific differences. Using the OS's functions extensively may result in the disadvantages outweighing the advantages. However, in projects that don't heavily rely on OS functions, the advantages outweigh the disadvantages, so I encourage you to introduce it into your development environment. Also, be sure to check out other articles written by the Flutter team members! Flutter development: Designing the border of a QR code using CustomPaint and Path Thank you for reading this blog to the end.
アバター
This article is the entry for day 19 in the KINTO Technologies Advent Calendar 2024 🎅🎄 This is Nakanishi from Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station Project was officially launched and later established as a team. We are also running an in-house podcast and we're excited to share some of its episodes in this year's Advent Calendar. What is the Learning Roadside Station? The Learning Roadside Station project was launched to enhance accessibility to the study group sessions frequently held within the company, with the goal of making them more accessible and effective. The initiative aims to support the organization of study group sessions led by enthusiastic employees and promote in-house knowledge sharing. Report on Participation in WWDC The KTC Learning Roadside Station podcast features interviews with team members organizing study group sessions in the company. These interviews are titled "A Peek into the Study Session Next Door". Today, we’re changing things up a bit and interviewing Nakano-san, who recently attended WWDC (Worldwide Developers Conference) in person in the hopes to share with everyone insights and inspirations that Nakano-san brought back from attending the event in person. Interview Akeda-san: Today we are joined by Nakano-san, who attended WWDC in person. Thank you for joining us. Nakano-san: Thank you for joining us. Akeda-san: First, could you please introduce yourself and tell us about the kind of work you usually do? Nakano-san: I am the product manager for the "KINTO Kantan Moushikomi App" (from now on, The App) in the Mobile App Development Group. In my work, I develop The App and synchronize it with new vehicle data from KINTO ONE on the web side. Since this requires regular updates, I participate in weekly meetings to stay informed. In addition to my work, I often serve as an MC for events within KTC. Akeda-san: Thank you. I have the impression that he is well-known within the company. Could you tell us what motivated you to attend WWDC? Nakano-san: Since The App is a mobile application, it must go through App Store review before release. Anyone with an Apple Developer Program–linked account can apply to attend WWDC. At our team we were told that it's open to anyone who wants to apply. I was an iOS engineer at my previous position here and had a developer account back then too, but unfortunately, I wasn’t selected at the time. This time, however, I was fortunate to be chosen and was able to attend. Akeda-san: Was it difficult to secure the funding to attend? Nakano-san: Well, actually, after I found out I was selected, I consulted with my manager about what to do. In the end, I took the matter to our CEO Kotera-san, prepared some materials, submitted them, and received approval. I guess it is because I was lucky that the company encourages employees to attend events. Akeda-san: Could you share any episodes during your time at WWDC that you haven’t mentioned elsewhere? Nakano-san: WWDC was held at Apple Park, where Californian plants filled the courtyard. There were also fruit trees, and employees were told not to pick them. The developer relations staff shared some fun trivia. It was fun. For example, the staff told me how long it would take to walk around the inner and outer perimeter. Akeda-san: Did you learn anything from communicating with engineers and tech developers? Nakano-san: Yes, I had the opportunity to interact with engineers and share what apps we were each developing. There were many people creating apps on their own, and many who weren't software engineers by profession but were developing apps as a hobby. Many people said that coming to Apple Park boosted their motivation. Akeda-san: Did attending WWDC help you with app development or your work in any way? Nakano-san: I had the opportunity to receive direct advice on app development and on several challenges I was facing. As a result, I am moving forward with improvements to The App's UI and UX. The next release will reflect the advice I received. Akeda-san: Can someone who is not confident in English still participate in WWDC? Nakano-san: I don't think it's a problem at all. It's better to be able to speak English, but I think you can get by with just the flow. Since I took an Uber to my destination and could easily specify the location through it, I had no difficulties. Knowing English definitely helps, but if you have the willingness to participate, you'll be fine. Akeda-san: Finally, could you say a few words to everyone listening to this interview? Nakano-san: I hope you will have the opportunity to attend WWDC and bring new ideas to KTC. To make KTC even better, I encourage everyone to take on the challenge. Determination leads to success. Akeda-san: Thank you very much. That's all for the interview with Nakano-san.
アバター
はじめに こんにちは。Toyota Woven City Payment Solution開発Groupで決済関係のバックエンド開発を担当している塩出です。 前回の記事 でも述べた通り、本Groupでは開発にKotlinを使用しており、webフレームワークにはKtor、ORMにはExposedを使用しています。またコードのアーキテクチャーとしてはクリーンアーキテクチャーを採用しています。 当初からKotlinの Result型 を使ってエラーハンドリングしていましたが、開発人数が増えていることもあり、Resultとthrowが混じったコードになっていました。Resultを使っているところにthrowが入ると、型でエラーハンドリングの必要性を表現しているにもかかわらず、try-catchも必要になってきてしまう状態でした。 KotlinではJavaの検査例外がないので、try-catchは簡単に呼び忘れてエラーハンドリング漏れが発生してしまいます。この状況を改善すべくチーム内で話し合ってKotlinのResultを使ったエラーハンドリングの方法を統一しました。 今回は本Groupでどのようにエラーハンドリングを書いているのかを紹介します。 この記事には以下の内容は含まれません クリーンアーキテクチャーの説明 Ktor, Exposedの説明 kotlin-result とKotlin公式のResult型との比較 アプリケーションのディレクトリ構成 本題に入る前に、本Groupでのアプリケーションのディレクトリ構造について説明します。 以下にクリーンアーキテクチャーの有名な図と本Groupのディレクトリ構成を載せます。本Groupではクリーンアーキテクチャーを採用しており、アプリケーションのディレクトリ構造もそれにおおよそ則った形で構成されています。 (出典: The Clean Code Blog ) App Route/ ├── domain ├── usecase/ │ ├── inputport │ └── interactor └── adapter/ ├── web/ │ └── controller └── gateway/ ├── db └── etc ディレクトリとクリーンアーキテクチャー図との対応は以下の通りです。 domain ディレクトリ: entities usecase ディレクトリ: Use Cases adapter/web/controller ディレクトリ: Controllers adapter/gateway ディレクトリ: Gateways 用語のズレは少々ありますが、基本的には domain ディレクトリが円の中心で、 usecase ディレクトリがその外側、 adapter 以下が円の一番外側といった感じになっています。 なので依存を許可する方向としては以下のようになります。 usecase -> domain adapter 以下 -> usecase or domain このような依存の方向性にすることで、webフレームワークやdatabaseの種類などに影響を受けることなく、ビジネスロジックを開発することができます。 エラーハンドリングの方針 基本的にエラーハンドリングは以下の方針にしています。 処理失敗の場合はthrowではなくResult型を使用する 関数がResult型を返却するとき、throwは使わない Exceptionを返却するときは独自定義したException型を利用する 次の章で個別の方針についてコード例を交えながら細かく紹介していきます。 関数が失敗する可能性がある場合は戻り値としてResult型を使用する KotlinではJavaのような検査例外がなく、呼び出し元にエラーハンドリングを強制する仕組みがありません。Result型を使うことで呼び出し元にエラーが返却される可能性があることを明示できるので、エラーハンドリングが漏れる可能性が低くなります。 ただし Result<Unit> のように戻り値を使用しない場合は強制させることはできません。この場合はcustom lintを定義する必要がありますが、現状定義できていません。 コード例 以下は簡単なコードの例です。割り算をする関数を定義する場合、通常分母がゼロの場合はエラーになります。このように関数が失敗する可能性がある場合は戻り値にResult型を指定します。この場合は Result<Int> を指定しています。 fun divide(numerator: Int, denominator: Int) : Result<Int> { if (denominator == 0) { return Result.failure(ZeroDenominatorException()) } return Result.success(numerator/denominator) } Result型でExceptionを返却する場合、そのExceptionを独自定義したExceptionでラップする Repositoryなどは通常interfaceがdomainにあり実装がadapter層にあります。Use Case層からRepositoryの関数を呼び出してエラーハンドリングする場合、adapter層でサードパーティlibraryのExceptionをそのまま返却してしまうと、そのサードパーティのExceptionをUse Case層が知らないといけません。その場合、Use Caseが実質adapter層に依存してしまうことになってしまいます。図で示すと以下のような感じです。 ![依存関係](/assets/blog/authors/reona-shiode/error-handling/dependency.png =400x) interface利用の依存とexceptionの依存(だめな例) それを避けるため、Result型で返却するExceptionは必ず独自定義したExceptionにラップして返却するようにしています。 クリーンアーキテクチャーにおいてExceptionはどこの層なのか悩むポイントですが、個人的にはdomain層だと思っています。 本Groupでは複数サービスで共通のException型を使用しているので、domain libraryとして切り出しています。 Kotlin公式のResult型ではExceptionの型を指定できないので、実装者に独自定義したExceptionを返すように強制できないのが悩みポイントです。その場合は kotlin-result の使用を検討するのが良さそうです。ただ、本GroupではdomainのコードにKotlin公式ではないサードパーティの型が入り込むのを避けたかったため、採用を見送りました。 コード例 以下のinterfaceがdomainに定義されているとします。 data class Entity(val id: String) interface EntityRepository { fun getEntityById(id: String): Result<Entity> } またサードパーティのlibraryが以下のようなmethodを持っていてそれを使う例を考えます。 fun thirdPartyMethod(id: String): Entity { throw ThirdPartyException() } NG例 adapter層の実装で以下のように直接Exceptionを返却してしまうと、 UseCase などの呼び出し元にサードパーティのExceptionが漏れてしまいます。 class EntityRepositoryImpl : EntityRepository { override fun getEntityById(id: String): Result<Entity> { return kotlin.runCatching { thirdPartyMethod(id) } // This returns the third party exception } } OK例 サードパーティのExceptionが呼び出し元に漏れないように、独自定義したExceptionでラップします。 class EntityRepositoryImpl : EntityRepository { override fun getEntityById(id: String): Result<Entity> { return kotlin.runCatching { thirdPartyMethod(id) }.onFailure { cause -> // wrap with our own exception CustomUnexpectedException(cause) } } } 関数がResult型を返却するとき、 throw は使わない もし関数がResult型を返却するか、Exceptionをthrowする場合、呼び出し側は両方に対応しないといけません。仮に関数を作った人が特定のExceptionは呼び出し元にハンドリングしてもらう必要がないと思っても、呼び出し元がハンドリングしたい場合もあります。従って明示的なthrowを使わずに Result型で統一しています。 DBのコネクションエラーなどは実際発生した場合 Use Case層でリカバリーすることは無理なので、adapter層でexceptionをthrowしてそのままAPIレスポンスまでscope outしても良いかもしれませんが、DB更新できないことによるサードパーティのSaaSとの不整合を検出するためのlogを出力したいこともあります。その場合throwでscope outしてしまうとアラートが適切に上がらない可能性が出てきてしまいます。 エラーハンドリングを要否を決めるのは呼び出し側にあると思いますので、関数作成者がエラーハンドリング不要だと思ってもResult型でExceptionを返却するようにしています。 コード例 リポジトリのsave関数を例に、コード例を紹介します。save関数ではEntity classを受け取り、結果を Result<Entity> で返します。 NG例 以下のようにConnection errorが発生したときにthrowをし、それ以外のエラーはResult型で返却するとします。 class EntityRepository(val db: Database) { fun saveEntity(entity: Entity): Result<Entity> { try { db.connect() db.save(entity) } catch (e: ConnectionException) { // return result instead throw OurConnectionException(e) } catch (e: throwable) { return Result.failure(OurUnexpectedException(e)) } return Result.success(entity) } } それを使うUse Case層ではSave時にエラーが発生したら何かしらのactionを取りたいとします。その場合には runCatching (内部ではtry-catchを利用してResult型に変換)を利用しなければなりません。 class UseCase(val repo: EntityRepository) { fun createNewEntity(): Result<Entity> { val entity = Entity.new() return runCatching { // need this runCatching in order to catch an exception repo.saveEntity(entity).getOrThrow() }.onFailure { // some error handling here } } } OK例 OKな例ではすべてのExceptionを独自定義したExceptionにラップしてResult型でそのExceptionを返却します。こうすることで呼び出し元は runCatching を削除することができ、コードがシンプルになります。 class EntityRepository(val db: Database) { fun saveEntity(entity: Entity): Result<Entity> { try { db.connect() db.save(entity) } catch (e: ConnectionException) { return Result.failure(OurConnectionException(e)) } catch (e: Exception) { return Result.failure(OurUnexpectedException(e)) } return Result.success(entity) } } class UseCase(val repo: EntityRepository) { fun createNewEntity(): Result<Entity> { val entity = Entity.new() return repo.saveEntity(entity).onFailure { // some error handling here } } } Result型を使う上での便利な自作関数の紹介 andThen Result型を使っているとそのResultが成功だったときにその値を使って別のResult型を返す処理をしたいことが多々あります。例えば特定のエンティティーに対してステータスの更新をする場合は以下のようになるかと思います。 fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> { val entity = repository.fetchEntityById(id).getOrElse { return Result.failure(it) } val updatedEntity = entity.updateStatus().getOrElse { return Result.failure(it) } return repository.save(updatedEntity) } このような場合はmethodチェーンで処理を繋げられると書きやすくなります。 kotlin-result では andThen という関数が用意されていますが、kotlin公式のResult型にはありません。そこで本Groupでは以下のようなmethodを定義して使っています。 inline fun <T, R> Result<T>.andThen(transform: (T) -> Result<R>): Result<R> { if (this.isSuccess) { return transform(getOrThrow()) } return Result.failure(exceptionOrNull()!!) } これを使うことで上記の例は以下のように書き換えることができます。同じコードの記述が減ったので少しスマートになりました。 fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> { return repository.fetchEntityById(id).andThen { entity -> entity.updateStatus() }.andThen { updatedEntity -> repository.save(updatedEntity) } } doInTransaction for Exposed 本GroupではORMapperとして Exposed を利用しています。Exposedでは transaction というラムダのスコープの中にDB処理を記述する必要があります。 transaction ラムダはそのscopeの中でExceptionがthrowされたら自動的にrollbackしてくれます。Result型を利用するとExceptionをthrowすることがないため、Resultが失敗だったときに自動的にrollbackをする関数を作成しました。 fun <T> doInTransaction(db: Database? = null, f: () -> Result<T>): Result<T> { return transaction(db) { f().onFailure { rollback() }.onSuccess { commit() } } } 先ほどの UseCaseImpl の例に当てはめると以下のように使用できます。 fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> { return doInTransaction { repository.fetchEntityByIdForUpdate(id).andThen { entity -> entity.updateStatus() }.andThen { updatedEntity -> repository.save(updatedEntity) } } } respondResult for Ktor 本Groupは Ktor をWebフレームワークとして利用しています。UseCaseで返却されたResult型をそのままレスポンスに指定できるように respondResult という関数を作成しました。 suspend inline fun <reified T : Any> ApplicationCall.respondResult(code: HttpStatusCode, result: Result<T?>) { result.onSuccess { when (it) { null, is Unit -> respond(code) else -> respond(code, it) } }.onFailure { // defined below respondError(it) } } suspend fun ApplicationCall.respondError(error: Throwable) { val response = error.toErrorResponse() val json = serializer.adapter(response.javaClass).toJson(response) logger.error(json, error) respondText( text = json, contentType = ContentType.Application.Json, status = e.errType.toHttpStatusCode(), ) } 単純ではありますが、この関数を使用することで Result.getOrThrow を呼ばなくても良くなるのでコードが少しスッキリします。 fun Route.route(useCase: UseCase) { val result = useCase.run() call.respondResult(HttpStatusCode.OK, result.map {it.toViewModel()} ) } ちなみに respondError はthrowableからエラーのレスポンスを返却する関数で、KtorのパイプラインでthrowされたExceptionもこの関数でレスポンスを返却するようにしています。Exceptionを処理するKtorのプラグインも自作しています。 val ErrorHandler = createApplicationPlugin("ErrorHandler") { on(CallFailed) { call, cause -> call.respondError(cause) } } さいごに 本Groupでのエラーハンドリングのやり方と、Result型の便利な自作関数を紹介しました。いろんな会社のtech blogをみていると kotlin-result を使っているところが多い印象でkotlin公式のResult型に関しては使っているという情報は少ない印象です。今のところKotlin公式のResult型でも十分にエラーハンドリングできているので皆様もぜひ使ってみてください。
アバター
本記事は、 KINTOテクノロジーズアドベントカレンダー2024 の19日目の投稿です。🎅🎄 はじめに こんにちは。モバイルアプリ開発グループでiOSエンジニアをしている ラセル・ミア です。今日は、iOS 17で導入された新しい @Observable マクロを使用して、SwiftUIのUIを更新するための改善されたアプローチを紹介します。その仕組み、解決する課題、そしてなぜこれを使うべきなのかを説明します。 TL;DR Observationを使用すると、Swiftにおけるオブザーバーデザインパターンを、堅牢で型安全かつ効率的に実装することができます。このパターンでは、Observableオブジェクトがオブザーバーのリストを保持し、特定または一般的な状態変化を通知することができます。これにより、オブジェクト同士を直接結びつけることなく、複数のオブザーバー間で暗黙的に更新を分配できるという利点があります。 Appleドキュメントより 簡単に言えば、 Observation はビューをデータの変更に応答させるための新しく簡単な方法です。 Observationを使用しない場合の課題 Observation を使う前に、従来の方法でUIを更新する手法と、それに関連する課題を説明します。 簡単な例から見てみましょう。 import SwiftUI class User:ObservableObject { @Published var name = "" let age = 20 func setName() { name = "KINTO Technologies" } } struct ParentView:View { @StateObject private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView:View { @ObservedObject var user:User var body: some View { let _ = print("ChildView.body") VStack { Text("Age is (user.age)") } } } User ObservableObject プロトコルに準拠し、状態の監視を可能にしています。 プロパティnameに @Published を付与することで、ビューに変更を通知します。 ParentView @StateObject を使用して User クラスのインスタンスを管理します。 ChildView @ObservedObject を使用してUserオブジェクトを受け取ります。 両方のビューで、 let _ = print("xxx.body") を使用して、ビューの更新をデバッグログに出力します。 プロジェクトをビルドすると、初期状態では次のようなログが表示されます。 ParentView.body ChildView.Body この時点では問題ありません。初期状態で両方のビューが描画されます。しかし、SetNameボタンを押すと、次のようなログが出力されます。 ParentView.body ChildView.Body ParentView と ChildView の両方が再描画されますが、 ParentView は User のプロパティをまったく使用していなかったため、これは予想外です。さらに、 ChildView ではUserモデルに依存しない定数プロパティを使用しているだけですが、それも再描画されています。このように、 ChildView が静的なTextを返すだけの場合でも、 User モデルへの参照を保持しているため、 User モデルが変更されると再描画されてしまいます。これはパフォーマンス上の重大な課題を浮き彫りにしています。ここで、 Observation フレームワークが私たちを救うために登場します。 こんにちは、@Observable! @Observable マクロは、WWDC 2023で ObservableObject とその @Published プロパティの代わりとして紹介されました。このマクロはプロパティを明示的にPublishedとマークする必要をなくし、SwiftUIビューが変更に応じて自動的に再描画されるようにします。 ObservableObject から @Observable マクロに移行するには、観測できるようにしたいクラスに新しいSwiftマクロ @Observable を付けるだけです。また、すべてのプロパティから @Published 属性を削除してください。 @Observable class User { var name = "" let age = 20 func setName() { name = "KINTO Technologies" } } 注意点:  Structは @Observable マクロをサポートしていません。 以上です!これで、UIはnameプロパティが変更されたときにのみ更新されるようになります。この動作を確認するには、以下のようにビューを修正します。 struct ParentView:View { // BEFORE // @StateObject private var user = User() // AFTER @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView:View { // BEFORE // @ObservedObject var user:User // AFTER @Bindable var user:User var body: some View { let _ = print("ChildView.body") VStack { Text("Age is (user.age)") } } } 修正後、カスタムのobservableモデルは @State を使用して宣言できます。この変更により、 @ObservedObject 、 ObservableObject 、 @Published 、 @EnvironmentObject が不要になります。 @Bindable:Observableオブジェクトの変更可能なプロパティに対してバインディングを作成するためのプロパティラッパー型です。 Appleドキュメントより コードを実行すると、初期レンダリング時に以下の出力が表示されます。 ParentView.body ChildView.Body しかし、 setName ボタンを押しても、コンソールには何も表示されません。これは、 ParentView がUserのプロパティを使用していないため、更新する必要がないからです。同様に、 ChildView にも適用されます。 ParentView に Text(user.name) を追加してプロジェクトをビルドします。その後、 setName ボタンを押すと、以下の出力が得られます。 struct ParentView:View { @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } Text(user.name) // <--- 追加部分 ChildView(user: user) } } } // 出力 ParentView.body // ChildViewで年齢を変更するコードを追加します。 @Observable class User { var name = "" var age = 20 func setName() { name = "KINTO Technologies" } } struct ParentView:View { @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView:View { @Bindable var user:User var body: some View { let _ = print("ChildView.body") VStack { Button("Change Age") { user.age = 30 } Text("Age is (user.age)") } } } // 出力 ChildView.Body この結果から、ビューが不要な再描画を行わず、正しく更新されていることが確認できます。これはパフォーマンスにおいて大きな改善を示しています。 @Observableの仕組み @Observableの仕組みは一見すると魔法のように思えるかもしれません。モデルに @Observable マクロを付与しただけで、SwiftUIビューが問題なく更新されるようになります。しかし、その背後ではいくつかの重要な処理が行われています。 ObservableObject プロトコルから Observation.Observable プロトコルへ移行しました。さらに、 @Published プロパティラッパーの代わりに @ObservationTracked マクロを使用し、 name と age は @ObservationTracked に関連付けられています。このマクロを展開することで、その実装内容を確認することができます。以下は展開されたコードです。 @Observable class User { @ObservationTracked var name = "" @ObservationTracked var age = 20 func setName() { name = "KINTO Technologies" } @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar() internal nonisolated func access<Member>( keyPath:KeyPath<User, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal nonisolated func withMutation<Member, MutationResult>( keyPath:KeyPath<User, Member>, _ mutation: () throws -> MutationResult ) rethrows -> MutationResult { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } extension User:Observation.Observable {} デフォルトでは、オブジェクトは観測オブジェクトにアクセス可能な、観測可能な型のすべてのプロパティを観測します。ただし、特定のプロパティが観測されないようにしたい場合は、そのプロパティに @ObservationIgnored マクロを付与することで制御できます。 @Observable class User { var name = "" @ObservationIgnored var age = 20 func setName() { name = "KINTO Technologies" } } これにより、ageプロパティの変更は追跡されなくなり、それに伴うUIの更新も発生しなくなります。 パフォーマンス分析 以下は、Instrumentsツールを使用して記録されたビュー更新回数のレポートです。 Observation未使用時 Observation使用時 記録結果からの考察 Observation未使用時(最初のイメージ) このInstrumentsセッションでは、SwiftUIアプリの最適化されていないパフォーマンスが記録されています。 表示されたメトリクス :ビューの更新、プロパティの変更、タイミングの概要が表示されます。 ビューの再描画回数 :初期レンダリングに加え、 Set Name ボタンを3回押した際の再描画がそれぞれ発生し、合計で 9 回の再描画が行われました。 パフォーマンス : 合計再描画時間 : 377.71 マイクロ秒 平均再描画時間 : 41.97マイクロ秒/回 Observation使用時(2番目のイメージ) このセッションでは、Observationフレームワークを使用した最適化されたレンダリングが記録されています。 表示されたメトリクス :最初のセッションと同じメトリクスが表示されますが、効率の向上が反映されています。 ビューの再描画回数 :ボタンを複数回タップしても、初期レンダリングと状態変化時の再描画が1回のみの 3 回の再描画で構成されます。 パフォーマンス : 合計再描画時間 : 235.58 マイクロ秒 平均再描画時間 : 78.53 マイクロ秒/回 定量的な比較 ビューの再描画回数の削減 : Observation未使用時: 9回再描画 。 Observation使用時: 3回の再描画 ( 66.67% 削減) 合計再描画時間の短縮 : Observation未使用時: 377.71 マイクロ秒 Observation使用時: 235.58 マイクロ秒 ( 37.65% 削減) 再描画の効率性 : Observation未使用時:より頻繁で、平均 41.97 マイクロ秒 Observation使用時:再描画回数は少ないが最適化されており、平均 78.53 マイクロ秒 この比較から、再描画回数が少なくなった結果としてインスタンスごとの平均再描画時間がやや増加しているものの、 Observation フレームワークが再描画回数の削減と全体的なレンダリングパフォーマンスの向上に大きく貢献していることが分かります。 まとめ この記事では、iOS 17で導入された新しい @Observable マクロによるSwiftUIの改善点について解説しました。重要なポイントを簡単にまとめると、次のようになります。 以前のアプローチの課題 従来の ObservableObject と @Published プロパティを使用する方法では、不要な再描画が発生し、特に一部のビューが変更されたデータに依存しない場合に、パフォーマンスの問題が生じていました。 @Observable の紹介 この新しいマクロは状態監視プロセスを簡素化し、 @Published 、 @ObservableObject 、 @EnvironmentObject の必要性を排除します。クラスに @Observable を付与し、ビューで @Bindable を使用することで、データの変更を自動的に追跡し、必要な場合にのみビューの更新をトリガーできます。 パフォーマンスの向上 @Observable を使用することで、関連するデータが変更された場合にのみビューが更新され、不要な再描画が削減されるため、パフォーマンスが向上します。さらに、 @ObservationIgnored マクロを活用することで、どのプロパティがUI更新をトリガーするかを開発者が細かく制御できるようになります。 メリット 的確な更新によるパフォーマンスの向上。 ObservableObjectと@Publishedが不要になることでコードが簡潔に。 状態変更を型安全に管理可能。 ビューの更新をトリガーするプロパティを細かく制御できる柔軟性。 @Observable を使用することで、SwiftUIでのUI更新の管理がより簡単かつ効率的になり、エラーが減少します。その結果、開発者にとっては作業効率が向上し、エンドユーザーにとってはスムーズな体験が提供されます。 以上で本日の内容は終了です。楽しいコーディングを! 参照- Observation SwiftUIにおけるObservationを探る(WWDC23)
アバター
This article is the entry for day 17 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-No-Michi-No-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station project was officially launched and later established as a team. As part of this initiative, we also run an in-house podcast, and for this year's Advent Calendar, we’re excited to share some of its episodes with you. What is Manabi-No-Michi-No-Eki (Learning Roadside Station) The Learning Roadside Station is a project launched to enhance accessibility and effectiveness of the study sessions that are frequently held within the company. The aim is to promote knowledge sharing by supporting study sessions led by volunteers within the company. Security & Privacy Study Session In our Podcast, we interview team members who hold in-house study sessions. It´s called “A Peek into the Study Session Next Door”. Today, we welcome Kuwahara-san, Morino-san, and Kasai-san, who are leading the Security and Privacy Study Sessions. Thank you all for joining us today. Could you start by introducing yourselves? Interview Morino-san: I’m Morino from the Security & Privacy Group. It’s a pleasure to be here. Kasai-san: My name is Kasai, and I’m part of the Data & Privacy Governance Team. Thank you for having me. Kuwahara-san: I’m Kuwahara from the Security CoE Group. Pleasure to meet you. HOKA-san: First, could you tell us about your daily work and responsibilities? Morino-san: Our team is divided into three sub-teams. The Data & Privacy Governance Team, the Information Security Team, and the Cybersecurity Defense Team. The Data & Privacy Governance Team is responsible for establishing rules and implementing governance regarding data and privacy. The Information Security Team conducts assessments based on information security standards. The Cybersecurity Defense Team manages vulnerabilities and investigates threat intelligence. Kasai-san: My team, the Data & Privacy Governance Team, establishes rules regarding data security management and personal information governance, conducts risk assessments, and reports the results to management. Kuwahara-san: The Security CoE Group is responsible for the security of cloud environments such as AWS, Azure, and Google Cloud. We set up guardrails to ensure safe cloud usage, monitor security risks, and provide support against potential threats. HOKA-san: Next, could you tell us more about the Security & Privacy Study Session? What kind of study session is it? Kuwahara-san: We started this study session to spread security-related knowledge among employees. Last year, we created detailed security guidelines, and to ensure awareness and adoption, we began hosting these study sessions. Morino-san: Our goal is to establish Security by Design and Privacy by Design as standard considerations in all projects. Kasai-san: Ultimately, we want engineers to be able to focus on development, while we support them with privacy and security risk management. HOKA-san: How has the response been so far? Kuwahara-san: So far, we have hosted four sessions, and the response has been very positive. Many attendees have shared that they found the sessions informative, and we’ve also received valuable feedback for improvements. Morino-san: Some participants have mentioned that our explanations have become clearer, and we continue to refine our approach. HOKA-san: What changes do you hope to see at KTC through these study sessions? Morino-san: We aim to have security and privacy features implemented as standard practices. Kuwahara-san: Ideally, security should become so natural and ingrained that our roles become unnecessary. Kasai-san: I want to create an environment where engineers can focus on development with confidence, knowing that security is taken care of. HOKA-san: Finally, what inspired each of you to pursue this field? Morino-san: I started in software development and gradually developed a deep interest in security. Kasai-san: Initially, I wasn't particularly interested in security or privacy, but I became fascinated by governance and its impact. Kuwahara-san: I began my career as an infrastructure engineer, but after experiencing a security breach on a mail server I was managing, I shifted my focus to security. HOKA-san: Thank you all for your time today. We hope the Security and Privacy Study Session continues to be a valuable resource for everyone at KTC. Thank you everyone. This time, we provided details about the Security and Privacy Study Session, the background to its operation, and future prospects. Please look forward to the next study session!
アバター
This article is the entry for day 16 in the KINTO Technologies Advent Calendar 2024 🎅🎄 1. Introduction Hello. I'm Okita from the Mobile App Development Group. It's December! The end of the year is here! Looking back, many events took place this year. One of the things that strongly stands out is: My participation in Developers Summit 2024 KANSAI as a member of Osaka Tech Lab. So, this time I'll be sharing the following: My participation and presentation at Developers Summit 2024 KANSAI The latest news from Osaka Tech Lab 2. How I Felt Before the Presentation As a sponsor, our company had the opportunity to set up a booth and give a presentation at the summit. *For more details, please check out this article^^ Reflecting on Developers Summit 2024 KANSAI We want to recruit people who will join us in envisioning the future! To that end, we want people to get to know KINTO Technologies Corporation and learn about Osaka Tech Lab! With this goal in mind, our company applied to become a sponsor. This is the first attempt of its kind for Osaka Tech Lab. I was entrusted with the important role of delivering a presentation as a sponsor on behalf of our company. At first, I was worried... I'd never given a speech before, so I wondered if I could do it... But it was a valuable opportunity so I decided to give it my best shot! 3. The Day of My Presentation - a Moment of Tension and Emotion Amidst all this, the day arrived. In fact, I spent the morning of the day refining my presentation and rehearsed until the very last minute. And, overwhelmed with nerves, I arrived at the venue early and spent some time wandering around. Meanwhile, the clock kept ticking, and before I knew it, it was my turn. The theme of my presentation was " Challenges " . "There's no point in panicking now." "Let's stay calm and speak slowly and carefully so that our message can reach as many people as possible." With that in mind, I began to speak slowly. As my speech neared its end, I looked up and saw people wearing black T-shirts. To my surprise, they were colleagues from our company, warmly watching my presentation in their KINTO Technologies T-shirts. I was deeply touched. While the audience of other companies participated in bright-colored T-shirts like orange, yellow, and blue, my coworkers wore the less noticeable black. That was so like the Osaka Tech Lab, which made me chuckle and relax. They quietly watched over me as I struggled to prepare for my presentation. Team members from other offices in our company also supported me, someone with no prior presentation experience, by helping me compile my profile and create the presentation content. A few days before the presentation, the managers and team leaders of the Mobile App Development Group suddenly found themselves scrambling to assist me, as I had reached out for their help. That's right; I just happened to be the one presenting, but the content was the result of the efforts and ideas of many others. Thanks to everyone, 174 people attended my presentation. I would like to take this opportunity to express my gratitude to the event management staff, our company members who supported me, and everyone who participated in the session. 4. How I Felt After the Presentation I would like to share my thoughts as I reflect on my experience after the presentation. I began preparing more than a month before, but I found myself scrambling until the last minute. -> It turned out to be a great experience, but I spent too much time overthinking, so I now believe there were more efficient ways to approach it. As it was my first presentation, I didn’t know how to prepare for it. -> In hindsight, I should have first clarified the purpose, goals, and key message of my speech before preparing the materials. During my rehearsals, no matter how many times I practiced, my presentation always finished in just 20 minutes. But on the day of the presentation, I was able to speak for exactly 30 minutes. I was holding the remote upside down, so even though I pressed the button, the next slide wouldn't show, and I got nervous right from the start.  I prepared the materials in PowerPoint and then exported them as a PDF but the font I had carefully chosen was not reflected in the PDF. It was disappointing, but it became a valuable lesson. As mentioned above, the preparations were very challenging, but in the end, I was able to give a presentation that I have no regrets about. I also made new connections with other companies that participated as sponsors, as well as people who contacted me through 'Ask the Speaker'. And to my surprise, the editorial department of CodeZine kindly offered to release a session report about me. What is the appeal of working at Osaka Tech Lab for PM and mobile app engineers challenging themselves in the Toyota Group? (in Japanese) Truly, I was deeply moved. Summary of my presentation - Learning and Growth This is a brief summary of my presentation. ■ Good things and things I've grown from By delivering my presentation at the event, I was able to organize and verbalize my daily efforts. As a result, the next steps for the project I belong to became clear. I was also able to level up myself.   -> I was able to experience the significance of "input and output" and "verbalization." ■ Improvements It was a bit heavy to have a 30-minute presentation slot for the first time in my life -> It is recommended to experience a presentation at an outside event in the 5 to 10-minute presentation slot first.♪ While I was able to convey my thoughts, I regret that my presentation lacked concrete examples that went a step further. -> I would like to aim for a presentation from which the participants can take away at least one lesson. I should have clarified the main points of my presentation before preparing my presentation materials and story. -> If you create the core of your presentation first, it will be easier to create a consistent story and you will be able to prepare more efficiently. (Sample) Key points for a presentation ■ Purpose of presentation ■ Goal (what to achieve in the presentation) ■ Messages to convey 6. Recent developments at Osaka Tech Lab Well, well. Time flies. It's already December. After the Developers Summit 2024 KANSAI, new developments have emerged at Osaka Tech Lab. In other words, taking on a challenge of organizing external events. The first of our challenges was: "Kansai Front-End Year-End Party 2024 HACK.BAR × KINTO Technologies" In the future, I hope to organize the Mobile App Development Group’s events. It would be fun to collaborate and hold events with other companies ♪ The list of things I want to try is overflowing, and it's becoming a bit of a problem, lol. I want to take one step at a time (or maybe half a step at a time, lol). 7. Conclusion How was it? I hope you found this article interesting. Join us in shaping the future of Osaka Tech Lab as we step into an exciting new era! We look forward to your application! KINTO Technologies Corporation Recruitment Top Page Wantedly
アバター
こんにちは!KINTOテクノロジーズで Prism Japan という観光メディア(Web/iOS/Android)を運営している喜多です。 2024年11月からPrism JapanのSNS運用を担当しています。従来の広告配信に留まらず、SNSを用いることでサービスの認知拡大と新たな集客方法として確立することを目的としていました。 ここではその約4ヶ月間の振り返りをしてみたいと思います! できたこと まずはSNS運用をしていく上で「これはできたな」ということからまとめていきます。 ショート動画の作成 TiktokおよびInstagram中心に、ショート動画は今のSNS環境においてキーとなる存在です。各媒体のアルゴリズムでは、投稿されたショート動画をフォロワー外のユーザーにも届け、その反応次第でさらに伸びるかどうかが決まります。 SNSを用いた「認知拡大」という点においては必須科目だったため、類似したカテゴリにおいてバズっている閲覧数の多い動画を真似するところからスタートしました。 そのイメージを元に動画編集ツールを触りながらスキルを習得しました。主に利用したツールは下記です。 ツール名 特徴 Canva 基本的に無料で利用可能。画像、動画の編集が自由自在に可能であり、共同編集者とのシェアもしやすく管理が楽である。過去一度だけ作業中にサーバーが落ちてやり直しになった。 Adobe Express 基本的に無料で利用可能。Canvaと大きくは変わらないが、サーバーが安定していて作業途中で落ちることもなかったため、個人的にはこちらが好み。 Capcut 基本的に無料で利用可能。動画編集機能がメイン。文字の読み上げ機能や画像の3D化、SNSで「よく見る」あのフォントが利用できるなど、SNS向けに特化した機能が多い。 ※無料版に付く「透かし」はInstagramにおいては評価されないため注意。 使う人や目的によって若干好みや使い方が分かれる部分もありますが、基本的にはどのツールを使っても問題はなく、無料の範囲でも充分なクオリティの動画が作成できると思います。 インプレッションの獲得 各媒体共通して多くのインプレッションを獲得することができました。 ここでは運用を本格的に開始した2024年11月1日~2025年3月17日(執筆時点)の期間で前後比較してみます。 X インプレッション数は293%増の62,500件となりました。 Tiktok Tiktokは初めての運用だったため、比較対象がないのですが、全体を通して44万回再生されています。 他のSNSと比較すると、後から再生数がじわじわと伸びてくるような特徴がありました。 Instagram リーチ数が90%増の5万件となりました。 ※閲覧数、インタラクションともに、2024年8月以前のデータが集計不可であったため正しい比較はできませんでした。 Tiktok,Instagramは特に顕著なのですが、年末年始のタイミングで山がグッと発生しています。 多くの人がお休みであったタイミングと初詣シーズンにぴったりの神社を紹介したことが重なって伸びたのだと考えています。 本格的なSNS運用前後で比較すると、投稿回数の増加やそもそも媒体を運用していなかったことも要因として挙げられますが、動画の形式や季節、話題性といったトレンドを押さえたこともあって、大幅な閲覧数の増加が見込めました。 これらにより一定の「認知拡大」に貢献できたのではないかと思っております。 ユーザーとのコミュニケーション 殊にXにおいては新たなユーザーの発掘およびコミュニケーションを取ることができたと思います。 従来の運用では、こちらからフォローを働きかけたりリプライするなどのアクションを行なっていませんでした。 しかし、Xはエンゲージメント重視のアルゴリズムが採用されているため、積極的に観光系に興味を持つユーザーへアプローチしました。 その結果、とあるユーザーさんが実際にアプリのインストールだけでなく、掲載しているスポットへお出かけレビューまで投稿してくださったことが判明しました! 何より企画のアイディアなどもいただけて大変参考になりました。プロダクトやサービスを持っている方は積極的に動くことで結果的にサービス利用やフィードバックがもらえることもあるのでおすすめです。 できなかったこと 続いてできなかったことを振り返りたいと思います。 アプリのインストールやWebサイトの流入には至らなかった 上記に記載したように、「閲覧数」という観点においては増加するなどの結果であった反面、アプリのインストールやWebサイトへの流入は見込めない結果となりました。 下記では各媒体の結果を分析したいと思います。評価期間はSNS運用を本格的に開始した2024年11月1日~2025年3月17日とします。 以下はAppsFlyerで計測した期間中のアプリのインストール数になります。 ※具体的なインストール数の公開は控えさせていただきます。 Tiktok × アプリインストール数 まずはTiktokの各指標とアプリのインストール数に相関関係がないか調査しました。 視聴数 上記で既出の画像となりますが、視聴数とインストール数を照らし合わせてみました。 ぱっと見のグラフの比較でお分かりかもしれませんが、相関関係はないと考えています。 例えば、視聴数が盛り上がっている2025年1月2日のインストール数に変化がありませんでした。 反対にインストール数が盛り上がっている2025年1月29日の視聴数には変化がありません。 このことからもTiktokの視聴数とインストール数は相関関係にないと結論づけています。 プロフィールの表示回数 続いてはプロフィールの表示回数です。プロフィールに来てくれている時点で一定の興味を持ってくれているユーザーであると考えていました。 嬉しいことに増加傾向にあるものの、現状ではインストール数に大きな影響を与えているとは言えないと思います。 いいね いいねの付いた数はどうでしょうか。ぱっと見は視聴数と同じような推移だと思います。 こちらも同様にいいねが最も付いた2025年1月2日のインストール数には変化がなく、2025年1月29日のインストール数の増加にも寄与しているとは言えませんでした。 コメント コメント数は全体通して81件であり、閲覧数を考慮すると少ない結果となりました。日によっては0件と連動しています。 2025年2月17日のコメント数は4件とやや多めですが、インストール数に変化はなかったです。 シェア シェアも視聴数、いいね数と似たような推移となりました。 いいねやシェアが多い動画は評価されて「おすすめ」に表示され、結果的に視聴数が伸びるというアルゴリズムを身を持って実感しました。 Instagram × アプリインストール数 続いてはInstagramです。 閲覧数 こちらも同様に年末年始にかけて大きく山ができています。 そのタイミングでは上記の通りインストール数に影響はありません。 一方で、複数日においてはやや相関関係にありました。全体を通して見ると相関関係は強くはないのですが、Tiktokと比較すると少しだけ関係がありそうです。 また、リーチ数(いわゆるユニーク数)も上記と同様の傾向が見られたためここでは割愛します。 インタラクション 最後にインタラクションです。Instagramにおいてはいいねや保存、コメント、シェアがこちらに該当します。 こちらも同様にインストールとの相関は見られませんでした。 まとめ 全体を通して、インストール数で見ると相関関係は見られず、SNSによって集客ができた、とは言えない状況だとわかりました。 ただ、あくまで相関関係を見ただけなので、仮に視聴者が時間をおいてインストールした可能性もあるとは思いますが、正確な評価は正直なところ難しいと考えています。 Webサイトへの流入 SEO以外の経路としてもSNSの集客が一定貢献することを想定していました。実際のところ、Instagramのストーリー機能およびXのポストから流入を獲得できましたが、総数で見ても約300件とわずかな送客しかできませんでした。 これはXのフォロワー数が低いことによる拡散力の頼りなさ、Instagramのストーリー閲覧者を確保できなかったことが主な要因だと考えています。 インプレッション数は伸びたが、大幅なフォロワー数の増加は見込めなかった X:425フォロワー Xにおいてはフォロー数やいいねなどのアクションがまだまだ足りていませんでした。 他媒体との兼ね合いもあって、長い期間をかけてゆっくりと活動していたことが原因です。 早期のうちに短期間でフォロー、いいねを行うことが好ましいかと思います。過度なフォローはシャドウバンや凍結にもつながるのでご注意ください。 Tiktok:155フォロワー 視聴数の割に全くフォロワーがつきませんでした。理由は大きく二つかと思います。 一つは汎用的な情報を流していたこと。このアカウントをフォローしないと見逃してしまう情報だ、というオリジナリティや独創性に欠けていたと思います。 二つ目はターゲットが曖昧であったこと。Prism Japanの特性上、旅行したいと考える人という幅広い層がターゲットになるかと思います。 その上で、Tiktokではどういう層(年代、性別)にするか曖昧であったため、結局間口が広いまま多数の人に情報を届けていました。 Instagram:6667フォロワー 一見、多い印象を受けるのですが、広告の配信を停止して以降は下降傾向にありました。 広告配信して獲得したフォロワーはエンゲージメントが低い傾向にあるのが一般的とも言われていることに加え、広告配信停止後の投稿は以前とガラリとニュアンスを変えていたため余計にユーザー離れが加速したように思います。 各SNSの特徴と改善ポイント ここでは実際に運用して感じた各媒体の強みと、投稿内容の分析をしていきます。 TikTok:フォロワー数が多くなくても内容次第でバズることができる 最も伸びた投稿: 東京で女子旅するのにぴったりの穴場スポット3選! 「東京で女子旅する人」というかなり狭めたターゲットを勝手に設定して投稿しました。 内容としてはアプリの実際の画面上で穴場のスポットを探すという実際のフローを踏んでいます。 扱っているスポットはまさに穴場で、「海外風、某アニメ、縁結び」をテーマに取り上げました。 結果として、94%の人が検索から流入し、「東京 遊ぶ場所 女子2人」「東京観光」といったクエリで検索した結果流入してくれました。 ターゲットを狭め、穴場として実際にリアルなスポットを取り上げられたことが要因の一つかと考えています。 最も伸びなかった投稿: 日比谷のイルミネーションが綺麗すぎた! 伸びなかった動画は日比谷のイルミネーションです。こちらは実際に現地に足を運んで撮影し、イルミネーションの鮮やかや雰囲気を伝えたかったのがポイントです。 また冒頭部分はBGMとムービーが連動していて興味を引くような作り方をしたつもりでしたが、全く視聴されませんでした。 「クリスマス」「イルミネーション」といったハイクオリティな動画を提供する競合がひしめくキーワード帯に突撃してしまったことが原因ではないかと考えています。 まとめ 上記を比較すると、伸びた動画はターゲットが明確かつ内容がそのターゲットにとって有用であったことが挙げられるかと思います。 結果、平均視聴数やフル視聴率といった各指標も高く、視聴数を大幅に上げられました。 X:ユーザーとのコミュニケーションが強み 最も伸びた投稿: 神社仏閣×紅葉のおすすめ4選 紅葉と神社仏閣のコラボが見どころのスポットをまとめて紹介しました。各スポットの位置情報や見どころポイントとそれに付随した画像を載せています。 最も伸びなかった投稿: 今日はクリスマスですね! クリスマスに盛り上がっていた #MerryChristmas のタグとともに後述する生成AIを利用した動画を添えて投稿しました。 目的としてはアカウントの「人間味」であったのとタグで若干のインプレッションを獲得したいところにありましたが、結果として盛り上がりに欠ける投稿となりました。 まとめ Xにおいては他の伸びた投稿を見ても時季やトレンドに関する投稿が伸びる傾向にありました。 そして情報として有用か(まとまっているか、自分にとって価値があるか)が重要だと思いました。 Instagram:ショート動画を用いてフォロワー外にリーチしやすくなった 最も伸びた投稿: 【24~25シーズン】都内から行ける!おすすめのスキー場4選 冬のスキーシーズン開始に伴って関東から行けるスキー場を画像で紹介しました。 各スキー場の開場期間や時間、料金情報を記載しました。その結果、「保存」が10件と他投稿と比べると多くなりました。 よって、55.4%のフォロワー外ユーザーへもリーチできています。私の感覚では、「リール動画」は保存数あまり関係なく半数はフォロワー外のユーザーへリーチできる一方、画像投稿は9割がフォロー内に留まる印象でした。 振り返るとユーザーが「保存」したくなる投稿が何よりも重要であると考えています。 最も伸びなかった投稿: AIがあなたを診断! こちらは上記とは打って変わってアプリの機能「気分で検索」をリール動画としてアピールしたものです。 実際のアプリ画面を録画し、手順通り進めることでユーザーが使うシーンを想起してもらうことを意図していましたが、結果としては最も再生されない動画でした。 ターゲットが不明瞭で誰にも刺さらない動画であったことと、ユーザーの便益が伝わりにくいものであったのではないかと思います。 まとめ リール動画、画像投稿によってフォロワー外へのリーチのしやすさ、に違いがあるものの、得てして「保存」されることで投稿が伸びるかどうかが決まっている印象を受けました。 有益な情報であることはもちろん、保存を促したり、定期的にその分析を行うことが大事です。 全体のまとめと今後の方向性 全体を通して、SNSが強く集客に貢献しているとは言えないことが分かりました。一方で必ずしも貢献していないとも断言はできないため、引き続き振り返りを経て改善したいと思います。 投稿において特に重要なことは下記2点です。 ターゲットを明確にすること 視聴者にとって有益な情報であること 当たり前と言えば当たり前かもしれないですが、この2点は媒体問わず共通して伸びる動画にとって必要であることだと実感しました。 また、Tiktokの内容をInstagramへもそのまま流用していましたが、それだと媒体でユーザー層が異なるため、反応が得にくい状態にありました。 とはいえ、媒体別に投稿することが望ましいのですが、リソースにも限界があるため、有意義な取り組みになっていませんでした。媒体ごとに特性やユーザー層が異なるため、目的に合わせて何がベストかを見極めることをおすすめします。 参考文献 SNSマーケティング7つの鉄則
アバター
こんにちは!KINTOテクノロジーズで Prism Japan という観光メディア(Web/iOS/Android)を運営している喜多です。 2024年11月からPrism JapanのSNS運用を担当しています。従来の広告配信に留まらず、SNSを用いることでサービスの認知拡大と新たな集客方法として確立することを目的としていました。 ここではその約4ヶ月間の振り返りをしてみたいと思います! できたこと まずはSNS運用をしていく上で「これはできたな」ということからまとめていきます。 ショート動画の作成 TiktokおよびInstagram中心に、ショート動画は今のSNS環境においてキーとなる存在です。各媒体のアルゴリズムでは、投稿されたショート動画をフォロワー外のユーザーにも届け、その反応次第でさらに伸びるかどうかが決まります。 SNSを用いた「認知拡大」という点においては必須科目だったため、類似したカテゴリにおいてバズっている閲覧数の多い動画を真似するところからスタートしました。 そのイメージを元に動画編集ツールを触りながらスキルを習得しました。主に利用したツールは下記です。 ツール名 特徴 Canva 基本的に無料で利用可能。画像、動画の編集が自由自在に可能であり、共同編集者とのシェアもしやすく管理が楽である。過去一度だけ作業中にサーバーが落ちてやり直しになった。 Adobe Express 基本的に無料で利用可能。Canvaと大きくは変わらないが、サーバーが安定していて作業途中で落ちることもなかったため、個人的にはこちらが好み。 Capcut 基本的に無料で利用可能。動画編集機能がメイン。文字の読み上げ機能や画像の3D化、SNSで「よく見る」あのフォントが利用できるなど、SNS向けに特化した機能が多い。 ※無料版に付く「透かし」はInstagramにおいては評価されないため注意。 使う人や目的によって若干好みや使い方が分かれる部分もありますが、基本的にはどのツールを使っても問題はなく、無料の範囲でも充分なクオリティの動画が作成できると思います。 インプレッションの獲得 各媒体共通して多くのインプレッションを獲得することができました。 ここでは運用を本格的に開始した2024年11月1日~2025年3月17日(執筆時点)の期間で前後比較してみます。 X インプレッション数は293%増の62,500件となりました。 Tiktok Tiktokは初めての運用だったため、比較対象がないのですが、全体を通して44万回再生されています。 他のSNSと比較すると、後から再生数がじわじわと伸びてくるような特徴がありました。 Instagram リーチ数が90%増の5万件となりました。 ※閲覧数、インタラクションともに、2024年8月以前のデータが集計不可であったため正しい比較はできませんでした。 Tiktok,Instagramは特に顕著なのですが、年末年始のタイミングで山がグッと発生しています。 多くの人がお休みであったタイミングと初詣シーズンにぴったりの神社を紹介したことが重なって伸びたのだと考えています。 本格的なSNS運用前後で比較すると、投稿回数の増加やそもそも媒体を運用していなかったことも要因として挙げられますが、動画の形式や季節、話題性といったトレンドを押さえたこともあって、大幅な閲覧数の増加が見込めました。 これらにより一定の「認知拡大」に貢献できたのではないかと思っております。 ユーザーとのコミュニケーション 殊にXにおいては新たなユーザーの発掘およびコミュニケーションを取ることができたと思います。 従来の運用では、こちらからフォローを働きかけたりリプライするなどのアクションを行なっていませんでした。 しかし、Xはエンゲージメント重視のアルゴリズムが採用されているため、積極的に観光系に興味を持つユーザーへアプローチしました。 その結果、とあるユーザーさんが実際にアプリのインストールだけでなく、掲載しているスポットへお出かけレビューまで投稿してくださったことが判明しました! 何より企画のアイディアなどもいただけて大変参考になりました。プロダクトやサービスを持っている方は積極的に動くことで結果的にサービス利用やフィードバックがもらえることもあるのでおすすめです。 できなかったこと 続いてできなかったことを振り返りたいと思います。 アプリのインストールやWebサイトの流入には至らなかった 上記に記載したように、「閲覧数」という観点においては増加するなどの結果であった反面、アプリのインストールやWebサイトへの流入は見込めない結果となりました。 下記では各媒体の結果を分析したいと思います。評価期間はSNS運用を本格的に開始した2024年11月1日~2025年3月17日とします。 以下はAppsFlyerで計測した期間中のアプリのインストール数になります。 ※具体的なインストール数の公開は控えさせていただきます。 Tiktok × アプリインストール数 まずはTiktokの各指標とアプリのインストール数に相関関係がないか調査しました。 視聴数 上記で既出の画像となりますが、視聴数とインストール数を照らし合わせてみました。 ぱっと見のグラフの比較でお分かりかもしれませんが、相関関係はないと考えています。 例えば、視聴数が盛り上がっている2025年1月2日のインストール数に変化がありませんでした。 反対にインストール数が盛り上がっている2025年1月29日の視聴数には変化がありません。 このことからもTiktokの視聴数とインストール数は相関関係にないと結論づけています。 プロフィールの表示回数 続いてはプロフィールの表示回数です。プロフィールに来てくれている時点で一定の興味を持ってくれているユーザーであると考えていました。 嬉しいことに増加傾向にあるものの、現状ではインストール数に大きな影響を与えているとは言えないと思います。 いいね いいねの付いた数はどうでしょうか。ぱっと見は視聴数と同じような推移だと思います。 こちらも同様にいいねが最も付いた2025年1月2日のインストール数には変化がなく、2025年1月29日のインストール数の増加にも寄与しているとは言えませんでした。 コメント コメント数は全体通して81件であり、閲覧数を考慮すると少ない結果となりました。日によっては0件と連動しています。 2025年2月17日のコメント数は4件とやや多めですが、インストール数に変化はなかったです。 シェア シェアも視聴数、いいね数と似たような推移となりました。 いいねやシェアが多い動画は評価されて「おすすめ」に表示され、結果的に視聴数が伸びるというアルゴリズムを身を持って実感しました。 Instagram × アプリインストール数 続いてはInstagramです。 閲覧数 こちらも同様に年末年始にかけて大きく山ができています。 そのタイミングでは上記の通りインストール数に影響はありません。 一方で、複数日においてはやや相関関係にありました。全体を通して見ると相関関係は強くはないのですが、Tiktokと比較すると少しだけ関係がありそうです。 また、リーチ数(いわゆるユニーク数)も上記と同様の傾向が見られたためここでは割愛します。 インタラクション 最後にインタラクションです。Instagramにおいてはいいねや保存、コメント、シェアがこちらに該当します。 こちらも同様にインストールとの相関は見られませんでした。 まとめ 全体を通して、インストール数で見ると相関関係は見られず、SNSによって集客ができた、とは言えない状況だとわかりました。 ただ、あくまで相関関係を見ただけなので、仮に視聴者が時間をおいてインストールした可能性もあるとは思いますが、正確な評価は正直なところ難しいと考えています。 Webサイトへの流入 SEO以外の経路としてもSNSの集客が一定貢献することを想定していました。実際のところ、Instagramのストーリー機能およびXのポストから流入を獲得できましたが、総数で見ても約300件とわずかな送客しかできませんでした。 これはXのフォロワー数が低いことによる拡散力の頼りなさ、Instagramのストーリー閲覧者を確保できなかったことが主な要因だと考えています。 インプレッション数は伸びたが、大幅なフォロワー数の増加は見込めなかった X:425フォロワー Xにおいてはフォロー数やいいねなどのアクションがまだまだ足りていませんでした。 他媒体との兼ね合いもあって、長い期間をかけてゆっくりと活動していたことが原因です。 早期のうちに短期間でフォロー、いいねを行うことが好ましいかと思います。過度なフォローはシャドウバンや凍結にもつながるのでご注意ください。 Tiktok:155フォロワー 視聴数の割に全くフォロワーがつきませんでした。理由は大きく二つかと思います。 一つは汎用的な情報を流していたこと。このアカウントをフォローしないと見逃してしまう情報だ、というオリジナリティや独創性に欠けていたと思います。 二つ目はターゲットが曖昧であったこと。Prism Japanの特性上、旅行したいと考える人という幅広い層がターゲットになるかと思います。 その上で、Tiktokではどういう層(年代、性別)にするか曖昧であったため、結局間口が広いまま多数の人に情報を届けていました。 Instagram:6667フォロワー 一見、多い印象を受けるのですが、広告の配信を停止して以降は下降傾向にありました。 広告配信して獲得したフォロワーはエンゲージメントが低い傾向にあるのが一般的とも言われていることに加え、広告配信停止後の投稿は以前とガラリとニュアンスを変えていたため余計にユーザー離れが加速したように思います。 各SNSの特徴と改善ポイント ここでは実際に運用して感じた各媒体の強みと、投稿内容の分析をしていきます。 TikTok:フォロワー数が多くなくても内容次第でバズることができる 最も伸びた投稿: 東京で女子旅するのにぴったりの穴場スポット3選! 「東京で女子旅する人」というかなり狭めたターゲットを勝手に設定して投稿しました。 内容としてはアプリの実際の画面上で穴場のスポットを探すという実際のフローを踏んでいます。 扱っているスポットはまさに穴場で、「海外風、某アニメ、縁結び」をテーマに取り上げました。 結果として、94%の人が検索から流入し、「東京 遊ぶ場所 女子2人」「東京観光」といったクエリで検索した結果流入してくれました。 ターゲットを狭め、穴場として実際にリアルなスポットを取り上げられたことが要因の一つかと考えています。 最も伸びなかった投稿: 日比谷のイルミネーションが綺麗すぎた! 伸びなかった動画は日比谷のイルミネーションです。こちらは実際に現地に足を運んで撮影し、イルミネーションの鮮やかや雰囲気を伝えたかったのがポイントです。 また冒頭部分はBGMとムービーが連動していて興味を引くような作り方をしたつもりでしたが、全く視聴されませんでした。 「クリスマス」「イルミネーション」といったハイクオリティな動画を提供する競合がひしめくキーワード帯に突撃してしまったことが原因ではないかと考えています。 まとめ 上記を比較すると、伸びた動画はターゲットが明確かつ内容がそのターゲットにとって有用であったことが挙げられるかと思います。 結果、平均視聴数やフル視聴率といった各指標も高く、視聴数を大幅に上げられました。 X:ユーザーとのコミュニケーションが強み 最も伸びた投稿: 神社仏閣×紅葉のおすすめ4選 紅葉と神社仏閣のコラボが見どころのスポットをまとめて紹介しました。各スポットの位置情報や見どころポイントとそれに付随した画像を載せています。 最も伸びなかった投稿: 今日はクリスマスですね! クリスマスに盛り上がっていた #MerryChristmas のタグとともに後述する生成AIを利用した動画を添えて投稿しました。 目的としてはアカウントの「人間味」であったのとタグで若干のインプレッションを獲得したいところにありましたが、結果として盛り上がりに欠ける投稿となりました。 まとめ Xにおいては他の伸びた投稿を見ても時季やトレンドに関する投稿が伸びる傾向にありました。 そして情報として有用か(まとまっているか、自分にとって価値があるか)が重要だと思いました。 Instagram:ショート動画を用いてフォロワー外にリーチしやすくなった 最も伸びた投稿: 【24~25シーズン】都内から行ける!おすすめのスキー場4選 冬のスキーシーズン開始に伴って関東から行けるスキー場を画像で紹介しました。 各スキー場の開場期間や時間、料金情報を記載しました。その結果、「保存」が10件と他投稿と比べると多くなりました。 よって、55.4%のフォロワー外ユーザーへもリーチできています。私の感覚では、「リール動画」は保存数あまり関係なく半数はフォロワー外のユーザーへリーチできる一方、画像投稿は9割がフォロー内に留まる印象でした。 振り返るとユーザーが「保存」したくなる投稿が何よりも重要であると考えています。 最も伸びなかった投稿: AIがあなたを診断! こちらは上記とは打って変わってアプリの機能「気分で検索」をリール動画としてアピールしたものです。 実際のアプリ画面を録画し、手順通り進めることでユーザーが使うシーンを想起してもらうことを意図していましたが、結果としては最も再生されない動画でした。 ターゲットが不明瞭で誰にも刺さらない動画であったことと、ユーザーの便益が伝わりにくいものであったのではないかと思います。 まとめ リール動画、画像投稿によってフォロワー外へのリーチのしやすさ、に違いがあるものの、得てして「保存」されることで投稿が伸びるかどうかが決まっている印象を受けました。 有益な情報であることはもちろん、保存を促したり、定期的にその分析を行うことが大事です。 全体のまとめと今後の方向性 全体を通して、SNSが強く集客に貢献しているとは言えないことが分かりました。一方で必ずしも貢献していないとも断言はできないため、引き続き振り返りを経て改善したいと思います。 投稿において特に重要なことは下記2点です。 ターゲットを明確にすること 視聴者にとって有益な情報であること 当たり前と言えば当たり前かもしれないですが、この2点は媒体問わず共通して伸びる動画にとって必要であることだと実感しました。 また、Tiktokの内容をInstagramへもそのまま流用していましたが、それだと媒体でユーザー層が異なるため、反応が得にくい状態にありました。 とはいえ、媒体別に投稿することが望ましいのですが、リソースにも限界があるため、有意義な取り組みになっていませんでした。媒体ごとに特性やユーザー層が異なるため、目的に合わせて何がベストかを見極めることをおすすめします。 参考文献 SNSマーケティング7つの鉄則
アバター