diff --git a/packages/app/graphs/code-node-generator.rivet-project b/packages/app/graphs/code-node-generator.rivet-project index 65f5e0dff..d7d307fdf 100644 --- a/packages/app/graphs/code-node-generator.rivet-project +++ b/packages/app/graphs/code-node-generator.rivet-project @@ -5,6 +5,218 @@ data: testSuites: [] version: 1 graphs: + HhKWCMAQ8eQOo8NEUe6WN: + metadata: + description: Generates the configuration and code for a Code Node based on a + user's request. + id: HhKWCMAQ8eQOo8NEUe6WN + name: Object Node Generator + nodes: + '[LSw9Od3M7kp4BDWMH2hK5]:comment "Comment"': + data: + backgroundColor: rgba(0,0,0,0.05) + color: rgba(255,255,255,1) + height: 1218.6503689348447 + text: "### Initial question to generate the code" + visualData: -1637.2291911884442/228.4700890675118/1639.5188375265523/79// + '[Mn2IP67COwFFtu53CTfR5]:text "Text"': + data: + text: >- + I would like the Object Node to follow the following + specification: + + + """ + + {{spec}} + + """ + + + Please respond with a JSON object based on what I am looking for. + + + If anything is vague with the request, attempt to fill in the gaps and generate something anyway. + outgoingConnections: + - output->"Assemble Prompt" skvkJKngOEyUrY9Jw7pAh/message2 + visualData: -1159.414078546703/820.6518251580231/330/44// + '[RoIocLRqGb2NjOUbgzxy8]:match "Match"': + data: + cases: + - configureExtractRegexNode + - impossibleRequest + outgoingConnections: + - case2->"Destructure" vhqBra6P8GrzFTEMk6KhB/object + visualData: 229.05132289300522/653.6214687275316/280/155// + '[WDAw39mquUZf-znE6HvDQ]:gptFunction "GPT Function"': + data: + description: "Call this function if the request is impossible. " + name: impossibleRequest + schema: >- + { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "A reason for why the user's request is impossible." + } + }, + "required": ["reason"] + } + outgoingConnections: + - function->"Chat" nAttnxSXzOBWVdLE-nBsA/functions + visualData: -746.4223083424073/928.2227958465032/280/156// + '[ZT9EfF-eAeWNWqMpb6Lub]:text "Text"': + data: + regex: "" + text: >- + Rivet is a visual programming IDE where you connect together Nodes + in order to make programs that interact with large language models + (LLMs). + + + The Object Node is a simple node that outputs a JSON object. It has a single code editor where the user can type in a JSON object. This object will be output by the Rivet executor as an object, for future nodes to use. + + + ## Placeholders + + + Additionally, placeholders delimited by double curly braces can be used around fields. For example, {\{foo}\} (ignore the backslashes) can be used to cause rivet to create a `foo` input port on the object node, where other objects can be passed in to the node! This allows objects to be composed from other values. + + + You must use: + + + {{ + + and + + }} + + + ## Examples + + + Simple JSON object: + + + ```json + + { + "stringValue": "foo", + "numberValue": 5 + } + + ``` + + + JSON object with a placeholder for a string (again, ignore the backslashes and use {{ directly for example): + + + ```json + + { + "stringValue": "{\{foo}\}", + "numberValue": 5 + } + + ``` + + + ## Your Role + + + The user will give you what they want the object node to output, in plain english. You are instructed to output JSON conforming to what the user wants the JSON to do. For example, the user may ask for a JSON schema and they will describe the schema they want in plain English. You must output a valid JSON schema for what they want. + outgoingConnections: + - output->"Chat" nAttnxSXzOBWVdLE-nBsA/systemPrompt + visualData: -726.7897865366365/329.1772579707348/330/33// + '[bN6cYKrHB1klwU_T2OxQS]:comment "Comment"': + data: + backgroundColor: rgba(0,0,0,0.05) + color: rgba(255,255,255,1) + height: 974.2145513177229 + text: "### Response Extraction" + visualData: 186.03967739541122/100.77978069649924/1213.1380762809863/145// + '[bcqJUkQJxLVz6AgBMtuKu]:prompt "Prompt"': + data: + enableFunctionCall: false + promptText: Hello, what would you like the Object Node to do? + type: assistant + useTypeInput: false + outgoingConnections: + - output->"Assemble Prompt" skvkJKngOEyUrY9Jw7pAh/message1 + visualData: -1131.5264250597115/626.2855847456499/280/111// + '[hzGuMYMZKFM0yUOBixEA3]:graphOutput "Graph Output"': + data: + dataType: string + id: error + visualData: 964.7106714933195/699.7796495316356/330/159// + '[lOg785BYP-taQMa0lkK6B]:graphInput "Graph Input"': + data: + dataType: string + defaultValue: gpt-4o-mini + id: model + useDefaultValueInput: false + outgoingConnections: + - data->"Chat" nAttnxSXzOBWVdLE-nBsA/model + visualData: -1135.2269437747914/447.68092596305564/294.1641220064855/45// + '[nAttnxSXzOBWVdLE-nBsA]:chat "Chat"': + data: + additionalParameters: [] + cache: false + enableFunctionUse: true + frequencyPenalty: 0 + maxTokens: 1024 + model: gpt-4o-mini + parallelFunctionCalling: false + presencePenalty: 0 + responseFormat: json + stop: "" + temperature: 0 + toolChoice: auto + top_p: 1 + useAsGraphPartialOutput: true + useFrequencyPenaltyInput: false + useMaxTokensInput: false + useModelInput: true + usePresencePenaltyInput: false + useStop: false + useStopInput: false + useTemperatureInput: false + useTopP: false + useTopPInput: false + useUseTopPInput: false + useUserInput: false + outgoingConnections: + - function-call->"Match" RoIocLRqGb2NjOUbgzxy8/input + - function-call->"Match" RoIocLRqGb2NjOUbgzxy8/value + - response->"Graph Output" xqA4sxEfxVx5KZ3Psdv_Y/value + visualData: -290.8634551725346/579.3170909210522/230/37// + '[skvkJKngOEyUrY9Jw7pAh]:assemblePrompt "Assemble Prompt"': + outgoingConnections: + - prompt->"Chat" nAttnxSXzOBWVdLE-nBsA/prompt + visualData: -717.0170939371682/713.5432398109425/280/32// + '[vhqBra6P8GrzFTEMk6KhB]:destructure "Destructure"': + data: + paths: + - $.arguments.reason + outgoingConnections: + - match_0->"Graph Output" hzGuMYMZKFM0yUOBixEA3/value + visualData: 629.8069910168186/709.4959880264801/280/158// + '[xqA4sxEfxVx5KZ3Psdv_Y]:graphOutput "Graph Output"': + data: + dataType: string + id: object + visualData: 219.69149899207565/429.6659119006463/330/161// + '[y7uo5P9hy8_yLq51K7xqy]:graphInput "Graph Input"': + data: + dataType: string + defaultValue: a JSON schema for a chair. Be creative + id: prompt + useDefaultValueInput: false + outgoingConnections: + - data->"Text" Mn2IP67COwFFtu53CTfR5/spec + visualData: -1552.419236105042/873.516337043934/287.64850782584654/51// fd-5pfqrBw3YAPli0X_yi: metadata: description: Generates the configuration and code for a Code Node based on a @@ -545,6 +757,190 @@ data: - firstBlock->"If" RYDwAKZ3ZmY5efXzi3dke/if - firstBlock->"Match" J4bFCY4R2l5RHElPJ-w-n/value visualData: 93.22129334676526/626.8160707712718/280/66// + gBKp-zMF5jubVEiROttP8: + metadata: + description: Generates the configuration and code for a Code Node based on a + user's request. + id: gBKp-zMF5jubVEiROttP8 + name: Structured Outputs JSON Schema Generator + nodes: + '[0OTOnd4b7LWdEQuKnHopk]:graphOutput "Graph Output"': + data: + dataType: string + id: schema + visualData: 219.69149899207565/429.6659119006463/330/161// + '[0vyVT0Y-m-O58xbIDYKPz]:destructure "Destructure"': + data: + paths: + - $.arguments.reason + outgoingConnections: + - match_0->"Graph Output" hJhsH30i2BmmV0oD-_QMg/value + visualData: 629.8069910168186/709.4959880264801/280/158// + '[4XYS0zR5hAsoOA4ci_1IT]:comment "Comment"': + data: + backgroundColor: rgba(0,0,0,0.05) + color: rgba(255,255,255,1) + height: 974.2145513177229 + text: "### Response Extraction" + visualData: 186.03967739541122/100.77978069649924/1213.1380762809863/145// + '[80Ca_FCyXWlaQJka3Ia47]:text "Text"': + data: + regex: "" + text: >- + Rivet is a visual programming IDE where you connect together Nodes + in order to make programs that interact with large language models + (LLMs). + + + The GPT Function node is used for passing available functions to AIs. It uses JSON Schema to define the functions. + + + ## JSON Schema Restrictions + + + For OpenAI Structured Outputs, all schemas must follow these rules: + + + 1. ALL properties of ALL objects must be required. This is accomplished by setting `"required": ["property1", "property2", etc]` for every object type. If a property needs to be optional, then its type should be, for example, `["string", null]`, and `required` should still include the property name. + + + 2. `additionalProperties` must be set to `false` on ALL objects. + + + 3. `description` is required on all properties of all objects. + + + ## Your Role + + + The user will give you plain english describing a JSON schema that they want. You output a compatible JSON schema for their request, following the above restructions. + outgoingConnections: + - output->"Chat" cVukG292qUBt_9g_BUfq-/systemPrompt + visualData: -726.7897865366365/329.1772579707348/330/33// + '[L9NiwGpPyqIKgD26BcMY3]:assemblePrompt "Assemble Prompt"': + outgoingConnections: + - prompt->"Chat" cVukG292qUBt_9g_BUfq-/prompt + visualData: -717.0170939371682/713.5432398109425/280/32// + '[QUwpC6FcpotEGetEmYaFR]:match "Match"': + data: + cases: + - configureExtractRegexNode + - impossibleRequest + outgoingConnections: + - case2->"Destructure" 0vyVT0Y-m-O58xbIDYKPz/object + visualData: 229.05132289300522/653.6214687275316/280/155// + '[XOPE_UBUwVuO78aByNTLO]:prompt "Prompt"': + data: + enableFunctionCall: false + promptText: Hello, what would you like the schema to be? + type: assistant + useTypeInput: false + outgoingConnections: + - output->"Assemble Prompt" L9NiwGpPyqIKgD26BcMY3/message1 + visualData: -1131.5264250597115/626.2855847456499/280/111// + '[XpZAe_9Wj_feWp9Ck6s-S]:graphInput "Graph Input"': + data: + dataType: string + defaultValue: gpt-4o-mini + id: model + useDefaultValueInput: false + outgoingConnections: + - data->"Chat" cVukG292qUBt_9g_BUfq-/model + visualData: -1135.2269437747914/447.68092596305564/294.1641220064855/45// + '[cVukG292qUBt_9g_BUfq-]:chat "Chat"': + data: + additionalParameters: [] + cache: false + enableFunctionUse: true + frequencyPenalty: 0 + maxTokens: 1024 + model: gpt-4o + parallelFunctionCalling: false + presencePenalty: 0 + responseFormat: json + stop: "" + temperature: 0 + toolChoice: auto + top_p: 1 + useAsGraphPartialOutput: true + useFrequencyPenaltyInput: false + useMaxTokensInput: false + useModelInput: true + usePresencePenaltyInput: false + useStop: false + useStopInput: false + useTemperatureInput: false + useTopP: false + useTopPInput: false + useUseTopPInput: false + useUserInput: false + outgoingConnections: + - function-call->"Match" QUwpC6FcpotEGetEmYaFR/input + - function-call->"Match" QUwpC6FcpotEGetEmYaFR/value + - response->"Graph Output" 0OTOnd4b7LWdEQuKnHopk/value + visualData: -290.8634551725346/579.3170909210522/230/37// + '[hJhsH30i2BmmV0oD-_QMg]:graphOutput "Graph Output"': + data: + dataType: string + id: error + visualData: 964.7106714933195/699.7796495316356/330/159// + '[hS4vKuOMPiwyQ-nzEngKa]:gptFunction "GPT Function"': + data: + description: "Call this function if the request is impossible. " + name: impossibleRequest + schema: >- + { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "A reason for why the user's request is impossible." + } + }, + "required": ["reason"] + } + outgoingConnections: + - function->"Chat" cVukG292qUBt_9g_BUfq-/functions + visualData: -731.2406725831552/928.2227958465032/280/163// + '[newQ7uMEHaP-_VLfuK_I-]:text "Text"': + data: + text: >- + I would like the schema to follow the following specification: + + + """ + + {{spec}} + + """ + + + Please respond with a JSON schema based on what I am looking for. + + + If anything is vague with the request, attempt to fill in the gaps and generate something anyway. + + + If I have omitted anything, guess what I want based on the context. + outgoingConnections: + - output->"Assemble Prompt" L9NiwGpPyqIKgD26BcMY3/message2 + visualData: -1159.414078546703/820.6518251580231/330/44// + '[nn8bg4rRre7R9Yfn1LNuU]:comment "Comment"': + data: + backgroundColor: rgba(0,0,0,0.05) + color: rgba(255,255,255,1) + height: 979.6483185797417 + text: "### Initial question to generate the code" + visualData: -1637.2291911884442/228.4700890675118/1646.590817879626/79// + '[wzww16NP9VoULLTpuSaMZ]:graphInput "Graph Input"': + data: + dataType: string + defaultValue: a box, with width, height, and depth. also a string contents. + id: prompt + useDefaultValueInput: false + outgoingConnections: + - data->"Text" newQ7uMEHaP-_VLfuK_I-/spec + visualData: -1528.1286188902386/932.7247165050172/287.64850782584654/164// jAiJgUaPnUwcZCrrYd8L9: metadata: description: Generates the configuration and code for a Code Node based on a diff --git a/packages/app/src/components/GraphRevisionList.tsx b/packages/app/src/components/GraphRevisionList.tsx index f3f660c2f..a36a4d5ab 100644 --- a/packages/app/src/components/GraphRevisionList.tsx +++ b/packages/app/src/components/GraphRevisionList.tsx @@ -6,12 +6,10 @@ import { css } from '@emotion/react'; import Button from '@atlaskit/button'; import { type CalculatedRevision } from '../utils/ProjectRevisionCalculator'; import { graphState, historicalGraphState, isReadOnlyGraphState } from '../state/graph'; -import { GraphId } from '@ironclad/rivet-core'; +import { GraphId, type NodeGraph } from '@ironclad/rivet-core'; -const styles = css` +export const revisionStyles = css` .revisions { - max-height: 800px; - overflow: auto; display: flex; flex-direction: column; margin-right: -12px; @@ -69,14 +67,14 @@ export const GraphRevisions: FC = () => { if (!enabled) { return ( -
+
); } return ( -
+
); @@ -86,7 +84,7 @@ export const GraphRevisionList: FC = () => { const { revisions, isLoading, stop, resume, numTotalRevisions, numProcessedRevisions } = useGraphRevisions(); return ( -
+
{revisions.map((revision) => ( @@ -120,7 +118,17 @@ export const GraphRevisionListEntry: FC<{ const setHistoricalGraph = useSetRecoilState(historicalGraphState); function chooseGraph() { - setGraph(revision.projectAtRevision!.graphs[currentGraphId]!); + const nodesBefore = revision.projectAtRevision!.graphs[currentGraphId]?.nodes ?? []; + const nodesAfter = revision.projectAtRevision!.graphs[currentGraphId]?.nodes!; + + const nodesDeleted = nodesAfter?.filter((node) => !nodesBefore?.some((n) => n.id === node.id)); + + const combinedGraph: NodeGraph = { + ...revision.projectAtRevision!.graphs[currentGraphId]!, + nodes: [...nodesAfter, ...nodesDeleted], + }; + + setGraph(combinedGraph); setIsReadOnlyGraph(true); setHistoricalGraph(revision); } diff --git a/packages/app/src/components/LeftSidebar.tsx b/packages/app/src/components/LeftSidebar.tsx index d4b2d8e9e..ead6309ee 100644 --- a/packages/app/src/components/LeftSidebar.tsx +++ b/packages/app/src/components/LeftSidebar.tsx @@ -38,6 +38,8 @@ const styles = css` .graph-info-section, .project-info-section { padding: 8px 12px; + height: 100%; + overflow: auto; } .toggle-tab { diff --git a/packages/app/src/components/NodeChangesModal.tsx b/packages/app/src/components/NodeChangesModal.tsx index a307dd2ed..55f543db9 100644 --- a/packages/app/src/components/NodeChangesModal.tsx +++ b/packages/app/src/components/NodeChangesModal.tsx @@ -21,8 +21,8 @@ export const NodeChangesModal: FC = () => { return null; } - const beforeYaml = yaml.stringify(changes.before!); - const afterYaml = yaml.stringify(changes.after!); + const beforeYaml = changes.before ? yaml.stringify(changes.before) : ''; + const afterYaml = changes.after ? yaml.stringify(changes.after!) : ''; const yamlDiff = diffStringsUnified(beforeYaml, afterYaml, { contextLines: 5, diff --git a/packages/app/src/components/ProjectInfoSidebarTab.tsx b/packages/app/src/components/ProjectInfoSidebarTab.tsx index 337ab2099..edf2b1816 100644 --- a/packages/app/src/components/ProjectInfoSidebarTab.tsx +++ b/packages/app/src/components/ProjectInfoSidebarTab.tsx @@ -1,7 +1,7 @@ import { useMemo, type FC, useState } from 'react'; import { InlineEditableTextfield } from '@atlaskit/inline-edit'; import { ProjectPluginsConfiguration } from './ProjectPluginConfiguration'; -import { Field } from '@atlaskit/form'; +import { Field, Label } from '@atlaskit/form'; import Select from '@atlaskit/select'; import { useRecoilState } from 'recoil'; import { projectContextState, projectState, savedGraphsState } from '../state/savedGraphs'; @@ -14,6 +14,7 @@ import { produce } from 'immer'; import Toggle from '@atlaskit/toggle'; import { entries } from '../../../core/src/utils/typeSafety'; import { css } from '@emotion/react'; +import { ProjectRevisions } from './ProjectRevisionList'; const styles = css` .context-list { @@ -190,6 +191,9 @@ export const ProjectInfoSidebarTab: FC = () => { )} + + +
); }; diff --git a/packages/app/src/components/ProjectRevisionList.tsx b/packages/app/src/components/ProjectRevisionList.tsx new file mode 100644 index 000000000..e3a2adb34 --- /dev/null +++ b/packages/app/src/components/ProjectRevisionList.tsx @@ -0,0 +1,73 @@ +import { useState, type FC } from 'react'; +import { loadedProjectState } from '../state/savedGraphs'; +import { useRecoilValue } from 'recoil'; +import { revisionStyles } from './GraphRevisionList'; +import Button from '@atlaskit/button'; +import { useProjectRevisions } from '../hooks/useGraphRevisions'; +import { type CalculatedRevision } from '../utils/ProjectRevisionCalculator'; + +export const ProjectRevisions: FC = () => { + const projectState = useRecoilValue(loadedProjectState); + + const [enabled, setEnabled] = useState(false); + + if (!projectState.loaded || !projectState.path) { + return
No git history
; + } + + if (!enabled) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +}; + +const ProjectRevisionList: FC = () => { + const { revisions, isLoading, stop, resume, numTotalRevisions, numProcessedRevisions } = useProjectRevisions(); + + return ( +
+
+ {revisions.map((revision) => ( + + ))} + {isLoading ? ( +
+
+ Loading... ({numProcessedRevisions} / {numTotalRevisions}) +
+ +
+ ) : ( +
+ Searched {numProcessedRevisions} revisions for changes to graph. + {(numProcessedRevisions < numTotalRevisions || numTotalRevisions === 0) && ( + + )} +
+ )} +
+
+ ); +}; + +export const ProjectRevisionListEntry: FC<{ + revision: CalculatedRevision; +}> = ({ revision }) => { + return ( +
+
+ {revision.hash.slice(0, 6)} +
+
{revision.message}
+
+ ); +}; diff --git a/packages/app/src/components/ProjectSidebar.tsx b/packages/app/src/components/ProjectSidebar.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/app/src/components/editors/CustomEditor.tsx b/packages/app/src/components/editors/CustomEditor.tsx index 6a5214dcf..d9515459e 100644 --- a/packages/app/src/components/editors/CustomEditor.tsx +++ b/packages/app/src/components/editors/CustomEditor.tsx @@ -5,6 +5,8 @@ import { match } from 'ts-pattern'; import { CodeNodeAIAssistEditor } from './custom/CodeNodeAIAssistEditor'; import { ToolCallHandlersEditor } from './custom/ToolCallHandlersEditor'; import { ExtractRegexNodeAiAssistEditor } from './custom/ExtractRegexNodeAiAssistEditor'; +import { ObjectNodeAiAssistEditor } from './custom/ObjectNodeAiAssistEditor'; +import { GptFunctionNodeJsonSchemaAiAssistEditor } from './custom/GptFunctionJsonSchemaAiAssistEditor'; export const CustomEditor: FC< SharedEditorProps & { @@ -15,5 +17,9 @@ export const CustomEditor: FC< .with('CodeNodeAIAssist', () => ) .with('ToolCallHandlers', () => ) .with('ExtractRegexNodeAiAssist', () => ) + .with('ObjectNodeAiAssist', () => ) + .with('GptFunctionNodeJsonSchemaAiAssist', () => ( + + )) .otherwise(() => null); }; diff --git a/packages/app/src/components/editors/custom/GptFunctionJsonSchemaAiAssistEditor.tsx b/packages/app/src/components/editors/custom/GptFunctionJsonSchemaAiAssistEditor.tsx new file mode 100644 index 000000000..cad0f7ee5 --- /dev/null +++ b/packages/app/src/components/editors/custom/GptFunctionJsonSchemaAiAssistEditor.tsx @@ -0,0 +1,128 @@ +import { useState, type FC } from 'react'; +import { type SharedEditorProps } from '../SharedEditorProps'; +import { + getError, + type ChartNode, + type CustomEditorDefinition, + coreCreateProcessor, + deserializeProject, + coerceTypeOptional, + type GptFunctionNodeData, +} from '@ironclad/rivet-core'; +import { Field } from '@atlaskit/form'; +import TextField from '@atlaskit/textfield'; +import Button from '@atlaskit/button'; +import { css } from '@emotion/react'; +import Select from '@atlaskit/select'; +import { toast } from 'react-toastify'; +import codeGeneratorProject from '../../../../graphs/code-node-generator.rivet-project?raw'; +import { useRecoilValue } from 'recoil'; +import { settingsState } from '../../../state/settings'; +import { fillMissingSettingsFromEnvironmentVariables } from '../../../utils/tauri'; +import { useDependsOnPlugins } from '../../../hooks/useDependsOnPlugins'; +import { marked } from 'marked'; + +const styles = css` + display: flex; + align-items: center; + gap: 8px; + + .model-selector { + width: 250px; + } +`; + +const modelOptions = [ + { label: 'GPT-4o', value: 'gpt-4o' }, + { label: 'GPT-4o mini', value: 'gpt-4o-mini' }, +]; + +export const GptFunctionNodeJsonSchemaAiAssistEditor: FC< + SharedEditorProps & { + editor: CustomEditorDefinition; + } +> = ({ node, isReadonly, isDisabled, onChange, editor }) => { + const [prompt, setPrompt] = useState(''); + const [working, setWorking] = useState(false); + const [model, setModel] = useState('gpt-4o-mini'); + + const settings = useRecoilValue(settingsState); + const plugins = useDependsOnPlugins(); + + const data = node.data as GptFunctionNodeData; + + const generateSchema = async () => { + try { + const [project] = deserializeProject(codeGeneratorProject); + const processor = coreCreateProcessor(project, { + graph: 'Structured Outputs JSON Schema Generator', + inputs: { + prompt, + model, + }, + ...(await fillMissingSettingsFromEnvironmentVariables(settings, plugins)), + }); + + setWorking(true); + + const outputs = await processor.run(); + + const schema = coerceTypeOptional(outputs.schema, 'string'); + const errorResponse = coerceTypeOptional(outputs.error, 'string'); + + if (errorResponse == null) { + onChange({ + ...node, + data: { + ...data, + schema: schema ?? '', + } satisfies GptFunctionNodeData, + }); + } else { + const markdownResponse = marked(errorResponse); + toast.info(
, { + autoClose: false, + containerId: 'wide', + toastId: 'ai-assist-response', + }); + } + } catch (err) { + toast.error(`Failed to generate schema: ${getError(err).message}`); + } finally { + setWorking(false); + } + }; + + const selectedModel = modelOptions.find((option) => option.value === model); + + return ( + + {() => ( +
+ setPrompt((e.target as HTMLInputElement).value)} + placeholder="What would you like your schema to be?" + onKeyDown={(e) => { + if (e.key === 'Enter') { + generateSchema(); + } + }} + /> + setModel(option!.value)} + isDisabled={isDisabled || working} + className="model-selector" + /> + +
+ )} +
+ ); +}; diff --git a/packages/app/src/vite-app.d.ts b/packages/app/src/vite-app.d.ts index 11f02fe2a..b1f45c786 100644 --- a/packages/app/src/vite-app.d.ts +++ b/packages/app/src/vite-app.d.ts @@ -1 +1,2 @@ /// +/// diff --git a/packages/core/src/model/DataValue.ts b/packages/core/src/model/DataValue.ts index b7222887e..fc5a86ad4 100644 --- a/packages/core/src/model/DataValue.ts +++ b/packages/core/src/model/DataValue.ts @@ -77,6 +77,7 @@ export type GptFunction = { namespace?: string; description: string; parameters: object; + strict: boolean; }; export type GptFunctionDataValue = DataValueDef<'gpt-function', GptFunction>; @@ -399,6 +400,7 @@ export const scalarDefaults: { [P in ScalarDataType]: Extract { }); } + if (this.data.responseFormat === 'json_schema') { + inputs.push({ + dataType: 'object', + id: 'responseSchema' as PortId, + title: 'Response Schema', + description: 'The JSON schema that the response will adhere to (Structured Outputs).', + required: true, + }); + inputs.push({ + dataType: 'string', + id: 'responseSchemaName' as PortId, + title: 'Response Schema Name', + description: 'The name of the JSON schema that the response will adhere to (Structured Outputs).', + required: false, + }); + } + return inputs; } @@ -518,6 +535,7 @@ export class ChatNodeImpl extends NodeImpl { { value: '', label: 'Default' }, { value: 'text', label: 'Text' }, { value: 'json', label: 'JSON Object' }, + { value: 'json_schema', label: 'JSON Schema' }, ], defaultValue: '', helperMessage: 'The format to force the model to reply in.', @@ -711,19 +729,20 @@ export class ChatNodeImpl extends NodeImpl { const endpoint = getInputOrData(this.data, inputs, 'endpoint'); const overrideModel = getInputOrData(this.data, inputs, 'overrideModel'); const seed = getInputOrData(this.data, inputs, 'seed', 'number'); - const responseFormat = getInputOrData(this.data, inputs, 'responseFormat') as 'text' | 'json' | ''; + const responseFormat = getInputOrData(this.data, inputs, 'responseFormat') as 'text' | 'json' | 'json_schema' | ''; const toolChoiceMode = getInputOrData(this.data, inputs, 'toolChoice', 'string') as 'none' | 'auto' | 'function'; - const toolChoice: ChatCompletionOptions['tool_choice'] = !toolChoiceMode - ? undefined - : toolChoiceMode === 'function' - ? { - type: 'function', - function: { - name: getInputOrData(this.data, inputs, 'toolChoiceFunction', 'string'), - }, - } - : toolChoiceMode; + const toolChoice: ChatCompletionOptions['tool_choice'] = + !toolChoiceMode || !this.data.enableFunctionUse + ? undefined + : toolChoiceMode === 'function' + ? { + type: 'function', + function: { + name: getInputOrData(this.data, inputs, 'toolChoiceFunction', 'string'), + }, + } + : toolChoiceMode; const openaiResponseFormat = !responseFormat?.trim() ? undefined @@ -731,9 +750,18 @@ export class ChatNodeImpl extends NodeImpl { ? ({ type: 'json_object', } as const) - : ({ - type: 'text', - } as const); + : responseFormat === 'json_schema' + ? { + type: 'json_schema' as const, + json_schema: { + name: coerceTypeOptional(inputs['responseSchemaName' as PortId], 'string') || 'response_schema', + strict: true, + schema: coerceType(inputs['responseSchema' as PortId], 'object'), + }, + } + : ({ + type: 'text', + } as const); const headersFromData = (this.data.headers ?? []).reduce( (acc, header) => { diff --git a/packages/core/src/model/nodes/GptFunctionNode.ts b/packages/core/src/model/nodes/GptFunctionNode.ts index 7aaa94540..4d8274d87 100644 --- a/packages/core/src/model/nodes/GptFunctionNode.ts +++ b/packages/core/src/model/nodes/GptFunctionNode.ts @@ -28,6 +28,8 @@ export type GptFunctionNodeData = { schema: string; useSchemaInput?: boolean; + + strict?: boolean; }; export class GptFunctionNodeImpl extends NodeImpl { @@ -123,6 +125,12 @@ export class GptFunctionNodeImpl extends NodeImpl { dataKey: 'name', useInputToggleDataKey: 'useNameInput', }, + { + type: 'toggle', + label: 'Strict', + dataKey: 'strict', + helperMessage: 'Sets the strict parameter, which determines if OpenAI Structured Outputs are used.', + }, { type: 'code', label: 'Description', @@ -131,6 +139,11 @@ export class GptFunctionNodeImpl extends NodeImpl { language: 'markdown', height: 100, }, + { + type: 'custom', + customEditorId: 'GptFunctionNodeJsonSchemaAiAssist', + label: 'AI Assist', + }, { type: 'code', label: 'Schema', @@ -189,6 +202,7 @@ export class GptFunctionNodeImpl extends NodeImpl { name, description, parameters: schema as object, + strict: this.data.strict ?? false, }, }, }; diff --git a/packages/core/src/model/nodes/ObjectNode.ts b/packages/core/src/model/nodes/ObjectNode.ts index a4666f10f..93feedc3e 100644 --- a/packages/core/src/model/nodes/ObjectNode.ts +++ b/packages/core/src/model/nodes/ObjectNode.ts @@ -70,6 +70,11 @@ export class ObjectNodeImpl extends NodeImpl { getEditors(): EditorDefinition[] { return [ + { + type: 'custom', + customEditorId: 'ObjectNodeAiAssist', + label: 'AI Assist', + }, { type: 'code', label: 'JSON Template', diff --git a/packages/core/src/utils/openai.ts b/packages/core/src/utils/openai.ts index aac2cab99..1487152d8 100644 --- a/packages/core/src/utils/openai.ts +++ b/packages/core/src/utils/openai.ts @@ -273,6 +273,7 @@ export type ChatCompletionOptions = { tool_choice?: | 'none' | 'auto' + | 'required' | { type: 'function'; function: { @@ -292,6 +293,14 @@ export type ChatCompletionOptions = { * Must be one of text or json_object. */ type: 'json_object'; + } + | { + type: 'json_schema'; + json_schema: { + name: string; + strict: boolean; + schema: object; + }; }; }; @@ -402,6 +411,7 @@ export type ChatCompletionFunction = { name: string; description: string; parameters: object; + strict: boolean; }; export async function* streamChatCompletions({