Skip to content

Commit d0e0829

Browse files
committed
chore: improve ux and add progress statuses
1 parent adf4837 commit d0e0829

File tree

3 files changed

+79
-58
lines changed

3 files changed

+79
-58
lines changed

api/src/functions/assistant.js

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ require("dotenv/config");
22

33
const { Readable } = require("node:stream");
44
const { app } = require("@azure/functions");
5-
app.setup({ enableHttpStream: true });
65

76
const { AzureOpenAI } = require("openai");
87
const { DefaultAzureCredential, getBearerTokenProvider } = require("@azure/identity");
@@ -19,7 +18,7 @@ const {
1918
// please add proper error handling.
2019

2120
async function initAzureOpenAI(context) {
22-
context.log("Using Azure OpenAI (w/ Microsoft Entra ID) ...");
21+
console.log("Using Azure OpenAI (w/ Microsoft Entra ID) ...");
2322
const credential = new DefaultAzureCredential();
2423
const azureADTokenProvider = getBearerTokenProvider(credential, "https://cognitiveservices.azure.com/.default");
2524
return new AzureOpenAI({
@@ -30,7 +29,8 @@ async function initAzureOpenAI(context) {
3029
const assistantDefinition = {
3130
name: "Finance Assistant",
3231
instructions:
33-
"You are a personal finance assistant. Retrieve the latest closing price of a stock using its ticker symbol. You also know how to generate a full body email in both plain text and html.",
32+
"You are a personal finance assistant. Retrieve the latest closing price of a stock using its ticker symbol. "
33+
+ "You also know how to generate a full body email in both plain text and html. Only use the functions you have been provideded with",
3434
tools: [
3535
{
3636
type: "function",
@@ -80,54 +80,73 @@ const assistantDefinition = {
8080
model: AZURE_DEPLOYMENT_NAME,
8181
};
8282

83-
async function* processQuery(userQuery, context) {
84-
context.log('Step 0: Connect and acquire an OpenAI instance');
85-
const openai = await initAzureOpenAI(context);
83+
async function* processQuery(userQuery) {
84+
console.log('Step 0: Connect and acquire an OpenAI instance');
85+
const openai = await initAzureOpenAI();
8686

87-
context.log('Step 1: Retrieve or Create an Assistant');
87+
console.log('Step 1: Retrieve or Create an Assistant');
8888
const assistant = ASSISTANT_ID
8989
? await openai.beta.assistants.retrieve(ASSISTANT_ID)
9090
: await openai.beta.assistants.create(assistantDefinition);
9191

92-
context.log('Step 2: Create a Thread');
92+
console.log('Step 2: Create a Thread');
9393
const thread = await openai.beta.threads.create();
9494

95-
context.log('Step 3: Add a Message to the Thread');
95+
console.log('Step 3: Add a Message to the Thread');
9696
const message = await openai.beta.threads.messages.create(thread.id, {
9797
role: "user",
9898
content: userQuery,
9999
});
100100

101-
context.log('Step 4: Create a Run (and stream the response)');
101+
console.log('Step 4: Create a Run (and stream the response)');
102102
const run = openai.beta.threads.runs.stream(thread.id, {
103103
assistant_id: assistant.id,
104104
stream: true,
105-
tool_choice: { "type": "function", "function": { "name": "getStockPrice" } }
106105
});
107106

108-
context.log('Step 5: Read streamed response', { run });
107+
console.log('Step 5: Read streamed response', { run });
109108
for await (const chunk of run) {
110109
const { event, data } = chunk;
111110

112-
if (event === "thread.message.delta") {
111+
console.log('processing event', { event, data });
112+
113+
if (event === "thread.run.created") {
114+
yield "@created";
115+
console.log('Processed thread.run.created');
116+
}
117+
else if (event === "thread.run.queued") {
118+
yield "@queued";
119+
console.log('Processed thread.run.queued');
120+
}
121+
else if (event === "thread.run.in_progress") {
122+
yield "@in_progress";
123+
console.log('Processed thread.run.in_progress');
124+
}
125+
else if (event === "thread.message.delta") {
113126
const delta = data.delta;
114127

115128
if (delta) {
116129
const value = delta.content[0]?.text?.value || "";
117130
yield value;
118-
context.log('Processed thread.message.delta', { value });
131+
console.log('Processed thread.message.delta', { value });
119132
}
120-
} else if (event === "thread.run.requires_action") {
121-
yield* handleRequiresAction(openai, data, data.id, data.thread_id, context);
133+
}
134+
else if (event === "thread.run.failed") {
135+
const value = data.last_error.message;
136+
yield value;
137+
console.log('Processed thread.run.failed', { value });
138+
}
139+
else if (event === "thread.run.requires_action") {
140+
yield* handleRequiresAction(openai, data, data.id, data.thread_id);
122141
}
123142
// else if ... handle the other events as needed
124143
}
125144

126-
context.log('Done!');
145+
console.log('Done!');
127146
}
128147

129-
async function* handleRequiresAction(openai, run, runId, threadId, context) {
130-
context.log('Handle Function Calling', { required_action: run.required_action.submit_tool_outputs.tool_calls });
148+
async function* handleRequiresAction(openai, run, runId, threadId) {
149+
console.log('Handle Function Calling', { required_action: run.required_action.submit_tool_outputs.tool_calls });
131150
try {
132151
const toolOutputs = await Promise.all(
133152
run.required_action.submit_tool_outputs.tool_calls.map(
@@ -156,36 +175,38 @@ async function* handleRequiresAction(openai, run, runId, threadId, context) {
156175
);
157176

158177
// Submit all the tool outputs at the same time
159-
yield* submitToolOutputs(openai, toolOutputs, runId, threadId, context);
178+
yield* submitToolOutputs(openai, toolOutputs, runId, threadId);
160179
} catch (error) {
161-
context.error("Error processing required action:", error);
180+
console.error("Error processing required action:", error);
162181
}
163182
}
164183

165-
async function* submitToolOutputs(openai, toolOutputs, runId, threadId, context) {
184+
async function* submitToolOutputs(openai, toolOutputs, runId, threadId) {
166185
try {
167186
// Use the submitToolOutputsStream helper
168-
context.log('Call Tool output and stream the response');
187+
console.log('Call Tool output and stream the response');
169188
const asyncStream = openai.beta.threads.runs.submitToolOutputsStream(
170189
threadId,
171190
runId,
172191
{ tool_outputs: toolOutputs }
173192
);
174193
for await (const chunk of asyncStream) {
175-
if (chunk.event === "thread.message.delta") {
194+
const { event, data } = chunk;
195+
// console.log({ event, data });
196+
if (event === "thread.message.delta") {
176197
// stream message back to UI
177-
const { delta } = chunk.data;
198+
const { delta } = data;
178199

179200
if (delta) {
180201
const value = delta.content[0]?.text?.value || "";
181202
yield value;
182-
context.log('Processed thread.message.delta (tool output)', { value });
203+
console.log('Processed thread.message.delta (tool output)', { value });
183204
}
184205
}
185206
// else if ... handle the other events as needed
186207
}
187208
} catch (error) {
188-
context.error("Error submitting tool outputs:", error);
209+
console.error("Error submitting tool outputs:", error);
189210
}
190211
}
191212

@@ -204,19 +225,19 @@ async function writeAndSendEmail(subject, text, html) {
204225
}
205226

206227
// API definition
207-
228+
app.setup({ enableHttpStream: true });
208229
app.http("assistant", {
209230
methods: ["POST"],
210231
authLevel: "anonymous",
211-
handler: async (request, context) => {
212-
context.log(`Http function processed request for url "${request.url}"`);
232+
handler: async (request) => {
233+
console.log(`Http function processed request for url "${request.url}"`);
213234
const query = await request.text();
214235

215236
return {
216237
headers: {
217238
'Content-Type': 'text/plain',
218239
"Transfer-Encoding": "chunked"
219-
}, body: Readable.from(processQuery(query, context))
240+
}, body: Readable.from(processQuery(query))
220241
};
221242
},
222243
});

src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ <h1>Azure OpenAI / Finance Assistant Demo</h1>
2929
<section class="output__container">
3030
<div id="loadingRef" class="loader__container hidden">
3131
<span class="spinner"><span class="spinner__tail"></span></span>
32-
Working on it ...
32+
<p>Working on it (<span id="statusLabelRef">waiting</span>)</p>
3333
</div>
3434
<div id="outputRef" class="hidden"></div>
3535
</section>

src/script.js

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const loadingRef = document.querySelector("#loadingRef");
66
const userQueryRef = document.querySelector("#userQueryRef");
77
const cancelQueryRef = document.querySelector("#cancelQueryRef");
88
const submitQueryRef = document.querySelector("#submitQueryRef");
9+
const statusLabelRef = document.querySelector("#statusLabelRef");
910

1011
userQueryRef.value =
1112
"Based on the latest financial data and current stock market trends, can you provide a detailed analysis of Microsoft's current state? Please include insights into their recent performance, market position, and future outlook. Additionally, retrieve and include the latest closing price of Microsoft's stock using its ticker symbol (MSFT). Send me the full analysis by email.";
@@ -22,69 +23,68 @@ submitQueryRef
2223
.addEventListener("click", async (event) => {
2324
const { value } = userQueryRef;
2425
if (value) {
26+
statusLabelRef.innerHTML = "waiting";
27+
outputRef.innerHTML = "";
2528

2629
loadingRef.classList.remove("hidden");
2730
outputRef.classList.add("hidden");
2831
cancelQueryRef.classList.remove("hidden");
2932
submitQueryRef.classList.add("hidden");
3033

3134
autoTimeout();
32-
submitQuery(value, insertText);
35+
submitQuery(value);
3336
}
3437
});
3538

3639
function autoTimeout() {
3740
autoAbortTimeout = setTimeout(() => {
3841
cancelQueryRef.click();
39-
42+
outputRef.classList.remove("hidden");
4043
if (outputRef.innerHTML === "") {
4144
outputRef.innerHTML = "Your Assistant could not fetch data. Please try again!"
4245
}
43-
44-
}, 30_000); // in case, cancel request after 30 of timeout
45-
46+
}, 60_000); // cancel request if it times out
4647
}
4748

48-
function insertText(chunk) {
49-
const delta = new TextDecoder().decode(chunk);
50-
outputRef.innerHTML += delta;
51-
outputRef.scrollTop = outputRef.scrollHeight; // scroll to bottom
52-
};
53-
54-
55-
function submitQuery(userQuery, cb) {
56-
49+
function submitQuery(body) {
5750
const { API_URL = 'http://localhost:7071' } = import.meta.env;
58-
5951
fetch(`${API_URL}/api/assistant`, {
52+
body,
6053
method: "POST",
61-
body: userQuery,
6254
signal: aborter.signal
6355
}).then(response => response.body)
64-
.then(rs => processReadableStream(rs, cb));
56+
.then(processReadableStream);
6557
}
6658

67-
function processReadableStream(rs, cb) {
68-
69-
rs.pipeTo(new WritableStream({
59+
function processReadableStream(stream) {
60+
stream.pipeTo(new WritableStream({
7061
write(chunk, controller) {
71-
cb(chunk);
72-
},
73-
start(controller) {
74-
outputRef.innerHTML = "";
62+
const value = new TextDecoder().decode(chunk);
63+
if (value.startsWith('@')) {
64+
statusLabelRef.innerHTML = value.replace('@', '');
65+
return;
66+
}
67+
7568
loadingRef.classList.add("hidden");
7669
outputRef.classList.remove("hidden");
70+
71+
outputRef.innerHTML += value;
72+
outputRef.scrollTop = outputRef.scrollHeight; // scroll to bottom
73+
},
74+
start(controller) {
7775
clearTimeout(autoAbortTimeout); // cancel
7876
},
7977
close(controller) {
8078
cancelQueryRef.classList.add("hidden");
8179
submitQueryRef.classList.remove("hidden");
8280
loadingRef.classList.add("hidden");
81+
outputRef.classList.remove("hidden");
82+
if (outputRef.innerHTML === "") {
83+
outputRef.innerHTML = "Whoops, something went wrong. Please try again!"
84+
}
8385
},
8486
abort(reason) {
8587
console.log(reason);
8688
},
8789
})).catch(console.error);
88-
89-
return rs;
9090
}

0 commit comments

Comments
 (0)