PaLM 2 APIを使ったタスク分解ツールを作ってみた

## はじめに

アドベントカレンダー初日(12/1)。
何を血迷ったのか、11月の半ばを過ぎたくらいに同じチームのKさんに誘われ、勢いとノリだけで初日に参加してしまいました。。。
さしあたって今流行りの生成AIについて書いていこうと思います。

## 半自動×自作TODOリストへの思い

皆さんはタスク管理に何を使っていますか？

遡れば数年前――初めて技術ブログに投稿したころは、自作のTODOリストを使ってタスクを管理するようにしていました。(そのころ「はてぶ」に投稿した記事も確か自作TODOリストに関する内容だったと記憶しています。)

ただしあくまでそれは当時の話です。年次が上がるにつれ粒度の大きいタスクに着手する機会が増えたのですが、その時使っていた自作のTODOリストには「粒度の細かい子タスクをつくる」機能がありませんでした。結局ブームは過ぎ去り、Macに標準装備されているメモ帳アプリでのタスク管理生活にもどったのでした。

しかし今の時代「生成AI」があります。
「子タスクを作る機能」なら当然作れる。なんなら「子タスクに分割することも自動化できるんじゃないか」と、思い立ったので実際に作ってみることにしました。

## いざ実装

細かい実装については省きます。
(また、今回は「入力したタスク・見積りに基づいて、配下に分割した子タスクを生成する機能」に限って実装していきます。)

### 下準備

今回はGoogleの提供しているVertexAIのテキスト生成のAPIを使っていきます。
https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text?hl=ja#stream_response_from_generative_ai_models
(下準備として、[こちらの作業](https://cloud.google.com/vertex-ai/docs/start/cloud-environment?hl=ja)が必要となります※課金を有効にする必要あり)

あとは、最近使っているFlutterのプロジェクトで開発していきます。今回はお試しですが、公式が[ライブラリ](https://cloud.google.com/vertex-ai/docs/start/client-libraries?hl=ja#node.js)を提供しているそうなので、本格的に実装するなら別の言語をお勧めします)
```powershell
flutter create <project-name>
```

※一応調べたらDartのライブラリも用意されていました。
https://pub.dev/packages/gcloud

### 開発

↓サンプルコード(一部)
```dart:repository.dart
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<String?> postMessage(String message) async {
  final url = Uri.parse(
"https://$API_ENDPOINT/v1/projects/$PROJECT_ID/locations/$lOCATION_ID/publishers/google/models/$MODEL_ID:predict");

  final response = await http.post(
    url,
    headers: {
      "Authorization": "Bearer ${await getAccessToken()}",
      "Content-Type": "application/json",
    },
    body: '''{
      "instances": [
        {"content": "$message"}
      ],
      "parameters": {
        "candidateCount": 1,
        "maxOutputTokens": 1024,
        "temperature": 0.2,
        "topP": 0.8,
        "topK": 40
      }
    } ''',
  );

  if (response.statusCode == 200) {
    print("Response: ${response.body}");
    return jsonDecode(response.body)["predictions"][0]["content"];
  } else {
    print("Error: ${response.statusCode}");
  }
}

Future<String> getAccessToken() async {
  // ここにアクセストークンを取得するためのコードを追加する
  // gcloud auth print-access-token を実行するか、他の認証メカニズムを使用する
  return "your-access-token";
}
```
今回は[http](https://pub.dev/packages/http)でお手軽に実装しました。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:palm_todo/repository.dart';
import 'package:palm_todo/utils.dart';
import 'package:palm_todo/models.dart';

void main() async {
  runApp(const TodoApp());
}

class TodoApp extends StatelessWidget {
  const TodoApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Todo App',
      home: TodoListScreen(),
    );
  }
}

class TodoListScreen extends StatefulWidget {
  const TodoListScreen({Key? key}) : super(key: key);

  @override
  _TodoListScreenState createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  final List<TodoItem> todos = [];
  int selectedQuotation = 1;

  TextEditingController todoController = TextEditingController();

 // Todoアイテムを追加するメソッド
  void addTodo() async {
    // テキストから引用を含んだメッセージを生成
    final message = getTodoSplitMessage(todoController.text, selectedQuotation);
    // メッセージを送信し、分割されたTodoアイテムを取得
    final response = await postMessage(message);
    final splittedTodos = splitLines(response!);

    // Todoアイテムをリストに追加し、入力欄をクリア
    todos.add(TodoItem(todo: todoController.text, subTodos: splittedTodos));
    todoController.clear();

    // UIを更新
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo List'),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                final todoItem = todos[index];

                return Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    ListTile(
                      title: Text(todoItem.todo),
                    ),
                    if (todoItem.subTodos.isNotEmpty)
                      Padding(
                        padding: const EdgeInsets.only(left: 16.0), // インデント
                        child: Column(
                          children: [
                            for (String subTodo in todoItem.subTodos)
                              ListTile(
                                title: Text(subTodo),
                              ),
                          ],
                        ),
                      ),
                  ],
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: todoController,
                    decoration: const InputDecoration(
                      hintText: 'Enter your todo',
                    ),
                  ),
                ),
                DropdownButton<int>(
                  value: selectedQuotation,
                  onChanged: (int? newValue) {
                    selectedQuotation = newValue!;
                    setState(() {});
                  },
                  items: <int>[1, 3, 5, 8, 13]
                      .map<DropdownMenuItem<int>>((int value) {
                    return DropdownMenuItem<int>(
                      value: value,
                      child: Text('$value'), 
                    );
                  }).toList(),
                ),
                IconButton(
                  icon: const Icon(Icons.add),
                  onPressed: addTodo,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

```

↓画面(Chrome)
![スクリーンショット 2023-11-30 214714.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651438/9079a6ec-e1d9-ad9c-de7c-32e7c1bb7e28.png)


## 何か入力してみる


まずは冬の定番、「おでんの作り方」について聞いてみましょう。レシピなどが見られるでしょうか。(見積りは「5」で設定してみます。)
![スクリーンショット 2023-11-30 224505.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651438/4f3aa9eb-7986-c062-74d0-916956a11c4e.png)
なるほど・・・。
大根と卵とこんにゃくのみのシンプルなおでんが完成しそうです。


続いて、「Palm APIについて完全に理解する」から子タスクを生成してみます。
![スクリーンショット 2023-11-30 225306.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651438/7d91b5e3-6f8f-7bdd-32ef-811bdbe12975.png)
やはり、子タスクが生成されるまでに少しラグがあります(体感2,3秒くらい)

最後に「Flutter,PalmAPIを使ったTODOリストを作成する」方法について伺ってみます。
![スクリーンショット 2023-11-30 230453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651438/33ddf852-90b1-eac5-4fb2-ad04f6bb9a9f.png)
簡単な確認ですが、タスクをいい感じに分割してくれているようです。

## まとめ
ツールに組み込むのは初めてでしたが、非常にお手軽に試せるところが利点だと思いました。
(子タスクに多様性をもたせたい場合は、temperatureの数値を１に近づけることでより創造的な答えが返ってくるようです。)
これでタスク管理を充実させていきたいですね。みなさまもぜひお試しください！