Compare commits

..

5 Commits

Author SHA1 Message Date
github-actions[bot] 139eb050f9 Release @llamaindex/server@0.1.0 (#1835)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 15:38:40 +07:00
Thuc Pham 3ffee26b77 feat: enhance config params for LlamaIndexServer (#1833) 2025-04-10 15:21:51 +07:00
Marcus Schiesser dc6e774d78 chore: remove deepresearch events (#1831) 2025-04-09 20:45:49 +07:00
github-actions[bot] 6716188e10 Release @llamaindex/server@0.0.9 (#1830)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 17:44:13 +07:00
Thuc Pham 0b75bd6d92 feat: component dir in llamaindex server (#1828) 2025-04-09 17:25:21 +07:00
19 changed files with 505 additions and 242 deletions
+6
View File
@@ -1,5 +1,11 @@
# @llamaindex/doc
## 0.2.7
### Patch Changes
- 3ffee26: feat: enhance config params for LlamaIndexServer
## 0.2.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@llamaindex/doc",
"version": "0.2.6",
"version": "0.2.7",
"private": true,
"scripts": {
"postinstall": "fumadocs-mdx",
@@ -0,0 +1,165 @@
---
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,
uiConfig: {
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
- `uiConfig`: An object to configure the chat UI containing the following properties:
- `appTitle`: The title of the application (default: `"LlamaIndex App"`)
- `starterQuestions`: List of starter questions for the chat UI (default: `[]`)
- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
- `llamaCloudIndexSelector`: Whether to show the LlamaCloud index selector in the chat UI (requires `LLAMA_CLOUD_API_KEY` to be set in the environment variables) (default: `false`)
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: createWorkflow,
uiConfig: {
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"]
}
+12
View File
@@ -1,5 +1,17 @@
# @llamaindex/server
## 0.1.0
### Minor Changes
- 3ffee26: feat: enhance config params for LlamaIndexServer
## 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);
}
+4
View File
@@ -211,3 +211,7 @@
animation: slideIn 0.5s ease-out;
}
}
.components-container * {
font-family: inherit !important;
}
+3 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@llamaindex/server",
"description": "LlamaIndex Server",
"version": "0.0.8",
"version": "0.1.0",
"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",
-13
View File
@@ -38,19 +38,6 @@ export class AgentRunEvent extends WorkflowEvent<{
data: AgentRunEventData;
}> {}
export type DeepResearchEventData = {
event: "retrieve" | "analyze" | "answer";
state: "pending" | "inprogress" | "done" | "error";
id?: string;
question?: string;
answer?: string;
};
export class DeepResearchEvent extends WorkflowEvent<{
type: "deep_research_event";
data: DeepResearchEventData;
}> {}
export function toSourceEventNode(node: NodeWithScore<Metadata>) {
const { file_name, pipeline_id } = node.node.metadata;
+1 -1
View File
@@ -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());
}
+34 -7
View File
@@ -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,22 +19,31 @@ 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.uiConfig?.componentsDir;
if (this.componentsDir) {
this.createComponentsDir(this.componentsDir);
}
this.modifyConfig(options);
}
private modifyConfig(options: LlamaIndexServerOptions) {
const appTitle = options.appTitle ?? "LlamaIndex App";
const starterQuestions = options.starterQuestions ?? [];
const llamaCloudApi = getEnv("LLAMA_CLOUD_API_KEY")
? "/api/chat/config/llamacloud"
: undefined;
const { uiConfig } = options;
const appTitle = uiConfig?.appTitle ?? "LlamaIndex App";
const starterQuestions = uiConfig?.starterQuestions ?? [];
const llamaCloudApi =
uiConfig?.llamaCloudIndexSelector && 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 +51,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 +73,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" &&
+8 -2
View File
@@ -24,8 +24,14 @@ export type WorkflowFactory = (
export type NextAppOptions = Parameters<typeof next>[0];
export type UIConfig = {
appTitle?: string;
starterQuestions?: string[];
componentsDir?: string;
llamaCloudIndexSelector?: boolean;
};
export type LlamaIndexServerOptions = NextAppOptions & {
workflow: WorkflowFactory;
starterQuestions?: string[];
appTitle?: string;
uiConfig?: UIConfig;
};
+1 -1
View File
@@ -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");
+24
View File
@@ -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