Skip to content

Commit a0c20a5

Browse files
authored
Merge eb11830 into 8c2bf88
2 parents 8c2bf88 + eb11830 commit a0c20a5

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed

tests/helpers/llm.workflow.test.ts

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
2+
import type { Message } from "telegraf/types";
3+
import type { ConfigChatType, GptContextType } from "../../src/types";
4+
import OpenAI from "openai";
5+
6+
const mockExecuteTools = jest.fn();
7+
const mockAddToHistory = jest.fn();
8+
const mockForgetHistory = jest.fn();
9+
const mockSendTelegramMessage = jest.fn();
10+
const mockBuildMessages = jest.fn();
11+
const mockUseApi = jest.fn();
12+
const mockUseLangfuse = jest.fn();
13+
const mockObserveOpenAI = jest.fn();
14+
const mockUseConfig = jest.fn();
15+
const mockUseThreads = jest.fn();
16+
const mockResolveChatTools = jest.fn();
17+
const mockGetToolsPrompts = jest.fn();
18+
const mockGetSystemMessage = jest.fn();
19+
const mockReplaceUrl = jest.fn((s: string) => Promise.resolve(s));
20+
const mockReplaceTool = jest.fn((s: string) => Promise.resolve(s));
21+
const mockLog = jest.fn();
22+
23+
jest.unstable_mockModule("../../src/helpers/gpt/tools.ts", () => ({
24+
executeTools: (...args: unknown[]) => mockExecuteTools(...args),
25+
resolveChatTools: (...args: unknown[]) => mockResolveChatTools(...args),
26+
getToolsPrompts: (...args: unknown[]) => mockGetToolsPrompts(...args),
27+
getToolsSystemMessages: jest.fn(),
28+
}));
29+
30+
jest.unstable_mockModule("../../src/helpers/history.ts", () => ({
31+
addToHistory: (...args: unknown[]) => mockAddToHistory(...args),
32+
forgetHistory: (...args: unknown[]) => mockForgetHistory(...args),
33+
}));
34+
35+
jest.unstable_mockModule("../../src/telegram/send.ts", () => ({
36+
sendTelegramMessage: (...args: unknown[]) => mockSendTelegramMessage(...args),
37+
getTelegramForwardedUser: jest.fn(),
38+
getFullName: jest.fn(),
39+
isAdminUser: jest.fn(),
40+
}));
41+
42+
jest.unstable_mockModule("../../src/helpers/gpt/messages.ts", () => ({
43+
buildMessages: (...args: unknown[]) => mockBuildMessages(...args),
44+
getSystemMessage: (...args: unknown[]) => mockGetSystemMessage(...args),
45+
}));
46+
47+
jest.unstable_mockModule("../../src/helpers/placeholders.ts", () => ({
48+
replaceUrlPlaceholders: (...args: unknown[]) => mockReplaceUrl(...args),
49+
replaceToolPlaceholders: (...args: unknown[]) => mockReplaceTool(...args),
50+
}));
51+
52+
jest.unstable_mockModule("../../src/helpers/useApi.ts", () => ({
53+
useApi: (...args: unknown[]) => mockUseApi(...args),
54+
}));
55+
56+
jest.unstable_mockModule("../../src/helpers/useLangfuse.ts", () => ({
57+
default: (...args: unknown[]) => mockUseLangfuse(...args),
58+
}));
59+
60+
jest.unstable_mockModule("langfuse", () => ({
61+
observeOpenAI: (...args: unknown[]) => mockObserveOpenAI(...args),
62+
}));
63+
64+
jest.unstable_mockModule("../../src/config.ts", () => ({
65+
useConfig: () => mockUseConfig(),
66+
}));
67+
68+
jest.unstable_mockModule("../../src/threads.ts", () => ({
69+
useThreads: () => mockUseThreads(),
70+
}));
71+
72+
jest.unstable_mockModule("../../src/helpers.ts", () => ({
73+
log: (...args: unknown[]) => mockLog(...args),
74+
}));
75+
76+
let llm: typeof import("../../src/helpers/gpt/llm.ts");
77+
let handleModelAnswer: typeof import("../../src/helpers/gpt/llm.ts").handleModelAnswer;
78+
let processToolResults: typeof import("../../src/helpers/gpt/llm.ts").processToolResults;
79+
let requestGptAnswer: typeof import("../../src/helpers/gpt/llm.ts").requestGptAnswer;
80+
81+
const baseMsg: Message.TextMessage = {
82+
chat: { id: 1, type: "private" },
83+
message_id: 1,
84+
text: "hi",
85+
} as Message.TextMessage;
86+
87+
const chatConfig: ConfigChatType = {
88+
name: "chat",
89+
completionParams: {},
90+
chatParams: {},
91+
toolParams: {},
92+
local_model: "loc",
93+
} as ConfigChatType;
94+
95+
const baseContext: GptContextType = {
96+
thread: { id: 1, messages: [], msgs: [], completionParams: {} },
97+
messages: [],
98+
systemMessage: "",
99+
chatTools: [],
100+
prompts: [],
101+
tools: [],
102+
} as GptContextType;
103+
104+
let mockCreate: jest.Mock;
105+
106+
beforeEach(async () => {
107+
jest.resetModules();
108+
mockExecuteTools.mockReset();
109+
mockAddToHistory.mockReset();
110+
mockForgetHistory.mockReset();
111+
mockSendTelegramMessage.mockReset();
112+
mockBuildMessages.mockReset();
113+
mockUseApi.mockReset();
114+
mockUseLangfuse.mockReset();
115+
mockObserveOpenAI.mockReset();
116+
mockUseConfig.mockReset();
117+
mockUseThreads.mockReset();
118+
mockResolveChatTools.mockReset();
119+
mockGetToolsPrompts.mockReset();
120+
mockGetSystemMessage.mockReset();
121+
mockReplaceUrl.mockReset();
122+
mockReplaceTool.mockReset();
123+
mockLog.mockReset();
124+
125+
mockUseThreads.mockReturnValue({});
126+
mockResolveChatTools.mockResolvedValue([]);
127+
mockGetToolsPrompts.mockResolvedValue([]);
128+
mockGetSystemMessage.mockResolvedValue("");
129+
mockBuildMessages.mockResolvedValue([]);
130+
mockUseLangfuse.mockReturnValue({ trace: undefined });
131+
mockObserveOpenAI.mockImplementation((api) => api);
132+
mockUseConfig.mockReturnValue({ local_models: [], chats: [], auth: {} });
133+
mockReplaceUrl.mockImplementation((s: string) => Promise.resolve(s));
134+
mockReplaceTool.mockImplementation((s: string) => Promise.resolve(s));
135+
136+
mockCreate = jest.fn();
137+
mockUseApi.mockReturnValue({ chat: { completions: { create: mockCreate } } });
138+
139+
llm = await import("../../src/helpers/gpt/llm.ts");
140+
handleModelAnswer = llm.handleModelAnswer;
141+
processToolResults = llm.processToolResults;
142+
requestGptAnswer = llm.requestGptAnswer;
143+
});
144+
145+
describe("handleModelAnswer with tool result", () => {
146+
it("returns processed result when tools respond", async () => {
147+
const toolCall = {
148+
id: "1",
149+
type: "function",
150+
function: { name: "tool", arguments: "{}" },
151+
};
152+
const res: OpenAI.ChatCompletion = {
153+
choices: [{ message: { role: "assistant", tool_calls: [toolCall] } }],
154+
} as OpenAI.ChatCompletion;
155+
mockExecuteTools.mockResolvedValue([{ content: "ok" }]);
156+
mockCreate.mockResolvedValueOnce({
157+
choices: [{ message: { content: "final" } }],
158+
});
159+
160+
const result = await handleModelAnswer({
161+
msg: { ...baseMsg },
162+
res,
163+
chatConfig,
164+
expressRes: undefined,
165+
gptContext: { ...baseContext },
166+
});
167+
168+
expect(result.content).toBe("final");
169+
expect(mockExecuteTools).toHaveBeenCalled();
170+
expect(mockCreate).toHaveBeenCalled();
171+
});
172+
});
173+
174+
describe("processToolResults non forget", () => {
175+
it("calls llm and returns answer", async () => {
176+
const tool_res = [{ content: "done" }];
177+
const messageAgent: OpenAI.ChatCompletionMessage = {
178+
role: "assistant",
179+
content: "",
180+
tool_calls: [
181+
{
182+
id: "1",
183+
type: "function",
184+
function: { name: "tool", arguments: "{}" },
185+
},
186+
],
187+
} as OpenAI.ChatCompletionMessage;
188+
mockCreate.mockResolvedValueOnce({
189+
choices: [{ message: { content: "answer" } }],
190+
});
191+
192+
const res = await processToolResults({
193+
tool_res,
194+
messageAgent,
195+
chatConfig,
196+
msg: { ...baseMsg },
197+
expressRes: undefined,
198+
noSendTelegram: false,
199+
gptContext: { ...baseContext },
200+
level: 1,
201+
});
202+
203+
expect(res.content).toBe("answer");
204+
expect(mockSendTelegramMessage).toHaveBeenCalled();
205+
expect(mockCreate).toHaveBeenCalled();
206+
});
207+
});
208+
209+
describe("runEvaluatorWorkflow via requestGptAnswer", () => {
210+
const evaluatorChat = {
211+
name: "url",
212+
agent_name: "url",
213+
id: 2,
214+
completionParams: { model: "loc" },
215+
systemMessage: "Check for URL in answer. 1 - no url, 5 - url present",
216+
} as ConfigChatType;
217+
218+
it("improves answer when score low", async () => {
219+
mockUseConfig.mockReturnValue({
220+
local_models: [{ name: "loc", model: "loc" }],
221+
chats: [evaluatorChat],
222+
auth: {},
223+
});
224+
const responses = [
225+
{ choices: [{ message: { content: "initial" } }] },
226+
{
227+
choices: [
228+
{
229+
message: {
230+
content: JSON.stringify({
231+
score: 2,
232+
justification: "bad",
233+
is_complete: false,
234+
}),
235+
},
236+
},
237+
],
238+
},
239+
{ choices: [{ message: { content: "fixed" } }] },
240+
{
241+
choices: [
242+
{
243+
message: {
244+
content: JSON.stringify({
245+
score: 5,
246+
justification: "good",
247+
is_complete: true,
248+
}),
249+
},
250+
},
251+
],
252+
},
253+
];
254+
mockCreate.mockImplementation(() => Promise.resolve(responses.shift()));
255+
256+
const configWithEval = {
257+
...chatConfig,
258+
evaluators: [{ agent_name: "url" }],
259+
} as ConfigChatType;
260+
const res = await requestGptAnswer({ ...baseMsg }, configWithEval);
261+
expect(res?.content).toBe("fixed");
262+
expect(mockCreate).toHaveBeenCalledTimes(4);
263+
});
264+
265+
it("keeps answer when score high", async () => {
266+
mockUseConfig.mockReturnValue({
267+
local_models: [{ name: "loc", model: "loc" }],
268+
chats: [evaluatorChat],
269+
auth: {},
270+
});
271+
const responses = [
272+
{ choices: [{ message: { content: "initial" } }] },
273+
{
274+
choices: [
275+
{
276+
message: {
277+
content: JSON.stringify({
278+
score: 5,
279+
justification: "good",
280+
is_complete: true,
281+
}),
282+
},
283+
},
284+
],
285+
},
286+
];
287+
mockCreate.mockImplementation(() => Promise.resolve(responses.shift()));
288+
289+
const configWithEval = {
290+
...chatConfig,
291+
evaluators: [{ agent_name: "url" }],
292+
} as ConfigChatType;
293+
const res = await requestGptAnswer({ ...baseMsg }, configWithEval);
294+
expect(res?.content).toBe("initial");
295+
expect(mockCreate).toHaveBeenCalledTimes(2);
296+
});
297+
});

0 commit comments

Comments
 (0)