Skip to content

Commit 6cc4b87

Browse files
author
Yann Leflour
committed
assistant lib v2
1 parent b8cca3c commit 6cc4b87

File tree

4 files changed

+225
-4
lines changed

4 files changed

+225
-4
lines changed

pnpm-lock.yaml

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui-sketcher-webview/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"canvas-size": "^1.2.6",
1515
"daisyui": "^4.4.19",
1616
"firacode": "^6.2.0",
17+
"iter-tools": "^7.5.3",
1718
"mermaid": "^10.6.1",
19+
"openai": "^4.16.1",
1820
"react": "^18.2.0",
1921
"react-dom": "^18.2.0",
2022
"valtio": "^1.12.1"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import OpenAI from "openai";
2+
import { zip, map, toArray } from "iter-tools";
3+
import { AssistantCreateParams } from "openai/resources/beta/assistants/assistants.mjs";
4+
5+
type PromiseValue<T> = T extends Promise<infer U> ? U : never;
6+
7+
const defaultErrorFormater = (error: any) => {
8+
if (typeof error === "string") return { success: false, error };
9+
if (error instanceof Error) return { success: false, error: error.message };
10+
11+
return { success: false as const, error: JSON.stringify(error) };
12+
};
13+
14+
type ErrorFormaterFn = typeof defaultErrorFormater;
15+
16+
const defaultSystemPrompt = `You're a senior developer at GAFA. Your objective is to assist the user into planning and executing on development tasks.
17+
When modifying files, read the file content with line numbers and use a git patch to apply the changes. Don't wait for confirmation before executing commands`;
18+
19+
export type ToolFn = (...args: any[]) => Promise<any>;
20+
21+
export type AssistantConfig = {
22+
openAIClient: OpenAI;
23+
systemPrompt?: string;
24+
toolsSchema?: AssistantCreateParams.AssistantToolsFunction[];
25+
toolsFns?: {
26+
[key: string]: ToolFn;
27+
};
28+
errorFormater?: ErrorFormaterFn;
29+
};
30+
31+
export const createAssistant = async (config: AssistantConfig) => {
32+
const openAIClient = config.openAIClient;
33+
const systemPrompt = config.systemPrompt ?? defaultSystemPrompt;
34+
const toolsSchema = config.toolsSchema ?? [];
35+
const toolsFns = config.toolsFns ?? {};
36+
const errorFormater = config.errorFormater ?? defaultErrorFormater;
37+
38+
const openAIAssistant = await openAIClient.beta.assistants.create({
39+
name: "pAIprog",
40+
model: "gpt-4-1106-preview",
41+
instructions: systemPrompt,
42+
tools: toolsSchema,
43+
});
44+
45+
const executeFunctions = async (run: OpenAI.Beta.Threads.Runs.Run) => {
46+
if (!run.required_action) {
47+
throw new Error("Empty tool function to execute");
48+
}
49+
50+
if (run.required_action.type !== "submit_tool_outputs") {
51+
throw new Error("Unsupported tool function, check your schema");
52+
}
53+
54+
const toolCalls = run.required_action.submit_tool_outputs.tool_calls;
55+
56+
const outputPromises = toolCalls.map((toolCall) => {
57+
if (toolCall.type !== "function") {
58+
return { success: false, error: "Unsupported tool call type" };
59+
}
60+
61+
const functionName = toolCall.function.name as keyof typeof toolsSchema;
62+
63+
const functionArguments = JSON.parse(toolCall.function.arguments);
64+
const fn = toolsFns[functionName as string];
65+
66+
if (!fn)
67+
return {
68+
success: false,
69+
error: `Unsupported tool function ${functionName as string}`,
70+
};
71+
72+
const output = fn(functionArguments).catch(errorFormater);
73+
74+
return output;
75+
});
76+
77+
const allOutputsPromise = Promise.all(outputPromises);
78+
79+
const outputs = await allOutputsPromise;
80+
81+
const toolsOutput = map(
82+
([toolCall, output]) => {
83+
return {
84+
tool_call_id: toolCall.id,
85+
output: JSON.stringify(output ?? { success: true }),
86+
};
87+
},
88+
zip(toolCalls, outputs),
89+
);
90+
91+
const isSuccess = outputs.every(
92+
(output) => !(output instanceof Object) || output.success,
93+
);
94+
95+
return [toArray(toolsOutput), isSuccess] as const;
96+
}
97+
98+
return {
99+
openAIAssistant,
100+
executeFunctions
101+
};
102+
};
103+
104+
export type Assistant = PromiseValue<ReturnType<typeof createAssistant>>;
105+
106+
export type ThreadConfig = {
107+
openAIClient: OpenAI;
108+
assistant: Assistant;
109+
}
110+
111+
export async function createThread(config: ThreadConfig) {
112+
const { assistant: defaultAssistant, openAIClient } = config;
113+
const openAIThread = await openAIClient.beta.threads.create({});
114+
115+
async function* sendMessage(text: string, assistant?: Assistant) {
116+
let run: OpenAI.Beta.Threads.Runs.Run;
117+
let isInterrupted = false;
118+
119+
const cancelRun = async () => {
120+
isInterrupted = true;
121+
if (!run) return;
122+
await openAIClient.beta.threads.runs.cancel(openAIThread.id, run.id).catch(() => {
123+
// Ignore error when trying to cancel a cancelled or completed run
124+
});
125+
};
126+
127+
await openAIClient.beta.threads.messages.create(openAIThread.id, {
128+
role: "user",
129+
content: text,
130+
});
131+
132+
const runAssistant = assistant ?? defaultAssistant;
133+
134+
run = await openAIClient.beta.threads.runs.create(openAIThread.id, {
135+
assistant_id: runAssistant.openAIAssistant.id,
136+
});
137+
138+
while (true) {
139+
if (
140+
run.status === "queued" ||
141+
run.status === "in_progress" ||
142+
run.status === "cancelling"
143+
) {
144+
await new Promise((resolve) => setTimeout(resolve, 1000));
145+
yield { type: run.status };
146+
run = await openAIClient.beta.threads.runs.retrieve(openAIThread.id, run.id);
147+
continue;
148+
}
149+
150+
if (run.status === "cancelled") {
151+
yield { type: "cancelled" as const };
152+
return;
153+
}
154+
155+
if (run.status === "failed") {
156+
yield {
157+
type: "failed" as const,
158+
error: run.last_error?.message ?? "Unknown Error",
159+
};
160+
return;
161+
}
162+
163+
if (run.status === "expired") {
164+
yield { type: "expired" as const };
165+
return;
166+
}
167+
168+
if (run.status === "requires_action") {
169+
const toolsNames =
170+
run.required_action?.submit_tool_outputs.tool_calls.map(
171+
(tool) => tool.function.name,
172+
) ?? [];
173+
yield { type: "executing_actions" as const, tools: toolsNames };
174+
try {
175+
const [tool_outputs, isSuccess] = await runAssistant.executeFunctions(run);
176+
const newRun = await openAIClient.beta.threads.runs.submitToolOutputs(
177+
openAIThread.id,
178+
run.id,
179+
{ tool_outputs },
180+
);
181+
if (!isSuccess) yield { type: "executing_actions_failure" as const };
182+
run = newRun;
183+
} catch (error) {
184+
// Recover from error when trying to send tool outputs on a cancelled run
185+
if (isInterrupted) continue;
186+
throw error;
187+
}
188+
}
189+
190+
if (run.status === "completed") {
191+
const messages = await openAIClient.beta.threads.messages.list(openAIThread.id);
192+
const lastMessage = messages.data[0].content[0];
193+
194+
if (lastMessage.type === "text") {
195+
// TODO: add annotations
196+
yield { type: "completed" as const, message: lastMessage.text.value };
197+
return;
198+
}
199+
}
200+
}
201+
}
202+
203+
return {
204+
openAIThread,
205+
sendMessage,
206+
};
207+
}
208+
209+
export type Thread = PromiseValue<ReturnType<typeof createThread>>;

ui-sketcher-webview/tsconfig.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
/* Linting */
1818
"strict": true,
19-
"noUnusedLocals": true,
20-
"noUnusedParameters": true,
2119
"noFallthroughCasesInSwitch": true
2220
},
2321
"include": ["src"],

0 commit comments

Comments
 (0)