1
2

CHATGPTにてRedmine view customizerのHTML要素を生成する(3)

Last updated at Posted at 2023-07-14

3回目の検討という事で、流石に今回で本件を終了したいと考えている。

 今回の取り組みは、CHATGPTでHTMLを返すという部分である。
プロンプトの延長で、HTMLを返せ・・・と書きたいところであるが、本来はユーザーでやることでは無くて、完全にAIエージェントが担当すべき領域である。

AIエージェント

 これは先月、人工知能学会で理研の中川先生が言っていたことであるが、要するに人がAIに代わる部分をAIアバターと呼ぶと、その先のAIとAI同士のやり取りは、AIエージェントが担うだろうという事である。
https://confit.atlas.jp/guide/event/jsai2023/subject/2B5-TS-1-01/advanced
https://speakerdeck.com/hiroshinakagawa/ren-gong-zhi-neng-xue-hui-da-hui-2023-ainiokerutorasuto

AIエージェントとは何か?
AIエージェントは、自律的に行動し、環境内で特定の目標を達成する能力を持つAIシステムを指します。これらのエージェントは、情報を収集し、解析し、理解し、そしてその結果に基づいて行動を決定します。
AIエージェントがAI同士のやり取りをどのように担当するか?
AIエージェントは、他のAIとのコミュニケーションを管理し、調整する役割を果たすことができます。例えば、エージェントは異なるAIシステム間でデータを共有したり、タスクを配分したり、結果を調整したりすることができます。
このように、AIエージェントがAI同士のやり取りを担当することで、より効率的かつ効果的なAIシステムの協働が可能になると考えられます。

要件定義

 要するに今回の場合は、TODOリストを作って、と言うような指示が出た場合に、AIが人間にどんな返事はしたらよいかを、AI同士が確認しあって考えるという部分であり、たとえば

  • 「Aさんが、こんな指示を出してきたけど、どういう返事をしたらいいかな」
  • 「前に、こんな指示があったので、Bさんにも同じ返信の仕方をしようか~」
    などなど、ユーザーのプロンプトに対して対処の仕方を、AI同士が情報共有をしあって決めるような仕組みである。

 現時点では、これらは膨大な情報になってくるので、手続き的にはバックエンドのデータベースに記録を付けて、その条件を取り出して判定するという、ある種のプログラム的な仕方が現実的だと思える。
 

TODOリストの場合

 TODOリストの場合、AIが提案してきた内容は、100%ユーザーにとってBESTなものとは限らないので、どういうものが必要なのかは、人間側で判断して、逆にAIに教えてあげる必要がある。この部分はAIアバターと人間の連携になるので、どういう趣向なのかは、現時点では100%人間が責任をもって調教する義務が生じる。
 もちろん理想論なので、現時点では単純ながら

  • AI提案は実行前に、必ず人間が確認する
    → 結局は、TODOリストのようなものを表示させて、これに対するアクションを人間が行う

  • チケット登録も、プロンプトで明示して実施する
    → ここは将来的には、HTMLボタン付きのFORMを生成して、エージェント的な気の利かせ方をしてほしい

Function Calling

  https://note.com/kaku33/n/nb6bedcf6ce41
 問い合わせ内容でプログラミング側の処理を分岐させる機能は、すでにCHATGPTで実装されている。今回はさらにfunction callingをプログラム的に拡張することになるが、自ずとメンテナンス性は低くなるだろう。かと言って、自動的にAI側がプログラムを書き換えることも、現時点ではやりたくない。
 本来はこの判断も、AIエージェントが判断していくべきで、そうなるとAPIサーバー内部に、別途エージェントサーバー機能を入れなければならず、最終式にはかなり大規模になるだろう。そのあたりはREDMINEサーバー管理者と、AIエージェント間の仕様締結契約みたいな? 訳の分からない世界に突入してしまう・・・

あなたが言及しているのは、AIエージェントがより自律的に動作するシステムの設計についてのようです。
具体的には、AIエージェントが自身で判断を行い、それに基づいて適切な処理や応答を返す能力を持つことを指しています。これを実現するためには、APIサーバー内部にエージェントサーバー機能(AIエージェントが活動するための環境またはフレームワーク)を設ける必要があると述べています。
しかし、このようなシステムは複雑さを増すため、大規模な開発と管理が必要となります。特に、AIエージェントの行動ルールやインタフェースの定義(「REDMINEサーバー管理者とAIエージェント間の仕様締結契約」に相当)は非常に難易度が高い作業となります。

実装

 四の五の言わずに早くやれ

 そう言う声が出てきたので、今回は簡易実装で終了する。

RESPONSE

 よくよく考えたのであるが、APIから返されたHTML要素を、Redmine内で表示させるには、最初にRedmine view custmizer自体を変更して、HTMLが返されたら、それをベースにオブジェクトを生成する機能が必要である。
 表題の目的からすると、そこは突破すべきなのであるが、今回はTEXT要素を返して、要素を手でグリグリ修正して、最後にチケット登録してくださいというプロンプトをCHATGPTに投げるという、すこし、まどろっこしい結論で終わりそうである。

サーバー側の処理

最初にfunction callingを2つ宣言している。

get_todo_list →  "TODOリストを作成する"
add_todo" →    "ToDoのリストをredmineに登録する",

これらをプロンプト内容から判断するというから、不確実性がこの上ない・・・

このコードは基本的に2つの関数を定義しています。それぞれの関数は特定のタスクを実行します。
get_todo_list
これは、TODOリストを作成する関数です。パラメータとして、オブジェクトが必要で、そのプロパティとしてitems配列(文字列型の要素)が必要とされています。itemsはTODOのリストを表現します。

{
    "name": "get_todo_list",
    "description": "TODOリストを作成する",
    "parameters": {
        "type": "object",
        "properties": {
            "items": {
                "type": "array",
                "items": {"type": "string"},
                "description": "TODOのリスト"
            }
        },
        "required": ["items"]
    }
}

add_todo
これは、TODOのリストをredmineに登録する関数です。パラメータとして、オブジェクトが必要で、そのプロパティとしてtodo_list配列(文字列型の要素)が必要とされています。todo_listは登録したいToDoのリストを表現します。

{
    "name": "add_todo",
    "description": "ToDoのリストをredmineに登録する",
    "parameters": {
        "type": "object",
        "properties": {
            "todo_list": {
                "type": "array",
                "items": {"type": "string"},
                "description": "ToDoのリスト"
            }
        },
        "required": ["todo_list"]
    }
}

それぞれの関数は特定のタスクを実行し、それぞれに必要なパラメータが明確に定義されています。これはAPIの定義やドキュメンテーションを作成する際に非常に役立ちます。

Redmine view custmizer

chatField にプロンプトを入れると、リストが帰ってきたら、テキストを追加することにしている
→ 本来はここでオブジェクトを出させて、ゴニョゴニョ考えていたが、今回は時間が無いので割愛する。

js.js
<textarea id="chatField" name="chatField" style="
    height: 52px;
    width: 516px;
"></textarea>

    <button id="chatButton">Click me!</button>
  <textarea id="resField" name="restField" style="
    height: 52px;
    width: 516px;
"></textarea>

<script>
$(function() {
    const chatField = $('#chatField');
    const resField = $('#resField');
    const chatButton = $('#chatButton');
    const url = "http://pypy2:5001/redmine";
    const form = document.getElementById("issue-form");

    chatButton.on('click', async (event) => {
        event.preventDefault();

        try {           
            var contextData = JSON.stringify(ViewCustomize.context);
            const formData = new FormData(form);
            formData.append('contextData', contextData);
            formData.append('chatText', chatField.val());

            const response = await fetch(url, { method: "POST", body: formData });
            const res = await response.json();

            if (Array.isArray(res)) {
                let formattedRes = res.join('\n');
                $('#chatField').val($('#chatField').val() + '\n' + formattedRes);
            } else {
                $('#chatField').val($('#chatField').val() + '\n' + 'res is not an array');
            }
        } catch (error) {
            console.error('Error:', error);
        }
    }); // Closing for chatButton.on()
}); // Closing for $(function() {})
 
</script>
main.py
from fastapi import FastAPI, Request
import openai
import json
import pdb
from redmine_api import RedmineWork

app = FastAPI()

# OpenAI APIの設定
openai.api_key = 

redmine_url = "http://redmine:3000/"

functions = [
    {
        "name": "get_todo_list",
        "description": "TODOリストを作成する",
        "parameters": {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "TODOのリスト"
                }
            },
            "required": ["items"]
        }
    },
    {
        "name": "add_todo",
        "description": "ToDoのリストをredmineに登録する",
        "parameters": {
            "type": "object",
            "properties": {
                "todo_list": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "ToDoのリスト"
                }
            },
            "required": ["todo_list"]
        }
    }
]


@app.post("/redmine")
async def post_handler(request: Request):
    form = await request.form()
    contextData = form['contextData']
    data = json.loads(contextData)
    openai_token = None

    for field in data['user']['customFields']:
        if field['name'] == 'chatgpt-token':
            openai.api_key = field['value']
        break

    text = """  以下のデータからchatFieldを読み取リ、指示を実施してください。
                issue[subject]はタイトルです
                issue[description]は詳細です
                --------------------
            """

    for field in form:
        value = form[field]
        text += f"{field}: {value}\n"

    print(text)

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

    print(response['choices'][0]['message']['function_call']['name'])

    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

    elif function_call and function_call['name'] == 'add_todo':
        items = json.loads(function_call['arguments'])["todo_list"]
        
        redmine = RedmineWork(redmine_url)
        redmine_token = data["user"]["apiKey"]
        issue_id = data['issue']['id']
        redmine.apiLogin(redmine_token)
        response = redmine.add_todo(items=items,ticket_id=issue_id)
        
        return response

    else:
        return None

チケット追加処理 Redmine-api

チケットのactivity_idは、省略可能(ただ設定側でデフォルト値をチェックしておくこと)

Redmine-api.py
import datetime as datetime
from redminelib import Redmine
import random, string
from datetime import datetime

class RedmineWork:
    def __init__(self, url):
        self.url = url

    def apiLogin(self, api):
        self.api = Redmine(self.url, key=api) 

    def add_todo(self,items,ticket_id):
        for i in items:
            time_entry = self.api.time_entry.new()
            time_entry.spent_on=datetime.now().date()
            time_entry.hours=1
            time_entry.issue_id = ticket_id
            time_entry.comments = i
            time_entry.save()
    

実行結果

実にあいまいで、何回かトライしないと出ない場合が多い。

失敗例
image.png

成功例
image.png

プロンプトをredmine登録に変える

image.png

登録が終わると、このようにチェックリストが作業時間(Time_entry)として完成する。

image.png

残すところ

いくつかUIの改善が必要

  • CHATGPTのThinking中の表示
  • CHATGPTのレスポンスをもう少し親切に
  • プロンプトの2回目を促すようなAI側からのアドバイス

 イロイロ思いつくことが沢山あるが、こうやって仕組みから実装していくと、どんどんコードとプロンプトの境界線が見えなくなってきて、ますます泥沼にはまっていく

最後に

 明日からキャンプに行ってきます。
image.png

参考

  • Redmineに関連する記事
  1. 仕事で使うREMINEに関する考察(19)
  2. WIKI一発記入 仕事で使うREDMINEに関する考察(18)
  3. CHATGPTにてRedmine view customizerのHTML要素を生成する(4)
  4. チケット運用の盲点 仕事で使うREDMINEに関する考察(17)
  5. CHATGPTにてRedmine view customizerのHTML要素を生成する(3)
  6. CHATGPTにてRedmine view customizerのHTML要素を生成する(2)
  7. CHATGPTにてRedmine view customizerのHTML要素を生成する(1)
  8. CHATGPT便りの開発方針 仕事で使うREDMINEに関する考察(16)
  9. チケットからTODO作業へ落とし込み 仕事で使うREDMINEに関する考察(15)
  10. 仕事で使うREDMINEに関する考察(14)
  11. CHATGPT利活用 仕事で使うREDMINEに関する考察(12)
  12. やりたいことを少しずつ 仕事で使うREDMINEに関する考察(11)
  13. 組織を巻き込むプレゼン資料 仕事で使うREDMINEに関する考察(10)
  14. OfficeのフローにREDMINEをねじ込む 仕事で使うREDMINEに関する考察(9)
  15. RedmineチケットにCHATGPTを実装(超簡単)
  16. RedmineのチケットにCHATGPTを実装(2)
  17. Office365からRedmineへのメール送信してチケット登録
  18. Redmineプラグイン開発
  19. RedmineをTODOリストに使う 仕事で使うREDMINEに関する考察(8)
  20. 仕事で使うREDMINEに関する考察(7)
  21. 仕事で使うREDMINEに関する考察(6)
  22. 仕事で使うREDMINEに関する考察(5)
  23. 仕事で使うREDMINEに関する考察(4)
  24. 仕事で使うREDMINEに関する考察(3)
  25. 仕事で使うREDMINEに関する考察(2)
  26. 仕事で使うREDMINEに関する考察
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2