GraphQL
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
はじめに はじめまして。株式会社スタメンのエンジニアの鈴木( @16suzu )です。 弊社スタメンでは、組織のエンゲージメントを高めるためのサービスである TUNAG と、チャットサービスの TUNAGチャット を開発・運営しています。 弊社ではドッグフーディングの一環としまして、この TUNAG と TUNAGチャットを全社で業務利用しています。 日々、これらのサービスを運営する中で、CSチーム経由でお客様からお問い合わせをいただきます。 これまでは、 TUNAGチャット上で連絡が来た際に、PdMが手動で GitHub Issue を起票し、エンジニアチームが対応していました。 しかし、このやり方ではPdMが介在してしまうため工数負荷が課題となっておりました。今回、私はこの課題を解決するために、 Google Forms をトリガーに TUNAGチャットの投稿を収集し、Gemini で要約した上で GitHub に Issue を自動起票するワークフロー を Google Apps Script(GAS)で構築しました。本記事ではその設計と実装を紹介します。 ※注意:本記事では検証のため AI Studio の API を使用していますが、実業務で顧客データなど機密情報を扱う場合は、データが学習に利用されない有料プランや Google Cloud Vertex AI の利用を推奨します。 課題 従来のフローには以下の問題点がありました。 TUNAGチャットの投稿内容を手動でコピーして、Issue を作成していた 起票のタイミングが担当者に依存し、対応漏れが発生することがあった ワークフローの全体像 そこで、以下のようなワークフローを構築しました。 CS メンバーが Priority を判断し Google Forms を送信 ↓ GAS がフォーム回答を受信 ↓ TUNAG チャットの API で問い合わせチャンネルの投稿とスレッド情報を取得 ↓ Gemini API で投稿内容を要約・整形 ↓ GitHub API で Issue を自動起票 ↓ Issue をお問い合わせ用の GitHub Projects に入れ Priority や Type を設定する フォームには「Tunag Chat Link」「Priority」「Ticket type」「メールアドレス」の入力項目を設け、起票時に Priority やチケットタイプを設定できるようにしました。 実装 次にコードを示します。 1. Google Formsの送信をトリガーに処理を開始 エントリーポイントとなるファイルです。 Google Forms の投稿をトリガーとし、各モジュールを順番に実行していきます。 // main.gs const TC_CHANNEL_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' const GITHUB_OWNER = "your-org" const GITHUB_REPO = "your-repo" function submitForm ( event ) { console . info ( event . namedValues ) ; const namedValues = event . namedValues ; const tunagChatLink = namedValues [ "Tunag Chat Link" ][ 0 ] ; const priority = namedValues [ "Priority" ][ 0 ] ; const ticketType = namedValues [ "Ticket type" ][ 0 ] ; const chatId = tunagChatLink . split ( "/pl/" )[ 1 ] ; // 問い合わせスレッドのID const email = namedValues [ "メールアドレス" ][ 0 ] ; const chatMessages = getChatThread ( chatId ) ; const image_links = imageLinks ( chatMessages ) ; const title = createIssueTitle ( JSON . stringify ( chatMessages )) ; const body = createIssueBody ( JSON . stringify ( chatMessages ) , tunagChatLink ) ; const body_with_image = body + `\n### 画像 \n ${ image_links . join ( "\n" )} ` ; // GitHub Issueを作成 const issue_url = createGitHubIssue ( GITHUB_OWNER , GITHUB_REPO , title , body_with_image , priority , ticketType ) ; // TUNAGチャットに投稿. 引数は channelId, rootId, issue_url postIssueUrl ( TC_CHANNEL_ID , chatId , issue_url , priority , ticketType , email ) ; } 2. TUNAGチャット から投稿を取得 TUNAGチャット上にお問合せの投稿がされるので、それを取得します。工夫した点として、スレッド内で行われるCS、エンジニアメンバー同士の議論も取得しています。 この議論も含めてGeminiに要約させることで、GitHub Issueに起票される際の文章の信頼度を高めています。 // tunag_chat.gs const CHAT_URL = "https://your-tunag-chat.example.com/" ; // tunag chat の url const BOT_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; // botの投稿をgeminiの解析対象から外すのに使う // TUNAGチャット 側の投稿を全て取得する function getChatThread ( chat_id ) { const tunag_chat_api_token = PropertiesService . getScriptProperties () . getProperty ( 'tunag_chat_api_token' ) const url = ` ${ CHAT_URL } /api/v4/posts/ ${ chat_id } /thread?per_page=200` ; const headers = { 'Authorization' : 'Bearer ' + tunag_chat_api_token } ; const options = { 'method' : "GET" , 'headers' : headers , } ; const response = UrlFetchApp . fetch ( url , options ) ; var jsonData = JSON . parse ( response . getContentText ()) ; let res = [] ; for ( const key of jsonData . order ) { let post = jsonData . posts [ key ] // botからの投稿は含めない if ( post . user_id == BOT_ID ) { continue } res . push ( { id : post . id , message : post . message , user_id : post . user_id , file_ids : post . file_ids , created_at : formatedDate ( post . create_at ) , }) ; } return res ; } // TUNAGチャット の元のスレッドに作成したIssueのURLを投稿する function postIssueUrl ( channelId , rootId , issue_url , priority , ticketType , email ) { const issuerName = email . split ( '@' )[ 0 ] ; const message = ` ${ issuerName } が Priority \` ${ priority } \` TicketType \` ${ ticketType } \` でIssueを作成しました。 ${ issue_url } ` const tunag_chat_api_token = PropertiesService . getScriptProperties () . getProperty ( 'tunag_chat_api_token' ) const postUrl = ` ${ CHAT_URL } /api/v4/posts` ; const payload = { channel_id : channelId , message : message , root_id : rootId , // ここで返信先のメッセージIDを指定 } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , headers : { 'Authorization' : `Bearer ${ tunag_chat_api_token } ` } , muteHttpExceptions : true } ; try { const response = UrlFetchApp . fetch ( postUrl , options ) ; const responseCode = response . getResponseCode () ; if ( responseCode === 201 ) { Logger . log ( 'メッセージがスレッドに正常に投稿されました。' ) ; return JSON . parse ( response . getContentText ()) ; } else { Logger . log ( `投稿に失敗しました。ステータスコード: ${ responseCode } ` ) ; Logger . log ( `レスポンス内容: ${ response . getContentText ()} ` ) ; return null ; } } catch ( e ) { Logger . log ( `APIリクエスト中にエラーが発生しました: ${ e . message } ` ) ; return null ; } } // unix timeを日本語の年月日の日付に変換 function formatedDate ( created_at ){ const date = new Date ( created_at ) ; // timestampはミリ秒単位かDate文字列 const year = date . getFullYear () ; const month = String ( date . getMonth () + 1 ) . padStart ( 2 , '0' ) ; // 月は0始まりなので+1 const day = String ( date . getDate ()) . padStart ( 2 , '0' ) ; const hours = String ( date . getHours ()) . padStart ( 2 , '0' ) ; const minutes = String ( date . getMinutes ()) . padStart ( 2 , '0' ) ; return ` ${ year } / ${ month } / ${ day } ${ hours } : ${ minutes } ` ; } /** * ファイルIDを持つチャットメッセージから、画像リンクの配列を生成する関数。 * @param {Array<Object>} chatMessages - 各要素がIDとファイルIDを持つチャットメッセージオブジェクトの配列。 * @returns {Array<string>} 画像へのURLリンクの配列。 */ function imageLinks ( chatMessages ) { return chatMessages . filter ( message => message . file_ids && message . file_ids . length > 0 ) . map ( message => ` ${ CHAT_URL } chat/pl/ ${ message . id } ` ) ; } 3. Gemini で要約 TUNAGチャットから取得したデータを、Gemini API を使ってフォーマットに従って要約します。 prompt に入っているのが Gemini に実際に投げているプロンプト文になります。 // gemini.gs // geminiのモデルを指定 const MODEL_NAME = 'gemini-2.5-flash' ; const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/ ${ MODEL_NAME } :generateContent` ; // gemini にIssueのタイトルを作らせる function createIssueTitle ( chat_messages ) { const prompt = "これはTUNAGというサービスの不具合のお問い合わせのチャットのスレッドです。 これらを要約してGitHub Issueを起票したいのでそのタイトルを考えてください。50文字以内でお願いします。 ただし最初に会社名と会社IDを[#133 会社名]の形式で入れてください。会社名が不明の場合は不要です。 [ここからチャットのスレッドです] " + chat_messages const API_KEY = PropertiesService . getScriptProperties () . getProperty ( 'gemini_api_token' ) const apiUrl = ` ${ GEMINI_API_URL } ?key= ${ API_KEY } ` ; const payload = { contents : [ { parts : [ { text : prompt , } , ] , } , ] , } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , muteHttpExceptions : true , } ; const response = UrlFetchApp . fetch ( apiUrl , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; const generatedText = data . candidates [ 0 ] . content . parts [ 0 ] . text ; return generatedText ; } // gemini にIssueの本文を作らせる function createIssueBody ( chat_messages , tunag_chat_link ) { const format = "ここから「フォーマット」です。 ### 課題 ### 機能 ### 発生頻度 ### 緊急度と重要度 ### 発生環境 ### 再現手順 ### チャットリンク" ; const prompt = `入力された「チャットメッセージ」に対して、下記の不具合報告書の「フォーマット」の形に整えた上で出力してください。表現は簡潔にまとめてください。 フォーマットのチャットリンク項目に ${ tunag_chat_link } を記入してください。 ここから「チャットメッセージ」です。 ${ chat_messages } ${ format } ` ; const API_KEY = PropertiesService . getScriptProperties () . getProperty ( 'gemini_api_token' ) ; const apiUrl = ` ${ GEMINI_API_URL } ?key= ${ API_KEY } ` ; const payload = { contents : [ { parts : [ { text : prompt , } , ] , } , ] , } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , muteHttpExceptions : true , } ; const response = UrlFetchApp . fetch ( apiUrl , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; const generatedText = data . candidates [ 0 ] . content . parts [ 0 ] . text ; return generatedText ; } 4. GitHub に Issue を起票 最後に、GitHub に Issue を起票します。起票後に Project への紐付けと、各 Field への値の設定を行います。 // github.gs // GitHub GraphQL API で利用するID const GITHUB_PROJECT_ID = "xxxxxxxxxxxxxxxxxxxx" ; const GITHUB_FIELD_ID_STATUS = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const GITHUB_OPTION_ID_TODO = "xxxxxxxx" ; const GITHUB_FIELD_ID_PRIORITY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const GITHUB_OPTION_ID_LOW = "xxxxxxxx" ; const GITHUB_OPTION_ID_MIDDLE = "xxxxxxxx" ; const GITHUB_OPTION_ID_HIGH = "xxxxxxxx" ; const GITHUB_FIELD_ID_TICKET_TYPE = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const GITHUB_OPTION_ID_BUG = "xxxxxxxx" ; const GITHUB_OPTION_ID_UPDATE = "xxxxxxxx" ; const GITHUB_OPTION_ID_RESEARCH = "xxxxxxxx" ; function createGitHubIssue ( owner = "your-org" , repo = "your-repo" , title = "" , body = "" , priority , ticketType ) { const pat = PropertiesService . getScriptProperties () . getProperty ( 'GITHUB_PAT' ) ; const url = `https://api.github.com/repos/ ${ owner } / ${ repo } /issues` ; const payload = { title : title , body : body } ; const options = { method : 'post' , contentType : 'application/json' , payload : JSON . stringify ( payload ) , headers : { 'Accept' : 'application/vnd.github.v3+json' , 'Authorization' : `Bearer ${ pat } ` , 'X-GitHub-Api-Version' : '2022-11-28' , } , muteHttpExceptions : true } ; try { const response = UrlFetchApp . fetch ( url , options ) ; const result = JSON . parse ( response . getContentText ()) ; // ステータスコードで成功/失敗を判定 if ( response . getResponseCode () === 201 ) { Logger . log ( 'Issueが正常に作成されました: ' + result . html_url ) ; Logger . log ( result . node_id ) ; // Project に追加する const result_add_to_project = addToProject ( GITHUB_PROJECT_ID , result . node_id ) const item_id = result_add_to_project . data . addProjectV2ItemById . item . id ; // Todo をセットする setField ( GITHUB_PROJECT_ID , item_id , // projectにおけるitem id GITHUB_FIELD_ID_STATUS , // fieldId(Status) GITHUB_OPTION_ID_TODO // optionId(Todo) ) // Priority をセットする setPriority ( item_id , priority ) // Ticket type をセットする setTicketType ( item_id , ticketType ) return result . html_url } else { Logger . log ( 'Issue作成に失敗しました: ' + JSON . stringify ( result )) ; } } catch ( e ) { Logger . log ( 'APIリクエスト中にエラーが発生しました: ' + e . message ) ; } } function setPriority ( item_id , priority ) { var p_id = "" ; switch ( priority ) { case 'High' : p_id = GITHUB_OPTION_ID_HIGH ; break; case 'Middle' : p_id = GITHUB_OPTION_ID_MIDDLE ; break; case 'Low' : p_id = GITHUB_OPTION_ID_LOW ; break; default: // 未指定の場合はGitHub IssueのPriorityを設定しない return; } setField ( GITHUB_PROJECT_ID , item_id , GITHUB_FIELD_ID_PRIORITY , p_id ) } function setTicketType ( item_id , ticketType ){ var tt_id = "" ; switch ( ticketType ) { case 'Bug' : tt_id = GITHUB_OPTION_ID_BUG break; case 'Update' : tt_id = GITHUB_OPTION_ID_UPDATE break; case '仕様調査' : tt_id = GITHUB_OPTION_ID_RESEARCH break; default: // 未指定の場合はTicketTypeを設定しない return; } setField ( GITHUB_PROJECT_ID , item_id , GITHUB_FIELD_ID_TICKET_TYPE , tt_id ) } // add to github projects function addToProject ( projectId , issueNodeId ){ const mutation = ` mutation addIssue($projectId: ID!, $issueNodeId: ID!) { addProjectV2ItemById(input: { projectId: $projectId, contentId: $issueNodeId }) { item { id } } } ` ; const variables = { projectId , issueNodeId } ; return callGitHubApi ( mutation , variables ) ; } // Fieldをセットする関数 function setField ( projectId , itemId , fieldId , optionId ) { const mutation = ` mutation updateItemStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) { projectV2Item { id } } } ` ; const variables = { projectId , itemId , fieldId , optionId } ; callGitHubApi ( mutation , variables ) ; } // GitHub GraphQL API にリクエストを送信する関数 function callGitHubApi ( query , variables ) { const GITHUB_TOKEN = PropertiesService . getScriptProperties () . getProperty ( 'GITHUB_PAT' ) ; const url = 'https://api.github.com/graphql' ; const headers = { 'Authorization' : `Bearer ${ GITHUB_TOKEN } ` , 'Content-Type' : 'application/json' } ; const payload = JSON . stringify ({ query : query , variables : variables }) ; const options = { 'method' : 'post' , 'headers' : headers , 'payload' : payload } ; try { const response = UrlFetchApp . fetch ( url , options ) ; const data = JSON . parse ( response . getContentText ()) ; if ( data . errors ) { throw new Error ( `GitHub API Error: ${ JSON . stringify ( data . errors )} ` ) ; } return data ; } catch ( e ) { Logger . log ( `API call failed: ${ e . message } ` ) ; throw e ; } } 実際に起票された GitHub Issue こちらが、実際に起票された Issue のサンプルです。 Projects がセットされ、StatusとPriorityも付与されています。 導入後の効果 フォームを送信するだけで Issue が作成されるため、担当者の負担が大幅に軽減されました 起票が自動化されたことで、議論が Issue として確実に残るようになりました Gemini での要約・整形により、Issue として読みやすい形式で残るようになりました おわりに このワークフローは Gemini と相談しながら約2日で作成することができました。 今回苦労したところは GitHub Projects に Issue をセットする部分で GitHub GraphQL を利用する点でした。 GraphQL の利用経験があまりないため、都度調査しながら進めました。 GAS は外部サービスとの連携が容易で、Google Forms のトリガーをそのまま利用できる点が今回の用途に適していました。 Gemini の活用により、単純な転記ではなく「整理された Issue」として起票できる点も大きなメリットです。 同様の課題を抱えるチームの参考になれば幸いです。 herp.careers
福岡Rubyist会議05 参加レポート こんにちは!Timeeでバックエンドエンジニアをしている志賀( @akitoshiga )です。 表題の通り「福岡Rubyist会議05 」に参加してきたのでそちらのレポートを書きたいと思います! regional.rubykaigi.org 今回「Kaigi Pass」という社内制度を利用して参加しました。 「Kaigi Pass」とは、世界中で開催されているあらゆる技術カンファレンスへの参加を支援する制度です。 productpr.timee.co.jp 会場の様子 当日は福岡県福岡市博多区にある「リファレンス駅東ビル」というところで行われました。 会場いっぱいに来場者が集まりとても賑わっていました。 また、会場には福岡県八女市の名物である八女茶が提供されていました。 自分もいただいたのですがとても美味しかったです。 セッション 特に自分の関心が高かったセッションを3つほど紹介したいと思います。 SQLQL とは何だったのか 発表者 : yancya( @yancya )さん www.slideshare.net yancyaさんが取り組まれているSQLQLについての概要と、技術の変遷に伴ったアップデートのお話でした。 SQLQLとはWebクライアントからSQLクエリを送信して、その実行結果をJSONで返すというコンセプトです。 GraphQLのSQL版と考えるとイメージが掴みやすいと思います。 SQLQLに関しては以下で詳しく解説されています。 SQLQL #Rails - Qiita Client Challenge なお、ここではRDBMSにPostgresを使用することを前提としています。 SQLQLはクライアントが任意でSQLを送信できるので、これを安全に使えるようにするためにさまざまな工夫が施されていました。 WITH句によるCTEを使って取得対象のDBをラップすることでアクセスできる属性を絞り、秘匿すべきデータへのアクセスを制限しています。 INSERT, UPDATE, DELETEといった副作用を伴う操作に関しては自由に実行できないようにSELECTのみの権限しか実行ユーザーには与えず、定義した書き込み権限のあるストアドプロシージャでしか副作用のある操作を実行できないような仕組みを取られていました。 また、悪意のあるクエリの対策としてpg_queryによってクエリをパースしてAST(抽象構文木)をチェックしていました。 課題も存在していて、再帰処理にはうまく対応できないそうです。 SQL ServerやMySQLは再帰処理の深さを設定できるのですが、Postgresだと存在していないため代わりにタイムアウトのみで設定しているそうです。 クライアントでクエリをフェッチすることが可能になるので、これを用いたときにAPI開発コストの削減に繋がるのは良さそうだなと思いました。 ビジネスロジックがストアドプロシージャに集中してしまわないかなというのと、サーバー側ではクエリを効率化できないのでデータ量が多かったりリレーションの複雑なテーブルからデータを取得する場合はクライアント側で工夫が必要だなと感じました。 SQLをそのままパラメーターにしてWeb経由でクエリを投げるアイデアと、安全性を担保するためのさまざまな試みに技術者のロマンを感じました。 再帰処理に関してはPostgreSQLに存在するCYCLE句を使えば安全に扱えそうなのですが、そうすると特定のRDBMSに依存してしまうのと、クライアントとクエリの決め打ちをしなければならずコンセプトと乖離しそうな気がするので悩みどころだなと感じました。 今回はこのSQLQLのアップデートとしてWAL監視のアイデアを取り込んだデモアプリを披露されていました。 WAL(先行書き込みログ)とはデータベースに変更を行う前に操作内容を記録するログのことであり、このWALを監視することでリアルタイムなデータストリームの観測が可能で、このアイデアをSQLQLに応用されていました。 WAL監視については自分は初見だったのですが、CDC(Change Data Capture)の実装に利用されたりしているそうです。e.g. Debezium Server このほかにもSQL標準の変遷やLive QueryなどSQLQLを起点としてDB 、データ基盤についての幅広いお話を伺いました。 Live Queryで話に上がっていたKafka Streamsについては内部動作と構造に興味が湧いたので後日その深掘りをしてみました。 また、WAL監視については今後複数コンポーネント間でリアルタイムにデータ更新を検知したいときなどにこのアイデアが役立ちそうだなと思いました。 型を書かないRuby開発への挑戦 発表者 : shia( @riseshia )さん speakerdeck.com 自作GemのTypeGuessrのお話。 TypeGuessrは「型情報を開発者が追加せずに型情報を得る」ことをコンセプトに開発されたGemで、Ruby LSPのアドオンという形で使用します。 github.com Rubyに型情報をもたらすツールはいろいろありますが、SteepやSorbetといったような型定義を行い正確な型情報を得るものとは明確に棲み分ける形で開発されています。 Hash/Arrayに対してStructural Typing(構造的部分型)をサポートしつつも複雑な型推論が走りそうな場合はあえてuntypedにして計算量をコントロールしています。 特徴として、Duck Typingに基づくヒューリスティックな推論を用いて型情報を取得しており、クラスに定義されたメソッドをそのレシーバーから辿ることによってこれを実現しています。 この方法でも行数が10万行以上のRailsプロジェクトであれば平均10%〜20%で型情報を得られるそうです。 Duck Typingを利用して振る舞いから型を推論する部分はRubyらしさを感じるとともにユニークで興味深いなと思いました。 まだできたばかりでパフォーマンスには向上の余地があるそうで、このアプローチでどのようにこれらを解決していくのか動向を追ってみたいなと思いました。 少し話は逸れますが、型検査ツールのパフォーマンス改善という話でふと去年の関西Ruby会議で聞いたpockeさんのこのキーノートを思い出しました。 speakerdeck.com Steepのメモリ削減のために自作のプロファイラで観測してプロセス間通信のレベルまで踏み込んで改善に取り組んだ過程がとても面白かったので、よかったらこちらもチェックしてみてください。 Ecosystem on parse.y 発表 : S.H.さん S.H.さんが開発されているparse.yを利用したGemであるkanayagoとigata、そしてこれらを組み合わせたエコシステムのお話 parse.yとはRubyの文法定義ファイルのことで、昨今のRubyist界隈を取り巻くパーサームーブメントを語る上で欠かせないものです。 parse.yはRubyコミッターのydahさんの以下のスライドがparse.y周辺の情報やパーサー自体についても丁寧に解説されていて理解しやすいです たのしいparse.y - Speaker Deck S.Hさんはこのparse.yを利用して、パースしたAST(抽象構文木)ノードをRubyクラスとして扱うことができるkanayagoを開発されました。 github.com ASTをRubyオブジェクトにすることで従来よりももっと手軽にASTを扱えるようになり、パターンマッチングにも対応可能になっています。 これによってコード解析・構文チェックなどといったRubyの文法のASTに関する操作がとても直感的・簡単に行えるようになっています。 またS.Hさんはこのkanayagoをベースとしてigataとkanayago-lspを開発されました。 github.com igataはkanayagoが生成するRubyクラス名、メソッド名、引数の情報などを抽出してMinitestやRSpecに対応したテストファイルを自動作成します。 これに加えてLLMと組み合わせて、テストの中身まで自動補完させる運用も想定されています。 kanayago-lspはkanayagoを用いてVSCodeやVimなどで利用でき、Rubyの新しいパーサーであるPrismでは検知できないものも検知できるそうです。 S.Hさんはこのparse.y、これを利用したkanayagoを利用したエコシステムの構築を目指しており今後はLinterの開発を目指しているそうです。 S.HさんのASTに使われる各ノードを体系立った独自のRubyクラス群にラップするというアイデアがとても素晴らしいなと思いました。 parse.yから生成できるノードは100種類以上あるのでかなり根気がいる作業だと思いましたが、ここは生成AIをうまく活用されたそうです。 Ruby3.4からデフォルトパーサーがparse.yから生成されるパーサーからPrismに置き換わりましたがparse.yの根強い人気というか盛り上がりを感じました。 先ほど紹介したTypeGuessrもそうなのですが、何かを作るにあたりよりRubyらしさを追求するアプローチをみなさんされていて、このマインドはRubyistに通じるものなのかなと感じていました。 これらは開発のお手伝いをしてくれる方を絶賛募集中とのことです。自分もとても興味があるので機会があればパッチを投げたいなと思いました。 そのためにkanayago自体も積極的に使ってみたいと思います。 これらの他にもこの日のセッションはPicoRuby、VM、Deep Researchなど、とてもバラエティに富んでいて非常に面白かったです。 まとめ 福岡Rubyist会議に参加したのは初めてだったのですが、自分は人との距離が近い地域Ruby会議が好きなので今回行くことができて良かったなと思いました。 セッション終了後にも以前からオンラインでお世話になっていたコミュニティの方や、現地コミュニティの方々とも交流できたのも非常に嬉しかったです。 ぜひまた次回も参加したいと思います!
はじめに こんにちは、クラウドエース株式会社 第一開発部の喜村です。 「自分だけのアプリを作ってみたいけど、時間が足りない」——エンジニアなら誰しも一度は感じたことがあるのではないでしょうか。業務で培った技術力はあっても、個人開発となると要件定義からデザイン、実装、デプロイまでをすべて一人でこなす必要があり、なかなかハードルが高いものです。 しかし近年、AI ツールの進化は目覚ましく、個人開発を取り巻く環境は大きく変わりました。本記事では、Gemini・Google AI Studio・Antigravity といった AI ツールを活用し、企画からデプロイまでを効率的に進めた個人開
動画
該当するコンテンツが見つかりませんでした








