Skip to content

Commit

Permalink
Object Node & GPT Function Node AI Assist, openai structured outputs …
Browse files Browse the repository at this point in the history
…for chat node
  • Loading branch information
abrenneke committed Aug 8, 2024
1 parent 9639ef5 commit 5089f97
Show file tree
Hide file tree
Showing 16 changed files with 832 additions and 26 deletions.
396 changes: 396 additions & 0 deletions packages/app/graphs/code-node-generator.rivet-project

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions packages/app/src/components/GraphRevisionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,14 +67,14 @@ export const GraphRevisions: FC = () => {

if (!enabled) {
return (
<div css={styles}>
<div css={revisionStyles}>
<Button onClick={() => setEnabled(true)}>Show Revisions</Button>
</div>
);
}

return (
<div css={styles}>
<div css={revisionStyles}>
<GraphRevisionList />
</div>
);
Expand All @@ -86,7 +84,7 @@ export const GraphRevisionList: FC = () => {
const { revisions, isLoading, stop, resume, numTotalRevisions, numProcessedRevisions } = useGraphRevisions();

return (
<div css={styles}>
<div css={revisionStyles}>
<div className="revisions">
{revisions.map((revision) => (
<GraphRevisionListEntry key={revision.hash} revision={revision} />
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const styles = css`
.graph-info-section,
.project-info-section {
padding: 8px 12px;
height: 100%;
overflow: auto;
}
.toggle-tab {
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/components/NodeChangesModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/components/ProjectInfoSidebarTab.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -190,6 +191,9 @@ export const ProjectInfoSidebarTab: FC = () => {
</>
)}
</Field>

<Label htmlFor="">Revisions</Label>
<ProjectRevisions />
</div>
);
};
Expand Down
73 changes: 73 additions & 0 deletions packages/app/src/components/ProjectRevisionList.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>No git history</div>;
}

if (!enabled) {
return (
<div css={revisionStyles}>
<Button onClick={() => setEnabled(true)}>Show Revisions</Button>
</div>
);
}

return (
<div css={revisionStyles}>
<ProjectRevisionList />
</div>
);
};

const ProjectRevisionList: FC = () => {
const { revisions, isLoading, stop, resume, numTotalRevisions, numProcessedRevisions } = useProjectRevisions();

return (
<div css={revisionStyles}>
<div className="revisions">
{revisions.map((revision) => (
<ProjectRevisionListEntry key={revision.hash} revision={revision} />
))}
{isLoading ? (
<div className="loading-area">
<div>
Loading... ({numProcessedRevisions} / {numTotalRevisions})
</div>
<Button onClick={() => stop()}>Stop Loading</Button>
</div>
) : (
<div className="loaded-area">
<span>Searched {numProcessedRevisions} revisions for changes to graph.</span>
{(numProcessedRevisions < numTotalRevisions || numTotalRevisions === 0) && (
<Button onClick={() => resume()}>Load More</Button>
)}
</div>
)}
</div>
</div>
);
};

export const ProjectRevisionListEntry: FC<{
revision: CalculatedRevision;
}> = ({ revision }) => {
return (
<div className="revision">
<div className="hash">
<span>{revision.hash.slice(0, 6)}</span>
</div>
<div className="message">{revision.message}</div>
</div>
);
};
Empty file.
6 changes: 6 additions & 0 deletions packages/app/src/components/editors/CustomEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand All @@ -15,5 +17,9 @@ export const CustomEditor: FC<
.with('CodeNodeAIAssist', () => <CodeNodeAIAssistEditor {...props} editor={editor} />)
.with('ToolCallHandlers', () => <ToolCallHandlersEditor {...props} editor={editor} />)
.with('ExtractRegexNodeAiAssist', () => <ExtractRegexNodeAiAssistEditor {...props} editor={editor} />)
.with('ObjectNodeAiAssist', () => <ObjectNodeAiAssistEditor {...props} editor={editor} />)
.with('GptFunctionNodeJsonSchemaAiAssist', () => (
<GptFunctionNodeJsonSchemaAiAssistEditor {...props} editor={editor} />
))
.otherwise(() => null);
};
Original file line number Diff line number Diff line change
@@ -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<ChartNode>;
}
> = ({ 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(<div dangerouslySetInnerHTML={{ __html: markdownResponse }}></div>, {
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 (
<Field name="aiAssist" label="Generate Using AI">
{() => (
<div css={styles}>
<TextField
isDisabled={isDisabled || working}
isReadOnly={isReadonly}
value={prompt}
onChange={(e) => setPrompt((e.target as HTMLInputElement).value)}
placeholder="What would you like your schema to be?"
onKeyDown={(e) => {
if (e.key === 'Enter') {
generateSchema();
}
}}
/>
<Select
options={modelOptions}
value={selectedModel}
onChange={(option) => setModel(option!.value)}
isDisabled={isDisabled || working}
className="model-selector"
/>
<Button appearance="primary" onClick={generateSchema} isDisabled={isDisabled || working}>
Generate
</Button>
</div>
)}
</Field>
);
};
Loading

0 comments on commit 5089f97

Please sign in to comment.