mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-02 20:13:52 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6716188e10 | |||
| 0b75bd6d92 |
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: Using LlamaIndex Server
|
||||
description: Running LlamaIndex workflows with both API endpoints and a user interface for interaction
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
# LlamaIndex Server
|
||||
|
||||
LlamaIndexServer is a Next.js-based application that allows you to quickly launch your [LlamaIndex Workflows](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/workflows) and [Agent Workflows](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) as an API server with an optional chat UI. It provides a complete environment for running LlamaIndex workflows with both API endpoints and a user interface for interaction.
|
||||
|
||||
## Features
|
||||
|
||||
- Serving a workflow as a chatbot
|
||||
- Built on Next.js for high performance and easy API development
|
||||
- Optional built-in chat UI with extendable UI components
|
||||
- Prebuilt development code
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install @llamaindex/server
|
||||
```
|
||||
|
||||
```shell tab="yarn"
|
||||
yarn add @llamaindex/server
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add @llamaindex/server
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create index.ts file and add the following code:
|
||||
|
||||
```ts
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { wiki } from "@llamaindex/tools"; // or any other tool
|
||||
|
||||
const createWorkflow = () => agent({ tools: [wiki()] })
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
appTitle: "LlamaIndex App",
|
||||
starterQuestions: ["Who is the first president of the United States?"],
|
||||
}).start();
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
In the same directory as `index.ts`, run the following command to start the server:
|
||||
|
||||
```bash
|
||||
tsx index.ts
|
||||
```
|
||||
The server will start at `http://localhost:3000`
|
||||
|
||||
You can also make a request to the server:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json" -d '{"message": "Who is the first president of the United States?"}'
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The LlamaIndexServer accepts the following configuration
|
||||
|
||||
- `workflow`: A callable function that creates a workflow instance for each request
|
||||
- `starterQuestions`: List of starter questions for the chat UI
|
||||
- `appTitle`: The title of the application
|
||||
- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
|
||||
|
||||
LlamaIndexServer accepts all the configuration options from Nextjs Custom Server such as `port`, `hostname`, `dev`, etc.
|
||||
See all Nextjs Custom Server options [here](https://nextjs.org/docs/app/building-your-application/configuring/custom-server).
|
||||
|
||||
## Default Endpoints and Features
|
||||
|
||||
### Chat Endpoint
|
||||
|
||||
The server includes a default chat endpoint at `/api/chat` for handling chat interactions.
|
||||
|
||||
### Chat UI
|
||||
|
||||
The server always provides a chat interface at the root path (`/`) with:
|
||||
|
||||
- Configurable starter questions
|
||||
- Real-time chat interface
|
||||
- API endpoint integration
|
||||
|
||||
### Static File Serving
|
||||
|
||||
- The server automatically mounts the `data` and `output` folders at `{server_url}{api_prefix}/files/data` (default: `/api/files/data`) and `{server_url}{api_prefix}/files/output` (default: `/api/files/output`) respectively.
|
||||
- Your workflows can use both folders to store and access files. As a convention, the `data` folder is used for documents that are ingested and the `output` folder is used for documents that are generated by the workflow.
|
||||
|
||||
|
||||
## Custom UI Components
|
||||
|
||||
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
|
||||
|
||||
### Overview
|
||||
|
||||
Custom UI components are a powerful feature that enables you to:
|
||||
|
||||
- Add custom interface elements to the chat UI using React JSX or TSX files
|
||||
- Extend the default chat interface functionality
|
||||
- Create specialized visualizations or interactions
|
||||
|
||||
### Configuration
|
||||
|
||||
Your workflow must emit events that fit this structure, allowing the LlamaIndex server to display the right UI components based on the event type.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "<event_name>",
|
||||
"data": <data model>
|
||||
}
|
||||
```
|
||||
|
||||
### Server Setup
|
||||
|
||||
1. Initialize the LlamaIndex server with a component directory:
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
appTitle: "LlamaIndex App",
|
||||
componentsDir: "components",
|
||||
}).start();
|
||||
```
|
||||
|
||||
2. Add the custom component code to the directory following the naming pattern:
|
||||
|
||||
- File Extension: `.jsx` and `.tsx` for React components
|
||||
- File Name: Should match the event type from your workflow (e.g., `deep_research_event.jsx` for handling `deep_research_event` type that you defined in your workflow). If there are TSX and JSX files with the same name, the TSX file will be used.
|
||||
- Component Name: Export a default React component named `Component` that receives props from the event data
|
||||
|
||||
Example component structure:
|
||||
|
||||
```jsx
|
||||
function Component({ events }) {
|
||||
// Your component logic here
|
||||
return (
|
||||
// Your UI code here
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always provide a workflow factory that creates fresh workflow instances
|
||||
2. Use environment variables for sensitive configuration
|
||||
3. Use starter questions to guide users in the chat UI
|
||||
|
||||
## Getting Started with a New Project
|
||||
|
||||
Want to start a new project with LlamaIndexServer? Check out our [create-llama](https://github.com/run-llama/create-llama) tool to quickly generate a new project with LlamaIndexServer.
|
||||
@@ -2,5 +2,5 @@
|
||||
"title": "Chat UI",
|
||||
"description": "Use chat-ui to add a chat interface to your LlamaIndexTS application.",
|
||||
"defaultOpen": false,
|
||||
"pages": ["install", "chat", "rsc"]
|
||||
"pages": ["install", "chat", "rsc", "llamaindex-server"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/server
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0b75bd6: feat: component dir in llamaindex server
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,12 +4,25 @@ import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui";
|
||||
import "@llamaindex/chat-ui/styles/markdown.css";
|
||||
import "@llamaindex/chat-ui/styles/pdf.css";
|
||||
import { useChat } from "ai/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Header from "./header";
|
||||
import CustomChatInput from "./ui/chat/chat-input";
|
||||
import CustomChatMessages from "./ui/chat/chat-messages";
|
||||
import {
|
||||
ComponentDef,
|
||||
fetchComponentDefinitions,
|
||||
} from "./ui/chat/dynamic-events";
|
||||
import { getConfig } from "./ui/lib/utils";
|
||||
|
||||
export default function ChatSection() {
|
||||
const [componentDefs, setComponentDefs] = useState<ComponentDef[]>([]);
|
||||
|
||||
// fetch component definitions and use Babel to tranform JSX code to JS code
|
||||
// this is triggered only once when the page is initialised
|
||||
useEffect(() => {
|
||||
fetchComponentDefinitions().then(setComponentDefs);
|
||||
}, []);
|
||||
|
||||
const handler = useChat({
|
||||
api: getConfig("CHAT_API"),
|
||||
onError: (error: unknown) => {
|
||||
@@ -28,7 +41,7 @@ export default function ChatSection() {
|
||||
<div className="flex h-[85vh] w-full flex-col gap-2">
|
||||
<Header />
|
||||
<ChatSectionUI handler={handler} className="min-h-0 w-full flex-1">
|
||||
<CustomChatMessages />
|
||||
<CustomChatMessages componentDefs={componentDefs} />
|
||||
<CustomChatInput />
|
||||
</ChatSectionUI>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { ChatMessage } from "@llamaindex/chat-ui";
|
||||
import { DeepResearchCard } from "./custom/deep-research-card";
|
||||
import { ComponentDef, DynamicEvents } from "./dynamic-events";
|
||||
import { ToolAnnotations } from "./tools/chat-tools";
|
||||
|
||||
export function ChatMessageContent() {
|
||||
export function ChatMessageContent({
|
||||
componentDefs,
|
||||
}: {
|
||||
componentDefs: ComponentDef[];
|
||||
}) {
|
||||
return (
|
||||
<ChatMessage.Content>
|
||||
<ChatMessage.Content.Event />
|
||||
<ChatMessage.Content.AgentEvent />
|
||||
<DeepResearchCard />
|
||||
<ToolAnnotations />
|
||||
<ChatMessage.Content.Image />
|
||||
<DynamicEvents componentDefs={componentDefs} />
|
||||
<ChatMessage.Content.Markdown />
|
||||
<ChatMessage.Content.DocumentFile />
|
||||
<ChatMessage.Content.Source />
|
||||
|
||||
@@ -4,9 +4,15 @@ import { ChatMessage, ChatMessages, useChatUI } from "@llamaindex/chat-ui";
|
||||
import { ChatMessageAvatar } from "./chat-avatar";
|
||||
import { ChatMessageContent } from "./chat-message-content";
|
||||
import { ChatStarter } from "./chat-starter";
|
||||
import { ComponentDef } from "./dynamic-events";
|
||||
|
||||
export default function CustomChatMessages() {
|
||||
export default function CustomChatMessages({
|
||||
componentDefs,
|
||||
}: {
|
||||
componentDefs: ComponentDef[];
|
||||
}) {
|
||||
const { messages } = useChatUI();
|
||||
|
||||
return (
|
||||
<ChatMessages className="rounded-xl shadow-xl">
|
||||
<ChatMessages.List>
|
||||
@@ -17,7 +23,7 @@ export default function CustomChatMessages() {
|
||||
isLast={index === messages.length - 1}
|
||||
>
|
||||
<ChatMessageAvatar />
|
||||
<ChatMessageContent />
|
||||
<ChatMessageContent componentDefs={componentDefs} />
|
||||
<ChatMessage.Actions />
|
||||
</ChatMessage>
|
||||
))}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { getCustomAnnotation, useChatMessage } from "@llamaindex/chat-ui";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
CircleDashed,
|
||||
Clock,
|
||||
NotebookPen,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../../accordion";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../../card";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Markdown } from "./markdown";
|
||||
|
||||
// Streaming event types
|
||||
type EventState = "pending" | "inprogress" | "done" | "error";
|
||||
|
||||
type DeepResearchEvent = {
|
||||
type: "deep_research_event";
|
||||
data: {
|
||||
event: "retrieve" | "analyze" | "answer";
|
||||
state: EventState;
|
||||
id?: string;
|
||||
question?: string;
|
||||
answer?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
// UI state types
|
||||
type QuestionState = {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string | null;
|
||||
state: EventState;
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
type DeepResearchCardState = {
|
||||
retrieve: {
|
||||
state: EventState | null;
|
||||
};
|
||||
analyze: {
|
||||
state: EventState | null;
|
||||
questions: QuestionState[];
|
||||
};
|
||||
};
|
||||
|
||||
interface DeepResearchCardProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const stateIcon: Record<EventState, React.ReactNode> = {
|
||||
pending: <Clock className="h-4 w-4 text-yellow-500" />,
|
||||
inprogress: <CircleDashed className="h-4 w-4 animate-spin text-blue-500" />,
|
||||
done: <CheckCircle2 className="h-4 w-4 text-green-500" />,
|
||||
error: <AlertCircle className="h-4 w-4 text-red-500" />,
|
||||
};
|
||||
|
||||
// Transform the state based on the event without mutations
|
||||
const transformState = (
|
||||
state: DeepResearchCardState,
|
||||
event: DeepResearchEvent,
|
||||
): DeepResearchCardState => {
|
||||
switch (event.data.event) {
|
||||
case "answer": {
|
||||
const { id, question, answer } = event.data;
|
||||
if (!id || !question) return state;
|
||||
|
||||
const updatedQuestions = state.analyze.questions.map((q) => {
|
||||
if (q.id !== id) return q;
|
||||
return {
|
||||
...q,
|
||||
state: event.data.state,
|
||||
answer: answer ?? q.answer,
|
||||
};
|
||||
});
|
||||
|
||||
const newQuestion = !state.analyze.questions.some((q) => q.id === id)
|
||||
? [
|
||||
{
|
||||
id,
|
||||
question,
|
||||
answer: answer ?? null,
|
||||
state: event.data.state,
|
||||
isOpen: false,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
...state,
|
||||
analyze: {
|
||||
...state.analyze,
|
||||
questions: [...updatedQuestions, ...newQuestion],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case "retrieve":
|
||||
case "analyze":
|
||||
return {
|
||||
...state,
|
||||
[event.data.event]: {
|
||||
...state[event.data.event],
|
||||
state: event.data.state,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Convert deep research events to state
|
||||
const deepResearchEventsToState = (
|
||||
events: DeepResearchEvent[] | undefined,
|
||||
): DeepResearchCardState => {
|
||||
if (!events?.length) {
|
||||
return {
|
||||
retrieve: { state: null },
|
||||
analyze: { state: null, questions: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: DeepResearchCardState = {
|
||||
retrieve: { state: null },
|
||||
analyze: { state: null, questions: [] },
|
||||
};
|
||||
|
||||
return events.reduce(
|
||||
(acc: DeepResearchCardState, event: DeepResearchEvent) =>
|
||||
transformState(acc, event),
|
||||
initialState,
|
||||
);
|
||||
};
|
||||
|
||||
export function DeepResearchCard({ className }: DeepResearchCardProps) {
|
||||
const { message } = useChatMessage();
|
||||
|
||||
const state = useMemo(() => {
|
||||
const deepResearchEvents = getCustomAnnotation<DeepResearchEvent>(
|
||||
message.annotations,
|
||||
(annotation) => annotation?.type === "deep_research_event",
|
||||
);
|
||||
if (!deepResearchEvents.length) return null;
|
||||
return deepResearchEventsToState(deepResearchEvents);
|
||||
}, [message.annotations]);
|
||||
|
||||
if (!state) return null;
|
||||
|
||||
return (
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader className="space-y-4">
|
||||
{state.retrieve.state !== null && (
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Search className="h-5 w-5" />
|
||||
{state.retrieve.state === "inprogress"
|
||||
? "Searching..."
|
||||
: "Search completed"}
|
||||
</CardTitle>
|
||||
)}
|
||||
{state.analyze.state !== null && (
|
||||
<CardTitle className="flex items-center gap-2 border-t pt-4">
|
||||
<NotebookPen className="h-5 w-5" />
|
||||
{state.analyze.state === "inprogress" ? "Analyzing..." : "Analysis"}
|
||||
</CardTitle>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{state.analyze.questions.length > 0 && (
|
||||
<Accordion type="single" collapsible className="space-y-2">
|
||||
{state.analyze.questions.map((question: QuestionState) => (
|
||||
<AccordionItem
|
||||
key={question.id}
|
||||
value={question.id}
|
||||
className="rounded-lg border [&[data-state=open]>div]:rounded-b-none"
|
||||
>
|
||||
<AccordionTrigger className="hover:bg-accent gap-2 px-3 py-3 hover:no-underline">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex-shrink-0">
|
||||
{stateIcon[question.state]}
|
||||
</div>
|
||||
<span className="flex-1 text-left font-medium">
|
||||
{question.question}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
{question.answer && (
|
||||
<AccordionContent className="border-t px-3 py-3">
|
||||
<Markdown content={question.answer} />
|
||||
</AccordionContent>
|
||||
)}
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
|
||||
import {
|
||||
getChatUIAnnotation,
|
||||
JSONValue,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
useChatMessage,
|
||||
} from "@llamaindex/chat-ui";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { getConfig } from "../lib/utils";
|
||||
|
||||
export type ComponentDef = {
|
||||
type: string; // eg. deep_research_event
|
||||
code: string; // eg. export const DeepResearchEvent = () => {...}
|
||||
filename: string; // eg. deep_research_event.tsx
|
||||
};
|
||||
|
||||
type EventComponent = ComponentDef & {
|
||||
events: JSONValue[];
|
||||
};
|
||||
|
||||
// image, document_file, sources, events, suggested_questions, agent
|
||||
const BUILT_IN_CHATUI_COMPONENTS = Object.values(MessageAnnotationType);
|
||||
|
||||
export const DynamicEvents = ({
|
||||
componentDefs,
|
||||
}: {
|
||||
componentDefs: ComponentDef[];
|
||||
}) => {
|
||||
const {
|
||||
message: { annotations },
|
||||
} = useChatMessage();
|
||||
|
||||
const shownWarningsRef = useRef<Set<string>>(new Set()); // track warnings
|
||||
|
||||
// Check for missing components in annotations
|
||||
useEffect(() => {
|
||||
if (!annotations?.length) return;
|
||||
|
||||
const availableComponents = new Set(componentDefs.map((comp) => comp.type));
|
||||
|
||||
annotations.forEach((annotation: MessageAnnotation) => {
|
||||
const type = annotation.type;
|
||||
if (!type) return; // skip if annotation doesn't have a type
|
||||
|
||||
const events = getChatUIAnnotation(annotations, type);
|
||||
|
||||
// Skip if it's a built-in component or if we've already shown the warning
|
||||
if (
|
||||
BUILT_IN_CHATUI_COMPONENTS.includes(type) ||
|
||||
shownWarningsRef.current.has(type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have events for a type but no component definition, show a warning
|
||||
if (events && !availableComponents.has(type)) {
|
||||
console.warn(
|
||||
`No component found for event type: ${type}. Please add a component file named ${type}.tsx or ${type}.jsx in your components directory.`,
|
||||
);
|
||||
shownWarningsRef.current.add(type);
|
||||
}
|
||||
});
|
||||
}, [annotations, componentDefs]);
|
||||
|
||||
const components: EventComponent[] = componentDefs
|
||||
.map((comp) => {
|
||||
const events = getChatUIAnnotation(annotations, comp.type) as JSONValue[]; // get all event data by type
|
||||
if (!events?.length) return null;
|
||||
return { ...comp, events };
|
||||
})
|
||||
.filter((comp) => comp !== null);
|
||||
|
||||
if (components.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="components-container">
|
||||
{components.map((component, index) => {
|
||||
return (
|
||||
<React.Fragment key={`${component.type}-${index}`}>
|
||||
{renderEventComponent(component)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export async function fetchComponentDefinitions(): Promise<ComponentDef[]> {
|
||||
const endpoint = getConfig("COMPONENTS_API");
|
||||
if (!endpoint) return [];
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint);
|
||||
const components = (await response.json()) as ComponentDef[];
|
||||
|
||||
// Only need to handle transpilation now
|
||||
const transpiledComponents = components
|
||||
.map((comp) => ({
|
||||
...comp,
|
||||
code: transpileCode(comp.code, comp.filename),
|
||||
}))
|
||||
.filter((comp): comp is ComponentDef => comp.code !== null);
|
||||
|
||||
return transpiledComponents;
|
||||
} catch (error) {
|
||||
console.log("Error fetching dynamic components:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// convert TSX code to JS code using Babel
|
||||
function transpileCode(code: string, filename: string): string | null {
|
||||
try {
|
||||
const transpiledCode = Babel.transform(code, {
|
||||
presets: ["react", "typescript"],
|
||||
filename,
|
||||
}).code;
|
||||
|
||||
if (!transpiledCode) {
|
||||
console.error("Transpiled code is empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
return transpiledCode;
|
||||
} catch (error) {
|
||||
console.error("Error transpiling code:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEventComponent(component: EventComponent) {
|
||||
try {
|
||||
const Component = createComponentFromCode(component.code);
|
||||
return React.createElement(Component, { events: component.events });
|
||||
} catch (error) {
|
||||
console.error(`Error rendering component ${component.type}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createComponentFromCode(code: string) {
|
||||
const componentFn = new Function("React", `${code}; return Component;`);
|
||||
return componentFn(React);
|
||||
}
|
||||
@@ -211,3 +211,7 @@
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.components-container * {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/server",
|
||||
"description": "LlamaIndex Server",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.9",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
@@ -43,6 +43,7 @@
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/babel__standalone": "^7.1.9",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
@@ -54,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "^4.2.0",
|
||||
"@babel/standalone": "^7.27.0",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"llamaindex": "workspace:*",
|
||||
"next": "^15.2.4",
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
import { runWorkflow } from "../utils/workflow";
|
||||
|
||||
export const handleChat = async (
|
||||
workflowFactory: WorkflowFactory,
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
workflowFactory: WorkflowFactory,
|
||||
) => {
|
||||
try {
|
||||
const body = await parseRequestBody(req);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import fs from "fs";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
import { sendJSONResponse } from "../utils/request";
|
||||
|
||||
export const getComponents = async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
componentsDir: string,
|
||||
) => {
|
||||
try {
|
||||
const exists = await promisify(fs.exists)(componentsDir);
|
||||
if (!exists) {
|
||||
return sendJSONResponse(res, 404, {
|
||||
error: "Components directory not found",
|
||||
});
|
||||
}
|
||||
|
||||
const files = await promisify(fs.readdir)(componentsDir);
|
||||
|
||||
// filter files with valid extensions
|
||||
const validExtensions = [".tsx", ".jsx"];
|
||||
const filteredFiles = files.filter((file) =>
|
||||
validExtensions.includes(path.extname(file)),
|
||||
);
|
||||
|
||||
// filter duplicate components
|
||||
const uniqueFiles = filterDuplicateComponents(filteredFiles);
|
||||
|
||||
const components = await Promise.all(
|
||||
uniqueFiles.map(async (file) => {
|
||||
const filePath = path.join(componentsDir, file);
|
||||
const content = await promisify(fs.readFile)(filePath, "utf-8");
|
||||
return {
|
||||
type: path.basename(file, path.extname(file)),
|
||||
code: content,
|
||||
filename: file,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
sendJSONResponse(res, 200, components);
|
||||
} catch (error) {
|
||||
console.error("Error reading components:", error);
|
||||
sendJSONResponse(res, 500, { error: "Failed to read components" });
|
||||
}
|
||||
};
|
||||
|
||||
function filterDuplicateComponents(files: string[]) {
|
||||
const compMap = new Map<string, string>();
|
||||
|
||||
for (const file of files) {
|
||||
const type = path.basename(file, path.extname(file));
|
||||
|
||||
if (compMap.has(type)) {
|
||||
const existingComp = compMap.get(type)!;
|
||||
if (file.endsWith(".tsx") && !existingComp.endsWith(".tsx")) {
|
||||
// prefer .tsx files over others
|
||||
console.warn(`Preferring ${file} over ${existingComp}`);
|
||||
compMap.set(type, file);
|
||||
}
|
||||
} else {
|
||||
compMap.set(type, file);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(compMap.values());
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import { createServer } from "http";
|
||||
import next from "next";
|
||||
import path from "path";
|
||||
import { parse } from "url";
|
||||
import { promisify } from "util";
|
||||
import { handleChat } from "./handlers/chat";
|
||||
import { getLlamaCloudConfig } from "./handlers/cloud";
|
||||
import { getComponents } from "./handlers/components";
|
||||
import { handleServeFiles } from "./handlers/files";
|
||||
import type { LlamaIndexServerOptions, ServerWorkflow } from "./types";
|
||||
|
||||
@@ -17,12 +19,18 @@ export class LlamaIndexServer {
|
||||
port: number;
|
||||
app: ReturnType<typeof next>;
|
||||
workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
|
||||
componentsDir?: string | undefined;
|
||||
|
||||
constructor(options: LlamaIndexServerOptions) {
|
||||
const { workflow, ...nextAppOptions } = options;
|
||||
this.app = next({ dev, dir: nextDir, ...nextAppOptions });
|
||||
this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
|
||||
this.workflowFactory = workflow;
|
||||
this.componentsDir = options.componentsDir;
|
||||
|
||||
if (this.componentsDir) {
|
||||
this.createComponentsDir(this.componentsDir);
|
||||
}
|
||||
|
||||
this.modifyConfig(options);
|
||||
}
|
||||
@@ -33,6 +41,7 @@ export class LlamaIndexServer {
|
||||
const llamaCloudApi = getEnv("LLAMA_CLOUD_API_KEY")
|
||||
? "/api/chat/config/llamacloud"
|
||||
: undefined;
|
||||
const componentsApi = this.componentsDir ? "/api/components" : undefined;
|
||||
|
||||
// content in javascript format
|
||||
const content = `
|
||||
@@ -40,12 +49,20 @@ export class LlamaIndexServer {
|
||||
CHAT_API: '/api/chat',
|
||||
APP_TITLE: ${JSON.stringify(appTitle)},
|
||||
LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
|
||||
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)}
|
||||
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
|
||||
COMPONENTS_API: ${JSON.stringify(componentsApi)}
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(configFile, content);
|
||||
}
|
||||
|
||||
private async createComponentsDir(componentsDir: string) {
|
||||
const exists = await promisify(fs.exists)(componentsDir);
|
||||
if (!exists) {
|
||||
await promisify(fs.mkdir)(componentsDir);
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.app.prepare();
|
||||
|
||||
@@ -54,13 +71,21 @@ export class LlamaIndexServer {
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
if (pathname === "/api/chat" && req.method === "POST") {
|
||||
return handleChat(this.workflowFactory, req, res);
|
||||
return handleChat(req, res, this.workflowFactory);
|
||||
}
|
||||
|
||||
if (pathname?.startsWith("/api/files") && req.method === "GET") {
|
||||
return handleServeFiles(req, res, pathname);
|
||||
}
|
||||
|
||||
if (
|
||||
this.componentsDir &&
|
||||
pathname === "/api/components" &&
|
||||
req.method === "GET"
|
||||
) {
|
||||
return getComponents(req, res, this.componentsDir);
|
||||
}
|
||||
|
||||
if (
|
||||
getEnv("LLAMA_CLOUD_API_KEY") &&
|
||||
pathname === "/api/chat/config/llamacloud" &&
|
||||
|
||||
@@ -28,4 +28,5 @@ export type LlamaIndexServerOptions = NextAppOptions & {
|
||||
workflow: WorkflowFactory;
|
||||
starterQuestions?: string[];
|
||||
appTitle?: string;
|
||||
componentsDir?: string;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function parseRequestBody(request: IncomingMessage) {
|
||||
export function sendJSONResponse(
|
||||
response: ServerResponse,
|
||||
statusCode: number,
|
||||
body: Record<string, unknown> | string,
|
||||
body: Record<string, unknown> | string | Array<unknown>,
|
||||
) {
|
||||
response.statusCode = statusCode;
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
|
||||
Generated
+24
@@ -1793,6 +1793,9 @@ importers:
|
||||
|
||||
packages/server:
|
||||
dependencies:
|
||||
'@babel/standalone':
|
||||
specifier: ^7.27.0
|
||||
version: 7.27.0
|
||||
'@llamaindex/chat-ui':
|
||||
specifier: 0.3.2
|
||||
version: 0.3.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@@ -1848,6 +1851,9 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
version: 4.0.9
|
||||
'@types/babel__standalone':
|
||||
specifier: ^7.1.9
|
||||
version: 7.1.9
|
||||
'@types/node':
|
||||
specifier: ^22.9.0
|
||||
version: 22.9.0
|
||||
@@ -2503,6 +2509,10 @@ packages:
|
||||
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/standalone@7.27.0':
|
||||
resolution: {integrity: sha512-UxFDpi+BuSz6Q1X73P3ZSM1CB7Nbbqys+7COi/tdouRuaqRsJ6GAzUyxTswbqItHSItVY3frQdd+paBHHGEk9g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.26.8':
|
||||
resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -5706,6 +5716,9 @@ packages:
|
||||
'@types/babel__generator@7.6.8':
|
||||
resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
|
||||
|
||||
'@types/babel__standalone@7.1.9':
|
||||
resolution: {integrity: sha512-IcCNPLqpevUD7UpV8QB0uwQPOyoOKACFf0YtYWRHcmxcakaje4Q7dbG2+jMqxw/I8Zk0NHvEps66WwS7z/UaaA==}
|
||||
|
||||
'@types/babel__template@7.4.4':
|
||||
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
|
||||
|
||||
@@ -14091,6 +14104,8 @@ snapshots:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/standalone@7.27.0': {}
|
||||
|
||||
'@babel/template@7.26.8':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
@@ -17545,6 +17560,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.26.8
|
||||
|
||||
'@types/babel__standalone@7.1.9':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.8
|
||||
'@babel/types': 7.26.8
|
||||
'@types/babel__core': 7.20.5
|
||||
'@types/babel__generator': 7.6.8
|
||||
'@types/babel__template': 7.4.4
|
||||
'@types/babel__traverse': 7.20.6
|
||||
|
||||
'@types/babel__template@7.4.4':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.8
|
||||
|
||||
Reference in New Issue
Block a user