Skip to content

Commit

Permalink
Merge staging
Browse files Browse the repository at this point in the history
  • Loading branch information
bracesproul committed Oct 22, 2024
2 parents 1bc9a95 + 047c1df commit 5ffd001
Show file tree
Hide file tree
Showing 41 changed files with 2,057 additions and 533 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/lang-sql": "^6.8.0",
"@langchain/anthropic": "^0.3.3",
"@langchain/core": "^0.3.9",
"@langchain/langgraph": "^0.2.10",
"@langchain/langgraph-sdk": "^0.0.17",
"@langchain/openai": "^0.3.5",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-progress": "^1.1.0",
Expand All @@ -52,6 +55,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"framer-motion": "^11.11.9",
"js-cookie": "^3.0.5",
"langsmith": "^0.1.61",
"lucide-react": "^0.441.0",
Expand Down
4 changes: 4 additions & 0 deletions src/agent/open-canvas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { updateArtifact } from "./nodes/updateArtifact";
import { respondToQuery } from "./nodes/respondToQuery";
import { rewriteCodeArtifactTheme } from "./nodes/rewriteCodeArtifactTheme";
import { reflectNode } from "./nodes/reflect";
import { customAction } from "./nodes/customAction";

const defaultInputs: Omit<
typeof OpenCanvasGraphAnnotation.State,
Expand All @@ -25,6 +26,7 @@ const defaultInputs: Omit<
fixBugs: undefined,
portLanguage: undefined,
lastNodeName: undefined,
customQuickActionId: undefined,
};

const routeNode = (state: typeof OpenCanvasGraphAnnotation.State) => {
Expand Down Expand Up @@ -54,6 +56,7 @@ const builder = new StateGraph(OpenCanvasGraphAnnotation)
.addNode("rewriteCodeArtifactTheme", rewriteCodeArtifactTheme)
.addNode("updateArtifact", updateArtifact)
.addNode("generateArtifact", generateArtifact)
.addNode("customAction", customAction)
.addNode("generateFollowup", generateFollowup)
.addNode("cleanState", cleanState)
.addNode("reflect", reflectNode)
Expand All @@ -65,6 +68,7 @@ const builder = new StateGraph(OpenCanvasGraphAnnotation)
"respondToQuery",
"generateArtifact",
"rewriteArtifact",
"customAction",
])
// Edges
.addEdge("generateArtifact", "generateFollowup")
Expand Down
121 changes: 121 additions & 0 deletions src/agent/open-canvas/nodes/customAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { ChatOpenAI } from "@langchain/openai";
import { OpenCanvasGraphAnnotation, OpenCanvasGraphReturnType } from "../state";
import {
CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT,
CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX,
CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT,
REFLECTIONS_QUICK_ACTION_PROMPT,
} from "../prompts";
import { ensureStoreInConfig, formatReflections } from "../../utils";
import {
ArtifactContent,
CustomQuickAction,
Reflections,
} from "../../../types";
import { LangGraphRunnableConfig } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";

const formatMessages = (messages: BaseMessage[]): string =>
messages
.map(
(msg) =>
`<${msg.getType()}>\n${msg.content as string}\n</${msg.getType()}>`
)
.join("\n");

export const customAction = async (
state: typeof OpenCanvasGraphAnnotation.State,
config: LangGraphRunnableConfig
): Promise<OpenCanvasGraphReturnType> => {
if (!state.customQuickActionId) {
throw new Error("No custom quick action ID found.");
}

const smallModel = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0.5,
});

const store = ensureStoreInConfig(config);
const assistantId = config.configurable?.assistant_id;
if (!assistantId) {
throw new Error("`assistant_id` not found in configurable");
}
const customActionsNamespace = ["custom_actions", assistantId];
const actionsKey = "actions";

const memoryNamespace = ["memories", assistantId];
const memoryKey = "reflection";

const [customActionsItem, memories] = await Promise.all([
store.get(customActionsNamespace, actionsKey),
store.get(memoryNamespace, memoryKey),
]);
if (!customActionsItem?.value) {
throw new Error("No custom actions found.");
}
const customQuickAction = customActionsItem.value[
state.customQuickActionId
] as CustomQuickAction | undefined;
if (!customQuickAction) {
throw new Error(
`No custom quick action found from ID ${state.customQuickActionId}`
);
}

let currentArtifactContent: ArtifactContent | undefined;
if (state.artifact) {
currentArtifactContent = state.artifact.contents.find(
(art) => art.index === state.artifact.currentContentIndex
);
}
if (!currentArtifactContent) {
throw new Error("No artifact content found.");
}

let formattedPrompt = `<custom-instructions>\n${customQuickAction.prompt}\n</custom-instructions>`;
if (customQuickAction.includeReflections && memories?.value) {
const memoriesAsString = formatReflections(memories.value as Reflections);
const reflectionsPrompt = REFLECTIONS_QUICK_ACTION_PROMPT.replace(
"{reflections}",
memoriesAsString
);
formattedPrompt += `\n\n${reflectionsPrompt}`;
}

if (customQuickAction.includePrefix) {
formattedPrompt = `${CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX}\n\n${formattedPrompt}`;
}

if (customQuickAction.includeRecentHistory) {
const formattedConversationHistory =
CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT.replace(
"{conversation}",
formatMessages(state.messages.slice(-5))
);
formattedPrompt += `\n\n${formattedConversationHistory}`;
}

formattedPrompt += `\n\n${CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT.replace("{artifactContent}", currentArtifactContent.content)}`;

const newArtifactValues = await smallModel.invoke([
{ role: "user", content: formattedPrompt },
]);

const newArtifact = {
...state.artifact,
currentContentIndex: state.artifact.contents.length + 1,
contents: [
...state.artifact.contents,
{
...currentArtifactContent,
index: state.artifact.contents.length + 1,
content: newArtifactValues.content as string,
},
],
};

return {
artifact: newArtifact,
};
};
2 changes: 1 addition & 1 deletion src/agent/open-canvas/nodes/generateArtifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const generateArtifact = async (
"The language of the artifact to generate. " +
" If generating code, it should be the programming language. " +
"For programming languages, ensure it's one of the following" +
"'javascript' | 'typescript' | 'cpp' | 'java' | 'php' | 'python' | 'html' | 'other'"
"'javascript' | 'typescript' | 'cpp' | 'java' | 'php' | 'python' | 'html' | 'sql' | 'other'"
),
artifact: z
.string()
Expand Down
6 changes: 6 additions & 0 deletions src/agent/open-canvas/nodes/generatePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const generatePath = async (
};
}

if (state.customQuickActionId) {
return {
next: "customAction",
};
}

let currentArtifactContent: ArtifactContent | undefined;
if (state.artifact) {
currentArtifactContent = state.artifact.contents.find(
Expand Down
1 change: 1 addition & 0 deletions src/agent/open-canvas/nodes/rewriteArtifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const rewriteArtifact = async (
"php",
"python",
"html",
"sql",
"other",
])
.optional()
Expand Down
3 changes: 3 additions & 0 deletions src/agent/open-canvas/nodes/rewriteCodeArtifactTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const rewriteCodeArtifactTheme = async (
case "html":
newLanguage = "HTML";
break;
case "sql":
newLanguage = "SQL";
break;
}
formattedPrompt = PORT_LANGUAGE_CODE_ARTIFACT_PROMPT.replace(
"{newLanguage}",
Expand Down
26 changes: 26 additions & 0 deletions src/agent/open-canvas/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,29 @@ Rules and guidelines:
- Ensure you do not port over language specific modules. E.g if the code contains imports from Node's fs module, you must use the closest equivalent in {newLanguage}.
${DEFAULT_CODE_PROMPT_RULES}
</rules-guidelines>`;

export const REFLECTIONS_QUICK_ACTION_PROMPT = `The following are reflections on the user's style guidelines and general memories/facts about the user.
Use these reflections as context when generating your response.
<reflections>
{reflections}
</reflections>`;

export const CUSTOM_QUICK_ACTION_ARTIFACT_PROMPT_PREFIX = `You are an AI assistant tasked with rewriting a users generated artifact.
They have provided custom instructions on how you should manage rewriting the artifact. The custom instructions are wrapped inside the <custom-instructions> tags.
Use this context about the application the user is interacting with when generating your response:
<app-context>
The name of the application is "Open Canvas". Open Canvas is a web application where users have a chat window and a canvas to display an artifact.
Artifacts can be any sort of writing content, emails, code, or other creative writing work. Think of artifacts as content, or writing you might find on you might find on a blog, Google doc, or other writing platform.
Users only have a single artifact per conversation, however they have the ability to go back and fourth between artifact edits/revisions.
</app-context>`;

export const CUSTOM_QUICK_ACTION_CONVERSATION_CONTEXT = `Here is the last 5 (or less) messages in the chat history between you and the user:
<conversation>
{conversation}
</conversation>`;

export const CUSTOM_QUICK_ACTION_ARTIFACT_CONTENT_PROMPT = `Here is the full artifact content the user has generated, and is requesting you rewrite according to their custom instructions:
<artifact>
{artifactContent}
</artifact>`;
4 changes: 4 additions & 0 deletions src/agent/open-canvas/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export const OpenCanvasGraphAnnotation = Annotation.Root({
* The name of the last node that was executed.
*/
lastNodeName: Annotation<string | undefined>,
/**
* The ID of the custom quick action to use.
*/
customQuickActionId: Annotation<string | undefined>,
});

export type OpenCanvasGraphReturnType = Partial<
Expand Down
60 changes: 60 additions & 0 deletions src/app/api/store/delete/id/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from "next/server";
import { Client } from "@langchain/langgraph-sdk";
import { LANGGRAPH_API_URL } from "@/constants";
import { User } from "@supabase/supabase-js";
import { verifyUserAuthenticated } from "../../../../../lib/supabase/verify_user_server";

export async function POST(req: NextRequest) {
let user: User | undefined;
try {
user = await verifyUserAuthenticated();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
} catch (e) {
console.error("Failed to fetch user", e);
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { namespace, key, id } = await req.json();

const lgClient = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
apiUrl: LANGGRAPH_API_URL,
});

try {
const currentItems = await lgClient.store.getItem(namespace, key);
if (!currentItems?.value) {
return new NextResponse(
JSON.stringify({
error: "Item not found",
success: false,
}),
{
status: 404,
headers: { "Content-Type": "application/json" },
}
);
}

const newValues = Object.fromEntries(
Object.entries(currentItems.value).filter(([k]) => k !== id)
);

await lgClient.store.putItem(namespace, key, newValues);

return new NextResponse(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (_) {
return new NextResponse(
JSON.stringify({ error: "Failed to share run after multiple attempts." }),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}
19 changes: 2 additions & 17 deletions src/app/api/store/delete/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,15 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { assistantId } = await req.json();

if (!assistantId) {
return new NextResponse(
JSON.stringify({
error: "`assistantId` is required to fetch stored data.",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
const { namespace, key } = await req.json();

const lgClient = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
apiUrl: LANGGRAPH_API_URL,
});

const memoryNamespace = ["memories", assistantId];
const memoryKey = "reflection";

try {
await lgClient.store.deleteItem(memoryNamespace, memoryKey);
await lgClient.store.deleteItem(namespace, key);

return new NextResponse(JSON.stringify({ success: true }), {
status: 200,
Expand Down
19 changes: 2 additions & 17 deletions src/app/api/store/get/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,15 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { assistantId } = await req.json();

if (!assistantId) {
return new NextResponse(
JSON.stringify({
error: "`assistantId` is required to fetch stored data.",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
const { namespace, key } = await req.json();

const lgClient = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
apiUrl: LANGGRAPH_API_URL,
});

const memoryNamespace = ["memories", assistantId];
const memoryKey = "reflection";

try {
const item = await lgClient.store.getItem(memoryNamespace, memoryKey);
const item = await lgClient.store.getItem(namespace, key);

return new NextResponse(JSON.stringify({ item }), {
status: 200,
Expand Down
Loading

0 comments on commit 5ffd001

Please sign in to comment.