Skip to content

Commit ed83fb7

Browse files
authored
Refactor response generation (#200)
All core functionality of response generation has been moved to `response.js`. One new change, in particular, is the `generateResponse` function which combines all 3 types of providers into a single AsyncGenerator. This makes the code much more maintainable. Performance wise, there should be no significant impact, but this needs to be observed for some time. As a result of this patch, #193 is also fixed. fixes #193
1 parent 44d571a commit ed83fb7

File tree

3 files changed

+188
-57
lines changed

3 files changed

+188
-57
lines changed

src/aiChat.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,8 @@ export default function Chat({ launchContext }) {
325325
const devMode = Preferences["devMode"];
326326

327327
if (!info.stream) {
328-
response = await getChatResponse(currentChatData, query);
328+
// Use getChatResponseSync for non-streaming providers to get the complete string directly
329+
response = await getChatResponseSync(currentChatData, query);
329330
setCurrentChatMessage(currentChatData, setCurrentChatData, messageID, { response: response });
330331

331332
elapsed = (Date.now() - start) / 1000;

src/api/gpt.jsx

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { useEffect, useState } from "react";
1717

1818
import * as providers from "./providers.js";
19+
import { generateResponse, generateResponseSync, generateChatResponse, generateChatResponseSync } from "./response.js";
1920

2021
import throttle, { AIChatDelayFunction } from "#root/src/helpers/throttle.js";
2122

@@ -24,10 +25,9 @@ import { PasteAction } from "../components/actions/pasteAction.jsx";
2425
import { autoCheckForUpdates } from "../helpers/update.jsx";
2526

2627
import { init } from "../api/init.js";
27-
import { Message, pairs_to_messages } from "../classes/message.js";
28+
import { Message } from "../classes/message.js";
2829
import { Preferences } from "./preferences.js";
2930

30-
import { truncate_chat } from "../helpers/helper.js";
3131
import { plainTextMarkdown } from "../helpers/markdown.js";
3232
import { getFormattedWebResult, systemResponse, web_search_mode, webSystemPrompt } from "./tools/web";
3333

@@ -200,7 +200,7 @@ export default (
200200
let start = Date.now();
201201

202202
if (!info.stream) {
203-
response = await chatCompletion(info, messages, options);
203+
response = await generateResponseSync(info, messages, options);
204204
setMarkdown(response);
205205

206206
elapsed = (Date.now() - start) / 1000;
@@ -223,7 +223,7 @@ export default (
223223

224224
const handler = throttle(_handler, { delay: 30, delayFunction: AIChatDelayFunction() });
225225

226-
await chatCompletion(info, messages, options, handler, get_status);
226+
await processStream(generateResponse(info, messages, options, get_status), info.provider, handler);
227227

228228
handler.flush();
229229
}
@@ -497,66 +497,21 @@ export default (
497497
);
498498
};
499499

500-
// Generate response using a chat context (array of Messages, NOT MessagePairs - conversion should be done before this)
501-
// and options. This is the core function of the extension.
502-
//
503-
// if stream_update is passed, we will call it with stream_update(new_message) every time a chunk is received
504-
// otherwise, this function returns an async generator (if stream = true) or a string (if stream = false)
505-
// if status is passed, we will stop generating when status() is true
506-
//
507-
// also note that the chat parameter is an array of Message objects, and how it is handled is up to the provider modules.
508-
// for most providers it is first converted into JSON format before being used.
509-
export const chatCompletion = async (info, chat, options, stream_update = null, status = null) => {
510-
const provider = info.provider; // provider object
511-
// additional options
512-
options = providers.get_options_from_info(info, options);
513-
514-
let response = await providers.generate(provider, chat, options, { stream_update });
515-
516-
// stream = false
517-
if (typeof response === "string") {
518-
// will not be a string if stream is enabled
519-
return response;
520-
}
521-
522-
// streaming related handling
523-
if (provider.customStream) return; // handled in the provider
524-
if (stream_update) {
525-
await processStream(response, provider, stream_update, status);
526-
return;
527-
}
528-
return response;
529-
};
530-
531500
// generate response. input: currentChat is a chat object from AI Chat; query (string) is optional
532501
// see the documentation of chatCompletion for details on the other parameters
533502
export const getChatResponse = async (currentChat, query = null, stream_update = null, status = null) => {
534-
// load provider and model
535-
const info = providers.get_provider_info(currentChat.provider);
536-
// additional options
537-
let options = providers.get_options_from_info(info, currentChat.options);
538-
539-
// format chat
540-
let chat = pairs_to_messages(currentChat.messages, query);
541-
chat = truncate_chat(chat, info);
503+
if (!stream_update) {
504+
// If no stream_update is provided, return the async generator
505+
return generateChatResponse(currentChat, query, {}, status);
506+
}
542507

543-
// generate response
544-
return await chatCompletion(info, chat, options, stream_update, status);
508+
// If stream_update is provided, process the stream
509+
await processStream(generateChatResponse(currentChat, query, {}, status), null, stream_update, status);
545510
};
546511

547512
// generate response using a chat context and a query, while forcing stream = false
548513
export const getChatResponseSync = async (currentChat, query = null) => {
549-
let r = await getChatResponse(currentChat, query);
550-
if (typeof r === "string") {
551-
return r;
552-
}
553-
554-
const info = providers.get_provider_info(currentChat.provider);
555-
let response = "";
556-
for await (const chunk of processChunks(r, info.provider)) {
557-
response = chunk;
558-
}
559-
return response;
514+
return await generateChatResponseSync(currentChat, query);
560515
};
561516

562517
// yield chunks incrementally from a response.

src/api/response.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import * as providers from "./providers.js";
2+
import { pairs_to_messages } from "#root/src/classes/message.js";
3+
import { truncate_chat } from "#root/src/helpers/helper.js";
4+
5+
/**
6+
* Generate a response from a provider.
7+
* Returns an async generator that yields each chunk of the response.
8+
*
9+
* @param {Object} info - Provider info object (contains provider, model, etc.)
10+
* @param {Array} chat - Array of messages
11+
* @param {Object} options - Additional options
12+
* @param {Function} status - Optional function that returns true when generation should stop
13+
* @returns {AsyncGenerator} - Async generator that yields chunks of the response
14+
*/
15+
export const generateResponse = async function* (info, chat, options = {}, status = null) {
16+
const provider = info.provider;
17+
18+
// Get options from info
19+
options = providers.get_options_from_info(info, options);
20+
21+
try {
22+
if (provider.customStream) {
23+
// For providers that use stream_update callback: Use a promise-based signal.
24+
let response = "";
25+
let queue = [];
26+
let signalPromiseResolve = null;
27+
let signalPromise = new Promise((resolve) => {
28+
signalPromiseResolve = resolve;
29+
});
30+
let done = false;
31+
let error = null;
32+
33+
// Start the provider's generate method in the background
34+
provider
35+
.generate(chat, options, {
36+
stream_update: (new_response) => {
37+
if (new_response === null || new_response === undefined) return;
38+
39+
const content = new_response.substring(response.length);
40+
response = new_response;
41+
42+
if (content) {
43+
queue.push(content);
44+
if (signalPromiseResolve) {
45+
// Signal that new data is available
46+
const resolveFunc = signalPromiseResolve;
47+
signalPromiseResolve = null; // Prevent resolving multiple times before next await
48+
signalPromise = new Promise((resolve) => {
49+
signalPromiseResolve = resolve;
50+
}); // Create next signal promise
51+
resolveFunc();
52+
}
53+
}
54+
},
55+
})
56+
.then(() => {
57+
done = true;
58+
if (signalPromiseResolve) signalPromiseResolve(); // Signal completion
59+
})
60+
.catch((e) => {
61+
console.error("Error in provider.generate:", e);
62+
error = e;
63+
done = true;
64+
if (signalPromiseResolve) signalPromiseResolve(); // Signal error/completion
65+
});
66+
67+
// Async generator loop consuming the queue, waiting for signals
68+
while (!done || queue.length > 0) {
69+
// Check status before yielding or waiting
70+
if (status && status()) {
71+
console.log("Generator stopping due to status check.");
72+
// TODO: Ideally, signal the provider.generate to stop if possible.
73+
return;
74+
}
75+
76+
// Yield all currently queued chunks
77+
while (queue.length > 0) {
78+
yield queue.shift();
79+
}
80+
81+
// If not done and queue is empty, wait for the next signal (data or completion)
82+
if (!done) {
83+
await signalPromise;
84+
}
85+
}
86+
87+
if (error) {
88+
console.log(error);
89+
}
90+
} else if (info.stream) {
91+
// For providers using async generators (standard streaming)
92+
const response = await provider.generate(chat, options, {});
93+
94+
// Yield chunks, checking status periodically
95+
let i = 0;
96+
for await (const chunk of response) {
97+
if ((i & 15) === 0 && status && status()) {
98+
return;
99+
}
100+
yield chunk;
101+
i++;
102+
}
103+
} else {
104+
// For non-streaming providers
105+
const response = await provider.generate(chat, options, {});
106+
yield response;
107+
}
108+
} catch (e) {
109+
console.error("Error generating response:", e);
110+
yield `Error: ${e.message}`;
111+
}
112+
};
113+
114+
/**
115+
* Generate a complete response as a string.
116+
*
117+
* @param {Object} info - Provider info object
118+
* @param {Array} chat - Array of messages
119+
* @param {Object} options - Additional options
120+
* @returns {Promise<string>} - Promise that resolves to the complete response
121+
*/
122+
export const generateResponseSync = async (info, chat, options = {}) => {
123+
let completeResponse = "";
124+
125+
for await (const chunk of generateResponse(info, chat, options)) {
126+
completeResponse += chunk;
127+
}
128+
129+
return completeResponse;
130+
};
131+
132+
/**
133+
* Format chat and generate a response.
134+
*
135+
* @param {Object} currentChat - The chat object
136+
* @param {string|null} query - Optional query to append
137+
* @param {Object} options - Additional options
138+
* @param {Function} status - Optional function that returns true when generation should stop
139+
* @returns {AsyncGenerator} - Async generator that yields chunks of the response
140+
*/
141+
export const generateChatResponse = async function* (currentChat, query = null, options = {}, status = null) {
142+
// Load provider and model
143+
const info = providers.get_provider_info(currentChat.provider);
144+
145+
// Format chat
146+
let chat = pairs_to_messages(currentChat.messages, query);
147+
chat = truncate_chat(chat, info);
148+
149+
// Merge options
150+
const mergedOptions = {
151+
...providers.get_options_from_info(info, currentChat.options),
152+
...options,
153+
};
154+
155+
// Generate response
156+
yield* generateResponse(info, chat, mergedOptions, status);
157+
};
158+
159+
/**
160+
* Get a complete chat response as a string
161+
*
162+
* @param {Object} currentChat - The chat object
163+
* @param {string|null} query - Optional query to append
164+
* @param {Object} options - Additional options
165+
* @returns {Promise<string>} - Promise that resolves to the complete response
166+
*/
167+
export const generateChatResponseSync = async (currentChat, query = null, options = {}) => {
168+
let completeResponse = "";
169+
170+
for await (const chunk of generateChatResponse(currentChat, query, options)) {
171+
completeResponse += chunk;
172+
}
173+
174+
return completeResponse;
175+
};

0 commit comments

Comments
 (0)