From a7975ff4ff3d924ab4d1dc5e2febccbbeaee194b Mon Sep 17 00:00:00 2001 From: ramnique <30795890+ramnique@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:24:06 +0530 Subject: [PATCH] add generate for agent instructions --- apps/rowboat/app/actions/actions.ts | 162 ------------- apps/rowboat/app/actions/copilot_actions.ts | 217 ++++++++++++++++++ .../[projectId]/workflow/agent_config.tsx | 206 +++++++++++++++-- .../projects/[projectId]/workflow/copilot.tsx | 4 +- ...ions.tsx => copilot_action_components.tsx} | 16 +- .../[projectId]/workflow/preview-modal.tsx | 56 +++-- .../[projectId]/workflow/workflow_editor.tsx | 2 + 7 files changed, 460 insertions(+), 203 deletions(-) create mode 100644 apps/rowboat/app/actions/copilot_actions.ts rename apps/rowboat/app/projects/[projectId]/workflow/{copilot_actions.tsx => copilot_action_components.tsx} (98%) diff --git a/apps/rowboat/app/actions/actions.ts b/apps/rowboat/app/actions/actions.ts index 9384bc71..7862305f 100644 --- a/apps/rowboat/app/actions/actions.ts +++ b/apps/rowboat/app/actions/actions.ts @@ -1,19 +1,6 @@ 'use server'; -import { convertToCopilotWorkflow } from "../lib/types/copilot_types"; import { convertFromAgenticAPIChatMessages } from "../lib/types/agents_api_types"; -import { convertToCopilotMessage } from "../lib/types/copilot_types"; -import { convertToCopilotApiMessage } from "../lib/types/copilot_types"; -import { convertToCopilotApiChatContext } from "../lib/types/copilot_types"; -import { CopilotAPIResponse } from "../lib/types/copilot_types"; -import { CopilotAPIRequest } from "../lib/types/copilot_types"; -import { CopilotChatContext } from "../lib/types/copilot_types"; -import { CopilotMessage } from "../lib/types/copilot_types"; -import { CopilotAssistantMessage } from "../lib/types/copilot_types"; import { AgenticAPIChatRequest } from "../lib/types/agents_api_types"; -import { CopilotWorkflow } from "../lib/types/copilot_types"; -import { Workflow } from "../lib/types/workflow_types"; -import { WorkflowTool } from "../lib/types/workflow_types"; -import { WorkflowPrompt } from "../lib/types/workflow_types"; import { WorkflowAgent } from "../lib/types/workflow_types"; import { EmbeddingRecord } from "../lib/types/datasource_types"; import { WebpageCrawlResponse } from "../lib/types/tool_types"; @@ -27,10 +14,8 @@ import { openai } from "@ai-sdk/openai"; import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js'; import { embeddingModel } from "../lib/embedding"; import { apiV1 } from "rowboat-shared"; -import { zodToJsonSchema } from 'zod-to-json-schema'; import { Claims, getSession } from "@auth0/nextjs-auth0"; import { callClientToolWebhook, getAgenticApiResponse, mockToolResponse, runRAGToolCall } from "../lib/utils"; -import { assert } from "node:console"; import { check_query_limit } from "../lib/rate_limiting"; import { QueryLimitError } from "../lib/client_utils"; import { projectAuthCheck } from "./project_actions"; @@ -114,153 +99,6 @@ export async function getAssistantResponse( }; } -export async function getCopilotResponse( - projectId: string, - messages: z.infer[], - current_workflow_config: z.infer, - context: z.infer | null, -): Promise<{ - message: z.infer, - rawRequest: unknown, - rawResponse: unknown, -}> { - await projectAuthCheck(projectId); - if (!await check_query_limit(projectId)) { - throw new QueryLimitError(); - } - - // prepare request - const request: z.infer = { - messages: messages.map(convertToCopilotApiMessage), - workflow_schema: JSON.stringify(zodToJsonSchema(CopilotWorkflow)), - current_workflow_config: JSON.stringify(convertToCopilotWorkflow(current_workflow_config)), - context: context ? convertToCopilotApiChatContext(context) : null, - }; - console.log(`copilot request`, JSON.stringify(request, null, 2)); - - // call copilot api - const response = await fetch(process.env.COPILOT_API_URL + '/chat', { - method: 'POST', - body: JSON.stringify(request), - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.COPILOT_API_KEY || 'test'}`, - }, - }); - if (!response.ok) { - console.error('Failed to call copilot api', response); - throw new Error(`Failed to call copilot api: ${response.statusText}`); - } - - // parse and return response - const json: z.infer = await response.json(); - console.log(`copilot response`, JSON.stringify(json, null, 2)); - if ('error' in json) { - throw new Error(`Failed to call copilot api: ${json.error}`); - } - // remove leading ```json and trailing ``` - const msg = convertToCopilotMessage({ - role: 'assistant', - content: json.response.replace(/^```json\n/, '').replace(/\n```$/, ''), - }); - - // validate response schema - assert(msg.role === 'assistant'); - if (msg.role === 'assistant') { - for (const part of msg.content.response) { - if (part.type === 'action') { - switch (part.content.config_type) { - case 'tool': { - const test = { - name: 'test', - description: 'test', - parameters: { - type: 'object', - properties: {}, - required: [], - }, - } as z.infer; - // iterate over each field in part.content.config_changes - // and test if the final object schema is valid - // if not, discard that field - for (const [key, value] of Object.entries(part.content.config_changes)) { - const result = WorkflowTool.safeParse({ - ...test, - [key]: value, - }); - if (!result.success) { - console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message); - delete part.content.config_changes[key]; - } - } - break; - } - case 'agent': { - const test = { - name: 'test', - description: 'test', - type: 'conversation', - instructions: 'test', - prompts: [], - tools: [], - model: 'gpt-4o', - ragReturnType: 'chunks', - ragK: 10, - connectedAgents: [], - controlType: 'retain', - } as z.infer; - // iterate over each field in part.content.config_changes - // and test if the final object schema is valid - // if not, discard that field - for (const [key, value] of Object.entries(part.content.config_changes)) { - const result = WorkflowAgent.safeParse({ - ...test, - [key]: value, - }); - if (!result.success) { - console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message); - delete part.content.config_changes[key]; - } - } - break; - } - case 'prompt': { - const test = { - name: 'test', - type: 'base_prompt', - prompt: "test", - } as z.infer; - // iterate over each field in part.content.config_changes - // and test if the final object schema is valid - // if not, discard that field - for (const [key, value] of Object.entries(part.content.config_changes)) { - const result = WorkflowPrompt.safeParse({ - ...test, - [key]: value, - }); - if (!result.success) { - console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message); - delete part.content.config_changes[key]; - } - } - break; - } - default: { - part.content.error = `Unknown config type: ${part.content.config_type}`; - break; - } - } - } - } - } - - return { - message: msg as z.infer, - rawRequest: request, - rawResponse: json, - }; -} - export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer[]): Promise { await projectAuthCheck(projectId); if (!await check_query_limit(projectId)) { diff --git a/apps/rowboat/app/actions/copilot_actions.ts b/apps/rowboat/app/actions/copilot_actions.ts new file mode 100644 index 00000000..a093330a --- /dev/null +++ b/apps/rowboat/app/actions/copilot_actions.ts @@ -0,0 +1,217 @@ +'use server'; +import { + convertToCopilotWorkflow, convertToCopilotMessage, convertToCopilotApiMessage, + convertToCopilotApiChatContext, CopilotAPIResponse, CopilotAPIRequest, + CopilotChatContext, CopilotMessage, CopilotAssistantMessage, CopilotWorkflow +} from "../lib/types/copilot_types"; +import { + Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent +} from "../lib/types/workflow_types"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { assert } from "node:console"; +import { check_query_limit } from "../lib/rate_limiting"; +import { QueryLimitError } from "../lib/client_utils"; +import { projectAuthCheck } from "./project_actions"; + +export async function getCopilotResponse( + projectId: string, + messages: z.infer[], + current_workflow_config: z.infer, + context: z.infer | null +): Promise<{ + message: z.infer; + rawRequest: unknown; + rawResponse: unknown; +}> { + await projectAuthCheck(projectId); + if (!await check_query_limit(projectId)) { + throw new QueryLimitError(); + } + + // prepare request + const request: z.infer = { + messages: messages.map(convertToCopilotApiMessage), + workflow_schema: JSON.stringify(zodToJsonSchema(CopilotWorkflow)), + current_workflow_config: JSON.stringify(convertToCopilotWorkflow(current_workflow_config)), + context: context ? convertToCopilotApiChatContext(context) : null, + }; + console.log(`copilot request`, JSON.stringify(request, null, 2)); + + // call copilot api + const response = await fetch(process.env.COPILOT_API_URL + '/chat', { + method: 'POST', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.COPILOT_API_KEY || 'test'}`, + }, + }); + if (!response.ok) { + console.error('Failed to call copilot api', response); + throw new Error(`Failed to call copilot api: ${response.statusText}`); + } + + // parse and return response + const json: z.infer = await response.json(); + console.log(`copilot response`, JSON.stringify(json, null, 2)); + if ('error' in json) { + throw new Error(`Failed to call copilot api: ${json.error}`); + } + // remove leading ```json and trailing ``` + const msg = convertToCopilotMessage({ + role: 'assistant', + content: json.response.replace(/^```json\n/, '').replace(/\n```$/, ''), + }); + + // validate response schema + assert(msg.role === 'assistant'); + if (msg.role === 'assistant') { + for (const part of msg.content.response) { + if (part.type === 'action') { + switch (part.content.config_type) { + case 'tool': { + const test = { + name: 'test', + description: 'test', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + } as z.infer; + // iterate over each field in part.content.config_changes + // and test if the final object schema is valid + // if not, discard that field + for (const [key, value] of Object.entries(part.content.config_changes)) { + const result = WorkflowTool.safeParse({ + ...test, + [key]: value, + }); + if (!result.success) { + console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message); + delete part.content.config_changes[key]; + } + } + break; + } + case 'agent': { + const test = { + name: 'test', + description: 'test', + type: 'conversation', + instructions: 'test', + prompts: [], + tools: [], + model: 'gpt-4o', + ragReturnType: 'chunks', + ragK: 10, + connectedAgents: [], + controlType: 'retain', + } as z.infer; + // iterate over each field in part.content.config_changes + // and test if the final object schema is valid + // if not, discard that field + for (const [key, value] of Object.entries(part.content.config_changes)) { + const result = WorkflowAgent.safeParse({ + ...test, + [key]: value, + }); + if (!result.success) { + console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message); + delete part.content.config_changes[key]; + } + } + break; + } + case 'prompt': { + const test = { + name: 'test', + type: 'base_prompt', + prompt: "test", + } as z.infer; + // iterate over each field in part.content.config_changes + // and test if the final object schema is valid + // if not, discard that field + for (const [key, value] of Object.entries(part.content.config_changes)) { + const result = WorkflowPrompt.safeParse({ + ...test, + [key]: value, + }); + if (!result.success) { + console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message); + delete part.content.config_changes[key]; + } + } + break; + } + default: { + part.content.error = `Unknown config type: ${part.content.config_type}`; + break; + } + } + } + } + } + + return { + message: msg as z.infer, + rawRequest: request, + rawResponse: json, + }; +} + +export async function getCopilotAgentInstructions( + projectId: string, + messages: z.infer[], + current_workflow_config: z.infer, + agentName: string, +): Promise { + await projectAuthCheck(projectId); + if (!await check_query_limit(projectId)) { + throw new QueryLimitError(); + } + + // prepare request + const request: z.infer = { + messages: messages.map(convertToCopilotApiMessage), + workflow_schema: JSON.stringify(zodToJsonSchema(CopilotWorkflow)), + current_workflow_config: JSON.stringify(convertToCopilotWorkflow(current_workflow_config)), + context: { + type: 'agent', + agentName: agentName, + } + }; + console.log(`copilot request`, JSON.stringify(request, null, 2)); + + // call copilot api + const response = await fetch(process.env.COPILOT_API_URL + '/edit_agent_instructions', { + method: 'POST', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.COPILOT_API_KEY || 'test'}`, + }, + }); + if (!response.ok) { + console.error('Failed to call copilot api', response); + throw new Error(`Failed to call copilot api: ${response.statusText}`); + } + + // parse and return response + const json = await response.json(); + console.log(`copilot response`, JSON.stringify(json, null, 2)); + let copilotResponse: z.infer; + try { + copilotResponse = CopilotAPIResponse.parse(json); + } catch (e) { + console.error('Failed to parse copilot response', e); + throw new Error(`Failed to parse copilot response: ${e}`); + } + if ('error' in copilotResponse) { + throw new Error(`Failed to call copilot api: ${copilotResponse.error}`); + } + + // return response + return copilotResponse.response; +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/workflow/agent_config.tsx index 09a897ec..31ddf809 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/agent_config.tsx @@ -1,7 +1,7 @@ "use client"; import { WithStringId } from "../../../lib/types/types"; import { AgenticAPITool } from "../../../lib/types/agents_api_types"; -import { WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types"; +import { WorkflowPrompt, WorkflowAgent, Workflow } from "../../../lib/types/workflow_types"; import { DataSource } from "../../../lib/types/datasource_types"; import { Button, Divider, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Radio, RadioGroup, Select, SelectItem } from "@nextui-org/react"; import { z } from "zod"; @@ -9,10 +9,19 @@ import { DataSourceIcon } from "../../../lib/components/datasource-icon"; import { ActionButton, StructuredPanel } from "../../../lib/components/structured-panel"; import { EditableField } from "../../../lib/components/editable-field"; import { Label } from "../../../lib/components/label"; -import { PlusIcon } from "lucide-react"; +import { PlusIcon, SparklesIcon } from "lucide-react"; import { List } from "./config_list"; +import { useState, useEffect, useRef } from "react"; +import { usePreviewModal } from "./preview-modal"; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/react"; +import { Textarea } from "@nextui-org/react"; +import { PreviewModalProvider } from "./preview-modal"; +import { CopilotMessage } from "@/app/lib/types/copilot_types"; +import { getCopilotAgentInstructions } from "@/app/actions/copilot_actions"; export function AgentConfig({ + projectId, + workflow, agent, usedAgentNames, agents, @@ -22,6 +31,8 @@ export function AgentConfig({ handleUpdate, handleClose, }: { + projectId: string, + workflow: z.infer, agent: z.infer, usedAgentNames: Set, agents: z.infer[], @@ -57,6 +68,9 @@ export function AgentConfig({ }); } + const [showGenerateModal, setShowGenerateModal] = useState(false); + const { showPreview } = usePreviewModal(); + return -
- { - handleUpdate({ - ...agent, - instructions: value - }); - }} - markdown - label="Instructions" - multiline - mentions - mentionsAtValues={atMentions} - /> +
+
+
+
+ { + handleUpdate({ + ...agent, + instructions: value + }); + }} + markdown + multiline + mentions + mentionsAtValues={atMentions} + /> +
@@ -270,6 +296,150 @@ export function AgentConfig({ Relinquish to 'start' agent
+ + + + + setShowGenerateModal(false)} + currentInstructions={agent.instructions} + onApply={(newInstructions) => { + handleUpdate({ + ...agent, + instructions: newInstructions + }); + }} + /> +
; } + +function GenerateInstructionsModal({ + projectId, + workflow, + agent, + isOpen, + onClose, + currentInstructions, + onApply +}: { + projectId: string, + workflow: z.infer, + agent: z.infer, + isOpen: boolean, + onClose: () => void, + currentInstructions: string, + onApply: (newInstructions: string) => void +}) { + const [prompt, setPrompt] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { showPreview } = usePreviewModal(); + const textareaRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setPrompt(""); + setIsLoading(false); + setError(null); + textareaRef.current?.focus(); + } + }, [isOpen]); + + const handleGenerate = async () => { + setIsLoading(true); + setError(null); + try { + const msgs: z.infer[] = [ + { + role: 'user', + content: prompt, + }, + ]; + const newInstructions = await getCopilotAgentInstructions(projectId, msgs, workflow, agent.name); + + onClose(); + + showPreview( + currentInstructions, + newInstructions, + true, // markdown enabled + "Generated Instructions", + "Review the changes below:", // message before diff + () => onApply(newInstructions) // apply callback + ); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (prompt.trim() && !isLoading) { + handleGenerate(); + } + } + }; + + return ( + + + Generate Instructions + +
+ {error && ( +
+

{error}

+ +
+ )} +