Skip to content

Commit

Permalink
Extract ConcurrencyLimitedFetch into own file
Browse files Browse the repository at this point in the history
  • Loading branch information
robatwilliams committed Dec 21, 2023
1 parent 57aa1bf commit e967a4d
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 74 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ npm run sideload:desktop

| Path | Description |
| --- | --- |
| src/functions/functions.js | The JavaScript functions which implement the functions. |
| src/functions/functions.json | The metadata which details each function and references its implementation. |
| src/functions/functions.mjs | The JavaScript functions which implement the functions. |
| shared.html | Root page loaded in the background during add-in startup. |
| manifest-local.xml | A version of manifest.xml which references https://localhost:3000/ for add-in development use. |
| manifest.xml | Configures where the add-in should be loaded from and what features it will make use of. |
Expand Down
2 changes: 1 addition & 1 deletion manifest-localhost.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3000/src/functions/functions.js"/>
<bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3000/src/functions/functions.mjs"/>
<bt:Url id="Functions.Metadata.Url" DefaultValue="https://localhost:3000/src/functions/functions.json"/>
<bt:Url id="Shared.Url" DefaultValue="https://localhost:3000/shared.html"/>
</bt:Urls>
Expand Down
2 changes: 1 addition & 1 deletion manifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<bt:Image id="Icon.80x80" DefaultValue="https://robatwilliams.github.io/openai-excel-functions/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Functions.Script.Url" DefaultValue="https://robatwilliams.github.io/openai-excel-functions/src/functions/functions.js"/>
<bt:Url id="Functions.Script.Url" DefaultValue="https://robatwilliams.github.io/openai-excel-functions/src/functions/functions.mjs"/>
<bt:Url id="Functions.Metadata.Url" DefaultValue="https://robatwilliams.github.io/openai-excel-functions/src/functions/functions.json"/>
<bt:Url id="Shared.Url" DefaultValue="https://robatwilliams.github.io/openai-excel-functions/shared.html"/>
</bt:Urls>
Expand Down
2 changes: 1 addition & 1 deletion shared.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
type="text/javascript"
src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"
></script>
<script type="module" src="src/functions/functions.js"></script>
<script type="module" src="src/functions/functions.mjs"></script>
</head>

<body></body>
Expand Down
59 changes: 59 additions & 0 deletions src/functions/ConcurrencyLimitedFetch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Limits the number of concurrent requests to the API.
*
* 1. Prevents flooding the API when full recalculation occurs in a sheet with
* many completion cells (e.g. when opening the workbook).
* 2. Gives the user a chance to cancel mass recalculation (e.g. by undo or
* delete) before all the requests are dispatched.
*/
export default class ConcurrencyLimitedFetch {
/**
* High enough to be unnoticeable for small scenarios, and to complete large
* scenarios in reasonable time.
* Low enough to avoid incurring excessive costs before the user has a chance
* to cancel, even for large model input/output sizes.
*/
static _PENDING_LIMIT = 10;

_queue = [];
_pendingCount = 0;

fetch(resource, options) {
const promise = new Promise((resolve, reject) => {
const task = {
args: { resource, options },
resolve,
reject,
};
this._queue.push(task);
});

this._process();

return promise;
}

_process() {
if (
this._queue.length === 0 ||
this._pendingCount > ConcurrencyLimitedFetch._PENDING_LIMIT
) {
return;
}

const task = this._queue.shift();

if (task.args.options.signal.aborted) {
task.reject(task.args.options.signal.reason);
}

this._pendingCount++;
const promise = fetch(task.args.resource, task.args.options);

promise.then(task.resolve, task.reject);
promise.finally(() => {
this._pendingCount--;
this._process();
});
}
}
72 changes: 3 additions & 69 deletions src/functions/functions.js → src/functions/functions.mjs
Original file line number Diff line number Diff line change
@@ -1,70 +1,12 @@
/**
* Limits the number of concurrent requests to the API.
*
* 1. Prevents flooding the API when full recalculation occurs in a sheet with
* many completion cells (e.g. when opening the workbook).
* 2. Gives the user a chance to cancel mass recalculation (e.g. by undo or
* delete) before all the requests are dispatched.
*/
class ConcurrencyLimitedFetch {
/**
* High enough to be unnoticeable for small scenarios, and to complete large
* scenarios in reasonable time.
* Low enough to avoid incurring excessive costs before the user has a chance
* to cancel, even for large model input/output sizes.
*/
static _PENDING_LIMIT = 10;

_queue = [];
_pendingCount = 0;

fetch(resource, options) {
const promise = new Promise((resolve, reject) => {
const task = {
args: { resource, options },
resolve,
reject,
};
this._queue.push(task);
});

this._process();

return promise;
}

_process() {
if (
this._queue.length === 0 ||
this._pendingCount > ConcurrencyLimitedFetch._PENDING_LIMIT
) {
return;
}

const task = this._queue.shift();

if (task.args.options.signal.aborted) {
task.reject(task.args.options.signal.reason);
}

this._pendingCount++;
const promise = fetch(task.args.resource, task.args.options);

promise.then(task.resolve, task.reject);
promise.finally(() => {
this._pendingCount--;
this._process();
});
}
}
import ConcurrencyLimitedFetch from './ConcurrencyLimitedFetch.mjs';

const COMPLETION_ENTITY_KIND = 'openai-excel-functions:chat-completion';
const EMPTY_OR_ZERO = 0;

const fetcher = new ConcurrencyLimitedFetch();

CustomFunctions.associate('CHAT_COMPLETE', chatComplete);
async function chatComplete(messages, params, invocation) {
export async function chatComplete(messages, params, invocation) {
const {
API_KEY: apiKey,
API_BASE: apiBase = 'https://api.openai.com/',
Expand Down Expand Up @@ -152,7 +94,7 @@ async function chatComplete(messages, params, invocation) {

// Terminology note: our _cost_ is driven by usage and OpenAI's _prices_.
CustomFunctions.associate('COST', cost);
function cost(completionsMatrix, pricesMatrix) {
export function cost(completionsMatrix, pricesMatrix) {
const allPrices = Object.fromEntries(
pricesMatrix.map((row) => [row[0], { input: row[1], output: row[2] }]),
);
Expand Down Expand Up @@ -225,11 +167,3 @@ function mapObject(object, callback) {
Object.entries(object).map(([key, value]) => [key, callback(value)]),
);
}

// For unit testing.
if (typeof module === 'object') {
module.exports = {
chatComplete,
cost,
};
}
2 changes: 1 addition & 1 deletion src/functions/functions.test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';
import { makeCompletionEntity } from '../../testFramework/completionEntityStub.mjs';
import { chatComplete, cost } from './functions.js';
import { makeCompletionResponse } from '../../testFramework/completionResponseStub.mjs';
import { chatComplete, cost } from './functions.mjs';

describe('CHAT_COMPLETE', () => {
it('makes a completion for given messages', async (t) => {
Expand Down

0 comments on commit e967a4d

Please sign in to comment.