이전글에서 chat GPT python API 에 대해 설명했다.
한두번 사용하기엔 상관없지만
매번 python 파일을 수정하는 건 번거롭고 바람직하지 않다.
간단하게 터미널에서 입력을 받아도 되지만
자유도, 활용도를 높이기 위해 GUI 를 구축해보자.
GUI 글에서 언급 했던 것처럼 웹앱을 만들어도 되지만
이번에는 데스크탑 앱을 만들어 보겠다.
그리고 보통 많이 사용되는 tkinter 나 PyQt 가 아니라
C# ( 닷넷 ) 을 활용해보자.
Python 으로 C#앱을 만든다고?
pythonnet 패키지를 이용하면
파이썬 에서 C# 코드를 간단하게 사용할 수 있다.
하지만 Windows Forms 까지 사용해본 사람은 드물 것이다.
다음에 해당하는 사람에게 이 가이드를 추천한다.
- Visual Studio (닷넷) 를 써보았다.
- C# 앱을 개발해봤다.
- tkinter 보다 복잡한 레이아웃을 만들고 싶다.
- 바이너리 크기를 최소화 하고 싶다.
이 방식은 기본적으로 닷넷 프레임워크를 사용하기때문에
바이너리 크기가 작은 장점이 있다.
기본적으로 간단한 UI는 CLI 환경에서
개발하는걸 추천한다.
만약 복잡한 UI를 개발할 거라면 Visual Studio로
UI 코드를 만든 후 포팅하길 바란다.
Chat GPT 앱 코드
설치 되어야 하는 필수 패키지는 다음과 같다.
- pythonnet
c함수 사용을 위한 ctypes 는 표준 함수라
설치 안해도 되지만 pythonnet ( c# ) 은 별도로 설치해야한다.
UI를 정의하는코드와 실행 코드를 나누었다.
우선 UI 코드를 설명하겠다.
#myform.py import clr clr.AddReference("System.Windows.Forms") clr.AddReference('System.Drawing') clr.AddReference('System') from System.Windows.Forms import * from System.Drawing import * from concurrent.futures import ThreadPoolExecutor import requests # chatGPT URL = "https://api.openai.com/v1/chat/completions" headers = {"Authorization": "Bearer 발급받은API키", "Content-Type": "application/json"} def task(form): if form.query != '': messages = [{"role": "system", "content": "You are a helpful assistant."}] if form.assist != '': messages.append({"role": "assistant", "content": form.assist}) form.log.write(f'Assist:{form.assist}\n') messages.append({"role": "user", "content": form.query}) Data = {"model": "gpt-3.5-turbo", "messages": messages, "temperature": form.temp, "max_tokens": form.max_token} res = requests.post(URL, headers=headers, json=Data) form.answer = res.json()['choices'][0]['message']['content'] form.log.write('Q:' + form.query + "\n") form.log.write('A:') form.log.writelines(form.answer + "\n") form.log.write("\n") form.abox.Text = form.answer print('result arrived') else: MessagBox.Show("Query is empty") class MyForm(Form): def __init__(self, temp, max_token, lq, la, log): self.log = log self.temp = temp self.max_token = max_token self.lq = lq self.la = la self.exe = ThreadPoolExecutor(max_workers=2) super().__init__() c_w = 1000 c_h = 1000 self.ClientSize = Size(c_w, c_h) self.pn = TableLayoutPanel() self.pn.RowCount = 3 self.pn.ColumnCount = 1 self.pn.ColumnStyles.Add(ColumnStyle(SizeType.Percent, 100.0)) self.pn.RowStyles.Add(RowStyle(SizeType.Percent, 10.0)) self.pn.RowStyles.Add(RowStyle(SizeType.Percent, 20.0)) self.pn.RowStyles.Add(RowStyle(SizeType.Percent, 70.0)) self.pn.Dock = DockStyle.Fill self.pn1 = TableLayoutPanel() self.pn1.RowCount = 2 self.pn1.ColumnCount = 2 self.pn1.ColumnStyles.Add(ColumnStyle(SizeType.Percent, 50.0)) self.pn1.ColumnStyles.Add(ColumnStyle(SizeType.Percent, 50.0)) self.pn1.RowStyles.Add(RowStyle(SizeType.Percent, 20.0)) self.pn1.RowStyles.Add(RowStyle(SizeType.Percent, 80.0)) self.pn1.Dock = DockStyle.Fill self.pn2 = TableLayoutPanel() self.pn2.RowCount = 2 self.pn2.ColumnCount = 3 b_width = 7.5 self.pn2.ColumnStyles.Add(ColumnStyle(SizeType.Percent, b_width)) self.pn2.ColumnStyles.Add(ColumnStyle(SizeType.Percent, 100 - 2 * b_width)) self.pn2.ColumnStyles.Add(ColumnStyle(SizeType.Percent, b_width)) self.pn2.RowStyles.Add(RowStyle(SizeType.Percent, 50.0)) self.pn2.RowStyles.Add(RowStyle(SizeType.Percent, 50.0)) self.pn2.Dock = DockStyle.Fill self.lbl = Label() self.lbl.Text = f'Temperature={temp}' self.lbl.TextAlign = ContentAlignment.MiddleCenter self.lbl.Dock = DockStyle.Fill self.lbl2 = Label() self.lbl2.Text = f'Max token={max_token}' self.lbl2.TextAlign = ContentAlignment.MiddleCenter self.lbl2.Dock = DockStyle.Fill self.trb = TrackBar() self.trb.Dock = DockStyle.Fill self.trb.Maximum = 20 self.trb.Minimum = 0 self.trb.TickFrequency = 1 self.trb.LargeChange = 2 self.trb.SmallChange = 1 self.trb.Value = int(temp * 10) self.trb.Scroll += self.temp_moved self.trb2 = TrackBar() self.trb2.Dock = DockStyle.Fill self.trb2.Maximum = 1000 self.trb2.Minimum = 0 self.trb2.TickFrequency = 100 self.trb2.LargeChange = 200 self.trb2.SmallChange = 100 self.trb2.Value = max_token self.trb2.Scroll += self.token_moved self.lbl3 = Label() self.lbl3.Text = 'Assitant:' self.lbl3.TextAlign = ContentAlignment.MiddleCenter self.lbl3.Dock = DockStyle.Fill self.txtbox = TextBox() self.txtbox.Dock = DockStyle.Fill self.txtbox.Multiline = True self.txtbox.Text = la self.lbl3_2 = Label() self.lbl3_2.Text = 'Query:' self.lbl3_2.TextAlign = ContentAlignment.MiddleCenter self.lbl3_2.Dock = DockStyle.Fill self.txtbox_2 = TextBox() self.txtbox_2.Dock = DockStyle.Fill self.txtbox_2.Multiline = True self.txtbox_2.Text = lq self.button1 = Button() self.button1.Text = "Clear" self.button1.ForeColor = Color.Black self.button1.Dock = DockStyle.Fill self.button1.Click += self.clear_clicked self.button2 = Button() self.button2.Text = "Send" self.button2.ForeColor = Color.Black self.button2.Dock = DockStyle.Fill self.button2.Click += self.send_clicked self.abox = TextBox() self.abox.Dock = DockStyle.Fill self.abox.Multiline = True self.Controls.Add(self.pn) self.pn1.Controls.Add(self.lbl, 0, 0) self.pn1.Controls.Add(self.lbl2, 1, 0) self.pn1.Controls.Add(self.trb, 0, 1) self.pn1.Controls.Add(self.trb2, 1, 1) self.pn2.Controls.Add(self.lbl3, 0, 0) self.pn2.Controls.Add(self.txtbox, 1, 0) self.pn2.Controls.Add(self.button1, 2, 0) self.pn2.Controls.Add(self.lbl3_2, 0, 1) self.pn2.Controls.Add(self.txtbox_2, 1, 1) self.pn2.Controls.Add(self.button2, 2, 1) self.pn.Controls.Add(self.pn1, 0, 0) self.pn.Controls.Add(self.pn2, 0, 1) self.pn.Controls.Add(self.abox, 0, 2) def run(self): Application.Run(self) def send_clicked(self, sender, args): self.assist = self.txtbox.Text self.query = self.txtbox_2.Text self.exe.submit(task, self) def clear_clicked(self, sender, args): self.txtbox.Clear() self.txtbox_2.Clear() print('Cleared') def temp_moved(self, sender, args): self.temp = self.trb.Value / 10.0 self.lbl.Text = f'Temperature={self.temp}' def token_moved(self, sender, args): self.max_token = int(self.trb2.Value) self.lbl2.Text = f'Max Token={self.max_token}'
task 함수에서 쿼리 요청 및 수신을 처리하고
수신된 결과를 로그에 기록한다.
사용되는 메세지의 구조 는 이전글을 참고하라.
ThreadpoolExecutor ( 멀티 스레드 ) 를 사용해
task 함수는 UI 와 별도 스레드에서 실행 된다.
Async 로도 짜보았지만 수신에 시간이 걸릴 경우
UI 스레드가 freeze 된다.
Qthread 처럼 System.Threading 의
Thread함수를 사용해도 된다.
init 함수에서 UI 요소를 설정하는 건
C# 과 동일하며 코드도 거의 일치 한다.
TableLayoutPanel 을 이용해 레이아웃을 만들었고
TrackBar , Label , Button , TextBox 를 사용했다.
각 함수의 초기화 코드 및 사용법은 인터넷에
잘 나오니 따로 설명하지 않겠다.
버튼, 트랙바에 사용되는 callback 함수도
코드에 나온대로 매우 간단하게 짤 수 있다.
실행 코드는 더욱 더 간단하다.
#Only for python embed import sys sys.path.append('.') import csv from myform import MyForm log=open('log.txt','a+') try: f=open('cfg.csv','r') r=csv.DictReader(f,delimiter=';') cfg=dict(r.__next__()) f.close() temp=float(cfg['Temperature']) max_token=int(cfg['MaxToken']) lq=cfg['LastQuery'] la=cfg['LastAssist'] except: temp=0.1 max_token=100 lq='' la='' a=MyForm(temp,max_token,lq,la,log) a.run() a.Dispose() if hasattr(a,'query'): with open('cfg.csv','w') as f: wr=csv.DictWriter(f,fieldnames=["Temperature","MaxToken","LastQuery","LastAssist"],delimiter=';') wr.writeheader() wr.writerow({"Temperature":a.temp,"MaxToken":a.max_token,"LastQuery":a.query,"LastAssist":a.assist}) log.close() print('')
path.append는 embeddable python 에만 필요하다.
그리고 필수적인 요소는 아니지만 참고를 위해
cfg.csv에 마지막으로 사용한 쿼리를 저장한다.
Assistant에 입력할 내용은 사전정보나
chat GPT 의 이전 답변이며 비워두어도 된다.
Query 에 질문을 적으면 된다.
로그 파일에 쿼리 들 과 답변을 기록한다.
이정도면 chat GPT 의 GUI 하이브리드 앱
코드에 대한 충분한 설명이 되었으리라 본다.
이 앱 은 openai 에 로그인 하기 귀찮은 사람이나
차단된 곳에서 활용하기 유용할 것이다.
30분만에 대충 짠 코드이니 UI, 성능에 대한
최적화는 직접 해보길 바란다.
(23.08.17 update)
Textbox에서는 \n 대신 \r\n을 사용해야한다.
이전글