Add prompt preview tooltip, add ability to disable textfields selectively, bug fixes (#97)

* Add tooltip to prompt preview button

* Focus scrollwheel on textfields textareas

* Replace escaped { and } with their bare versions

* Escape braces in tabular data by default. Ignore empty rows.

* Add ability to disable fields on textfields

* Make sure deleting a field deletes its fields_visibility

* Add withinPortal to Tooltips on side-buttons in text fields

* Add Anthropic model Claude-2.
This commit is contained in:
ianarawjo 2023-07-12 17:12:16 -04:00 committed by GitHub
parent 318f81e1df
commit 3657609c32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 702 additions and 532 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
{
"files": {
"main.css": "/static/css/main.26e6dbb2.css",
"main.js": "/static/js/main.0ddb49f0.js",
"main.css": "/static/css/main.d97bf957.css",
"main.js": "/static/js/main.44a6025f.js",
"static/js/787.4c72bb55.chunk.js": "/static/js/787.4c72bb55.chunk.js",
"index.html": "/index.html",
"main.26e6dbb2.css.map": "/static/css/main.26e6dbb2.css.map",
"main.0ddb49f0.js.map": "/static/js/main.0ddb49f0.js.map",
"main.d97bf957.css.map": "/static/css/main.d97bf957.css.map",
"main.44a6025f.js.map": "/static/js/main.44a6025f.js.map",
"787.4c72bb55.chunk.js.map": "/static/js/787.4c72bb55.chunk.js.map"
},
"entrypoints": [
"static/css/main.26e6dbb2.css",
"static/js/main.0ddb49f0.js"
"static/css/main.d97bf957.css",
"static/js/main.44a6025f.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script async src="https://www.googletagmanager.com/gtag/js?id=G-RN3FDBLMCR"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-RN3FDBLMCR")</script><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A visual programming environment for prompt engineering"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>ChainForge</title><script defer="defer" src="/static/js/main.0ddb49f0.js"></script><link href="/static/css/main.26e6dbb2.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script async src="https://www.googletagmanager.com/gtag/js?id=G-RN3FDBLMCR"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-RN3FDBLMCR")</script><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A visual programming environment for prompt engineering"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>ChainForge</title><script defer="defer" src="/static/js/main.44a6025f.js"></script><link href="/static/css/main.d97bf957.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -258,9 +258,10 @@ const EvaluatorNode = ({ data, id }) => {
runButtonTooltip="Run evaluator over inputs"
customButtons={[
<Tooltip label='Info' key="eval-info">
<button onClick={openInfoModal} className='custom-button' style={{border:'none'}}>
<IconInfoCircle size='12pt' color='gray' style={{marginBottom: '-4px'}} />
</button></Tooltip>]}
<button onClick={openInfoModal} className='custom-button' style={{border:'none'}}>
<IconInfoCircle size='12pt' color='gray' style={{marginBottom: '-4px'}} />
</button>
</Tooltip>]}
/>
<LLMResponseInspectorModal ref={inspectModal} jsonResponses={lastResponses} />
<Modal title={default_header} size='60%' opened={infoModalOpened} onClose={closeInfoModal} styles={{header: {backgroundColor: '#FFD700'}, root: {position: 'relative', left: '-80px'}}}>

View File

@ -9,6 +9,7 @@ import { Collapse, Flex, MultiSelect, NativeSelect } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import * as XLSX from 'xlsx';
import useStore from './store';
import { filterDict } from './backend/utils';
// Helper funcs
const truncStr = (s, maxLen) => {
@ -17,13 +18,6 @@ const truncStr = (s, maxLen) => {
else
return s;
};
const filterDict = (dict, keyFilterFunc) => {
return Object.keys(dict).reduce((acc, key) => {
if (keyFilterFunc(key) === true)
acc[key] = dict[key];
return acc;
}, {});
};
const groupResponsesBy = (responses, keyFunc) => {
let responses_by_key = {};
let unspecified_group = [];

View File

@ -11,12 +11,13 @@
*/
import { APP_IS_RUNNING_LOCALLY } from "./backend/utils";
import { filterDict } from './backend/utils';
// Available LLMs in ChainForge, in the format expected by LLMListItems.
export let AvailableLLMs = [
{ name: "GPT3.5", emoji: "🤖", model: "gpt-3.5-turbo", base_model: "gpt-3.5-turbo", temp: 1.0 }, // The base_model designates what settings form will be used, and must be unique.
{ name: "GPT4", emoji: "🥵", model: "gpt-4", base_model: "gpt-4", temp: 1.0 },
{ name: "Claude", emoji: "📚", model: "claude-v1", base_model: "claude-v1", temp: 0.5 },
{ name: "Claude", emoji: "📚", model: "claude-2", base_model: "claude-v1", temp: 0.5 },
{ name: "PaLM2", emoji: "🦬", model: "chat-bison-001", base_model: "palm2-bison", temp: 0.7 },
{ name: "Azure OpenAI", emoji: "🔷", model: "azure-openai", base_model: "azure-openai", temp: 1.0 },
{ name: "HuggingFace", emoji: "🤗", model: "tiiuae/falcon-7b-instruct", base_model: "hf", temp: 1.0 },
@ -25,14 +26,6 @@ if (APP_IS_RUNNING_LOCALLY()) {
AvailableLLMs.push({ name: "Dalai (Alpaca.7B)", emoji: "🦙", model: "alpaca.7B", base_model: "dalai", temp: 0.5 });
}
const filterDict = (dict, keyFilterFunc) => {
return Object.keys(dict).reduce((acc, key) => {
if (keyFilterFunc(key) === true)
acc[key] = dict[key];
return acc;
}, {});
};
const ChatGPTSettings = {
fullName: "GPT-3.5+ (OpenAI)",
schema: {
@ -249,9 +242,9 @@ const ClaudeSettings = {
"type": "string",
"title": "Model Version",
"description": "Select a version of Claude to query. For more details on the differences, see the Anthropic API documentation.",
"enum": ["claude-v1", "claude-v1-100k", "claude-instant-v1", "claude-instant-v1-100k", "claude-v1.3",
"enum": ["claude-2", "claude-2.0", "claude-instant-1", "claude-instant-1.1", "claude-v1", "claude-v1-100k", "claude-instant-v1", "claude-instant-v1-100k", "claude-v1.3",
"claude-v1.3-100k", "claude-v1.2", "claude-v1.0", "claude-instant-v1.1", "claude-instant-v1.1-100k", "claude-instant-v1.0"],
"default": "claude-v1"
"default": "claude-2"
},
"temperature": {
"type": "number",
@ -314,7 +307,7 @@ const ClaudeSettings = {
"ui:autofocus": true
},
"model": {
"ui:help": "Defaults to claude-v1."
"ui:help": "Defaults to claude-2. Note that Anthropic models in particular are subject to change. Model names prior to Claude 2, including 100k context window, are no longer listed on the Anthropic site, so they may or may not work."
},
"temperature": {
"ui:help": "Defaults to 1.0.",

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Handle } from 'react-flow-renderer';
import { Menu, Button, Progress, Textarea, Text, Popover, Center, Modal, Box } from '@mantine/core';
import { Menu, Button, Progress, Textarea, Text, Popover, Center, Modal, Box, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { v4 as uuid } from 'uuid';
import { IconSearch, IconList } from '@tabler/icons-react';
@ -69,11 +69,13 @@ const PromptListPopover = ({ promptInfos, onHover, onClick }) => {
}, [onHover, open]);
return (
<Popover width={400} position="right-start" withArrow withinPortal shadow="rgb(38, 57, 77) 0px 10px 30px -14px" key="query-info" opened={opened} styles={{dropdown: {maxHeight: '500px', overflowY: 'auto', backgroundColor: '#fff'}}}>
<Popover position="right-start" withArrow withinPortal shadow="rgb(38, 57, 77) 0px 10px 30px -14px" key="query-info" opened={opened} styles={{dropdown: {maxHeight: '500px', maxWidth: '400px', overflowY: 'auto', backgroundColor: '#fff'}}}>
<Popover.Target>
<button className='custom-button' onMouseEnter={_onHover} onMouseLeave={close} onClick={onClick} style={{border:'none'}}>
<IconList size='12pt' color='gray' style={{marginBottom: '-4px'}} />
</button>
<Tooltip label='Click to view all prompts' withArrow>
<button className='custom-button' onMouseEnter={_onHover} onMouseLeave={close} onClick={onClick} style={{border:'none'}}>
<IconList size='12pt' color='gray' style={{marginBottom: '-4px'}} />
</button>
</Tooltip>
</Popover.Target>
<Popover.Dropdown sx={{ pointerEvents: 'none' }}>
<Center><Text size='xs' fw={500} color='#666'>Preview of generated prompts ({promptInfos.length} total)</Text></Center>

View File

@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Handle } from 'react-flow-renderer';
import { Textarea } from '@mantine/core';
import { IconTextPlus } from '@tabler/icons-react';
import { Textarea, Tooltip } from '@mantine/core';
import { IconTextPlus, IconEye, IconEyeOff } from '@tabler/icons-react';
import useStore from './store';
import NodeLabel from './NodeLabelComponent';
import TemplateHooks, { extractBracketedSubstrings } from './TemplateHooksComponent';
@ -26,13 +26,16 @@ const setsAreEqual = (setA, setB) => {
return equal;
}
const delButtonId = 'del-';
const visibleButtonId = 'eye-';
const TextFieldsNode = ({ data, id }) => {
const [templateVars, setTemplateVars] = useState(data.vars || []);
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const delButtonId = 'del-';
const [textfieldsValues, setTextfieldsValues] = useState(data.fields || {});
const [fieldVisibility, setFieldVisibility] = useState(data.fields_visibility || {});
const getUID = useCallback(() => {
if (textfieldsValues) {
@ -48,15 +51,18 @@ const TextFieldsNode = ({ data, id }) => {
const handleDelete = useCallback((event) => {
// Update the data for this text field's id.
let new_fields = { ...textfieldsValues };
let new_vis = { ...fieldVisibility };
var item_id = event.target.id.substring(delButtonId.length);
delete new_fields[item_id];
delete new_vis[item_id];
// if the new_data is empty, initialize it with one empty field
if (Object.keys(new_fields).length === 0) {
new_fields[getUID()] = "";
}
setTextfieldsValues(new_fields);
setDataPropsForNode(id, {fields: new_fields});
}, [textfieldsValues, id, delButtonId, setDataPropsForNode]);
setFieldVisibility(new_vis);
setDataPropsForNode(id, {fields: new_fields, fields_visibility: new_vis});
}, [textfieldsValues, fieldVisibility, id, delButtonId, setDataPropsForNode]);
// Initialize fields (run once at init)
useEffect(() => {
@ -76,6 +82,14 @@ const TextFieldsNode = ({ data, id }) => {
setDataPropsForNode(id, { fields: new_fields });
}, [textfieldsValues, id, setDataPropsForNode]);
// Disable/hide a text field temporarily
const handleDisableField = useCallback((field_id) => {
let vis = {...fieldVisibility};
vis[field_id] = fieldVisibility[field_id] === false; // toggles it
setFieldVisibility(vis);
setDataPropsForNode(id, { fields_visibility: vis });
}, [fieldVisibility, setDataPropsForNode]);
// Save the state of a textfield when it changes and update hooks
const handleTextFieldChange = useCallback((field_id, val) => {
@ -145,11 +159,26 @@ const TextFieldsNode = ({ data, id }) => {
{Object.keys(textfieldsValues).map(i => (
<div className="input-field" key={i}>
<Textarea id={i} name={i}
className="text-field-fixed nodrag"
className="text-field-fixed nodrag nowheel"
minRows="2"
value={textfieldsValues[i]}
disabled={fieldVisibility[i] === false}
onChange={(event) => handleTextFieldChange(i, event.currentTarget.value)} />
{Object.keys(textfieldsValues).length > 1 ? (<button id={delButtonId + i} className="remove-text-field-btn nodrag" onClick={handleDelete}>X</button>) : <></>}
{Object.keys(textfieldsValues).length > 1 ? (
<div style={{display: 'flex', flexDirection: 'column'}}>
<Tooltip label='remove field' position='right' withArrow arrowSize={10} withinPortal>
<button id={delButtonId + i} className="remove-text-field-btn nodrag" onClick={handleDelete} style={{flex: 1}}>X</button>
</Tooltip>
<Tooltip label={(fieldVisibility[i] === false ? 'enable' : 'disable') + ' field'} position='right' withArrow arrowSize={10} withinPortal>
<button id={visibleButtonId + i} className="remove-text-field-btn nodrag" onClick={() => handleDisableField(i)} style={{flex: 1}}>
{fieldVisibility[i] === false ?
<IconEyeOff size='14pt' pointerEvents='none' />
: <IconEye size='14pt' pointerEvents='none' />
}
</button>
</Tooltip>
</div>
) : <></>}
</div>))}
</div>
<Handle

View File

@ -1,4 +1,4 @@
import { StringTemplate, PromptTemplate, PromptPermutationGenerator } from '../template';
import { StringTemplate, PromptTemplate, PromptPermutationGenerator, escapeBraces } from '../template';
import { expect, test } from '@jest/globals';
test('string template', () => {
@ -93,4 +93,14 @@ test('carry together vars', () => {
num_prompts += 1;
}
expect(num_prompts).toBe(3*3);
});
test('escaped braces', () => {
// Escaped braces \{ and \} should not be treated as real variables, one, and two, should be
// removed when calling the 'toString' method on a PromptTemplate:
let promptTemplate = new PromptTemplate('For what show did \\{person\\} get a Netflix deal?');
let filledTemplate = promptTemplate.fill({'person': 'Meghan Markle'});
expect(promptTemplate.toString()).toBe('For what show did {person} get a Netflix deal?');
expect(promptTemplate.toString()).toEqual(filledTemplate.toString());
expect(escapeBraces('Why is the set {0, 1, 2} of size 3?')).toEqual('Why is the set \\{0, 1, 2\\} of size 3?');
});

View File

@ -31,6 +31,10 @@ export enum LLM {
Dalai_Llama_65B = "llama.65B",
// Anthropic
Claude_v2 = "claude-2",
Claude_v2_0 = "claude-2.0",
Claude_1_instant = "claude-instant-1",
Claude_1_instant_1 = "claude-instant-1.1",
Claude_v1 = "claude-v1",
Claude_v1_0 = "claude-v1.0",
Claude_v1_2 = "claude-v1.2",

View File

@ -16,6 +16,14 @@ function isDict(o: any): boolean {
return typeof o === 'object' && !Array.isArray(o);
}
/**
* Given a string, returns the same string with braces { and } escaped, \{ and \}. Does nothing else.
* @param str The string to transform
*/
export function escapeBraces(str: string): string {
return str.replace(/[{}]/g, '\\$&');
}
export class StringTemplate {
val: string;
/**
@ -143,8 +151,9 @@ export class PromptTemplate {
this.metavars = {};
}
/** Returns the value of this.template, with any escaped braces \{ and \} with the escape \ char removed. */
toString(): string {
return this.template;
return this.template.replaceAll('\\{', '{').replaceAll('\\}', '}');
}
toValue(): string {

View File

@ -692,3 +692,11 @@ export function merge_response_objs(resp_obj_A: LLMResponseObject | undefined, r
metavars: resp_obj_B.metavars,
};
}
export const filterDict = (dict: Dict, keyFilterFunc: (key: string) => boolean) => {
return Object.keys(dict).reduce((acc, key) => {
if (keyFilterFunc(key) === true)
acc[key] = dict[key];
return acc;
}, {});
};

View File

@ -4,6 +4,8 @@ import {
applyNodeChanges,
applyEdgeChanges,
} from 'react-flow-renderer';
import { escapeBraces } from './backend/template';
import { filterDict } from './backend/utils';
// Initial project settings
const initialAPIKeys = {};
@ -124,17 +126,25 @@ const useStore = create((set, get) => ({
// Extract all the data for every row of the source column, appending the other values as 'meta-vars':
return rows.map(row => {
const row_keys = Object.keys(row);
// Check if this is an 'empty' row (with all empty strings); if so, skip it:
if (row_keys.every(key => key === '__uid' || !row[key] || row[key].trim() === ""))
return undefined;
const row_excluding_col = {};
Object.keys(row).forEach(key => {
row_keys.forEach(key => {
if (key !== src_col.key && key !== '__uid')
row_excluding_col[col_header_lookup[key]] = row[key];
});
return {
text: ((src_col.key in row) ? row[src_col.key] : ""),
// We escape any braces in the source text before they're passed downstream.
// This is a special property of tabular data nodes: we don't want their text to be treated as prompt templates.
text: escapeBraces((src_col.key in row) ? row[src_col.key] : ""),
metavars: row_excluding_col,
associate_id: row.__uid, // this is used by the backend to 'carry' certain values together
}
});
}).filter(r => r !== undefined);
} else {
console.error(`Could not find table column with source handle name ${sourceHandleKey}`);
return null;
@ -145,8 +155,15 @@ const useStore = create((set, get) => ({
if ("fields" in src_node.data) {
if (Array.isArray(src_node.data["fields"]))
return src_node.data["fields"];
else
return Object.values(src_node.data["fields"]);
else {
// We have to filter over a special 'fields_visibility' prop, which
// can select what fields get output:
if ("fields_visibility" in src_node.data)
return Object.values(filterDict(src_node.data["fields"],
fid => src_node.data["fields_visibility"][fid] !== false));
else // return all field values
return Object.values(src_node.data["fields"]);
}
}
// NOTE: This assumes it's on the 'data' prop, with the same id as the handle:
else return src_node.data[sourceHandleKey];

View File

@ -32,6 +32,7 @@
text-align: left;
border-radius: 6px;
padding: 5px;
pointer-events: none;
/* Position the tooltip */
position: absolute;

View File

@ -6,7 +6,7 @@ def readme():
setup(
name='chainforge',
version='0.2.0.6',
version='0.2.1',
packages=find_packages(),
author="Ian Arawjo",
description="A Visual Programming Environment for Prompt Engineering",