diff --git a/.gitignore b/.gitignore index 0e4052c2a..91c0adfc5 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,4 @@ dist __pycache__/ models *.egg-info -.hypothesis \ No newline at end of file +.hypothesis diff --git a/python/src/aiconfig/editor/client/src/LocalEditor.tsx b/python/src/aiconfig/editor/client/src/LocalEditor.tsx index bbd66bc46..1e47426b0 100644 --- a/python/src/aiconfig/editor/client/src/LocalEditor.tsx +++ b/python/src/aiconfig/editor/client/src/LocalEditor.tsx @@ -132,6 +132,12 @@ export default function LocalEditor() { return await ufetch.post(ROUTE_TABLE.CLEAR_OUTPUTS, {}); }, []); + const deleteOutput = useCallback(async (promptName: string) => { + return await ufetch.post(ROUTE_TABLE.DELETE_OUTPUT, { + prompt_name: promptName, + }); + }, []); + const runPrompt = useCallback( async ( promptName: string, @@ -249,6 +255,7 @@ export default function LocalEditor() { addPrompt, cancel, clearOutputs, + deleteOutput, deleteModelSettings, deletePrompt, getModels, diff --git a/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx b/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx index 653759114..04e2b78b2 100644 --- a/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx +++ b/python/src/aiconfig/editor/client/src/components/AIConfigEditor.tsx @@ -122,6 +122,7 @@ export type AIConfigCallbacks = { ) => Promise<{ aiconfig: AIConfig }>; cancel: (cancellationToken: string) => Promise; clearOutputs: () => Promise<{ aiconfig: AIConfig }>; + deleteOutput: (promptName: string) => Promise<{ aiconfig: AIConfig }>; deleteModelSettings?: (modelName: string) => Promise; deletePrompt: (promptName: string) => Promise; download?: () => Promise; @@ -811,6 +812,39 @@ function AIConfigEditorBase({ [deletePromptCallback, dispatch, logEventHandler, showNotification] ); + const deleteOutputCallback = callbacks?.deleteOutput; + const onDeleteOutput = useCallback( + async (promptId: string) => { + if (!deleteOutputCallback) { + // Just no-op if no callback specified. We could technically perform + // client-side updates but that might be confusing + return; + } + + dispatch({ + type: "DELETE_OUTPUT", + id: promptId, + }); + logEventHandler?.("DELETE_OUTPUT"); + + try { + const prompt = getPrompt(stateRef.current, promptId); + if (!prompt) { + throw new Error(`Could not find prompt with id ${promptId}`); + } + await deleteOutputCallback(prompt.name); + } catch (err: unknown) { + const message = (err as RequestCallbackError).message ?? null; + showNotification({ + title: "Error deleting output for prompt", + message, + type: "error", + }); + } + }, + [deleteOutputCallback, dispatch, logEventHandler, showNotification] + ); + const clearOutputsCallback = callbacks?.clearOutputs; const onClearOutputs = useCallback(async () => { if (!clearOutputsCallback) { @@ -1236,6 +1270,7 @@ function AIConfigEditorBase({ onChangePromptInput={onChangePromptInput} onChangePromptName={onChangePromptName} onDeletePrompt={onDeletePrompt} + onDeleteOutput={onDeleteOutput} onRunPrompt={onRunPrompt} onUpdatePromptMetadata={onUpdatePromptMetadata} onUpdatePromptModel={onUpdatePromptModel} diff --git a/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx b/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx index 86985f18a..dd287a89a 100644 --- a/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx +++ b/python/src/aiconfig/editor/client/src/components/prompt/PromptContainer.tsx @@ -5,12 +5,14 @@ import { ClientPrompt } from "../../shared/types"; import { getPromptSchema } from "../../utils/promptUtils"; import { Flex, Card, createStyles } from "@mantine/core"; import { PromptInput as AIConfigPromptInput, JSONObject } from "aiconfig"; -import { memo, useCallback, useEffect, useMemo, useRef } from "react"; +import { memo, useCallback, useContext, useEffect, useMemo, useRef } from "react"; import PromptOutputBar from "./PromptOutputBar"; import PromptName from "./PromptName"; import ModelSelector from "./ModelSelector"; import { DEBOUNCE_MS } from "../../utils/constants"; import { debounce } from "lodash"; +import PromptMenuButton from "./PromptMenuButton"; +import AIConfigContext from "../../contexts/AIConfigContext"; type Props = { prompt: ClientPrompt; @@ -22,6 +24,8 @@ type Props = { ) => void; onChangePromptName: (promptId: string, newName: string) => void; onRunPrompt(promptId: string): Promise; + onDeletePrompt(promptId: string): void; + onDeleteOutput(promptId: string): void; onUpdateModel: (promptId: string, newModel?: string) => void; onUpdateModelSettings: ( promptId: string, @@ -65,6 +69,8 @@ export default memo(function PromptContainer({ onChangePromptName, defaultConfigModelName, onRunPrompt, + onDeletePrompt, + onDeleteOutput, onUpdateModel, onUpdateModelSettings, onUpdateParameters, @@ -72,6 +78,7 @@ export default memo(function PromptContainer({ isRunButtonDisabled = false, }: Props) { const { classes } = useStyles(); + const { readOnly } = useContext(AIConfigContext); const promptId = prompt._ui.id; const onChangeInput = useCallback( (newInput: AIConfigPromptInput) => onChangePromptInput(promptId, newInput), @@ -104,6 +111,16 @@ export default memo(function PromptContainer({ [promptId, onRunPrompt] ); + const deletePrompt = useCallback( + async () => await onDeletePrompt(promptId), + [promptId, onDeletePrompt] + ); + + const deleteOutput = useCallback( + async () => await onDeleteOutput(promptId), + [promptId, onDeleteOutput] + ); + const onCancelRun = useCallback(async () => { if (!cancel) { return; @@ -158,54 +175,65 @@ export default memo(function PromptContainer({ const inputSchema = promptSchema?.input; return ( - - - - - - - - - - {prompt.outputs && prompt.outputs.length > 0 && ( - <> - - - - )} - - -
- + {!readOnly && ( + -
-
+ )} + + + + + + + + + + {prompt.outputs && prompt.outputs.length > 0 && ( + <> + + + + + + )} + + +
+ +
+
+ ); }); diff --git a/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx b/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx index 23e2e41b5..b5f963783 100644 --- a/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx +++ b/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx @@ -1,5 +1,5 @@ import { Button, Menu, createStyles } from "@mantine/core"; -import { IconDotsVertical, IconTrash } from "@tabler/icons-react"; +import { IconDotsVertical, IconEraser, IconTrash } from "@tabler/icons-react"; import { memo } from "react"; const useStyles = createStyles(() => ({ @@ -9,11 +9,13 @@ const useStyles = createStyles(() => ({ })); export default memo(function PromptMenuButton({ - promptId, + showDeleteOutput, + onDeleteOutput, onDeletePrompt, }: { - promptId: string; - onDeletePrompt: (id: string) => void; + showDeleteOutput: boolean; + onDeletePrompt: () => void; + onDeleteOutput: () => void; }) { const { classes } = useStyles(); @@ -31,10 +33,18 @@ export default memo(function PromptMenuButton({ + {showDeleteOutput ? ( + } + onClick={onDeleteOutput} + > + Clear Output + + ) : null} } color="red" - onClick={() => onDeletePrompt(promptId)} + onClick={onDeletePrompt} > Delete Prompt diff --git a/python/src/aiconfig/editor/client/src/components/prompt/PromptsContainer.tsx b/python/src/aiconfig/editor/client/src/components/prompt/PromptsContainer.tsx index 4ddc3b49a..56f711f06 100644 --- a/python/src/aiconfig/editor/client/src/components/prompt/PromptsContainer.tsx +++ b/python/src/aiconfig/editor/client/src/components/prompt/PromptsContainer.tsx @@ -3,7 +3,6 @@ import { memo, useContext } from "react"; import AIConfigContext from "../../contexts/AIConfigContext"; import AddPromptButton from "./AddPromptButton"; import { ClientPrompt } from "../../shared/types"; -import PromptMenuButton from "./PromptMenuButton"; import PromptContainer from "./PromptContainer"; import { JSONObject, PromptInput } from "aiconfig"; @@ -18,6 +17,7 @@ type Props = { ) => Promise; onChangePromptName: (promptId: string, newName: string) => Promise; onDeletePrompt: (promptId: string) => Promise; + onDeleteOutput: (promptId: string) => Promise; onRunPrompt: (promptId: string) => Promise; onUpdatePromptMetadata: ( promptId: string, @@ -58,18 +58,14 @@ export default memo(function PromptsContainer(props: Props) { /> )} {props.prompts.map((prompt: ClientPrompt, i: number) => { + const promptId = prompt._ui.id; const isAnotherPromptRunning = props.runningPromptId !== undefined && - props.runningPromptId !== prompt._ui.id; + props.runningPromptId !== promptId; + return ( - {!readOnly && ( - props.onDeletePrompt(prompt._ui.id)} - /> - )} { + return { + ...statePrompt, + outputs: undefined, + }; + } + + return reduceReplacePrompt( + dirtyState, + action.id, + clearOutput + ); + } + case "DELETE_GLOBAL_MODEL_SETTINGS": { const newModels = { ...state.metadata.models }; delete newModels[action.modelName]; diff --git a/python/src/aiconfig/editor/client/src/shared/types.ts b/python/src/aiconfig/editor/client/src/shared/types.ts index 4d9eddf91..f1575e306 100644 --- a/python/src/aiconfig/editor/client/src/shared/types.ts +++ b/python/src/aiconfig/editor/client/src/shared/types.ts @@ -66,6 +66,7 @@ export function aiConfigToClientConfig(aiconfig: AIConfig): ClientAIConfig { export type LogEvent = | "ADD_PROMPT" | "CLEAR_OUTPUTS" + | "DELETE_OUTPUT" | "DELETE_GLOBAL_MODEL_SETTINGS" | "DELETE_PROMPT" | "DOWNLOAD_BUTTON_CLICKED" diff --git a/python/src/aiconfig/editor/client/src/utils/api.ts b/python/src/aiconfig/editor/client/src/utils/api.ts index fe07b9b5a..d8495ef09 100644 --- a/python/src/aiconfig/editor/client/src/utils/api.ts +++ b/python/src/aiconfig/editor/client/src/utils/api.ts @@ -12,6 +12,7 @@ export const ROUTE_TABLE = { ADD_PROMPT: urlJoin(API_ENDPOINT, "/add_prompt"), CANCEL: urlJoin(API_ENDPOINT, "/cancel"), CLEAR_OUTPUTS: urlJoin(API_ENDPOINT, "/clear_outputs"), + DELETE_OUTPUT: urlJoin(API_ENDPOINT, "/delete_output"), DELETE_MODEL: urlJoin(API_ENDPOINT, "/delete_model"), DELETE_PROMPT: urlJoin(API_ENDPOINT, "/delete_prompt"), GET_AICONFIGRC: urlJoin(API_ENDPOINT, "/get_aiconfigrc"), diff --git a/python/src/aiconfig/editor/server/server.py b/python/src/aiconfig/editor/server/server.py index 2c7918157..9374cf284 100644 --- a/python/src/aiconfig/editor/server/server.py +++ b/python/src/aiconfig/editor/server/server.py @@ -1,18 +1,22 @@ import asyncio import copy import ctypes -import dotenv import json import logging +import os import threading import time import uuid import webbrowser -import os from typing import Any, Dict, Literal, Type, Union +import dotenv import lastmile_utils.lib.core.api as core_utils import result +from flask import Flask, Response, request, stream_with_context +from flask_cors import CORS +from result import Err, Ok, Result + from aiconfig.Config import AIConfigRuntime from aiconfig.editor.server.queue_iterator import ( STOP_STREAMING_SIGNAL, @@ -42,10 +46,6 @@ ) from aiconfig.model_parser import InferenceOptions from aiconfig.registry import ModelParserRegistry -from flask import Flask, Response, request, stream_with_context -from flask_cors import CORS -from result import Err, Ok, Result - from aiconfig.schema import ExecuteResult, Output, Prompt, PromptMetadata logging.getLogger("werkzeug").disabled = True @@ -788,6 +788,7 @@ def clear_outputs() -> FlaskResponse: """ Clears all outputs in the server state's AIConfig. """ + method_name = MethodName("clear_outputs") state = get_server_state(app) aiconfig = state.aiconfig request_json = request.get_json() @@ -811,9 +812,32 @@ def _op( signature: dict[str, Type[Any]] = {} return run_aiconfig_operation_with_request_json( - aiconfig, request_json, f"method_", _op, signature + aiconfig, request_json, method_name, _op, signature ) +@app.route("/api/delete_output", methods=["POST"]) +def delete_output() -> FlaskResponse: + """ + Clears a single outputs in the server state's AIConfig based on prompt name. + """ + method_name = MethodName("delete_output") + state = get_server_state(app) + aiconfig = state.aiconfig + request_json = request.get_json() + + if aiconfig is None: + LOGGER.info("No AIConfig in memory, can't run clear outputs.") + return HttpResponseWithAIConfig( + message="No AIConfig in memory, can't run clear outputs.", + code=400, + aiconfig=None, + ).to_flask_format() + + signature: dict[str, Type[Any]] = {"prompt_name": str} + operation = make_op_run_method(method_name) + return run_aiconfig_operation_with_request_json( + aiconfig, request_json, method_name, operation, signature + ) @app.route("/api/get_aiconfigrc", methods=["GET"]) def get_aiconfigrc() -> FlaskResponse: