Skip to content

Commit 17eb59f

Browse files
authored
make responses of chatRequestAccess proposal easier to consume and digest (microsoft#195479)
* make responses of `chatRequestAccess` proposal easier to consume and digest * allow same modern JS in API as in the rest
1 parent 7da6dc1 commit 17eb59f

File tree

4 files changed

+206
-21
lines changed

4 files changed

+206
-21
lines changed

src/tsconfig.vscode-dts.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
"forceConsistentCasingInFileNames": true,
1414
"types": [],
1515
"lib": [
16-
"es5",
17-
"ES2015.Iterable"
16+
"ES2022"
1817
],
1918
},
2019
"include": [

src/vs/base/common/async.ts

+57
Original file line numberDiff line numberDiff line change
@@ -1887,4 +1887,61 @@ export function createCancelableAsyncIterable<T>(callback: (token: CancellationT
18871887
});
18881888
}
18891889

1890+
export class DeferredAsyncIterableObject<T> {
1891+
1892+
private readonly _deferred = new DeferredPromise<void>();
1893+
private readonly _asyncIterable: AsyncIterableObject<T>;
1894+
1895+
private _errorFn: (error: Error) => void;
1896+
private _emitFn: (item: T) => void;
1897+
1898+
constructor() {
1899+
this._asyncIterable = new AsyncIterableObject(emitter => {
1900+
1901+
if (earlyError) {
1902+
emitter.reject(earlyError);
1903+
return;
1904+
}
1905+
if (earlyItems) {
1906+
emitter.emitMany(earlyItems);
1907+
}
1908+
this._errorFn = (error: Error) => emitter.reject(error);
1909+
this._emitFn = (item: T) => emitter.emitOne(item);
1910+
return this._deferred.p;
1911+
});
1912+
1913+
let earlyError: Error | undefined;
1914+
let earlyItems: T[] | undefined;
1915+
1916+
this._emitFn = (item: T) => {
1917+
if (!earlyItems) {
1918+
earlyItems = [];
1919+
}
1920+
earlyItems.push(item);
1921+
};
1922+
this._errorFn = (error: Error) => {
1923+
if (!earlyError) {
1924+
earlyError = error;
1925+
}
1926+
};
1927+
}
1928+
1929+
get asyncIterable(): AsyncIterableObject<T> {
1930+
return this._asyncIterable;
1931+
}
1932+
1933+
complete(): void {
1934+
this._deferred.complete();
1935+
}
1936+
1937+
error(error: Error): void {
1938+
this._errorFn(error);
1939+
this._deferred.complete();
1940+
}
1941+
1942+
emit(item: T): void {
1943+
this._emitFn(item);
1944+
}
1945+
}
1946+
18901947
//#endregion

src/vs/workbench/api/common/extHostChatProvider.ts

+86-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { CancellationToken } from 'vs/base/common/cancellation';
6+
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
77
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
88
import { ILogService } from 'vs/platform/log/common/log';
99
import { ExtHostChatProviderShape, IMainContext, MainContext, MainThreadChatProviderShape } from 'vs/workbench/api/common/extHost.protocol';
@@ -12,12 +12,82 @@ import type * as vscode from 'vscode';
1212
import { Progress } from 'vs/platform/progress/common/progress';
1313
import { IChatMessage, IChatResponseFragment } from 'vs/workbench/contrib/chat/common/chatProvider';
1414
import { ExtensionIdentifier, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions';
15+
import { DeferredAsyncIterableObject } from 'vs/base/common/async';
16+
import { Emitter } from 'vs/base/common/event';
1517

1618
type ProviderData = {
1719
readonly extension: ExtensionIdentifier;
1820
readonly provider: vscode.ChatResponseProvider;
1921
};
2022

23+
class ChatResponseStream {
24+
25+
readonly apiObj: vscode.ChatResponseStream;
26+
readonly stream = new DeferredAsyncIterableObject<string>();
27+
28+
constructor(option: number, stream?: DeferredAsyncIterableObject<string>) {
29+
this.stream = stream ?? new DeferredAsyncIterableObject<string>();
30+
const that = this;
31+
this.apiObj = {
32+
option: option,
33+
response: that.stream.asyncIterable
34+
};
35+
}
36+
}
37+
38+
class ChatRequest {
39+
40+
readonly apiObject: vscode.ChatRequest;
41+
42+
private readonly _onDidStart = new Emitter<vscode.ChatResponseStream>();
43+
private readonly _responseStreams = new Map<number, ChatResponseStream>();
44+
private readonly _defaultStream = new DeferredAsyncIterableObject<string>();
45+
private _isDone: boolean = false;
46+
47+
constructor(
48+
promise: Promise<any>,
49+
cts: CancellationTokenSource
50+
) {
51+
const that = this;
52+
this.apiObject = {
53+
result: promise,
54+
response: that._defaultStream.asyncIterable,
55+
onDidStartResponseStream: that._onDidStart.event,
56+
cancel() { cts.cancel(); },
57+
};
58+
59+
promise.finally(() => {
60+
this._isDone = true;
61+
if (this._responseStreams.size > 0) {
62+
for (const [, value] of this._responseStreams) {
63+
value.stream.complete();
64+
}
65+
} else {
66+
this._defaultStream.complete();
67+
}
68+
});
69+
}
70+
71+
handleFragment(fragment: IChatResponseFragment): void {
72+
if (this._isDone) {
73+
return;
74+
}
75+
let res = this._responseStreams.get(fragment.index);
76+
if (!res) {
77+
if (this._responseStreams.size === 0) {
78+
// the first response claims the default response
79+
res = new ChatResponseStream(fragment.index, this._defaultStream);
80+
} else {
81+
res = new ChatResponseStream(fragment.index);
82+
}
83+
this._responseStreams.set(fragment.index, res);
84+
this._onDidStart.fire(res.apiObj);
85+
}
86+
res.stream.emit(fragment.part);
87+
}
88+
89+
}
90+
2191
export class ExtHostChatProvider implements ExtHostChatProviderShape {
2292

2393
private static _idPool = 1;
@@ -62,7 +132,7 @@ export class ExtHostChatProvider implements ExtHostChatProviderShape {
62132

63133
//#region --- making request
64134

65-
private readonly _pendingRequest = new Map<number, vscode.Progress<vscode.ChatResponseFragment>>();
135+
private readonly _pendingRequest = new Map<number, { res: ChatRequest }>();
66136

67137
private readonly _chatAccessAllowList = new ExtensionIdentifierMap<Promise<unknown>>();
68138

@@ -84,24 +154,31 @@ export class ExtHostChatProvider implements ExtHostChatProviderShape {
84154
get isRevoked() {
85155
return !that._chatAccessAllowList.has(from);
86156
},
87-
async makeRequest(messages, options, progress, token) {
157+
makeRequest(messages, options, token) {
88158

89159
if (!that._chatAccessAllowList.has(from)) {
90160
throw new Error('Access to chat has been revoked');
91161
}
92162

163+
const cts = new CancellationTokenSource(token);
93164
const requestId = (Math.random() * 1e6) | 0;
94-
that._pendingRequest.set(requestId, progress);
95-
try {
96-
await that._proxy.$fetchResponse(from, identifier, requestId, messages.map(typeConvert.ChatMessage.from), options, token);
97-
} finally {
165+
const requestPromise = that._proxy.$fetchResponse(from, identifier, requestId, messages.map(typeConvert.ChatMessage.from), options ?? {}, cts.token);
166+
const res = new ChatRequest(requestPromise, cts);
167+
that._pendingRequest.set(requestId, { res });
168+
169+
requestPromise.finally(() => {
98170
that._pendingRequest.delete(requestId);
99-
}
171+
});
172+
173+
return res.apiObject;
100174
},
101175
};
102176
}
103177

104178
async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise<void> {
105-
this._pendingRequest.get(requestId)?.report(chunk);
179+
const data = this._pendingRequest.get(requestId);//.report(chunk);
180+
if (data) {
181+
data.res.handleFragment(chunk);
182+
}
106183
}
107184
}

src/vscode-dts/vscode.proposed.chatRequestAccess.d.ts

+62-10
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,84 @@
55

66
declare module 'vscode' {
77

8-
export interface ChatResponseFragment {
9-
index: number;
10-
part: string;
8+
export interface ChatResponseStream {
9+
10+
/**
11+
* The response stream.
12+
*/
13+
readonly response: AsyncIterable<string>;
14+
15+
/**
16+
* The variant of multiple responses. This is used to disambiguate between multiple
17+
* response streams when having asked for multiple response options
18+
*/
19+
readonly option: number;
20+
}
21+
22+
export interface ChatRequest {
23+
24+
/**
25+
* The overall result of the request which represents failure or success
26+
* but _not_ the actual response or responses
27+
*/
28+
result: Thenable<any>;
29+
30+
/**
31+
* The _default response_ stream. This is the stream of the first response option
32+
* receiving data.
33+
*
34+
* Usually there is only one response option and this stream is more convienient to use
35+
* than the {@link onDidStartResponseStream `onDidStartResponseStream`} event.
36+
*/
37+
response: AsyncIterable<string>;
38+
39+
/**
40+
* An event that fires whenever a new response option is available. The response
41+
* itself is a stream of the actual response.
42+
*
43+
* *Note* that the first time this event fires, the {@link ChatResponseStream.response response stream}
44+
* is the same as the {@link response `default response stream`}.
45+
*
46+
* *Note* that unless requested there is only one response option, so this event will only fire
47+
* once.
48+
*/
49+
onDidStartResponseStream: Event<ChatResponseStream>;
50+
51+
/**
52+
* Cancel this request.
53+
*/
54+
// TODO@API remove this? We pass a token to makeRequest call already
55+
cancel(): void;
1156
}
1257

1358
/**
1459
* Represents access to using a chat provider (LLM). Access is granted and temporary, usually only valid
15-
* for the duration of an user interaction.
60+
* for the duration of an user interaction or specific time frame.
1661
*/
1762
export interface ChatAccess {
1863

1964
/**
20-
* Whether the access to chat has been revoked. This happens when the user interaction that allowed for
21-
* chat access is finished.
65+
* Whether the access to chat has been revoked. This happens when the condition that allowed for
66+
* chat access doesn't hold anymore, e.g a user interaction has ended.
2267
*/
2368
isRevoked: boolean;
2469

2570
/**
26-
* TODO: return an AsyncIterable instead of asking to pass Progress<...>?
71+
* Make a chat request.
72+
*
73+
* The actual response will be reported back via the `progress` callback. The promise returned by this function
74+
* returns a overall result which represents failure or success of the request.
75+
*
76+
* Chat can be asked for multiple response options. In that case the `progress` callback will be called multiple
77+
* time with different `ChatResponseStream` objects. Each object represents a different response option and the actual
78+
* response will be reported back via their `stream` property.
79+
*
80+
* *Note:* This will throw an error if access has been revoked.
2781
*
2882
* @param messages
2983
* @param options
30-
* @param progress
31-
* @param token
3284
*/
33-
makeRequest(messages: ChatMessage[], options: { [name: string]: any }, progress: Progress<ChatResponseFragment>, token: CancellationToken): Thenable<any>;
85+
makeRequest(messages: ChatMessage[], options: { [name: string]: any }, token: CancellationToken): ChatRequest;
3486
}
3587

3688
export namespace chat {

0 commit comments

Comments
 (0)