From 0c75ee8009373eae76084abe613d4ba90232008b Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 22 Mar 2024 09:56:12 +0100 Subject: [PATCH 1/8] Add basic flight assistant example from docs This fills the gap of not having an example that uses the `render` function from `ai/rsc`. Based on the guide at https://sdk.vercel.ai/docs/concepts/ai-rsc. --- examples/next-ai-rsc-basic/.env.example | 1 + examples/next-ai-rsc-basic/.gitignore | 9 + examples/next-ai-rsc-basic/README.md | 47 ++ examples/next-ai-rsc-basic/app/action.tsx | 116 +++++ examples/next-ai-rsc-basic/app/favicon.ico | Bin 0 -> 25931 bytes examples/next-ai-rsc-basic/app/globals.css | 17 + examples/next-ai-rsc-basic/app/layout.tsx | 31 ++ examples/next-ai-rsc-basic/app/page.tsx | 42 ++ examples/next-ai-rsc-basic/next-env.d.ts | 5 + examples/next-ai-rsc-basic/package.json | 53 ++ examples/next-ai-rsc-basic/postcss.config.js | 5 + .../public/apple-touch-icon.png | Bin 0 -> 10423 bytes .../public/favicon-16x16.png | Bin 0 -> 539 bytes examples/next-ai-rsc-basic/public/favicon.ico | Bin 0 -> 15406 bytes examples/next-ai-rsc-basic/tailwind.config.ts | 74 +++ examples/next-ai-rsc-basic/tsconfig.json | 27 + pnpm-lock.yaml | 484 ++++++------------ 17 files changed, 581 insertions(+), 330 deletions(-) create mode 100644 examples/next-ai-rsc-basic/.env.example create mode 100644 examples/next-ai-rsc-basic/.gitignore create mode 100644 examples/next-ai-rsc-basic/README.md create mode 100644 examples/next-ai-rsc-basic/app/action.tsx create mode 100644 examples/next-ai-rsc-basic/app/favicon.ico create mode 100644 examples/next-ai-rsc-basic/app/globals.css create mode 100644 examples/next-ai-rsc-basic/app/layout.tsx create mode 100644 examples/next-ai-rsc-basic/app/page.tsx create mode 100644 examples/next-ai-rsc-basic/next-env.d.ts create mode 100644 examples/next-ai-rsc-basic/package.json create mode 100644 examples/next-ai-rsc-basic/postcss.config.js create mode 100644 examples/next-ai-rsc-basic/public/apple-touch-icon.png create mode 100644 examples/next-ai-rsc-basic/public/favicon-16x16.png create mode 100644 examples/next-ai-rsc-basic/public/favicon.ico create mode 100644 examples/next-ai-rsc-basic/tailwind.config.ts create mode 100644 examples/next-ai-rsc-basic/tsconfig.json diff --git a/examples/next-ai-rsc-basic/.env.example b/examples/next-ai-rsc-basic/.env.example new file mode 100644 index 000000000000..bcadf8e160fc --- /dev/null +++ b/examples/next-ai-rsc-basic/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY="xxxxxxxxxxxx" \ No newline at end of file diff --git a/examples/next-ai-rsc-basic/.gitignore b/examples/next-ai-rsc-basic/.gitignore new file mode 100644 index 000000000000..56a2586450ab --- /dev/null +++ b/examples/next-ai-rsc-basic/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +node_modules +.turbo +*.log +.next +*.local +.env +.cache +.turbo diff --git a/examples/next-ai-rsc-basic/README.md b/examples/next-ai-rsc-basic/README.md new file mode 100644 index 000000000000..c2420d6051ea --- /dev/null +++ b/examples/next-ai-rsc-basic/README.md @@ -0,0 +1,47 @@ + + Generative UI Demo +

Basic Generative UI Demo

+
+ +

+ A very basic, experimental preview of AI SDK 3.0 with Generative UI support, based on the example from the docs. +

+ +## Features + +- [Next.js](https://nextjs.org) App Router + React Server Components +- [Vercel AI SDK 3.0](https://sdk.vercel.ai/docs) for Generative UI +- OpenAI Tools Calling + +## Quick Links + +- [Read the blog post](https://vercel.com/blog/ai-sdk-3-generative-ui) +- [See the demo](https://sdk.vercel.ai/demo) +- [Visit the docs](https://sdk.vercel.ai/docs/concepts/ai-rsc) + +## Running locally + +You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. + +> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts. + +1. Install Vercel CLI: `npm i -g vercel` +2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` +3. Download your environment variables: `vercel env pull` + +```bash +pnpm install +pnpm dev +``` + +Your app should now be running on [localhost:3000](http://localhost:3000/). + +## Authors + +This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from: + +- Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com) +- Max Leiter ([@max_leiter](https://twitter.com/max_leiter)) - [Vercel](https://vercel.com) +- Jeremy Philemon ([@jeremyphilemon](https://github.com/jeremyphilemon)) - [Vercel](https://vercel.com) +- shadcn ([@shadcn](https://twitter.com/shadcn)) - [Vercel](https://vercel.com) +- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com) diff --git a/examples/next-ai-rsc-basic/app/action.tsx b/examples/next-ai-rsc-basic/app/action.tsx new file mode 100644 index 000000000000..8d6750b7972f --- /dev/null +++ b/examples/next-ai-rsc-basic/app/action.tsx @@ -0,0 +1,116 @@ +import { OpenAI } from 'openai'; +import { createAI, getMutableAIState, render } from 'ai/rsc'; +import { z } from 'zod'; + +interface FlightInfo { + readonly flightNumber: string; + readonly departure: string; + readonly arrival: string; +} + +interface FlightCardProps { + readonly flightInfo: FlightInfo; +} + +type AIStateItem = + | { + readonly role: 'user' | 'assistant' | 'system'; + readonly content: string; + } + | { + readonly role: 'function'; + readonly content: string; + readonly name: string; + }; + +interface UIStateItem { + readonly id: number; + readonly display: React.ReactNode; +} + +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +async function getFlightInfo(flightNumber: string): Promise { + return { + flightNumber, + departure: 'New York', + arrival: 'San Francisco', + }; +} + +function Spinner() { + return
Loading...
; +} + +function FlightCard({ flightInfo }: FlightCardProps) { + return ( +
+

Flight Information

+

Flight Number: {flightInfo.flightNumber}

+

Departure: {flightInfo.departure}

+

Arrival: {flightInfo.arrival}

+
+ ); +} + +async function submitUserMessage(userInput: string): Promise { + 'use server'; + + const aiState = getMutableAIState(); + + aiState.update([...aiState.get(), { role: 'user', content: userInput }]); + + const ui = render({ + model: 'gpt-4-0125-preview', + provider: openai, + messages: [ + { role: 'system', content: 'You are a flight assistant' }, + { role: 'user', content: userInput }, + ...aiState.get(), + ], + text: ({ content, done }) => { + if (done) { + aiState.done([...aiState.get(), { role: 'assistant', content }]); + } + + return

{content}

; + }, + tools: { + get_flight_info: { + description: 'Get the information for a flight', + parameters: z + .object({ + flightNumber: z.string().describe('the number of the flight'), + }) + .required(), + render: async function* ({ flightNumber }) { + yield ; + + const flightInfo = await getFlightInfo(flightNumber); + + aiState.done([ + ...aiState.get(), + { + role: 'function', + name: 'get_flight_info', + content: JSON.stringify(flightInfo), + }, + ]); + + return ; + }, + }, + }, + }); + + return { id: Date.now(), display: ui }; +} + +const initialAIState: AIStateItem[] = []; +const initialUIState: UIStateItem[] = []; + +export const AI = createAI({ + actions: { submitUserMessage }, + initialUIState, + initialAIState, +}); diff --git a/examples/next-ai-rsc-basic/app/favicon.ico b/examples/next-ai-rsc-basic/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/examples/next-ai-rsc-basic/app/globals.css b/examples/next-ai-rsc-basic/app/globals.css new file mode 100644 index 000000000000..ea734f55125d --- /dev/null +++ b/examples/next-ai-rsc-basic/app/globals.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} diff --git a/examples/next-ai-rsc-basic/app/layout.tsx b/examples/next-ai-rsc-basic/app/layout.tsx new file mode 100644 index 000000000000..01c93b1b8491 --- /dev/null +++ b/examples/next-ai-rsc-basic/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import { AI } from './action'; +import './globals.css'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'Basic AI RSC Demo', + description: + 'Demo of a flight assistant built using Next.js and Vercel AI SDK.', + icons: { + icon: '/favicon.ico', + shortcut: '/favicon-16x16.png', + apple: '/apple-touch-icon.png', + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/next-ai-rsc-basic/app/page.tsx b/examples/next-ai-rsc-basic/app/page.tsx new file mode 100644 index 000000000000..351a33524067 --- /dev/null +++ b/examples/next-ai-rsc-basic/app/page.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useState } from 'react'; +import { useUIState, useActions } from 'ai/rsc'; +import type { AI } from './action'; + +export default function Page() { + const [inputValue, setInputValue] = useState(''); + const [messages, setMessages] = useUIState(); + const { submitUserMessage } = useActions(); + + return ( +
+ {messages.map(message => ( +
{message.display}
+ ))} + +
{ + e.preventDefault(); + + setMessages(currentMessages => [ + ...currentMessages, + { id: Date.now(), display:
{inputValue}
}, + ]); + + const responseMessage = await submitUserMessage(inputValue); + setMessages(currentMessages => [...currentMessages, responseMessage]); + setInputValue(''); + }} + > + { + setInputValue(event.target.value); + }} + /> +
+
+ ); +} diff --git a/examples/next-ai-rsc-basic/next-env.d.ts b/examples/next-ai-rsc-basic/next-env.d.ts new file mode 100644 index 000000000000..4f11a03dc6cc --- /dev/null +++ b/examples/next-ai-rsc-basic/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/next-ai-rsc-basic/package.json b/examples/next-ai-rsc-basic/package.json new file mode 100644 index 000000000000..acaa71896112 --- /dev/null +++ b/examples/next-ai-rsc-basic/package.json @@ -0,0 +1,53 @@ +{ + "name": "ai-rsc-basic-demo", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx}\" --cache" + }, + "dependencies": { + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tooltip": "^1.0.7", + "@vercel/analytics": "^1.2.2", + "@vercel/kv": "^1.0.1", + "ai": "latest", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "d3-scale": "^4.0.2", + "date-fns": "^3.3.1", + "geist": "^1.2.2", + "next": "14.1.2", + "next-themes": "^0.2.1", + "openai": "^4.28.4", + "react": "^18", + "react-dom": "^18", + "react-intersection-observer": "^9.8.0", + "react-textarea-autosize": "^8.5.3", + "tailwind-merge": "^2.2.1", + "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^2.15.1", + "zod": "3.22.4", + "zod-to-json-schema": "3.22.4" + }, + "devDependencies": { + "@types/d3-scale": "^4.0.8", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "8.57.0", + "eslint-config-next": "14.1.0", + "postcss": "^8", + "prettier": "^3.2.5", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/examples/next-ai-rsc-basic/postcss.config.js b/examples/next-ai-rsc-basic/postcss.config.js new file mode 100644 index 000000000000..ee5f90b30902 --- /dev/null +++ b/examples/next-ai-rsc-basic/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + tailwindcss: {}, + }, +}; diff --git a/examples/next-ai-rsc-basic/public/apple-touch-icon.png b/examples/next-ai-rsc-basic/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb831fd3f83deadb29cb4fc846e9feb919a61c2 GIT binary patch literal 10423 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 E3($lMk^lez literal 0 HcmV?d00001 diff --git a/examples/next-ai-rsc-basic/tailwind.config.ts b/examples/next-ai-rsc-basic/tailwind.config.ts new file mode 100644 index 000000000000..b1f3888e2a2b --- /dev/null +++ b/examples/next-ai-rsc-basic/tailwind.config.ts @@ -0,0 +1,74 @@ +import { fontFamily } from 'tailwindcss/defaultTheme'; +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: [ + './ai_user_components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx}', + './ai_hooks/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: { + fontFamily: { + sans: ['var(--font-geist-sans)', ...fontFamily.sans], + mono: ['var(--font-geist-mono)', ...fontFamily.mono], + }, + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': + 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + }, + colors: { + green: { + '50': '#f0fdf6', + '100': '#dbfdec', + '200': '#baf8d9', + '300': '#68eeac', + '400': '#47e195', + '500': '#1fc876', + '600': '#13a65e', + '700': '#13824c', + '800': '#146740', + '900': '#135436', + '950': '#042f1c', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; +export default config; diff --git a/examples/next-ai-rsc-basic/tsconfig.json b/examples/next-ai-rsc-basic/tsconfig.json new file mode 100644 index 000000000000..e60a647be00c --- /dev/null +++ b/examples/next-ai-rsc-basic/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2015", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30c975f05c54..e42259e52431 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,118 @@ importers: specifier: ^5 version: 5.1.6 + examples/next-ai-rsc-basic: + dependencies: + '@radix-ui/react-icons': + specifier: ^1.3.0 + version: 1.3.0(react@18.2.0) + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': + specifier: ^1.0.2 + version: 1.0.2(@types/react@18.2.8)(react@18.2.0) + '@radix-ui/react-toast': + specifier: ^1.1.5 + version: 1.1.5(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-tooltip': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) + '@vercel/analytics': + specifier: ^1.2.2 + version: 1.2.2(next@14.1.2)(react@18.2.0) + '@vercel/kv': + specifier: ^1.0.1 + version: 1.0.1 + ai: + specifier: latest + version: link:../../packages/core + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.1.0 + version: 2.1.0 + d3-scale: + specifier: ^4.0.2 + version: 4.0.2 + date-fns: + specifier: ^3.3.1 + version: 3.3.1 + geist: + specifier: ^1.2.2 + version: 1.2.2(next@14.1.2) + next: + specifier: 14.1.2 + version: 14.1.2(react-dom@18.2.0)(react@18.2.0) + next-themes: + specifier: ^0.2.1 + version: 0.2.1(next@14.1.2)(react-dom@18.2.0)(react@18.2.0) + openai: + specifier: ^4.28.4 + version: 4.29.0 + react: + specifier: ^18 + version: 18.2.0 + react-dom: + specifier: ^18 + version: 18.2.0(react@18.2.0) + react-intersection-observer: + specifier: ^9.8.0 + version: 9.8.1(react-dom@18.2.0)(react@18.2.0) + react-textarea-autosize: + specifier: ^8.5.3 + version: 8.5.3(@types/react@18.2.8)(react@18.2.0) + tailwind-merge: + specifier: ^2.2.1 + version: 2.2.1 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.1) + usehooks-ts: + specifier: ^2.15.1 + version: 2.15.1(react@18.2.0) + zod: + specifier: 3.22.4 + version: 3.22.4 + zod-to-json-schema: + specifier: 3.22.4 + version: 3.22.4(zod@3.22.4) + devDependencies: + '@types/d3-scale': + specifier: ^4.0.8 + version: 4.0.8 + '@types/node': + specifier: ^20 + version: 20.9.0 + '@types/react': + specifier: ^18 + version: 18.2.8 + '@types/react-dom': + specifier: ^18 + version: 18.2.4 + eslint: + specifier: 8.57.0 + version: 8.57.0 + eslint-config-next: + specifier: 14.1.0 + version: 14.1.0(eslint@8.57.0)(typescript@5.1.6) + postcss: + specifier: ^8 + version: 8.4.31 + prettier: + specifier: ^3.2.5 + version: 3.2.5 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.1 + typescript: + specifier: ^5 + version: 5.1.6 + examples/next-anthropic: dependencies: '@anthropic-ai/sdk': @@ -502,7 +614,7 @@ importers: version: link:../../packages/core langchain: specifier: ^0.0.196 - version: 0.0.196 + version: 0.0.196(@aws-sdk/client-bedrock-runtime@3.451.0)(@huggingface/inference@2.6.4)(cohere-ai@7.6.2)(jsdom@23.0.0) next: specifier: 14.1.1 version: 14.1.1(react-dom@18.2.0)(react@18.2.0) @@ -2846,7 +2958,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 - dev: false /@babel/standalone@7.23.3: resolution: {integrity: sha512-ZfB6wyLVqr9ANl1F0l/0aqoNUE1/kcWlQHmk0wF9OTEKDK1whkXYLruRMt53zY556yS2+84EsOpr1hpjZISTRg==} @@ -5365,7 +5476,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.24.0 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.8)(react@18.2.0) '@types/react': 18.2.8 react: 18.2.0 @@ -7282,6 +7393,43 @@ packages: zod-to-json-schema: 3.22.4(zod@3.22.4) dev: false + /ai@3.0.12(react@18.2.0)(solid-js@1.8.7)(svelte@4.2.3)(vue@3.3.8)(zod@3.22.4): + resolution: {integrity: sha512-cP/Moag7PcDOE3kA7WU00YS+mQiuPpAxY+uf57lkWwnqSB1K3/RzwnRF+LD1FqgJfCubI4WEbajMPbnnCr8lAg==} + engines: {node: '>=14.6'} + peerDependencies: + react: ^18.2.0 + solid-js: ^1.7.7 + svelte: ^3.0.0 || ^4.0.0 + vue: ^3.3.4 + zod: ^3.0.0 + peerDependenciesMeta: + react: + optional: true + solid-js: + optional: true + svelte: + optional: true + vue: + optional: true + zod: + optional: true + dependencies: + eventsource-parser: 1.0.0 + jsondiffpatch: 0.6.0 + nanoid: 3.3.6 + react: 18.2.0 + solid-js: 1.8.7 + solid-swr-store: 0.10.7(solid-js@1.8.7)(swr-store@0.10.6) + sswr: 2.0.0(svelte@4.2.3) + svelte: 4.2.3 + swr: 2.2.0(react@18.2.0) + swr-store: 0.10.6 + swrv: 1.0.4(vue@3.3.8) + vue: 3.3.8(typescript@5.1.3) + zod: 3.22.4 + zod-to-json-schema: 3.22.4(zod@3.22.4) + dev: false + /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -8268,7 +8416,6 @@ packages: url-join: 4.0.1 transitivePeerDependencies: - encoding - dev: true /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -9701,7 +9848,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.24.0 aria-query: 5.3.0 array-includes: 3.1.7 array.prototype.flatmap: 1.3.2 @@ -9725,7 +9872,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.24.0 aria-query: 5.3.0 array-includes: 3.1.7 array.prototype.flatmap: 1.3.2 @@ -11982,328 +12129,6 @@ packages: - encoding dev: false - /langchain@0.0.196: - resolution: {integrity: sha512-kt17GGTDFWHNv3jJOIXymsQxfa+h9UQ6hrHbhur+V2pV6RBKO5E+RRCvnCqBzbnOPtrlkENF6Wl3Ezmsfo21dg==} - engines: {node: '>=18'} - peerDependencies: - '@aws-crypto/sha256-js': ^5.0.0 - '@aws-sdk/client-bedrock-runtime': ^3.422.0 - '@aws-sdk/client-dynamodb': ^3.310.0 - '@aws-sdk/client-kendra': ^3.352.0 - '@aws-sdk/client-lambda': ^3.310.0 - '@aws-sdk/client-s3': ^3.310.0 - '@aws-sdk/client-sagemaker-runtime': ^3.310.0 - '@aws-sdk/client-sfn': ^3.310.0 - '@aws-sdk/credential-provider-node': ^3.388.0 - '@azure/storage-blob': ^12.15.0 - '@clickhouse/client': ^0.0.14 - '@cloudflare/ai': ^1.0.12 - '@elastic/elasticsearch': ^8.4.0 - '@getmetal/metal-sdk': '*' - '@getzep/zep-js': ^0.9.0 - '@gomomento/sdk': ^1.47.1 - '@gomomento/sdk-core': ^1.47.1 - '@gomomento/sdk-web': ^1.47.1 - '@google-ai/generativelanguage': ^0.2.1 - '@google-cloud/storage': ^6.10.1 - '@huggingface/inference': ^2.6.4 - '@mozilla/readability': '*' - '@notionhq/client': ^2.2.10 - '@opensearch-project/opensearch': '*' - '@pinecone-database/pinecone': ^1.1.0 - '@planetscale/database': ^1.8.0 - '@qdrant/js-client-rest': ^1.2.0 - '@raycast/api': ^1.55.2 - '@rockset/client': ^0.9.1 - '@smithy/eventstream-codec': ^2.0.5 - '@smithy/protocol-http': ^3.0.6 - '@smithy/signature-v4': ^2.0.10 - '@smithy/util-utf8': ^2.0.0 - '@supabase/postgrest-js': ^1.1.1 - '@supabase/supabase-js': ^2.10.0 - '@tensorflow-models/universal-sentence-encoder': '*' - '@tensorflow/tfjs-converter': '*' - '@tensorflow/tfjs-core': '*' - '@upstash/redis': ^1.20.6 - '@vercel/kv': ^0.2.3 - '@vercel/postgres': ^0.5.0 - '@writerai/writer-sdk': ^0.40.2 - '@xata.io/client': ^0.25.1 - '@xenova/transformers': ^2.5.4 - '@zilliz/milvus2-sdk-node': '>=2.2.7' - apify-client: ^2.7.1 - assemblyai: ^2.0.2 - axios: '*' - cassandra-driver: ^4.7.2 - cheerio: ^1.0.0-rc.12 - chromadb: '*' - closevector-common: 0.1.0-alpha.1 - closevector-node: 0.1.0-alpha.10 - closevector-web: 0.1.0-alpha.16 - cohere-ai: '>=6.0.0' - convex: ^1.3.1 - d3-dsv: ^2.0.0 - epub2: ^3.0.1 - faiss-node: ^0.5.1 - fast-xml-parser: ^4.2.7 - firebase-admin: ^11.9.0 - google-auth-library: ^8.9.0 - googleapis: ^126.0.1 - hnswlib-node: ^1.4.2 - html-to-text: ^9.0.5 - ignore: ^5.2.0 - ioredis: ^5.3.2 - jsdom: '*' - llmonitor: ^0.5.9 - lodash: ^4.17.21 - mammoth: '*' - mongodb: ^5.2.0 - mysql2: ^3.3.3 - neo4j-driver: '*' - node-llama-cpp: '*' - notion-to-md: ^3.1.0 - pdf-parse: 1.1.1 - peggy: ^3.0.2 - pg: ^8.11.0 - pg-copy-streams: ^6.0.5 - pickleparser: ^0.2.1 - playwright: ^1.32.1 - portkey-ai: ^0.1.11 - puppeteer: ^19.7.2 - redis: ^4.6.4 - replicate: ^0.18.0 - sonix-speech-recognition: ^2.1.1 - srt-parser-2: ^1.2.2 - typeorm: ^0.3.12 - typesense: ^1.5.3 - usearch: ^1.1.1 - vectordb: ^0.1.4 - voy-search: 0.6.2 - weaviate-ts-client: ^1.4.0 - web-auth-library: ^1.0.3 - ws: ^8.14.2 - youtube-transcript: ^1.0.6 - youtubei.js: ^5.8.0 - peerDependenciesMeta: - '@aws-crypto/sha256-js': - optional: true - '@aws-sdk/client-bedrock-runtime': - optional: true - '@aws-sdk/client-dynamodb': - optional: true - '@aws-sdk/client-kendra': - optional: true - '@aws-sdk/client-lambda': - optional: true - '@aws-sdk/client-s3': - optional: true - '@aws-sdk/client-sagemaker-runtime': - optional: true - '@aws-sdk/client-sfn': - optional: true - '@aws-sdk/credential-provider-node': - optional: true - '@azure/storage-blob': - optional: true - '@clickhouse/client': - optional: true - '@cloudflare/ai': - optional: true - '@elastic/elasticsearch': - optional: true - '@getmetal/metal-sdk': - optional: true - '@getzep/zep-js': - optional: true - '@gomomento/sdk': - optional: true - '@gomomento/sdk-core': - optional: true - '@gomomento/sdk-web': - optional: true - '@google-ai/generativelanguage': - optional: true - '@google-cloud/storage': - optional: true - '@huggingface/inference': - optional: true - '@mozilla/readability': - optional: true - '@notionhq/client': - optional: true - '@opensearch-project/opensearch': - optional: true - '@pinecone-database/pinecone': - optional: true - '@planetscale/database': - optional: true - '@qdrant/js-client-rest': - optional: true - '@raycast/api': - optional: true - '@rockset/client': - optional: true - '@smithy/eventstream-codec': - optional: true - '@smithy/protocol-http': - optional: true - '@smithy/signature-v4': - optional: true - '@smithy/util-utf8': - optional: true - '@supabase/postgrest-js': - optional: true - '@supabase/supabase-js': - optional: true - '@tensorflow-models/universal-sentence-encoder': - optional: true - '@tensorflow/tfjs-converter': - optional: true - '@tensorflow/tfjs-core': - optional: true - '@upstash/redis': - optional: true - '@vercel/kv': - optional: true - '@vercel/postgres': - optional: true - '@writerai/writer-sdk': - optional: true - '@xata.io/client': - optional: true - '@xenova/transformers': - optional: true - '@zilliz/milvus2-sdk-node': - optional: true - apify-client: - optional: true - assemblyai: - optional: true - axios: - optional: true - cassandra-driver: - optional: true - cheerio: - optional: true - chromadb: - optional: true - closevector-common: - optional: true - closevector-node: - optional: true - closevector-web: - optional: true - cohere-ai: - optional: true - convex: - optional: true - d3-dsv: - optional: true - epub2: - optional: true - faiss-node: - optional: true - fast-xml-parser: - optional: true - firebase-admin: - optional: true - google-auth-library: - optional: true - googleapis: - optional: true - hnswlib-node: - optional: true - html-to-text: - optional: true - ignore: - optional: true - ioredis: - optional: true - jsdom: - optional: true - llmonitor: - optional: true - lodash: - optional: true - mammoth: - optional: true - mongodb: - optional: true - mysql2: - optional: true - neo4j-driver: - optional: true - node-llama-cpp: - optional: true - notion-to-md: - optional: true - pdf-parse: - optional: true - peggy: - optional: true - pg: - optional: true - pg-copy-streams: - optional: true - pickleparser: - optional: true - playwright: - optional: true - portkey-ai: - optional: true - puppeteer: - optional: true - redis: - optional: true - replicate: - optional: true - sonix-speech-recognition: - optional: true - srt-parser-2: - optional: true - typeorm: - optional: true - typesense: - optional: true - usearch: - optional: true - vectordb: - optional: true - voy-search: - optional: true - weaviate-ts-client: - optional: true - web-auth-library: - optional: true - ws: - optional: true - youtube-transcript: - optional: true - youtubei.js: - optional: true - dependencies: - '@anthropic-ai/sdk': 0.9.1 - binary-extensions: 2.2.0 - expr-eval: 2.0.2 - flat: 5.0.2 - js-tiktoken: 1.0.7 - js-yaml: 4.1.0 - jsonpointer: 5.0.1 - langchain-core: 0.0.1 - langchainhub: 0.0.6 - langsmith: 0.0.48 - ml-distance: 4.0.1 - openai: 4.28.4 - openapi-types: 12.1.3 - p-retry: 4.6.2 - uuid: 9.0.1 - yaml: 2.3.4 - zod: 3.22.4 - zod-to-json-schema: 3.20.3(zod@3.22.4) - transitivePeerDependencies: - - encoding - dev: false - /langchain@0.0.196(@aws-sdk/client-bedrock-runtime@3.451.0)(@huggingface/inference@2.6.4)(cohere-ai@7.6.2)(jsdom@23.0.0): resolution: {integrity: sha512-kt17GGTDFWHNv3jJOIXymsQxfa+h9UQ6hrHbhur+V2pV6RBKO5E+RRCvnCqBzbnOPtrlkENF6Wl3Ezmsfo21dg==} engines: {node: '>=18'} @@ -12628,7 +12453,6 @@ packages: zod-to-json-schema: 3.20.3(zod@3.22.4) transitivePeerDependencies: - encoding - dev: true /langchainhub@0.0.6: resolution: {integrity: sha512-SW6105T+YP1cTe0yMf//7kyshCgvCTyFBMTgH2H3s9rTAR4e+78DA/BBrUL/Mt4Q5eMWui7iGuAYb3pgGsdQ9w==} @@ -15480,7 +15304,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.24.0 /regexp.prototype.flags@1.5.1: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} From 7fd06f9a7a4436f849aba0bd443a337cb0a5c01b Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 22 Mar 2024 09:59:17 +0100 Subject: [PATCH 2/8] Add `compose` renderer to support multiple tool calls --- examples/next-ai-rsc-basic/app/action.tsx | 9 + .../__snapshots__/streamable.ui.test.tsx.snap | 242 ++++++++++++++++-- packages/core/rsc/streamable.tsx | 228 ++++++++++++----- 3 files changed, 390 insertions(+), 89 deletions(-) diff --git a/examples/next-ai-rsc-basic/app/action.tsx b/examples/next-ai-rsc-basic/app/action.tsx index 8d6750b7972f..5f68db3b4b58 100644 --- a/examples/next-ai-rsc-basic/app/action.tsx +++ b/examples/next-ai-rsc-basic/app/action.tsx @@ -68,6 +68,15 @@ async function submitUserMessage(userInput: string): Promise { { role: 'user', content: userInput }, ...aiState.get(), ], + compose: ({ text, functionCall, toolCalls }) => ( +
+ {text} + {functionCall && functionCall.node} + {toolCalls.map(({ id, node }) => ( +
{node}
+ ))} +
+ ), text: ({ content, done }) => { if (done) { aiState.done([...aiState.get(), { role: 'assistant', content }]); diff --git a/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap b/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap index bdc89a0aae3c..304480be3be8 100644 --- a/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap +++ b/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap @@ -10,13 +10,75 @@ exports[`rsc - render() > should emit React Nodes with async render function 1`] "done": false, "next": { "done": true, - "value":
- Weather -
, + "value": + + + + + + Weather + , + }, + "value":
+ Weather +
, + } + } + /> +
+
, }, - "value":
- Weather -
, + "value": + + + + + + Weather + , + }, + "value":
+ Weather +
, + } + } + /> +
+
, }, }, "type": "", @@ -37,20 +99,88 @@ exports[`rsc - render() > should emit React Nodes with generator render function "n": { "done": false, "next": { - "done": false, - "next": { - "done": true, - "value":
- Weather -
, - }, - "value":
- Weather -
, + "done": true, + "value": + + + + + + Weather + , + }, + "value":
+ Weather +
, + }, + "value":
+ Loading... +
, + } + } + /> +
+
, }, - "value":
- Loading... -
, + "value": + + + + + + Weather + , + }, + "value":
+ Weather +
, + }, + "value":
+ Loading... +
, + } + } + /> +
+
, }, }, "type": "", @@ -72,13 +202,75 @@ exports[`rsc - render() > should emit React Nodes with sync render function 1`] "done": false, "next": { "done": true, - "value":
- Weather -
, + "value": + + + + + + Weather + , + }, + "value":
+ Weather +
, + } + } + /> +
+
, }, - "value":
- Weather -
, + "value": + + + + + + Weather + , + }, + "value":
+ Weather +
, + } + } + /> +
+
, }, }, "type": "", diff --git a/packages/core/rsc/streamable.tsx b/packages/core/rsc/streamable.tsx index 09799b545d64..863036a2ee88 100644 --- a/packages/core/rsc/streamable.tsx +++ b/packages/core/rsc/streamable.tsx @@ -1,5 +1,5 @@ -import type { ReactNode } from 'react'; import type OpenAI from 'openai'; +import * as React from 'react'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; @@ -267,14 +267,31 @@ export function createStreamableValue(initialValue?: T) { }; } -type Streamable = ReactNode | Promise; -type Renderer = ( - props: T, +type Streamable = React.ReactNode | Promise; + +type Renderer = ( + props: TProps, ) => | Streamable | Generator | AsyncGenerator; +type StreamableUI = ReturnType; + +interface StreamableUIContext { + ui: StreamableUI; + finished: Promise | undefined; +} + +interface StreamableFunctionUIContext extends StreamableUIContext { + name: string; +} + +interface StreamableToolUIContext extends StreamableUIContext { + name: string; + id: string; +} + /** * `render` is a helper function to create a streamable UI from some LLMs. * Currently, it only supports OpenAI's GPT models with Function Calling and Assistants Tools. @@ -302,6 +319,16 @@ export function render< messages: Parameters< typeof OpenAI.prototype.chat.completions.create >[0]['messages']; + /** + * Control how text, function calls, and tool calls are composed into a single UI. + * + * Per default, text, function and tool nodes are wrapped in a React Fragment. + */ + compose?: Renderer<{ + text: React.ReactNode; + functionCall: { name: keyof FS; node: React.ReactNode } | undefined; + toolCalls: { name: keyof TS; id: string; node: React.ReactNode }[]; + }>; text?: Renderer<{ /** * The full text content from the model so far. @@ -310,7 +337,7 @@ export function render< /** * The new appended text content from the model since the last `text` call. */ - delta: string; + delta?: string; /** * Whether the model is done generating text. * If `true`, the `content` will be the final output and this call will be the last. @@ -321,6 +348,7 @@ export function render< [name in keyof TS]: { description?: string; parameters: TS[name]; + initial?: React.ReactNode; render: Renderer>; }; }; @@ -328,18 +356,40 @@ export function render< [name in keyof FS]: { description?: string; parameters: FS[name]; + initial?: React.ReactNode; render: Renderer>; }; }; - initial?: ReactNode; + initial?: React.ReactNode; temperature?: number; -}): ReactNode { - const ui = createStreamableUI(options.initial); +}): React.ReactNode { + const composedUIContext: StreamableUIContext = { + ui: createStreamableUI(options.initial), + finished: undefined, + }; + + const textUIContext: StreamableUIContext = { + ui: createStreamableUI(), + finished: undefined, + }; + + let functionUIContext: StreamableFunctionUIContext | undefined; + const toolUIContexts: StreamableToolUIContext[] = []; // The default text renderer just returns the content as string. - const text = options.text - ? options.text - : ({ content }: { content: string }) => content; + const text = options.text ?? (({ content }: { content: string }) => content); + + const compose = + options.compose ?? + (({ text, functionCall, toolCalls }) => ( + <> + {text} + {functionCall && functionCall.node} + {toolCalls.map(({ id, node }) => ( + {node} + ))} + + )); const functions = options.functions ? Object.entries(options.functions).map( @@ -377,21 +427,19 @@ export function render< ); } - let finished: Promise | undefined; - - async function handleRender( - args: any, - renderer: undefined | Renderer, - res: ReturnType, + async function handleRender( + args: TProps, + renderer: Renderer, + context: StreamableUIContext, ) { if (!renderer) return; const resolvable = createResolvablePromise(); - if (finished) { - finished = finished.then(() => resolvable.promise); + if (context.finished) { + context.finished = context.finished.then(() => resolvable.promise); } else { - finished = resolvable.promise; + context.finished = resolvable.promise; } const value = renderer(args); @@ -403,7 +451,7 @@ export function render< typeof value.then === 'function') ) { const node = await (value as Promise); - res.update(node); + context.ui.update(node); resolvable.resolve(void 0); } else if ( value && @@ -417,7 +465,7 @@ export function render< >; while (true) { const { done, value } = await it.next(); - res.update(value); + context.ui.update(value); if (done) break; } resolvable.resolve(void 0); @@ -425,87 +473,139 @@ export function render< const it = value as Generator; while (true) { const { done, value } = it.next(); - res.update(value); + context.ui.update(value); if (done) break; } resolvable.resolve(void 0); } else { - res.update(value); + context.ui.update(value); resolvable.resolve(void 0); } } + function updateComposedUI() { + handleRender( + { + text: textUIContext.ui.value, + functionCall: functionUIContext + ? { + name: functionUIContext.name, + node: functionUIContext.ui.value, + } + : undefined, + toolCalls: toolUIContexts.map(toolUIContexts => ({ + name: toolUIContexts.name, + id: toolUIContexts.id, + node: toolUIContexts.ui.value, + })), + }, + compose, + composedUIContext, + ); + } + (async () => { - let hasFunction = false; let content = ''; consumeStream( OpenAIStream( - (await options.provider.chat.completions.create({ + await options.provider.chat.completions.create({ model: options.model, messages: options.messages, temperature: options.temperature, stream: true, - ...(functions - ? { - functions, - } - : {}), - ...(tools - ? { - tools, - } - : {}), - })) as any, + ...(functions ? { functions } : {}), + ...(tools ? { tools } : {}), + }), { ...(functions ? { async experimental_onFunctionCall(functionCallPayload) { - hasFunction = true; - handleRender( - functionCallPayload.arguments, - options.functions?.[functionCallPayload.name as any] - ?.render, - ui, - ); + const functionConfig = + options.functions?.[functionCallPayload.name]; + + if (functionConfig) { + functionUIContext = { + name: functionCallPayload.name, + ui: createStreamableUI(functionConfig.initial), + finished: undefined, + }; + + handleRender( + functionCallPayload.arguments, + functionConfig.render, + functionUIContext, + ); + + updateComposedUI(); + } }, } : {}), ...(tools ? { - async experimental_onToolCall(toolCallPayload: any) { - hasFunction = true; - - // TODO: We might need Promise.all here? + async experimental_onToolCall(toolCallPayload) { for (const tool of toolCallPayload.tools) { - handleRender( - tool.func.arguments, - options.tools?.[tool.func.name as any]?.render, - ui, - ); + const toolConfig = options.tools?.[tool.func.name]; + + if (toolConfig) { + const toolUIContext: StreamableToolUIContext = { + name: tool.func.name, + id: tool.id, + ui: createStreamableUI(toolConfig.initial), + finished: undefined, + }; + + toolUIContexts.push(toolUIContext); + + handleRender( + tool.func.arguments, + toolConfig.render, + toolUIContext, + ); + + updateComposedUI(); + } } }, } : {}), onText(chunk) { content += chunk; - handleRender({ content, done: false, delta: chunk }, text, ui); + + handleRender( + { content, done: false, delta: chunk }, + text, + textUIContext, + ); + + // Update the composed UI when receiving the first text chunk. Until + // then, then initial UI is used. + if (content === chunk) { + updateComposedUI(); + } }, async onFinal() { - if (hasFunction) { - await finished; - ui.done(); - return; - } - - handleRender({ content, done: true }, text, ui); - await finished; - ui.done(); + handleRender({ content, done: true }, text, textUIContext); + + const contexts = [ + composedUIContext, + textUIContext, + ...(functionUIContext ? [functionUIContext] : []), + ...toolUIContexts, + ]; + + await Promise.all( + contexts.map(async ({ ui, finished }) => { + await finished; + ui.done(); + }), + ); }, }, ), ); })(); - return ui.value; + return composedUIContext.ui.value; } From a25380447d6552e09ae3f2d73bef53cdb146abb4 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 22 Mar 2024 10:36:18 +0100 Subject: [PATCH 3/8] Add a unit test for `compose` renderer --- .../__snapshots__/streamable.ui.test.tsx.snap | 189 +++++++++++++ packages/core/rsc/streamable.ui.test.tsx | 50 +++- packages/core/tests/snapshots/openai-chat.ts | 264 ++++++++++++++++++ 3 files changed, 498 insertions(+), 5 deletions(-) diff --git a/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap b/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap index 304480be3be8..41d3756a2495 100644 --- a/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap +++ b/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap @@ -1,5 +1,194 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`rsc - render() > should emit React Nodes for multiple tool calls with a compose render function 1`] = ` +{ + "children": { + "children": {}, + "props": { + "c": undefined, + "n": { + "done": false, + "next": { + "done": false, + "next": { + "done": true, + "value":
+ + + + + + + Flight + 42 +
, + }, + "value":
+ Flight + 42 +
, + } + } + /> + + + + + + Flight + 43 + , + }, + "value":
+ Flight + 43 +
, + } + } + /> +
+
+ , + }, + "value":
+ + + + + + + Flight + 42 +
, + }, + "value":
+ Flight + 42 +
, + } + } + /> + + + + + + Flight + 43 + , + }, + "value":
+ Flight + 43 +
, + } + } + /> +
+
+ , + }, + "value":
+ + + + + + + Flight + 42 +
, + }, + "value":
+ Flight + 42 +
, + } + } + /> + + + , + }, + }, + "type": "", + }, + "props": { + "fallback": undefined, + }, + "type": "Symbol(react.suspense)", +} +`; + exports[`rsc - render() > should emit React Nodes with async render function 1`] = ` { "children": { diff --git a/packages/core/rsc/streamable.ui.test.tsx b/packages/core/rsc/streamable.ui.test.tsx index 13de97c2421b..f3acc02b957f 100644 --- a/packages/core/rsc/streamable.ui.test.tsx +++ b/packages/core/rsc/streamable.ui.test.tsx @@ -1,12 +1,15 @@ import { + chatCompletionChunksWithMultipleToolCalls, openaiChatCompletionChunks, openaiFunctionCallChunks, } from '../tests/snapshots/openai-chat'; import { DEFAULT_TEST_URL, createMockServer } from '../tests/utils/mock-server'; import { createStreamableUI, render } from './streamable'; +import * as React from 'react'; import { z } from 'zod'; const FUNCTION_CALL_TEST_URL = DEFAULT_TEST_URL + 'mock-func-call'; +const TOOL_CALLS_TEST_URL = DEFAULT_TEST_URL + 'mock-tool-calls'; // This is a workaround to render the Flight response in a test environment. async function flightRender(node: React.ReactNode, byChunk?: boolean) { @@ -61,6 +64,12 @@ const server = createMockServer([ formatChunk: chunk => `data: ${JSON.stringify(chunk)}\n\n`, suffix: 'data: [DONE]', }, + { + url: TOOL_CALLS_TEST_URL, + chunks: chatCompletionChunksWithMultipleToolCalls, + formatChunk: chunk => `data: ${JSON.stringify(chunk)}\n\n`, + suffix: 'data: [DONE]', + }, ]); beforeAll(() => { @@ -172,12 +181,12 @@ function getFinalValueFromResolved(node: any) { return node; } -function createMockUpProvider() { +function createMockUpProvider(url: string) { return { chat: { completions: { create: async () => { - return await fetch(FUNCTION_CALL_TEST_URL); + return await fetch(url); }, }, }, @@ -189,7 +198,7 @@ describe('rsc - render()', () => { const ui = render({ model: 'gpt-3.5-turbo', messages: [], - provider: createMockUpProvider(), + provider: createMockUpProvider(FUNCTION_CALL_TEST_URL), functions: { get_current_weather: { description: 'Get the current weather', @@ -209,7 +218,7 @@ describe('rsc - render()', () => { const ui = render({ model: 'gpt-3.5-turbo', messages: [], - provider: createMockUpProvider(), + provider: createMockUpProvider(FUNCTION_CALL_TEST_URL), functions: { get_current_weather: { description: 'Get the current weather', @@ -230,7 +239,7 @@ describe('rsc - render()', () => { const ui = render({ model: 'gpt-3.5-turbo', messages: [], - provider: createMockUpProvider(), + provider: createMockUpProvider(FUNCTION_CALL_TEST_URL), functions: { get_current_weather: { description: 'Get the current weather', @@ -247,6 +256,37 @@ describe('rsc - render()', () => { const rendered = await simulateFlightServerRender(ui as any); expect(rendered).toMatchSnapshot(); }); + + it('should emit React Nodes for multiple tool calls with a compose render function', async () => { + const ui = render({ + model: 'gpt-3.5-turbo', + messages: [], + provider: createMockUpProvider(TOOL_CALLS_TEST_URL), + compose: ({ text, functionCall, toolCalls }) => ( +
+ {text} + {functionCall && functionCall.node} + {toolCalls.map(({ id, node }) => ( + {node} + ))} +
+ ), + tools: { + get_flight_info: { + description: 'Get the information for a flight', + parameters: z.object({ + flightNumber: z.string().describe('the number of the flight'), + }), + render: ({ flightNumber }) => { + return
Flight {flightNumber}
; + }, + }, + }, + }); + + const rendered = await simulateFlightServerRender(ui as any); + expect(rendered).toMatchSnapshot(); + }); }); describe('rsc - createStreamableUI()', () => { diff --git a/packages/core/tests/snapshots/openai-chat.ts b/packages/core/tests/snapshots/openai-chat.ts index 2fc69482ec59..cbf3698110bf 100644 --- a/packages/core/tests/snapshots/openai-chat.ts +++ b/packages/core/tests/snapshots/openai-chat.ts @@ -691,3 +691,267 @@ export const chatCompletionChunksWithToolCall = [ ], }, ]; + +export const chatCompletionChunksWithMultipleToolCalls = [ + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { role: 'assistant', content: null }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + id: 'call_7xmmAK3oE6Pji28q3iomKf6k', + type: 'function', + function: { name: 'get_flight_info', arguments: '' }, + }, + ], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: '{"fl' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: 'ightN' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: 'umber"' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: ': "4' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: '2"}' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 1, + id: 'call_mLzeoLUPXCQURZ102cKy3ZvM', + type: 'function', + function: { name: 'get_flight_info', arguments: '' }, + }, + ], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 1, function: { arguments: '{"fl' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 1, function: { arguments: 'ightN' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 1, function: { arguments: 'umber"' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 1, function: { arguments: ': "4' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 1, function: { arguments: '3"}' } }], + }, + logprobs: null, + finish_reason: null, + }, + ], + }, + + { + id: 'chatcmpl-95UsCKxJcrGFnqNSXe1gHpT6fMf2A', + object: 'chat.completion.chunk', + created: 1711097344, + model: 'gpt-4-0125-preview', + system_fingerprint: 'fp_31c0f205d1', + choices: [ + { + index: 0, + delta: {}, + logprobs: null, + finish_reason: 'tool_calls', + }, + ], + }, +]; From 9efe5ae11b7c87b44403683d8c98d85e077c2e6d Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 22 Mar 2024 10:48:23 +0100 Subject: [PATCH 4/8] Omit empty text nodes from composed UI --- .../__snapshots__/streamable.ui.test.tsx.snap | 126 ------------------ packages/core/rsc/streamable.tsx | 44 +++--- 2 files changed, 23 insertions(+), 147 deletions(-) diff --git a/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap b/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap index 41d3756a2495..10d975545f42 100644 --- a/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap +++ b/packages/core/rsc/__snapshots__/streamable.ui.test.tsx.snap @@ -15,20 +15,6 @@ exports[`rsc - render() > should emit React Nodes for multiple tool calls with a "value":
- - - should emit React Nodes for multiple tool calls with a "value":
- - - should emit React Nodes for multiple tool calls with a "value":
- - - should emit React Nodes with async render function 1`] "next": { "done": true, "value": - - - should emit React Nodes with async render function 1`] , }, "value": - - - should emit React Nodes with generator render function "next": { "done": true, "value": - - - should emit React Nodes with generator render function , }, "value": - - - should emit React Nodes with sync render function 1`] "next": { "done": true, "value": - - - should emit React Nodes with sync render function 1`] , }, "value": - - - ; @@ -368,11 +368,7 @@ export function render< finished: undefined, }; - const textUIContext: StreamableUIContext = { - ui: createStreamableUI(), - finished: undefined, - }; - + let textUIContext: StreamableUIContext | undefined; let functionUIContext: StreamableFunctionUIContext | undefined; const toolUIContexts: StreamableToolUIContext[] = []; @@ -486,7 +482,7 @@ export function render< function updateComposedUI() { handleRender( { - text: textUIContext.ui.value, + text: textUIContext?.ui.value, functionCall: functionUIContext ? { name: functionUIContext.name, @@ -531,13 +527,13 @@ export function render< finished: undefined, }; + updateComposedUI(); + handleRender( functionCallPayload.arguments, functionConfig.render, functionUIContext, ); - - updateComposedUI(); } }, } @@ -557,14 +553,13 @@ export function render< }; toolUIContexts.push(toolUIContext); + updateComposedUI(); handleRender( tool.func.arguments, toolConfig.render, toolUIContext, ); - - updateComposedUI(); } } }, @@ -573,28 +568,35 @@ export function render< onText(chunk) { content += chunk; + if (!textUIContext) { + textUIContext = { ui: createStreamableUI(), finished: undefined }; + updateComposedUI(); + } + handleRender( { content, done: false, delta: chunk }, text, textUIContext, ); - - // Update the composed UI when receiving the first text chunk. Until - // then, then initial UI is used. - if (content === chunk) { - updateComposedUI(); - } }, async onFinal() { - handleRender({ content, done: true }, text, textUIContext); + if (textUIContext) { + handleRender({ content, done: true }, text, textUIContext); + } - const contexts = [ + const contexts: StreamableUIContext[] = [ composedUIContext, - textUIContext, - ...(functionUIContext ? [functionUIContext] : []), ...toolUIContexts, ]; + if (textUIContext) { + contexts.push(textUIContext); + } + + if (functionUIContext) { + contexts.push(functionUIContext); + } + await Promise.all( contexts.map(async ({ ui, finished }) => { await finished; From 48f198f874dcd595dad9a1f1400740dc65c42051 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 22 Mar 2024 11:21:52 +0100 Subject: [PATCH 5/8] Fix CI build issue due to missing OpenAI key --- examples/next-ai-rsc-basic/app/action.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/next-ai-rsc-basic/app/action.tsx b/examples/next-ai-rsc-basic/app/action.tsx index 5f68db3b4b58..06316fdc0881 100644 --- a/examples/next-ai-rsc-basic/app/action.tsx +++ b/examples/next-ai-rsc-basic/app/action.tsx @@ -28,7 +28,9 @@ interface UIStateItem { readonly display: React.ReactNode; } -const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY || '', +}); async function getFlightInfo(flightNumber: string): Promise { return { From b05c3bc09c6be404aa31e730dd3200b005a21205 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Sat, 23 Mar 2024 08:36:44 +0100 Subject: [PATCH 6/8] Remove unnecessary stuff from `examples/next-ai-rsc-basic` --- examples/next-ai-rsc-basic/app/favicon.ico | Bin 25931 -> 0 bytes examples/next-ai-rsc-basic/app/globals.css | 14 --- examples/next-ai-rsc-basic/package.json | 21 +--- examples/next-ai-rsc-basic/tailwind.config.ts | 62 +----------- pnpm-lock.yaml | 94 ------------------ 5 files changed, 3 insertions(+), 188 deletions(-) delete mode 100644 examples/next-ai-rsc-basic/app/favicon.ico diff --git a/examples/next-ai-rsc-basic/app/favicon.ico b/examples/next-ai-rsc-basic/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/examples/next-ai-rsc-basic/app/globals.css b/examples/next-ai-rsc-basic/app/globals.css index ea734f55125d..b5c61c956711 100644 --- a/examples/next-ai-rsc-basic/app/globals.css +++ b/examples/next-ai-rsc-basic/app/globals.css @@ -1,17 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} diff --git a/examples/next-ai-rsc-basic/package.json b/examples/next-ai-rsc-basic/package.json index acaa71896112..e491c85061b4 100644 --- a/examples/next-ai-rsc-basic/package.json +++ b/examples/next-ai-rsc-basic/package.json @@ -11,35 +11,16 @@ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx}\" --cache" }, "dependencies": { - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@vercel/analytics": "^1.2.2", - "@vercel/kv": "^1.0.1", "ai": "latest", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "d3-scale": "^4.0.2", "date-fns": "^3.3.1", "geist": "^1.2.2", "next": "14.1.2", - "next-themes": "^0.2.1", "openai": "^4.28.4", "react": "^18", "react-dom": "^18", - "react-intersection-observer": "^9.8.0", - "react-textarea-autosize": "^8.5.3", - "tailwind-merge": "^2.2.1", - "tailwindcss-animate": "^1.0.7", - "usehooks-ts": "^2.15.1", - "zod": "3.22.4", - "zod-to-json-schema": "3.22.4" + "zod": "3.22.4" }, "devDependencies": { - "@types/d3-scale": "^4.0.8", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/examples/next-ai-rsc-basic/tailwind.config.ts b/examples/next-ai-rsc-basic/tailwind.config.ts index b1f3888e2a2b..ca803e5fe811 100644 --- a/examples/next-ai-rsc-basic/tailwind.config.ts +++ b/examples/next-ai-rsc-basic/tailwind.config.ts @@ -2,73 +2,15 @@ import { fontFamily } from 'tailwindcss/defaultTheme'; import type { Config } from 'tailwindcss'; const config: Config = { - content: [ - './ai_user_components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx}', - './ai_hooks/**/*.{js,ts,jsx,tsx}', - ], + content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'], theme: { extend: { fontFamily: { sans: ['var(--font-geist-sans)', ...fontFamily.sans], mono: ['var(--font-geist-mono)', ...fontFamily.mono], }, - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, - colors: { - green: { - '50': '#f0fdf6', - '100': '#dbfdec', - '200': '#baf8d9', - '300': '#68eeac', - '400': '#47e195', - '500': '#1fc876', - '600': '#13a65e', - '700': '#13824c', - '800': '#146740', - '900': '#135436', - '950': '#042f1c', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - }, }, }, - plugins: [require('tailwindcss-animate')], }; + export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e42259e52431..663463b09120 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,42 +175,9 @@ importers: examples/next-ai-rsc-basic: dependencies: - '@radix-ui/react-icons': - specifier: ^1.3.0 - version: 1.3.0(react@18.2.0) - '@radix-ui/react-label': - specifier: ^2.0.2 - version: 2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-separator': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': - specifier: ^1.0.2 - version: 1.0.2(@types/react@18.2.8)(react@18.2.0) - '@radix-ui/react-toast': - specifier: ^1.1.5 - version: 1.1.5(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-tooltip': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0) - '@vercel/analytics': - specifier: ^1.2.2 - version: 1.2.2(next@14.1.2)(react@18.2.0) - '@vercel/kv': - specifier: ^1.0.1 - version: 1.0.1 ai: specifier: latest version: link:../../packages/core - class-variance-authority: - specifier: ^0.7.0 - version: 0.7.0 - clsx: - specifier: ^2.1.0 - version: 2.1.0 - d3-scale: - specifier: ^4.0.2 - version: 4.0.2 date-fns: specifier: ^3.3.1 version: 3.3.1 @@ -220,9 +187,6 @@ importers: next: specifier: 14.1.2 version: 14.1.2(react-dom@18.2.0)(react@18.2.0) - next-themes: - specifier: ^0.2.1 - version: 0.2.1(next@14.1.2)(react-dom@18.2.0)(react@18.2.0) openai: specifier: ^4.28.4 version: 4.29.0 @@ -232,31 +196,10 @@ importers: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) - react-intersection-observer: - specifier: ^9.8.0 - version: 9.8.1(react-dom@18.2.0)(react@18.2.0) - react-textarea-autosize: - specifier: ^8.5.3 - version: 8.5.3(@types/react@18.2.8)(react@18.2.0) - tailwind-merge: - specifier: ^2.2.1 - version: 2.2.1 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.1) - usehooks-ts: - specifier: ^2.15.1 - version: 2.15.1(react@18.2.0) zod: specifier: 3.22.4 version: 3.22.4 - zod-to-json-schema: - specifier: 3.22.4 - version: 3.22.4(zod@3.22.4) devDependencies: - '@types/d3-scale': - specifier: ^4.0.8 - version: 4.0.8 '@types/node': specifier: ^20 version: 20.9.0 @@ -7393,43 +7336,6 @@ packages: zod-to-json-schema: 3.22.4(zod@3.22.4) dev: false - /ai@3.0.12(react@18.2.0)(solid-js@1.8.7)(svelte@4.2.3)(vue@3.3.8)(zod@3.22.4): - resolution: {integrity: sha512-cP/Moag7PcDOE3kA7WU00YS+mQiuPpAxY+uf57lkWwnqSB1K3/RzwnRF+LD1FqgJfCubI4WEbajMPbnnCr8lAg==} - engines: {node: '>=14.6'} - peerDependencies: - react: ^18.2.0 - solid-js: ^1.7.7 - svelte: ^3.0.0 || ^4.0.0 - vue: ^3.3.4 - zod: ^3.0.0 - peerDependenciesMeta: - react: - optional: true - solid-js: - optional: true - svelte: - optional: true - vue: - optional: true - zod: - optional: true - dependencies: - eventsource-parser: 1.0.0 - jsondiffpatch: 0.6.0 - nanoid: 3.3.6 - react: 18.2.0 - solid-js: 1.8.7 - solid-swr-store: 0.10.7(solid-js@1.8.7)(swr-store@0.10.6) - sswr: 2.0.0(svelte@4.2.3) - svelte: 4.2.3 - swr: 2.2.0(react@18.2.0) - swr-store: 0.10.6 - swrv: 1.0.4(vue@3.3.8) - vue: 3.3.8(typescript@5.1.3) - zod: 3.22.4 - zod-to-json-schema: 3.22.4(zod@3.22.4) - dev: false - /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: From 79142320134063504869407badbe08295f6c03a2 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Sat, 23 Mar 2024 08:48:46 +0100 Subject: [PATCH 7/8] Avoid repetition in JSDoc comment --- packages/core/rsc/streamable.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/rsc/streamable.tsx b/packages/core/rsc/streamable.tsx index 787e2e6ba9d7..203e75e9ff95 100644 --- a/packages/core/rsc/streamable.tsx +++ b/packages/core/rsc/streamable.tsx @@ -322,7 +322,7 @@ export function render< /** * Control how text, function calls, and tool calls are composed into a single UI. * - * Per default, text, function and tool nodes are wrapped in a React Fragment. + * Per default, the nodes of all three kinds are wrapped in a React Fragment. */ compose?: Renderer<{ text: React.ReactNode | undefined; @@ -375,6 +375,7 @@ export function render< // The default text renderer just returns the content as string. const text = options.text ?? (({ content }: { content: string }) => content); + // The default compose renderer wraps the nodes of all three kinds in a React Fragment. const compose = options.compose ?? (({ text, functionCall, toolCalls }) => ( From 6af84afc8dfa8948b39005dfd5acdb0269dbb634 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Tue, 26 Mar 2024 11:12:51 +0100 Subject: [PATCH 8/8] Add changeset for multiple tool calls in `render` --- .changeset/serious-timers-drive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/serious-timers-drive.md diff --git a/.changeset/serious-timers-drive.md b/.changeset/serious-timers-drive.md new file mode 100644 index 000000000000..64440d051ce1 --- /dev/null +++ b/.changeset/serious-timers-drive.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +feat(ai/rsc): support multiple tool calls in render function