> [!caution]
>
> この[[ノート]]は自身の備忘録みたいなものであり、やったことのない人が書かれている内容をなぞると[[OpenAI API]]で何か作れるようになるものではないで注意。
## [[Python]]で[[OpenAI API]]を使ってパースしてみる
雑に実装してみた。
````python
import os
import openai
import json
import sys
from typing import List
from dataclasses import dataclass
openai.api_key = os.getenv("OPENAI_API_KEY")
@dataclass(frozen=True)
class WorkSchedule:
employee_name: str
begin_time: str
end_time: str
days: List[str]
@dataclass(frozen=True)
class Company:
name: str
work_schedules: List[WorkSchedule]
def question(input: str) -> Company:
response = openai.Completion.create(
model="text-davinci-003",
prompt=f"""以下をJSONに変換してください。
```
{input}
```
以下はJSONの定義です。
- name (文字列)
- work_schedules (配列)
- employee_name
- begin_time (HH:mm)
- end_time (HH:mm)
- days (月火水木金土日からなる配列)
""",
temperature=0,
max_tokens=512,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
)
res: dict = response # type: ignore
cost_yen = res["usage"]["total_tokens"] / 1000 * 0.02 * 132
print(f"{cost_yen}円")
text_res = res["choices"][0]["text"].replace("```json", "").replace("```", "")
print(text_res)
return Company(**json.loads(text_res))
def main():
args = args = sys.argv[1:]
input = args[0]
company = question(input)
print(company)
if __name__ == "__main__":
main()
````
ポイント。
- モデルは`text-davinci-003`
- Chat系のモデルだと冗長なコメントがついてしまう
- davinciより速度重視のモデルにすると期待する結果が得られないことが多い
- 結果にはどうしてもコードブロックが入ってしまうが、生の[[JSON]]が欲しいので、`replace`で邪魔な文字列を消す
- レスポンスは毎回同じにしたいので`temperature=0`
- 金額が気になるので`cost_yen`として出力 ($1 = 132円計算)
実行例。
```json
$ python .\main.py "[株式会社ももたけ] 太郎: 月・水・金 10:00-19:00 木 9:00-18:00。次郎: 金土日 8:00-17:00"
0.9636円
{
"name": "株式会社ももたけ",
"work_schedules": [
{
"employee_name": "太郎",
"begin_time": "10:00",
"end_time": "19:00",
"days": ["月", "火", "水", "金"]
},
{
"employee_name": "次郎",
"begin_time": "08:00",
"end_time": "17:00",
"days": ["金", "土", "日"]
}
]
}
Company(name='株式会社ももたけ', work_schedules=[{'employee_name': '太郎', 'begin_time': '10:00', 'end_time': '19:00', 'days': ['月', '火', '水', '金']}, {'employee_name': '次郎', 'begin_time': '08:00', 'end_time': '17:00', 'days': ['金', '土', '日']}]
```
## [[TypeScript]]でUIも含めて実装してみる
もうちょっとリアルなユースケースとして、[[GUI]]から入力し、UIに出力するWebツールを実装してみた。[[Vue3]]と[[Naive UI]]で。
### openai.ts
[[Python]]のロジックほぼそのまま。レスポンスが少し変わってるくらい。
```ts
import { Configuration, OpenAIApi } from "openai";
export interface Result {
yen: number;
answer: Company;
}
export interface Company {
name: string;
workSchedules: WorkSchedule[];
}
export interface WorkSchedule {
employeeName: string;
beginTime: string;
endTime: string;
days: string[];
}
export async function question(input: string): Promise<Result> {
const configuration = new Configuration({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
const response = await openai.createCompletion({
model: "text-davinci-003",
prompt: `以下をJSONに変換してください。
${input}
以下はJSONの定義です。
- name (文字列)
- workSchedules (配列)
- employeeName
- beginTime (HH:mm)
- endTime (HH:mm)
- days (月火水木金土日からなる配列)
`,
temperature: 0,
max_tokens: 512,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
});
const yen = (response.data.usage!.total_tokens / 1000) * 0.02 * 132;
const jsonText = response.data.choices[0]
.text!.replace(/```json/, "")
.replace(/```/, "");
return {
yen,
answer: JSON.parse(jsonText),
};
}
```
### App.vue
UI側の処理。`question`関数を除けばただのWeb実装。特に工夫もなにもない。
```html
<script setup lang="ts">
import { reactive, ref } from "vue";
import { Company, question, Result } from "./openai";
interface State {
input: string;
isLoading: boolean;
result: Result | null;
}
const state = reactive<State>({
input: "",
isLoading: false,
result: null,
});
const handleAICompletion = async () => {
state.isLoading = true;
state.result = await question(state.input);
state.isLoading = false;
};
const DAYS_OPTIONS = [
{ label: "月", value: "月" },
{ label: "火", value: "火" },
{ label: "水", value: "水" },
{ label: "木", value: "木" },
{ label: "金", value: "金" },
{ label: "土", value: "土" },
{ label: "日", value: "日" },
];
</script>
<template>
<n-space vertical>
<n-input
v-model:value="state.input"
size="large"
round
placeholder="シフト情報を記載する"
type="textarea"
style="width: 800px; height: 180px; text-align: left"
/>
<n-button :loading="state.isLoading" type="info" @click="handleAICompletion"
>AI completion</n-button
>
<div v-if="state.result">
<n-alert title="OpenAI APIの利用料金" type="info">
{{ state.result.yen }}円 (1ドル=132円換算)
</n-alert>
<n-divider>AIによる解析結果を自動で挿入</n-divider>
<n-form
:label-width="80"
:model="state.result.answer"
style="text-align: left"
>
<n-form-item label="会社名">
<n-input v-model:value="state.result.answer.name" />
</n-form-item>
<n-space>
<template v-for="ws in state.result.answer.workSchedules">
<n-card :title="ws.employeeName">
<n-space>
<n-form-item label="出勤時間">
<n-time-picker
v-model:formatted-value="ws.beginTime"
value-format="HH:mm"
format="HH:mm"
/>
</n-form-item>
<n-form-item label="退勤時間">
<n-time-picker
v-model:formatted-value="ws.endTime"
value-format="HH:mm"
format="HH:mm"
/>
</n-form-item>
</n-space>
<n-form-item label="出勤曜日">
<n-select
v-model:value="ws.days"
multiple
:options="DAYS_OPTIONS"
/>
</n-form-item>
</n-card>
</template>
</n-space>
<n-form-item
style="display: flex; justify-content: center; margin-top: 30px"
>
<n-button type="primary" style="width: 200px"> 投稿 </n-button>
</n-form-item>
</n-form>
</div>
</n-space>
</template>
```
### main.ts
[[Naive UI]]を読み込んでいるだけ。
```ts
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import naive from "naive-ui";
createApp(App).use(naive).mount("#app");
```
結果はこんな感じ。単純な文字や箇条書きではなく、バイトでよくあるシフトテーブル形式でもほぼ正確に読み取ってくれた。(1箇所惜しい間違いはあるけど仕方ない)
![[2023-03-29_22h18_29.png|frame]]
*AIがシフト表を解釈してformにデフォルト値を挿入した図*
実行時間は5~10秒くらいだった。人間が急いで入力すればほぼ同じくらいかもしれないが、その意味は似て非なるものになる。なぜなら、人間が入力すると100%のリソースを使用するのに対し、AIを使うと自身のリソースはほぼ消耗しない。[[OpenAI API]]の返答を非同期で待っている間に別のことができるからだ。
とはいえ5秒ちょっとだと別の作業をやるには微妙な時間だ。作業の効率化で利用したいなら、脳の[[コンテキストスイッチ]]を切り替えずにできる作業が行えるよう業務フローを設計した方がいいだろう。
## 感想
AIにすべて全自動で任せるのは怖いけど、人間が入力するのに時間がかかる場所のひな型を入れてもらうのはアリだと思った。[[GitHub Copilot]]や[[Microsoft Office]]のAI機能だって本質的にやっていることは一緒だし。
## 余談
[[gpt-3.5-turbo]]だと[[text-davinci-003]]より10倍安いから推奨されていたので、[[Chat completion API]]を使って実装しなおしてみた。
```ts
export async function question(input: string): Promise<Result> {
const configuration = new Configuration({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
const response = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: `結果のJSONだけを回答してください。解説や注意は不要です。
以下をJSONに変換してください。
----
${input}
----
以下はJSONの定義です。他のプロパティは存在しません。
- name (会社名)
- workSchedules (配列)
- employeeName (従業員名)
- beginTime (例: 03:00)
- endTime (例: 19:00)
- days (月火水木金土日からなる配列)
`,
},
],
temperature: 0,
max_tokens: 512,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
});
console.log(response.data);
const yen = (response.data.usage!.total_tokens / 1000) * 0.002 * 132;
const jsonText = response.data.choices[0].message?.content!;
return {
yen,
answer: JSON.parse(jsonText),
};
}
```
コストは1/10になるがレスポンス速度は3~4倍くらいかかるようになった。チャットの少しずつ入力するのは演出じゃなくてstreamingで受け取っているからなのだろうか...? [[ChatGPT]]みたいに途中でstopするかもしれないリスクを考えると、コストが問題にならないなら[[Completion API]]の[[text-davinci-003]]を使いたい。