@@ -9,7 +9,7 @@ Parameters:
99
1010 ModelId :
1111 Type : String
12- Default : ' us.anthropic.claude-3-7-sonnet-20250219 -v1:0'
12+ Default : ' us.amazon.nova-pro -v1:0'
1313 Description : ' Foundation model ID for the agent'
1414
1515 DynamoDBTableName :
@@ -55,6 +55,7 @@ Resources:
5555 Action : sts:AssumeRole
5656 ManagedPolicyArns :
5757 - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
58+ - arn:aws:iam::aws:policy/AmazonBedrockFullAccess
5859 Policies :
5960 - PolicyName : !Sub '${AgentName}-DynamoDBReadPolicy'
6061 PolicyDocument :
@@ -84,53 +85,45 @@ Resources:
8485 import json
8586 import os
8687 import boto3
87- from boto3.dynamodb.conditions import Attr, Contains
88- from decimal import Decimal
88+ from boto3.dynamodb.conditions import Attr
8989
9090 def lambda_handler(event, context):
9191 """
92- Bedrock Agent Action Groupμ μν Lambda ν¨μ
93- μ¬μ©μ μ§λ¬Έμ κΈ°λ°νμ¬ κ΄λ ¨ μμμ κ²μν©λλ€.
92+ Bedrockμ νμ©ν μλ―Έ κΈ°λ° μμ κ²μ
9493 """
9594
9695 print(f"π₯ λ°μ μ΄λ²€νΈ: {json.dumps(event, default=str)}")
9796
9897 try:
99- # Bedrock Agent μ΄λ²€νΈ ꡬ쑰 νμ±
10098 api_path = event.get('apiPath', '')
10199 http_method = event.get('httpMethod', '')
102100 request_body = event.get('requestBody', {})
103101
104102 print(f"π API Path: {api_path}, Method: {http_method}")
105- print(f"π¦ Request Body: {request_body}")
106103
107- # DynamoDB ν΄λΌμ΄μΈνΈ μ΄κΈ°ν
108104 dynamodb = boto3.resource('dynamodb', region_name='us-west-2')
105+ bedrock = boto3.client('bedrock-runtime', region_name='us-west-2')
109106
110107 if api_path == '/search_classes' and http_method == 'POST':
111- # requestBodyμμ content μΆμΆ
112108 content = request_body.get('content', {})
113109 app_json = content.get('application/json', {})
114110 properties = app_json.get('properties', [])
115111
116- # propertiesμμ query νλΌλ―Έν° μΆμΆ
117112 query = ''
118113 for prop in properties:
119114 if prop.get('name') == 'query':
120115 query = prop.get('value', '')
121116 break
122117
123- print(f"π μΆμΆλ query : {query}")
124- result = search_classes (dynamodb, query)
125- function_name = 'search_classes'
118+ print(f"π μ¬μ©μ μ§λ¬Έ : {query}")
119+ result = search_with_bedrock (dynamodb, bedrock , query)
120+
126121 else:
127122 result = {
128123 'statusCode': 400,
129124 'body': json.dumps({'error': 'Unknown endpoint'})
130125 }
131- function_name = 'unknown'
132126
133- # Bedrock Agent μλ΅ νμ (AWS 곡μ μ€ν)
134127 response = {
135128 'messageVersion': '1.0',
136129 'response': {
@@ -154,8 +147,7 @@ Resources:
154147 import traceback
155148 print(f"π μ€ν νΈλ μ΄μ€: {traceback.format_exc()}")
156149
157- error_body = {'error': str(e)}
158- error_response = {
150+ return {
159151 'messageVersion': '1.0',
160152 'response': {
161153 'actionGroup': event.get('actionGroup', ''),
@@ -164,123 +156,128 @@ Resources:
164156 'httpStatusCode': 500,
165157 'responseBody': {
166158 'application/json': {
167- 'body': error_body
159+ 'body': {'error': str(e)}
168160 }
169161 }
170162 }
171163 }
172-
173- print(f"π€ μλ¬ μλ΅: {json.dumps(error_response, ensure_ascii=False, default=str)}")
174- return error_response
175164
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}")
165+ def search_with_bedrock(dynamodb, bedrock, query):
166+ """Bedrock Nova Proλ‘ μ§λ¬Έ μ΄ν΄ ν DynamoDB κ²μ"""
185167
186168 table_name = os.environ['DYNAMODB_TABLE_NAME']
187169
188170 try:
171+ # 1. DynamoDBμμ λͺ¨λ νμ± κ°μ κ°μ Έμ€κΈ°
189172 table = dynamodb.Table(table_name)
173+ response = table.scan(
174+ FilterExpression=Attr('class_flag').ne(10) & (Attr('class_flag').eq(0) | Attr('class_flag').not_exists())
175+ )
176+
177+ all_courses = response.get('Items', [])
178+ print(f"π μ 체 κ°μ μ: {len(all_courses)}κ°")
190179
191- # κ²μμ΄κ° μμΌλ©΄ λΉ κ²°κ³Ό λ°ν
192- if not search_terms:
193- print("β οΈ κ²μμ΄ μμ - λΉ κ²°κ³Ό λ°ν")
180+ if not all_courses:
194181 return {
195182 'statusCode': 200,
196183 'body': json.dumps({
197184 'courses_found': 0,
198185 'courses': [],
199- 'message': 'κ²μμ΄λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ .'
186+ 'message': 'λ±λ‘λ κ°μκ° μμ΅λλ€ .'
200187 }, ensure_ascii=False)
201188 }
202189
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
190+ # 2. κ°μ λͺ©λ‘μ ν
μ€νΈλ‘ λ³ν (μ΅λ 50κ°)
191+ courses_text = "\n\n".join([
192+ f"κ°μ {i+1}:\nμ λͺ©: {c.get('name', '')}\nμ€λͺ
: {c.get('description', '')}\nλμ΄λ: {c.get('difficulty', 'intermediate')}"
193+ for i, c in enumerate(all_courses[:50])
194+ ])
228195
229- final_filter = base_filter & search_filter
196+ # 3. Bedrock Nova Pro ν둬ννΈ
197+ prompt = f"""μ¬μ©μ μ§λ¬Έ: {query}
198+
199+ λ€μμ μ¬μ©ν μ μλ AWS κ°μ λͺ©λ‘μ
λλ€:
200+
201+ {courses_text}
202+
203+ μ κ°μ μ€μμ μ¬μ©μ μ§λ¬Έμ κ°μ₯ μ ν©ν κ°μλ₯Ό μ΅λ 3κ° μ ννκ³ , κ° κ°μ λ²νΈλ§ JSON λ°°μ΄λ‘ λ°ννμΈμ.
204+ μμ: {{"selected": [1, 3, 5]}}
205+
206+ λ§μ½ μ ν©ν κ°μκ° μλ€λ©΄ λΉ λ°°μ΄μ λ°ννμΈμ: {{"selected": []}}
207+
208+ JSONλ§ λ°ννκ³ λ€λ₯Έ μ€λͺ
μ νμ§ λ§μΈμ."""
209+
210+ # 4. Bedrock Nova Pro νΈμΆ
211+ body = json.dumps({
212+ "messages": [
213+ {
214+ "role": "user",
215+ "content": [{"text": prompt}]
216+ }
217+ ],
218+ "inferenceConfig": {
219+ "max_new_tokens": 500,
220+ "temperature": 0.3
221+ }
222+ })
230223
231- # μ€μΊ μ€ν
232- response = table.scan(
233- FilterExpression=final_filter,
234- Limit=20
224+ bedrock_response = bedrock.invoke_model(
225+ modelId='us.amazon.nova-pro-v1:0',
226+ body=body
235227 )
236228
237- print(f"π μ€μΊ κ²°κ³Ό: {len(response.get('Items', []))}κ° νλͺ©")
229+ response_body = json.loads(bedrock_response['body'].read())
230+ bedrock_text = response_body['output']['message']['content'][0]['text']
231+ print(f"π€ Bedrock μλ΅: {bedrock_text}")
238232
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'))
249- }
250- classes.append(class_info)
233+ # 5. JSON μΆμΆ
234+ selected_indices = json.loads(bedrock_text)['selected']
251235
252- # μμ 5κ°λ§ λ°ν
253- top_classes = classes[:5]
236+ # 6. μ νλ κ°μ μ 보 ꡬμ±
237+ selected_courses = []
238+ for idx in selected_indices:
239+ if 0 < idx <= len(all_courses):
240+ course = all_courses[idx - 1]
241+ selected_courses.append({
242+ 'title': str(course.get('name', '')),
243+ 'description': str(course.get('description', ''))[:150] + '...' if len(str(course.get('description', ''))) > 150 else str(course.get('description', '')),
244+ 'url': str(course.get('url', '')),
245+ 'thumbnail': str(course.get('image', '')),
246+ 'author': str(course.get('author', '')),
247+ 'difficulty': str(course.get('difficulty', 'intermediate'))
248+ })
254249
255- message = f"'{' '.join(search_terms) }' κ΄λ ¨ κ°μ {len(top_classes )}κ°λ₯Ό μ°Ύμμ΅λλ€." if top_classes else f"'{' '.join(search_terms) }' κ΄λ ¨ κ°μλ₯Ό μ°Ύμ§ λͺ»νμ΅λλ€."
250+ message = f"'{query }' κ΄λ ¨ κ°μ {len(selected_courses )}κ°λ₯Ό μ°Ύμμ΅λλ€." if selected_courses else f"'{query }' κ΄λ ¨ κ°μλ₯Ό μ°Ύμ§ λͺ»νμ΅λλ€."
256251
257252 result_data = {
258- 'courses_found': len(top_classes ),
259- 'courses': top_classes ,
253+ 'courses_found': len(selected_courses ),
254+ 'courses': selected_courses ,
260255 'message': message,
261256 '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': ''}
257+ {'type': 'preprocessing', 'content': f"π '{query }' κ²μ μμ", 'timestamp': ''},
258+ {'type': 'function_call', 'content': f'β‘ Bedrock Nova Proλ‘ {len(all_courses )}κ° κ°μ λΆμ ', 'timestamp': ''},
259+ {'type': 'observation', 'content': f'β
{len(selected_courses )}κ° κ°μ μ ν μλ£', 'timestamp': ''}
265260 ]
266261 }
267262
268- print(f"β
κ²μ μλ£: {len(top_classes)}κ° μμ λ°κ²¬")
269- print(f"π κ²°κ³Ό λ°μ΄ν°: {json.dumps(result_data, ensure_ascii=False)}")
263+ print(f"β
κ²μ μλ£: {len(selected_courses)}κ° κ°μ λ°κ²¬")
270264
271265 return {
272266 'statusCode': 200,
273267 'body': json.dumps(result_data, ensure_ascii=False)
274268 }
275269
276270 except Exception as e:
277- print(f"β ν
μ΄λΈ μ κ·Ό μ€λ₯: {str(e)}")
271+ print(f"β κ²μ μ€λ₯: {str(e)}")
272+ import traceback
273+ print(traceback.format_exc())
278274 return {
279275 'statusCode': 500,
280- 'body': json.dumps({'error': f'Database error: {str(e)}'})
276+ 'body': json.dumps({'error': f'Search error: {str(e)}'})
281277 }
282278
283279
280+
284281 # Lambda Permission for Bedrock Agent
285282 LambdaInvokePermission :
286283 Type : AWS::Lambda::Permission
0 commit comments