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

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

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

# ＡＩエージェント
　これは先月、人工知能学会で理研の中川先生が言っていたことであるが、要するに人がＡＩに代わる部分をＡＩアバターと呼ぶと、その先のＡＩとＡＩ同士のやり取りは、ＡＩエージェントが担うだろうという事である。
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同士が確認しあって考えるという部分であり、たとえば
* 「Ａさんが、こんな指示を出してきたけど、どういう返事をしたらいいかな」
* 「前に、こんな指示があったので、Ｂさんにも同じ返信の仕方をしようか~」
などなど、ユーザーのプロンプトに対して対処の仕方を、ＡＩ同士が情報共有をしあって決めるような仕組みである。

　現時点では、これらは膨大な情報になってくるので、手続き的にはバックエンドのデータベースに記録を付けて、その条件を取り出して判定するという、ある種のプログラム的な仕方が現実的だと思える。
　
# TODOリストの場合
　TODOリストの場合、ＡＩが提案してきた内容は、100%ユーザーにとってBESTなものとは限らないので、どういうものが必要なのかは、人間側で判断して、逆にＡＩに教えてあげる必要がある。この部分はＡＩアバターと人間の連携になるので、どういう趣向なのかは、現時点では100%人間が責任をもって調教する義務が生じる。
　もちろん理想論なので、現時点では単純ながら

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

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

# Function Calling
　　https://note.com/kaku33/n/nb6bedcf6ce41
　問い合わせ内容でプログラミング側の処理を分岐させる機能は、すでにCHATGPTで実装されている。今回はさらにfunction callingをプログラム的に拡張することになるが、自ずとメンテナンス性は低くなるだろう。かと言って、自動的にＡＩ側がプログラムを書き換えることも、現時点ではやりたくない。
　本来はこの判断も、ＡＩエージェントが判断していくべきで、そうなると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のリストを表現します。

```json-doc
{
    "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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/237e9ae6-c052-cbc7-3baf-d7b2ad5c203e.png)


成功例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/a102a7b6-3da3-0976-a14f-60db72698b00.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/53b661fc-4d6b-e269-e412-074d51244a6b.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/63754f1e-6660-839f-4999-bd0756b15b67.png)

# 残すところ

いくつかＵＩの改善が必要

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

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

# 最後に

　明日からキャンプに行ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/20b5489b-d1f8-f58b-2e7b-595332a2ff6a.png)

https://www.youtube.com/watch?v=UwkmoHgTGwE&t=2s
# 参考
- Redmineに関連する記事
1. [仕事で使うREMINEに関する考察（１９）](https://qiita.com/EasyCording/items/6ca2cb52a654df882598) 
2. [WIKI一発記入　仕事で使うREDMINEに関する考察(18)](https://qiita.com/EasyCording/items/a7d6adc1ab5b1d41a95f) 
3. [CHATGPTにてRedmine view customizerのHTML要素を生成する(4)](https://qiita.com/EasyCording/items/e04b73d185bb406cdf7a) 
4. [チケット運用の盲点　仕事で使うREDMINEに関する考察(17)](https://qiita.com/EasyCording/items/f1a802a1b8c334c34828) 
5. [CHATGPTにてRedmine view customizerのHTML要素を生成する(3)](https://qiita.com/EasyCording/items/752332e2883ce666460c) 
6. [CHATGPTにてRedmine view customizerのHTML要素を生成する(2)](https://qiita.com/EasyCording/items/90e2708aadcd3c1ae917) 
7. [CHATGPTにてRedmine view customizerのHTML要素を生成する(1)](https://qiita.com/EasyCording/items/6740c3ba7117ee18c2a9) 
8. [CHATGPT便りの開発方針　仕事で使うREDMINEに関する考察(16)](https://qiita.com/EasyCording/items/cab4dbb6c36820727f69) 
9. [チケットからTODO作業へ落とし込み　仕事で使うREDMINEに関する考察(15)](https://qiita.com/EasyCording/items/6fe972110b39117b91ae) 
10. [仕事で使うREDMINEに関する考察(14)](https://qiita.com/EasyCording/items/a76b7fe02f8d5ecefe5e) 
11. [CHATGPT利活用　仕事で使うREDMINEに関する考察(12)](https://qiita.com/EasyCording/items/857b7cf12b7dd091ff82) 
12. [やりたいことを少しずつ　仕事で使うREDMINEに関する考察(11)](https://qiita.com/EasyCording/items/b9d939a861c06bb57f18) 
13. [組織を巻き込むプレゼン資料　仕事で使うREDMINEに関する考察(10)](https://qiita.com/EasyCording/items/605a860e5a4d66011747) 
14. [OfficeのフローにREDMINEをねじ込む　仕事で使うREDMINEに関する考察(9)](https://qiita.com/EasyCording/items/e63cbe486b08f99e1702) 
15. [RedmineチケットにCHATGPTを実装（超簡単）](https://qiita.com/EasyCording/items/20439251ad82d492d654) 
16. [RedmineのチケットにCHATGPTを実装（２）](https://qiita.com/EasyCording/items/3df63dcbb7e1f9f2d14e) 
17. [Office365からRedmineへのメール送信してチケット登録](https://qiita.com/EasyCording/items/3176b4020954e2eddbb6) 
18. [Redmineプラグイン開発](https://qiita.com/EasyCording/items/5e837a3425d3df8bfb0c) 
19. [RedmineをTODOリストに使う　仕事で使うREDMINEに関する考察(8)](https://qiita.com/EasyCording/items/bbf470b09a08d06b87f3) 
20. [仕事で使うREDMINEに関する考察(7)](https://qiita.com/EasyCording/items/3260595d70a86c21308e) 
21. [仕事で使うREDMINEに関する考察(6)](https://qiita.com/EasyCording/items/c2c3a5850941f45d1317) 
22. [仕事で使うREDMINEに関する考察(5)](https://qiita.com/EasyCording/items/f46ce0718f005f8b4d96) 
23. [仕事で使うREDMINEに関する考察(4)](https://qiita.com/EasyCording/items/e2f6fffde3d8e0a0012b) 
24. [仕事で使うREDMINEに関する考察(3)](https://qiita.com/EasyCording/items/44dd27c6b897b0d7d82b) 
25. [仕事で使うREDMINEに関する考察(2)](https://qiita.com/EasyCording/items/7f8fbba0553635a7421d) 
26. [仕事で使うREDMINEに関する考察](https://qiita.com/EasyCording/items/c3b62c9e5ca3f0cc379f)