Skip to content

Commit 228f54f

Browse files
committed
Restructure TravelAI module - numbered scripts with CREATE EXTERNAL MODEL
1 parent 7ee1763 commit 228f54f

16 files changed

Lines changed: 314 additions & 862 deletions
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Deploy the Bedrock Embedding Proxy (Lambda + API Gateway).
4+
This creates the serverless translation layer that allows SQL Server
5+
to call Bedrock via CREATE EXTERNAL MODEL + AI_GENERATE_EMBEDDINGS.
6+
7+
Usage:
8+
python3 03_deploy_embedding_proxy.py
9+
"""
10+
import boto3
11+
import json
12+
import os
13+
import time
14+
import zipfile
15+
import tempfile
16+
17+
region = os.environ.get('AWS_REGION', 'us-west-2')
18+
account_id = boto3.client('sts').get_caller_identity()['Account']
19+
20+
print("Deploying Bedrock Embedding Proxy...")
21+
print(f" Region: {region}")
22+
print(f" Account: {account_id}")
23+
24+
iam = boto3.client('iam')
25+
lam = boto3.client('lambda', region_name=region)
26+
apigw = boto3.client('apigateway', region_name=region)
27+
28+
# Step 1: Create IAM Role
29+
ROLE_NAME = 'bedrock-embedding-lambda-role'
30+
print("\n1. Creating IAM role...")
31+
try:
32+
role = iam.create_role(
33+
RoleName=ROLE_NAME,
34+
AssumeRolePolicyDocument=json.dumps({
35+
"Version": "2012-10-17",
36+
"Statement": [{"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]
37+
})
38+
)
39+
iam.put_role_policy(RoleName=ROLE_NAME, PolicyName='BedrockAccess', PolicyDocument=json.dumps({
40+
"Version": "2012-10-17",
41+
"Statement": [
42+
{"Effect": "Allow", "Action": ["bedrock:InvokeModel"], "Resource": "*"},
43+
{"Effect": "Allow", "Action": ["logs:*"], "Resource": "*"}
44+
]
45+
}))
46+
print(f" Role created: {role['Role']['Arn']}")
47+
time.sleep(10) # Wait for role propagation
48+
except iam.exceptions.EntityAlreadyExistsException:
49+
print(f" Role already exists")
50+
role = iam.get_role(RoleName=ROLE_NAME)
51+
52+
role_arn = f"arn:aws:iam::{account_id}:role/{ROLE_NAME}"
53+
54+
# Step 2: Create Lambda Function
55+
FUNCTION_NAME = 'bedrock-embedding-proxy'
56+
print("\n2. Creating Lambda function...")
57+
58+
lambda_code = '''
59+
import json
60+
import boto3
61+
62+
bedrock = boto3.client('bedrock-runtime', region_name='us-west-2')
63+
64+
def lambda_handler(event, context):
65+
try:
66+
body = json.loads(event.get('body', '{}'))
67+
input_text = body.get('input', body.get('inputText', ''))
68+
if isinstance(input_text, list):
69+
input_text = input_text[0]
70+
dimensions = body.get('dimensions', 1024)
71+
72+
response = bedrock.invoke_model(
73+
modelId='amazon.titan-embed-text-v2:0',
74+
contentType='application/json',
75+
accept='application/json',
76+
body=json.dumps({'inputText': input_text[:8000], 'dimensions': dimensions})
77+
)
78+
79+
result = json.loads(response['body'].read())
80+
embedding = result['embedding']
81+
82+
return {
83+
'statusCode': 200,
84+
'headers': {'Content-Type': 'application/json'},
85+
'body': json.dumps({
86+
'object': 'list',
87+
'data': [{'object': 'embedding', 'embedding': embedding, 'index': 0}],
88+
'model': 'amazon.titan-embed-text-v2',
89+
'usage': {'prompt_tokens': len(input_text.split()), 'total_tokens': len(input_text.split())}
90+
})
91+
}
92+
except Exception as e:
93+
return {
94+
'statusCode': 500,
95+
'headers': {'Content-Type': 'application/json'},
96+
'body': json.dumps({'error': {'message': str(e), 'type': 'server_error'}})
97+
}
98+
'''
99+
100+
# Create zip
101+
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp:
102+
zip_path = tmp.name
103+
with zipfile.ZipFile(zip_path, 'w') as zf:
104+
zf.writestr('lambda_function.py', lambda_code)
105+
106+
with open(zip_path, 'rb') as f:
107+
zip_bytes = f.read()
108+
109+
try:
110+
lam.create_function(
111+
FunctionName=FUNCTION_NAME, Runtime='python3.12',
112+
Role=role_arn, Handler='lambda_function.lambda_handler',
113+
Code={'ZipFile': zip_bytes}, Timeout=30, MemorySize=256
114+
)
115+
print(f" Function created: {FUNCTION_NAME}")
116+
except lam.exceptions.ResourceConflictException:
117+
lam.update_function_code(FunctionName=FUNCTION_NAME, ZipFile=zip_bytes)
118+
lam.update_function_configuration(FunctionName=FUNCTION_NAME, Handler='lambda_function.lambda_handler')
119+
print(f" Function updated: {FUNCTION_NAME}")
120+
121+
time.sleep(5)
122+
123+
# Step 3: Create API Gateway
124+
print("\n3. Creating API Gateway...")
125+
apis = apigw.get_rest_apis()['items']
126+
existing = [a for a in apis if a['name'] == 'bedrock-embedding-api']
127+
128+
if existing:
129+
api_id = existing[0]['id']
130+
print(f" API already exists: {api_id}")
131+
else:
132+
api = apigw.create_rest_api(name='bedrock-embedding-api', endpointConfiguration={'types': ['REGIONAL']})
133+
api_id = api['id']
134+
print(f" API created: {api_id}")
135+
136+
# Get root resource
137+
resources = apigw.get_resources(restApiId=api_id)['items']
138+
root_id = [r for r in resources if r['path'] == '/'][0]['id']
139+
140+
# Create /embed resource if not exists
141+
embed_resources = [r for r in resources if r.get('pathPart') == 'embed']
142+
if embed_resources:
143+
resource_id = embed_resources[0]['id']
144+
else:
145+
resource = apigw.create_resource(restApiId=api_id, parentId=root_id, pathPart='embed')
146+
resource_id = resource['id']
147+
148+
# Create POST method
149+
try:
150+
apigw.put_method(restApiId=api_id, resourceId=resource_id, httpMethod='POST', authorizationType='NONE')
151+
except:
152+
pass
153+
154+
# Create Lambda integration
155+
lambda_uri = f"arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/arn:aws:lambda:{region}:{account_id}:function:{FUNCTION_NAME}/invocations"
156+
try:
157+
apigw.put_integration(restApiId=api_id, resourceId=resource_id, httpMethod='POST', type='AWS_PROXY', integrationHttpMethod='POST', uri=lambda_uri)
158+
except:
159+
pass
160+
161+
# Add Lambda permission
162+
try:
163+
lam.add_permission(FunctionName=FUNCTION_NAME, StatementId='apigateway-invoke', Action='lambda:InvokeFunction', Principal='apigateway.amazonaws.com')
164+
except:
165+
pass
166+
167+
# Deploy
168+
apigw.create_deployment(restApiId=api_id, stageName='prod')
169+
170+
endpoint = f"https://{api_id}.execute-api.{region}.amazonaws.com/prod/embed"
171+
print(f"\n Endpoint: {endpoint}")
172+
173+
# Step 4: Save endpoint for SQL scripts
174+
env_file = '/tmp/embedding_endpoint.txt'
175+
with open(env_file, 'w') as f:
176+
f.write(endpoint)
177+
178+
print(f"\n{'='*60}")
179+
print(f"DONE! Embedding proxy deployed.")
180+
print(f"Endpoint: {endpoint}")
181+
print(f"\nNext: python3.11 run_sql_file.py load_generator/04_register_model.sql")
182+
print(f"{'='*60}")

labs/travelapp/load_generator/travelai_setup.sql renamed to labs/travelapp/load_generator/01_create_database.sql

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
-- TravelAI Setup: Database + Schema + Indexes + Seed Data
2-
-- Run this first to create the TravelAI database with all tables and sample data.
3-
-- Usage: python3.11 run_sql_file.py load_generator/travelai_setup.sql
1+
-- TravelAI Database Setup: CREATE DATABASE + Schema + Indexes + Seed Data
2+
-- Usage: python3.11 run_sql_file.py load_generator/01_create_database.sql
43

54
IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'TravelAI')
65
CREATE DATABASE TravelAI;

labs/travelapp/load_generator/travelai_app_procs.sql renamed to labs/travelapp/load_generator/02_create_search_procedures.sql

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
-- TravelAI Search Stored Procedures
2-
-- Created for the Search UI comparison demo
3-
-- These run against the TravelAI database
4-
1+
-- TravelAI Search Procedures
2+
-- Usage: python3.11 run_sql_file.py load_generator/02_create_search_procedures.sql
53
USE TravelAI;
64
GO
75

86
SET NOCOUNT ON;
97
GO
108

11-
-- =============================================
129
-- usp_SearchSQL: Pure WHERE clause matching
13-
-- Maps keywords to climate categories
14-
-- =============================================
1510
CREATE OR ALTER PROCEDURE dbo.usp_SearchSQL
1611
@QueryText NVARCHAR(1000),
1712
@TopK INT = 5
@@ -20,56 +15,41 @@ BEGIN
2015
SET NOCOUNT ON;
2116
DECLARE @Climate NVARCHAR(30) = NULL;
2217
IF @QueryText LIKE '%beach%' OR @QueryText LIKE '%tropical%' OR @QueryText LIKE '%island%'
23-
SET @Climate = 'Tropical';
24-
ELSE IF @QueryText LIKE '%mountain%' OR @QueryText LIKE '%hiking%'
25-
SET @Climate = 'Temperate';
18+
SET @Climate = 'tropical';
19+
ELSE IF @QueryText LIKE '%mountain%' OR @QueryText LIKE '%hiking%' OR @QueryText LIKE '%alpine%'
20+
SET @Climate = 'alpine';
2621
ELSE IF @QueryText LIKE '%desert%' OR @QueryText LIKE '%arid%'
27-
SET @Climate = 'Arid';
28-
ELSE IF @QueryText LIKE '%arctic%' OR @QueryText LIKE '%glacier%'
29-
SET @Climate = 'Arctic';
22+
SET @Climate = 'semi-arid';
3023

3124
IF @Climate IS NOT NULL
3225
SELECT TOP (@TopK) destination_id, name AS Title, country_code AS Country, region AS Continent, climate AS Climate, best_season AS Season, LEFT(description,200) AS Snippet, popularity_score, 100 AS RelevanceScore
33-
FROM Destinations WHERE Climate = @Climate ORDER BY popularity_score DESC;
26+
FROM Destinations WHERE climate = @Climate ORDER BY popularity_score DESC;
3427
ELSE
3528
SELECT TOP (@TopK) destination_id, name AS Title, country_code AS Country, region AS Continent, climate AS Climate, best_season AS Season, LEFT(description,200) AS Snippet, popularity_score, 50 AS RelevanceScore
3629
FROM Destinations ORDER BY popularity_score DESC;
3730
END;
3831
GO
3932

40-
-- =============================================
41-
-- usp_SearchLIKE: LIKE pattern matching
42-
-- Splits first two words, searches description + name + Country
43-
-- =============================================
33+
-- usp_SearchLIKE: Pattern matching
4434
CREATE OR ALTER PROCEDURE dbo.usp_SearchLIKE
4535
@QueryText NVARCHAR(1000),
4636
@TopK INT = 5
4737
AS
4838
BEGIN
4939
SET NOCOUNT ON;
5040
DECLARE @Word1 NVARCHAR(100) = LEFT(@QueryText, CHARINDEX(' ', @QueryText + ' ') - 1);
51-
DECLARE @Word2 NVARCHAR(100) = NULL;
52-
53-
IF CHARINDEX(' ', @QueryText) > 0
54-
SET @Word2 = SUBSTRING(@QueryText, CHARINDEX(' ', @QueryText) + 1,
55-
CHARINDEX(' ', @QueryText + ' ', CHARINDEX(' ', @QueryText) + 1) - CHARINDEX(' ', @QueryText) - 1);
56-
57-
SELECT TOP (@TopK)
58-
destination_id, name AS Title, country_code AS Country, region AS Continent, climate AS Climate, best_season AS Season,
59-
description AS Snippet, popularity_score, 120 AS RelevanceScore
41+
SELECT TOP (@TopK)
42+
destination_id, name AS Title, country_code AS Country, region AS Continent, climate AS Climate, best_season AS Season,
43+
LEFT(description,200) AS Snippet, popularity_score, 120 AS RelevanceScore
6044
FROM Destinations
61-
WHERE Description LIKE '%' + @Word1 + '%'
62-
OR (@Word2 IS NOT NULL AND Description LIKE '%' + @Word2 + '%')
45+
WHERE description LIKE '%' + @Word1 + '%'
6346
OR name LIKE '%' + @Word1 + '%'
6447
OR country_code LIKE '%' + @Word1 + '%'
6548
ORDER BY popularity_score DESC;
6649
END;
6750
GO
6851

69-
-- =============================================
70-
-- usp_SearchFreetext: Full-Text Search (FREETEXT only, no RAG)
71-
-- Uses SQL Server Full-Text engine with stemming and word forms
72-
-- =============================================
52+
-- usp_SearchFreetext: Full-Text Search
7353
CREATE OR ALTER PROCEDURE dbo.usp_SearchFreetext
7454
@QueryText NVARCHAR(1000),
7555
@TopK INT = 5
@@ -83,18 +63,13 @@ BEGIN
8363
END;
8464
GO
8565

86-
-- =============================================
8766
-- usp_TravelSearch: Hybrid (FREETEXT + RAG document chunks)
88-
-- Returns 2 result sets: destinations + supporting document context
89-
-- =============================================
9067
CREATE OR ALTER PROCEDURE dbo.usp_TravelSearch
9168
@QueryText NVARCHAR(1000),
9269
@TopK INT = 5
9370
AS
9471
BEGIN
9572
SET NOCOUNT ON;
96-
97-
-- Result set 1: Destinations ranked by Full-Text relevance
9873
SELECT TOP (@TopK)
9974
'Destination' AS ResultType,
10075
d.destination_id AS SourceID,
@@ -111,7 +86,6 @@ BEGIN
11186
ON d.destination_id = ft.[KEY]
11287
ORDER BY ft.[RANK] DESC;
11388

114-
-- Result set 2: RAG context from document chunks
11589
SELECT TOP 3
11690
'Document' AS ResultType,
11791
dc.chunk_id AS SourceID,
@@ -125,12 +99,7 @@ BEGIN
12599
END;
126100
GO
127101

128-
PRINT 'Search SPs created: usp_SearchSQL, usp_SearchLIKE, usp_SearchFreetext, usp_TravelSearch';
129-
GO
130-
131-
-- =============================================
132-
-- usp_SearchVector: Semantic vector search using VECTOR_DISTANCE
133-
-- =============================================
102+
-- usp_SearchVector: Semantic vector search
134103
CREATE OR ALTER PROCEDURE dbo.usp_SearchVector
135104
@QueryEmbedding VECTOR(1024),
136105
@TopK INT = 5
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
-- Register Bedrock Embedding Model via API Gateway proxy
2+
-- NOTE: Replace <API_GATEWAY_URL> with the endpoint from 03_deploy_embedding_proxy.py
3+
-- Usage: python3.11 run_sql_file.py load_generator/04_register_model.sql
4+
USE TravelAI;
5+
GO
6+
7+
-- Create credential for API Gateway (no auth needed, just a placeholder header)
8+
IF EXISTS (SELECT 1 FROM sys.database_scoped_credentials WHERE name LIKE '%execute-api%')
9+
BEGIN
10+
-- Drop model first if exists
11+
IF EXISTS (SELECT 1 FROM sys.external_models WHERE name = 'bedrock_embed')
12+
DROP EXTERNAL MODEL bedrock_embed;
13+
14+
DECLARE @cred_name NVARCHAR(500);
15+
SELECT @cred_name = name FROM sys.database_scoped_credentials WHERE name LIKE '%execute-api%';
16+
EXEC('DROP DATABASE SCOPED CREDENTIAL [' + @cred_name + ']');
17+
END
18+
GO
19+
20+
-- Read endpoint from file (written by 03_deploy_embedding_proxy.py)
21+
-- For manual setup, replace the URL below with your API Gateway endpoint
22+
DECLARE @endpoint NVARCHAR(500) = N'<API_GATEWAY_URL>';
23+
24+
-- Create credential
25+
DECLARE @sql NVARCHAR(MAX) = N'
26+
CREATE DATABASE SCOPED CREDENTIAL [' + @endpoint + N']
27+
WITH IDENTITY = ''HTTPEndpointHeaders'',
28+
SECRET = ''{"x-api-key":"none"}''';
29+
EXEC sp_executesql @sql;
30+
31+
-- Create external model
32+
SET @sql = N'
33+
CREATE EXTERNAL MODEL bedrock_embed
34+
WITH (
35+
LOCATION = ''' + @endpoint + N''',
36+
API_FORMAT = ''OpenAI'',
37+
MODEL_TYPE = EMBEDDINGS,
38+
MODEL = ''amazon.titan-embed-text-v2'',
39+
CREDENTIAL = [' + @endpoint + N']
40+
)';
41+
EXEC sp_executesql @sql;
42+
43+
PRINT 'External model bedrock_embed registered successfully';
44+
PRINT 'Test: SELECT DATALENGTH(AI_GENERATE_EMBEDDINGS(N''hello'' USE MODEL bedrock_embed))';
45+
GO
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- Populate vector embeddings using AI_GENERATE_EMBEDDINGS
2+
-- Usage: python3.11 run_sql_file.py load_generator/05_populate_vectors.sql
3+
USE TravelAI;
4+
GO
5+
6+
-- Embed all destinations (one line per table)
7+
UPDATE Destinations
8+
SET description_vector = AI_GENERATE_EMBEDDINGS(description USE MODEL bedrock_embed)
9+
WHERE description_vector IS NULL;
10+
GO
11+
12+
-- Embed all document chunks
13+
UPDATE DocumentChunks
14+
SET content_vector = AI_GENERATE_EMBEDDINGS(content USE MODEL bedrock_embed)
15+
WHERE content_vector IS NULL;
16+
GO
17+
18+
-- Verify
19+
SELECT 'Destinations' AS Source, COUNT(*) AS Embedded FROM Destinations WHERE description_vector IS NOT NULL
20+
UNION ALL
21+
SELECT 'DocumentChunks', COUNT(*) FROM DocumentChunks WHERE content_vector IS NOT NULL;
22+
GO

0 commit comments

Comments
 (0)