From 18a703cdea51a9715c7815cebb56bc6ca7fe9030 Mon Sep 17 00:00:00 2001 From: Biswanath Mukherjee Date: Fri, 19 Dec 2025 11:40:24 +0530 Subject: [PATCH 1/4] initial commit --- apigw-lambda-bedrock-sam-node/README.md | 101 +++++++++++ .../api-gateway-non-streaming-config.yaml | 33 ++++ .../api-gateway-streaming-config.yaml | 35 ++++ .../diagram/response-streaming.png | Bin 0 -> 85890 bytes .../example-pattern.json | 66 ++++++++ .../src/nonstreaming/index.js | 66 ++++++++ .../src/nonstreaming/package.json | 20 +++ .../src/package.json | 20 +++ .../src/streaming/index.js | 94 ++++++++++ .../src/streaming/package.json | 20 +++ apigw-lambda-bedrock-sam-node/template.yaml | 160 ++++++++++++++++++ 11 files changed, 615 insertions(+) create mode 100644 apigw-lambda-bedrock-sam-node/README.md create mode 100644 apigw-lambda-bedrock-sam-node/api-gateway-non-streaming-config.yaml create mode 100644 apigw-lambda-bedrock-sam-node/api-gateway-streaming-config.yaml create mode 100644 apigw-lambda-bedrock-sam-node/diagram/response-streaming.png create mode 100644 apigw-lambda-bedrock-sam-node/example-pattern.json create mode 100644 apigw-lambda-bedrock-sam-node/src/nonstreaming/index.js create mode 100644 apigw-lambda-bedrock-sam-node/src/nonstreaming/package.json create mode 100644 apigw-lambda-bedrock-sam-node/src/package.json create mode 100644 apigw-lambda-bedrock-sam-node/src/streaming/index.js create mode 100644 apigw-lambda-bedrock-sam-node/src/streaming/package.json create mode 100644 apigw-lambda-bedrock-sam-node/template.yaml diff --git a/apigw-lambda-bedrock-sam-node/README.md b/apigw-lambda-bedrock-sam-node/README.md new file mode 100644 index 000000000..d6aa95934 --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/README.md @@ -0,0 +1,101 @@ +# Amazon API Gateway response streaming with AWS Lambda integration + +This sample project demonstrates how to deploy Amazon API Gateway REST API with response streaming for AWS Lambda backend. The Lambda function invokes an Amazon Bedrock model to get response to user queries. The API Gateway streams the response back to the client. + + +## Requirements + +- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed +- [Node 24 or above](https://nodejs.org/en/download) installed +- [Maven 3.8.6 or above](https://maven.apache.org/download.cgi) installed + + +## Prerequisites + +- AWS CLI installed and configured +- AWS SAM CLI installed +- Node.js 24.x or later +- Access to Amazon Bedrock models + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` + +2. Change directory to the pattern directory: + + ```bash + cd serverless-patterns/apigw-lambda-bedrock-sam-node + ``` + +3. From the command line, run the following commands: + + ```bash + sam build + sam deploy --guided + ``` + +4. During the prompts: + + - Enter a stack name + - Enter the desired AWS Region e.g. `us-east-1`. + - Allow SAM CLI to create IAM roles with the required permissions. + - Keep default values to the rest of the parameters. + + Once you have run `sam deploy --guided` mode once and saved arguments to a configuration file (samconfig.toml), you can use `sam deploy` in future to use these defaults. + +5. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for next step as well as testing. + +## How it works + +This SAM project uses Amazon Bedrock API to generate content based on given user prompt. This is exposed through a serverless REST API. Please refer to the architecture diagram below: + +![End to End Architecture](diagram/response-streaming.png) + +Here's a breakdown of the steps: + +The SAM template deploys two APIs following the same architecture - one with response streaming and another with buffered response. This is to demonstrate the difference in user experience. + +1. **Amazon API Gateway**: Receives the HTTP POST request containing the user prompt. + +2. **AWS Lambda**: The API Gateway triggers the Lambda functions which use either `InvokeModelWithResponseStreamCommand` or `InvokeModelCommand` to call Bedrock for streaming and non-streaming use cases. + +3. **Amazon Bedrock**: Based on the given prompt, generates the content and streams/ returns the response to the respective Lambda functions. + +4. **Response**: The API Gateway either streams the responses back to the client (Fig. 1) or returns the whole response together. + + +## Testing + +Use [curl](https://curl.se/) to send a HTTP POST request to the API. Make sure to replace `api-id` with the one from your `sam deploy --guided` output: + +```bash +curl -X POST https://[api-id].execute-api.us-west-1.amazonaws.com/Prod/ask \ + -H "Content-Type: application/json" \ + -d '{"message": "Explain quantum computing in simple terms"}' +``` + +Test with both `NonStreamingApiUrl` and `StreamingApiUrl` to compare the difference. The `StreamingApiUrl` streams the output to the console as received while `NonStreamingApiUrl` buffers the whole response and renders together. + + + +## Cleanup + +1. To delete the resources deployed to your AWS account via AWS SAM, run the following command: + +```bash +sam delete +``` + + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-lambda-bedrock-sam-node/api-gateway-non-streaming-config.yaml b/apigw-lambda-bedrock-sam-node/api-gateway-non-streaming-config.yaml new file mode 100644 index 000000000..d9ed82318 --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/api-gateway-non-streaming-config.yaml @@ -0,0 +1,33 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +openapi: "3.0.1" +info: + title: "non-streaming-api" + version: "1.0.0" +paths: + /ask: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: "The message to send to the Bedrock model" + required: + - message + responses: + "200": + description: "Complete response from Bedrock model" + content: + text/plain: + schema: + type: string + x-amazon-apigateway-integration: + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${NonStreamingResponseFunction.Arn}/invocations" + httpMethod: "POST" + type: "aws_proxy" \ No newline at end of file diff --git a/apigw-lambda-bedrock-sam-node/api-gateway-streaming-config.yaml b/apigw-lambda-bedrock-sam-node/api-gateway-streaming-config.yaml new file mode 100644 index 000000000..1eaad000d --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/api-gateway-streaming-config.yaml @@ -0,0 +1,35 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +openapi: "3.0.1" +info: + title: "streaming-api" + version: "1.0.0" +paths: + /ask: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: "The message to send to the Bedrock model" + required: + - message + responses: + "200": + description: "Streaming response from Bedrock model" + content: + text/plain: + schema: + type: string + description: "Newline-delimited JSON objects representing streaming events" + x-amazon-apigateway-integration: + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${StreamingResponseFunction.Arn}/response-streaming-invocations" + httpMethod: "POST" + responseTransferMode: "STREAM" + type: "aws_proxy" \ No newline at end of file diff --git a/apigw-lambda-bedrock-sam-node/diagram/response-streaming.png b/apigw-lambda-bedrock-sam-node/diagram/response-streaming.png new file mode 100644 index 0000000000000000000000000000000000000000..6f1dc1f4bfe59c465efa7aa87f45bc228c44c34b GIT binary patch literal 85890 zcmeFa2O!mZ|2S@Dh05L}R7U2pDMDpOHXVB%4#!>@8QGPRvO=;sA{p6H*&`gAkiC+X z_5U1>dsX*7_dd_>x%avE|GoF>IG^|D{dteq`?beMu!@o#AwD%e8X6km)hm~;qoHAf z(9qC3agPBltJ>k(z%O)%>vB?Pxh=HQXlSNIjxt(~Hm+ut5K}Z3KIwyREWF$nPzOgA zzRN7UyfP3|V@DX&7WfLZ+dxe$%`8n#4({=C^9peC@N)9-sPpi!@Ldw%13tL9xp;U% zItTp?%}s4l11Un?Eg=v?7G7BaE^c6`vxR@aLK{fLnv=~8QCZ@21o&dafS@>jFxGw=+kw1LWCYFZg zFhd*Q!qyPj&i*ti0Ju3(k6WV7NYPe9^OB&D7N_$KS!Ed`b-_zAzNaPtD7K(5ln?Vv@FAAlsX!5nrt4{BOugPNuL;a2t+4s>&}G%
jx`I37Y1289CS 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�Fs{ z)x`$uY5gA+7|bU+2E%fL9WRZ$O-df)#C80PgAgisAePm6$%&~AoHEOVwJKiJNUfxk zHyHd9TzoV2I2C+)rIRm4!Ke55vv70tIQ~}j$4=?x6{!;bN1V#(Ri1;Rs zP{^0kuf<(ek2uC$b)uNkpidVLdk%Z&qu9#1ckU72ynB6Xp}ldEx(?&KLsiLVM3^dD zlMrlpdz+gCR)Fp==lmi4QJ9$Q`+KL%W(zSrK-Y+jGR@yl&zA(^ARS@|smWfj# zqMok8FFYspjdS}g?kZW#F!4tM`jwS~?NY}e?%Nwpqhv}1EgqH}(X`Kv*PFHMhCXL- z3iXaXQDGyjQofq(lZM}BWjqkvyLM@e7;cZn-$yvtXd117LjnoBQSf0p(X6Efsx@t6(S;=ngb+-CfEBSsiwL~m zoog{vTq7)+#)Jvd2Z0Ig#&Xv$H-T%M_tu z6NaFtIh-tY*_?br^sFv$o`@wY+(%KNqBC_dg0xM8#)ITsJ30h$p){@?R(p9iEF%5+ zo-?^wUo1PDN!9a}K=V=Yc1I}3rS_WCFUtyNqv%K2I-$hPT5iFK+l?5)kB9h^uAL|0 z)%L@Zxw3XFe+c)in^7Q!!?EoM>#`b{u1AkB~i$ zbUVwM;5{Z#6;PvxrZ^IO4J>W*vWxYF;M~BagLluxiyZKfrQlV)45aH z*9?et%*E$eJTfA!<+(+BMNnCK838+qVrvUgB|jmPkgwJHT%8uaiMo7hW%kw zNfJ(9m!D9@=5_k4h=o1r4eNMsJX-#YXH?^!gnOnB;MgSO@4Hb?r^yfXm3<{Z3J$WV zJIn>BuGg#r%-1j19*i;TqK_3YYE@$n%ohl%ke#P-#zz!;L{AjQB%>t?3={-)gP*Ht z8;=nc7CVH^yYb6Pt+?Eoyo(hJ%5qvlRon7A@cxZf4p^^5-CsB`ElJZgyLGFx;}Xn>2V{3f*ue-hsI15yXF z5pdw{7e@j6!kAt%gk7z?t>?7W(?x{|fP0nhlvWO0d7wH#%Yq&o<8f1_C&9y5U^$4q zqU!2dEgLMzwmsPus&*N4m$g;`euPj*2c318=vzcI!&ud)f{DEhI`@^QS*+vA zXlJ~;#O*@31f##yCi{q%g%cqBD|HG3u2sn{B*#3zXyO~5XZMkSa&wH90I?x0bB>wy z95Sn*m*}wos5v^Ix^d=abMaag21ukAL#g7G&vpi$6uh3YEgyT%OXEF6>LG{W2yC3* zZo@VPo6W`3_VaZ7o%`%x*%`qAFWkw4O(%)sUnWPuZ7QmQ^T6W~pq2;#S8iN-&(MjC z;4g#$8kA@0835*e##K%}1o>V7kKeoEOhkS- z;T?Q%twRmGd4aV&Q85gczHKUqJGA+$v6-GX<&cpvJ2j_}Y}NbtQ}0sH-j=I##^QAy z_bFJJt3;tkz!ddWL zpZmN5OS?`zF2CcXrqJCG&NJAH+evx9SDSv!K4G;1<`{++dn|O+{nSPjjeR=&g9W^QjVN1ek&(WGX3P}y#ph*LKDL&nG1p*XhpB;jPZtO&co5T)a? zPxn6gdG#-rbhDTeHNJWji2-4Y$}*;!^0CwsSX6qFoex9@-;=G9-GKm5l~R8JEbsD; z^-cE^de~yvG#(d?`;Fw_MzN1HVk+J`3m4WSFz&K7Y&)*Jkkg=7ZupeXG!~)(t>$A7 zf=HOwD+*T2x**8yV9(HCsc`>*?)N8P7rpkJ&XK%e1sM)q@cvjs=l781%%w-?p{|)J zv<%J2g;J*S%W8M3c5a9=-#h6VX81guU{jwiYZge*`9zWH*t&>+8H9jKF9+!kL52!h zULZ;GRwZd_l|`;k8B+9yfsI;Ajj(9@hX?Z)TuI0{wbIxDNzn3oFIyF@X!qEF=lB3w z^Sz5jK2hSrcg0IMJG6EeRt-2z1sa;2f(M_bc*@UbLb`N}b$f6l-Qp#Hs_QoxCuCf(2heGB z&)S<==HqY!WXIA3#)d}uPq zUs3RA5iW0xxhtNJ$M;ISCO~&j$9wOqG+re6AOmH>+JzW!N)As#QTyN+o>bor#j1M! zu5lsNs{N&QKlE;aykz%MFF>Le-5DfmvBP=%LIhQeAmO4KPr!cR%Cr72D6n7JKTt5U zGyUu&(F7+DI-ZRoYm&boCvNG*mw@%7{6jv^CgGae)p4PcXqio z8?IDjYDoGsA0Ney0v2mXaz_`t&1xD(H3khKM#M~KC5b0}xOdMO*D+D4CqChC)2{Ho zvBTE$ZH`>*zTQo)oaFzal;%uT`#Icab@kd{42%H}U2@@aUa5McmFjGGxJ^tW-j%IOmAiOdM=L_`i zo6Z%+w{i{#Tx64lYtTW>7wi+xi6!)+VZ+L4S)elZenu@8L`+paf(-lVQ~z#Cx%UZm z_1yuDj|JW`fJxr1nDjflaH<1yl06TQk?D6B3M;#HEEZq*v<~E0G@w^rCt=O240Y(K ztuh;7yNcz>ukf4(s zP0Nn&N(4&4I`y<7i3n5_O*JT~nd<^fw&=}JRBtwnJu;NI`x5b>*$UD9$!*DpiOiVE zg_ndiwpNmYqy{8|%68gj3ix0C#5wGr#gbdl4IO=M|l7)ziMAx3e&ly?M&N z>PBzpl zE`rrW_SogT^!muY*%Mm(059BzAy}W}nTzn0`Clpb0v#UVm@k6d5P}%{v`m zu4po90TzJ0=|lU~GWKduLb~}(&z&-#@Um;~?UQ`~k*MDu=3YYmWkrkm%+%|JalUSn zrV(^3T0$F$bT)QvyuoQE+$XoiZ<`C> zU$cq?ZqGJQP=2DJo?5$J`4yKaq0rMv;xh<1k;lFEf$5Y6n)9Ox@(TC>1jb)b5htLP z4@U+VEXoxYy26%cN6_o|o@8&0PtA+SY4V(dQr8|C&lsTXe4hD)rPC5R+yL5!-f+4YadU{s^g^o!`_1O($F6?q^1 z``7LGZ&c~S?ceqpL3hH5Vj<8mrS5qSY-$Mqz!V6{!Tay^19rs_{a)gW7`yPs36H8& z55xOkF@&fgA*tR5^ez)_E!T`C?*zPluSmIBD-c(@{RUrxr0==IEC>kJM~dWX<{8-1 zUBHpi$39I02^%qLJ#E}d*o=Fj$PLg-$LT{#*DovBxIL|^7g~Cy8N{9yQke}J@{e$y z)Um?`ede%}lM!ZRJvX8Z&!>>;O;05vRy@0e^#J4KLT`@)EiYKjg!VR$g>J~iN`v^w zXRl#2x!a2O3ORO#g~j)1H95&Yr*a!2lG!)rJ4mcoqeM*XGT9lb%01RK-eOtqUBB5{ zFutzp`woLfQ>E5c7$)uTK#&yBXqXr-TzOA#i4QoB^=#g~j~Jdm(H>Q;nH3Ib9y9(z z7&LC=#n+W?MP}e{kU$1{dt?4;KX-glz!*eOa{MBRpsik^b&pcEz=DCL z>UQxvG(ZQhzen>Vy5E569v%orEVE2==?TN#bPL(D_Q#Zb3;Qa8Sec9{A z*JE;hB&+L(b^3HM#}w&uLH%EzHvuNZcFHO>&F#FH8CAq^4}FY%vvgHEIBJnoyp~L3 zXq*~Q@hodb+#cC|u1VAgZT`UZxYp9r69{3Punre|r1znT1OjLBB+apIZ_vJo=HhpZ za#S|2ee^30>(~=o<(4q6l@8Ud*;|gHQxO(0Y>7*CMXh)=oPD9XY$tEjok zm;)-E|IC3qk)_*5ehvQ(7CdPvH#yGDT?qiznm)+*BNyMA0Z2}3oMGq^VexX?XJs~X zYde0#co9jp+qGt`vv)iE(F-n&ya{;Q?QZVe`aEQ))QNq_Yh7Pi;Ne(G62^`$q^_~& zn#dg0C1383O6t6F0Tx&C1Bm5JOJ8(PxkGY5{`bJX&Fm#bA6K9N@&&%S95&z4mz%0G zHLX#(HvEvKUh8p|v6nB&&FXiL&u-lpkSb~E+{uh3WyQa2j(zQ=m0#jDt@@O;`}Tuj zL|7vc^b4h_!t9MbBk1*aJ9iDzNKSzZ#-39_*^0E(*9>@#Yq)Mngu+?tox_`&<kbL@yIm!OM;TC6&dwa@QYt$x}J|>XAiS+%Z*qkQ}RS- zHurKd%fGjtk)|2GZ_SzJFS9(MT3x9pYyz1SUQjVAn`UKBTx~MA91v#)BYWK3E^s0P z%r_T9RU4tL2Y5t4Nz$OGuO@vC@57cwn-LD}5E?bcn&DTwXtsNX48WO~D6Z*Y+)nEc z>&sW+euy`$AR06_sqIq-;?Gm&Ds%58YAJ&B`dv@GR_DLPUcb)6TO~V4;Y3&VtOO=a zP&a}OBd*)fJ=Z`m0)^X~`sTC6WQiSU_ET7*y2iC)pC!USzY27$YQ-x{f)0xg)6w7QMAX zZ!X!`raeldk8|T|TOPUq}EO*dguC`jG@fueDa)tf(zW5Af65+Jv^8- z1k4}Kfi}hF?kC33_%J@k5J19weX|84-rV|l&x6-$C~CKYXn?ad5j@uL^&`=l%MWoa zbCZ+ZRwSI(<2J{H+?3uq)G+kiV>2zr1eJ=Jchf*QMC+aV`Z0qlNuoWmX`)xMm2j+` z+@n-#@S+6mNciDY4mB9`#u1%$O}iNaM)FDitcCBbFeD#4-{d#`LR9ee!&L_8_Ew8S zO%&}t=&!Y*DEcewq0~yIgRKHh1wxDduCplJx zMKOU&!(F*{RA+Z{=+MA`SZJjKLmHePd(d zcVU;X!nh|a?R&Ei_E zwbdI{m#n8@`z%P}#1-OgYPu4%54h*6jB}DyBz&QXNbsuA!l~-0-V`rc6BCIhek)+z z4c=Ot@Agzq5UU1qO+$e6;*?uEUyE<;i1ZcNWcg8yx5U!`S*lV%YMlMzkoZgQmhH`z zd)$Un$7zL5+{jd~h8xx{O|>)Aia6qmx-47{z$fE&|6&2;RmTt@D%hu)tlAlRgu05y%65j(yYlE^X%be#!o-4+RWCsx@{Fpo*4y3^I#2s7fd(13H z^x&M@vB{SB+8~BocYqv~r$7$0MehriKq9)#zV&(O{Adna zYF^U@e<1G;JX#a50WjxF?~H`kW;U1KGN|So$zUsL7nsS6)rXUv$)B&ktdigCy}qE| zRS)Dl8v%*??;f&K7`RWfVDr(5I;+on0U1v7Yg-+u^7tXF$_Zhv%OB5af}(GfyE}D7 z0zJ;!;5tJ*mOs|rc#O=y{XE$kiATb^;VB`F2N?(t{FL5_0UCXHz+WVWKNygYT!1gv z>C`?iBRSpoNMyUbn^nYzkZojqknZKspux~5rhCo0@b>T?@Ce=qgo9`S5H)vXppoOU zj;LaN#sN}O(&gaac6@j`JI4N)NGqXuq8WG6)q3tKs)}$Q<74X&l4P!U>)>70z4PUk zP+%nm=uSX#hT@dS+L?$rH;dJ~x&klmWB53~PIM=DbbR zh@Cw5*w^CN1D56y)* zUNt`bpsjiK-96?8UKmgkbLPt3i#xKELas|s{TTe30Tulolk~Awn(Dh6#PrsA^g6e1 zZOy;3vq`C&7?rMlF6HA#?6Dbc4f(=2v#|__t^$kp(?L6|ZK~;Uf#FQt1sALa3IzEarbS+%Hxrhl} z*}{Z)=0AfEE@)Xh?s;Z39ze4Y@vf|&7yZ`l>ZT3!Cwklu$Hc3TMDpo8% zt0T4B@gQ#x@Q!`$h7o$%^ZCtb7~OIGY#{A&BpIlBu!;|-f3t#KvwE5qo~9T}4irzQ zM&p)3mjz1$cYtaN4S9Ud^H}IQ_P7^VQ>W?7C#?S?k=(O zMU9N(QdvTKNR9IgF=G+;FX@BZ<}Go-;QXY3v+~g#2)NkDElap% zdvbuzVoLQVfEinf-}4FIY1FD>J&i!*dPI5f0@{v`_%nUdId;T`m5hClPD)7n)rST|c3=o6VzxxAm>nN7Iwl#PIuUL>Iu8Jha*drRu@p*Z z1$Tqld+S9xH^Om1)aP;8>q5xXZo_henTjC;#j4s?xCH)W82A)(olarV4>56w(qFO_ zDr|q9&9h<+!1UH}Gy}E|)J$bu#1yGQ8!VlABEufc3}@m-WSPIe&tk<%#IWGcRoYq? zK}WOlshOZhv)l55Wc6)3$B70qC$auahKR!R!ZYidH@#9X26MsYF02zJ%j*tTcs*nX z+(_n+vV6j=;Q$K^eeosmv-ouHjm63oFKn8d9B1bO*t)3)tu;@6h|2&%p+X`pswSYZ z<@S@1abc!byghO4)6t}W?Oy7WZ>G%2yr5+iR`oFw-nL*hpI>KI?VuI2=W*G>&eYJ} zq@CyUT~WeUuSkP7U%VAaa;9!%>s%XdrY!6VqpKvjjce43KIbN_!590 z2zxc;&K^e|(RUSY=qPwhR>*lS1|v3#O*zvCM?qP0@<6M-%sS_>W@^xIf%HW9OK zkm^rBGCck#i+m3#{}rEa*o@IKv5S+-kX z0&wYDi5n)Cqch*k;S#B2ZXRquQ|cG~x=ea&kWRZ5&-(+-6}aKnl;0YVga{T-cy56S zdb<$YK6#e~Qg960TT7326COZt3{1nSgM-QKX=V4GUXa}CHMWTdk&xt`ZGBa<*|x&j zg3Grx60>6KrIrg>J$aT+5GA~ZiswlxUznH{UfWs@1Iudi=29iaK2g$Zh3#&;y}Xh@ zPQeN!Zu)W=(SGRnJI0*W9+Yqi_lpjVCSj2#@d|egdhI!qFiO?t_Q2$b&z)TnvmyL( zy+yUEmD&u|chJeS*tV!7Z+Vh^7N)#f#roARysQUMTSpUR?s+MfdJJuQ#fjc_m2+~( zWT}7g8k^ZW`=KFK(e%T058vUkl&k7EG%k6}`?~v}bzPUe?=c$9RnaZ+J|99+w~xM5 z@)t1Jb{SxEZV4sK59AvoZcB-UB(Lp}`()cIY%r4DWr?WsldhUpt`auS2j$q9p~tr3 z(7vf2ThFd4Fz)tPaAY=#Xb0Dp8=S$GMMtMWr%}%3C}9&O? z=U$!$jky<0E-D5Cl%V(jvG>+dSv76nuyl8WbazR2w{%LUz)6=NjRMl$iiC8X2uP=t zlpx&*Qc8Ef!}Z+vbH}&7|KIhlYpwGS*bDYKduH~`F~={Ck>i|0*~$zbhz=r|0t`&+ zJNM0jQP`Tlz*n>ZED6}Z)@;Y zRj*}HycabYTQA*r4pU1XNi&DBUx%@o$V%96@|oNnvOUaI=8rb(4vFhR7;d(R;gk9L zXRTv=36=5}8)PKrZly<6nXq$^xc?36U`WgujKY?~oDbzWd5fLU)6V598?h1jbAi*N*yqZuGs^#0pslFU7$hgZsXEPSu_=U!R}(KS;?A@Dq%=bzxjO`}CRe zrrSeth7P|>$#*goEh9`Em@{E#nipKuTS%bLGz@04W)+O^c^T|TBH_A{LHZ8()`ypB zf?WaFTW zan#$5lzwbsOf9j%+O50cET?gedgFL-UYH_)mZ4*JX+G*SL zC%aLCL;UBrF!c8|)7z$3KLDqJ)+d??TMz*pXJ1IV)wj#Nu&-MnmMS@bns|o{kk>%# zhXrdlE?-E+cgpLx@1~Hjs=tMKUi5}RXAu4{dFMi6c-DR7c;~tM%St~cd)p4J`e>N0sPdmn}9PQhV8L!4cT0pJeuEJ9gY9M#5L4zw>O7U*#R=m z@V5J(03s%nHdr$L%?&5>UKDW1W>%5@=%9ypIas%cc=!Wj2=PN)vZ+6w_Zt;+H#_$D zyzZY$s!SgQ|3tfa1obou71KQ1Il0apFEcHYv}gY%Zvn7^?p#qQkfui)X6+}#es~gv zqbxdT2l*&b$=7TgWC+^k-<(4CnQ2uACMYl)L@1{ zg7eFYen$}7i@FWPP{zUBPBJ^v;Q6JXhGmiqjLZi1F!*JuRlxXExukbmX%8%bR;4jj zkXza1c4rteqH{PuyUQ3ave5Hi?yOZRq|@yD@E11eZWjrw(}^c}WWDO5ZW8pu(UTkm zAFn1P-bFE7#51jC!h(~%U2s8=hk#M$xXp+m)sq()?nGIl+RQ)4#lvDbj%U?)2`LdS zOSLPXG8|I=_|@N8wP0%;2>Gqjcjv0Qf%ePN)u~(JN-%Mo1tWnaS=r zneFK;F`|G_Qig^AQKxzlYfu*SpyAgKH`n09ZQN<@E8vsTTS1M>nh?hBCH!OUhSBz_{4oTVZ06}JJ`t` zK`0NNf7Co&kM#WQV5+WiC&L>~2m362)J8OZQv!-uUf^@Fp3G{3773dO!w<8gn1F`C zGP~Wr*J0VWvD6u8ZKtVwmyT(;sm@o^L5S)ZU;c_*h15ciuMFEu2lp9mVbGNp~aTXaStYR7aaJK{_@WJ3OtE1rOZdDoz28Fv6H>KOc8Pf z*t80=Hc)K-YJkXpH2{HZX#*-ULfo>R=@qM*rvv5s{CqZaF!#c=|GaI6+q@OwWkz#D z?UUI1)2f@tFI0yy>yn?5j^r|u|1(7a(zgv~H*mHwHh_2J6ELW$2_h*VZBKoawkuU- zlmp1sXG@%}GhA)OYdmILWO&vq#LVg`HznQEfN1(tUtF!mu;yK44fi?k9@yP0%AVBheXT~<&JxV7Q z|I?mBh7i*FJ8lCP0k+`r;hX=meAqxWK|tnDYSpIlZZciANioisMAr#6ryUeQI1Lm% zch4TSbrz&m!jGK$t0DJ~9v^D%|7Otg0O0zgjv_5f{J$(ok{SN8BvJX7JH1;jsNA=vKj=Q>bANxg&5)SUi*b8f4Pd_{>$Ix0g#B+gVa8K{R=7g zVFcU=ktMqHe_6y_nt{Q6aAcvEmqG}Ieo@6w>8)h&7<&rnlK=W6*#_|ai2;a(f&b|q z1AaR+1pl8)p91?2XPNwL*b7S0H-pyFIRYz@DOJElAt;i7HO(Qc{HvAOt2G4u2)WC< zqjia+Z>$d|EYjYMug}pJE&8(UQW?9!bNmI_|K&4-U5lzyb;rzM(iGnI zLT?qM%DO{(6f|#=r)TRdF(+D6wdz|O52Ot0AQ*v5^%PJuU%3GN;_;Hw{fRE|aH9Pv{~rRkec$`I2?|tD{S=S9#all6tv(Cxe-On2DPSqU38&(7n?rmM5rlp4L!nZ_M80 zFpAp~xv;!FbXgktdDy5BBr3n`!(#c+po_zh3&Ni?ebj$g-9$nVp5J>D!jlyZ79cTU zU;VD`NNjU8ME^Zw(uHJ)7M)UsiOFUAoHWkz?aBI%SrE0t+6A|zqr&tu&Yp%dJC4Q2$XUpNnymH?<{9Sr;RBU!Q zlE6J%>p(#v>i=eC%uM=&@!YLZ#3Ldohp@?J9NtyHz^pj?*+%GRe>53=vsBH_LZ#R; zyC!#)PC2)OZh0j9(R$uG(MYA<``j6J ztt(~y(uld0SlPr-U){javBFZ1L(P#g#T+J!#(NRCY_%6()^9S=T=#G3Tea(`gumRO zE8#NA&WMK{=|yc^+KdGncetUkXHE8}mJ8Il?ywXprs2!9?}UHRqDNxqKOp1`%?+B1 zj)=dPyqll;-h9>nidv8D)_c>(pZ8VWH?yYO*TlT`hijgMui37uGV=ZY;tvT!paj_oVRfg(!)tuPXkPy>Hg4KN=wP=QLbK|Ji}}b!8$7p)1W;* z)equWDtZ+?&Gfun9?Q6pm3ksdy~<$jM8$0Oxi)TU+mkpgcldpzZO7$!kBdK? zj;1D2RW3=b&@lXsKF++y`}B4fjn9=>*snIU{@X8=@|A5P{T4rZ)!fepsR}iN3H`x8%op_-ee){0mhRRi$*=f2JM|^-;QBFCzFK8zQwnn7U0?Ym*l+ zs>)OtFbro4%VYEjW|(JFi2J|cY`M@Tb(yav|FOx|H|ZAU_OM1<&N?4TU;aI4R?zc0 zUx>#}G2(owU2`kMT!GbaezNp?xyp-*KpF7MX zn|3K#RYp=sPM{CpHEoW}N%<_wvT{~WXzY`|ZZG>pX31VQdyxH^t;cFPC)XNd#5|O^ zuYkKg`x7bX(i$=*a@H*%)~<80wOJ{0IDCr^?5rr8PbuE8k^Cq>-a)+v{kA>PPJi!z zjK9}%t}B@&;qv^;fk>!Yy|PUr?`yrk%#t92rL}(J&st9>qAzPeWJ)wbI@2?{bi?ho z`}FChFJHsnZ0!CDx?D?I?9qk9Luj%(gte>4;^cg`X)8Fir6_k&cZ~4$-p0un*|+nr zs%14NON?MvMly=)cFt`aS!ZKzoC&9$7enQE`)Ca(zE*XGGyGl&dLV~d^~)b`42m5W zpZuapX#L+!QUukN9jOxA_(e~;h{;g+*+B>vOImLpZT}R6!K}`|G~KT_GBhmoQ<>#f z(Ir0*2MA;VisU4G z(Y;3Qs9A7t_l@uEo<)APQlF>2w)Q z?fGL((&28xP%-zM-RF77Q(DwFt+2HH>b*zW#Z&X7cdGNvwlG=@>Sdjq=}SlzAA7b? z+3U>eUcM>lSI;wQ+Z1ea;pH%H&R-M}GL)`)`2$*9JAWwnUOwiA;>Q+8U-+lbhx3lm z#I3z3y&=?i>g_|*O}glo3-W`09p)tF=#!v_`v|;__qvk2RzF=ueUD}9)i2~)ZjODG ze{<(nzQJY?FUWj0OQ)U%A9n5CMHx>i0%5F_RTDpenBe(dedDX=Pnb3r|FS!E{bXnc z^7Y%*3ag4hvP?8ah;3&u%=X+5%TYV!bPjUck*v4Cge?zzAF$kcbA9vPXIbh;k=l5P zW=7)&W1B}zkJrYfKh?b@a7q#isQf4TRi2Ct`;%D{UGLqhqwhpc6 zrJWC5J~(E#s&y&fMHV%RsLq=b~5c` zWB>D~->CFNrDJba?5<$wh`^EvlRg$h^T$8SLBs?@Xt7bN&CPX5vX;T=f#`m1OyrIWE5ld&ux_33NRmRE&};(hN&>0 znmd^a5tV9x(&(@m(4N>eA~LBq+|eJ_48C{?<>5AyDdsv)PCd<$3NMIe|E& zaIyYW7=PO9&xZYXqu=`hcE6+g?C5eo?MxKPw=d336-GPdPx*cyct<>W!z2sG+-LzX;mx75<)^G(@W$6ETU$r1O%(xB#0{k7vyo25^+Kvo2#o`VTxJLn1S4Ib1HQnYNa3-0Fdv`*&W_{ z^9Em>$tQ}`qJbyWYuyOzsAW`iVsX=}P%_T_lJ8aunp&qSE3-)QT32;aPQPx(>fz~G zC;P%}J|}RfeKk~TF7o^{ZiaD~afy9z@VuHFfetC6- zQ*?=?44y)j0%70HMc-2rLDUw0PML1_qZG&aHY6<9onyW~^Q}5>H8!7wRAIL++cznt zaJ9RKkP2_MAZxZRGwb82iC2a7ojJb68^?K+2--2GkGzkmFyyhk|4Co~wM`~~>`s-H zg_69i8Ap5P)U`P@md)}T_lLe1ut9*r`g*Vk#A)9I=~!r=eFp00d!e$KS=ZO*1zplF zf}hR$PkB6Ps5YypCF=^mzxt(|E?aQTH|64~uJnNo1y})D%~)!8*uOxC$Jo`;@Bdt# zGfpmXOUBnZK}dVgV2&qtFqJXSYUJX3_X9ySLf91QjfY7gUbHBHLN?>FthZmP3ZFf? zC$8;i1Qe}4lVKecJtb0YQ&|MC2qwn%(56>AEM=2Up?Yd$KR@@K=?JNR5ZApRG;#iH ztz6x1MbX^iF1iY44jMn3-QmEyZ(cE8d=7J(VvhFIwW2|sCFk{!6`7@zYZ56U;Gprc zrc|rF&PZje;(e2cwXWm+=}Fw!uvd0No?oi_H_IM7wyv!|SJ4JMc1ffwvk2M!@s#Vp zb*~?=O2!-AtbW&D0{T=wuzLE&Bf}bnm<>Rv0VC7tH;6*dH0F#l+-SX;glN*55N_D?MfqL#} z=~SDN_s5aTG(ggjSKqTS>WRQ7nU+DfpwF6JOrLaa`9)_#W$we7$dUD$Y`B^Ij)Z z)~&E2g1oHSgx$u>6_8s;ObeO)5D<|n^yPK`^tZ}BLmE2qfa~-rKE|?_%zH7JsBC6! zFk(0Ho%gwVKYNMHbZA`V>m*Cdwig@T8cmK8QO@|dCYF65m`X-6UdXGYgwH?b6PJpA z?xZ7*Dc$%g_ae@lh__6&#@^sd^Au{=ifJtQ3yfJJ4wKla=%TU#hunt!3S$Anv7C>_ zRF0MbF`(fi()DBZRzK}#5rZ<+15u9i`a)p&CA!!HQfY*@M`bT^@Mp$ zgkZc@3C{m`JYrkqRUNFTzB#YcxpaM`?_!P1TSkZJj|PX@w`<#u0gK9eN_^gmW_-5h$_Z5BMn0FMfbgQZZ=M6sqQ7o6 zeC13xQLM6e9-vUOcCxuggODPZ{o9v8^?lfRW!7q#vQ6sS^yy;t-GGwr4G$XoJ**$P zFYUx><+1i-fdwZSMpnH7IPTH_{o;JSiBHR3p_hrokfRQJ1WXUxdUAEgb$4@Lc#y~O zJ3X4_qB(@@0)==Q0NJAw@>thU66H<NKCu%Zk$c0bgQ@5_i6y^c*y@k*XyEk=tNqoL5udh7?s-T+dpGODO|GA!>SZAR zIfY=T@!aQ+zUzJtQZ5~N>opIniLQKsH(mjNTA;9dVC&T$c*4f z0(E3|!>5-E8*xEuFu1$+5_~b*{^*H^35-vKeD;_1!>Fu&K1uj2?Z@A5>PaA8BOI_~ zX7htrLZZST+Ws@q+EK3hBy9Isc2FpJ%X#_PlZvoJbwM(@_at+Xp9PKC!~%YDwKThr z#q_a@gbMSxqvF}k?1bmrd2i4VAuQ?!!v%5_ZVfGG5bs3e3~o>NI@bENZBEyWi9R30 zbvKs`@^~*;vL#l5hqQ*_pf3~6fI;)@9pLnobGK<128R|4$1X6<;1q>>m6apZw{E6a z8B@YQPW?!XxIr9PNVxTbfZhJ*o88Q>I`z^d=vNKdehu8SFf)kU+>0y)2<#dBgh%VW zZU)tHonJ-g#nfLYH6liS?o&lKQPU^V2No81vzLDgBqD|JQ}oeJ8%8NVe>s*u zp}Os;>(VbPHRmXX$2iS)AYRje^zZtsuF8uAQV>309HSq4A4&|IygDt81rIvRn~Z zp_twHmWqaeim#!U3EqK{WH}kh2e2KDTVH85;bKF?Z~pKiXsWWmXpjXyzWC-i5byoPcfh8m#2foNAUTHP(ks82 z_u||;(u5gBW>8L%91$dBGVB%pTt>;{scyee3!RS-Ww5yRY=4nol107x$KsRw0rL-+ zHCSJbtsWMGdS_l_l^i$0R~s2#t2{P+uzbO*fA-UyME2w_*P=}n#PTJsmTs`Vy9^lA z6KDJ)Oa9EnObTgynXdU^fBALr;Ezovyg!`TX{+;R*p%&|~PN zFs@77cec=FrBiWTW!BJrg_b@A@fxe1FC(F7{^V-K0gH)bik^70mCVR8k4A)u=PCGl zxC_(H{^^d;0d< z${pOeEZ&H()y=ye_aw_kW6_e2e;Uo$o;cbbw<=W5R9%`8_I<&*(0|(ZyV`d`v|f)& z>b7x>0Yj*K$WW&ZQ<7+)9?|$#g`9Z1bJjVYB{fVggt+X<%1Dl=VxnkibC94fF5_tS zbCY4A!PiH}1a!(7RNWsq?~@Wu>Cv&qp1`>+`}Y{JDPuW}cxKM7O&yr9{&{e`>hl5sXgC@9KM1v`wq> zgOiBMtle*kdiJ}-dtO1~oMCH`a{t{%@io|fj`8?Qc4qymoZ8dYqwu%0+f`}8UJk#W z_wn&-7a8zBbw`41H#hiSxg)R%1K(s6dX+RCoC#!pPp1MjQoNZ(SMRLf_Q?G5Lz;jS z$@S{cyzV8A*R&b@Z<@$&XWuI;RH{SP@~Lr*h;1Ty)B8#&Ju%4mEV4*dc}a+t-71H+ z-~9lFnu)26+ooX{L|b|lbBb~r`DBF21;`RHgt9f`n(?pLOl*|X*=$Xo4ILQEDFHqT zLfe~4B2$0r#bGEfu$Fw|?0qU=)9iPLcz?Gg@ki_xkWirT3%D$M6Pxq`&StduNuO*0 zO2lG<%9yMNFE>ZC?FLi$^b`k2oaZkHZ-C`o$I;4&);?21m8|Afb4o+>{~EVT9ul|` z6U6GJ0o!N`u>F3Umx@*a2DYh=^S}sD=gqSPK7PQqmdclXhac&G)JycuPdXZF=1f6(dd@e+n` zc7;!qbdLinx9-AR93ZA5nc0+bo1=2^sAo-*{7R=|Jo4}Ln92A z?QL>xxza^Dttcg3SCDc&2WRU6cVc7pz5K>LjOYVK4(t zwp(2ei^$-y{dny1DKqCc$@bPaCiXbbPr2+~EI0ci#Js+UMKKhSp;sPAe}of5<^I>^ zp6Z%)74UrJnF~^7iXR;m1*!pQTzIiR+*Rv~vxy_czK=}jYG}}lnz*{ znHzN9WVA7M3X&-$0ZthaWMR7?^bxWtovX9Q1Mcuu#*>YK5%11MbA+_t7BS3j45ecs zVLsKErj_^j;pmqCU>{2&R#@#_s_fPCmdARm$va$p(b#+wbJn#3Z9iwIx7Ka_t5j|X z?UW_oesw4E)5TjQ&S9G?m-Z{f%~b4X4FBW;aGKf+bYl_Gw7~IK* zQ2+B=Z$w=O0nKT8c{y5$tJ}&K8MX_;t|GYwXYYb?S6<0|OjR?(s>HQ>az}oM=1mA! ziWSJa>TVF!FI=9Gf3xorx;$}UGi)+796PoA)1#m+ILUX;TFsmY$K;D-(uVdIFG&zPCX_{y{d($Qe22nnNlR;myji%7Pi2W`Q#Vk z4`i#nmGOe-uR4bsHhJ^<`HM`FqP$Iff9?JI<21qJj=St4(2Oe6=g!U5!^dCoLPk!= z;qKQAOVm?5+N2or>6zv=#+xibP1qQc38bx2v#&Q(q-BZK+ zuZ{owalL&hce3qap{)L|N(xk3_^v4Yi4!aWtW)ONd7(CmbNLU{y+&X`S%~8`P3p(NQW*BFO{GnEs2weo0yQ0kdh{kor0T?KuJ$eP0h}}gp91Hj2!9y z@Nm$S)AHHJWw>%_Zt1|6A~&_o&UVy)baCz^^FqvS0|-`9^W~z2v(JH*@?bayS4g^a{Aij*JtBG&&*eVOT2G-6SAaYLv=?Co% zhnXsNV9TC7oXI=1ztp1UWc610Vt*-l>%;5ke4l`rhv((@k4%;)-&m^^f%xI8^Oqrz z;wbdonK<72vZ4%qnBE#iq|xG%!K~8QcY`pwNIA zb-y^+PH4a*q(5qu29O)x^E+iF!C75_>J5`y*zkX_#uMFteH+{!FF?>Fkd+(#AjZo0 zJSPEYmsApIvk(eemTYr*xOO~i6+ak9E>dXL5qJzXGDWCmZ=rVa^Rhc|`}Fnx_@Ngr zZJ**i01e)Lskt81Op)ihq!4udh^r$z5O}$U)O++pU4o#nEtXQkbWkso$7bW^phLgk zr=I|a<>@+5HEQiGe71`hraW!Yv~^no2Yr>Z$JqK4$V*RviZWh{bV^Bz5t!tY2O8fW zw#)YxzNmm&BDz6T`3Sa+qkVDY<-2RMU_vDV@KXKy;-LEaHwYOuHPHf0^4WplQERIs z=pisfFl<2vOKjs@P*B|`rv_{0XVv>KB&;;f_Dkub+-LSl!r^l#-ZSx_j7e=&vqf&> zy=z-Ryqn2KiMXdiG>`Rdnm0kBp{NSZpR?_~K)e1d;?~?pDY&^Nk7CPAkY;`5hPAHr zIQ@flQ8XYG^SluKR{D>6MDplV8tGyj;$#NLP*tMZLuRVY ze4I}}<$%)2u(`;QN27H_Xyl|hb_9$MPz6rjdkPaFdPkDxQ6L5=K1_s(qrc)d?|I?| z@G$q|4JF@;C7)F5#bz&+cetn%&FvsPc7_M1AuGY9@wKyAJy*P}E#b`vvDeB6%Wa>4 zZfkduTd+TfIH83Qy@(nmf#@(eSSrbL!R+-OAK$Bfs;7H!66g$xwwp>2D(VD*fhXTU z`jYkHV8w^xI#GcFCwqheU@khaKurnaZ7M@zbQw&p$o>v)E>6?d#-vCvRm#poa5nI8 zsL=<0PnH%~!&}0Yc1d1=9K604RoQ+cc%$`M^eRfO?{)v4x6pxLHpOh_g}v; zkg8;MrJpj0P4Pr_C4Y7PZ3?C6tM8dnn>(#;PyN$DDygE~?D8Jeelo;-D)hP8#H zotCcex>yl6K|i*Twpx7*M~1hC!-18xs}nT`#!6kVeCEAWJU|cYfE~klO59084>yjb zh>93S{ARsWpaE3HNfyRJ$E5beMn!Kw0&DQ^NxPEh6)>`5s3MWrh--e%7>Etn46kxZzg4sPm0)00Y!7?mR z3LnlrJQ4*w2wcj=@@sVymjnvgI4bspWU$Ybcu4utUE|-q(P6EV02w-mpjkwBghZ*3 z+n+{X%_+CE^P~}qW7-e9(UhwWMSS|3d{2rLo@1k#xSBLCP`F#F%JIHlZk!f7qou>K zgcIt&6%aL${6-R3Z3y;`X_CtBr)26|7@ECjSlq8q#}!ha$ZRn2--b!T(n-RuF-k&F zB&;Yx;PbH$Q=^J#`H6L?c|sWX{Ssn}r$VAIv>e+3aiU}c# zD<@)?EPF9uXFo+}KdDfNSR2A`!4;>7q?J!agiXTEuc+Or4oj<_hdJZI5&SSn}Ut1`9|DVq0V&#u~Zl3Ti zg&Kd@qH|ST11fb2KaMEt;MpZfMgzOkDTx86f?j*pNet&(2xkxN(sq=k<>G|9k%50w zs!M!I&5qt3R{T>jvB#VO!5%_9oPWn89DwN|%w+}><|+F`1N0psVsMHS?5Q~p4*YPn z9i9YJ|9HBYbNlei>pSqdtU zT)U+}7$=Z*!|syaC8M(i;e|u)Wl&AOy46>dfM^|-0qo5cNj(i$u~nmP8vMTBh`ILA z5J;m4rq+O{8_#H^G-^)E9`$UMrPXFDj?>9x{I-ki?@nX{OE{8oo_!_A=|%6Wr_)?WQ+l(87>>n zuOrE?(RQ2@v7~S67J} zEfDx;@EOF?5#jXFT}^rscN~cSSef$!P68hhqPLUHfja@e5HyQD~%@GIGPtsc}OF@ZfdE zPNd6nKM1@P(oR5imE0vGl$OLtouNE`7up_fZvI-`5-!Bs=jq{T-{7m~9~%14kW3Sd zkTWw+`odtp1K@Dhb)rF8b^~rv4n$oa=jJ2&SE|>i}gKc zg3WP;;Q5eP3%^k>4b{IAaZ$e`E_1|TektsQJ$y(7I}v#hJymt1@t*fW=5dqJqn3n1 zmDOv==~ksBi_O0WxL4)Hg*S}u`KYs?{jI9!s;;)AMb$0tJgtY;8VO!vX5dYV8Ls)#W&xHo@ zf5k$7y-troQD-f4~HPot5cx>3eKIV^i8S=&;lmEAi;1A5xz^Rdq zd7Ab=T8>-uco&lyTtP;Ql|v&RPi z+lTG(E^}2DJ^y|Y{5%XgEQ7w+?_bUS>k-nxU7(Pe8jJtB{&%}+)W5gXzayFd_X0=_HV^54asmEc=l@>k|F2!=6|v!+A0}C)a_j`3^Jw-`tj)4t zD8U{jNmjRI|NQ0K`&H>|K!0M-f{CA7D!2cCPKlAc@KpR?f_6#l<{eYR8Omn0nfUc~ zFdAq%u!Cn$=M|cczEt~duruS61(ia@|L5EqN(XYS6${M#A#Uq>DCtjn8EV>#XW{$U zw<`fM$O2sOk{-PyFY2xOF`2arBOi&FQAC^^fRgX?IwyP@~LqHZDcpOUj0%}!H0MmZ%;}PzbMiP32mj0Qi zmx0T9k!1}8pYYQc_}QI}2yFyk5%b3Z7WLBd1f+w$-Q8SFty0iC&DR|Rd`0osx74W) z%D_vDN1^T114zu@Bmw)mou!s9*B88FqO^~|5HM5~`n3f>Gp%wxc4p(o8rAp0eA^8X zuaN)(mA>%BWe9kI5$wyjJv<%-SBbzO)vS?;KV|snWYI071Ad-K z01sq7BIPmn!^piZAyY(&#wRg<`Y2kYJpOLp!6Eh&}@Lw>O_r&OuflW zm5HuS^jr9!yfC6)BO@b`ub4kb1WC1TODJBS-Wn${sNo*pF;0?eL12IT%0pn+3H2Mg zd-N9jyWKdWS<$7Jo`Z*$eVhTCMx#^ zJWL(&>uuV>N1PL28OjSlRKW^HJ{e%aEbkgUw&J4DW1k9Xxo?fK$CC1a@6J*LDDWqf z7Vl#KDJp?R+W^p+h3Cz=?O>AJr^)x?0Y-ad{dXJ5>Kn6AD`g|my=v>hB&}vo`%=w< zaCAy><40RS8f`ES5?g!@zJc0;_L-BwR6cC~7``P;%M0&M>5b?Sdxvv!RBHKPB3ui- zZ>_cX?;Gv4?`yPLx4k^IuWuLzGpNHPvTLh|U>`?0{tXBJr3|%ee0Pr2bFeeJ2XHGtAJ1EmFFWXiG zj+OcEFz7bHtF)_xMk5Jcv;?4g)0@f`bjt@euo{@;!iDou0~+-XQvfM1$8Q`HzZou{ zy_~0Xe)E#Zxk#`N_^?vxsmg`TLh=_c1D4V&Sf!yyF-|9GmB)Umr1)eV)fKY~AZto| z10EKL?6k3n?+6gh>gs2W+x-nc1e_b1O-*e9ELh+E@DS3yxE z(5^5YYnw)K^Rq2Mlu{0`N-P)s&trAJj|zYGY*A73@D!o;XI10!OMrkg18c)LErJDF zCv<@>37|gKDt=xm4ywS;OsRe6v-}JYvomW{0af*zo)Pks;74dgw#oo%xjQ&c=F%z! zMY-Os9mQngU%u&P;x{-gSVI~qG)%^_9#68V-A~8RD%MjY;VJ28ohd2BXEib~s)tm0 zH@-a5y4wBiVm*FKX}u8lp69B?{&+qAlU9L|uJ1z|qE_MIiDk?z(ELiz`_C(3=*eSH z(k%G=Nf=yFOYc<)N-EQ3Q7S%nqSx+=nQn4rTTIf38lj#0Hn2`0edC-S6kl1t;Xa9< ztq7I+CNR;Fh#1!~rP%v{$0iCC*_Nw$f<4i8W;{P*`dUp+50#Lugok=_EVr6R_)r69 z4qqNBwNV6?ibt!tR9MWpf8y*;@ewJH__igljxiWB{PIvectnuG7ox#9;MEC; ziuHwp@}gzR>1H{lS8BuFp z^Ro${Uu7cc48l%*)EwxzRG>C{?-?;`7sW&!KXX|&exw6vbsk~EV<9Ax#u4aHKQo|1 z4jKyrqz8I3;M46982&cukP64c;z%F>aI9HaY4xj&cMpW_muWG`u_VX&t<@wU3AN_{ z2WM#-%$4ZCCA+OW0Obli+SxZ(rE?fmGd^nl;AZ&lKgw%qbuS%d54~!Fu!qbN zM+e3~H%@vQ6?^5#R>)V$!r!R0R`kVs5SL&_ih|?As5xIfAkdWe!n>XrxB1nlaGIGe z4FE2q4LwsM$~r@=FK?rgG(>a{OFfxN1yp0#(98ln9@I2wxa?YZJR>j=9zkV3G53~- zqQp@2_60%5JuHDjj+&%)Gsz};Q)%?65S>R{E053lMUvPHy+O0aM#xg5-t!z$davC! z#;rh!vi9PjJ1-L++aY$etqC`-;(^*x=s_*Eg>i*~KuX&0tkA1}7y zqXn0njn%xKTHm|fPGO-CCjw=t?{&dcXbA=#d}5mUP>-RC$OTdpR4Z>*Rvtoybx9W0 zRKsM_?3oBCDW&+8s01t}1gg~31OA=ADXHMW-l<0uHl|5MqAqrp5_KpEp%R}B1RE@i zlRwyPwCY}Jo_%?yU8Jh4103S%9|7>hr?M7@7bG5z;2piHp$xi(UW?_+mc&oG4tKmA z_$V1pW%UT;&l7z`+SxP^d}8m4{Sk14UWsz#OW~89tbEJBC2JkV<@q?2C84f>_tZap zrMm4^-*MtU2deEIhO_k`I^MfG69NniM^PwuRn%tbQC0F4wrSw+$}c)hPZ5|c4VKVP z><{r#)@)i`kS4;&*t^#Trg~vvSoJD2I{KCEJMPZ2amie6WORywdFC!N4$ZShsrAj6 zUZgUe(tPLI$a{l44X{ZgM5(P>hV?$1uyc!4d-s#GM2E*;-Tl`v_3vqy1Ye2a1WF?I zL?1G~L^M0~0jqfNvJV?YBPlY08A7@|h3eH`8C*cUX6`sSw0p)#x#WRK-{V45k98$5Xnq9vvM&xNH^&tyV<+HUZhDkC z6eVN|k(l|LsZQ?!Ppb5xLIXsAXRy5to|d1tzO)@HH$^#|)#1n>Dg9C=NZl?RO;AI` z+GahN^nJmgmdOCu@syz-YcCa#QJn=UWnKKbnsWn)Y-(TLKlQ!dDNebKA-(^kYzAVL z55}#YhPBqC_y&xOHI>rz)SZCYur|w>FR2$buK~%A!q}YMmh+3T4AbGhC6{v`(nEeb zOhps=vfND^eFMhB2+dkaRETHivVS`8UA;Eg8NkyQ@-2a;vL>Rl~6B75M zR-+jCim!-EdyBx;Hp*Ja1{16}`&ca>eVm7^PvdUr=w8g6#7z~1p3W(%a9xQSFDTNG!4K>(q@!#Or{ z{u(9r%P(@PBKkE6R1m}Wf!D{<<7}7DH&4fY>v%W=FPX#Rx!qU9*M4`gyAJy?9~{G0 zq-^IK;{8{CWN7vjnl8<|Uk+$)-5=_lwOvT0$V9}K-Hn)C7Whx|xlddxjAXmU#SW^7 zKE=iT7GT8ZAOq!QI=HXtm{e*`l0VPDJ^3_ZZAsP8xSC6u6VnStJFJf)jX#;R-FN?^$6q`p} z>kd;xCSEp-c;?yK08_eyO|RbHD{2%KJ4iMXHhAL~7kyRPI^(G(BI6YF3;u@bz3BU> zQ%NoCUq`yIiOgm9LeUpFs0{v=UDWz3&1;qO2fI9_lrJp^sUt+XW!Gf@8WytRfR z!YpEQA0S#BT$)-+LngNHx`McmMsqTR340lA+ev&Lt`yjp6IpCnd;vE`l^lOz)!upj znSH*jikeeSFzZ__>24gT)C}>~z5%^M^7mfE&|o5CS~3`W>na?w!i1f4BgeVmVDnyX z{96gr7>rUW`^83R5}xQ2?)H8}503=PI2~6-<)!9CPTZaYs36v6->QVH_jiiIRI7Xm z8ih#kNQbW%DY4H$wSstI2CW}Zx@Qw330ZLw*)Or~W98m(ArWB-b4yGwxm{6^Jvis& z#mqKFy>+9A<-Qw+pw&}l;*k09e2QPM*2%Qb1w9gNwy)CnmEV`Xpd^>6W2H`IW#v!p zox3%B@qh=veO3>{<)Abyw;WZDW!OclcJ_+Igtm#`+4MIC3gmUVQ9E=D#LEcVJ%x$m^^KnMl&rM`45k> zi1J&b1!7!uz$`7@#B(D!524D0@v97Apth$O=%JH|#6NcodgD&H^(4VCYJ0m0vwb5A zozONW_S$)dE*u<_yoscg_PtZn`=z(*c`+)3f~t6#C?L&~R&nQ*+&&f2LT?d&^t}i@L>(x*e(?ofhY}%R27+1zY`74=7-0=0 z^jaIIJnDFMjH0{PV*{1cMJKE}pU#SW?wJ-3E1Cvf3AioH4IQyz6L4-Z+cq$y4 zGSB=pBO>~lmKX|U-cm;`i;rAJln=_9g%Fku%$|lL8ilwzL>k6Q!b#*W3Jd8iPU=Q^ z`nc(*9e9DX)ZE*9o`wC{E-yQ4S0d(z$as(uR%mhDb+960bYE0w5?=(bKMvuPRLFMj zZ4x>m)7w3j$&*9T^hh%>s53_=m_3SuN@{#RW$dM2(@vvVq85$#nZBc#ozf9COgddI zGLw;ViRH1FwVmTg=cxo13ha=Qg9mkI>@qT^I~V@4aE~j$yymiBbqX{iWFlufaUT;` zvyv>j2?jG51)S;7fl3fk%8=4sP5*exGh(}rLd|G}Zi11z^O^TjLwuEj_zOy$A)QQZ z2`Eo_6N`)AcR1hOK$=>v7jCMFJ1N)myNdofRq~5XT9@8*E)h#J?A}|$j7W8sk2U=s zh~f3W%KWjy;YkTWaBM>F7PDE)n)6qDqq5WkA)dBX=kJEwcP=Y4ToE+Zpk6(H=(&hx zNt+osrkuDaARlWJvLV=n3#d}0JYdZB=cH5ufLQT@&OCVT8!1nYrrOy3Sel)^kcK$~6~6Ud z9@_fDaeKuySH;(-yu_`!Qht*1;4W&hKVD}8!6vOdIJ{t(j4V$z*D<(fLEa{6f4Jyl znM022M>FwiA%z7tqvK6Qbizg?jX67%$a7zFd3qJrRQU=5T=e%Ylgt;2yU$X?q&ll0 z?)*%u;|TRan4^$A6FkLu*82UCV^hRq4(y69PE#z?9GZuZQ%Z(+tu=B})1FBe2$x*uk z;L_=vH3y13I!}2BQu+8cRDB(u6obp|tb09DhpGKN(XA#IUZox0`Gm-l-L^n)i04X6 zzlZWj#O%gke2#-3Px7ARe&{SsDsKnUYva~E*zm%o&9|$RozFB*Qzh8MD>ODv$%eFL zNLBEiJq|&0It$D`Aw}q`LXA;JS3+v*hBWNZdF{}V0pqs{mTEsgVcsvsFG4DPCFs>z z@TA=jld`mkUZq_Gi{ubw1*kT2s4k$Wmr21!@Z4Z>y&Zr=6f2j1m@PmJUJOZWFN$2!(Sv>oF z=b&m9oN{ho-HZQND4ROV5Fv6t_-TN53%s@L;ij*8D9#}^MZ3nytitlAPvNW)N}GS6LtwlY@ijs~1B z@dcyjxr^k29idI}YoO%Mf!QuPh;sQ0;(9QErhQov!4)`Ws^J>tYgQETOjW3eaLl!o z6lmf4oz|GH!#VWq7VBMpX7jsnpC^h!o$PVa+VRCu%zqS$BV?E6=W~w6xrpUjGN-D& zLmJ8`CT}bkldwY(u!=*FeiG1Ax(tYVsGZbz25Wqx?1)CP(BCzNtCa6q*?x8D5QobD zK&Kr~W?hC4XqrPq%`S9+gPNcadl zs3sd>F$&vc{zmF}3KPyWoHnZzZylxHazqZKK8#0I@fZ2ZzLW)$jL&?W- z7ihjD^giW-6xyJt$#>r+$V=|i30i8B#Y_trH6x0AG7^PJlP2;gjy?ASHI0KQDbb_L z43za><+#WflB7y7KR%~no%Tx=IU*dEE{H@fC{b2gxlQH9m1smJK_i7nT|u(^x-pDwh0rVunNLK%Ho z6@%gw7#HScUQThHlpcx1TK3P8)M{seig5t2`TK_Yrr$ndaQmrp75< z*y(wSpg~;KpGn=>6}Pmi$Pmj?y28&jbne~oS7C|`(i_)Ky=Z%*qhN4342G$iP;k4} zb9I;|Bo`Wik!PRquGx}6pj*ZSon+;xzboXXWoye=3A%jwxqs{hw;u>jOZ#0ZN{s)) zuZpS$RQl9tgV4DTd^Cm{4+B(|n-kXz_g}fHPyVYwTAbaf=BgzD-5R(V{y^F@#mjiK zfx2J#G2>Cagxyg-MsMm&=SLFgT#jxaW~#-uZC|16JvYvGtJ4nF&dlx*3Pd9nC@F1l z%eSR_=ZOXHgT@lPW zy3}Qw#I@&;PvNXxW9G0YMW z!(JhP&x6b6cpJKyuIg|>X*wVjL%72@pn^y!Ba6v|m$G8zfi-|^nW3~03-YZ)2W@I4 zy>s$>y_Yq23C74b>PiGH6JmRz@$l6G@!P;hbLL!*Uo&vSU92Pk%cwXPBU2&p3; z{x|q#B(l9dUj2wOpZ8vAw~B~>nP5w$pm-is|chzPOvx2%zT=% zRv}H0@49!=ip-RxPg#k3jH>#%vb=7eOX{JVgajlqr4l_uU?qd_U_QwbHhJ2qr$!C` zF8hP!=Hxj`XV)9;5?O@1un55y$e+q5I=kMnE>^K;1xEIm0gf@D_D~_Oy^v_(&fVmFTf$D1V9pk7U zp4OM^zc9lJD8N95`F89lci4!9Nf%B>Q*(esB`WB%JHeFB@W|O|yEr)0Qt!Oe zFj9{eC5B+rhmHKJM#q*w>HsBuD@s2b4-eOJQjSb--1b@=jHcc~eK@018al&9;w~fh zn-9mmb#Wr_AjpUcr1>2ZZ@M+rx~4W-hB)APIo`Vlz6$?R6MqW-gwm(PB$f*cZV^xk z+gh3NuGkwn96myd{0gZeGn!N?PYgCChnRLrNjV^B_l;fefar2(#m`{BVXUEr{p#D4xH(9+oc39mZ-pG)w9$4K_ ze$U^66>JR?7Ngb^z-U_Os{Z>C)yLM01$99^0MC0X(S2+8H7sOblVO-84H3NH>PG#s zVwTel23?MJcXfxw!x+GXXT`F%=e(C-N!{7&Oy{bp#k7HqIONR!0OdSi)21>U-o$k# zSm2u0Lhf*7NjYkWC>bVSSTfT1(Wx4V3i9jDRUtC1es?^BwX`znD)MA&LUv+%BDqLiiKRg26`PUKNsg&ZFoIo|KkF9iK~m? zR3n>geq^8WXMz1`{`pdTQd$a8gbuRzRS8lt-Y+VOa)Xg)$9PTzeWN1*m5Hn`-`L|t z{6+0>6{?4O9YDkK%*YEp!-yQCmfYb;IX4G?2k6_iB#DqVdhsJCO@A z%z?Cp#*`}@QbL;WF37uwvFQDjLL-vTXTCo_y_C5ED9mRR1iHHx-tHY%P~JNw#D`$^ z);Dja#O^K_-nNYxsWKYpPW=t6g1w{2mC{aqnc*(+D*bNO{gW+kU2=3li0-p20D6T~ zS&RjZ5L9Yv6P4o;8W~Gi^5tb&%Kex40fUnNuGNtG1gnSDs~O53?G1`rXn04_M{Vx8d%p8Ty`f7wE^R1^`CBl) z9bYf`RBUPUIN`;6=I4rwE<=aOa5{7BuU&*c4xdO^A=IxfuR~awAkk5=&cr26Oh|@i zBad?C{&Mpq`34xJ#jPj~|MkvT*!Ee7TEH86?0iXl^o4m_J6VW}9OChN-)gZ%$=tI zh+`gq2EN#1HX}22Hbvf%*k$@R8E2ae+Bs1x1l4}wV4L96H;nL_`?8qjc99_UqoAtF z5#RmKP)c-0Yq?bTp<&C`GYT4Tt2t?BRa#kacokmJaqfi!G}n*q&AYGF7kd!hCV z;rd8FU!ql7SS+03>?}X-qAC#OSM0_#s{Eta*p$>B!+yhnXdbsxpiec3eBM@MalvV6 zP-l}UXv-z=dG1?XS$2LWwXb%nP%_j~Rx(+spSp8&I(OfdX6ro0;)nOC4v&6`8nlAn>)Ru?sax-RxoTC_3W%7@-rL-uLgz4#hJ&LZOh$vPk(5`EulL zllzd=A+;(k1MCwI_pP>MoD8&aT2HoSBD8ak;Og4tUjvN?;v4WYv3nO7} zVZDUo++f@xwHPpJ{IrL};QhtFg-h4v=f27J5N#{pENnu95j^%-bWIJZ<7)a8oV$#! z)r78VF}cym)NVEk$kjlTxGh4+&g+nR&*!fMzA!#zfWU`ud3Mvji~+1V?HeLFCo451 z=M@h*OU^4Ua*hTuZ)>yFeFG`8IE{L1|4dpPpW6N&RF~;1&l7GgiNX3SA!r%j`_&RT zjnoouvqWfbjsbjZAtZVkA3A!QN>Op|mJ1))H0T+xeH2g$7#iJ{dzZ0|2&EbO=C>XqOiHxwxR89#GLvp)nPR@AQ2oL99MaQ@~ftYtS|ApthuRyZM z5Y2@@X~TytFjGv5MrM~gnCYtTZasn3pDmexHHmSAI<1;}@TbQ27{(2E6Mt>)j_zMq z%5@JwN&R3R!c)i1tW_ZfVnFwy!+?mg7DAH|Z84TF4VVyXz%XdE!P%<%wMd))Q`4oN zek$LU$m%^#i#*;811OLRECDtUBwVOnUGx&4O$?AB$be67Ebsu3&XY+nS?!5A8ekvp zskWK|6bb=lwgf(Rhs&MH;amXP0T#x;Oa*`ovN`=#I^7>i=j{hsuYV7@J{~-X#X+c` z!|}Sj#qV!^UU!!pM+d*qbo@(U7%(PCnUGgrmiv>BpMxXTc(?*lmtENc-!jz|D}~5^ z8zG43NUVFU+BYZe?3Hy|?Q1aG=(8say_q-AEg!xX)7@`7s9iJwUB?$!u6Ggwx?@AS zwVV&T7u-$8{g_&cCw66D_uG$cG#fv z-d`5~W{YwL8QePSAl{q0L9WEPihTm(dp^dM8NceY?Y7}+{`e=_=ufMWs1xGt_;ZH< z1Tw$hPWuabz-g8lX&;Y8U-w`k9Vndp0QDmNr;ugK?U{}i$R9r0oz?uxq(cqZE2<*? z*foH@qy4~mc~DAMASC+{3C9DlnEHWMq*H4n4SbDO3o?CFVzOv5y;L6Ls5uf5v7lYv zABc>F82kCQj(2ES@SYLCu^OZCyskFDFDfRMvTEM4Q^v^H_~h^4)98RI|VH_Ab<4% zZ)=nPfm*Bm2`Qh4{}0fL5!?xqCQX4a+k+9qGzg@`X4ZZG5M8PMrXs*U_|;i8AbLor zaltT?A5h(4Ql5V>+y)`&4@Jfp)Had84d8q!<1|NH0GtA1$;DBq-qSvK7>wisu7p-( zD^4f%@4UQ+iE8?9qG&rnKy-w?r|mBC8CO>L18^3GdhgPGRy#e@jg|mU3UPCj<#m*# z0}bH@HiF1JP03&8cT&kV<~`()xJVvyj(>h-tPhDHurXS#*Z7PN>#+|Lm_|hhX%YKZ z9#XD$mHBLCKb5q_qvMOgw|88Kh*n94j@ow!Ogo-5jOBcdaFEjueIYJ*s@3EMvt%Zx z1cFVaO~VIs*g_NVXz7&7 z6;erUN}V{wdTPY(#^iIA5sKxBD%`RRRegIT3M+p^l<+eB~ zF?&)LfRSqW2OwC`1D;`(<%EoP&i|0H^L#p|H$x#i*0bdR7Dt}3kSN^QU& z*u_YH^#y0<E)wgWo%XNi~6T(0ox7F_SBM;;fOl<5MzS3T7V3ftV~aIUGmmAC<+m zMK@Uf^;(Ab5lwY3Z?g3kOUp4DHBXw6vD{

)VL%q>zr+pQ*6>MN-#xTszDr1Dwi}Y51DB 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 delete." + ] + }, + "authors": [ + { + "name": "Biswanath Mukherjee", + "image": "https://serverlessland.com/assets/images/resources/contributors/biswanath-mukherjee.jpg", + "bio": "I am a Sr. Solutions Architect working at AWS India. I help strategic global enterprise customer to architect their workload to run on AWS.", + "linkedin": "biswanathmukherjee" + }, + { + "name": "Giedrius Praspaliauskas", + "image": "https://serverlessland.com/assets/images/resources/contributors/giedrius-praspaliauskas.jpg", + "bio": "I am a serverless specialist Sr. Solutions Architect working at AWS.", + "linkedin": "gpraspaliauskas" + } + ] +} diff --git a/apigw-lambda-bedrock-sam-node/src/nonstreaming/index.js b/apigw-lambda-bedrock-sam-node/src/nonstreaming/index.js new file mode 100644 index 000000000..16c51c1b2 --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/src/nonstreaming/index.js @@ -0,0 +1,66 @@ +import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime"; + +const bedrockClient = new BedrockRuntimeClient({ + region: process.env.BEDROCK_REGION || 'us-east-1' +}); + +export const handler = async (event) => { + console.log('Non-streaming Event:', JSON.stringify(event, null, 2)); + + try { + // Extract the message from the request body + const body = JSON.parse(event.body || '{}'); + const prompt = body.message || body.prompt || 'Hello, how can I help you?'; + + // Configure the model parameters + const modelId = 'global.anthropic.claude-sonnet-4-5-20250929-v1:0'; + const payload = { + anthropic_version: "bedrock-2023-05-31", + max_tokens: 1000, + messages: [ + { + role: "user", + content: prompt + } + ] + }; + + // Create the command for non-streaming response + const command = new InvokeModelCommand({ + modelId: modelId, + body: JSON.stringify(payload), + contentType: 'application/json', + accept: 'application/json' + }); + + // Invoke the model without streaming + const response = await bedrockClient.send(command); + + // Parse the response + const responseBody = JSON.parse(new TextDecoder().decode(response.body)); + const content = responseBody.content?.[0]?.text || 'No response generated'; + + return { + statusCode: 200, + headers: { + 'Content-Type': 'text/plain', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS' + }, + body: content + }; + + } catch (error) { + console.error('Non-streaming Error:', error); + + return { + statusCode: 500, + headers: { + 'Content-Type': 'text/plain', + 'Access-Control-Allow-Origin': '*' + }, + body: `Error: ${error.message}` + }; + } +}; \ No newline at end of file diff --git a/apigw-lambda-bedrock-sam-node/src/nonstreaming/package.json b/apigw-lambda-bedrock-sam-node/src/nonstreaming/package.json new file mode 100644 index 000000000..26254449e --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/src/nonstreaming/package.json @@ -0,0 +1,20 @@ +{ + "name": "lambda-streaming-response", + "version": "1.0.0", + "description": "AWS Lambda function that streams responses", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "aws", + "lambda", + "streaming" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.485.0" + } +} diff --git a/apigw-lambda-bedrock-sam-node/src/package.json b/apigw-lambda-bedrock-sam-node/src/package.json new file mode 100644 index 000000000..26254449e --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/src/package.json @@ -0,0 +1,20 @@ +{ + "name": "lambda-streaming-response", + "version": "1.0.0", + "description": "AWS Lambda function that streams responses", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "aws", + "lambda", + "streaming" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.485.0" + } +} diff --git a/apigw-lambda-bedrock-sam-node/src/streaming/index.js b/apigw-lambda-bedrock-sam-node/src/streaming/index.js new file mode 100644 index 000000000..32122c131 --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/src/streaming/index.js @@ -0,0 +1,94 @@ +import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime"; + +const bedrockClient = new BedrockRuntimeClient({ + region: process.env.BEDROCK_REGION || 'us-east-1' +}); + +const streamifyResponse = globalThis.awslambda?.streamifyResponse; +const HttpResponseStream = globalThis.awslambda?.HttpResponseStream; + +if (!streamifyResponse || !HttpResponseStream) { + throw new Error('Lambda streaming functionality not available. Ensure you are using Node.js 20.x or later.'); +} + +export const handler = streamifyResponse(async (event, responseStream, context) => { + console.log('Event:', JSON.stringify(event, null, 2)); + + try { + // Set up HTTP response metadata + const httpResponseMetadata = { + statusCode: 200, + headers: { + 'Content-Type': 'text/plain', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS' + } + }; + + // Create the response stream with metadata + responseStream = HttpResponseStream.from(responseStream, httpResponseMetadata); + + // Extract the message from the request body + const body = JSON.parse(event.body || '{}'); + const prompt = body.message || body.prompt || 'Hello, how can I help you?'; + + // Configure the model parameters + const modelId = 'global.anthropic.claude-sonnet-4-5-20250929-v1:0'; + const payload = { + anthropic_version: "bedrock-2023-05-31", + max_tokens: 1000, + messages: [ + { + role: "user", + content: prompt + } + ] + }; + + // Create the command for streaming response + const command = new InvokeModelWithResponseStreamCommand({ + modelId: modelId, + body: JSON.stringify(payload), + contentType: 'application/json', + accept: 'application/json' + }); + + // Invoke the model with streaming + const response = await bedrockClient.send(command); + + // Stream the response chunks as they arrive + if (response.body) { + for await (const chunk of response.body) { + if (chunk.chunk?.bytes) { + const chunkData = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes)); + if (chunkData.type === 'content_block_delta' && chunkData.delta?.text) { + responseStream.write(chunkData.delta.text); + } + } + } + } + + // End the response stream + responseStream.end(); + + } catch (error) { + console.error('Error:', error); + + // Set error response metadata + const errorResponseMetadata = { + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + }; + + responseStream = HttpResponseStream.from(responseStream, errorResponseMetadata); + responseStream.write(JSON.stringify({ + error: 'Internal server error', + message: error.message + })); + responseStream.end(); + } +}); \ No newline at end of file diff --git a/apigw-lambda-bedrock-sam-node/src/streaming/package.json b/apigw-lambda-bedrock-sam-node/src/streaming/package.json new file mode 100644 index 000000000..26254449e --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/src/streaming/package.json @@ -0,0 +1,20 @@ +{ + "name": "lambda-streaming-response", + "version": "1.0.0", + "description": "AWS Lambda function that streams responses", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "aws", + "lambda", + "streaming" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.485.0" + } +} diff --git a/apigw-lambda-bedrock-sam-node/template.yaml b/apigw-lambda-bedrock-sam-node/template.yaml new file mode 100644 index 000000000..653ad8efc --- /dev/null +++ b/apigw-lambda-bedrock-sam-node/template.yaml @@ -0,0 +1,160 @@ + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Amazon API Gateway response streaming with AWS Lambda proxy integration +Resources: + # CloudWatch Log Group for API Gateway Access Logs + ApiGatewayAccessLogs: + Type: AWS::Logs::LogGroup + Properties: + RetentionInDays: 30 + LogGroupName: !Sub "/aws/apigateway/${AWS::StackName}/access-logs" + + # IAM Role for API Gateway to write to CloudWatch Logs + ApiGatewayCloudWatchRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: apigateway.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + + # API Gateway Account Configuration + ApiGatewayAccount: + Type: AWS::ApiGateway::Account + Properties: + CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn + + StreamingApi: + Type: AWS::Serverless::Api + DependsOn: ApiGatewayAccount + Properties: + StageName: Prod + EndpointConfiguration: + Type: REGIONAL + AccessLogSetting: + DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn + Format: '{"requestId":"$context.requestId","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","path":"$context.path","resourcePath":"$context.resourcePath","status":$context.status,"responseLength":$context.responseLength,"responseTime":$context.responseTime,"userAgent":"$context.identity.userAgent","sourceIp":"$context.identity.sourceIp","protocol":"$context.protocol","error.message":"$context.error.message","error.messageString":"$context.error.messageString"}' + MethodSettings: + - ResourcePath: "/*" + HttpMethod: "*" + LoggingLevel: INFO + DataTraceEnabled: true + MetricsEnabled: true + DefinitionBody: + Fn::Transform: + Name: AWS::Include + Parameters: + Location: api-gateway-streaming-config.yaml + + StreamingResponseFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/streaming + Handler: index.handler + Runtime: nodejs24.x + Architectures: + - x86_64 + MemorySize: 256 + Timeout: 300 + Environment: + Variables: + BEDROCK_REGION: !Ref AWS::Region + Policies: + - AWSLambdaBasicExecutionRole + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + - Effect: Allow + Action: + - bedrock-runtime:InvokeModel + - bedrock-runtime:InvokeModelWithResponseStream + Resource: "*" + + # Non-streaming API Gateway + NonStreamingApi: + Type: AWS::Serverless::Api + DependsOn: ApiGatewayAccount + Properties: + StageName: Prod + EndpointConfiguration: + Type: REGIONAL + AccessLogSetting: + DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn + Format: '{"requestId":"$context.requestId","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","path":"$context.path","resourcePath":"$context.resourcePath","status":$context.status,"responseLength":$context.responseLength,"responseTime":$context.responseTime,"userAgent":"$context.identity.userAgent","sourceIp":"$context.identity.sourceIp","protocol":"$context.protocol","error.message":"$context.error.message","error.messageString":"$context.error.messageString"}' + MethodSettings: + - ResourcePath: "/*" + HttpMethod: "*" + LoggingLevel: INFO + DataTraceEnabled: true + MetricsEnabled: true + DefinitionBody: + Fn::Transform: + Name: AWS::Include + Parameters: + Location: api-gateway-non-streaming-config.yaml + + NonStreamingResponseFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/nonstreaming + Handler: index.handler + Runtime: nodejs24.x + Architectures: + - x86_64 + MemorySize: 256 + Timeout: 300 + Environment: + Variables: + BEDROCK_REGION: !Ref AWS::Region + Policies: + - AWSLambdaBasicExecutionRole + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + - Effect: Allow + Action: + - bedrock-runtime:InvokeModel + - bedrock-runtime:InvokeModelWithResponseStream + Resource: "*" + + ApiGatewayInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref StreamingResponseFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${StreamingApi}/*/*" + + ApiGatewayNonStreamingInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref NonStreamingResponseFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${NonStreamingApi}/*/*" + +Outputs: + StreamingApiUrl: + Description: "API Gateway streaming endpoint URL" + Value: !Sub "https://${StreamingApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ask" + + NonStreamingApiUrl: + Description: "API Gateway non-streaming endpoint URL" + Value: !Sub "https://${NonStreamingApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ask" From 7ed505f12e548c42dd98ad01a7ec7f8e6509c17e Mon Sep 17 00:00:00 2001 From: Biswanath Mukherjee Date: Fri, 19 Dec 2025 13:42:59 +0530 Subject: [PATCH 2/4] updated readme --- apigw-lambda-bedrock-sam-node/README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apigw-lambda-bedrock-sam-node/README.md b/apigw-lambda-bedrock-sam-node/README.md index d6aa95934..153781626 100644 --- a/apigw-lambda-bedrock-sam-node/README.md +++ b/apigw-lambda-bedrock-sam-node/README.md @@ -13,12 +13,6 @@ This sample project demonstrates how to deploy Amazon API Gateway REST API with - [Maven 3.8.6 or above](https://maven.apache.org/download.cgi) installed -## Prerequisites - -- AWS CLI installed and configured -- AWS SAM CLI installed -- Node.js 24.x or later -- Access to Amazon Bedrock models ## Deployment Instructions From 1bcc8fe0c1526ee26756028028b0d655a0b82383 Mon Sep 17 00:00:00 2001 From: Biswanath Mukherjee Date: Fri, 19 Dec 2025 14:41:08 +0530 Subject: [PATCH 3/4] updated example-pattern.json --- apigw-lambda-bedrock-sam-node/example-pattern.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-lambda-bedrock-sam-node/example-pattern.json b/apigw-lambda-bedrock-sam-node/example-pattern.json index 8af940ae2..c2ed9d8ac 100644 --- a/apigw-lambda-bedrock-sam-node/example-pattern.json +++ b/apigw-lambda-bedrock-sam-node/example-pattern.json @@ -35,7 +35,7 @@ }, "deploy": { "text": [ - "mvn clean package", + "sam build", "sam deploy --guided" ] }, From 70db76afdd0c69ff2c02cf25a91358122cd0cf67 Mon Sep 17 00:00:00 2001 From: Biswanath Mukherjee Date: Sat, 20 Dec 2025 08:53:31 +0530 Subject: [PATCH 4/4] fixed review comments --- apigw-lambda-bedrock-sam-node/README.md | 4 +--- apigw-lambda-bedrock-sam-node/example-pattern.json | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apigw-lambda-bedrock-sam-node/README.md b/apigw-lambda-bedrock-sam-node/README.md index 153781626..2c4ac90cf 100644 --- a/apigw-lambda-bedrock-sam-node/README.md +++ b/apigw-lambda-bedrock-sam-node/README.md @@ -10,7 +10,6 @@ This sample project demonstrates how to deploy Amazon API Gateway REST API with - [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed - [Node 24 or above](https://nodejs.org/en/download) installed -- [Maven 3.8.6 or above](https://maven.apache.org/download.cgi) installed @@ -58,13 +57,12 @@ The SAM template deploys two APIs following the same architecture - one with res 1. **Amazon API Gateway**: Receives the HTTP POST request containing the user prompt. -2. **AWS Lambda**: The API Gateway triggers the Lambda functions which use either `InvokeModelWithResponseStreamCommand` or `InvokeModelCommand` to call Bedrock for streaming and non-streaming use cases. +2. **AWS Lambda**: The API Gateway invokes the Lambda functions which use either `InvokeModelWithResponseStreamCommand` or `InvokeModelCommand` to call Bedrock for streaming and non-streaming use cases. 3. **Amazon Bedrock**: Based on the given prompt, generates the content and streams/ returns the response to the respective Lambda functions. 4. **Response**: The API Gateway either streams the responses back to the client (Fig. 1) or returns the whole response together. - ## Testing Use [curl](https://curl.se/) to send a HTTP POST request to the API. Make sure to replace `api-id` with the one from your `sam deploy --guided` output: diff --git a/apigw-lambda-bedrock-sam-node/example-pattern.json b/apigw-lambda-bedrock-sam-node/example-pattern.json index c2ed9d8ac..89987c24e 100644 --- a/apigw-lambda-bedrock-sam-node/example-pattern.json +++ b/apigw-lambda-bedrock-sam-node/example-pattern.json @@ -53,13 +53,13 @@ { "name": "Biswanath Mukherjee", "image": "https://serverlessland.com/assets/images/resources/contributors/biswanath-mukherjee.jpg", - "bio": "I am a Sr. Solutions Architect working at AWS India. I help strategic global enterprise customer to architect their workload to run on AWS.", + "bio": "I am a Senior Solutions Architect working at AWS India. I help strategic global enterprise customer to architect their workload to run on AWS.", "linkedin": "biswanathmukherjee" }, { "name": "Giedrius Praspaliauskas", "image": "https://serverlessland.com/assets/images/resources/contributors/giedrius-praspaliauskas.jpg", - "bio": "I am a serverless specialist Sr. Solutions Architect working at AWS.", + "bio": "I am a Senior Specialist Solutions Architect for serverless based in California. I work with customers to help them leverage serverless services to build scalable, fault-tolerant, high-performing, cost-effective applications.", "linkedin": "gpraspaliauskas" } ]