From 38710f3cb007b2c632bc0885573c68568e5ce020 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Thu, 6 Mar 2025 20:04:55 -0800 Subject: [PATCH 01/11] feat: Open Code agent --- agent/open-code/index.ts | 18 ++++++++++++++++++ agent/open-code/nodes/executor.ts | 5 +++++ agent/open-code/nodes/interrupt.ts | 5 +++++ agent/open-code/nodes/planner.ts | 25 +++++++++++++++++++++++++ agent/open-code/types.ts | 11 +++++++++++ 5 files changed, 64 insertions(+) create mode 100644 agent/open-code/index.ts create mode 100644 agent/open-code/nodes/executor.ts create mode 100644 agent/open-code/nodes/interrupt.ts create mode 100644 agent/open-code/nodes/planner.ts create mode 100644 agent/open-code/types.ts diff --git a/agent/open-code/index.ts b/agent/open-code/index.ts new file mode 100644 index 0000000..2ba2b72 --- /dev/null +++ b/agent/open-code/index.ts @@ -0,0 +1,18 @@ +import { END, START, StateGraph } from "@langchain/langgraph"; +import { OpenCodeAnnotation, OpenCodeState } from "./types"; +import { planner } from "./nodes/planner"; +import { interrupt } from "./nodes/interrupt"; +import { executor } from "./nodes/executor"; + +function handleRoutingFromExecutor(state: OpenCodeState): "executor" | "interrupt" {} + +function handleRoutingFromInterrupt(state: OpenCodeState): "executor" | typeof END {} + +const workflow = new StateGraph(OpenCodeAnnotation) + .addNode("planner", planner) + .addNode("executor", executor) + .addNode("interrupt", interrupt) + .addEdge(START, "planner") + .addEdge("planner", "executor") + .addConditionalEdges("executor", handleRoutingFromExecutor, ["executor", "interrupt"]) + .addConditionalEdges("interrupt", handleRoutingFromInterrupt, ["executor", END]) \ No newline at end of file diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts new file mode 100644 index 0000000..e7e7023 --- /dev/null +++ b/agent/open-code/nodes/executor.ts @@ -0,0 +1,5 @@ +import { OpenCodeState, OpenCodeUpdate } from "../types"; + +export async function executor(state: OpenCodeState): Promise { + throw new Error("Not implemented" + state); +} \ No newline at end of file diff --git a/agent/open-code/nodes/interrupt.ts b/agent/open-code/nodes/interrupt.ts new file mode 100644 index 0000000..8d73de4 --- /dev/null +++ b/agent/open-code/nodes/interrupt.ts @@ -0,0 +1,5 @@ +import { OpenCodeState, OpenCodeUpdate } from "../types"; + +export async function interrupt(state: OpenCodeState): Promise { + throw new Error("Not implemented" + state); +} \ No newline at end of file diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts new file mode 100644 index 0000000..b978741 --- /dev/null +++ b/agent/open-code/nodes/planner.ts @@ -0,0 +1,25 @@ +import { v4 as uuidv4 } from "uuid"; +import { AIMessage } from "@langchain/langgraph-sdk"; +import { OpenCodeState, OpenCodeUpdate } from "../types"; + +export async function planner(state: OpenCodeState): Promise { + const aiMessage: AIMessage = { + type: "ai", + id: uuidv4(), + content: "", + tool_calls: [ + { + name: "update_file", + args: { + args: { + new_file_content: "ADD_CODE_HERE" + }, + }, + id: uuidv4(), + type: "tool_call", + } + ] + } + + const toolMessage = {} +} \ No newline at end of file diff --git a/agent/open-code/types.ts b/agent/open-code/types.ts new file mode 100644 index 0000000..4b87557 --- /dev/null +++ b/agent/open-code/types.ts @@ -0,0 +1,11 @@ +import { Annotation } from "@langchain/langgraph"; +import { GenerativeUIAnnotation } from "../types"; + +export const OpenCodeAnnotation = Annotation.Root({ + messages: GenerativeUIAnnotation.spec.messages, + ui: GenerativeUIAnnotation.spec.ui, + timestamp: GenerativeUIAnnotation.spec.timestamp, +}); + +export type OpenCodeState = typeof OpenCodeAnnotation.State; +export type OpenCodeUpdate = typeof OpenCodeAnnotation.Update; \ No newline at end of file From 066b2191074eb555b10727933361520b30b6ba9c Mon Sep 17 00:00:00 2001 From: bracesproul Date: Thu, 6 Mar 2025 20:12:47 -0800 Subject: [PATCH 02/11] cr --- agent/open-code/index.ts | 5 ++++- agent/open-code/types.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/agent/open-code/index.ts b/agent/open-code/index.ts index 2ba2b72..4247e1e 100644 --- a/agent/open-code/index.ts +++ b/agent/open-code/index.ts @@ -4,7 +4,10 @@ import { planner } from "./nodes/planner"; import { interrupt } from "./nodes/interrupt"; import { executor } from "./nodes/executor"; -function handleRoutingFromExecutor(state: OpenCodeState): "executor" | "interrupt" {} +function handleRoutingFromExecutor(state: OpenCodeState): "executor" | "interrupt" { + const lastAIMessage = state.messages.findLast((m) => m.getType() === "ai"); + if (lastAIMessage) +} function handleRoutingFromInterrupt(state: OpenCodeState): "executor" | typeof END {} diff --git a/agent/open-code/types.ts b/agent/open-code/types.ts index 4b87557..a9594e3 100644 --- a/agent/open-code/types.ts +++ b/agent/open-code/types.ts @@ -5,6 +5,7 @@ export const OpenCodeAnnotation = Annotation.Root({ messages: GenerativeUIAnnotation.spec.messages, ui: GenerativeUIAnnotation.spec.ui, timestamp: GenerativeUIAnnotation.spec.timestamp, + next: Annotation<"executor" | "interrupt"> }); export type OpenCodeState = typeof OpenCodeAnnotation.State; From 7ebcbb3a2873b4dd61788a7a37a048ada2f306cd Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 7 Mar 2025 10:47:08 -0800 Subject: [PATCH 03/11] implement components --- agent/agent.tsx | 11 +- agent/open-code/index.ts | 17 +-- agent/open-code/nodes/executor.ts | 102 ++++++++++++++- agent/open-code/nodes/interrupt.ts | 5 - agent/open-code/nodes/plan-code/step-1.txt | 5 + agent/open-code/nodes/plan-code/step-2.txt | 21 +++ agent/open-code/nodes/plan-code/step-3.txt | 22 ++++ agent/open-code/nodes/plan-code/step-4.txt | 33 +++++ agent/open-code/nodes/plan-code/step-5.txt | 22 ++++ agent/open-code/nodes/plan-code/step-6.txt | 13 ++ agent/open-code/nodes/plan-code/step-7.txt | 0 agent/open-code/nodes/planner.ts | 57 ++++++-- agent/open-code/types.ts | 3 +- agent/types.ts | 4 +- agent/uis/index.tsx | 4 + agent/uis/open-code/plan/index.css | 1 + agent/uis/open-code/plan/index.tsx | 21 +++ agent/uis/open-code/proposed-change/index.css | 122 ++++++++++++++++++ agent/uis/open-code/proposed-change/index.tsx | 46 +++++++ 19 files changed, 472 insertions(+), 37 deletions(-) delete mode 100644 agent/open-code/nodes/interrupt.ts create mode 100644 agent/open-code/nodes/plan-code/step-1.txt create mode 100644 agent/open-code/nodes/plan-code/step-2.txt create mode 100644 agent/open-code/nodes/plan-code/step-3.txt create mode 100644 agent/open-code/nodes/plan-code/step-4.txt create mode 100644 agent/open-code/nodes/plan-code/step-5.txt create mode 100644 agent/open-code/nodes/plan-code/step-6.txt create mode 100644 agent/open-code/nodes/plan-code/step-7.txt create mode 100644 agent/uis/open-code/plan/index.css create mode 100644 agent/uis/open-code/plan/index.tsx create mode 100644 agent/uis/open-code/proposed-change/index.css create mode 100644 agent/uis/open-code/proposed-change/index.tsx diff --git a/agent/agent.tsx b/agent/agent.tsx index dfacb6f..30f3db9 100644 --- a/agent/agent.tsx +++ b/agent/agent.tsx @@ -6,9 +6,11 @@ import { stockbrokerGraph } from "./stockbroker"; import { ChatOpenAI } from "@langchain/openai"; import { tripPlannerGraph } from "./trip-planner"; import { formatMessages } from "./utils/format-messages"; +import { graph as openCodeGraph } from "./open-code"; const allToolDescriptions = `- stockbroker: can fetch the price of a ticker, purchase/sell a ticker, or get the user's portfolio -- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location.`; +- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location. +- openCode: can write code for the user. call this tool when the user asks you to write code`; async function router( state: GenerativeUIState, @@ -19,7 +21,7 @@ ${allToolDescriptions} `; const routerSchema = z.object({ route: z - .enum(["stockbroker", "tripPlanner", "generalInput"]) + .enum(["stockbroker", "tripPlanner", "openCode", "generalInput"]) .describe(routerDescription), }); const routerTool = { @@ -73,7 +75,7 @@ Please pick the proper route based on the most recent message, in the context of function handleRoute( state: GenerativeUIState, -): "stockbroker" | "tripPlanner" | "generalInput" { +): "stockbroker" | "tripPlanner" | "openCode" | "generalInput" { return state.next; } @@ -104,16 +106,19 @@ const builder = new StateGraph(GenerativeUIAnnotation) .addNode("router", router) .addNode("stockbroker", stockbrokerGraph) .addNode("tripPlanner", tripPlannerGraph) + .addNode("openCode", openCodeGraph) .addNode("generalInput", handleGeneralInput) .addConditionalEdges("router", handleRoute, [ "stockbroker", "tripPlanner", + "openCode", "generalInput", ]) .addEdge(START, "router") .addEdge("stockbroker", END) .addEdge("tripPlanner", END) + .addEdge("openCode", END) .addEdge("generalInput", END); export const graph = builder.compile(); diff --git a/agent/open-code/index.ts b/agent/open-code/index.ts index 4247e1e..91f0a9e 100644 --- a/agent/open-code/index.ts +++ b/agent/open-code/index.ts @@ -1,21 +1,14 @@ import { END, START, StateGraph } from "@langchain/langgraph"; -import { OpenCodeAnnotation, OpenCodeState } from "./types"; +import { OpenCodeAnnotation } from "./types"; import { planner } from "./nodes/planner"; -import { interrupt } from "./nodes/interrupt"; import { executor } from "./nodes/executor"; -function handleRoutingFromExecutor(state: OpenCodeState): "executor" | "interrupt" { - const lastAIMessage = state.messages.findLast((m) => m.getType() === "ai"); - if (lastAIMessage) -} - -function handleRoutingFromInterrupt(state: OpenCodeState): "executor" | typeof END {} - const workflow = new StateGraph(OpenCodeAnnotation) .addNode("planner", planner) .addNode("executor", executor) - .addNode("interrupt", interrupt) .addEdge(START, "planner") .addEdge("planner", "executor") - .addConditionalEdges("executor", handleRoutingFromExecutor, ["executor", "interrupt"]) - .addConditionalEdges("interrupt", handleRoutingFromInterrupt, ["executor", END]) \ No newline at end of file + .addEdge("executor", END); + +export const graph = workflow.compile(); +graph.name = "Open Code Graph"; diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts index e7e7023..a9e50c9 100644 --- a/agent/open-code/nodes/executor.ts +++ b/agent/open-code/nodes/executor.ts @@ -1,5 +1,101 @@ +import fs from "fs/promises"; +import { v4 as uuidv4 } from "uuid"; +import { AIMessage } from "@langchain/langgraph-sdk"; import { OpenCodeState, OpenCodeUpdate } from "../types"; +import { LangGraphRunnableConfig } from "@langchain/langgraph"; +import ComponentMap from "../../uis"; +import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; +import { PLAN } from "./planner"; -export async function executor(state: OpenCodeState): Promise { - throw new Error("Not implemented" + state); -} \ No newline at end of file +export async function executor( + state: OpenCodeState, + config: LangGraphRunnableConfig, +): Promise { + const ui = typedUi(config); + + const numOfUpdateFileCalls = state.messages.filter( + (m) => + m.getType() === "ai" && + (m as unknown as AIMessage).tool_calls?.some( + (tc) => tc.name === "update_file", + ), + ).length; + const planItem = PLAN[numOfUpdateFileCalls - 1]; + + let updateFileContents = ""; + switch (numOfUpdateFileCalls) { + case 0: + updateFileContents = await fs.readFile( + "agent/open-code/nodes/plan-code/step-1.txt", + "utf-8", + ); + break; + case 1: + updateFileContents = await fs.readFile( + "agent/open-code/nodes/plan-code/step-2.txt", + "utf-8", + ); + break; + case 2: + updateFileContents = await fs.readFile( + "agent/open-code/nodes/plan-code/step-3.txt", + "utf-8", + ); + break; + case 3: + updateFileContents = await fs.readFile( + "agent/open-code/nodes/plan-code/step-4.txt", + "utf-8", + ); + break; + case 4: + updateFileContents = await fs.readFile( + "agent/open-code/nodes/plan-code/step-5.txt", + "utf-8", + ); + break; + case 5: + updateFileContents = await fs.readFile( + "agent/open-code/nodes/plan-code/step-6.txt", + "utf-8", + ); + break; + default: + updateFileContents = ""; + } + + if (!updateFileContents) { + throw new Error("No file updates found!"); + } + + const toolCallId = uuidv4(); + const aiMessage: AIMessage = { + type: "ai", + id: uuidv4(), + content: "", + tool_calls: [ + { + name: "update_file", + args: { + args: { + new_file_content: updateFileContents, + }, + }, + id: toolCallId, + type: "tool_call", + }, + ], + }; + + ui.write("proposed-change", { + toolCallId, + change: updateFileContents, + planItem, + }); + + return { + messages: [aiMessage], + ui: ui.collect as OpenCodeUpdate["ui"], + timestamp: Date.now(), + }; +} diff --git a/agent/open-code/nodes/interrupt.ts b/agent/open-code/nodes/interrupt.ts deleted file mode 100644 index 8d73de4..0000000 --- a/agent/open-code/nodes/interrupt.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { OpenCodeState, OpenCodeUpdate } from "../types"; - -export async function interrupt(state: OpenCodeState): Promise { - throw new Error("Not implemented" + state); -} \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-1.txt b/agent/open-code/nodes/plan-code/step-1.txt new file mode 100644 index 0000000..e4776f4 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-1.txt @@ -0,0 +1,5 @@ +```bash +npx create-react-app todo-app --template typescript +cd todo-app +mkdir -p src/{components,styles,utils} +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-2.txt b/agent/open-code/nodes/plan-code/step-2.txt new file mode 100644 index 0000000..93c51c9 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-2.txt @@ -0,0 +1,21 @@ +```tsx +// src/components/TodoItem.tsx +import React from 'react'; +import styles from '../styles/TodoItem.module.css'; + +interface TodoItemProps { + id: string; + text: string; + completed: boolean; + onToggle: (id: string) => void; + onDelete: (id: string) => void; +} + +export const TodoItem: React.FC = ({ id, text, completed, onToggle, onDelete }) => ( +
+ onToggle(id)} /> + {text} + +
+); +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-3.txt b/agent/open-code/nodes/plan-code/step-3.txt new file mode 100644 index 0000000..3e6d746 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-3.txt @@ -0,0 +1,22 @@ +```tsx +// src/context/TodoContext.tsx +import React, { createContext, useContext, useReducer } from 'react'; + +type Todo = { id: string; text: string; completed: boolean; }; + +type TodoState = { todos: Todo[]; }; +type TodoAction = + | { type: 'ADD_TODO'; payload: string } + | { type: 'TOGGLE_TODO'; payload: string } + | { type: 'DELETE_TODO'; payload: string }; + +const TodoContext = createContext<{ + state: TodoState; + dispatch: React.Dispatch; +} | undefined>(undefined); + +export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [state, dispatch] = useReducer(todoReducer, { todos: [] }); + return {children}; +}; +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-4.txt b/agent/open-code/nodes/plan-code/step-4.txt new file mode 100644 index 0000000..b3c2a37 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-4.txt @@ -0,0 +1,33 @@ +```tsx +// src/components/AddTodo.tsx +import React, { useState } from 'react'; +import styles from '../styles/AddTodo.module.css'; + +export const AddTodo: React.FC<{ onAdd: (text: string) => void }> = ({ onAdd }) => { + const [text, setText] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!text.trim()) { + setError('Todo text cannot be empty'); + return; + } + onAdd(text.trim()); + setText(''); + setError(''); + }; + + return ( +
+ setText(e.target.value)} + placeholder='Add a new todo' + /> + {error &&
{error}
} + +
+ ); +}; +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-5.txt b/agent/open-code/nodes/plan-code/step-5.txt new file mode 100644 index 0000000..2082d9d --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-5.txt @@ -0,0 +1,22 @@ +```tsx +// src/components/TodoFilters.tsx +import React from 'react'; + +type FilterType = 'all' | 'active' | 'completed'; + +export const TodoFilters: React.FC<{ + currentFilter: FilterType; + onFilterChange: (filter: FilterType) => void; + onSortChange: (ascending: boolean) => void; +}> = ({ currentFilter, onFilterChange, onSortChange }) => ( +
+ + + +
+); +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-6.txt b/agent/open-code/nodes/plan-code/step-6.txt new file mode 100644 index 0000000..84d96fa --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-6.txt @@ -0,0 +1,13 @@ +```tsx +// src/utils/storage.ts +const STORAGE_KEY = 'todos'; + +export const saveTodos = (todos: Todo[]) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); +}; + +export const loadTodos = (): Todo[] => { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : []; +}; +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-7.txt b/agent/open-code/nodes/plan-code/step-7.txt new file mode 100644 index 0000000..e69de29 diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts index b978741..29a2361 100644 --- a/agent/open-code/nodes/planner.ts +++ b/agent/open-code/nodes/planner.ts @@ -1,25 +1,60 @@ import { v4 as uuidv4 } from "uuid"; -import { AIMessage } from "@langchain/langgraph-sdk"; +import { AIMessage, ToolMessage } from "@langchain/langgraph-sdk"; import { OpenCodeState, OpenCodeUpdate } from "../types"; +import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; +import { LangGraphRunnableConfig } from "@langchain/langgraph"; +import ComponentMap from "../../uis"; +import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; -export async function planner(state: OpenCodeState): Promise { +export const PLAN = [ + "Set up project scaffolding using Create React App and implement basic folder structure for components, styles, and utilities.", + "Create reusable UI components for TodoItem, including styling with CSS modules.", + "Implement state management using React Context to handle todo items, including actions for adding, updating, and deleting todos.", + "Add form functionality for creating new todos with input validation and error handling.", + "Create filtering and sorting capabilities to allow users to view completed, active, or all todos.", + "Implement local storage integration to persist todo items between page refreshes.", +]; + +export async function planner( + _state: OpenCodeState, + config: LangGraphRunnableConfig, +): Promise { + const ui = typedUi(config); + + const toolCallId = uuidv4(); const aiMessage: AIMessage = { type: "ai", id: uuidv4(), - content: "", + content: "I've come up with a detailed plan for building the todo app.", tool_calls: [ { - name: "update_file", + name: "plan", args: { args: { - new_file_content: "ADD_CODE_HERE" + plan: PLAN, }, }, - id: uuidv4(), + id: toolCallId, type: "tool_call", - } - ] - } + }, + ], + }; - const toolMessage = {} -} \ No newline at end of file + ui.write("code-plan", { + toolCallId, + plan: PLAN, + }); + + const toolMessage: ToolMessage = { + type: "tool", + id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, + tool_call_id: toolCallId, + content: "User has approved the plan.", + }; + + return { + messages: [aiMessage, toolMessage], + ui: ui.collect as OpenCodeUpdate["ui"], + timestamp: Date.now(), + }; +} diff --git a/agent/open-code/types.ts b/agent/open-code/types.ts index a9594e3..3a32290 100644 --- a/agent/open-code/types.ts +++ b/agent/open-code/types.ts @@ -5,8 +5,7 @@ export const OpenCodeAnnotation = Annotation.Root({ messages: GenerativeUIAnnotation.spec.messages, ui: GenerativeUIAnnotation.spec.ui, timestamp: GenerativeUIAnnotation.spec.timestamp, - next: Annotation<"executor" | "interrupt"> }); export type OpenCodeState = typeof OpenCodeAnnotation.State; -export type OpenCodeUpdate = typeof OpenCodeAnnotation.Update; \ No newline at end of file +export type OpenCodeUpdate = typeof OpenCodeAnnotation.Update; diff --git a/agent/types.ts b/agent/types.ts index 20f7017..74c0f47 100644 --- a/agent/types.ts +++ b/agent/types.ts @@ -12,7 +12,9 @@ export const GenerativeUIAnnotation = Annotation.Root({ UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[] >({ default: () => [], reducer: uiMessageReducer }), timestamp: Annotation, - next: Annotation<"stockbroker" | "tripPlanner" | "generalInput">(), + next: Annotation< + "stockbroker" | "tripPlanner" | "openCode" | "generalInput" + >(), }); export type GenerativeUIState = typeof GenerativeUIAnnotation.State; diff --git a/agent/uis/index.tsx b/agent/uis/index.tsx index 89d151c..f932995 100644 --- a/agent/uis/index.tsx +++ b/agent/uis/index.tsx @@ -5,6 +5,8 @@ import BookAccommodation from "./trip-planner/book-accommodation"; import RestaurantsList from "./trip-planner/restaurants-list"; import BookRestaurant from "./trip-planner/book-restaurant"; import BuyStock from "./stockbroker/buy-stock"; +import Plan from "./open-code/plan"; +import ProposedChange from "./open-code/proposed-change"; const ComponentMap = { "stock-price": StockPrice, @@ -14,5 +16,7 @@ const ComponentMap = { "restaurants-list": RestaurantsList, "book-restaurant": BookRestaurant, "buy-stock": BuyStock, + "code-plan": Plan, + "proposed-change": ProposedChange, } as const; export default ComponentMap; diff --git a/agent/uis/open-code/plan/index.css b/agent/uis/open-code/plan/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/agent/uis/open-code/plan/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx new file mode 100644 index 0000000..c1f2bba --- /dev/null +++ b/agent/uis/open-code/plan/index.tsx @@ -0,0 +1,21 @@ +import "./index.css"; + +interface PlanProps { + toolCallId: string; + plan: string[]; +} + +export default function Plan(props: PlanProps) { + return ( +
+

Plan

+
+ {props.plan.map((step, index) => ( +

+ {index + 1}. {step} +

+ ))} +
+
+ ); +} diff --git a/agent/uis/open-code/proposed-change/index.css b/agent/uis/open-code/proposed-change/index.css new file mode 100644 index 0000000..e69504a --- /dev/null +++ b/agent/uis/open-code/proposed-change/index.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx new file mode 100644 index 0000000..d47df48 --- /dev/null +++ b/agent/uis/open-code/proposed-change/index.tsx @@ -0,0 +1,46 @@ +import { Button } from "@/components/ui/button"; +import "./index.css"; +import ReactMarkdown from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; + +interface ProposedChangeProps { + toolCallId: string; + change: string; + planItem: string; +} + +export default function ProposedChange(props: ProposedChangeProps) { + const handleReject = () => {}; + const handleAccept = () => {}; + + return ( +
+

Proposed Change

+ + ) : ( + {children} + ); + }, + }} + /> +
+ + +
+
+ ); +} From c338522a347ae87f548be01899f76b08265c6977 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 7 Mar 2025 11:21:49 -0800 Subject: [PATCH 04/11] cr --- agent/open-code/nodes/executor.ts | 16 ++-- agent/open-code/nodes/planner.ts | 24 +++++- agent/uis/open-code/plan/index.tsx | 34 ++++++-- agent/uis/open-code/proposed-change/index.tsx | 85 +++++++++++++++++-- 4 files changed, 132 insertions(+), 27 deletions(-) diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts index a9e50c9..6276e1e 100644 --- a/agent/open-code/nodes/executor.ts +++ b/agent/open-code/nodes/executor.ts @@ -13,17 +13,19 @@ export async function executor( ): Promise { const ui = typedUi(config); - const numOfUpdateFileCalls = state.messages.filter( + const lastPlanToolCall = state.messages.findLast( (m) => m.getType() === "ai" && - (m as unknown as AIMessage).tool_calls?.some( - (tc) => tc.name === "update_file", - ), - ).length; - const planItem = PLAN[numOfUpdateFileCalls - 1]; + (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), + ) as AIMessage | undefined; + const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args?.args; + const numOfExecutedPlanItems: number = + planToolCallArgs?.executedPlans?.length ?? 0; + + const planItem = PLAN[numOfExecutedPlanItems - 1]; let updateFileContents = ""; - switch (numOfUpdateFileCalls) { + switch (numOfExecutedPlanItems) { case 0: updateFileContents = await fs.readFile( "agent/open-code/nodes/plan-code/step-1.txt", diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts index 29a2361..2c5f94b 100644 --- a/agent/open-code/nodes/planner.ts +++ b/agent/open-code/nodes/planner.ts @@ -16,22 +16,37 @@ export const PLAN = [ ]; export async function planner( - _state: OpenCodeState, + state: OpenCodeState, config: LangGraphRunnableConfig, ): Promise { const ui = typedUi(config); + const lastPlanToolCall = state.messages.findLast( + (m) => + m.getType() === "ai" && + (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), + ) as AIMessage | undefined; + const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args?.args; + const executedPlans = planToolCallArgs?.executedPlans ?? []; + const remainingPlans = planToolCallArgs?.remainingPlans ?? PLAN; + + const content = + executedPlans.length > 0 + ? `I've updated the plan list based on the executed plans.` + : `I've come up with a detailed plan for building the todo app.`; + const toolCallId = uuidv4(); const aiMessage: AIMessage = { type: "ai", id: uuidv4(), - content: "I've come up with a detailed plan for building the todo app.", + content, tool_calls: [ { name: "plan", args: { args: { - plan: PLAN, + executedPlans, + remainingPlans, }, }, id: toolCallId, @@ -42,7 +57,8 @@ export async function planner( ui.write("code-plan", { toolCallId, - plan: PLAN, + executedPlans, + remainingPlans, }); const toolMessage: ToolMessage = { diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx index c1f2bba..46a6166 100644 --- a/agent/uis/open-code/plan/index.tsx +++ b/agent/uis/open-code/plan/index.tsx @@ -2,19 +2,35 @@ import "./index.css"; interface PlanProps { toolCallId: string; - plan: string[]; + executedPlans: string[]; + remainingPlans: string[]; } export default function Plan(props: PlanProps) { return ( -
-

Plan

-
- {props.plan.map((step, index) => ( -

- {index + 1}. {step} -

- ))} +
+

Code Plan

+
+
+

+ Executed Plans +

+ {props.executedPlans.map((step, index) => ( +

+ {index + 1}. {step} +

+ ))} +
+
+

+ Remaining Plans +

+ {props.remainingPlans.map((step, index) => ( +

+ {props.executedPlans.length + index + 1}. {step} +

+ ))} +
); diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx index d47df48..7a41980 100644 --- a/agent/uis/open-code/proposed-change/index.tsx +++ b/agent/uis/open-code/proposed-change/index.tsx @@ -1,8 +1,13 @@ -import { Button } from "@/components/ui/button"; import "./index.css"; +import { v4 as uuidv4 } from "uuid"; +import { Button } from "@/components/ui/button"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui"; +import { Message } from "@langchain/langgraph-sdk"; +import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; +import { useState } from "react"; interface ProposedChangeProps { toolCallId: string; @@ -11,12 +16,72 @@ interface ProposedChangeProps { } export default function ProposedChange(props: ProposedChangeProps) { - const handleReject = () => {}; - const handleAccept = () => {}; + const [isAccepted, setIsAccepted] = useState(false); + + const thread = useStreamContext< + { messages: Message[]; ui: UIMessage[] }, + { MetaType: { ui: UIMessage | undefined } } + >(); + + const handleReject = () => { + alert("Rejected. (just kidding, you can't reject me silly!)"); + }; + const handleAccept = () => { + const content = "User accepted the proposed change. Please continue."; + thread.submit({ + messages: [ + { + type: "tool", + tool_call_id: props.toolCallId, + id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, + name: "buy-stock", + content, + }, + { + type: "human", + content: `Accepted change.`, + }, + ], + }); + + setIsAccepted(true); + }; + + if (isAccepted) { + return ( +
+
+

Accepted Change

+

{props.planItem}

+
+ + ) : ( + {children} + ); + }, + }} + /> +
+ ); + } return ( -
-

Proposed Change

+
+
+

Proposed Change

+

{props.planItem}

+
- - +
); From 6204bb984d15500268909201480b54515ac2536e Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Fri, 7 Mar 2025 21:26:48 +0100 Subject: [PATCH 05/11] Proposed fix --- agent/open-code/nodes/executor.ts | 5 +++-- agent/open-code/nodes/planner.ts | 5 +++-- src/components/thread/messages/ai.tsx | 19 +++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts index 6276e1e..13f7eec 100644 --- a/agent/open-code/nodes/executor.ts +++ b/agent/open-code/nodes/executor.ts @@ -89,15 +89,16 @@ export async function executor( ], }; - ui.write("proposed-change", { + const msg = ui.create("proposed-change", { toolCallId, change: updateFileContents, planItem, }); + msg.additional_kwargs["message_id"] = aiMessage.id; return { messages: [aiMessage], - ui: ui.collect as OpenCodeUpdate["ui"], + ui: [msg], timestamp: Date.now(), }; } diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts index 2c5f94b..65e867c 100644 --- a/agent/open-code/nodes/planner.ts +++ b/agent/open-code/nodes/planner.ts @@ -55,11 +55,12 @@ export async function planner( ], }; - ui.write("code-plan", { + const msg = ui.create("code-plan", { toolCallId, executedPlans, remainingPlans, }); + msg.additional_kwargs["message_id"] = aiMessage.id; const toolMessage: ToolMessage = { type: "tool", @@ -70,7 +71,7 @@ export async function planner( return { messages: [aiMessage, toolMessage], - ui: ui.collect as OpenCodeUpdate["ui"], + ui: [msg], timestamp: Date.now(), }; } diff --git a/src/components/thread/messages/ai.tsx b/src/components/thread/messages/ai.tsx index 607d113..5784841 100644 --- a/src/components/thread/messages/ai.tsx +++ b/src/components/thread/messages/ai.tsx @@ -18,29 +18,28 @@ function CustomComponent({ const [apiUrl] = useQueryParam("apiUrl", StringParam); const meta = thread.getMessagesMetadata(message); const seenState = meta?.firstSeenState; - const customComponent = seenState?.values.ui + const customComponents = seenState?.values.ui ?.slice() - .reverse() - .find( + .filter( ({ additional_kwargs }) => - additional_kwargs.run_id === seenState.metadata?.run_id, + additional_kwargs.run_id === seenState.metadata?.run_id && + (!additional_kwargs.message_id || + additional_kwargs.message_id === message.id), ); - if (!customComponent) { - return null; - } - + if (!customComponents?.length) return null; return (
- {customComponent && ( + {customComponents.map((customComponent) => ( - )} + ))}
); } From 0607f042e71c295212924d2aba5be93308d5e28b Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 7 Mar 2025 12:43:22 -0800 Subject: [PATCH 06/11] improve components --- agent/open-code/nodes/executor.ts | 30 +++++++++----- agent/open-code/nodes/planner.ts | 39 +++++++++++++------ agent/uis/open-code/plan/index.tsx | 4 +- agent/uis/open-code/proposed-change/index.tsx | 19 +++++++-- src/components/thread/messages/human.tsx | 4 +- 5 files changed, 68 insertions(+), 28 deletions(-) diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts index 6276e1e..738c609 100644 --- a/agent/open-code/nodes/executor.ts +++ b/agent/open-code/nodes/executor.ts @@ -5,7 +5,6 @@ import { OpenCodeState, OpenCodeUpdate } from "../types"; import { LangGraphRunnableConfig } from "@langchain/langgraph"; import ComponentMap from "../../uis"; import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; -import { PLAN } from "./planner"; export async function executor( state: OpenCodeState, @@ -18,11 +17,25 @@ export async function executor( m.getType() === "ai" && (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), ) as AIMessage | undefined; - const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args?.args; - const numOfExecutedPlanItems: number = - planToolCallArgs?.executedPlans?.length ?? 0; + const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args as Record< + string, + any + >; + const nextPlanItem = planToolCallArgs?.remainingPlans?.[0] as + | string + | undefined; + const numOfExecutedPlanItems = planToolCallArgs?.executedPlans?.length ?? 0; - const planItem = PLAN[numOfExecutedPlanItems - 1]; + if (!nextPlanItem) { + // All plans have been executed + const successfullyFinishedMsg: AIMessage = { + type: "ai", + id: uuidv4(), + content: + "Successfully completed all the steps in the plan. Please let me know if you need anything else!", + }; + return { messages: [successfullyFinishedMsg] }; + } let updateFileContents = ""; switch (numOfExecutedPlanItems) { @@ -79,9 +92,8 @@ export async function executor( { name: "update_file", args: { - args: { - new_file_content: updateFileContents, - }, + new_file_content: updateFileContents as any, + executed_plan_item: nextPlanItem as any, }, id: toolCallId, type: "tool_call", @@ -92,7 +104,7 @@ export async function executor( ui.write("proposed-change", { toolCallId, change: updateFileContents, - planItem, + planItem: nextPlanItem, }); return { diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts index 2c5f94b..6454483 100644 --- a/agent/open-code/nodes/planner.ts +++ b/agent/open-code/nodes/planner.ts @@ -6,7 +6,7 @@ import { LangGraphRunnableConfig } from "@langchain/langgraph"; import ComponentMap from "../../uis"; import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; -export const PLAN = [ +const PLAN = [ "Set up project scaffolding using Create React App and implement basic folder structure for components, styles, and utilities.", "Create reusable UI components for TodoItem, including styling with CSS modules.", "Implement state management using React Context to handle todo items, including actions for adding, updating, and deleting todos.", @@ -21,19 +21,36 @@ export async function planner( ): Promise { const ui = typedUi(config); + const lastUpdateCodeToolCall = state.messages.findLast( + (m) => + m.getType() === "ai" && + (m as unknown as AIMessage).tool_calls?.some( + (tc) => tc.name === "update_file", + ), + ) as AIMessage | undefined; const lastPlanToolCall = state.messages.findLast( (m) => m.getType() === "ai" && (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), ) as AIMessage | undefined; - const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args?.args; - const executedPlans = planToolCallArgs?.executedPlans ?? []; - const remainingPlans = planToolCallArgs?.remainingPlans ?? PLAN; - const content = - executedPlans.length > 0 - ? `I've updated the plan list based on the executed plans.` - : `I've come up with a detailed plan for building the todo app.`; + const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args as Record< + string, + any + >; + const executedPlans: string[] = planToolCallArgs?.executedPlans ?? []; + let remainingPlans: string[] = planToolCallArgs?.remainingPlans ?? PLAN; + + const executedPlanItem = lastUpdateCodeToolCall?.tool_calls?.[0]?.args + ?.executed_plan_item as string | undefined; + if (executedPlanItem) { + executedPlans.push(executedPlanItem); + remainingPlans = remainingPlans.filter((p) => p !== executedPlanItem); + } + + const content = executedPlanItem + ? `I've updated the plan list based on the executed plans.` + : `I've come up with a detailed plan for building the todo app.`; const toolCallId = uuidv4(); const aiMessage: AIMessage = { @@ -44,10 +61,8 @@ export async function planner( { name: "plan", args: { - args: { - executedPlans, - remainingPlans, - }, + executedPlans, + remainingPlans, }, id: toolCallId, type: "tool_call", diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx index 46a6166..0714577 100644 --- a/agent/uis/open-code/plan/index.tsx +++ b/agent/uis/open-code/plan/index.tsx @@ -8,9 +8,9 @@ interface PlanProps { export default function Plan(props: PlanProps) { return ( -
+

Code Plan

-
+

Executed Plans diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx index 7a41980..f6fac0e 100644 --- a/agent/uis/open-code/proposed-change/index.tsx +++ b/agent/uis/open-code/proposed-change/index.tsx @@ -7,7 +7,8 @@ import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui"; import { Message } from "@langchain/langgraph-sdk"; import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { getToolResponse } from "../../utils/get-tool-response"; interface ProposedChangeProps { toolCallId: string; @@ -15,6 +16,9 @@ interface ProposedChangeProps { planItem: string; } +const ACCEPTED_CHANGE_CONTENT = + "User accepted the proposed change. Please continue."; + export default function ProposedChange(props: ProposedChangeProps) { const [isAccepted, setIsAccepted] = useState(false); @@ -27,7 +31,6 @@ export default function ProposedChange(props: ProposedChangeProps) { alert("Rejected. (just kidding, you can't reject me silly!)"); }; const handleAccept = () => { - const content = "User accepted the proposed change. Please continue."; thread.submit({ messages: [ { @@ -35,7 +38,7 @@ export default function ProposedChange(props: ProposedChangeProps) { tool_call_id: props.toolCallId, id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, name: "buy-stock", - content, + content: ACCEPTED_CHANGE_CONTENT, }, { type: "human", @@ -47,9 +50,17 @@ export default function ProposedChange(props: ProposedChangeProps) { setIsAccepted(true); }; + useEffect(() => { + if (typeof window === "undefined" || isAccepted) return; + const toolResponse = getToolResponse(props.toolCallId, thread); + if (toolResponse && toolResponse.content === ACCEPTED_CHANGE_CONTENT) { + setIsAccepted(true); + } + }, []); + if (isAccepted) { return ( -
+

Accepted Change

{props.planItem}

diff --git a/src/components/thread/messages/human.tsx b/src/components/thread/messages/human.tsx index 7c0b553..a394874 100644 --- a/src/components/thread/messages/human.tsx +++ b/src/components/thread/messages/human.tsx @@ -84,7 +84,9 @@ export function HumanMessage({ onSubmit={handleSubmitEdit} /> ) : ( -

{contentString}

+

+ {contentString} +

)}
Date: Fri, 7 Mar 2025 12:45:32 -0800 Subject: [PATCH 07/11] cr --- agent/open-code/nodes/plan-code/step-7.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 agent/open-code/nodes/plan-code/step-7.txt diff --git a/agent/open-code/nodes/plan-code/step-7.txt b/agent/open-code/nodes/plan-code/step-7.txt deleted file mode 100644 index e69de29..0000000 From 84cdbbe5507e2feebe8bed857828fecd1becae72 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 7 Mar 2025 13:42:37 -0800 Subject: [PATCH 08/11] feat: implement accept and dont ask again feature --- agent/open-code/index.ts | 39 ++++++- agent/open-code/nodes/executor.ts | 17 ++- agent/open-code/nodes/planner.ts | 32 +++++- agent/uis/open-code/plan/index.tsx | 41 ++++--- agent/uis/open-code/proposed-change/index.tsx | 107 ++++++++++++++---- src/components/thread/messages/tool-calls.tsx | 2 +- 6 files changed, 185 insertions(+), 53 deletions(-) diff --git a/agent/open-code/index.ts b/agent/open-code/index.ts index 91f0a9e..2707cff 100644 --- a/agent/open-code/index.ts +++ b/agent/open-code/index.ts @@ -1,14 +1,45 @@ -import { END, START, StateGraph } from "@langchain/langgraph"; -import { OpenCodeAnnotation } from "./types"; +import { + END, + LangGraphRunnableConfig, + START, + StateGraph, +} from "@langchain/langgraph"; +import { OpenCodeAnnotation, OpenCodeState } from "./types"; import { planner } from "./nodes/planner"; -import { executor } from "./nodes/executor"; +import { + executor, + SUCCESSFULLY_COMPLETED_STEPS_CONTENT, +} from "./nodes/executor"; +import { AIMessage } from "@langchain/langgraph-sdk"; + +function conditionallyEnd( + state: OpenCodeState, + config: LangGraphRunnableConfig, +): typeof END | "planner" { + const fullWriteAccess = !!config.configurable?.permissions?.full_write_access; + const lastAiMessage = state.messages.findLast( + (m) => m.getType() === "ai", + ) as unknown as AIMessage; + + // If the user did not grant full write access, or the last AI message is the success message, end + // otherwise, loop back to the start. + if ( + (typeof lastAiMessage.content === "string" && + lastAiMessage.content === SUCCESSFULLY_COMPLETED_STEPS_CONTENT) || + !fullWriteAccess + ) { + return END; + } + + return "planner"; +} const workflow = new StateGraph(OpenCodeAnnotation) .addNode("planner", planner) .addNode("executor", executor) .addEdge(START, "planner") .addEdge("planner", "executor") - .addEdge("executor", END); + .addConditionalEdges("executor", conditionallyEnd, ["planner", END]); export const graph = workflow.compile(); graph.name = "Open Code Graph"; diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts index a8e59e4..7452e54 100644 --- a/agent/open-code/nodes/executor.ts +++ b/agent/open-code/nodes/executor.ts @@ -6,6 +6,9 @@ import { LangGraphRunnableConfig } from "@langchain/langgraph"; import ComponentMap from "../../uis"; import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; +export const SUCCESSFULLY_COMPLETED_STEPS_CONTENT = + "Successfully completed all the steps in the plan. Please let me know if you need anything else!"; + export async function executor( state: OpenCodeState, config: LangGraphRunnableConfig, @@ -24,21 +27,24 @@ export async function executor( const nextPlanItem = planToolCallArgs?.remainingPlans?.[0] as | string | undefined; - const numOfExecutedPlanItems = planToolCallArgs?.executedPlans?.length ?? 0; + const numSeenPlans = + [ + ...(planToolCallArgs?.executedPlans ?? []), + ...(planToolCallArgs?.rejectedPlans ?? []), + ]?.length ?? 0; if (!nextPlanItem) { // All plans have been executed const successfullyFinishedMsg: AIMessage = { type: "ai", id: uuidv4(), - content: - "Successfully completed all the steps in the plan. Please let me know if you need anything else!", + content: SUCCESSFULLY_COMPLETED_STEPS_CONTENT, }; return { messages: [successfullyFinishedMsg] }; } let updateFileContents = ""; - switch (numOfExecutedPlanItems) { + switch (numSeenPlans) { case 0: updateFileContents = await fs.readFile( "agent/open-code/nodes/plan-code/step-1.txt", @@ -101,10 +107,13 @@ export async function executor( ], }; + const fullWriteAccess = !!config.configurable?.permissions?.full_write_access; + const msg = ui.create("proposed-change", { toolCallId, change: updateFileContents, planItem: nextPlanItem, + fullWriteAccess, }); msg.additional_kwargs["message_id"] = aiMessage.id; diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts index 5c9bf5d..5135750 100644 --- a/agent/open-code/nodes/planner.ts +++ b/agent/open-code/nodes/planner.ts @@ -28,28 +28,46 @@ export async function planner( (tc) => tc.name === "update_file", ), ) as AIMessage | undefined; + const lastUpdateToolCallResponse = state.messages.findLast( + (m) => + m.getType() === "tool" && + (m as unknown as ToolMessage).tool_call_id === + lastUpdateCodeToolCall?.tool_calls?.[0]?.id, + ) as ToolMessage | undefined; const lastPlanToolCall = state.messages.findLast( (m) => m.getType() === "ai" && (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), ) as AIMessage | undefined; + const wasPlanRejected = ( + lastUpdateToolCallResponse?.content as string | undefined + ) + ?.toLowerCase() + .includes("rejected"); + const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args as Record< string, any >; const executedPlans: string[] = planToolCallArgs?.executedPlans ?? []; + const rejectedPlans: string[] = planToolCallArgs?.rejectedPlans ?? []; let remainingPlans: string[] = planToolCallArgs?.remainingPlans ?? PLAN; - const executedPlanItem = lastUpdateCodeToolCall?.tool_calls?.[0]?.args + const proposedChangePlanItem = lastUpdateCodeToolCall?.tool_calls?.[0]?.args ?.executed_plan_item as string | undefined; - if (executedPlanItem) { - executedPlans.push(executedPlanItem); - remainingPlans = remainingPlans.filter((p) => p !== executedPlanItem); + if (proposedChangePlanItem) { + if (wasPlanRejected) { + rejectedPlans.push(proposedChangePlanItem); + } else { + executedPlans.push(proposedChangePlanItem); + } + + remainingPlans = remainingPlans.filter((p) => p !== proposedChangePlanItem); } - const content = executedPlanItem - ? `I've updated the plan list based on the executed plans.` + const content = proposedChangePlanItem + ? `I've updated the plan list based on the last proposed change.` : `I've come up with a detailed plan for building the todo app.`; const toolCallId = uuidv4(); @@ -62,6 +80,7 @@ export async function planner( name: "plan", args: { executedPlans, + rejectedPlans, remainingPlans, }, id: toolCallId, @@ -73,6 +92,7 @@ export async function planner( const msg = ui.create("code-plan", { toolCallId, executedPlans, + rejectedPlans, remainingPlans, }); msg.additional_kwargs["message_id"] = aiMessage.id; diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx index 0714577..d6bbce3 100644 --- a/agent/uis/open-code/plan/index.tsx +++ b/agent/uis/open-code/plan/index.tsx @@ -3,31 +3,42 @@ import "./index.css"; interface PlanProps { toolCallId: string; executedPlans: string[]; + rejectedPlans: string[]; remainingPlans: string[]; } export default function Plan(props: PlanProps) { return ( -
-

Code Plan

-
-
-

- Executed Plans -

- {props.executedPlans.map((step, index) => ( -

- {index + 1}. {step} -

- ))} -
-
+
+

Code Plan

+
+

Remaining Plans

{props.remainingPlans.map((step, index) => (

- {props.executedPlans.length + index + 1}. {step} + {index + 1}. {step} +

+ ))} +
+
+

+ Executed Plans +

+ {props.executedPlans.map((step, index) => ( +

+ {step} +

+ ))} +
+
+

+ Rejected Plans +

+ {props.rejectedPlans.map((step, index) => ( +

+ {step}

))}
diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx index f6fac0e..d89c35e 100644 --- a/agent/uis/open-code/proposed-change/index.tsx +++ b/agent/uis/open-code/proposed-change/index.tsx @@ -9,18 +9,28 @@ import { Message } from "@langchain/langgraph-sdk"; import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; import { useEffect, useState } from "react"; import { getToolResponse } from "../../utils/get-tool-response"; +import { cn } from "@/lib/utils"; interface ProposedChangeProps { toolCallId: string; change: string; planItem: string; + /** + * Whether or not to show the "Accept"/"Reject" buttons + * If true, this means the user selected the "Accept, don't ask again" + * button for this session. + */ + fullWriteAccess: boolean; } const ACCEPTED_CHANGE_CONTENT = "User accepted the proposed change. Please continue."; +const REJECTED_CHANGE_CONTENT = + "User rejected the proposed change. Please continue."; export default function ProposedChange(props: ProposedChangeProps) { const [isAccepted, setIsAccepted] = useState(false); + const [isRejected, setIsRejected] = useState(false); const thread = useStreamContext< { messages: Message[]; ui: UIMessage[] }, @@ -28,41 +38,81 @@ export default function ProposedChange(props: ProposedChangeProps) { >(); const handleReject = () => { - alert("Rejected. (just kidding, you can't reject me silly!)"); - }; - const handleAccept = () => { thread.submit({ messages: [ { type: "tool", tool_call_id: props.toolCallId, id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - name: "buy-stock", - content: ACCEPTED_CHANGE_CONTENT, + name: "update_file", + content: REJECTED_CHANGE_CONTENT, }, { type: "human", - content: `Accepted change.`, + content: `Rejected change.`, }, ], }); + setIsRejected(true); + }; + + const handleAccept = (shouldGrantFullWriteAccess = false) => { + const humanMessageContent = `Accepted change. ${shouldGrantFullWriteAccess ? "Granted full write access." : ""}`; + thread.submit( + { + messages: [ + { + type: "tool", + tool_call_id: props.toolCallId, + id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, + name: "update_file", + content: ACCEPTED_CHANGE_CONTENT, + }, + { + type: "human", + content: humanMessageContent, + }, + ], + }, + { + config: { + configurable: { + permissions: { + full_write_access: shouldGrantFullWriteAccess, + }, + }, + }, + }, + ); + setIsAccepted(true); }; useEffect(() => { if (typeof window === "undefined" || isAccepted) return; const toolResponse = getToolResponse(props.toolCallId, thread); - if (toolResponse && toolResponse.content === ACCEPTED_CHANGE_CONTENT) { - setIsAccepted(true); + if (toolResponse) { + if (toolResponse.content === ACCEPTED_CHANGE_CONTENT) { + setIsAccepted(true); + } else if (toolResponse.content === REJECTED_CHANGE_CONTENT) { + setIsRejected(true); + } } }, []); - if (isAccepted) { + if (isAccepted || isRejected) { return ( -
+
-

Accepted Change

+

+ {isAccepted ? "Accepted" : "Rejected"} Change +

{props.planItem}

-
- - -
+ {!props.fullWriteAccess && ( +
+ + + +
+ )}
); } diff --git a/src/components/thread/messages/tool-calls.tsx b/src/components/thread/messages/tool-calls.tsx index b94f885..816a440 100644 --- a/src/components/thread/messages/tool-calls.tsx +++ b/src/components/thread/messages/tool-calls.tsx @@ -15,7 +15,7 @@ export function ToolCalls({ if (!toolCalls || toolCalls.length === 0) return null; return ( -
+
{toolCalls.map((tc, idx) => { const args = tc.args as Record; const hasArgs = Object.keys(args).length > 0; From df30c65b4c1784b8ef2a28b33fbd9d66337ae64d Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 7 Mar 2025 13:48:40 -0800 Subject: [PATCH 09/11] better ui for rendering plan --- agent/uis/open-code/plan/index.tsx | 94 +++++++++++++++++++----------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx index d6bbce3..2b524ee 100644 --- a/agent/uis/open-code/plan/index.tsx +++ b/agent/uis/open-code/plan/index.tsx @@ -1,4 +1,7 @@ import "./index.css"; +import { motion } from "framer-motion"; +import { ChevronDown } from "lucide-react"; +import { useState } from "react"; interface PlanProps { toolCallId: string; @@ -8,41 +11,66 @@ interface PlanProps { } export default function Plan(props: PlanProps) { + const [isExpanded, setIsExpanded] = useState(false); + return ( -
-

Code Plan

-
-
-

- Remaining Plans -

- {props.remainingPlans.map((step, index) => ( -

- {index + 1}. {step} -

- ))} -
-
-

- Executed Plans -

- {props.executedPlans.map((step, index) => ( -

- {step} -

- ))} -
-
-

- Rejected Plans -

- {props.rejectedPlans.map((step, index) => ( -

- {step} -

- ))} -
+
+
+

Code Plan

+ +
+
+

+ Remaining Plans +

+ {props.remainingPlans.map((step, index) => ( +

+ {index + 1}. {step} +

+ ))} +
+
+

+ Executed Plans +

+ {props.executedPlans.map((step, index) => ( +

+ {step} +

+ ))} +
+
+

+ Rejected Plans +

+ {props.rejectedPlans.map((step, index) => ( +

+ {step} +

+ ))} +
+
+
+ setIsExpanded(!isExpanded)} + animate={{ rotate: isExpanded ? 180 : 0 }} + transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }} + > + +
); } From 360a48f4e1946724351c287cecaf1ff5cc5abc8a Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 7 Mar 2025 13:56:23 -0800 Subject: [PATCH 10/11] cr --- agent/uis/open-code/proposed-change/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx index d89c35e..402c00c 100644 --- a/agent/uis/open-code/proposed-change/index.tsx +++ b/agent/uis/open-code/proposed-change/index.tsx @@ -177,7 +177,7 @@ export default function ProposedChange(props: ProposedChangeProps) { Accept