mirror of
https://github.com/ianarawjo/ChainForge.git
synced 2025-03-14 08:16:37 +00:00
Add a Split Node (#167)
* Add a split node * Repair damage to model settings schema * cleanup * Fix bug w warning msg in Split node * Bug fix removing LLM meta key * Rebuild react and package * Add markdown parser package dependency
This commit is contained in:
parent
5319f8b5a1
commit
f7be853554
@ -1,15 +1,15 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.8665fcca.css",
|
||||
"main.js": "/static/js/main.74fd3890.js",
|
||||
"main.js": "/static/js/main.358435c9.js",
|
||||
"static/js/787.4c72bb55.chunk.js": "/static/js/787.4c72bb55.chunk.js",
|
||||
"index.html": "/index.html",
|
||||
"main.8665fcca.css.map": "/static/css/main.8665fcca.css.map",
|
||||
"main.74fd3890.js.map": "/static/js/main.74fd3890.js.map",
|
||||
"main.358435c9.js.map": "/static/js/main.358435c9.js.map",
|
||||
"787.4c72bb55.chunk.js.map": "/static/js/787.4c72bb55.chunk.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.8665fcca.css",
|
||||
"static/js/main.74fd3890.js"
|
||||
"static/js/main.358435c9.js"
|
||||
]
|
||||
}
|
@ -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.74fd3890.js"></script><link href="/static/css/main.8665fcca.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.358435c9.js"></script><link href="/static/css/main.8665fcca.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
@ -61,6 +61,7 @@
|
||||
"mantine-react-table": "^1.0.0-beta.8",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mathjs": "^11.8.2",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"net": "^1.0.2",
|
||||
"net-browserify": "^0.2.4",
|
||||
"node-fetch": "^2.6.11",
|
||||
|
22
chainforge/react-server/src/App.js
vendored
22
chainforge/react-server/src/App.js
vendored
@ -8,7 +8,7 @@ import ReactFlow, {
|
||||
} from 'reactflow';
|
||||
import { Button, Menu, LoadingOverlay, Text, Box, List, Loader, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { IconSettings, IconTextPlus, IconTerminal, IconCsv, IconSettingsAutomation, IconFileSymlink, IconRobot, IconRuler2, IconArrowMerge } from '@tabler/icons-react';
|
||||
import { IconSettings, IconTextPlus, IconTerminal, IconCsv, IconSettingsAutomation, IconFileSymlink, IconRobot, IconRuler2, IconArrowMerge, IconArrowsSplit } from '@tabler/icons-react';
|
||||
import RemoveEdge from './RemoveEdge';
|
||||
import TextFieldsNode from './TextFieldsNode'; // Import a custom node
|
||||
import PromptNode from './PromptNode';
|
||||
@ -20,6 +20,7 @@ import AlertModal from './AlertModal';
|
||||
import CsvNode from './CsvNode';
|
||||
import TabularDataNode from './TabularDataNode';
|
||||
import JoinNode from './JoinNode';
|
||||
import SplitNode from './SplitNode';
|
||||
import CommentNode from './CommentNode';
|
||||
import GlobalSettingsModal from './GlobalSettingsModal';
|
||||
import ExampleFlowsModal from './ExampleFlowsModal';
|
||||
@ -89,6 +90,7 @@ const nodeTypes = {
|
||||
table: TabularDataNode,
|
||||
comment: CommentNode,
|
||||
join: JoinNode,
|
||||
split: SplitNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
@ -232,6 +234,10 @@ const App = () => {
|
||||
const { x, y } = getViewportCenter();
|
||||
addNode({ id: 'join-'+Date.now(), type: 'join', data: {}, position: {x: x-200, y:y-100} });
|
||||
};
|
||||
const addSplitNode = () => {
|
||||
const { x, y } = getViewportCenter();
|
||||
addNode({ id: 'split-'+Date.now(), type: 'split', data: {}, position: {x: x-200, y:y-100} });
|
||||
};
|
||||
|
||||
const onClickExamples = () => {
|
||||
if (examplesModal && examplesModal.current)
|
||||
@ -725,11 +731,12 @@ const App = () => {
|
||||
|
||||
<div id="custom-controls" style={{position: 'fixed', left: '10px', top: '10px', zIndex:8}}>
|
||||
<Menu transitionProps={{ transition: 'pop-top-left' }}
|
||||
position="top-start"
|
||||
width={220}
|
||||
closeOnClickOutside={true}
|
||||
closeOnEscape
|
||||
>
|
||||
position="top-start"
|
||||
width={220}
|
||||
closeOnClickOutside={true}
|
||||
closeOnEscape
|
||||
styles={{item: { maxHeight: '28px' }}}
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button size="sm" variant="gradient" compact mr='sm'>Add Node +</Button>
|
||||
</Menu.Target>
|
||||
@ -779,6 +786,9 @@ const App = () => {
|
||||
<MenuTooltip label="Concatenate responses or input data together before passing into later nodes, within or across variables and LLMs.">
|
||||
<Menu.Item onClick={addJoinNode} icon={<IconArrowMerge size='14pt' />}> Join Node </Menu.Item>
|
||||
</MenuTooltip>
|
||||
<MenuTooltip label="Split responses or input data by some format. For instance, you can split a markdown list into separate items.">
|
||||
<Menu.Item onClick={addSplitNode} icon={<IconArrowsSplit size='14pt' />}> Split Node </Menu.Item>
|
||||
</MenuTooltip>
|
||||
<Menu.Divider />
|
||||
<Menu.Label>Misc</Menu.Label>
|
||||
<MenuTooltip label="Make a comment about your flow.">
|
||||
|
13
chainforge/react-server/src/CsvNode.js
vendored
13
chainforge/react-server/src/CsvNode.js
vendored
@ -5,6 +5,7 @@ import NodeLabel from './NodeLabelComponent'
|
||||
import { IconCsv } from '@tabler/icons-react';
|
||||
import { Handle } from 'reactflow';
|
||||
import BaseNode from './BaseNode';
|
||||
import { processCSV } from "./backend/utils"
|
||||
|
||||
const CsvNode = ({ data, id }) => {
|
||||
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
|
||||
@ -21,20 +22,10 @@ const CsvNode = ({ data, id }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const processCsv = (csv) => {
|
||||
var matches = csv.match(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g);
|
||||
if (!matches) return;
|
||||
for (var n = 0; n < matches.length; ++n) {
|
||||
matches[n] = matches[n].trim();
|
||||
if (matches[n] == ',') matches[n] = '';
|
||||
}
|
||||
return matches.map(e => e.trim()).filter(e => e.length > 0);
|
||||
}
|
||||
|
||||
// Handle a change in a text fields' input.
|
||||
const handleInputChange = useCallback((event) => {
|
||||
// Update the data for this text fields' id.
|
||||
let new_data = { 'text': event.target.value, 'fields': processCsv(event.target.value) };
|
||||
let new_data = { 'text': event.target.value, 'fields': processCSV(event.target.value) };
|
||||
setDataPropsForNode(id, new_data);
|
||||
pingOutputNodes(id);
|
||||
}, [id, pingOutputNodes, setDataPropsForNode]);
|
||||
|
25
chainforge/react-server/src/JoinNode.js
vendored
25
chainforge/react-server/src/JoinNode.js
vendored
@ -17,19 +17,19 @@ const formattingOptions = [
|
||||
{value: "[]", label:'["list", "of", "strings"]'}
|
||||
];
|
||||
|
||||
const joinTexts = (texts, formatting) => {
|
||||
const joinTexts = (texts, format) => {
|
||||
const escaped_texts = texts.map(t => escapeBraces(t));
|
||||
|
||||
if (formatting === "\n\n" || formatting === "\n")
|
||||
return escaped_texts.join(formatting);
|
||||
else if (formatting === "-")
|
||||
if (format === "\n\n" || format === "\n")
|
||||
return escaped_texts.join(format);
|
||||
else if (format === "-")
|
||||
return escaped_texts.map((t) => ('- ' + t)).join("\n");
|
||||
else if (formatting === "1.")
|
||||
else if (format === "1.")
|
||||
return escaped_texts.map((t, i) => (`${i+1}. ${t}`)).join("\n");
|
||||
else if (formatting === '[]')
|
||||
else if (format === '[]')
|
||||
return JSON.stringify(escaped_texts);
|
||||
|
||||
console.error(`Could not join: Unknown formatting option: ${formatting}`);
|
||||
console.error(`Could not join: Unknown formatting option: ${format}`);
|
||||
return escaped_texts;
|
||||
};
|
||||
|
||||
@ -72,7 +72,7 @@ const tagMetadataWithLLM = (input_data) => {
|
||||
};
|
||||
const extractLLMLookup = (input_data) => {
|
||||
let llm_lookup = {};
|
||||
Object.entries(input_data).forEach(([varname, resp_objs]) => {
|
||||
Object.values(input_data).forEach((resp_objs) => {
|
||||
resp_objs.forEach(r => {
|
||||
if (typeof r === 'string' || !r?.llm?.key || r.llm.key in llm_lookup) return;
|
||||
llm_lookup[r.llm.key] = r.llm;
|
||||
@ -84,16 +84,17 @@ const removeLLMTagFromMetadata = (metavars) => {
|
||||
if (!('__LLM_key' in metavars))
|
||||
return metavars;
|
||||
let mcopy = JSON.parse(JSON.stringify(metavars));
|
||||
delete metavars['__LLM_key'];
|
||||
delete mcopy['__LLM_key'];
|
||||
return mcopy;
|
||||
};
|
||||
|
||||
const truncStr = (s, maxLen) => {
|
||||
if (s.length > maxLen) // Cut the name short if it's long
|
||||
return s.substring(0, maxLen) + '...'
|
||||
return s.substring(0, maxLen) + '...';
|
||||
else
|
||||
return s;
|
||||
};
|
||||
|
||||
const groupResponsesBy = (responses, keyFunc) => {
|
||||
let responses_by_key = {};
|
||||
let unspecified_group = [];
|
||||
@ -192,7 +193,7 @@ const JoinNode = ({ data, id }) => {
|
||||
const handleOnConnect = useCallback(() => {
|
||||
let input_data = pullInputData(["__input"], id);
|
||||
if (!input_data?.__input) {
|
||||
console.warn('Join Node: No input data detected.');
|
||||
// soft fail
|
||||
return;
|
||||
}
|
||||
|
||||
@ -336,7 +337,7 @@ const JoinNode = ({ data, id }) => {
|
||||
<BaseNode classNames="join-node" nodeId={id}>
|
||||
<NodeLabel title={data.title || 'Join Node'}
|
||||
nodeId={id}
|
||||
icon={<IconArrowMerge size='14pt'/>}
|
||||
icon={<IconArrowMerge size='12pt'/>}
|
||||
customButtons={[
|
||||
<JoinedTextsPopover key='joined-text-previews' textInfos={joinedTexts} onHover={handleOnConnect} onClick={openInfoModal} getColorForLLM={getColorForLLMAndSetIfNotFound} />
|
||||
]} />
|
||||
|
@ -364,7 +364,11 @@ const PaLM2Settings = {
|
||||
"title": "Model",
|
||||
"description": "Select a PaLM model to query. For more details on the differences, see the Google PaLM API documentation.",
|
||||
"enum": ["text-bison-001", "chat-bison-001"],
|
||||
"default": "chat-bison-001"
|
||||
"default": "chat-bison-001",
|
||||
"shortname_map": {
|
||||
"text-bison-001": "PaLM2-text",
|
||||
"chat-bison-001": "PaLM2-chat",
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
@ -636,9 +640,19 @@ const HuggingFaceTextInferenceSettings = {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"title": "Model",
|
||||
"description": "Select a suggested HuggingFace-hosted model to query using the Inference API. For more details, check out https://huggingface.co/inference-api",
|
||||
"enum": ["tiiuae/falcon-7b-instruct", "microsoft/DialoGPT-large", "bigscience/bloom-560m", "gpt2", "bigcode/santacoder", "bigcode/starcoder", "Other (HuggingFace)"],
|
||||
"default": "tiiuae/falcon-7b-instruct",
|
||||
"description": "Select a suggested HuggingFace-hosted model to query using the Inference API. For more details, check out https://huggingface.co/inference-api",
|
||||
"enum": ["mistralai/Mistral-7B-Instruct-v0.1", "HuggingFaceH4/zephyr-7b-beta", "tiiuae/falcon-7b-instruct", "microsoft/DialoGPT-large", "bigscience/bloom-560m", "gpt2", "bigcode/santacoder", "bigcode/starcoder", "Other (HuggingFace)"],
|
||||
"default": "tiiuae/falcon-7b-instruct",
|
||||
"shortname_map": {
|
||||
"mistralai/Mistral-7B-Instruct-v0.1": "Mistral-7B",
|
||||
"HuggingFaceH4/zephyr-7b-beta": "Zephyr-7B",
|
||||
"tiiuae/falcon-7b-instruct": "Falcon-7B",
|
||||
"microsoft/DialoGPT-large": "DialoGPT",
|
||||
"bigscience/bloom-560m": "Bloom560M",
|
||||
"gpt2": "GPT-2",
|
||||
"bigcode/santacoder": "santacoder",
|
||||
"bigcode/starcoder": "starcoder"
|
||||
},
|
||||
},
|
||||
"custom_model": {
|
||||
"type": "string",
|
||||
@ -775,7 +789,7 @@ const AlephAlphaLuminousSettings = {
|
||||
title: "Nickname",
|
||||
description:
|
||||
"Unique identifier to appear in ChainForge. Keep it short.",
|
||||
default: "AA-Luminous",
|
||||
default: "Luminous",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
@ -791,6 +805,14 @@ const AlephAlphaLuminousSettings = {
|
||||
"luminous-supreme-control",
|
||||
],
|
||||
default: "luminous-base",
|
||||
shortname_map: {
|
||||
"luminous-extended": "luminous-ext",
|
||||
"luminous-extended-control": "luminous-ext-ctrl",
|
||||
"luminous-base-control": "luminous-base-ctrl",
|
||||
"luminous-base": "luminous-base",
|
||||
"luminous-supreme": "luminous-supr",
|
||||
"luminous-supreme-control": "luminous-supr-ctrl",
|
||||
},
|
||||
},
|
||||
temperature: {
|
||||
type: "number",
|
||||
|
313
chainforge/react-server/src/SplitNode.js
vendored
Normal file
313
chainforge/react-server/src/SplitNode.js
vendored
Normal file
@ -0,0 +1,313 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Handle } from 'reactflow';
|
||||
import useStore from './store';
|
||||
import BaseNode from './BaseNode';
|
||||
import NodeLabel from './NodeLabelComponent';
|
||||
import fetch_from_backend from './fetch_from_backend';
|
||||
import { IconArrowMerge, IconArrowsSplit, IconList } from '@tabler/icons-react';
|
||||
import { Divider, NativeSelect, Text, Popover, Tooltip, Center, Modal, Box } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { escapeBraces } from './backend/template';
|
||||
import { processCSV } from "./backend/utils";
|
||||
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
|
||||
const formattingOptions = [
|
||||
{value: "list", label:"- list items"},
|
||||
{value: "\n", label:"newline \\n"},
|
||||
{value: "\n\n", label:"double newline \\n\\n"},
|
||||
{value: ",", label:"commas (,)"},
|
||||
{value: "code", label:"code blocks"},
|
||||
{value: "paragraph", label:"paragraphs (md)"},
|
||||
];
|
||||
|
||||
const deepcopy = (v) => JSON.parse(JSON.stringify(v));
|
||||
const deepcopy_and_modify = (v, new_val_dict) => {
|
||||
let new_v = deepcopy(v);
|
||||
Object.entries(new_val_dict).forEach(([key, val]) => {
|
||||
new_v[key] = val;
|
||||
});
|
||||
return new_v;
|
||||
};
|
||||
const excluding_key = (d, key) => {
|
||||
if (!(key in d)) return d;
|
||||
const copy_d = {...d};
|
||||
delete copy_d[key];
|
||||
return copy_d;
|
||||
};
|
||||
const truncStr = (s, maxLen) => {
|
||||
if (s.length > maxLen) // Cut the name short if it's long
|
||||
return s.substring(0, maxLen) + '...'
|
||||
else
|
||||
return s;
|
||||
};
|
||||
const tagMetadataWithLLM = (input_data) => {
|
||||
let new_data = {};
|
||||
Object.entries(input_data).forEach(([varname, resp_objs]) => {
|
||||
new_data[varname] = resp_objs.map(r => {
|
||||
if (!r || typeof r === 'string' || !r?.llm?.key) return r;
|
||||
let r_copy = JSON.parse(JSON.stringify(r));
|
||||
r_copy.metavars["__LLM_key"] = r.llm.key;
|
||||
return r_copy;
|
||||
});
|
||||
});
|
||||
return new_data;
|
||||
};
|
||||
const extractLLMLookup = (input_data) => {
|
||||
let llm_lookup = {};
|
||||
Object.entries(input_data).forEach(([varname, resp_objs]) => {
|
||||
resp_objs.forEach(r => {
|
||||
if (typeof r === 'string' || !r?.llm?.key || r.llm.key in llm_lookup) return;
|
||||
llm_lookup[r.llm.key] = r.llm;
|
||||
});
|
||||
});
|
||||
return llm_lookup;
|
||||
};
|
||||
const removeLLMTagFromMetadata = (metavars) => {
|
||||
if (!('__LLM_key' in metavars))
|
||||
return metavars;
|
||||
let mcopy = JSON.parse(JSON.stringify(metavars));
|
||||
delete mcopy['__LLM_key'];
|
||||
return mcopy;
|
||||
};
|
||||
|
||||
/** Flattens markdown AST as dict to text (string) */
|
||||
function compileTextFromMdAST(md) {
|
||||
if (md?.value !== undefined)
|
||||
return md.value ?? "";
|
||||
else if (md?.children?.length > 0)
|
||||
return md.children.map(compileTextFromMdAST).join("");
|
||||
return "";
|
||||
}
|
||||
|
||||
const splitText = (s, format) => {
|
||||
// If format is newline separators, we can just split:
|
||||
if (format === "\n\n" || format === "\n")
|
||||
return s.split(format).map(s => escapeBraces(s.trim())).filter(s => s.length > 0);
|
||||
else if (format === ',')
|
||||
return processCSV(s).map(s => escapeBraces(s)).filter(s => s.length > 0);
|
||||
|
||||
// Other formatting rules require markdown parsing:
|
||||
// Parse string as markdown
|
||||
const md = fromMarkdown(s);
|
||||
let results = [];
|
||||
|
||||
const extract_md_blocks = (block_type) => {
|
||||
if (md?.children.length > 0 && md.children.some(c => c.type === block_type)) {
|
||||
// Find the relevant block(s) that appear in the markdown text, at the root level:
|
||||
const md_blocks = md.children.filter(c => c.type === block_type);
|
||||
for (const md_block of md_blocks) {
|
||||
if (block_type === 'list') {
|
||||
// Extract the list items, flattening the ASTs to text
|
||||
const items = "children" in md_block ? md_block.children : [];
|
||||
for (const item of items) {
|
||||
const text = compileTextFromMdAST(item).trim();
|
||||
results.push(text);
|
||||
}
|
||||
} else if (md_block?.children !== undefined) {
|
||||
results.push(compileTextFromMdAST(md_block).trim());
|
||||
}
|
||||
if (md_block.value !== undefined)
|
||||
results.push(md_block.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extract_md_blocks(format);
|
||||
results = results.filter(s => s.length > 0).map(escapeBraces);
|
||||
|
||||
// NOTE: It is possible to have an empty [] results after split.
|
||||
// This happens if the splitter is a markdown separator, and none were found in the input(s).
|
||||
return results;
|
||||
};
|
||||
|
||||
const displaySplitTexts = (textInfos, getColorForLLM) => {
|
||||
const color_for_llm = (llm) => (getColorForLLM(llm) + '99');
|
||||
return textInfos.map((info, idx) => {
|
||||
|
||||
const vars = info.fill_history;
|
||||
let var_tags = vars === undefined ? [] : Object.keys(vars).map((varname) => {
|
||||
const v = truncStr(vars[varname].trim(), 72);
|
||||
return (<div key={varname} className="response-var-inline">
|
||||
<span className="response-var-name">{varname} = </span><span className="response-var-value">{v}</span>
|
||||
</div>);
|
||||
});
|
||||
|
||||
const ps = (<pre className='small-response'>{info.text ?? info}</pre>);
|
||||
|
||||
return (
|
||||
<div key={"r"+idx} className="response-box" style={{ backgroundColor: (info.llm ? color_for_llm(info.llm?.name) : '#ddd'), width: `100%`}}>
|
||||
<div className="response-var-inline-container">
|
||||
{var_tags}
|
||||
</div>
|
||||
{info.llm === undefined ?
|
||||
ps
|
||||
: (<div className="response-item-llm-name-wrapper">
|
||||
<h1>{info.llm?.name}</h1>
|
||||
{ps}
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const SplitTextsPopover = ({ textInfos, onHover, onClick, getColorForLLM }) => {
|
||||
const [opened, { close, open }] = useDisclosure(false);
|
||||
|
||||
const _onHover = useCallback(() => {
|
||||
onHover();
|
||||
open();
|
||||
}, [onHover, open]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Tooltip label='Click to view all split inputs' 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 split inputs ({textInfos?.length} total)</Text></Center>
|
||||
{displaySplitTexts(textInfos, getColorForLLM)}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const SplitNode = ({ data, id }) => {
|
||||
|
||||
const [splitTexts, setSplitTexts] = useState([]);
|
||||
|
||||
// For an info pop-up that previews all the joined inputs
|
||||
const [infoModalOpened, { open: openInfoModal, close: closeInfoModal }] = useDisclosure(false);
|
||||
|
||||
const [pastInputs, setPastInputs] = useState([]);
|
||||
const pullInputData = useStore((state) => state.pullInputData);
|
||||
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
|
||||
|
||||
// Global lookup for what color to use per LLM
|
||||
const getColorForLLMAndSetIfNotFound = useStore((state) => state.getColorForLLMAndSetIfNotFound);
|
||||
|
||||
const [splitOnFormat, setSplitOnFormat] = useState(data.splitFormat || 'list');
|
||||
|
||||
const handleOnConnect = useCallback(() => {
|
||||
const formatting = splitOnFormat;
|
||||
|
||||
let input_data = pullInputData(["__input"], id);
|
||||
if (!input_data?.__input) {
|
||||
// soft fail if no inputs detected
|
||||
return;
|
||||
}
|
||||
|
||||
// Create lookup table for LLMs in input, indexed by llm key
|
||||
const llm_lookup = extractLLMLookup(input_data);
|
||||
|
||||
// Tag all response objects in the input data with a metavar for their LLM (using the llm key as a uid)
|
||||
input_data = tagMetadataWithLLM(input_data);
|
||||
|
||||
// Generate (flatten) the inputs, which could be recursively chained templates
|
||||
// and a mix of LLM resp objects, templates, and strings.
|
||||
// (We tagged each object with its LLM key so that we can use built-in features to keep track of the LLM associated with each response object)
|
||||
fetch_from_backend('generatePrompts', {
|
||||
prompt: "{__input}",
|
||||
vars: input_data,
|
||||
}).then(promptTemplates => {
|
||||
|
||||
// Convert the templates into response objects
|
||||
let resp_objs = promptTemplates.map(p => ({
|
||||
text: p.toString(),
|
||||
fill_history: excluding_key(p.fill_history, "__input"),
|
||||
llm: "__LLM_key" in p.metavars ? llm_lookup[p.metavars['__LLM_key']] : undefined,
|
||||
metavars: removeLLMTagFromMetadata(p.metavars),
|
||||
}));
|
||||
|
||||
// The naive splitter is just to look at every
|
||||
// response object's text value, and split that into N objects
|
||||
// that have the exact same properties except for their text values.
|
||||
const split_objs = resp_objs.map(resp_obj => {
|
||||
if (typeof resp_obj === "string") return splitText(resp_obj, formatting);
|
||||
const texts = splitText(resp_obj?.text, formatting);
|
||||
if (texts !== undefined && texts.length >= 1)
|
||||
return texts.map(t => deepcopy_and_modify(resp_obj, {text: t}));
|
||||
else if (texts?.length === 0)
|
||||
return [];
|
||||
else
|
||||
return deepcopy(resp_obj);
|
||||
}).flat(); // flatten the split response objects
|
||||
|
||||
setSplitTexts(split_objs);
|
||||
setDataPropsForNode(id, { fields: split_objs });
|
||||
});
|
||||
|
||||
}, [pullInputData, splitOnFormat, splitText, extractLLMLookup, tagMetadataWithLLM]);
|
||||
|
||||
if (data.input) {
|
||||
// If there's a change in inputs...
|
||||
if (data.input !== pastInputs) {
|
||||
setPastInputs(data.input);
|
||||
handleOnConnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh split output anytime the dropdown changes
|
||||
useEffect(() => {
|
||||
handleOnConnect();
|
||||
}, [splitOnFormat])
|
||||
|
||||
useEffect(() => {
|
||||
if (data.refresh && data.refresh === true) {
|
||||
// Recreate the visualization:
|
||||
setDataPropsForNode(id, { refresh: false });
|
||||
handleOnConnect();
|
||||
}
|
||||
}, [data, id, handleOnConnect, setDataPropsForNode]);
|
||||
|
||||
return (
|
||||
<BaseNode classNames="split-node" nodeId={id}>
|
||||
<NodeLabel title={data.title || 'Split Node'}
|
||||
nodeId={id}
|
||||
icon={<IconArrowsSplit size='12pt'/>}
|
||||
customButtons={[
|
||||
<SplitTextsPopover key='split-text-previews' textInfos={splitTexts} onHover={handleOnConnect} onClick={openInfoModal} getColorForLLM={getColorForLLMAndSetIfNotFound} />
|
||||
]} />
|
||||
<Modal title={'List of split inputs (' + splitTexts.length + ' total)'} size='xl' opened={infoModalOpened} onClose={closeInfoModal} styles={{header: {backgroundColor: '#FFD700'}, root: {position: 'relative', left: '-5%'}}}>
|
||||
<Box size={600} m='lg' mt='xl'>
|
||||
{displaySplitTexts(splitTexts, getColorForLLMAndSetIfNotFound)}
|
||||
</Box>
|
||||
</Modal>
|
||||
<NativeSelect onChange={(e) => {setSplitOnFormat(e.target.value); setDataPropsForNode(id, {splitFormat: e.target.value});}}
|
||||
className='nowheel'
|
||||
label={'Split on'}
|
||||
data={formattingOptions}
|
||||
size="xs"
|
||||
value={splitOnFormat}
|
||||
miw='80px'
|
||||
mr='xs'
|
||||
mt='-6px' />
|
||||
{ !(splitOnFormat?.length <= 2) ?
|
||||
<Text color='gray' size='8pt' mt='xs' maw='150px'>All other parts of the input text will be ignored.</Text>
|
||||
: <></>
|
||||
}
|
||||
<Handle
|
||||
type="target"
|
||||
position="left"
|
||||
id="__input"
|
||||
className="grouped-handle"
|
||||
style={{ top: "50%" }}
|
||||
onConnect={handleOnConnect}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position="right"
|
||||
id="output"
|
||||
className="grouped-handle"
|
||||
style={{ top: "50%" }}
|
||||
/>
|
||||
</BaseNode>);
|
||||
};
|
||||
|
||||
export default SplitNode;
|
@ -892,4 +892,14 @@ export const filterDict = (dict: Dict, keyFilterFunc: (key: string) => boolean)
|
||||
acc[key] = dict[key];
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
};
|
||||
|
||||
export const processCSV = (csv: string): string[] => {
|
||||
var matches = csv.match(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g);
|
||||
if (!matches) return;
|
||||
for (var n = 0; n < matches.length; ++n) {
|
||||
matches[n] = matches[n].trim();
|
||||
if (matches[n] == ',') matches[n] = '';
|
||||
}
|
||||
return matches.map(e => e.trim()).filter(e => e.length > 0);
|
||||
}
|
2
chainforge/react-server/src/store.js
vendored
2
chainforge/react-server/src/store.js
vendored
@ -26,7 +26,7 @@ export const colorPalettes = {
|
||||
var: varColorPalette,
|
||||
}
|
||||
|
||||
const refreshableOutputNodeTypes = new Set(['evaluator', 'prompt', 'inspect', 'vis', 'llmeval', 'textfields', 'chat', 'simpleval', 'join']);
|
||||
const refreshableOutputNodeTypes = new Set(['evaluator', 'prompt', 'inspect', 'vis', 'llmeval', 'textfields', 'chat', 'simpleval', 'join', 'split']);
|
||||
|
||||
export let initLLMProviders = [
|
||||
{ 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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user