mirror of
https://github.com/langchain-ai/langchain-extract.git
synced 2026-07-01 20:24:03 -04:00
@@ -1,183 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
Table,
|
||||
TableCaption,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import Form from "@rjsf/chakra-ui";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import {
|
||||
useCreateExample,
|
||||
useDeleteExample,
|
||||
useGetExtractor,
|
||||
useListExamples,
|
||||
} from "../utils/api";
|
||||
|
||||
type ExtractorProps = {
|
||||
extractorId: string;
|
||||
isShared: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to view and manage an extractor.
|
||||
*/
|
||||
const Examples = ({ extractorId }: { extractorId: string }) => {
|
||||
const listExamplesQuery = useListExamples({
|
||||
extractor_id: extractorId,
|
||||
limit: 10,
|
||||
offset: 0, // Hard code pagination for now
|
||||
});
|
||||
const useDeleteMutation = useDeleteExample();
|
||||
if (listExamplesQuery.isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
if (listExamplesQuery.isError) {
|
||||
return (
|
||||
<div>Unable to load examples for extractor with ID: {extractorId}</div>
|
||||
);
|
||||
}
|
||||
const data = listExamplesQuery.data;
|
||||
return (
|
||||
<div className="w-5/5">
|
||||
{data && data.length === 0 ? (
|
||||
<Text>Use the form below to add examples to the extractor</Text>
|
||||
) : (
|
||||
<Table variant="simple">
|
||||
<TableCaption placement={"top"}>
|
||||
View and Delete Examples
|
||||
</TableCaption>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Content</Th>
|
||||
<Th>Output</Th>
|
||||
<Th>Action</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data &&
|
||||
data.map((example) => (
|
||||
<Tr key={example.uuid}>
|
||||
<Td>{example.content}</Td>
|
||||
<Td>
|
||||
<SyntaxHighlighter language="json" style={docco}>
|
||||
{JSON.stringify(example.output, null, 2)}
|
||||
</SyntaxHighlighter>
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
useDeleteMutation.mutate({ uuid: example.uuid })
|
||||
}
|
||||
variant={"outline"}
|
||||
isLoading={useDeleteMutation.isPending}
|
||||
colorScheme="red"
|
||||
aria-label="Delete example"
|
||||
icon={<TrashIcon />}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExampleEditor = ({ extractorId, isShared }: ExtractorProps) => {
|
||||
const { data, isLoading, isError } = useGetExtractor(extractorId, isShared);
|
||||
const createExampleMutation = useCreateExample();
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
if (isError) {
|
||||
return <div>Unable to load extractor with ID: {extractorId}</div>;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
const newSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
content: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
},
|
||||
output: {
|
||||
// Array of data.schema
|
||||
type: "array",
|
||||
items: {
|
||||
...data.schema,
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["content"],
|
||||
};
|
||||
|
||||
const UISchema = {
|
||||
content: {
|
||||
"ui:autoFocus": true,
|
||||
"ui:placeholder": "Enter contact to extract",
|
||||
"ui:widget": "textarea",
|
||||
"ui:description": "Enter the text to extract information from",
|
||||
},
|
||||
output: {
|
||||
"ui:autoFocus": false,
|
||||
"ui:title": "Outputs",
|
||||
},
|
||||
};
|
||||
|
||||
const handleAddExample = (
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
formData: any,
|
||||
) => {
|
||||
createExampleMutation.mutate({
|
||||
extractor_id: extractorId,
|
||||
content: formData.formData.content,
|
||||
output: formData.formData.output,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mr-auto">
|
||||
<Examples extractorId={extractorId} />
|
||||
<Box position="relative" padding="10">
|
||||
<Divider />
|
||||
<AbsoluteCenter bg="white" px="4">
|
||||
<Text>Add</Text>
|
||||
</AbsoluteCenter>
|
||||
</Box>
|
||||
<Form
|
||||
// @ts-expect-error - Need to investigate
|
||||
schema={newSchema}
|
||||
uiSchema={UISchema}
|
||||
validator={validator}
|
||||
onSubmit={(formData) => handleAddExample(formData)}
|
||||
>
|
||||
<div>
|
||||
<Button type="submit">Add Example</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import Form from "@rjsf/chakra-ui";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { useGetExtractor } from "../utils/api";
|
||||
|
||||
type ExtractorProps = {
|
||||
extractorId: string;
|
||||
isShared: boolean;
|
||||
};
|
||||
|
||||
export const Extractor = ({ extractorId, isShared }: ExtractorProps) => {
|
||||
const { data, isLoading, isError } = useGetExtractor(extractorId, isShared);
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
if (isError) {
|
||||
return <div>Unable to load extractor with ID: {extractorId}</div>;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mr-auto">
|
||||
<Tabs className="mt-5" variant={"enclosed"} colorScheme="blue" size="sm">
|
||||
<TabList>
|
||||
<Tab>Form</Tab>
|
||||
<Tab>Code</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Form schema={data.schema} validator={validator}>
|
||||
{true} {/* Disables the submit button */}
|
||||
</Form>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Text className="mt-1 mb-5">
|
||||
This shows the raw JSON Schema that describes what information the
|
||||
extractor will be extracting from the content.
|
||||
</Text>
|
||||
<SyntaxHighlighter language="json" style={docco}>
|
||||
{JSON.stringify(data.schema, null, 2)}
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +1,29 @@
|
||||
"use client";
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Heading,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Spinner,
|
||||
Tab,
|
||||
Box,
|
||||
Divider,
|
||||
AbsoluteCenter,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Text,
|
||||
Textarea,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
} from "@chakra-ui/react";
|
||||
import Form from "@rjsf/chakra-ui";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { runExtraction, useConfiguration, useGetExtractor } from "../utils/api";
|
||||
import { ExampleEditor } from "./ExampleEditor";
|
||||
import { runExtraction, useConfiguration } from "../utils/api";
|
||||
import { Extractor } from "./Extractor";
|
||||
import { ResultsTable } from "./ResultsTable";
|
||||
|
||||
interface PlaygroundProps {
|
||||
@@ -48,8 +44,6 @@ export const Playground = (props: PlaygroundProps) => {
|
||||
mutationFn: runExtraction,
|
||||
});
|
||||
|
||||
const extractorQuery = useGetExtractor(extractorId, isShared);
|
||||
|
||||
const requestServerConfig = useConfiguration();
|
||||
const [isDisabled, setIsDisabled] = React.useState(true);
|
||||
|
||||
@@ -103,163 +97,97 @@ export const Playground = (props: PlaygroundProps) => {
|
||||
setIsDisabled(false);
|
||||
};
|
||||
|
||||
if (extractorQuery.isLoading) {
|
||||
return (
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor="gray.200"
|
||||
color="blue.500"
|
||||
size="xl"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (extractorQuery.isError) {
|
||||
return <div>Unable to load extractor with ID: {extractorId}</div>;
|
||||
}
|
||||
|
||||
if (extractorQuery.data === undefined) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
// Project the schema without the title, name, description
|
||||
const cleanedSchema = {
|
||||
...extractorQuery.data.schema,
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex-col justify-between">
|
||||
<div className="m-auto">
|
||||
<Heading size="md" className="m-auto w-4/5" textAlign="center">
|
||||
{extractorQuery.data?.name}
|
||||
{isShared && (
|
||||
<Badge
|
||||
colorScheme="orange"
|
||||
fontSize="xl"
|
||||
fontWeight={"bold"}
|
||||
marginLeft={2}
|
||||
>
|
||||
SHARED
|
||||
</Badge>
|
||||
{isShared && <Heading>Using a shared exractor</Heading>}
|
||||
<div>
|
||||
<Extractor extractorId={extractorId} isShared={isShared} />
|
||||
</div>
|
||||
<Heading>Extract</Heading>
|
||||
|
||||
<form
|
||||
className="m-auto flex flex-col content-between gap-5 mt-10 mb-10"
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{requestServerConfig.isFetched && (
|
||||
<FormControl as="fieldset">
|
||||
<FormLabel as="legend">Extraction Model</FormLabel>
|
||||
<RadioGroup
|
||||
name="modelId"
|
||||
defaultValue={requestServerConfig.data?.models[0].name}
|
||||
>
|
||||
<HStack spacing="24px">
|
||||
{requestServerConfig.data?.models.map((model) => (
|
||||
<Radio value={model.name} key={model.name}>
|
||||
{model.description}
|
||||
</Radio>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
</Heading>
|
||||
<Text className="m-auto w-4/5" textAlign="center">
|
||||
{extractorQuery.data?.description}
|
||||
</Text>
|
||||
<Tabs variant={"enclosed"} colorScheme="blue" size="sm" marginTop={2}>
|
||||
{requestServerConfig.isFetched && (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
accept={requestServerConfig.data?.accepted_mimetypes.join(", ")}
|
||||
color="blue"
|
||||
className="border-2 border-dashed border-gray-300 rounded-md p-4 w-full file:mr-4"
|
||||
/>
|
||||
<Text fontSize="xs">
|
||||
Max file size is: {requestServerConfig.data?.max_file_size_mb}MB
|
||||
</Text>
|
||||
<Text fontSize="xs">
|
||||
Supported mimetypes:{" "}
|
||||
{requestServerConfig.data?.accepted_mimetypes.join(", ")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Box position="relative" padding="10">
|
||||
<Divider />
|
||||
<AbsoluteCenter bg="white" px="4">
|
||||
OR
|
||||
</AbsoluteCenter>
|
||||
</Box>
|
||||
<Textarea
|
||||
placeholder="Enter text to extract information from..."
|
||||
name="text"
|
||||
className="textarea textarea-bordered h-3/4"
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button type="submit" isDisabled={isDisabled}>
|
||||
Run
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="m-auto">
|
||||
{data?.content_too_long && (
|
||||
<Text color={"red"} margin={5}>
|
||||
The content was too long to be processed. Extraction was run on a
|
||||
truncated version of the content.
|
||||
</Text>
|
||||
)}
|
||||
<Tabs variant={"enclosed"} colorScheme="blue" size="sm">
|
||||
<TabList>
|
||||
<Tab>Extract</Tab>
|
||||
<Tab>JSON Schema</Tab>
|
||||
<Tab isDisabled={isShared}>Examples</Tab>
|
||||
<Tab>Table</Tab>
|
||||
<Tab>JSON</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Form schema={cleanedSchema} validator={validator}>
|
||||
{true} {/* Disables the submit button */}
|
||||
</Form>
|
||||
<form
|
||||
className="m-auto flex flex-col content-between gap-5 mt-2 mb-10"
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{requestServerConfig.isFetched && (
|
||||
<FormControl as="fieldset" marginTop={5}>
|
||||
<FormLabel as="legend">Model For Extraction</FormLabel>
|
||||
<RadioGroup
|
||||
name="modelId"
|
||||
defaultValue={requestServerConfig.data?.models[0].name}
|
||||
>
|
||||
<HStack spacing="24px">
|
||||
{requestServerConfig.data?.models.map((model) => (
|
||||
<Radio value={model.name} key={model.name}>
|
||||
{model.description}
|
||||
</Radio>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
{requestServerConfig.isFetched && (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
accept={requestServerConfig.data?.accepted_mimetypes.join(
|
||||
", ",
|
||||
)}
|
||||
color="blue"
|
||||
className="border-2 border-dashed border-gray-300 rounded-md p-4 w-full file:mr-4"
|
||||
/>
|
||||
<Text fontSize="xs">
|
||||
Max file size is:{" "}
|
||||
{requestServerConfig.data?.max_file_size_mb}MB
|
||||
</Text>
|
||||
<Text fontSize="xs">
|
||||
Supported mimetypes:{" "}
|
||||
{requestServerConfig.data?.accepted_mimetypes.join(", ")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Box position="relative" padding="10">
|
||||
<Divider />
|
||||
<AbsoluteCenter bg="white" px="4">
|
||||
OR
|
||||
</AbsoluteCenter>
|
||||
</Box>
|
||||
<Textarea
|
||||
placeholder="Enter text to extract information from..."
|
||||
name="text"
|
||||
className="textarea textarea-bordered h-3/4"
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button type="submit" isDisabled={isDisabled}>
|
||||
Run
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="m-auto">
|
||||
{data?.content_too_long && (
|
||||
<Text color={"red"} margin={5}>
|
||||
The content was too long to be processed. Extraction was run
|
||||
on a truncated version of the content.
|
||||
</Text>
|
||||
)}
|
||||
<Tabs variant={"enclosed"} colorScheme="blue" size="sm">
|
||||
<TabList>
|
||||
<Tab>Table</Tab>
|
||||
<Tab>JSON</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<ResultsTable data={data} isPending={isPending} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Text className="mt-1 mb-5">
|
||||
Shows the output from the extractor in JSON format.
|
||||
</Text>
|
||||
<SyntaxHighlighter language="json" style={docco}>
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
<ResultsTable data={data} isPending={isPending} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Text className="mt-1 mb-5">
|
||||
The raw JSON schema for the extractor.
|
||||
Shows the output from the extractor in JSON format.
|
||||
</Text>
|
||||
<SyntaxHighlighter language="json" style={docco}>
|
||||
{JSON.stringify(extractorQuery.data.schema, null, 2)}
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ExampleEditor extractorId={extractorId} isShared={isShared} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -181,96 +181,3 @@ export const useCreateExtractor = ({
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type CreateExampleRequest = {
|
||||
extractor_id: string;
|
||||
content: string;
|
||||
// Any can be any JSON serializable object
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
output: any[];
|
||||
};
|
||||
|
||||
type CreateExampleResponse = {
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
const createExample: MutationFunction<
|
||||
CreateExampleResponse,
|
||||
CreateExampleRequest
|
||||
> = async (example) => {
|
||||
const baseUrl = getBaseApiUrl();
|
||||
const response = await axiosClient.post(`${baseUrl}/examples`, example);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useCreateExample = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createExample,
|
||||
onSuccess: () => {
|
||||
// TDOO: invalidate only for the extractor ID asscoiated with the example
|
||||
// that was deleted.
|
||||
queryClient.invalidateQueries({ queryKey: ["listExamples"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
type DeleteExampleParams = {
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
const deleteExample = async ({ uuid }: DeleteExampleParams): Promise<void> => {
|
||||
const baseUrl: string = getBaseApiUrl();
|
||||
await axiosClient.delete(`${baseUrl}/examples/${uuid}`);
|
||||
};
|
||||
|
||||
export const useDeleteExample = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteExample,
|
||||
onSuccess: () => {
|
||||
// TDOO: invalidate only for the extractor ID asscoiated with the example
|
||||
// that was deleted.
|
||||
queryClient.invalidateQueries({ queryKey: ["listExamples"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type Example = {
|
||||
uuid: string;
|
||||
content: string;
|
||||
output: any[];
|
||||
};
|
||||
|
||||
type ListExamplesParams = {
|
||||
extractor_id: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
const fetchExamples = async ({
|
||||
queryKey,
|
||||
}: {
|
||||
queryKey: [string, string, number, number];
|
||||
}): Promise<Example[]> => {
|
||||
const [, extractor_id, limit = 10, offset = 0] = queryKey;
|
||||
const baseUrl = getBaseApiUrl();
|
||||
const queryParams: string = new URLSearchParams({
|
||||
extractor_id,
|
||||
limit: limit.toString(),
|
||||
offset: offset.toString(),
|
||||
}).toString();
|
||||
const response = await axiosClient.get(`${baseUrl}/examples?${queryParams}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useListExamples = (params: ListExamplesParams) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
"listExamples",
|
||||
params.extractor_id,
|
||||
params.limit,
|
||||
params.offset,
|
||||
],
|
||||
queryFn: fetchExamples,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user