Merge remote-tracking branch 'origin/form_io' into python_custom_node_component

This commit is contained in:
gustavoschaedler 2023-06-28 16:33:54 +01:00
commit 91f8573bed
27 changed files with 217 additions and 1048 deletions

View file

@ -26,8 +26,9 @@ def remove_api_keys(flow: dict):
def build_input_keys_response(langchain_object):
"""Build the input keys response."""
input_keys_response = {
"input_keys": langchain_object.input_keys,
"input_keys": {key: "" for key in langchain_object.input_keys},
"memory_keys": [],
}
# If the object has memory, that memory will have a memory_variables attribute
@ -44,4 +45,7 @@ def build_input_keys_response(langchain_object):
# Add memory variables to memory_keys
input_keys_response["memory_keys"] = langchain_object.memory.memory_variables
if hasattr(langchain_object, "prompt"):
input_keys_response["template"] = langchain_object.prompt.template
return input_keys_response

View file

@ -43,7 +43,7 @@ class ChatMessage(BaseModel):
"""Chat message schema."""
is_bot: bool = False
message: Union[str, None] = None
message: Union[str, None, dict] = None
type: str = "human"

View file

@ -31,21 +31,36 @@ def post_validate_code(code: Code):
def post_validate_prompt(prompt: ValidatePromptRequest):
try:
input_variables = validate_prompt(prompt.template)
# Reinitialize custom_fields
old_custom_fields = prompt.frontend_node.custom_fields.copy()
prompt.frontend_node.custom_fields = []
# Add new variables to the template
for variable in input_variables:
try:
template_field = TemplateField(
name=variable, field_type="str", show=True, advanced=False
name=variable,
field_type="str",
show=True,
advanced=False,
input_types=["BaseLoader"],
)
prompt.frontend_node.template[variable] = template_field.to_dict()
prompt.frontend_node.custom_fields.append(variable)
for key in prompt.frontend_node.template:
if key not in input_variables:
prompt.frontend_node.template.pop(key, None)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
# Remove variables that are not in the template anymore
for variable in old_custom_fields:
if variable not in input_variables:
try:
prompt.frontend_node.template.pop(variable, None)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
return PromptValidationResponse(
input_variables=input_variables,
frontend_node=prompt.frontend_node,

View file

@ -111,9 +111,9 @@ class ChatManager:
self, client_id: str, payload: Dict, langchain_object: Any
):
# Process the graph data and chat message
chat_message = payload.pop("message", "")
chat_message = ChatMessage(message=chat_message)
self.chat_history.add_message(client_id, chat_message)
chat_inputs = payload.pop("inputs", "")
chat_inputs = ChatMessage(message=chat_inputs)
self.chat_history.add_message(client_id, chat_inputs)
# graph_data = payload
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
@ -126,7 +126,7 @@ class ChatManager:
result, intermediate_steps = await process_graph(
langchain_object=langchain_object,
chat_message=chat_message,
chat_inputs=chat_inputs,
websocket=self.active_connections[client_id],
)
except Exception as e:

View file

@ -7,7 +7,7 @@ from langflow.utils.logger import logger
async def process_graph(
langchain_object,
chat_message: ChatMessage,
chat_inputs: ChatMessage,
websocket: WebSocket,
):
langchain_object = try_setting_streaming_options(langchain_object, websocket)
@ -23,7 +23,7 @@ async def process_graph(
try:
logger.debug("Generating result and thought")
result, intermediate_steps = await get_result_and_steps(
langchain_object, chat_message.message or "", websocket=websocket
langchain_object, chat_inputs.message or "", websocket=websocket
)
logger.debug("Generated result and intermediate_steps")
return result, intermediate_steps

View file

@ -6,23 +6,12 @@ from langflow.processing.process import fix_memory_inputs, format_actions
from langflow.utils.logger import logger
async def get_result_and_steps(langchain_object, message: str, **kwargs):
async def get_result_and_steps(langchain_object, inputs: dict, **kwargs):
"""Get result and thought from extracted json"""
try:
if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True
chat_input = None
memory_key = ""
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
memory_key = langchain_object.memory.memory_key
if hasattr(langchain_object, "input_keys"):
for key in langchain_object.input_keys:
if key not in [memory_key, "chat_history"]:
chat_input = {key: message}
else:
chat_input = message # type: ignore
if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068
@ -33,12 +22,12 @@ async def get_result_and_steps(langchain_object, message: str, **kwargs):
fix_memory_inputs(langchain_object)
try:
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
output = await langchain_object.acall(chat_input, callbacks=async_callbacks)
output = await langchain_object.acall(inputs, callbacks=async_callbacks)
except Exception as exc:
# make the error message more informative
logger.debug(f"Error: {str(exc)}")
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
output = langchain_object(chat_input, callbacks=sync_callbacks)
output = langchain_object(inputs, callbacks=sync_callbacks)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []

View file

@ -21,6 +21,7 @@ class TemplateFieldCreator(BaseModel, ABC):
name: str = ""
display_name: Optional[str] = None
advanced: bool = False
input_types: list[str] = []
def to_dict(self):
result = self.dict()

View file

@ -78,7 +78,6 @@ export default function GenericNode({
}
useEffect(() => {}, [closePopUp, data.node.template]);
console.log({data})
return (
<>
<NodeToolbar>

View file

@ -1,21 +0,0 @@
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
import { ProgressBarType } from "../../types/components";
import { Progress } from "../../components/ui/progress";
import { progressContext } from "../../contexts/ProgressContext";
import { setInterval } from "timers/promises";
export default function ProgressBarComponent({
value,
children,
}: ProgressBarType) {
const ref = useRef(0);
const reff = useRef();
const { progress } = useContext(progressContext);
useEffect(() => {
ref.current = progress * 100;
console.log(progress);
}, [progress]);
return <Progress className="h-2.5" value={ref.current} />;
}

View file

@ -1,51 +0,0 @@
import { Transition } from "@headlessui/react";
import { FormInput, MessagesSquare } from "lucide-react";
import { alertContext } from "../../../contexts/alertContext";
import { useContext } from "react";
export default function FormTrigger({ open, setOpen, isBuilt }) {
const { setErrorData } = useContext(alertContext);
function handleClick() {
if (isBuilt) {
setOpen(true);
} else {
setErrorData({
title: "Flow not built",
list: ["Please build the flow before chatting"],
});
}
}
return (
<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="fixed bottom-36 right-4">
<div
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
onClick={handleClick}
>
<button>
<div className="flex gap-3">
<FormInput
className="pth-6 w-6 stroke-2 stroke-[#5c8be1]"
style={{ color: "white" }}
strokeWidth={1.5}
/>
</div>
</button>
</div>
</div>
</Transition>
);
}

View file

@ -3,11 +3,9 @@ import { useNodes } from "reactflow";
import { ChatType } from "../../types/chat";
import ChatTrigger from "./chatTrigger";
import BuildTrigger from "./buildTrigger";
import ChatModal from "../../modals/chatModal";
import { getBuildStatus } from "../../controllers/API";
import { NodeType } from "../../types/flow";
import FormTrigger from "./formTrigger";
import FormModal from "../../modals/formModal";
export default function Chat({ flow }: ChatType) {
@ -46,10 +44,11 @@ export default function Chat({ flow }: ChatType) {
const prevNodesRef = useRef<any[] | undefined>();
const nodes = useNodes();
useEffect(() => {
const prevNodes = prevNodesRef.current;
const currentNodes = nodes.map(
(node: NodeType) => node.data.node.template.value
);
(node: NodeType) => node.data.node.template
);
if (
prevNodes &&
@ -66,15 +65,13 @@ export default function Chat({ flow }: ChatType) {
{isBuilt ? (
<div>
<BuildTrigger
open={open || openForm}
open={open}
flow={flow}
setIsBuilt={setIsBuilt}
isBuilt={isBuilt}
/>
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
<FormModal key={flow.id + "form"} flow={flow} open={openForm} setOpen={setOpenForm} />
<ChatTrigger open={open || openForm} setOpen={setOpen} isBuilt={isBuilt} />
<FormTrigger open={open || openForm} setOpen={setOpenForm} isBuilt={isBuilt} />
<FormModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
<ChatTrigger open={open} setOpen={setOpen} isBuilt={isBuilt} />
</div>
) : (
<BuildTrigger

View file

@ -41,7 +41,6 @@ export default function IntComponent({
if (disableCopyPaste) setDisableCopyPaste(false);
}}
onKeyDown={(event) => {
// console.log(event);
if (
event.key !== "Backspace" &&
event.key !== "Enter" &&

View file

@ -73,6 +73,8 @@ export default function PromptAreaComponent({
setMyValue(t);
onChange(t);
}}
nodeClass={nodeClass}
setNodeClass={setNodeClass}
/>
);
}}

View file

@ -3,18 +3,25 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../utils";
const badgeVariants = cva(
"inline-flex items-center border rounded-full px-2.5 h-6 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center border rounded-full px-2.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
gray:
"bg-border hover:bg-border/80 text-secondary-foreground",
secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-foreground",
},
size: {
sm: "h-4 text-xs",
md: "h-5 text-sm",
lg: "h-6 text-base",
}
},
defaultVariants: {
variant: "default",
@ -26,9 +33,9 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
function Badge({ className, variant, size, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
<div className={cn(badgeVariants({ variant, size }), className)} {...props} />
);
}

View file

@ -22,6 +22,13 @@ export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
export const CODE_DIALOG_SUBTITLE =
"Export your flow to use it with this code.";
/**
* The base text for subtitle of Chat Form
* @constant
*/
export const CHAT_FORM_DIALOG_SUBTITLE =
"Set up the input variables defined in prompt templates. Interact with agents and chains.";
/**
* The base text for subtitle of Edit Node Dialog
* @constant

View file

@ -87,12 +87,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
Saveflows.forEach((flow) => {
if (flow.data && flow.data?.nodes)
flow.data?.nodes.forEach((node) => {
// console.log(node.data.type);
//looking for file fields to prevent saving the content and breaking the flow for exceeding the the data limite for local storage
Object.keys(node.data.node.template).forEach((key) => {
// console.log(node.data.node.template[key].type);
if (node.data.node.template[key].type === "file") {
// console.log(node.data.node.template[key]);
node.data.node.template[key].content = null;
node.data.node.template[key].value = "";
}

View file

@ -1,78 +0,0 @@
import { classNames } from "../../../utils";
import { useContext, useEffect, useRef, useState } from "react";
import { TabsContext } from "../../../contexts/tabsContext";
import { INPUT_STYLE } from "../../../constants";
import { Lock, Send } from "lucide-react";
export default function ChatInput({
lockChat,
chatValue,
sendMessage,
setChatValue,
inputRef,
}) {
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
useEffect(() => {
if (inputRef.current) {
inputRef.current.style.height = "inherit"; // Reset the height
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
}
}, [chatValue]);
return (
<div className="relative">
<textarea
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => {
setChatValue(e.target.value);
}}
className={classNames(
lockChat
? " bg-input text-black dark:bg-gray-700 dark:text-gray-300"
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" +
INPUT_STYLE
)}
placeholder={"Send a message..."}
/>
<div className="absolute bottom-0.5 right-3">
<button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<Lock
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true"
/>
) : (
<Send
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true"
/>
)}
</button>
</div>
</div>
);
}

View file

@ -1,82 +0,0 @@
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
import { FC, memo, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { programmingLanguages } from "../../../../utils";
interface Props {
language: string;
value: string;
}
export const CodeBlock: FC<Props> = memo(({ language, value }) => {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
});
};
const downloadAsFile = () => {
const fileExtension = programmingLanguages[language] || ".file";
const suggestedFileName = `${"generated-code"}${fileExtension}`;
const fileName = window.prompt("enter file name", suggestedFileName);
if (!fileName) {
// user pressed cancel on prompt
return;
}
const blob = new Blob([value], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = fileName;
link.href = url;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return (
<div className="codeblock font-sans text-[16px]">
<div className="flex items-center justify-between py-1.5 px-4">
<span className="text-xs lowercase text-white">{language}</span>
<div className="flex items-center">
<button
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
onClick={copyToClipboard}
>
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
{isCopied ? "Copied!" : "Copy code"}
</button>
<button
className="flex items-center rounded bg-none p-1 text-xs text-white"
onClick={downloadAsFile}
>
<IconDownload size={18} />
</button>
</div>
</div>
<SyntaxHighlighter
className=" w-[570px]"
language={language}
style={oneDark}
customStyle={{ margin: 0 }}
>
{value}
</SyntaxHighlighter>
</div>
);
});
CodeBlock.displayName = "CodeBlock";

View file

@ -1,166 +0,0 @@
import { useEffect, useRef, useState } from "react";
import { ChatMessageType } from "../../../types/chat";
import { classNames } from "../../../utils";
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
import AiIconStill from "../../../assets/froze-flow.png";
import FileCard from "../fileComponent";
import ReactMarkdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { CodeBlock } from "./codeBlock";
import Convert from "ansi-to-html";
import { User2, MessageCircle } from "lucide-react";
export default function ChatMessage({
chat,
lockChat,
lastMessage,
}: {
chat: ChatMessageType;
lockChat: boolean;
lastMessage: boolean;
}) {
const convert = new Convert({ newline: true });
const [message, setMessage] = useState("");
const imgRef = useRef(null);
useEffect(() => {
setMessage(chat.message);
}, [chat.message]);
const [hidden, setHidden] = useState(true);
return (
<div
className={classNames(
"w-full py-2 pl-2 flex",
chat.isSend
? "bg-background dark:bg-gray-900 "
: "bg-input dark:bg-gray-800"
)}
>
<div
className={classNames(
"rounded-full overflow-hidden w-8 h-8 flex items-center my-3 justify-center"
)}
>
{!chat.isSend && (
<div className="relative w-8 h-8">
<img
className={
"absolute transition-opacity duration-500 scale-150 " +
(lockChat ? "opacity-100" : "opacity-0")
}
src={lastMessage ? AiIcon : AiIconStill}
/>
<img
className={
"absolute transition-opacity duration-500 scale-150 " +
(lockChat ? "opacity-0" : "opacity-100")
}
src={AiIconStill}
/>
</div>
)}
{chat.isSend && (
<User2 className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" />
)}
</div>
{!chat.isSend ? (
<div className="w-full text-start flex items-center">
<div className="w-full relative text-start inline-block text-gray-600 dark:text-gray-300 text-sm font-normal">
{hidden && chat.thought && chat.thought !== "" && (
<div
onClick={() => setHidden((prev) => !prev)}
className="absolute -top-1 -left-2 cursor-pointer"
>
<MessageCircle className="w-5 h-5 animate-bounce dark:text-white" />
</div>
)}
{chat.thought && chat.thought !== "" && !hidden && (
<div
onClick={() => setHidden((prev) => !prev)}
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
bg-muted dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
dangerouslySetInnerHTML={{
__html: convert.toHtml(chat.thought),
}}
></div>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="w-full px-4 pb-3 pt-3 pr-8">
<div className="dark:text-white w-full">
<div className="w-full">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
className="markdown prose dark:prose-invert text-gray-600 dark:text-gray-200"
components={{
code({ node, inline, className, children, ...props }) {
if (children.length) {
if (children[0] == "▍") {
return (
<span className="animate-pulse cursor-default mt-1">
▍
</span>
);
}
children[0] = (children[0] as string).replace(
"`▍`",
"▍"
);
}
const match = /language-(\w+)/.exec(className || "");
return !inline ? (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ""}
value={String(children).replace(/\n$/, "")}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{message}
</ReactMarkdown>
</div>
{chat.files && (
<div className="my-2 w-full">
{chat.files.map((file, index) => {
return (
<div key={index} className="my-2 w-full">
<FileCard
fileName={"Generated File"}
fileType={file.data_type}
content={file.data}
/>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</div>
) : (
<div className="w-full flex items-center">
<div className="text-start inline-block px-3 text-gray-600 dark:text-white">
<span
className="text-gray-600 dark:text-gray-200"
dangerouslySetInnerHTML={{
__html: message.replace(/\n/g, "<br>"),
}}
></span>
</div>
</div>
)}
</div>
);
}

View file

@ -1,79 +0,0 @@
import * as base64js from "base64-js";
import { useState } from "react";
import { DownloadCloud, File } from "lucide-react";
export default function FileCard({ fileName, content, fileType }) {
const handleDownload = () => {
const byteArray = new Uint8Array(base64js.toByteArray(content));
const blob = new Blob([byteArray], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName + ".png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const [isHovered, setIsHovered] = useState(false);
function handleMouseEnter() {
setIsHovered(true);
}
function handleMouseLeave() {
setIsHovered(false);
}
if (fileType === "image") {
return (
<div
className="relative w-1/4 h-1/4"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<img
src={`data:image/png;base64,${content}`}
alt="generated image"
className="rounded-lg w-full h-full"
/>
{isHovered && (
<div
className={`absolute top-0 right-0 bg-muted text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
>
<button
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
onClick={handleDownload}
>
<DownloadCloud className="hover:scale-110 w-5 h-5 text-current" />
</button>
</div>
)}
</div>
);
}
return (
<button
onClick={handleDownload}
className="bg-muted shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
>
<div className="flex gap-2 text-current items-center w-full mr-2">
{" "}
{fileType === "image" ? (
<img
src={`data:image/png;base64,${content}`}
alt=""
className="w-8 h-8"
/>
) : (
<File className="w-8 h-8" />
)}
<div className="flex flex-col items-start">
{" "}
<div className="truncate text-sm text-current">{fileName}</div>
<div className="truncate text-xs text-gray-500">{fileType}</div>
</div>
<DownloadCloud className="w-6 h-6 text-current ml-auto" />
</div>
</button>
);
}

Some files were not shown because too many files have changed in this diff Show more