Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dev-ai #264

Merged
merged 40 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
832d183
[Env] update `openai` to `4.80.1`
Bistard Jan 28, 2025
871a21d
[Feat] new configuration: `application.ai.textModel`
Bistard Jan 31, 2025
81b2255
Merge branch 'dev-hoverbox' into dev-ai
Bistard Jan 31, 2025
411d242
[Fix] circular dependency on `EditorPaneDescriptor`
Bistard Jan 31, 2025
2d35148
[Refactor] revist old AI-related files and renew them
Bistard Feb 1, 2025
0fd1201
[Chore] rename `GPTModel` to `TextGPTModel`
Bistard Feb 1, 2025
f7864ef
[Chore]
Bistard Feb 1, 2025
8512f71
[Chore]
Bistard Feb 1, 2025
e1be271
[Chore]
Bistard Feb 1, 2025
2949dd1
[Refactor] introduce base class `TextSharedOpenAIModel` and introduce…
Bistard Feb 1, 2025
35d5b04
[Chore] rename
Bistard Feb 1, 2025
c65e9f6
[Feat] introduce `IEncryptionService` and registered into IPC
Bistard Feb 2, 2025
806167d
[Fix] typo
Bistard Feb 2, 2025
3e7668b
[Feat] new api: `isEncryptionAvailable()`
Bistard Feb 2, 2025
f2ca0c0
[Doc]
Bistard Feb 2, 2025
1ee2f13
[Fix] invalid configuration register
Bistard Feb 2, 2025
c0ec640
[Fix] correctly set `status` and `configuration` in inspector window
Bistard Feb 2, 2025
633917b
[Fix] inspector encrypt data
Bistard Feb 2, 2025
9f1b917
[Feat] integrate API key handling (encrypt/decrypt)
Bistard Feb 3, 2025
4a3f2da
[Feat] add `AsyncOnly` type utility to enforce `ProxyChannel` namespace
Bistard Feb 3, 2025
4b734cf
[Refactor] avoid using `AsyncResult` in `AI`-related codes
Bistard Feb 3, 2025
67b026e
[Chore]
Bistard Feb 3, 2025
b3cacc9
[Feat] new api: `updateAPIKey`
Bistard Feb 3, 2025
76ffb76
[Chore] rename
Bistard Feb 3, 2025
9f80d82
[Feat] implement IPC channel for communication with AI text service
Bistard Feb 3, 2025
16cff02
[Feat] implement streaming request handling in AI text channels
Bistard Feb 3, 2025
2d74688
[Fix] potential memory leak
Bistard Feb 3, 2025
681d9aa
[Chore] rename
Bistard Feb 3, 2025
f3fb513
[Typo]
Bistard Feb 4, 2025
11aa82c
[Chore]
Bistard Feb 4, 2025
803e53a
[Fix] typo
Bistard Feb 4, 2025
e9d46ca
[Feat] new `AIError` class for standardized LLM error handling
Bistard Feb 4, 2025
580d392
[Refactor] `AITextService` methods returns `AsyncResult` with `AIErro…
Bistard Feb 4, 2025
f65c600
[Chore]
Bistard Feb 4, 2025
147faa1
[Chore]
Bistard Feb 4, 2025
5558073
Merge branch 'dev-ai' of https://github.com/Bistard/nota into dev-ai
Bistard Feb 4, 2025
d478ea2
[Chore] fix
Bistard Feb 4, 2025
7d82296
[Feat] new: `AI.Modality`
Bistard Feb 4, 2025
b156a71
[Feat] AI model registration and management system
Bistard Feb 4, 2025
463aefc
[Refactor] Rename model type to model name and introduce `LLMModel` b…
Bistard Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"katex": "^0.16.21",
"marked": "^14.1.3",
"minimist": "^1.2.8",
"openai": "^4.28.0",
"openai": "^4.80.1",
"prosemirror-history": "^1.4.1",
"prosemirror-model": "^1.18.1",
"prosemirror-state": "^1.4.2",
Expand Down
171 changes: 170 additions & 1 deletion src/base/common/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IDisposable, toDisposable } from "src/base/common/dispose";
import { Result, err, ok } from "src/base/common/result";
import { mixin } from "src/base/common/utilities/object";
import { errorToMessage, panic } from "src/base/common/utilities/panic";
import { isPromise } from "src/base/common/utilities/type";
import { isObject, isPromise, isString } from "src/base/common/utilities/type";

type IErrorCallback = (error: any) => void;
type IErrorListener = IErrorCallback;
Expand Down Expand Up @@ -319,3 +319,172 @@ export class Stacktrace {
return this;
}
}

/**
* A standardized Error for LLM operations, encapsulating both
* provider-specific errors (e.g. OpenAI) and internal errors.
*/
export class AIError extends Error {

// [fields]

/** Indicates the name of the LLM model caused the error */
public readonly modelName: string | null;

/** Error category for quick classification */
public readonly errorCategory:
| 'API'
| 'Network'
| 'Auth'
| 'Client'
| 'Internal'
| 'Unknown';

/** HTTP status code (if applicable) */
public readonly status?: number;

/** HTTP headers from the response (if applicable) */
public readonly headers?: Record<string, string>;

/** Original error object (if available) */
public readonly internal?: unknown;

/** Provider-specific error code (e.g. 'invalid_api_key') */
public readonly code?: string;

/** Request ID from response headers (if available) */
public readonly requestId?: string;

// [constructor]

constructor(modelName: string | null, rawError: unknown) {
const { message, metadata } = AIError.__parseRawError(rawError);
super(message);

this.modelName = modelName;
this.errorCategory = metadata.category;
this.internal = metadata.internal;

// API Error metadata
this.status = metadata.status;
this.headers = metadata.headers;
this.code = metadata.code;
this.requestId = metadata.headers?.['x-request-id'];
}

// [static methods]

private static __parseRawError(raw: unknown): {
message: string;
metadata: __ErrorParseMetadata;
} {

// raw string, simple return
if (isString(raw)) {
return {
message: raw,
metadata: { category: 'Unknown' }
};
}

// non object, no can do here.
if (!isObject(raw)) {
return {
message: 'Unknown non-object error',
metadata: {
category: 'Unknown',
internal: raw
}
};
}

// default metadata
const metadata: __ErrorParseMetadata = {
category: 'Unknown',
status: undefined,
headers: undefined,
code: undefined,
internal: raw
};

// obtain metadata
const status = Number(raw['status']) ?? undefined;
const headers = raw['headers'];
const code = this.__safeGetCode(raw);
const message = this.__getHumanMessage(raw, status, code);

// clean up
metadata.status = status;
metadata.headers = headers;
metadata.code = code;
metadata.category = this.__determineCategory(raw, status, code);

return { message, metadata };
}

private static __determineCategory(
raw: object,
status?: number,
code?: string
): AIError['errorCategory'] {

// Network errors (timeout, connection issues)
if (raw instanceof Error && [
'ETIMEDOUT',
'ECONNREFUSED',
'ENOTFOUND'
].some(code => code === raw['code'])
) {
return 'Network';
}

// API errors classification
if (status) {
if (status === 401) return 'Auth';
if (status === 429) return 'API';
if (status >= 400 && status < 500) return 'Client';
if (status >= 500) return 'Internal';
}

// OpenAI-style error codes
if (code) {
if (code.includes('invalid_api')) return 'Auth';
if (code.includes('rate_limit')) return 'API';
}

return 'Unknown';
}

private static __getHumanMessage(
raw: object,
status?: number,
code?: string
): string {
const baseMessage = raw['message'] || 'Unknown error';
const parts: string[] = [];

if (status) parts.push(`Status: ${status}`);
if (code) parts.push(`Code: ${code}`);

return parts.length > 0
? `${baseMessage} (${parts.join(', ')})`
: baseMessage;
}

private static __safeGetCode(raw: object): string | undefined {
const codeSources = [
(<any>raw).code,
(<any>raw).error?.code,
(<any>raw).body?.error?.code
];
return codeSources.find(v => isString(v));
}
}

type __ErrorParseMetadata = {
category: AIError['errorCategory'];
status?: number;
headers?: Record<string, string>;
code?: string;
internal?: unknown;
};
2 changes: 1 addition & 1 deletion src/base/common/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ interface IJsonSchemaForString extends IJsonSchemaBase<'string'> {
/** The maximum length of the string. */
maxLength?: number;

/** The predefined format of the string. Example: 'email', 'phone number', 'post address' etc. */
/** The predefined format of the string (written Regular Expression). Example: 'email', 'phone number', 'post address' etc. */
Bistard marked this conversation as resolved.
Show resolved Hide resolved
format?: string;

/** Regular expression to match the valid string. */
Expand Down
46 changes: 35 additions & 11 deletions src/base/common/utilities/panic.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { IpcErrorTag } from "src/base/common/error";
import type { ArrayToUnion } from "src/base/common/utilities/type";

/**
* To prevent potential circular dependency issues due to the wide use of `panic`
* throughout the program, this function has been relocated to a separate file
* throughout the program, these functions has been relocated to a separate file
* that does not import any other files.
*/

function __stringifySafe(obj: unknown): string {
try {
// eslint-disable-next-line local/code-no-json-stringify
return JSON.stringify(obj);
} catch (err) {
return '';
}
}

/**
* @description Panics the program by throwing an error with the provided message.
*
Expand All @@ -31,7 +23,7 @@ export function panic(error: unknown): never {
throw new Error('unknown panic error');
}

if (error instanceof Error) {
if (isError(error)) {
// eslint-disable-next-line local/code-no-throw
throw error;
}
Expand All @@ -40,6 +32,29 @@ export function panic(error: unknown): never {
throw new Error(errorToMessage(error));
}

/**
* @description An ipc-safe function to check the given object is {@link Error}
* or not.
*/
export function isError(obj: any): obj is Error {
if (obj instanceof Error) {
return true;
}

// not even object
if (typeof obj !== "object" || obj === null) {
return false;
}

// check for standard errors
if (typeof obj['name'] === 'string' && typeof obj['message'] === 'string') {
return true;
}

// check for IPC-converted errors
return obj?.[IpcErrorTag] === null;
}

/**
* @description Asserts that the provided object is neither `undefined` nor
* `null`.
Expand Down Expand Up @@ -223,3 +238,12 @@ function __stackToMessage(stack: any): string {
return stack;
}
}

function __stringifySafe(obj: unknown): string {
try {
// eslint-disable-next-line local/code-no-json-stringify
return JSON.stringify(obj);
} catch (err) {
return '';
}
}
16 changes: 16 additions & 0 deletions src/base/common/utilities/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Register } from "src/base/common/event";

/**
* Represents all the falsy value in JavaScript.
Expand Down Expand Up @@ -445,6 +446,21 @@ export type Promisify<T> = {
: T[K]
};

/**
* Ensure all functions in an object all return {@link Promise} (event register
* are ignored).
*/
export type AsyncOnly<T> = {
[K in keyof T]:
T[K] extends Register<any>
? T[K] // keep event-register
: T[K] extends (...args: any[]) => infer R
? R extends Promise<any>
? T[K] // only allow async function
: never // no sync function
: T[K]; // non-functional remain the same
};

/**
* Split string into a tuple by a deliminator.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/code/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class BrowserInstance extends Disposable implements IBrowserService {

// alert error from main process
onMainProcess(IpcChannel.rendererAlertError, error => {
ErrorHandler.onUnexpectedError(error);
this.commandService.executeCommand(AllCommands.alertError, 'MainProcess', error);
});

// execute command request from main process
Expand Down
24 changes: 21 additions & 3 deletions src/code/browser/inspector/inspectorItemRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { FuzzyScore } from "src/base/common/fuzzy";
import { isBoolean, isNullable, isNumber, isString } from "src/base/common/utilities/type";
import { InspectorItem } from "src/code/browser/inspector/inspectorTree";
import { IConfigurationService, ConfigurationModuleType } from "src/platform/configuration/common/configuration";
import { IEncryptionService } from "src/platform/encryption/common/encryptionService";
import { IHostService } from "src/platform/host/common/hostService";
import { InspectorDataType } from "src/platform/inspector/common/inspector";
import { StatusKey } from "src/platform/status/common/status";

interface IInspectorItemMetadata extends IListViewMetadata {
readonly keyElement: HTMLElement;
Expand All @@ -21,7 +25,10 @@ export class InspectorItemRenderer implements ITreeListRenderer<InspectorItem, F
public readonly type: RendererType = InspectorRendererType;

constructor(
private readonly configurationService: IConfigurationService,
private readonly getCurrentView: () => InspectorDataType | undefined,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IHostService private readonly hostService: IHostService,
@IEncryptionService private readonly encryptionService: IEncryptionService,
) {}

public render(element: HTMLElement): IInspectorItemMetadata {
Expand Down Expand Up @@ -63,7 +70,7 @@ export class InspectorItemRenderer implements ITreeListRenderer<InspectorItem, F
}
// editable
else {
valuePart.addEventListener('change', e => {
valuePart.addEventListener('change', async e => {
const raw = valuePart.value;
const rawLower = raw.toLowerCase();
let value: any;
Expand Down Expand Up @@ -91,7 +98,18 @@ export class InspectorItemRenderer implements ITreeListRenderer<InspectorItem, F
else {
value = raw;
}
this.configurationService.set(data.id!, value, { type: ConfigurationModuleType.User });

const currView = this.getCurrentView();
if (currView === InspectorDataType.Status) {
// special handling: write these values as encrypted
if (data.id === StatusKey.textAPIKey) {
value = await this.encryptionService.encrypt(value);
}
this.hostService.setApplicationStatus(data.id as StatusKey, value);
} else if (currView === InspectorDataType.Configuration) {
this.configurationService.set(data.id!, value, { type: ConfigurationModuleType.User });
}

e.stopPropagation();
});
}
Expand Down
Loading
Loading