@@ -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