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

fix: runtime bug #252

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
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 components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type { XProviderProps } from './x-provider';

export { default as useXChat } from './useXChat';

export { default as useXAgent } from './useXAgent';
export { default as useXAgent, XAgent } from './useXAgent';

export { default as XStream } from './x-stream';
export type { XStreamOptions } from './x-stream';
Expand Down
4 changes: 2 additions & 2 deletions components/useXChat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface XChatConfig<
AgentMessage extends SimpleType = string,
BubbleMessage extends SimpleType = AgentMessage,
> {
agent: XAgent<AgentMessage>;
agent?: XAgent<AgentMessage>;

defaultMessages?: DefaultMessageInfo<AgentMessage>[];

Expand Down Expand Up @@ -187,7 +187,7 @@ export default function useXChat<
return msg;
};

agent.request(
agent?.request(
{
message,
messages: getRequestMessages(),
Expand Down
80 changes: 58 additions & 22 deletions components/x-request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type XRequestOptions = XRequestBaseOptions & XRequestCustomOptions;

type XRequestMessageContent = string | AnyObject;

interface XRequestMessage extends AnyObject {
export interface XRequestMessage extends AnyObject {
role?: string;
content?: XRequestMessageContent;
}
Expand All @@ -68,11 +68,11 @@ export interface XRequestParams {
messages?: XRequestMessage[];
}

export interface XRequestCallbacks<Output> {
export interface XRequestCallbacks {
/**
* @description Callback when the request is successful
*/
onSuccess: (chunks: Output[]) => void;
onSuccess: (chunk: SSEOutput, chunks?: SSEOutput[]) => void;

/**
* @description Callback when the request fails
Expand All @@ -82,23 +82,28 @@ export interface XRequestCallbacks<Output> {
/**
* @description Callback when the request is updated
*/
onUpdate: (chunk: Output) => void;
onUpdate: (chunk: SSEOutput, chunks?: SSEOutput[]) => void;
}

export type XRequestFunction<Input = AnyObject, Output = SSEOutput> = (
params: XRequestParams & Input,
callbacks: XRequestCallbacks<Output>,
transformStream?: XStreamOptions<Output>['transformStream'],
export type XRequestCreate<Params extends XRequestParams = AnyObject> = (
params: Params,
callbacks?: XRequestCallbacks,
transformStream?: XStreamOptions<SSEOutput>['transformStream'],
) => Promise<void>;

class XRequestClass {
export type XRequestFunction<Params extends XRequestParams = AnyObject> = (
params: Params,
callbacks: XRequestCallbacks,
) => Promise<void>;

class XRequestClass<Params extends XRequestParams = AnyObject> {
readonly baseURL;
readonly model;

private defaultHeaders;
private customOptions;

private static instanceBuffer: Map<string, XRequestClass> = new Map();
private static instanceBuffer = new Map();

private constructor(options: XRequestOptions) {
const { baseURL, model, dangerouslyApiKey, ...customOptions } = options;
Expand All @@ -114,22 +119,24 @@ class XRequestClass {
this.customOptions = customOptions;
}

public static init(options: XRequestOptions): XRequestClass {
public static init<P extends XRequestParams = AnyObject>(
options: XRequestOptions,
): XRequestClass<P> {
YumoImer marked this conversation as resolved.
Show resolved Hide resolved
const id = options.baseURL;

if (!id || typeof id !== 'string') throw new Error('The baseURL is not valid!');

if (!XRequestClass.instanceBuffer.has(id)) {
XRequestClass.instanceBuffer.set(id, new XRequestClass(options));
XRequestClass.instanceBuffer.set(id, new XRequestClass<P>(options));
}

return XRequestClass.instanceBuffer.get(id) as XRequestClass;
return XRequestClass.instanceBuffer.get(id);
}

public create = async <Input = AnyObject, Output = SSEOutput>(
params: XRequestParams & Input,
callbacks?: XRequestCallbacks<Output>,
transformStream?: XStreamOptions<Output>['transformStream'],
public create = async (
params: Params,
callbacks?: XRequestCallbacks,
transformStream?: XStreamOptions<SSEOutput>['transformStream'],
) => {
const { onSuccess, onError, onUpdate } = callbacks || {};

Expand All @@ -150,7 +157,8 @@ class XRequestClass {

const contentType = response.headers.get('content-type') || '';

const chunks: Output[] = [];
const chunks: SSEOutput[] = [];
let deltaChunk: SSEOutput;

if (contentType.includes('text/event-stream')) {
for await (const chunk of XStream({
Expand All @@ -159,19 +167,23 @@ class XRequestClass {
})) {
chunks.push(chunk);

onUpdate?.(chunk);
deltaChunk = this.delta(chunks);

onUpdate?.(deltaChunk, chunks);
}
} else if (contentType.includes('application/json')) {
const chunk: Output = await response.json();
const chunk: SSEOutput = await response.json();

chunks.push(chunk);

onUpdate?.(chunk);
deltaChunk = chunk;

onUpdate?.(deltaChunk, chunks);
} else {
throw new Error(`The response content-type: ${contentType} is not support!`);
}

onSuccess?.(chunks);
onSuccess?.(deltaChunk!, chunks);
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error!');

Expand All @@ -180,6 +192,30 @@ class XRequestClass {
throw err;
}
};

public deltaContentRegex = new RegExp(
/"delta":\s*\{[^}]*?"content"\s*:\s*"([^"\\]*(\\.[^"\\]*)*)"/s,
);
YumoImer marked this conversation as resolved.
Show resolved Hide resolved

private delta = (chunks: SSEOutput[]): SSEOutput => {
let deltaContent = '';

for (const chunk of chunks) {
const match = this.deltaContentRegex.exec(chunk.data);
if (match?.[1]) {
deltaContent += match[1];
}
}

const lastChunk = chunks[chunks.length - 1];

const lastChunkContentMatch = this.deltaContentRegex.exec(lastChunk.data) || [];

return {
...lastChunk,
data: lastChunk.data.replace(lastChunkContentMatch[1], deltaContent),
};
};
YumoImer marked this conversation as resolved.
Show resolved Hide resolved
}

const XRequest = XRequestClass.init;
Expand Down
7 changes: 6 additions & 1 deletion components/x-stream/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,19 @@ function splitStream() {
});
}

/**
* @link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields
*/
export type SSEFields = 'data' | 'event' | 'id' | 'retry';

/**
* @example
* const sseObject = {
* event: 'delta',
* data: '{ key: "world!" }',
* };
*/
export type SSEOutput = Record<string, any>;
export type SSEOutput = Partial<Record<SSEFields, any>>;

/**
* @description A TransformStream inst that transforms a part string into {@link SSEOutput}
Expand Down
2 changes: 1 addition & 1 deletion docs/playground/independent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const useStyle = createStyles(({ token, css }) => {
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 24px 0;
padding: 24px;
gap: 16px;
`,
messages: css`
Expand Down
7 changes: 6 additions & 1 deletion docs/react/model-use-qwen.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ This method is **a ready-to-use solution for React environments** provided by An
```tsx
import { useXAgent } from '@ant-design/x';

interface YourMessageType {
role?: string;
content?: string;
}

// ... react env
const [agent] = useXAgent({
const [agent] = useXAgent<YourMessageType>({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: 'qwen-plus',
// Use cautiously in production!
Expand Down
7 changes: 6 additions & 1 deletion docs/react/model-use-qwen.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ order: 1
```tsx
import { useXAgent } from '@ant-design/x';

interface YourMessageType {
role?: string;
content?: string;
}
YumoImer marked this conversation as resolved.
Show resolved Hide resolved

// ... react env
const [agent] = useXAgent({
const [agent] = useXAgent<YourMessageType>({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: 'qwen-plus',
// 请谨慎在生产环境使用!
Expand Down
Loading