Skip to content

Commit 323ecde

Browse files
Adds streaming "basic" article (#4)
* Adds streaming tutorial examples * Make the streaming article a bit more basic, conform to the template format * Revise article content
1 parent 1552ad8 commit 323ecde

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4800
-0
lines changed

basics/streaming/README.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Streaming
2+
3+
<details>
4+
<summary>How to run this example</summary>
5+
6+
```bash
7+
# Set your API key as an environment variable.
8+
export SUBSTRATE_API_KEY=ENTER_YOUR_KEY
9+
10+
# Run the TypeScript + NextJS example
11+
cd typescript/quotes-nextjs # Navigate to the typescript example
12+
npm install # Install dependencies
13+
npm run dev # Run the example
14+
open http://127.0.0.1:3000 # View the web app
15+
16+
# Run the Python + FastAPI example
17+
# Note: First install dependencies in the root examples directory.
18+
cd python/quotes-fastapi # Navigate to the python example
19+
poetry install # Install dependencies
20+
poetry run fastapi dev main.py # Run the example
21+
open http://127.0.0.1:8000 # View the web app
22+
```
23+
24+
</details>
25+
26+
![hero](hero.png)
27+
28+
Substrate supports streaming responses in order to help improve the UX of your application by reducing the time it takes before a user sees a response.
29+
30+
The prevailing method of implementing streaming amongst inference providers is via Server-Sent Events and this is what we have chosen due to it's
31+
widespread support and simple API.
32+
33+
We've designed our streaming API to work intuitively with the graph-oriented nature of the Substrate system. As a result we will stream back
34+
messages for every node in the graph you run and messages relating to the graph as a whole. For many models we only support streaming back the final
35+
result from a node, but for most LLM nodes we will also stream incremental chunks as the result is produced. We'll use the `ComputeText` node the the
36+
examples here which emits these chunks within the delta message in the response stream.
37+
38+
In this article we're going to step through the process of making a streaming API request to Substrate and displaying streamed content to the user
39+
in a web frontend. This example will focus on using NextJS and FastAPI, but there are other code samples included for some other popular alternatives.
40+
41+
First, let's take a look at making a streaming request with a single-node graph.
42+
43+
In TypeScript this looks like the following,
44+
45+
```typescript
46+
import { Substrate, ComputeText } from "substrate";
47+
48+
const substrate = new Substrate({ apiKey: process.env["SUBSTRATE_API_KEY"] });
49+
50+
const node = new ComputeText({ prompt: "an inspirational programming quote" });
51+
52+
const stream = await substrate.stream(node);
53+
54+
for await (let message of stream) {
55+
if (message.node_id === node.id && message.object === "node.delta") {
56+
process.stdout.write(message.data.text);
57+
}
58+
}
59+
```
60+
61+
And when using Python it looks like this,
62+
63+
```python
64+
import os
65+
66+
from substrate import ComputeText, Substrate
67+
68+
substrate = Substrate(api_key=os.environ.get("SUBSTRATE_API_KEY"))
69+
70+
node = ComputeText(prompt="an inspirational programming quote")
71+
72+
stream = substrate.stream(node)
73+
74+
for message in stream.iter():
75+
if message.data["object"] == "node.delta":
76+
print(message.data["data"]["text"], end="")
77+
```
78+
79+
Because we're only using a single node, we know here that every `node.delta` message was produced by our single node. Every message also contains
80+
the `node_id` - which we can use to identify messages from different nodes, but we're going to keep this example simple with one node.
81+
82+
When building an application that exposes the streaming result of a Substrate graph, what we will need to do is have our backend expose an endpoint
83+
that reponds with a `text/event-stream`.
84+
85+
When using NextJS you will use a route handler to do so and it will look like this:
86+
87+
```typescript
88+
"use server";
89+
90+
import { Substrate, ComputeText } from "substrate";
91+
92+
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
93+
94+
const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
95+
96+
export async function POST() {
97+
const node = new ComputeText({
98+
prompt: "an inspirational programming quote",
99+
});
100+
const stream = await substrate.stream(node);
101+
return new Response(stream.apiResponse.body, {
102+
headers: { "Content-Type": "text/event-stream" },
103+
});
104+
}
105+
```
106+
107+
When using FastAPI it will look like this:
108+
109+
```python
110+
import os
111+
112+
from fastapi import FastAPI
113+
from fastapi.responses import StreamingResponse
114+
from substrate import ComputeText, Substrate
115+
116+
app = FastAPI()
117+
118+
@app.get("/quote")
119+
def quote():
120+
substrate = Substrate(api_key=os.environ.get("SUBSTRATE_API_KEY"))
121+
node = ComputeText(prompt="an inspirational programming quote")
122+
stream = substrate.stream(node)
123+
return StreamingResponse(stream.iter_events(), media_type="text/event-stream")
124+
```
125+
126+
In the TypeScript example we're accessing the response body from the Substrate API and using this stream in the response directly, and in
127+
the Python example we're exposing an iterator that produces formatted Server-Sent Event messages that can be used in the `StreamingResponse`.
128+
129+
Once these endpoints are setup we need to consume these streams on our web front end. In the following examples we've implemented a
130+
simple UI to make the request and as the stream is recieved we upate the content the user is shown one chunk at a time.
131+
132+
In our NextJS example we're able to use the `substrate` TypeScript SDK, which exposes a helper method for parsing Server-Sent Event messages.
133+
This makes it a little easier to deal with a streaming response in a similar we are on the server.
134+
135+
```tsx
136+
"use client";
137+
import { useState } from "react";
138+
import { sb } from "substrate";
139+
140+
export function Example() {
141+
const [quote, setQuote] = useState<string>("");
142+
const getQuote = async (e: any) => {
143+
e.preventDefault();
144+
const response = await fetch("/quote", { method: "POST" });
145+
setQuote("");
146+
const stream = await sb.streaming.fromSSEResponse(response);
147+
148+
for await (let message of stream) {
149+
if (message.object === "node.delta") {
150+
setQuote((state) => state + message.data.text);
151+
}
152+
}
153+
};
154+
155+
return (
156+
<>
157+
<button onClick={getQuote}>Get a quote</button>
158+
<article>{quote}</article>
159+
</>
160+
);
161+
}
162+
```
163+
164+
In our FastAPI example we're not using a JS bundler, but instead are demonstrating how this might work when using Vanilla JS and some built-in web APIs
165+
to accomplish the same task. We're using the `EventSource` object to handle the connection and event-stream parsing, but we'll also need to use `JSON.parse` on
166+
the message data since we use a structured format for encoding the message contents there. Lastly, we make sure to `close()` the `EventSource`
167+
once we receive the final message so that we do not make additional stream requests.
168+
169+
```javascript
170+
document.addEventListener("DOMContentLoaded", () => {
171+
const output = document.getElementById("output");
172+
173+
const button = document.getElementById("button");
174+
button.addEventListener("click", async (e) => {
175+
output.innerText = "";
176+
177+
const sse = new EventSource("/quote");
178+
179+
sse.addEventListener("message", (e) => {
180+
const message = JSON.parse(e.data);
181+
if (message.object === "node.delta") {
182+
// when we have a delta message, append the text data to our output element
183+
output.innerText += message.data.text;
184+
}
185+
if (message.object === "graph.result") {
186+
// last message is the graph result.
187+
sse.close();
188+
}
189+
});
190+
});
191+
});
192+
```

basics/streaming/hero.png

4 MB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import os
2+
3+
from substrate import ComputeText, Substrate
4+
5+
substrate = Substrate(api_key=os.environ.get("SUBSTRATE_API_KEY"))
6+
7+
node = ComputeText(prompt="an inspirational programming quote")
8+
9+
stream = substrate.stream(node)
10+
11+
for message in stream.iter():
12+
if message.data["object"] == "node.delta":
13+
print(message.data["data"]["text"], end="")

0 commit comments

Comments
 (0)