Skip to content

Commit 8b47446

Browse files
authored
refactor: create llm response (#5499)
* feat: add LLM response processing functions, including the creation of stream-based and complete responses * feat: add volta configuration for node and pnpm versions * refactor: update LLM response handling and event structure in tool choice logic * feat: update LLM response structure and integrate with tool choice logic * refactor: clean up imports and remove unused streamResponse function in chat and toolChoice modules * refactor: rename answer variable to answerBuffer for clarity in LLM response handling * feat: enhance LLM response handling with tool options and integrate tools into chat and tool choice logic * refactor: remove volta configuration from package.json * refactor: reorganize LLM response types and ensure default values for token counts * refactor: streamline LLM response handling by consolidating response structure and removing redundant checks * refactor: enhance LLM response handling by consolidating tool options and streamlining event callbacks * fix: build error * refactor: update tool type definitions for consistency in tool handling
1 parent b56b5c0 commit 8b47446

File tree

6 files changed

+528
-621
lines changed

6 files changed

+528
-621
lines changed

.husky/pre-commit

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env sh
22
. "$(dirname -- "$0")/_/husky.sh"
33

4-
if command -v npx >/dev/null 2>&1; then
4+
if command -v pnpm >/dev/null 2>&1; then
5+
pnpm lint-staged
6+
elif command -v npx >/dev/null 2>&1; then
57
npx lint-staged
68
fi

packages/global/core/ai/request.ts

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import type {
2+
ChatCompletion,
3+
ChatCompletionCreateParamsNonStreaming,
4+
ChatCompletionCreateParamsStreaming,
5+
ChatCompletionMessageToolCall,
6+
ChatCompletionToolType,
7+
CompletionFinishReason,
8+
CompletionUsage,
9+
StreamChatType
10+
} from './type';
11+
import {
12+
parseLLMStreamResponse,
13+
parseReasoningContent,
14+
removeDatasetCiteText
15+
} from '../../../service/core/ai/utils';
16+
import { createChatCompletion } from '../../../service/core/ai/config';
17+
import type { OpenaiAccountType } from '../../support/user/team/type';
18+
import { getNanoid } from '../../common/string/tools';
19+
20+
type BasicResponseParams = {
21+
reasoning?: boolean;
22+
toolMode?: 'toolChoice' | 'prompt';
23+
abortSignal?: () => boolean | undefined;
24+
retainDatasetCite?: boolean;
25+
};
26+
27+
type CreateStreamResponseParams = BasicResponseParams & {
28+
stream: StreamChatType;
29+
};
30+
31+
type CreateCompleteResopnseParams = Omit<BasicResponseParams, 'abortSignal'> & {
32+
completion: ChatCompletion;
33+
};
34+
35+
type ResponseEvents = {
36+
onStreaming?: ({ responseContent }: { responseContent: string }) => void;
37+
onReasoning?: ({ reasoningContent }: { reasoningContent: string }) => void;
38+
onToolCalling?: ({
39+
callingTool,
40+
toolId
41+
}: {
42+
callingTool: { name: string; arguments: string };
43+
toolId: string;
44+
}) => void;
45+
onToolParaming?: ({
46+
currentTool,
47+
params
48+
}: {
49+
currentTool: ChatCompletionMessageToolCall;
50+
params: string;
51+
}) => void;
52+
onReasoned?: ({ reasoningContent }: { reasoningContent: string }) => void;
53+
onToolCalled?: ({ calls }: { calls: ChatCompletionMessageToolCall[] }) => void;
54+
onCompleted?: ({ responseContent }: { responseContent: string }) => void;
55+
};
56+
57+
type CreateStreamResponseProps = {
58+
params: CreateStreamResponseParams;
59+
} & ResponseEvents;
60+
61+
type CreateCompleteResopnseProps = {
62+
params: CreateCompleteResopnseParams;
63+
} & ResponseEvents;
64+
65+
type CreateLLMResponseProps = {
66+
llmOptions: Omit<
67+
ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming,
68+
'tools'
69+
>;
70+
params: BasicResponseParams;
71+
tools: ChatCompletionToolType[];
72+
userKey?: OpenaiAccountType;
73+
} & ResponseEvents;
74+
75+
type LLMResponse = {
76+
answerText: string;
77+
reasoningText: string;
78+
toolCalls: ChatCompletionMessageToolCall[];
79+
finish_reason: CompletionFinishReason;
80+
isStreamResponse: boolean;
81+
getEmptyResponseTip: () => string;
82+
inputTokens: number;
83+
outputTokens: number;
84+
};
85+
86+
type StreamResponse = Pick<
87+
LLMResponse,
88+
'answerText' | 'reasoningText' | 'toolCalls' | 'finish_reason'
89+
> & {
90+
usage?: CompletionUsage;
91+
};
92+
93+
type CompleteResopnse = Pick<
94+
LLMResponse,
95+
'answerText' | 'reasoningText' | 'toolCalls' | 'finish_reason'
96+
> & {
97+
usage?: CompletionUsage;
98+
};
99+
100+
export const createLLMResponse = async (args: CreateLLMResponseProps): Promise<LLMResponse> => {
101+
const { llmOptions, tools, userKey, params, ...events } = args;
102+
103+
const body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming = {
104+
tools,
105+
...llmOptions
106+
};
107+
108+
const { response, isStreamResponse, getEmptyResponseTip } = await createChatCompletion({
109+
body,
110+
userKey,
111+
options: {
112+
headers: {
113+
Accept: 'application/json, text/plain, */*'
114+
}
115+
}
116+
});
117+
118+
if (isStreamResponse) {
119+
const { usage, ...streamResults } = await createStreamResponse({
120+
params: { stream: response, ...params },
121+
...events
122+
});
123+
124+
return {
125+
inputTokens: usage?.prompt_tokens ?? 0,
126+
outputTokens: usage?.completion_tokens ?? 0,
127+
isStreamResponse,
128+
getEmptyResponseTip,
129+
...streamResults
130+
};
131+
} else {
132+
const { usage, ...completeResults } = await createCompleteResponse({
133+
params: { completion: response, ...params },
134+
...events
135+
});
136+
137+
return {
138+
inputTokens: usage?.prompt_tokens ?? 0,
139+
outputTokens: usage?.completion_tokens ?? 0,
140+
isStreamResponse,
141+
getEmptyResponseTip,
142+
...completeResults
143+
};
144+
}
145+
};
146+
147+
export const createStreamResponse = async (
148+
args: CreateStreamResponseProps
149+
): Promise<StreamResponse> => {
150+
const { params, ...events } = args;
151+
152+
const {
153+
abortSignal,
154+
stream,
155+
reasoning,
156+
retainDatasetCite = true,
157+
toolMode = 'toolChoice'
158+
} = params;
159+
160+
const { parsePart, getResponseData, updateFinishReason } = parseLLMStreamResponse();
161+
162+
let calls: ChatCompletionMessageToolCall[] = [];
163+
let startResponseWrite = false;
164+
let answer = '';
165+
166+
for await (const part of stream) {
167+
if (abortSignal && abortSignal()) {
168+
stream.controller?.abort();
169+
updateFinishReason('close');
170+
break;
171+
}
172+
173+
const responseChoice = part.choices?.[0]?.delta;
174+
const {
175+
reasoningContent,
176+
responseContent,
177+
content: originContent
178+
} = parsePart({
179+
part,
180+
parseThinkTag: true,
181+
retainDatasetCite
182+
});
183+
184+
if (reasoning && reasoningContent) {
185+
events?.onReasoning?.({ reasoningContent });
186+
}
187+
if (responseContent && originContent) {
188+
if (toolMode === 'prompt') {
189+
answer += originContent;
190+
if (startResponseWrite) {
191+
events?.onStreaming?.({ responseContent });
192+
} else if (answer.length >= 3) {
193+
answer = answer.trimStart();
194+
if (/0(:|)/.test(answer)) {
195+
startResponseWrite = true;
196+
197+
// find first : index
198+
const firstIndex =
199+
answer.indexOf('0:') !== -1 ? answer.indexOf('0:') : answer.indexOf('0:');
200+
answer = answer.substring(firstIndex + 2).trim();
201+
202+
events?.onStreaming?.({ responseContent: answer });
203+
}
204+
}
205+
} else {
206+
events?.onStreaming?.({ responseContent });
207+
}
208+
}
209+
if (responseChoice?.tool_calls?.length) {
210+
let callingTool: { name: string; arguments: string } | null = null;
211+
responseChoice.tool_calls.forEach((toolCall, i) => {
212+
const index = toolCall.index ?? i;
213+
214+
// Call new tool
215+
const hasNewTool = toolCall?.function?.name || callingTool;
216+
if (hasNewTool) {
217+
// 有 function name,代表新 call 工具
218+
if (toolCall?.function?.name) {
219+
callingTool = {
220+
name: toolCall.function?.name || '',
221+
arguments: toolCall.function?.arguments || ''
222+
};
223+
} else if (callingTool) {
224+
// Continue call(Perhaps the name of the previous function was incomplete)
225+
callingTool.name += toolCall.function?.name || '';
226+
callingTool.arguments += toolCall.function?.arguments || '';
227+
}
228+
229+
if (!callingTool) {
230+
return;
231+
}
232+
233+
const toolId = getNanoid();
234+
235+
events?.onToolCalling?.({ callingTool, toolId });
236+
237+
calls[index] = {
238+
...toolCall,
239+
id: toolId,
240+
type: 'function',
241+
function: callingTool
242+
};
243+
callingTool = null;
244+
} else {
245+
/* 追加到当前工具的参数里 */
246+
const arg: string = toolCall?.function?.arguments ?? '';
247+
const currentTool = calls[index];
248+
if (currentTool && arg) {
249+
currentTool.function.arguments += arg;
250+
events?.onToolParaming?.({ currentTool, params: arg });
251+
}
252+
}
253+
});
254+
}
255+
}
256+
257+
const { reasoningContent, content, finish_reason, usage } = getResponseData();
258+
259+
return {
260+
answerText: content,
261+
reasoningText: reasoningContent,
262+
toolCalls: calls.filter(Boolean),
263+
finish_reason,
264+
usage
265+
};
266+
};
267+
268+
export const createCompleteResponse = async (
269+
args: CreateCompleteResopnseProps
270+
): Promise<CompleteResopnse> => {
271+
const { params, ...events } = args;
272+
273+
const { completion, reasoning, retainDatasetCite = true } = params;
274+
275+
const finish_reason = completion.choices?.[0]?.finish_reason as CompletionFinishReason;
276+
const calls = completion.choices?.[0]?.message?.tool_calls || [];
277+
const usage = completion.usage;
278+
279+
const { content, reasoningContent } = (() => {
280+
const content = completion.choices?.[0]?.message?.content || '';
281+
// @ts-ignore
282+
const reasoningContent: string = completion.choices?.[0]?.message?.reasoning_content || '';
283+
284+
// API already parse reasoning content
285+
if (reasoningContent || !reasoning) {
286+
return {
287+
content,
288+
reasoningContent
289+
};
290+
}
291+
292+
const [think, answer] = parseReasoningContent(content);
293+
return {
294+
content: answer,
295+
reasoningContent: think
296+
};
297+
})();
298+
299+
const formatReasonContent = removeDatasetCiteText(reasoningContent, retainDatasetCite);
300+
const formatContent = removeDatasetCiteText(content, retainDatasetCite);
301+
302+
if (reasoning && reasoningContent) {
303+
events?.onReasoned?.({ reasoningContent: formatReasonContent });
304+
}
305+
if (calls.length !== 0) {
306+
events?.onToolCalled?.({ calls });
307+
}
308+
if (content) {
309+
events?.onCompleted?.({ responseContent: formatContent });
310+
}
311+
312+
return {
313+
reasoningText: formatReasonContent,
314+
answerText: formatContent,
315+
toolCalls: calls,
316+
finish_reason,
317+
usage
318+
};
319+
};

packages/global/core/ai/type.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export type ChatCompletionMessageToolCall = ChatCompletionMessageToolCall & {
6464
toolName?: string;
6565
toolAvatar?: string;
6666
};
67+
export type ChatCompletionToolType = ChatCompletionTool & {
68+
index?: number;
69+
toolName?: string;
70+
toolAvatar?: string;
71+
};
6772
export type ChatCompletionMessageFunctionCall =
6873
SdkChatCompletionAssistantMessageParam.FunctionCall & {
6974
id?: string;

0 commit comments

Comments
 (0)