Skip to content

Commit

Permalink
[Meru AB] Use LLM to generate instructions suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
philipperolet committed Feb 27, 2024
1 parent 65606b1 commit df0e738
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 41 deletions.
149 changes: 108 additions & 41 deletions front/components/assistant_builder/InstructionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,29 @@ import {
Spinner,
} from "@dust-tt/sparkle";
import type {
APIError,
BuilderSuggestionsType,
ModelConfig,
PlanType,
Result,
SUPPORTED_MODEL_CONFIGS,
SupportedModel,
} from "@dust-tt/types";
import type { WorkspaceType } from "@dust-tt/types";
import {
CLAUDE_DEFAULT_MODEL_CONFIG,
CLAUDE_INSTANT_DEFAULT_MODEL_CONFIG,
Err,
GEMINI_PRO_DEFAULT_MODEL_CONFIG,
GPT_3_5_TURBO_MODEL_CONFIG,
GPT_4_TURBO_MODEL_CONFIG,
MISTRAL_MEDIUM_MODEL_CONFIG,
MISTRAL_NEXT_MODEL_CONFIG,
MISTRAL_SMALL_MODEL_CONFIG,
Ok,
} from "@dust-tt/types";
import type { ComponentType } from "react";
import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";

import type { AssistantBuilderState } from "@app/components/assistant_builder/types";
import { getSupportedModelConfig } from "@app/lib/assistant";
Expand Down Expand Up @@ -117,7 +122,10 @@ export function InstructionScreen({
name="assistantInstructions"
/>
{isDevelopmentOrDustWorkspace(owner) && (
<Suggestions instructions={builderState.instructions} />
<Suggestions
owner={owner}
instructions={builderState.instructions || ""}
/>
)}
</div>
);
Expand Down Expand Up @@ -287,36 +295,64 @@ function AssistantBuilderTextArea({
);
}

const STATIC_SUGGESTIONS = [
"I want you to act as the king of the bongo.",
"I want you to act as the king of the bongo, Bong.",
"I want you to act as the king of the cats, Soupinou.",
];
const STATIC_SUGGESTIONS = {
status: "ok" as const,
suggestions: [
"I want you to act as the king of the bongo.",
"I want you to act as the king of the bongo, Bong.",
"I want you to act as the king of the cats, Soupinou.",
],
};

const SUGGESTION_DEBOUNCE_DELAY = 1500;

function Suggestions({ instructions }: { instructions: string | null }) {
const [suggestions, setSuggestions] = useState<string[]>(STATIC_SUGGESTIONS);
function Suggestions({
owner,
instructions,
}: {
owner: WorkspaceType;
instructions: string;
}) {
const [suggestions, setSuggestions] =
useState<BuilderSuggestionsType>(STATIC_SUGGESTIONS);

const [loading, setLoading] = useState(false);
const [error, setError] = useState<APIError | null>(null);

const debounceHandle = useRef<NodeJS.Timeout | undefined>(undefined);

const updateSuggestions = useCallback(async () => {
setLoading(true);
const suggestions = await getInstructionsSuggestions(owner, instructions);
if (suggestions.isErr()) {
setError(suggestions.error);
setLoading(false);
return;
}

setSuggestions(suggestions.value);
setError(null);
setLoading(false);
}, [owner, instructions]);

useEffect(() => {
if (debounceHandle.current) {
clearTimeout(debounceHandle.current);
debounceHandle.current = undefined;
}
if (!instructions) {
setError(null);
setLoading(false);
setSuggestions(STATIC_SUGGESTIONS);
}
if (instructions) {
// Debounced request to generate suggestions
debounceHandle.current = setTimeout(async () => {
setLoading(true);
const suggestions = await getInstructionsSuggestions(instructions);
setSuggestions(suggestions);
setLoading(false);
}, SUGGESTION_DEBOUNCE_DELAY);
debounceHandle.current = setTimeout(
updateSuggestions,
SUGGESTION_DEBOUNCE_DELAY
);
}
}, [instructions]);
}, [instructions, updateSuggestions]);

return (
<Collapsible defaultOpen>
Expand All @@ -328,19 +364,44 @@ function Suggestions({ instructions }: { instructions: string | null }) {
</Collapsible.Button>
<Collapsible.Panel>
<div className="flex gap-2">
{loading && <Spinner size="sm" />}
{!loading &&
suggestions.map((suggestion, index) => (
<ContentMessage
size="sm"
title="First suggestion"
variant="pink"
key={`suggestion-${index}`}
>
{suggestion}
</ContentMessage>
))}
{!loading && suggestions.length === 0 && "Looking good! 🎉"}
{(() => {
if (loading) {
return <Spinner size="sm" />;
}
if (error) {
return (
<ContentMessage size="sm" title="Error" variant="red">
{error.message}
</ContentMessage>
);
}
if (suggestions.status === "ok") {
if (suggestions.suggestions.length === 0) {
return (
<ContentMessage size="sm" variant="slate" title="">
Looking good! 🎉
</ContentMessage>
);
}
return suggestions.suggestions.map((suggestion, index) => (
<ContentMessage
size="sm"
title=""
variant="pink"
key={`suggestion-${index}`}
>
{suggestion}
</ContentMessage>
));
}
if (suggestions.status === "unavailable") {
return (
<ContentMessage size="sm" variant="slate" title="">
Suggestions will appear when you're done writing.
</ContentMessage>
);
}
})()}
</div>
</Collapsible.Panel>
</div>
Expand All @@ -349,18 +410,24 @@ function Suggestions({ instructions }: { instructions: string | null }) {
}

async function getInstructionsSuggestions(
owner: WorkspaceType,
instructions: string
): Promise<string[]> {
return new Promise((resolve) => {
setTimeout(() => {
if (instructions.endsWith("testgood")) {
resolve([]);
} else {
resolve([
"A first suggestion related to " + instructions.substring(0, 20),
"A second suggestion at time " + new Date().toLocaleTimeString(),
]);
}
}, 1000);
): Promise<Result<BuilderSuggestionsType, APIError>> {
const res = await fetch(`/api/w/${owner.sId}/assistant/builder/suggestions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "instructions",
inputs: { current_instructions: instructions },
}),
});
if (!res.ok) {
return new Err({
type: "internal_server_error",
message: "Failed to get suggestions",
});
}
return new Ok(await res.json());
}
114 changes: 114 additions & 0 deletions front/pages/api/w/[wId]/assistant/builder/suggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type {
BuilderSuggestionsType,
WithAPIErrorReponse,
} from "@dust-tt/types";
import {
BuilderSuggestionsResponseBodySchema,
DustProdActionRegistry,
InternalPostBuilderSuggestionsRequestBodySchema,
} from "@dust-tt/types";
import { isLeft } from "fp-ts/lib/Either";
import * as reporter from "io-ts-reporters";
import type { NextApiRequest, NextApiResponse } from "next";

import { runAction } from "@app/lib/actions/server";
import { Authenticator, getSession } from "@app/lib/auth";
import { apiError, withLogging } from "@app/logger/withlogging";

async function handler(
req: NextApiRequest,
res: NextApiResponse<WithAPIErrorReponse<BuilderSuggestionsType>>
): Promise<void> {
const session = await getSession(req, res);
const auth = await Authenticator.fromSession(
session,
req.query.wId as string
);
const owner = auth.workspace();
if (!owner) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "workspace_not_found",
message: "The workspace you're trying to modify was not found.",
},
});
}

switch (req.method) {
case "POST":
if (!auth.isUser()) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "app_auth_error",
message: "Only users of the workspace can use suggestions.",
},
});
}

const bodyValidation =
InternalPostBuilderSuggestionsRequestBodySchema.decode(req.body);
if (isLeft(bodyValidation)) {
const pathError = reporter.formatValidationErrors(bodyValidation.left);
return apiError(req, res, {
status_code: 400,
api_error: {
type: "invalid_request_error",
message: `Invalid request body: ${pathError}`,
},
});
}
const suggestionsType = bodyValidation.right.type;
const suggestionsInputs = bodyValidation.right.inputs;
const suggestionsResponse = await runAction(
auth,
`assistant-builder-${suggestionsType}-suggestions`,
DustProdActionRegistry[
`assistant-builder-${suggestionsType}-suggestions`
].config,
[suggestionsInputs]
);

if (suggestionsResponse.isErr() || !suggestionsResponse.value.results) {
const message = suggestionsResponse.isErr()
? JSON.stringify(suggestionsResponse.error)
: "No results available";
return apiError(req, res, {
status_code: 500,
api_error: {
type: "internal_server_error",
message,
},
});
}
const responseValidation = BuilderSuggestionsResponseBodySchema.decode(
suggestionsResponse.value.results[0][0].value
);
if (isLeft(responseValidation)) {
const pathError = reporter.formatValidationErrors(
responseValidation.left
);
return apiError(req, res, {
status_code: 500,
api_error: {
type: "internal_server_error",
message: `Invalid response from action: ${pathError}`,
},
});
}
const suggestions = responseValidation.right;
return res.status(200).json(suggestions);

default:
return apiError(req, res, {
status_code: 405,
api_error: {
type: "method_not_supported_error",
message: "The method passed is not supported, POST is expected.",
},
});
}
}

export default withLogging(handler);
33 changes: 33 additions & 0 deletions types/src/front/api_handlers/internal/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,36 @@ export const InternalPostConversationsRequestBodySchema = t.type({
t.undefined,
]),
});

export const InternalPostBuilderSuggestionsRequestBodySchema = t.union([
t.type({
type: t.literal("name"),
inputs: t.type({ instructions: t.string, description: t.string }),
}),
t.type({
type: t.literal("instructions"),
inputs: t.type({ current_instructions: t.string }),
}),
]);

export type BuilderSuggestionsRequestType = t.TypeOf<
typeof InternalPostBuilderSuggestionsRequestBodySchema
>;

export const BuilderSuggestionsResponseBodySchema = t.union([
t.type({
status: t.literal("ok"),
suggestions: t.array(t.string),
}),
t.type({
status: t.literal("unavailable"),
reason: t.union([
t.literal("user_not_finished"), // The user has not finished inputing data for suggestions to make sense
t.literal("irrelevant"),
]),
}),
]);

export type BuilderSuggestionsType = t.TypeOf<
typeof BuilderSuggestionsResponseBodySchema
>;
Loading

0 comments on commit df0e738

Please sign in to comment.