Skip to content

Commit a28c760

Browse files
committed
agent modification
1 parent c77db57 commit a28c760

File tree

3 files changed

+386
-212
lines changed

3 files changed

+386
-212
lines changed

script/agent-cloudformation/bedrock-agent.yaml

Lines changed: 194 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -82,74 +82,205 @@ Resources:
8282
Code:
8383
ZipFile: |
8484
import json
85-
import boto3
8685
import os
87-
from boto3.dynamodb.conditions import Key, Attr
88-
89-
dynamodb = boto3.resource('dynamodb')
90-
table = dynamodb.Table(os.environ['DYNAMODB_TABLE_NAME'])
91-
86+
import boto3
87+
from boto3.dynamodb.conditions import Attr, Contains
88+
from decimal import Decimal
89+
9290
def lambda_handler(event, context):
91+
"""
92+
Bedrock Agent Action Group을 위한 Lambda 함수
93+
사용자 질문에 기반하여 관련 영상을 검색합니다.
94+
"""
95+
96+
print(f"📥 받은 이벤트: {json.dumps(event, default=str)}")
97+
9398
try:
94-
# Parse the input from Bedrock Agent
95-
agent_input = event.get('inputText', '')
96-
session_id = event.get('sessionId', '')
99+
# Bedrock Agent 이벤트 구조 파싱
100+
api_path = event.get('apiPath', '')
101+
http_method = event.get('httpMethod', '')
102+
request_body = event.get('requestBody', {})
97103
98-
# Extract search parameters
99-
parameters = event.get('parameters', [])
100-
search_query = ''
104+
print(f"🔍 API Path: {api_path}, Method: {http_method}")
105+
print(f"📦 Request Body: {request_body}")
101106
102-
for param in parameters:
103-
if param.get('name') == 'query':
104-
search_query = param.get('value', '')
107+
# DynamoDB 클라이언트 초기화
108+
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
105109
106-
if not search_query:
107-
search_query = agent_input
110+
if api_path == '/search_classes' and http_method == 'POST':
111+
# requestBody에서 content 추출
112+
content = request_body.get('content', {})
113+
app_json = content.get('application/json', {})
114+
properties = app_json.get('properties', [])
115+
116+
# properties에서 query 파라미터 추출
117+
query = ''
118+
for prop in properties:
119+
if prop.get('name') == 'query':
120+
query = prop.get('value', '')
121+
break
122+
123+
print(f"🔎 추출된 query: {query}")
124+
result = search_classes(dynamodb, query)
125+
function_name = 'search_classes'
126+
else:
127+
result = {
128+
'statusCode': 400,
129+
'body': json.dumps({'error': 'Unknown endpoint'})
130+
}
131+
function_name = 'unknown'
108132
109-
# Search in DynamoDB
110-
response = table.scan(
111-
FilterExpression=Attr('name').contains(search_query) |
112-
Attr('description').contains(search_query) |
113-
Attr('searchableText').contains(search_query),
114-
Limit=10
115-
)
133+
# Bedrock Agent 응답 형식 (AWS 공식 스펙)
134+
response = {
135+
'messageVersion': '1.0',
136+
'response': {
137+
'actionGroup': event.get('actionGroup', 'ClassSearchActions'),
138+
'apiPath': api_path,
139+
'httpMethod': http_method,
140+
'httpStatusCode': result.get('statusCode', 200),
141+
'responseBody': {
142+
'application/json': {
143+
'body': result.get('body', '{}')
144+
}
145+
}
146+
}
147+
}
148+
149+
print(f"📤 반환 응답: {json.dumps(response, ensure_ascii=False, default=str)}")
150+
return response
116151
117-
courses = response.get('Items', [])
152+
except Exception as e:
153+
print(f"❌ 오류 발생: {str(e)}")
154+
import traceback
155+
print(f"📋 스택 트레이스: {traceback.format_exc()}")
118156
119-
# Format response for Bedrock Agent
120-
if courses:
121-
course_list = []
122-
for course in courses:
123-
course_info = {
124-
'title': course.get('name', 'Unknown'),
125-
'description': course.get('description', ''),
126-
'url': course.get('url', ''),
127-
'author': course.get('author', ''),
128-
'difficulty': course.get('difficulty', 'Unknown')
157+
error_body = {'error': str(e)}
158+
error_response = {
159+
'messageVersion': '1.0',
160+
'response': {
161+
'actionGroup': event.get('actionGroup', ''),
162+
'apiPath': event.get('apiPath', ''),
163+
'httpMethod': event.get('httpMethod', ''),
164+
'httpStatusCode': 500,
165+
'responseBody': {
166+
'application/json': {
167+
'body': error_body
168+
}
129169
}
130-
course_list.append(course_info)
131-
132-
result = {
133-
'courses_found': len(course_list),
134-
'courses': course_list
135170
}
136-
else:
137-
result = {
138-
'courses_found': 0,
139-
'message': f'No courses found for query: {search_query}'
171+
}
172+
173+
print(f"📤 에러 응답: {json.dumps(error_response, ensure_ascii=False, default=str)}")
174+
return error_response
175+
176+
def search_classes(dynamodb, query):
177+
"""DynamoDB에서 영상 검색 - description 기반 간단 검색"""
178+
179+
print(f"🔍 검색 쿼리: {query}")
180+
181+
# 검색어를 개별 키워드로 분리
182+
search_terms = [term.strip().lower() for term in query.split() if term.strip()] if query else []
183+
184+
print(f"🔎 검색어: {search_terms}")
185+
186+
table_name = os.environ['DYNAMODB_TABLE_NAME']
187+
188+
try:
189+
table = dynamodb.Table(table_name)
190+
191+
# 검색어가 없으면 빈 결과 반환
192+
if not search_terms:
193+
print("⚠️ 검색어 없음 - 빈 결과 반환")
194+
return {
195+
'statusCode': 200,
196+
'body': json.dumps({
197+
'courses_found': 0,
198+
'courses': [],
199+
'message': '검색어를 입력해주세요.'
200+
}, ensure_ascii=False)
201+
}
202+
203+
# 활성 영상만 조회
204+
base_filter = Attr('class_flag').ne(10) & (Attr('class_flag').eq(0) | Attr('class_flag').not_exists())
205+
206+
# name과 description 필드에서 대소문자 구분 없이 검색
207+
search_conditions = []
208+
for term in search_terms:
209+
term_lower = term.lower()
210+
term_upper = term.upper()
211+
term_title = term.title()
212+
213+
search_conditions.append(
214+
Contains(Attr('name'), term) |
215+
Contains(Attr('name'), term_lower) |
216+
Contains(Attr('name'), term_upper) |
217+
Contains(Attr('name'), term_title) |
218+
Contains(Attr('description'), term) |
219+
Contains(Attr('description'), term_lower) |
220+
Contains(Attr('description'), term_upper) |
221+
Contains(Attr('description'), term_title)
222+
)
223+
224+
# OR 조건으로 결합
225+
search_filter = search_conditions[0]
226+
for condition in search_conditions[1:]:
227+
search_filter = search_filter | condition
228+
229+
final_filter = base_filter & search_filter
230+
231+
# 스캔 실행
232+
response = table.scan(
233+
FilterExpression=final_filter,
234+
Limit=20
235+
)
236+
237+
print(f"📊 스캔 결과: {len(response.get('Items', []))}개 항목")
238+
239+
# 결과 포맷팅 - thumbnail과 URL만 포함
240+
classes = []
241+
for item in response.get('Items', []):
242+
class_info = {
243+
'title': str(item.get('name', '')),
244+
'description': str(item.get('description', ''))[:150] + '...' if len(str(item.get('description', ''))) > 150 else str(item.get('description', '')),
245+
'url': str(item.get('url', '')),
246+
'thumbnail': str(item.get('image', '')),
247+
'author': str(item.get('author', '')),
248+
'difficulty': str(item.get('difficulty', 'intermediate'))
140249
}
250+
classes.append(class_info)
251+
252+
# 상위 5개만 반환
253+
top_classes = classes[:5]
254+
255+
message = f"'{' '.join(search_terms)}' 관련 강의 {len(top_classes)}개를 찾았습니다." if top_classes else f"'{' '.join(search_terms)}' 관련 강의를 찾지 못했습니다."
256+
257+
result_data = {
258+
'courses_found': len(top_classes),
259+
'courses': top_classes,
260+
'message': message,
261+
'traces': [
262+
{'type': 'preprocessing', 'content': f"🔍 '{' '.join(search_terms)}' 검색 시작", 'timestamp': ''},
263+
{'type': 'function_call', 'content': f'⚡ DynamoDB에서 {len(classes)}개 항목 발견', 'timestamp': ''},
264+
{'type': 'observation', 'content': f'✅ 상위 {len(top_classes)}개 강의 선택 완료', 'timestamp': ''}
265+
]
266+
}
267+
268+
print(f"✅ 검색 완료: {len(top_classes)}개 영상 발견")
269+
print(f"📊 결과 데이터: {json.dumps(result_data, ensure_ascii=False)}")
141270
142271
return {
143272
'statusCode': 200,
144-
'body': json.dumps(result)
273+
'body': json.dumps(result_data, ensure_ascii=False)
145274
}
146275
147276
except Exception as e:
277+
print(f"❌ 테이블 접근 오류: {str(e)}")
148278
return {
149279
'statusCode': 500,
150-
'body': json.dumps({'error': str(e)})
280+
'body': json.dumps({'error': f'Database error: {str(e)}'})
151281
}
152282
283+
153284
# Lambda Permission for Bedrock Agent
154285
LambdaInvokePermission:
155286
Type: AWS::Lambda::Permission
@@ -170,39 +301,33 @@ Resources:
170301
Instruction: |
171302
당신은 AWS 학습 영상 검색 전문가입니다.
172303
173-
사용자가 질문하면:
174-
1. 질문에서 핵심 키워드를 추출하세요 (예: Bedrock, EKS, Lambda, DynamoDB 등)
175-
2. search_classes 함수를 호출하여 DynamoDB Class 테이블에서 관련 영상을 검색하세요
176-
3. 인터넷에서 관련된 영상을 같이 검색해줘 (예: youtube, AWS Skill Builder website 등)
177-
4. 검색된 영상들을 분석하여 사용자 질문과 가장 관련성이 높은 영상들을 추천하세요
304+
You are an AWS course search expert.
305+
You should reply with the language of the written query.
178306
179-
응답은 반드시 다음 JSON 형태로만 제공하세요:
307+
User question → call search_classes → recommend max 3 courses
180308
309+
Response format (JSON only):
310+
JSON FORMAT (copy exact values):
181311
{
182-
"title": "응답 제목",
312+
"title": "Title",
183313
"sections": [
184-
{
185-
"type": "header",
186-
"level": 1,
187-
"content": "메인 섹션 제목"
188-
},
189-
{
190-
"type": "text",
191-
"content": "설명 텍스트"
192-
},
193314
{
194315
"type": "course",
195-
"title": "강의 제목",
196-
"difficulty": "초급|중급|고급",
197-
"instructor": "강사명",
198-
"description": "강의 설명",
199-
"link": "강의 URL",
200-
"reason": "추천 이유"
316+
"title": "<copy from courses[].title>",
317+
"difficulty": "<copy from courses[].difficulty>",
318+
"instructor": "<copy from courses[].author>",
319+
"description": "<copy from courses[].description>",
320+
"link": "<copy from courses[].url>",
321+
"thumbnail": "<copy from courses[].thumbnail>",
322+
"reason": "<your 1 sentence>"
201323
}
202324
]
203325
}
204326
205-
JSON 형태가 아닌 다른 형태의 응답은 절대 하지 마세요. 반드시 유효한 JSON만 반환하세요.
327+
- No header or text sections
328+
- Max 3 courses only
329+
- Each field limited to 1 sentence
330+
- Complete within 20 seconds
206331
ActionGroups:
207332
- ActionGroupName: 'CourseSearchActionGroup'
208333
Description: 'Search for courses in DynamoDB'

0 commit comments

Comments
 (0)