チケットからTODO作業へ落とし込み　仕事で使うREDMINEに関する考察(15)

https://qiita.com/EasyCording/items/a76b7fe02f8d5ecefe5e


REDMINEからCHATGPTを使ってうんぬん・・・
なんて書こうとしたけど、まだCHATGPT-APIの粋な使い方には達していない

その前に、REDMINEが今後もOSSの核となることを書いている。

# お題
　今日のお題はこれまで検討を進めているチケットから、どうやって実際の作業に進めていこうかと言うところである。
　普通はチケットと言うのは、ざっくり言えば目的だったり目標だったりして、それは達成イメージに過ぎない。実際にチケットを実行しようとすると、その実行方法は自分自身で考えないといけなかったりする。

　ここで出番となるのは、CHATGPTさんの出番である。

# ユースケース（ 以下はCHATGPT）

例えば、「ソロキャンプの実行」というチケットがあったとする。
当然のことであるが、メモ欄にはあらかじめの決め事は書いてあり、あとは実行するだけという状態の場合。
・行先　Ｍ高原
・人数　５人
・参加費　２０００円
・自動車（ガソリン、費用、地図、諸経費）

以下はCHATGPT

>「ソロキャンプの実行」というチケットがあります。あらかじめの決め事は書いてあり、あとは実行するだけです。実行計画の内容を精査し、足りない内容は追加し（理由も書いて下さい）、最後は実行時のチェックリストという形で、□付きでリストアップしてください
・行先　Ｍ高原
・人数　５人
・参加費　２０００円
・自動車（ガソリン、費用、地図、諸経費）

### ソロキャンプの実行計画に関する精査を行ったところ、以下のような項目が足りていないと考えられます。

* キャンプサイトの予約
理由：人数が5人で行くため、事前に予約をしておかないと、人数分のスペースを確保できない可能性があります。

* 食料や水の準備
理由：行先までの移動中や現地で調達できるものはあるかもしれませんが、基本的には自己完結型の旅行なので、食料や水の確保は重要です。

* 着替えや洗浄用具の準備
理由：脱衣所やシャワーがある場合は問題ありませんが、キャンプ場によっては利用できない場合があります。そのため、着替えや洗浄用具を準備しておく必要があります。

* テント設営に必要な道具
理由：テントを設営するために必要な道具が不足している場合は、設営の際にトラブルが発生する可能性があります。

### 以上の点を踏まえ、実行計画の詳細に以下のような情報が必要です。

* キャンプサイトの予約状況（予約番号や確認書類）
* 食料や水の購入先や準備量
* 着替えや洗浄用具の内容
* テント設営に必要な道具のリスト

### 最後に、実行時のチェックリストは以下のようになります。

□キャンプサイトの予約確認書類
□飲料水
□食料（朝食、昼食、夕食）
□着替え
□洗浄用具（タオル、石鹸、シャンプー）
□テント
□テント用のペグ・ロープ
□テントを建てるためのハンマーなどのツール
□寝袋
□枕
□キャンプチェアまたは座布団
□タープやその他の雨具
□燃料（バーナー、ガス缶、キャンドル、着火材）
□料理器具（フライパン、包丁、まな板、箸、お皿、カップ、スプーン、フォーク）
□クーラーボックスまたは保冷剤
□携帯電話充電器

# 次のステップ

こんな感じでチケットからプロンプトを作ければチェックリストまではすんなり完成する。

お次は、これをTODOリストとして、作業時間に一つづつ割り当てる。この際はまだ未実施なので、カスタムフィールドで未実施を割り当てればよいだろう。

そんな感じで、要件定義は固まったので、あとはシステム構想である。

# システム構想

　今回はチケットを生成させるというところが肝になってくる。もちろん自身はREDMINEプラグインの開発スキルは無い（勉強する気もない）ので、PYTHONでバックエンドを作ることが一番手っ取り早いし、今後の導入やメンテナンス（人材教育含める）にも便利である。
　少しPYTHONのバックエンド構築にあたり、自宅のNAS、もしくは会社のＰＣ、AWSサーバーなど、立てる場所で悩んでいたのであるが、VERCELでタダでPYTHONサーバーが立てられるというので、ここが真っ先に使いやすいと考えた。
　OPEN-APIのトークンは、さすがにVERCELに乗せるのは気が引けるので、POST時にREDMINEからOPEN-APIのTOKENとREDMINEのユーザーTOKENを渡して、POSTすると言うのが良さそうである。
　
　OPEN-APIのTOKENについては、共有グループで管理しても良いが、今のところはユーザーと紐付けするために、ユーザー情報にカスタムフィールドを作るのが良さそうだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/12ce8f39-e8b9-0e58-7672-570fe8ad07f3.png)

# 実装

　ひとまずFASTAPIでエンドポイントを書いてみた。
今回はfunction callingを使っている。

　プロンプトの埋め込みをするため、リクエストにはsubject,description,numberを宣言している。
やはりプロンプトは前後関係の文脈が大事だ。あと数値限定を入れることで、検討項目に幅を持たせた回答が期待できる。

```main.py
from fastapi import FastAPI
import openai
import json

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# OpenAI APIの設定
openai.api_key = "sk-xxxxxxxxxxxxxxxxxxxx"

# ChatGPTのエンジンIDを設定
engine_id =  "gpt-3.5-turbo-0301"

functions=[
            {
                "name": "get_todo_list",
                "description": "実行時のチェックリスト",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "items": {
                            "type": "array",
                            "items": {"type": "string"},
                            "description": "分かり易く関係にTODOをリストで返すこと"
                        }
                    },
                    "required": ["items"]
                }
            }
        ]

class TicketRequest(BaseModel):
    subject: str
    description: str 
    number: int

@app.post("/todo")
async def create_ticket(ticket_request: TicketRequest):

    # textをChatGPTで解析して、intent（意図）を取得
    todo = await get_intent_from_text(ticket_request)
    
    if not todo == None:

        # TODOリストを生成
        todo_list = generate_todo_list_from_ticket(todo)

        return {"message": "TODOリストを生成して保存しました。","list":todo_list}

    else:
        return {"message": "対応するintentが見つかりませんでした。"}


async def get_intent_from_text(ticket_req: TicketRequest) -> str:
   
# テンプレート文字列の生成
    template_str = """「{}」というチケットがあります。
    あらかじめの決め事は書いてあり、あとは実行するだけです。
    実行計画の内容の不足点を{}個以上指摘し、その理由と共に実行時のチェックリストという形でリストアップしてください\n
    {}"""

# テンプレート文字列に変数を埋め込む
    message = template_str.format(ticket_req.subject, ticket_req.number,ticket_req.description)
    
    print(message)

    response = openai.ChatCompletion.create(
  model="gpt-3.5-turbo-0613",
functions=functions,function_call="auto",
  messages=[
        {"role": "user", "content": message}
    ]
)

    function_call = response['choices'][0]['message']['function_call']
    if function_call and function_call['name'] == 'get_todo_list':
        todo_list = json.loads(function_call['arguments'])['items']
        return todo_list
    else:
        return None

def generate_todo_list_from_ticket(todo):

    # TODOリストを生成して返す
    todo_list = {"items": todo}
    return todo_list
```

# TEST

動かした結果はこんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/19e86eb8-a7ce-b72b-ad23-1ef5579fa350.png)

申し訳ないぐらい、考えてくれる・・・（ありがたや）

```json
{
  "message": "TODOリストを生成して保存しました。",
  "list": {
    "items": [
      "ガソリン補給",
      "費用の計算",
      "地図の確認",
      "諸経費の考慮",
      "テントの準備",
      "寝袋の準備",
      "マットの準備",
      "キャンプ用品の準備",
      "食事の準備",
      "飲み物の準備",
      "焚き火の準備",
      "トイレの準備",
      "シャワーの準備",
      "防寒対策",
      "日焼け対策",
      "虫除け対策",
      "快適な服装の準備",
      "照明の準備",
      "携帯電話の充電",
      "防災グッズの準備",
      "緊急連絡先の確認",
      "テント設営の方法確認",
      "火の取り扱い方法の確認",
      "トイレの場所確認",
      "シャワーの場所確認",
      "天気予報のチェック",
      "周辺施設の確認",
      "食材の購入",
      "食材の保冷",
      "ゴミの捨て方の確認",
      "レジャーシートの準備",
      "レジャーシートの保管場所の確認"
    ]
  }
}
```