Skip to content

Commit 4f2e7bf

Browse files
committed
LaaS image 기능 추가
1 parent b1a5e97 commit 4f2e7bf

File tree

5 files changed

+89
-23
lines changed

5 files changed

+89
-23
lines changed

app.py

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import sys
33
import json
4+
import base64
45
import signal
56
import threading
67
import contextlib
@@ -18,6 +19,7 @@
1819
from middleware.laas.heuristic import outside_slack_jira_user_map
1920
from middleware.laas.jira_operator import JiraOperator
2021
from middleware.laas.jira_fields_schema import Issue, get_format_instructions
22+
from middleware.laas.mimetype import get_mime_type_from_url, is_supported_mime_type
2123

2224
if os.getenv('DEBUG', False):
2325
from dotenv import load_dotenv
@@ -39,7 +41,7 @@ class PICollection:
3941
workspace = 'wantedlab.atlassian.net'
4042
project = 'PI'
4143
trigger_emoji = 'pi_jira_gen'
42-
laas_jira_hash = '7d7e1e4c2652e5c82b29e9dd88a7630a1e0f004b4cd971314b8126e4f16aab1c'
44+
laas_jira_hash = '8008b106b08d86b0af7a55d0ad18ca058aab88fc7e7a5945eedee7f16827d21e'
4345

4446

4547
class SlackOperator:
@@ -53,22 +55,23 @@ def __init__(self, event, say, trigger_emoji):
5355
self.reaction_user = event['user']
5456
self.emoji = trigger_emoji
5557

56-
# after get_conversation_data
58+
# after set_conversation_data
5759
self.thread_ts = None
58-
self.context = None
59-
self.screenshots = None
60+
self.messages = None
61+
self.file_data = None
6062

6163
self.user_map = {
6264
x['id']: {'real_name': x['real_name'], 'email': x['profile'].get('email')}
6365
for x in get_all_slack_user_list() if not x['deleted']
6466
}
6567

66-
def get_conversation_data(self):
68+
def set_conversation_data(self):
6769
"""
6870
스레드의 모든 메시지를 가져와 정제합니다
71+
https://laas.wanted.co.kr/docs/guide/api/api-preset#%EC%B6%94%EA%B0%80-%EB%A9%94%EC%8B%9C%EC%A7%80%EC%97%90-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%ED%8F%AC%ED%95%A8%ED%95%9C-%ED%98%B8%EC%B6%9C
6972
"""
70-
context = ''
71-
screenshots = []
73+
messages = []
74+
file_data = []
7275
conversations = app.client.conversations_replies(
7376
channel=self.item_channel,
7477
ts=self.item_ts,
@@ -80,36 +83,73 @@ def get_conversation_data(self):
8083
# Process each message in the thread
8184
message_dt = datetime.fromtimestamp(float(message['ts'])).isoformat()
8285
user_name = self.user_map.get(message['user'], {'real_name': 'Unknown'})['real_name']
83-
text = message.get("text", "")
84-
context += f'{message_dt} {user_name}: """{text}"""' + '\n\n'
86+
text = f'{message_dt} {user_name}: """{message.get("text", "")}"""'
87+
images = []
8588

8689
# conversation 에 대한 모든 첨부파일을 복제합니다.
8790
for file in message.get('files', []):
8891
private_file_url = file['url_private']
92+
93+
# 슬랙 파일 다운로드
8994
headers = {'Authorization': f'Bearer {os.environ["SLACK_BOT_TOKEN"]}'}
9095
req = Request(private_file_url, headers=headers)
9196
try:
9297
response = urlopen(req)
9398
except HTTPError:
9499
continue
100+
95101
content = response.read()
96-
screenshots.append(content)
102+
file_data.append(content)
103+
104+
# MIME 타입 확인
105+
mime_type = response.getheader('Content-Type') or get_mime_type_from_url(private_file_url)
106+
107+
# 지원되는 형식인지 확인
108+
if not is_supported_mime_type(mime_type):
109+
# raise ValueError(f"Unsupported MIME type: {mime_type}")
110+
continue
97111

98-
self.context = context
99-
self.screenshots = screenshots
112+
# Non-animated GIF인지 확인 (GIF에만 적용)
113+
if mime_type == "image/gif" and b"NETSCAPE2.0" in content:
114+
# raise ValueError("Animated GIFs are not supported.")
115+
continue
116+
117+
# Base64 인코딩 수행
118+
base64_image = base64.b64encode(content).decode('utf-8')
119+
120+
# 웹에서 사용할 수 있는 형식으로 반환
121+
if base64_image:
122+
images.append({
123+
"type": "image_url",
124+
"image_url": {"url": f"data:{mime_type};base64,{base64_image}"},
125+
})
126+
127+
if images:
128+
messages.append({
129+
"role": "user",
130+
"content": [{"type": "text", "text": text}, *images]
131+
})
132+
else:
133+
messages.append({
134+
"role": "user",
135+
"content": text,
136+
})
137+
138+
self.messages = messages
139+
self.file_data = file_data
100140
return True
101141

102142
@property
103143
def link(self):
104144
return f'https://{SlackCollection.workspace}/archives/{self.item_channel}/p{self.item_ts.replace(".", "")}{f"?thread_ts={self.thread_ts}" if self.thread_ts else ""}'
105145

106-
def check_gpt_response(self, hash, params):
146+
def check_gpt_response(self, hash, params, messages):
107147
"""
108148
GPT 응답이 올바른지 확인합니다.
109149
이 단계는 LaaS 서버의 응답을 잘 받았는지 확인하는 단계입니다
110150
"""
111151
try:
112-
gpt_response = jira_summary_generator(hash, params)
152+
gpt_response = jira_summary_generator(hash, params, messages)
113153
except Exception as e:
114154
self.say(
115155
channel=self.reaction_user,
@@ -309,13 +349,13 @@ def laas_jira(event, say, collection: PICollection):
309349

310350
slack = SlackOperator(event, say, collection.trigger_emoji)
311351

312-
if not slack.get_conversation_data():
352+
if not slack.set_conversation_data():
313353
return
314354

315-
316355
gpt_response = slack.check_gpt_response(
317356
collection.laas_jira_hash,
318-
{'conversations': slack.context, 'schema': get_format_instructions(Issue)},
357+
{'schema': get_format_instructions(Issue)},
358+
slack.messages,
319359
)
320360
gpt_metadata = slack.validate_gpt_response_json(gpt_response, say)
321361

@@ -372,7 +412,7 @@ def laas_jira(event, say, collection: PICollection):
372412
)
373413

374414
try:
375-
jira_response = jira.safe_create_issues(refined_fields, slack.screenshots)
415+
jira_response = jira.safe_create_issues(refined_fields, slack.file_data)
376416
except Exception as e:
377417
slack.say(
378418
channel=slack.reaction_user,

middleware/laas/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
def call_wanted_api(method, path, **kwargs):
66
"""
77
Wanted LaaS API를 호출합니다.
8+
https://laas.wanted.co.kr/docs/guide/api/api-preset
89
"""
910
return requests.request(
1011
method=method,
@@ -18,11 +19,12 @@ def call_wanted_api(method, path, **kwargs):
1819
)
1920

2021

21-
def jira_summary_generator(hash, params: dict):
22+
def jira_summary_generator(hash, params: dict, messages: list):
2223
"""
2324
Wanted LaaS API 중 Jira 생성기를 호출합니다.
2425
"""
2526
return call_wanted_api('POST', '/api/preset/v2/chat/completions', json={
2627
"hash": hash,
2728
"params": params,
29+
"messages": messages,
2830
})

middleware/laas/jira_fields_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def get_format_instructions(cls: BaseModel) -> str:
2525
if "type" in reduced_schema:
2626
del reduced_schema["type"]
2727
# Ensure json in context is well-formed with double quotes.
28-
return json.dumps(reduced_schema)
28+
return json.dumps(reduced_schema, ensure_ascii=False)
2929

3030

3131
class Issue(BaseModel):

middleware/laas/jira_operator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ def get_user_id_from_email(self, email):
3333
except IndexError:
3434
return None
3535

36-
def safe_create_issues(self, refined_fields, screenshots):
36+
def safe_create_issues(self, refined_fields, file_data):
3737
"""
3838
Jira 이슈를 생성합니다.
3939
이 단계는 Jira API를 사용하여 이슈를 생성하는 단계입니다.
4040
"""
4141
response = self.client.create_issue(fields=refined_fields)
42-
if screenshots:
43-
self.update_attachments(issue_key=response['key'], attachments=screenshots)
42+
if file_data:
43+
self.update_attachments(issue_key=response['key'], attachments=file_data)
4444
return response

middleware/laas/mimetype.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import mimetypes
2+
from urllib.parse import urlparse
3+
4+
5+
# 지원 가능한 MIME 타입 목록 정의
6+
SUPPORTED_MIME_TYPES = {
7+
"image/png",
8+
"image/jpeg",
9+
"image/jpg", # 일부 서비스는 jpg로 MIME 타입을 표시함
10+
"image/webp",
11+
"image/gif",
12+
}
13+
14+
15+
def is_supported_mime_type(mime_type):
16+
"""MIME 타입이 지원되는지 검사"""
17+
return mime_type in SUPPORTED_MIME_TYPES
18+
19+
20+
def get_mime_type_from_url(url):
21+
# URL에서 파일 확장자로 MIME 타입 추론
22+
path = urlparse(url).path
23+
mime_type, _ = mimetypes.guess_type(path)
24+
return mime_type or "application/octet-stream" # 기본 MIME 타입

0 commit comments

Comments
 (0)