This commit is contained in:
Ian Arawjo 2024-03-10 18:29:17 -04:00
parent a20d70b5f2
commit d098d26793
20 changed files with 524 additions and 379 deletions

View File

@ -105,6 +105,7 @@
"devDependencies": {
"@craco/craco": "^7.1.0",
"@types/papaparse": "^5.3.14",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-edit-text": "^5.0.4",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
@ -7001,6 +7002,15 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-beautiful-dnd": {
"version": "13.1.8",
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
"integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",

View File

@ -131,6 +131,7 @@
"devDependencies": {
"@craco/craco": "^7.1.0",
"@types/papaparse": "^5.3.14",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-edit-text": "^5.0.4",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",

View File

@ -18,7 +18,7 @@ import {
getAIFeaturesModels,
} from "./backend/ai";
import { IconSparkles, IconAlertCircle } from "@tabler/icons-react";
import AlertModal, { AlertModalHandles } from "./AlertModal";
import AlertModal, { AlertModalRef } from "./AlertModal";
import useStore from "./store";
import {
INFO_CODEBLOCK_JS,
@ -30,7 +30,7 @@ import { queryLLM } from "./backend/backend";
import { splitText } from "./SplitNode";
import { escapeBraces } from "./backend/template";
import { cleanMetavarsFilterFunc } from "./backend/utils";
import { Dict, TemplateVarInfo } from "./backend/typing";
import { Dict, TemplateVarInfo, VarsContext } from "./backend/typing";
const zeroGap = { gap: "0rem" };
const popoverShadow = "rgb(38, 57, 77) 0px 10px 30px -14px";
@ -94,10 +94,7 @@ ${specPrompt}`;
// Builds part of a longer prompt to the LLM about the shape of Response objects
// input into an evaluator (the names of template vars, and available metavars)
export const buildContextPromptForVarsMetavars = (context: {
vars: string[];
metavars: string[];
}) => {
export const buildContextPromptForVarsMetavars = (context: VarsContext) => {
if (!context) return "";
const promptify_key_arr = (arr: string[]) => {
@ -106,10 +103,11 @@ export const buildContextPromptForVarsMetavars = (context: {
};
let context_str = "";
const metavars = context.metavars
? context.metavars.filter(cleanMetavarsFilterFunc)
: [];
const has_vars = context.vars && context.vars.length > 0;
const metavars =
"metavars" in context
? context.metavars.filter(cleanMetavarsFilterFunc)
: [];
const has_vars = "vars" in context && context.vars.length > 0;
const has_metavars = metavars && metavars.length > 0;
const has_context = has_vars || has_metavars;
if (has_context) context_str = "\nThe ResponseInfo instances have ";
@ -257,7 +255,7 @@ export function AIGenReplaceItemsPopover({
const aiFeaturesProvider = useStore((state) => state.aiFeaturesProvider);
// Alerts
const alertModal = useRef<AlertModalHandles>(null);
const alertModal = useRef<AlertModalRef>(null);
// Command Fill state
const [commandFillNumber, setCommandFillNumber] = useState<number>(3);
@ -485,7 +483,7 @@ export interface AIGenCodeEvaluatorPopoverProps {
// Callback that takes a boolean that the popover will call to set whether the values are loading and are done loading
onLoadingChange: (isLoading: boolean) => void;
// The keys available in vars and metavar dicts, for added context to the LLM
context: { vars: string[]; metavars: string[] };
context: VarsContext;
// The code currently in the evaluator
currentEvalCode: string;
}
@ -510,7 +508,7 @@ export function AIGenCodeEvaluatorPopover({
const [awaitingResponse, setAwaitingResponse] = useState(false);
// Alerts
const alertModal = useRef<AlertModalHandles>(null);
const alertModal = useRef<AlertModalRef>(null);
const [didEncounterError, setDidEncounterError] = useState(false);
// Handle errors

View File

@ -8,38 +8,36 @@ const ALERT_MODAL_STYLE = {
root: { position: "relative", left: "-5%" },
} as Styles<ModalBaseStylesNames>;
export interface AlertModalHandles {
export interface AlertModalRef {
trigger: (msg?: string) => void;
}
const AlertModal = forwardRef<AlertModalHandles>(
function AlertModal(props, ref) {
// Mantine modal popover for alerts
const [opened, { open, close }] = useDisclosure(false);
const [alertMsg, setAlertMsg] = useState("");
const AlertModal = forwardRef<AlertModalRef>(function AlertModal(props, ref) {
// Mantine modal popover for alerts
const [opened, { open, close }] = useDisclosure(false);
const [alertMsg, setAlertMsg] = useState("");
// This gives the parent access to triggering the modal alert
const trigger = (msg?: string) => {
if (!msg) msg = "Unknown error.";
console.error(msg);
setAlertMsg(msg);
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
// This gives the parent access to triggering the modal alert
const trigger = (msg?: string) => {
if (!msg) msg = "Unknown error.";
console.error(msg);
setAlertMsg(msg);
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
return (
<Modal
opened={opened}
onClose={close}
title="Error"
styles={ALERT_MODAL_STYLE}
>
<p style={{ whiteSpace: "pre-line" }}>{alertMsg}</p>
</Modal>
);
},
);
return (
<Modal
opened={opened}
onClose={close}
title="Error"
styles={ALERT_MODAL_STYLE}
>
<p style={{ whiteSpace: "pre-line" }}>{alertMsg}</p>
</Modal>
);
});
export default AlertModal;

View File

@ -31,7 +31,7 @@ import CodeEvaluatorNode from "./CodeEvaluatorNode";
import VisNode from "./VisNode";
import InspectNode from "./InspectorNode";
import ScriptNode from "./ScriptNode";
import AlertModal, { AlertModalHandles } from "./AlertModal";
import AlertModal, { AlertModalRef } from "./AlertModal";
import ItemsNode from "./ItemsNode";
import TabularDataNode from "./TabularDataNode";
import JoinNode from "./JoinNode";
@ -243,7 +243,7 @@ const App = () => {
const { hideContextMenu } = useContextMenu();
// For displaying error messages to user
const alertModal = useRef<AlertModalHandles>(null);
const alertModal = useRef<AlertModalRef>(null);
// For displaying a pending 'loading' status
const [isLoading, setIsLoading] = useState(true);
@ -455,8 +455,8 @@ const App = () => {
// Save the current flow to localStorage for later recall. Useful to getting
// back progress upon leaving the site / browser crash / system restart.
const saveFlow = useCallback(
(rf_inst) => {
const rf = rf_inst || rfInstance;
(rf_inst: ReactFlowInstance) => {
const rf = rf_inst ?? rfInstance;
if (!rf) return;
// NOTE: This currently only saves the front-end state. Cache files

View File

@ -8,73 +8,72 @@ export interface AreYouSureModalProps {
onConfirm?: () => void;
}
export interface AreYouSureModalHandles {
export interface AreYouSureModalRef {
trigger: () => void;
}
/** Modal that lets user rename a single value, using a TextInput field. */
const AreYouSureModal = forwardRef<
AreYouSureModalHandles,
AreYouSureModalProps
>(function AreYouSureModal({ title, message, onConfirm }, ref) {
const [opened, { open, close }] = useDisclosure(false);
const description = message || "Are you sure?";
const AreYouSureModal = forwardRef<AreYouSureModalRef, AreYouSureModalProps>(
function AreYouSureModal({ title, message, onConfirm }, ref) {
const [opened, { open, close }] = useDisclosure(false);
const description = message || "Are you sure?";
// This gives the parent access to triggering the modal alert
const trigger = () => {
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
// This gives the parent access to triggering the modal alert
const trigger = () => {
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
const confirmAndClose = () => {
close();
if (onConfirm) onConfirm();
};
const confirmAndClose = () => {
close();
if (onConfirm) onConfirm();
};
return (
<Modal
opened={opened}
onClose={close}
title={title}
styles={{
header: { backgroundColor: "orange", color: "white" },
root: { position: "relative", left: "-5%" },
}}
>
<Box maw={400} mx="auto" mt="md" mb="md">
<Text>{description}</Text>
</Box>
<Flex
mih={50}
gap="md"
justify="space-evenly"
align="center"
direction="row"
wrap="wrap"
return (
<Modal
opened={opened}
onClose={close}
title={title}
styles={{
header: { backgroundColor: "orange", color: "white" },
root: { position: "relative", left: "-5%" },
}}
>
<Button
variant="light"
color="orange"
type="submit"
w="40%"
onClick={close}
<Box maw={400} mx="auto" mt="md" mb="md">
<Text>{description}</Text>
</Box>
<Flex
mih={50}
gap="md"
justify="space-evenly"
align="center"
direction="row"
wrap="wrap"
>
Cancel
</Button>
<Button
variant="filled"
color="blue"
type="submit"
w="40%"
onClick={confirmAndClose}
>
Confirm
</Button>
</Flex>
</Modal>
);
});
<Button
variant="light"
color="orange"
type="submit"
w="40%"
onClick={close}
>
Cancel
</Button>
<Button
variant="filled"
color="blue"
type="submit"
w="40%"
onClick={confirmAndClose}
>
Confirm
</Button>
</Flex>
</Modal>
);
},
);
export default AreYouSureModal;

View File

@ -18,7 +18,6 @@ import {
Switch,
} from "@mantine/core";
import { Prism } from "@mantine/prism";
import { Language } from "prism-react-renderer";
import { useDisclosure } from "@mantine/hooks";
import useStore from "./store";
import BaseNode from "./BaseNode";
@ -29,7 +28,9 @@ import {
IconInfoCircle,
IconBox,
} from "@tabler/icons-react";
import LLMResponseInspectorModal from "./LLMResponseInspectorModal";
import LLMResponseInspectorModal, {
LLMResponseInspectorModalRef,
} from "./LLMResponseInspectorModal";
// Ace code editor
import AceEditor from "react-ace";
@ -37,7 +38,6 @@ import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-xcode";
import "ace-builds/src-noconflict/ext-language_tools";
import fetch_from_backend from "./fetch_from_backend";
import {
APP_IS_RUNNING_LOCALLY,
getVarsAndMetavars,
@ -48,8 +48,17 @@ import InspectFooter from "./InspectFooter";
import { escapeBraces } from "./backend/template";
import LLMResponseInspectorDrawer from "./LLMResponseInspectorDrawer";
import { AIGenCodeEvaluatorPopover } from "./AiPopover";
import { Dict, EvaluatedResponsesResults, StandardizedLLMResponse } from "./backend/typing";
import {
Dict,
EvaluatedResponsesResults,
PythonInterpreter,
StandardizedLLMResponse,
TemplateVarInfo,
VarsContext,
} from "./backend/typing";
import { Status } from "./StatusIndicatorComponent";
import { executejs, executepy, grabResponses } from "./backend/backend";
import { AlertModalRef } from "./AlertModal";
// Whether we are running on localhost or not, and hence whether
// we have access to the Flask backend for, e.g., Python code evaluation.
@ -162,18 +171,26 @@ function process(response) {
return "NOT FOUND";
}`;
export interface CodeEvaluatorComponentHandles {
run: (inputs: Dict[], script_paths?: string[], runInSandbox?: boolean) => Promise<({
code: string, responses?: StandardizedLLMResponse[], error?: string | Error, logs?: string[] })>,
serialize: () => ({code: string}),
setCodeText: (code: string) => void,
export interface CodeEvaluatorComponentRef {
run: (
inputs: StandardizedLLMResponse[],
script_paths?: string[],
runInSandbox?: boolean,
) => Promise<{
code: string;
responses?: StandardizedLLMResponse[];
error?: string | undefined;
logs?: string[];
}>;
serialize: () => { code: string };
setCodeText: (code: string) => void;
}
export interface CodeEvaluatorComponentProps {
code: string;
id: string;
type: 'evaluator' | 'processor';
progLang: 'python' | 'javascript';
type: "evaluator" | "processor";
progLang: "python" | "javascript";
showUserInstruction: boolean;
onCodeEdit?: (code: string) => void;
onCodeChangedFromLastRun?: () => void;
@ -183,155 +200,159 @@ export interface CodeEvaluatorComponentProps {
/**
* Inner component for code evaluators/processors, storing the body of the UI (outside of the header and footers).
*/
export const CodeEvaluatorComponent = forwardRef<CodeEvaluatorComponentHandles, CodeEvaluatorComponentProps>(
function CodeEvaluatorComponent(
{
code,
id,
type: node_type,
progLang,
showUserInstruction,
onCodeEdit,
onCodeChangedFromLastRun,
onCodeEqualToLastRun,
},
ref,
) {
// Code in the editor
const [codeText, setCodeText] = useState(code ?? "");
const [codeTextOnLastRun, setCodeTextOnLastRun] = useState<boolean | string>(false);
// Controlled handle when user edits code
const handleCodeEdit = (code: string) => {
if (codeTextOnLastRun !== false) {
const code_changed = code !== codeTextOnLastRun;
if (code_changed && onCodeChangedFromLastRun)
onCodeChangedFromLastRun();
else if (!code_changed && onCodeEqualToLastRun) onCodeEqualToLastRun();
}
setCodeText(code);
if (onCodeEdit) onCodeEdit(code);
};
// Runs the code evaluator/processor over the inputs, returning the results as a Promise.
// Errors are raised as a rejected Promise.
const run = (inputs: Dict[], script_paths?: string[], runInSandbox?: boolean) => {
// Double-check that the code includes an 'evaluate' or 'process' function, whichever is needed:
const find_func_regex =
node_type === "evaluator"
? progLang === "python"
? /def\s+evaluate\s*(.*):/
: /function\s+evaluate\s*(.*)/
: progLang === "python"
? /def\s+process\s*(.*):/
: /function\s+process\s*(.*)/;
if (codeText.search(find_func_regex) === -1) {
const req_func_name =
node_type === "evaluator" ? "evaluate" : "process";
const err_msg = `Could not find required function '${req_func_name}'. Make sure you have defined an '${req_func_name}' function.`;
return Promise.reject(new Error(err_msg)); // hard fail
}
const codeTextOnRun = codeText + "";
const execute_route = progLang === "python" ? "executepy" : "executejs";
let executor = progLang === "python" ? "pyodide" : undefined;
// Enable running Python in Flask backend (unsafe) if running locally and the user has turned off the sandbox:
if (progLang === "python" && IS_RUNNING_LOCALLY && !runInSandbox)
executor = "flask";
return fetch_from_backend(execute_route, {
id,
code: codeTextOnRun,
responses: inputs,
scope: "response",
process_type: node_type,
script_paths,
executor,
}).then(function (json) {
json = json as EvaluatedResponsesResults;
// Check if there's an error; if so, bubble it up to user and exit:
if (json.error) {
if (json.logs) json.logs.push(json.error);
} else {
setCodeTextOnLastRun(codeTextOnRun);
}
return {
code, // string
responses: json?.responses, // array of ResponseInfo Objects
error: json?.error, // undefined or, if present, a string of the error message
logs: json?.logs, // an array of strings representing console.logs/prints made during execution
};
});
};
// Export the current internal state as JSON
const serialize = () => ({ code: codeText });
// Define functions accessible from the parent component
useImperativeHandle(ref, () => ({
run,
serialize,
setCodeText,
}));
// Helpful instruction for user
const code_instruct_header = useMemo(() => {
if (node_type === "evaluator")
return (
<div className="code-mirror-field-header">
Define an <Code>evaluate</Code> func to map over each response:
</div>
);
else
return (
<div className="code-mirror-field-header">
Define a <Code>process</Code> func to map over each response:
</div>
);
}, [node_type]);
return (
<div className="core-mirror-field">
{showUserInstruction ? code_instruct_header : <></>}
<div className="ace-editor-container nodrag">
<AceEditor
mode={progLang}
theme="xcode"
onChange={handleCodeEdit}
value={code}
name={"aceeditor_" + id}
editorProps={{ $blockScrolling: true }}
width="100%"
height="100px"
style={{ minWidth: "310px" }}
setOptions={{ useWorker: false }}
tabSize={2}
onLoad={(editorInstance) => {
// Make Ace Editor div resizeable.
editorInstance.container.style.resize = "both";
document.addEventListener("mouseup", () =>
editorInstance.resize(),
);
}}
/>
</div>
</div>
);
export const CodeEvaluatorComponent = forwardRef<
CodeEvaluatorComponentRef,
CodeEvaluatorComponentProps
>(function CodeEvaluatorComponent(
{
code,
id,
type: node_type,
progLang,
showUserInstruction,
onCodeEdit,
onCodeChangedFromLastRun,
onCodeEqualToLastRun,
},
);
ref,
) {
// Code in the editor
const [codeText, setCodeText] = useState(code ?? "");
const [codeTextOnLastRun, setCodeTextOnLastRun] = useState<boolean | string>(
false,
);
// Controlled handle when user edits code
const handleCodeEdit = (code: string) => {
if (codeTextOnLastRun !== false) {
const code_changed = code !== codeTextOnLastRun;
if (code_changed && onCodeChangedFromLastRun) onCodeChangedFromLastRun();
else if (!code_changed && onCodeEqualToLastRun) onCodeEqualToLastRun();
}
setCodeText(code);
if (onCodeEdit) onCodeEdit(code);
};
// Runs the code evaluator/processor over the inputs, returning the results as a Promise.
// Errors are raised as a rejected Promise.
const run = (
inputs: StandardizedLLMResponse[],
script_paths?: string[],
runInSandbox?: boolean,
) => {
// Double-check that the code includes an 'evaluate' or 'process' function, whichever is needed:
const find_func_regex =
node_type === "evaluator"
? progLang === "python"
? /def\s+evaluate\s*(.*):/
: /function\s+evaluate\s*(.*)/
: progLang === "python"
? /def\s+process\s*(.*):/
: /function\s+process\s*(.*)/;
if (codeText.search(find_func_regex) === -1) {
const req_func_name = node_type === "evaluator" ? "evaluate" : "process";
const err_msg = `Could not find required function '${req_func_name}'. Make sure you have defined an '${req_func_name}' function.`;
return Promise.reject(new Error(err_msg)); // hard fail
}
const codeTextOnRun = codeText + "";
const execute_route = progLang === "python" ? executepy : executejs;
let executor: PythonInterpreter | undefined =
progLang === "python" ? "pyodide" : undefined;
// Enable running Python in Flask backend (unsafe) if running locally and the user has turned off the sandbox:
if (progLang === "python" && IS_RUNNING_LOCALLY && !runInSandbox)
executor = "flask";
return execute_route(
id,
codeTextOnRun,
inputs,
"response",
node_type,
script_paths,
executor,
).then(function (json) {
json = json as EvaluatedResponsesResults;
// Check if there's an error; if so, bubble it up to user and exit:
if (json.error) {
if (json.logs) json.logs.push(json.error);
} else {
setCodeTextOnLastRun(codeTextOnRun);
}
return {
code, // string
responses: json?.responses, // array of ResponseInfo Objects
error: json?.error, // undefined or, if present, a string of the error message
logs: json?.logs, // an array of strings representing console.logs/prints made during execution
};
});
};
// Export the current internal state as JSON
const serialize = () => ({ code: codeText });
// Define functions accessible from the parent component
useImperativeHandle(ref, () => ({
run,
serialize,
setCodeText,
}));
// Helpful instruction for user
const code_instruct_header = useMemo(() => {
if (node_type === "evaluator")
return (
<div className="code-mirror-field-header">
Define an <Code>evaluate</Code> func to map over each response:
</div>
);
else
return (
<div className="code-mirror-field-header">
Define a <Code>process</Code> func to map over each response:
</div>
);
}, [node_type]);
return (
<div className="core-mirror-field">
{showUserInstruction ? code_instruct_header : <></>}
<div className="ace-editor-container nodrag">
<AceEditor
mode={progLang}
theme="xcode"
onChange={handleCodeEdit}
value={code}
name={"aceeditor_" + id}
editorProps={{ $blockScrolling: true }}
width="100%"
height="100px"
style={{ minWidth: "310px" }}
setOptions={{ useWorker: false }}
tabSize={2}
onLoad={(editorInstance) => {
// Make Ace Editor div resizeable.
editorInstance.container.style.resize = "both";
document.addEventListener("mouseup", () => editorInstance.resize());
}}
/>
</div>
</div>
);
});
export interface CodeEvaluatorNodeProps {
data: {
title: string;
code: string;
language: 'python' | 'javascript';
language: "python" | "javascript";
sandbox: boolean;
refresh: boolean;
};
id: string;
type: 'evaluator' | 'processor';
id: string;
type: "evaluator" | "processor";
}
/**
@ -339,9 +360,13 @@ export interface CodeEvaluatorNodeProps {
* It has two possible node_types: 'evaluator' and 'processor' mode.
* Evaluators annotate responses with scores; processors transform response objects themselves.
*/
const CodeEvaluatorNode: React.FC<CodeEvaluatorNodeProps> = ({ data, id, type: node_type }) => {
const CodeEvaluatorNode: React.FC<CodeEvaluatorNodeProps> = ({
data,
id,
type: node_type,
}) => {
// The inner component storing the code UI and providing an interface to run the code over inputs
const codeEvaluatorRef = useRef<CodeEvaluatorComponentHandles>(null);
const codeEvaluatorRef = useRef<CodeEvaluatorComponentRef>(null);
const currentCode = useMemo(() => data.code, [data.code]);
// Whether to sandbox the code execution. Currently this option only displays for Python running locally,
@ -359,17 +384,17 @@ const CodeEvaluatorNode: React.FC<CodeEvaluatorNodeProps> = ({ data, id, type: n
// For genAI features
const flags = useStore((state) => state.flags);
const [isEvalCodeGenerating, setIsEvalCodeGenerating] = useState(false);
const [lastContext, setLastContext] = useState({});
const [lastContext, setLastContext] = useState<VarsContext>({});
// For displaying error messages to user
const alertModal = useRef(null);
const alertModal = useRef<AlertModalRef>(null);
// For an info pop-up that explains the type of ResponseInfo
const [infoModalOpened, { open: openInfoModal, close: closeInfoModal }] =
useDisclosure(false);
// For a way to inspect responses without having to attach a dedicated node
const inspectModal = useRef(null);
const inspectModal = useRef<LLMResponseInspectorModalRef>(null);
// eslint-disable-next-line
const [uninspectedResponses, setUninspectedResponses] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
@ -380,19 +405,35 @@ const CodeEvaluatorNode: React.FC<CodeEvaluatorNodeProps> = ({ data, id, type: n
const [progLang, setProgLang] = useState(data.language ?? "python");
const [lastRunLogs, setLastRunLogs] = useState("");
const [lastResponses, setLastResponses] = useState([]);
const [lastResponses, setLastResponses] = useState<StandardizedLLMResponse[]>(
[],
);
const [lastRunSuccess, setLastRunSuccess] = useState(true);
const handleError = useCallback(
(err: string | Error) => {
setStatus(Status.ERROR);
setLastRunSuccess(false);
if (typeof err !== "string") console.error(err);
alertModal.current?.trigger(typeof err === "string" ? err : err?.message);
},
[alertModal],
);
const pullInputs = useCallback(() => {
// Pull input data
let pulled_inputs = pullInputData(["responseBatch"], id);
let pulled_inputs: Dict | StandardizedLLMResponse[] = pullInputData(
["responseBatch"],
id,
);
if (!pulled_inputs || !pulled_inputs.responseBatch) {
console.warn(`No inputs for code ${node_type} node.`);
return null;
}
// Convert to standard response format (StandardLLMResponseFormat)
pulled_inputs = pulled_inputs.responseBatch.map(toStandardResponseFormat);
return pulled_inputs;
return pulled_inputs.responseBatch.map(
toStandardResponseFormat,
) as StandardizedLLMResponse[];
}, [id, pullInputData]);
// On initialization
@ -408,15 +449,13 @@ The Python interpeter in the browser is Pyodide. You may not be able to run some
}
// Attempt to grab cache'd responses
fetch_from_backend("grabResponses", {
responses: [id],
}).then(function (json) {
if (json.responses && json.responses.length > 0) {
grabResponses([id])
.then(function (resps) {
// Store responses and set status to green checkmark
setLastResponses(stripLLMDetailsFromResponses(json.responses));
setLastResponses(stripLLMDetailsFromResponses(resps));
setStatus(Status.READY);
}
});
})
.catch(handleError);
}, []);
// On upstream changes
@ -449,20 +488,17 @@ The Python interpeter in the browser is Pyodide. You may not be able to run some
setLastRunLogs("");
setLastResponses([]);
const rejected = (err) => {
setStatus(Status.ERROR);
setLastRunSuccess(false);
if (typeof err !== "string") console.error(err);
alertModal.current.trigger(typeof err === "string" ? err : err?.message);
};
// Get all the Python script nodes, and get all the folder paths
// NOTE: Python only!
let script_paths: string[] = [];
if (progLang === "python") {
const script_nodes = nodes.filter((n) => n.type === "script");
script_paths = script_nodes
.map((n) => Object.values(n.data.scriptFiles as Dict<string>).filter((f: string) => f !== ""))
.map((n) =>
Object.values(n.data.scriptFiles as Dict<string>).filter(
(f: string) => f !== "",
),
)
.flat();
}
@ -473,10 +509,12 @@ The Python interpeter in the browser is Pyodide. You may not be able to run some
if (json?.logs) setLastRunLogs(json.logs.join("\n > "));
// Check if there's an error; if so, bubble it up to user and exit:
if (!json || json.error) {
rejected(json?.error);
return;
}
if (!json || json.error || json.responses === undefined)
throw new Error(
typeof json.error === "string"
? json.error
: "Unknown error when running code evaluator.",
);
console.log(json.responses);
@ -495,13 +533,13 @@ The Python interpeter in the browser is Pyodide. You may not be able to run some
.map((resp_obj) =>
resp_obj.responses.map((r) => {
// Carry over the response text, prompt, prompt fill history (vars), and llm data
const o = {
const o: TemplateVarInfo = {
text: escapeBraces(r),
prompt: resp_obj.prompt,
fill_history: resp_obj.vars,
metavars: resp_obj.metavars || {},
llm: resp_obj.llm,
batch_id: resp_obj.uid,
uid: resp_obj.uid,
};
// Carry over any chat history
@ -513,8 +551,13 @@ The Python interpeter in the browser is Pyodide. You may not be able to run some
)
.flat(),
});
if (status !== Status.READY && !showDrawer)
setUninspectedResponses(true);
setStatus(Status.READY);
})
.catch(rejected);
.catch(handleError);
};
const hideStatusIndicator = () => {

View File

@ -9,10 +9,10 @@ import {
export interface InspectFooterProps {
label: React.ReactNode;
onClick: () => void;
showNotificationDot: boolean;
showDrawerButton: boolean;
onDrawerClick: () => void;
isDrawerOpen: boolean;
showNotificationDot?: boolean;
}
/**
@ -22,10 +22,10 @@ export interface InspectFooterProps {
const InspectFooter: React.FC<InspectFooterProps> = ({
label,
onClick,
showNotificationDot,
showDrawerButton,
onDrawerClick,
isDrawerOpen,
showNotificationDot,
}) => {
const text = useMemo(
() =>

View File

@ -0,0 +1,65 @@
/**
* A fullscreen version of the Inspect node that
* appears in a Mantine modal pop-up which takes up much of the screen.
*/
import React, { forwardRef, useImperativeHandle } from "react";
import { Modal } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import LLMResponseInspector, { exportToExcel } from "./LLMResponseInspector";
import { StandardizedLLMResponse } from "./backend/typing";
export interface LLMResponseInspectorModalRef {
trigger: () => void;
}
export interface LLMResponseInspectorModalProps {
jsonResponses: StandardizedLLMResponse[];
}
const LLMResponseInspectorModal = forwardRef<
LLMResponseInspectorModalRef,
LLMResponseInspectorModalProps
>(function LLMResponseInspectorModal({ jsonResponses }, ref) {
const [opened, { open, close }] = useDisclosure(false);
// This gives the parent access to triggering the modal
const trigger = () => {
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
return (
<Modal
size="90%"
keepMounted
opened={opened}
onClose={close}
closeOnClickOutside={true}
style={{ position: "relative", left: "-5%" }}
title={
<div>
<span>Response Inspector</span>
<button
className="custom-button"
style={{ marginTop: "auto", marginRight: "14px", float: "right" }}
onClick={() => exportToExcel(jsonResponses)}
>
Export data to Excel
</button>
</div>
}
styles={{ title: { justifyContent: "space-between", width: "100%" } }}
>
<div
className="inspect-modal-response-container"
style={{ padding: "6px", overflow: "scroll" }}
>
<LLMResponseInspector jsonResponses={jsonResponses} wideFormat={true} />
</div>
</Modal>
);
});
export default LLMResponseInspectorModal;

View File

@ -17,7 +17,13 @@ import {
getDefaultModelFormData,
postProcessFormData,
} from "./ModelSettingSchemas";
import { Dict, Func, JSONCompatible, LLMSpec, ModelSettingsDict } from "./backend/typing";
import {
Dict,
Func,
JSONCompatible,
LLMSpec,
ModelSettingsDict,
} from "./backend/typing";
export interface ModelSettingsModalHandle {
trigger: () => void;
@ -28,7 +34,10 @@ export interface ModelSettingsModalProps {
}
type FormData = LLMSpec["formData"];
const ModelSettingsModal = forwardRef<ModelSettingsModalHandle, ModelSettingsModalProps>(function ModelSettingsModal({model, onSettingsSubmit}, ref) {
const ModelSettingsModal = forwardRef<
ModelSettingsModalHandle,
ModelSettingsModalProps
>(function ModelSettingsModal({ model, onSettingsSubmit }, ref) {
const [opened, { open, close }] = useDisclosure(false);
const [formData, setFormData] = useState<FormData>(undefined);
@ -42,8 +51,12 @@ const ModelSettingsModal = forwardRef<ModelSettingsModalHandle, ModelSettingsMod
const [uiSchema, setUISchema] = useState<ModelSettingsDict["uiSchema"]>({});
const [baseModelName, setBaseModelName] = useState("(unknown)");
const [initShortname, setInitShortname] = useState<string | undefined>(undefined);
const [initModelName, setInitModelName] = useState<string | undefined>(undefined);
const [initShortname, setInitShortname] = useState<string | undefined>(
undefined,
);
const [initModelName, setInitModelName] = useState<string | undefined>(
undefined,
);
// Totally necessary emoji picker
const [modelEmoji, setModelEmoji] = useState("");
@ -98,7 +111,7 @@ const ModelSettingsModal = forwardRef<ModelSettingsModalHandle, ModelSettingsMod
const saveFormState = useCallback(
(fdata: FormData) => {
if (fdata === undefined) return;
if (fdata === undefined) return;
// For some reason react-json-form-schema returns 'undefined' on empty strings.
// We need to (1) detect undefined values for keys in formData and (2) if they are of type string, replace with "",
// if that property is marked with a special "allow_empty_str" property.
@ -117,11 +130,7 @@ const ModelSettingsModal = forwardRef<ModelSettingsModalHandle, ModelSettingsMod
if (onSettingsSubmit) {
model.emoji = modelEmoji;
onSettingsSubmit(
model,
patched_fdata,
postprocess(patched_fdata),
);
onSettingsSubmit(model, patched_fdata, postprocess(patched_fdata));
}
},
[model, modelEmoji, schema, setFormData, onSettingsSubmit, postprocess],

View File

@ -10,8 +10,8 @@ import { EditText, onSaveProps } from "react-edit-text";
import "react-edit-text/dist/index.css";
import useStore from "./store";
import StatusIndicator, { Status } from "./StatusIndicatorComponent";
import AlertModal, { AlertModalHandles } from "./AlertModal";
import AreYouSureModal, { AreYouSureModalHandles } from "./AreYouSureModal";
import AlertModal, { AlertModalRef } from "./AlertModal";
import AreYouSureModal, { AreYouSureModalRef } from "./AreYouSureModal";
export interface NodeLabelProps {
title: string;
@ -22,7 +22,7 @@ export interface NodeLabelProps {
editable?: boolean;
status?: Status;
isRunning?: boolean;
alertModal?: React.Ref<AlertModalHandles>;
alertModal?: React.Ref<AlertModalRef>;
customButtons?: React.ReactElement[];
handleRunClick?: () => void;
handleStopClick?: (nodeId: string) => void;
@ -58,7 +58,7 @@ export const NodeLabel: React.FC<NodeLabelProps> = ({
const removeNode = useStore((state) => state.removeNode);
// For 'delete node' confirmation popup
const deleteConfirmModal = useRef<AreYouSureModalHandles>(null);
const deleteConfirmModal = useRef<AreYouSureModalRef>(null);
const [deleteConfirmProps, setDeleteConfirmProps] =
useState<DeleteConfirmProps>({
title: "Delete node",

View File

@ -2,7 +2,13 @@ import React from "react";
import { Dict } from "./backend/typing";
import { truncStr } from "./backend/utils";
const PlotLegend = ({ labels, onClickLabel }: { labels: Dict<string>, onClickLabel: (label: string) => void }) => {
const PlotLegend = ({
labels,
onClickLabel,
}: {
labels: Dict<string>;
onClickLabel: (label: string) => void;
}) => {
return (
<div className="plot-legend">
{Object.entries(labels).map(([label, color]) => (

View File

@ -51,7 +51,7 @@ import {
StandardizedLLMResponse,
TemplateVarInfo,
} from "./backend/typing";
import { AlertModalHandles } from "./AlertModal";
import { AlertModalRef } from "./AlertModal";
import { Status } from "./StatusIndicatorComponent";
const getUniqueLLMMetavarKey = (responses: StandardizedLLMResponse[]) => {
@ -199,7 +199,7 @@ const PromptNode = ({ data, id, type: node_type }) => {
const [llmItemsCurrState, setLLMItemsCurrState] = useState<LLMSpec[]>([]);
// For displaying error messages to user
const alertModal = useRef<AlertModalHandles | null>(null);
const alertModal = useRef<AlertModalRef | null>(null);
// For a way to inspect responses without having to attach a dedicated node
const inspectModal = useRef(null);
@ -441,7 +441,7 @@ const PromptNode = ({ data, id, type: node_type }) => {
fill_history: info.fill_history,
metavars: info.metavars,
llm: info?.llm?.name,
batch_id: uuid(),
uid: uuid(),
};
});
@ -873,7 +873,7 @@ Soft failing by replacing undefined with empty strings.`,
llm: _llmItemsCurrState.find(
(item) => item.name === resp_obj.llm,
),
batch_id: resp_obj.uid,
uid: resp_obj.uid,
};
// Carry over any metavars

View File

@ -10,63 +10,62 @@ export interface RenameValueModalProps {
onSubmit?: (val: string) => void;
}
export interface RenameValueModalHandles {
export interface RenameValueModalRef {
trigger: (msg?: string) => void;
}
/** Modal that lets user rename a single value, using a TextInput field. */
const RenameValueModal = forwardRef<
RenameValueModalHandles,
RenameValueModalProps
>(function RenameValueModal({ initialValue, title, label, onSubmit }, ref) {
const [opened, { open, close }] = useDisclosure(false);
const form = useForm({
initialValues: {
value: initialValue,
},
validate: {
value: (v) =>
v.trim().length > 0
? null
: "Column names must have at least one character",
},
});
const RenameValueModal = forwardRef<RenameValueModalRef, RenameValueModalProps>(
function RenameValueModal({ initialValue, title, label, onSubmit }, ref) {
const [opened, { open, close }] = useDisclosure(false);
const form = useForm({
initialValues: {
value: initialValue,
},
validate: {
value: (v) =>
v.trim().length > 0
? null
: "Column names must have at least one character",
},
});
useEffect(() => {
form.setValues({ value: initialValue });
}, [initialValue]);
useEffect(() => {
form.setValues({ value: initialValue });
}, [initialValue]);
// This gives the parent access to triggering the modal alert
const trigger = () => {
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
// This gives the parent access to triggering the modal alert
const trigger = () => {
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
return (
<Modal opened={opened} onClose={close} title={title}>
<Box maw={300} mx="auto">
<form
onSubmit={form.onSubmit((values) => {
if (onSubmit) onSubmit(values.value);
close();
})}
>
<TextInput
withAsterisk
label={label}
autoFocus={true}
{...form.getInputProps("value")}
/>
return (
<Modal opened={opened} onClose={close} title={title}>
<Box maw={300} mx="auto">
<form
onSubmit={form.onSubmit((values) => {
if (onSubmit) onSubmit(values.value);
close();
})}
>
<TextInput
withAsterisk
label={label}
autoFocus={true}
{...form.getInputProps("value")}
/>
<Group position="right" mt="md">
<Button type="submit">Submit</Button>
</Group>
</form>
</Box>
</Modal>
);
});
<Group position="right" mt="md">
<Button type="submit">Submit</Button>
</Group>
</form>
</Box>
</Modal>
);
},
);
export default RenameValueModal;

View File

@ -12,8 +12,8 @@ import {
import TemplateHooks from "./TemplateHooksComponent";
import BaseNode from "./BaseNode";
import NodeLabel from "./NodeLabelComponent";
import AlertModal, { AlertModalHandles } from "./AlertModal";
import RenameValueModal, { RenameValueModalHandles } from "./RenameValueModal";
import AlertModal, { AlertModalRef } from "./AlertModal";
import RenameValueModal, { RenameValueModalRef } from "./RenameValueModal";
import useStore from "./store";
import { sampleRandomElements } from "./backend/utils";
import { Dict, TabularDataRowType, TabularDataColType } from "./backend/typing";
@ -91,10 +91,10 @@ const TabularDataNode: React.FC<TabularDataNodeProps> = ({ data, id }) => {
const [hooksY, setHooksY] = useState(120);
// For displaying error messages to user
const alertModal = useRef<AlertModalHandles>(null);
const alertModal = useRef<AlertModalRef>(null);
// For renaming a column
const renameColumnModal = useRef<RenameValueModalHandles>(null);
const renameColumnModal = useRef<RenameValueModalRef>(null);
const [renameColumnInitialVal, setRenameColumnInitialVal] = useState<
TabularDataColType | string
>("");

View File

@ -187,7 +187,9 @@ function load_from_cache(storageKey: string): Dict {
return StorageCache.get(storageKey) || {};
}
function load_cache_responses(storageKey: string): Array<Dict> {
function load_cache_responses(
storageKey: string,
): Dict<StandardizedLLMResponse[]> | StandardizedLLMResponse[] {
const data = load_from_cache(storageKey);
if (Array.isArray(data)) return data;
else if (typeof data === "object" && data.responses_last_run !== undefined) {
@ -1228,9 +1230,9 @@ export async function evalWithLLM(
return { error: `Did not find cache file for id ${cache_id}` };
// Load the raw responses from the cache + clone them all:
const resp_objs = load_cache_responses(fname).map((r) =>
JSON.parse(JSON.stringify(r)),
) as StandardizedLLMResponse[];
const resp_objs = (
load_cache_responses(fname) as StandardizedLLMResponse[]
).map((r) => JSON.parse(JSON.stringify(r))) as StandardizedLLMResponse[];
if (resp_objs.length === 0) continue;
// We need to keep track of the index of each response in the response object.
@ -1337,15 +1339,17 @@ export async function evalWithLLM(
* @returns If success, a Dict with a single key, 'responses', with an array of StandardizedLLMResponse objects
* If failure, a Dict with a single key, 'error', with the error message.
*/
export async function grabResponses(responses: Array<string>): Promise<Dict> {
export async function grabResponses(
responses: string[],
): Promise<StandardizedLLMResponse[]> {
// Grab all responses with the given ID:
let grabbed_resps: Dict[] = [];
let grabbed_resps: StandardizedLLMResponse[] = [];
for (const cache_id of responses) {
const storageKey = `${cache_id}.json`;
if (!StorageCache.has(storageKey))
return { error: `Did not find cache data for id ${cache_id}` };
throw new Error(`Did not find cache data for id ${cache_id}`);
let res: Dict | Array<{ [key: string]: Dict }> =
let res: StandardizedLLMResponse[] | Dict<StandardizedLLMResponse[]> =
load_cache_responses(storageKey);
if (typeof res === "object" && !Array.isArray(res)) {
// Convert to standard response format
@ -1356,7 +1360,7 @@ export async function grabResponses(responses: Array<string>): Promise<Dict> {
grabbed_resps = grabbed_resps.concat(res);
}
return { responses: grabbed_resps };
return grabbed_resps;
}
/**

View File

@ -1,6 +1,4 @@
export class DuplicateVariableNameError extends Error {
variable: string;
constructor(variable: string) {
super();
this.name = "DuplicateVariableNameError";

View File

@ -201,7 +201,11 @@ export type LLMResponsesByVarDict = Dict<
(BaseLLMResponseObject | StandardizedLLMResponse)[]
>;
export type EvaluatedResponsesResults = ({responses?: StandardizedLLMResponse[], logs?: string[], error?: string});
export type EvaluatedResponsesResults = {
responses?: StandardizedLLMResponse[];
logs?: string[];
error?: string;
};
/** The outputs of prompt nodes, text fields or other data passed internally in the front-end and to the PromptTemplate backend.
* Used to populate prompt templates and carry variables/metavariables along the chain. */
@ -210,10 +214,19 @@ export interface TemplateVarInfo {
fill_history: Dict<string>;
metavars?: Dict<string>;
associate_id?: string;
prompt?: string;
uid?: ResponseUID;
llm?: string | LLMSpec;
chat_history?: ChatHistory;
}
export type VarsContext =
| {
vars: string[];
metavars: string[];
}
| object;
export type PromptVarType = string | TemplateVarInfo;
export type PromptVarsDict = {
[key: string]: PromptVarType[];
@ -229,3 +242,5 @@ export type TabularDataColType = {
key: string;
header: string;
};
export type PythonInterpreter = "flask" | "pyodide";

View File

@ -17,9 +17,9 @@ import {
GeminiChatContext,
GeminiChatMessage,
StandardizedLLMResponse,
BaseLLMResponseObject,
LLMResponsesByVarDict,
Func,
VarsContext,
} from "./typing";
import { v4 as uuid } from "uuid";
import { StringTemplate } from "./template";
@ -1682,8 +1682,8 @@ export const getLLMsInPulledInputData = (pulled_data: Dict) => {
};
export const stripLLMDetailsFromResponses = (
resps: (StandardizedLLMResponse | BaseLLMResponseObject)[],
) =>
resps: StandardizedLLMResponse[],
): StandardizedLLMResponse[] =>
resps.map((r) => ({
...r,
llm: typeof r?.llm === "string" ? r?.llm : r?.llm?.name ?? "undefined",
@ -1834,11 +1834,11 @@ export function sampleRandomElements(arr: any[], num_sample: number): any[] {
return Array.from(idxs).map((idx) => arr[idx]);
}
export const getVarsAndMetavars = (input_data: Dict) => {
export const getVarsAndMetavars = (input_data: Dict): VarsContext => {
// Find all vars and metavars in the input data (if any):
// NOTE: The typing is purposefully general for some backwards compatibility concenrs.
const varnames = new Set();
const metavars = new Set();
const varnames = new Set<string>();
const metavars = new Set<string>();
const add_from_resp_obj = (resp_obj: Dict) => {
if (typeof resp_obj === "string") return;