docs(next): ask ai using llamacloud (#1400)

This commit is contained in:
Alex Yang
2024-10-27 00:01:55 -05:00
committed by GitHub
parent 98ba1e71bd
commit efb7e1b868
39 changed files with 1878 additions and 455 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@llamaindex/core": patch
"llamaindex": patch
---
refactor: move `RetrieverQueryEngine` into core module
+17 -3
View File
@@ -6,35 +6,49 @@
"build": "pnpm run build:docs && next build",
"dev": "next dev",
"start": "next start",
"postinstall": "fumadocs-mdx",
"postdev": "fumadocs-mdx",
"postbuild": "fumadocs-mdx",
"build:docs": "node ./scripts/generate-docs.mjs"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^10.1.0",
"@llamaindex/cloud": "workspace:*",
"@llamaindex/core": "workspace:*",
"@llamaindex/openai": "workspace:*",
"@mdx-js/mdx": "^3.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"ai": "^3.3.21",
"class-variance-authority": "^0.7.0",
"clsx": "2.1.1",
"foxact": "^0.2.39",
"framer-motion": "^11.11.10",
"fumadocs-core": "14.0.2",
"fumadocs-docgen": "^1.3.0",
"fumadocs-mdx": "11.0.0",
"fumadocs-openapi": "^5.5.3",
"fumadocs-twoslash": "^2.0.0",
"fumadocs-ui": "14.0.2",
"hast-util-to-jsx-runtime": "^2.3.2",
"lucide-react": "^0.436.0",
"next": "15.0.0",
"next": "15.0.1",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-text-transition": "^3.1.0",
"react-use-measure": "^2.1.1",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"rimraf": "^6.0.1",
"shiki": "^1.22.0",
"shiki-magic-move": "^0.5.0",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
+111 -1
View File
@@ -1,7 +1,117 @@
import { PipelinesService } from "@llamaindex/cloud/api";
import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
import { fileGenerator, remarkDocGen, remarkInstall } from "fumadocs-docgen";
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
import { transformerTwoslash } from "fumadocs-twoslash";
import { relative } from "node:path";
import { fileURLToPath } from "node:url";
import rehypeKatex from "rehype-katex";
import remarkMath from "remark-math";
const baseDir = fileURLToPath(new URL("../src/content", import.meta.url));
export const { docs, meta } = defineDocs({
dir: "./src/content/docs",
});
export default defineConfig();
export default defineConfig({
lastModifiedTime: "git",
mdxOptions: {
rehypeCodeOptions: {
inline: "tailing-curly-colon",
themes: {
light: "catppuccin-latte",
dark: "catppuccin-mocha",
},
transformers: [
...(rehypeCodeDefaultOptions.transformers ?? []),
transformerTwoslash(),
{
name: "transformers:remove-notation-escape",
code(hast) {
for (const line of hast.children) {
if (line.type !== "element") continue;
const lastSpan = line.children.findLast(
(v) => v.type === "element",
);
const head = lastSpan?.children[0];
if (head?.type !== "text") return;
head.value = head.value.replace(/\[\\!code/g, "[!code");
}
},
},
],
},
remarkPlugins: [
remarkMath,
[remarkInstall, { persist: { id: "package-manager" } }],
[remarkDocGen, { generators: [fileGenerator()] }],
() => {
return (_, file, next) => {
const metadata = file.data.frontmatter as Record<string, unknown>;
const title = metadata.title as string;
const description = metadata.description as string;
let content: string;
if (file.value instanceof Uint8Array) {
content = file.value.toString();
} else {
content = file.value;
}
if (file.path.includes("content/docs/cloud/api")) {
// skip cloud api docs
return next();
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NODE_ENV === "development") {
// skip development
return next();
}
if (!title || !description) {
throw new Error(`Missing title or description in ${file.path}`);
}
const id = relative(baseDir, file.path);
if (
// eslint-disable-next-line turbo/no-undeclared-env-vars
process.env.LLAMA_CLOUD_UPSERT_PIPELINE_DOCUMENTS === "true" &&
// eslint-disable-next-line turbo/no-undeclared-env-vars
process.env.LLAMA_CLOUD_PIPELINE_ID !== undefined
) {
PipelinesService.upsertBatchPipelineDocumentsApiV1PipelinesPipelineIdDocumentsPut(
{
baseUrl: "https://api.cloud.llamaindex.ai/",
body: [
{
metadata: {
title,
description,
documentUrl: id,
},
text: content,
id,
},
],
path: {
// eslint-disable-next-line turbo/no-undeclared-env-vars
pipeline_id: process.env.LLAMA_CLOUD_PIPELINE_ID,
},
throwOnError: true,
headers: {
// eslint-disable-next-line turbo/no-undeclared-env-vars
Authorization: `Bearer ${process.env.LLAMA_CLOUD_API_KEY}`,
},
},
).catch((error) => {
console.error(error);
});
}
return next();
};
},
],
rehypePlugins: (v) => [rehypeKatex, ...v],
},
});
+107
View File
@@ -0,0 +1,107 @@
import { ClientMDXContent } from "@/components/mdx";
import { BotMessage } from "@/components/message";
import { Skeleton } from "@/components/ui/skeleton";
import { LlamaCloudRetriever } from "@/deps/cloud";
import { Settings } from "@llamaindex/core/global";
import { ChatMessage } from "@llamaindex/core/llms";
import { RetrieverQueryEngine } from "@llamaindex/core/query-engine";
import { OpenAI } from "@llamaindex/openai";
import { createAI, createStreamableUI, getMutableAIState } from "ai/rsc";
import { ReactNode } from "react";
Settings.llm = new OpenAI({
model: "gpt-4o",
});
const retriever = new LlamaCloudRetriever({
// eslint-disable-next-line turbo/no-undeclared-env-vars
apiKey: process.env.LLAMA_CLOUD_API_KEY!,
baseUrl: "https://api.cloud.llamaindex.ai/",
// eslint-disable-next-line turbo/no-undeclared-env-vars
pipelineId: process.env.LLAMA_CLOUD_PIPELINE_ID!,
});
const initialAIState = {
messages: [],
} as {
messages: ChatMessage[];
};
type UIMessage = {
id: number;
display: ReactNode;
};
const initialUIState = {
messages: [],
} as {
messages: UIMessage[];
};
const runAsyncFnWithoutBlocking = (fn: (...args: any) => Promise<any>) => {
fn().catch((error) => {
console.error(error);
});
};
export const AIProvider = createAI({
initialAIState,
initialUIState,
actions: {
query: async (message: string): Promise<UIMessage> => {
"use server";
const queryEngine = new RetrieverQueryEngine(retriever);
const id = Date.now();
const aiState = getMutableAIState<typeof AIProvider>();
aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{
role: "user",
content: message,
},
],
});
const ui = createStreamableUI(
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>,
);
runAsyncFnWithoutBlocking(async () => {
const response = await queryEngine.query({
query: message,
stream: true,
});
let content = "";
for await (const { delta } of response) {
content += delta;
ui.update(<ClientMDXContent id={id} content={content} />);
}
ui.done();
aiState.done({
...aiState.get(),
messages: [
...aiState.get().messages,
{
role: "assistant",
content,
},
],
});
});
return {
id,
display: <BotMessage>{ui.value}</BotMessage>,
};
},
},
});
+26 -1
View File
@@ -1,11 +1,36 @@
import { baseOptions } from "@/app/layout.config";
import { AITrigger } from "@/components/ai-chat";
import { buttonVariants } from "@/components/ui/button";
import { source } from "@/lib/source";
import { cn } from "@/lib/utils";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { MessageCircle } from "lucide-react";
import type { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions}>
<DocsLayout
tree={source.pageTree}
{...baseOptions}
nav={{
...baseOptions.nav,
children: (
<AITrigger
className={cn(
buttonVariants({
variant: "secondary",
size: "xs",
className:
"md:flex-1 px-2 ms-2 gap-1.5 text-fd-muted-foreground rounded-full",
}),
)}
>
<MessageCircle className="size-3" />
Ask LlamaCloud
</AITrigger>
),
}}
>
{children}
</DocsLayout>
);
+13 -1
View File
@@ -1,5 +1,16 @@
import { DOCUMENT_URL } from "@/lib/const";
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import Image from "next/image";
const logo = (
<Image
src="/logo-large.png"
alt="Logo"
className="size-8"
width={147}
height={147}
/>
);
/**
* Shared layout configurations
@@ -10,7 +21,8 @@ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
*/
export const baseOptions: BaseLayoutProps = {
nav: {
title: "LlamaIndex.TS",
title: logo,
transparentMode: "top",
},
githubUrl: "https://github.com/run-llama/LlamaIndexTS",
links: [
+7 -1
View File
@@ -1,3 +1,5 @@
import { AIProvider } from "@/actions";
import { TooltipProvider } from "@/components/ui/tooltip";
import { RootProvider } from "fumadocs-ui/provider";
import { Inter } from "next/font/google";
import type { ReactNode } from "react";
@@ -31,7 +33,11 @@ export default function Layout({ children }: { children: ReactNode }) {
/>
</head>
<body className="flex flex-col min-h-screen">
<RootProvider>{children}</RootProvider>
<TooltipProvider>
<AIProvider>
<RootProvider>{children}</RootProvider>
</AIProvider>
</TooltipProvider>
</body>
</html>
);
+143
View File
@@ -0,0 +1,143 @@
"use client";
import type { AIProvider } from "@/actions";
import { UserMessage } from "@/components/message";
import { useActions, useUIState } from "ai/rsc";
import { Info } from "lucide-react";
import { ButtonHTMLAttributes, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { Button } from "./ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Textarea } from "./ui/textarea";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
type AITriggerProps = ButtonHTMLAttributes<HTMLButtonElement>;
function ChatList({ messages }: { messages: any[] }) {
if (messages.length === 0) {
return null;
}
return (
<div className="relative mx-auto w-full px-4">
{messages.map((message, index) => (
<div key={index} className="pb-4">
{message.display}
</div>
))}
</div>
);
}
export const AITrigger = (props: AITriggerProps) => {
const [{ messages }, setUIState] = useUIState<typeof AIProvider>();
const { query } = useActions<typeof AIProvider>();
const [inputValue, setInputValue] = useState("");
return (
<Dialog>
<DialogTrigger {...props} />
<DialogPortal>
<DialogOverlay className="fixed inset-0 z-50 bg-fd-background/50 backdrop-blur-sm data-[state=closed]:animate-fd-fade-out data-[state=open]:animate-fd-fade-in" />
<DialogContent
onOpenAutoFocus={(e) => {
document.getElementById("nd-ai-input")?.focus();
e.preventDefault();
}}
className="fixed left-1/2 z-50 my-[5vh] flex max-h-[90dvh] w-[98vw] max-w-[860px] origin-left -translate-x-1/2 flex-col rounded-lg border bg-fd-popover text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[state=closed]:animate-fd-dialog-out data-[state=open]:animate-fd-dialog-in"
>
<DialogHeader>
<DialogTitle className="sr-only">Search AI</DialogTitle>
<DialogDescription className="sr-only">
Ask AI some questions.
</DialogDescription>
<Alert>
<Info className="size-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
Answers from LlamaCloud may be inaccurate, please use with
discretion.
</AlertDescription>
</Alert>
</DialogHeader>
<div className="overflow-scroll flex-grow mt-4">
<ChatList messages={messages} />
</div>
<form
className="px-4 py-2 space-y-4"
action={async () => {
const value = inputValue.trim();
setInputValue("");
if (!value) return;
// Add user message UI
setUIState((state) => ({
...state,
messages: [
...state.messages,
{
id: Date.now(),
display: <UserMessage>{value}</UserMessage>,
},
],
}));
try {
// Submit and get response message
const responseMessage = await query(value);
setUIState((state) => ({
...state,
messages: [...state.messages, responseMessage],
}));
} catch (error) {
// You may want to show a toast or trigger an error state.
console.error(error);
}
}}
>
<div className="flex flex-row w-full items-center gap-2">
<Textarea
tabIndex={0}
placeholder="Ask AI about documentation."
className="w-full resize-none bg-transparent px-4 focus-within:outline-none sm:text-sm"
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
event.currentTarget.form?.requestSubmit(null);
}
}}
autoFocus
spellCheck={false}
autoComplete="off"
autoCorrect="off"
name="message"
rows={1}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
size="icon"
disabled={inputValue === ""}
>
<span className="sr-only">Send message</span>
</Button>
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
</div>
</form>
</DialogContent>
</DialogPortal>
</Dialog>
);
};
+41
View File
@@ -0,0 +1,41 @@
"use client";
import { createProcessor, run } from "@mdx-js/mdx";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { ReactNode, useDeferredValue } from "react";
import * as runtime from "react/jsx-runtime";
import useSWR from "swr";
const processor = createProcessor({
outputFormat: "function-body",
});
export function ClientMDXContent({
content,
id,
}: {
content: string;
id: number;
}): ReactNode {
const deferredContent = useDeferredValue(content);
const { data: jsx } = useSWR(["mdx", id, deferredContent], {
fetcher: async () => {
const code = await processor
.process(deferredContent)
.then((vfile) => vfile.value);
const { default: Content } = await run(code, {
...runtime,
baseUrl: import.meta.url,
});
return (
<Content
components={{
...defaultMdxComponents,
}}
/>
);
},
suspense: true,
});
return jsx;
}
+78
View File
@@ -0,0 +1,78 @@
"use client";
import { cn } from "@/lib/utils";
import Image from "next/image";
import { ReactNode } from "react";
import { IconAI, IconUser } from "./ui/icons";
export function UserMessage({ children }: { children: ReactNode }) {
return (
<div className="group relative flex items-start">
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow-sm bg-background">
<IconUser />
</div>
<div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
{children}
</div>
</div>
);
}
export function BotMessage({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("group relative flex items-start", className)}>
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow-sm">
<Image
src="/logo-large.png"
alt="Logo"
className="size-4"
width={147}
height={147}
/>
</div>
<div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
{children}
</div>
</div>
);
}
export function BotCard({
children,
showAvatar = true,
}: {
children: ReactNode;
showAvatar?: boolean;
}) {
return (
<div className="group relative flex items-start md:-ml-12">
<div
className={cn(
"flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow-sm bg-primary text-primary-foreground",
!showAvatar && "invisible",
)}
>
<IconAI />
</div>
<div className="ml-4 flex-1 px-1">{children}</div>
</div>
);
}
export function SystemMessage({ children }: { children: React.ReactNode }) {
return (
<div
className={
"mt-2 flex items-center justify-center gap-2 text-xs text-gray-500"
}
>
<div className={"max-w-[600px] flex-initial px-2 py-2"}>{children}</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertDescription, AlertTitle };
+1
View File
@@ -24,6 +24,7 @@ const buttonVariants = cva(
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
xs: "px-1.5 py-0.5 text-xs",
icon: "h-9 w-9",
},
},
+122
View File
@@ -0,0 +1,122 @@
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
+30
View File
@@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
export function IconAI({ className, ...props }: React.ComponentProps<"svg">) {
return (
<svg
fill="currentColor"
viewBox="0 0 256 256"
role="img"
xmlns="http://www.w3.org/2000/svg"
className={cn("h-4 w-4", className)}
{...props}
>
<path d="M197.58,129.06l-51.61-19-19-51.65a15.92,15.92,0,0,0-29.88,0L78.07,110l-51.65,19a15.92,15.92,0,0,0,0,29.88L78,178l19,51.62a15.92,15.92,0,0,0,29.88,0l19-51.61,51.65-19a15.92,15.92,0,0,0,0-29.88ZM140.39,163a15.87,15.87,0,0,0-9.43,9.43l-19,51.46L93,172.39A15.87,15.87,0,0,0,83.61,163h0L32.15,144l51.46-19A15.87,15.87,0,0,0,93,115.61l19-51.46,19,51.46a15.87,15.87,0,0,0,9.43,9.43l51.46,19ZM144,40a8,8,0,0,1,8-8h16V16a8,8,0,0,1,16,0V32h16a8,8,0,0,1,0,16H184V64a8,8,0,0,1-16,0V48H152A8,8,0,0,1,144,40ZM248,88a8,8,0,0,1-8,8h-8v8a8,8,0,0,1-16,0V96h-8a8,8,0,0,1,0-16h8V72a8,8,0,0,1,16,0v8h8A8,8,0,0,1,248,88Z"></path>
</svg>
);
}
export function IconUser({ className, ...props }: React.ComponentProps<"svg">) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn("h-4 w-4", className)}
{...props}
>
<path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z" />
</svg>
);
}
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";
export { Textarea };
+32
View File
@@ -0,0 +1,32 @@
"use client";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
+96
View File
@@ -0,0 +1,96 @@
import {
type MetadataFilter,
type MetadataFilters,
PipelinesService,
type RetrievalParams,
type TextNodeWithScore,
} from "@llamaindex/cloud/api";
import { QueryBundle } from "@llamaindex/core/query-engine";
import { BaseRetriever } from "@llamaindex/core/retriever";
import { jsonToNode, NodeWithScore, ObjectType } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
export type CloudRetrieveParams = Omit<
RetrievalParams,
"query" | "search_filters" | "dense_similarity_top_k"
> & { similarityTopK?: number; filters?: MetadataFilters };
export type LlamaCloudRetrieverParams = {
apiKey: string;
baseUrl: string;
pipelineId: string;
} & CloudRetrieveParams;
export class LlamaCloudRetriever extends BaseRetriever {
baseUrl: string;
apiKey: string;
retrieveParams: CloudRetrieveParams;
organizationId?: string;
pipelineId: string;
private resultNodesToNodeWithScore(
nodes: TextNodeWithScore[],
): NodeWithScore[] {
return nodes.map((node: TextNodeWithScore) => {
const textNode = jsonToNode(node.node, ObjectType.TEXT);
textNode.metadata = {
...textNode.metadata,
...node.node.extra_info,
};
return {
node: textNode,
score: node.score ?? undefined,
};
});
}
private convertFilter(filters?: MetadataFilters): MetadataFilters | null {
if (!filters) return null;
const processFilter = (
filter: MetadataFilter | MetadataFilters,
): MetadataFilter | MetadataFilters => {
if ("filters" in filter) {
// type MetadataFilters
return { ...filter, filters: filter.filters.map(processFilter) };
}
return { ...filter, value: filter.value ?? null };
};
return { ...filters, filters: filters.filters.map(processFilter) };
}
constructor(params: LlamaCloudRetrieverParams) {
super();
this.baseUrl = params.baseUrl;
this.apiKey = params.apiKey;
this.retrieveParams = params;
this.pipelineId = params.pipelineId;
}
override async _retrieve(query: QueryBundle): Promise<NodeWithScore[]> {
const filters = this.convertFilter(this.retrieveParams.filters);
const pipelineId = this.pipelineId;
const { data: results } =
await PipelinesService.runSearchApiV1PipelinesPipelineIdRetrievePost({
throwOnError: true,
path: {
pipeline_id: pipelineId,
},
baseUrl: this.baseUrl,
body: {
...this.retrieveParams,
query: extractText(query),
search_filters: filters,
dense_similarity_top_k: this.retrieveParams.similarityTopK!,
},
headers: {
authorization: `Bearer ${this.apiKey}`,
},
});
return this.resultNodesToNodeWithScore(results.retrieval_nodes);
}
}
+16 -1
View File
@@ -297,6 +297,20 @@
"types": "./data-structs/dist/index.d.ts",
"default": "./data-structs/dist/index.js"
}
},
"./postprocessor": {
"require": {
"types": "./postprocessor/dist/index.d.cts",
"default": "./postprocessor/dist/index.cjs"
},
"import": {
"types": "./postprocessor/dist/index.d.ts",
"default": "./postprocessor/dist/index.js"
},
"default": {
"types": "./postprocessor/dist/index.d.ts",
"default": "./postprocessor/dist/index.js"
}
}
},
"files": [
@@ -320,7 +334,8 @@
"./retriever",
"./vector-store",
"./tools",
"./data-structs"
"./data-structs",
"./postprocessor"
],
"scripts": {
"dev": "bunchee --watch",
+1
View File
@@ -0,0 +1 @@
export type { BaseNodePostprocessor } from "./type";
@@ -1,5 +1,5 @@
import type { MessageContent } from "@llamaindex/core/llms";
import type { NodeWithScore } from "@llamaindex/core/schema";
import type { MessageContent } from "../llms";
import type { NodeWithScore } from "../schema";
export interface BaseNodePostprocessor {
/**
+4 -3
View File
@@ -36,9 +36,10 @@ export type QueryFn = (
) => Promise<AsyncIterable<EngineResponse> | EngineResponse>;
export abstract class BaseQueryEngine extends PromptMixin {
protected constructor(protected readonly _query: QueryFn) {
super();
}
abstract _query(
strOrQueryBundle: QueryType,
stream?: boolean,
): Promise<AsyncIterable<EngineResponse> | EngineResponse>;
async retrieve(params: QueryType): Promise<NodeWithScore[]> {
throw new Error(
+1
View File
@@ -1,2 +1,3 @@
export { BaseQueryEngine, type QueryBundle, type QueryType } from "./base";
export { RetrieverQueryEngine } from "./retriever";
export type { QueryEndEvent, QueryStartEvent } from "./type";
@@ -0,0 +1,91 @@
import type { MessageContent } from "../llms";
import type { BaseNodePostprocessor } from "../postprocessor";
import { BaseQueryEngine, type QueryType } from "../query-engine";
import {
type BaseSynthesizer,
getResponseSynthesizer,
} from "../response-synthesizers";
import { BaseRetriever } from "../retriever";
import type { NodeWithScore } from "../schema";
import { extractText } from "../utils";
export class RetrieverQueryEngine extends BaseQueryEngine {
retriever: BaseRetriever;
responseSynthesizer: BaseSynthesizer;
nodePostprocessors: BaseNodePostprocessor[];
constructor(
retriever: BaseRetriever,
responseSynthesizer?: BaseSynthesizer,
nodePostprocessors?: BaseNodePostprocessor[],
) {
super();
this.retriever = retriever;
this.responseSynthesizer =
responseSynthesizer || getResponseSynthesizer("compact");
this.nodePostprocessors = nodePostprocessors || [];
}
override async _query(strOrQueryBundle: QueryType, stream?: boolean) {
const nodesWithScore = await this.retrieve(
typeof strOrQueryBundle === "string"
? strOrQueryBundle
: extractText(strOrQueryBundle),
);
if (stream) {
return this.responseSynthesizer.synthesize(
{
query:
typeof strOrQueryBundle === "string"
? { query: strOrQueryBundle }
: strOrQueryBundle,
nodes: nodesWithScore,
},
true,
);
}
return this.responseSynthesizer.synthesize({
query:
typeof strOrQueryBundle === "string"
? { query: strOrQueryBundle }
: strOrQueryBundle,
nodes: nodesWithScore,
});
}
protected _getPrompts() {
return {};
}
protected _updatePrompts() {}
_getPromptModules() {
return {
responseSynthesizer: this.responseSynthesizer,
};
}
private async applyNodePostprocessors(
nodes: NodeWithScore[],
query: MessageContent,
) {
let nodesWithScore = nodes;
for (const postprocessor of this.nodePostprocessors) {
nodesWithScore = await postprocessor.postprocessNodes(
nodesWithScore,
query,
);
}
return nodesWithScore;
}
override async retrieve(query: QueryType) {
const nodes = await this.retriever.retrieve(query);
const messageContent = typeof query === "string" ? query : query.query;
return await this.applyNodePostprocessors(nodes, messageContent);
}
}
@@ -1,8 +1,8 @@
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { BaseQueryEngine } from "@llamaindex/core/query-engine";
import type { BaseSynthesizer } from "@llamaindex/core/response-synthesizers";
import type { Document } from "@llamaindex/core/schema";
import { RetrieverQueryEngine } from "../engines/query/RetrieverQueryEngine.js";
import type { BaseNodePostprocessor } from "../postprocessors/types.js";
import type { CloudRetrieveParams } from "./LlamaCloudRetriever.js";
import { LlamaCloudRetriever } from "./LlamaCloudRetriever.js";
import type { CloudConstructorParams } from "./type.js";
@@ -11,6 +11,7 @@ import type {
MessageType,
} from "@llamaindex/core/llms";
import { BaseMemory, ChatMemoryBuffer } from "@llamaindex/core/memory";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import {
type ContextSystemPrompt,
type ModuleRecord,
@@ -25,7 +26,6 @@ import {
streamReducer,
} from "@llamaindex/core/utils";
import { Settings } from "../../Settings.js";
import type { BaseNodePostprocessor } from "../../postprocessors/index.js";
import { DefaultContextGenerator } from "./DefaultContextGenerator.js";
import type { ContextGenerator } from "./types.js";
@@ -1,4 +1,5 @@
import type { MessageContent, MessageType } from "@llamaindex/core/llms";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import {
type ContextSystemPrompt,
defaultContextSystemPrompt,
@@ -8,7 +9,6 @@ import {
import { createMessageContent } from "@llamaindex/core/response-synthesizers";
import type { BaseRetriever } from "@llamaindex/core/retriever";
import { MetadataMode, type NodeWithScore } from "@llamaindex/core/schema";
import type { BaseNodePostprocessor } from "../../postprocessors/index.js";
import type { Context, ContextGenerator } from "./types.js";
export class DefaultContextGenerator
@@ -1,90 +1,4 @@
import type { MessageContent } from "@llamaindex/core/llms";
import { BaseQueryEngine, type QueryType } from "@llamaindex/core/query-engine";
import type { BaseSynthesizer } from "@llamaindex/core/response-synthesizers";
import { getResponseSynthesizer } from "@llamaindex/core/response-synthesizers";
import { BaseRetriever } from "@llamaindex/core/retriever";
import { type NodeWithScore } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import type { BaseNodePostprocessor } from "../../postprocessors/index.js";
/**
* A query engine that uses a retriever to query an index and then synthesizes the response.
* todo: this file should be removed in the major release
*/
export class RetrieverQueryEngine extends BaseQueryEngine {
retriever: BaseRetriever;
responseSynthesizer: BaseSynthesizer;
nodePostprocessors: BaseNodePostprocessor[];
constructor(
retriever: BaseRetriever,
responseSynthesizer?: BaseSynthesizer,
nodePostprocessors?: BaseNodePostprocessor[],
) {
super(async (strOrQueryBundle, stream) => {
const nodesWithScore = await this.retrieve(
typeof strOrQueryBundle === "string"
? strOrQueryBundle
: extractText(strOrQueryBundle),
);
if (stream) {
return this.responseSynthesizer.synthesize(
{
query:
typeof strOrQueryBundle === "string"
? { query: strOrQueryBundle }
: strOrQueryBundle,
nodes: nodesWithScore,
},
true,
);
}
return this.responseSynthesizer.synthesize({
query:
typeof strOrQueryBundle === "string"
? { query: strOrQueryBundle }
: strOrQueryBundle,
nodes: nodesWithScore,
});
});
this.retriever = retriever;
this.responseSynthesizer =
responseSynthesizer || getResponseSynthesizer("compact");
this.nodePostprocessors = nodePostprocessors || [];
}
protected _getPrompts() {
return {};
}
protected _updatePrompts() {}
_getPromptModules() {
return {
responseSynthesizer: this.responseSynthesizer,
};
}
private async applyNodePostprocessors(
nodes: NodeWithScore[],
query: MessageContent,
) {
let nodesWithScore = nodes;
for (const postprocessor of this.nodePostprocessors) {
nodesWithScore = await postprocessor.postprocessNodes(
nodesWithScore,
query,
);
}
return nodesWithScore;
}
override async retrieve(query: QueryType) {
const nodes = await this.retriever.retrieve(query);
const messageContent = typeof query === "string" ? query : query.query;
return await this.applyNodePostprocessors(nodes, messageContent);
}
}
export { RetrieverQueryEngine } from "@llamaindex/core/query-engine";
@@ -1,6 +1,7 @@
import {
BaseQueryEngine,
type QueryBundle,
type QueryType,
} from "@llamaindex/core/query-engine";
import {
BaseSynthesizer,
@@ -63,19 +64,7 @@ export class RouterQueryEngine extends BaseQueryEngine {
summarizer?: BaseSynthesizer | undefined;
verbose?: boolean | undefined;
}) {
super(async (strOrQueryBundle, stream) => {
const response = await this.queryRoute(
typeof strOrQueryBundle === "string"
? { query: strOrQueryBundle }
: strOrQueryBundle,
);
if (stream) {
throw new Error("Streaming is not supported yet.");
}
return response;
});
super();
this.selector = init.selector;
this.queryEngines = init.queryEngineTools.map((tool) => tool.queryEngine);
@@ -87,6 +76,20 @@ export class RouterQueryEngine extends BaseQueryEngine {
this.verbose = init.verbose ?? false;
}
override async _query(strOrQueryBundle: QueryType, stream?: boolean) {
const response = await this.queryRoute(
typeof strOrQueryBundle === "string"
? { query: strOrQueryBundle }
: strOrQueryBundle,
);
if (stream) {
throw new Error("Streaming is not supported yet.");
}
return response;
}
protected _getPrompts() {
return {};
}
@@ -9,6 +9,7 @@ import type { PromptsRecord } from "@llamaindex/core/prompts";
import {
BaseQueryEngine,
type QueryBundle,
type QueryType,
} from "@llamaindex/core/query-engine";
import type { BaseQuestionGenerator, SubQuestion } from "./types.js";
@@ -26,44 +27,7 @@ export class SubQuestionQueryEngine extends BaseQueryEngine {
responseSynthesizer: BaseSynthesizer;
queryEngineTools: BaseTool[];
}) {
super(async (strOrQueryBundle, stream) => {
let query: QueryBundle;
if (typeof strOrQueryBundle === "string") {
query = {
query: strOrQueryBundle,
};
} else {
query = strOrQueryBundle;
}
const subQuestions = await this.questionGen.generate(
this.metadatas,
strOrQueryBundle,
);
const subQNodes = await Promise.all(
subQuestions.map((subQ) => this.querySubQ(subQ)),
);
const nodesWithScore: NodeWithScore[] = subQNodes.filter(
(node) => node !== null,
);
if (stream) {
return this.responseSynthesizer.synthesize(
{
query,
nodes: nodesWithScore,
},
true,
);
}
return this.responseSynthesizer.synthesize(
{
query,
nodes: nodesWithScore,
},
false,
);
});
super();
this.questionGen = init.questionGen;
this.responseSynthesizer =
@@ -72,6 +36,45 @@ export class SubQuestionQueryEngine extends BaseQueryEngine {
this.metadatas = init.queryEngineTools.map((tool) => tool.metadata);
}
override async _query(strOrQueryBundle: QueryType, stream?: boolean) {
let query: QueryBundle;
if (typeof strOrQueryBundle === "string") {
query = {
query: strOrQueryBundle,
};
} else {
query = strOrQueryBundle;
}
const subQuestions = await this.questionGen.generate(
this.metadatas,
strOrQueryBundle,
);
const subQNodes = await Promise.all(
subQuestions.map((subQ) => this.querySubQ(subQ)),
);
const nodesWithScore: NodeWithScore[] = subQNodes.filter(
(node) => node !== null,
);
if (stream) {
return this.responseSynthesizer.synthesize(
{
query,
nodes: nodesWithScore,
},
true,
);
}
return this.responseSynthesizer.synthesize(
{
query,
nodes: nodesWithScore,
},
false,
);
}
protected _getPrompts(): PromptsRecord {
return {};
}
@@ -8,7 +8,6 @@ import { MetadataMode } from "@llamaindex/core/schema";
import type { ServiceContext } from "../../ServiceContext.js";
import { serviceContextFromDefaults } from "../../ServiceContext.js";
import { RetrieverQueryEngine } from "../../engines/query/index.js";
import type { BaseNodePostprocessor } from "../../postprocessors/index.js";
import type { StorageContext } from "../../storage/StorageContext.js";
import { storageContextFromDefaults } from "../../storage/StorageContext.js";
import type { BaseDocumentStore } from "../../storage/docStore/types.js";
@@ -23,6 +22,7 @@ import {
import { KeywordTable } from "@llamaindex/core/data-structs";
import type { LLM } from "@llamaindex/core/llms";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import {
defaultKeywordExtractPrompt,
defaultQueryKeywordExtractPrompt,
@@ -1,3 +1,4 @@
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import {
type ChoiceSelectPrompt,
defaultChoiceSelectPrompt,
@@ -19,7 +20,6 @@ import {
nodeParserFromSettingsOrContext,
} from "../../Settings.js";
import { RetrieverQueryEngine } from "../../engines/query/index.js";
import type { BaseNodePostprocessor } from "../../postprocessors/index.js";
import type { StorageContext } from "../../storage/StorageContext.js";
import { storageContextFromDefaults } from "../../storage/StorageContext.js";
import type {
@@ -3,6 +3,7 @@ import {
type BaseEmbedding,
} from "@llamaindex/core/embeddings";
import type { MessageContent } from "@llamaindex/core/llms";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { QueryBundle } from "@llamaindex/core/query-engine";
import type { BaseSynthesizer } from "@llamaindex/core/response-synthesizers";
import { BaseRetriever } from "@llamaindex/core/retriever";
@@ -26,7 +27,6 @@ import {
DocStoreStrategy,
createDocStoreStrategy,
} from "../../ingestion/strategies/index.js";
import type { BaseNodePostprocessor } from "../../postprocessors/types.js";
import type { StorageContext } from "../../storage/StorageContext.js";
import { storageContextFromDefaults } from "../../storage/StorageContext.js";
import type { BaseIndexStore } from "../../storage/indexStore/types.js";
@@ -1,6 +1,6 @@
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { NodeWithScore } from "@llamaindex/core/schema";
import { MetadataMode } from "@llamaindex/core/schema";
import type { BaseNodePostprocessor } from "./types.js";
export class MetadataReplacementPostProcessor implements BaseNodePostprocessor {
targetMetadataKey: string;
@@ -1,5 +1,5 @@
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { NodeWithScore } from "@llamaindex/core/schema";
import type { BaseNodePostprocessor } from "./types.js";
export class SimilarityPostprocessor implements BaseNodePostprocessor {
similarityCutoff?: number | undefined;
@@ -1,4 +1,3 @@
export * from "./MetadataReplacementPostProcessor.js";
export * from "./rerankers/index.js";
export * from "./SimilarityPostprocessor.js";
export * from "./types.js";
@@ -1,10 +1,10 @@
import { CohereClient } from "cohere-ai";
import type { MessageContent } from "@llamaindex/core/llms";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { NodeWithScore } from "@llamaindex/core/schema";
import { MetadataMode } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import type { BaseNodePostprocessor } from "../types.js";
type CohereRerankOptions = {
topN?: number;
@@ -1,9 +1,9 @@
import type { MessageContent } from "@llamaindex/core/llms";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { NodeWithScore } from "@llamaindex/core/schema";
import { MetadataMode } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import { getEnv } from "@llamaindex/env";
import type { BaseNodePostprocessor } from "../types.js";
interface JinaAIRerankerResult {
index: number;
@@ -5,8 +5,8 @@ import { MetadataMode } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import type { MessageContent } from "@llamaindex/core/llms";
import type { BaseNodePostprocessor } from "@llamaindex/core/postprocessor";
import type { BaseNode, NodeWithScore } from "@llamaindex/core/schema";
import type { BaseNodePostprocessor } from "../types.js";
type RerankingRequestWithoutInput = Omit<
MixedbreadAI.RerankingRequest,
+780 -291
View File
File diff suppressed because it is too large Load Diff