mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-03 08:24:39 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85e5e7e662 | |||
| 58362542c0 | |||
| 6f44185f68 | |||
| afe9e9fc16 | |||
| 1b5a519f13 | |||
| f072308d03 | |||
| 1df8cfbdc2 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Split artifacts use case to document generator and code generator
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Fix typing check issue
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
fix chromadb dependency issue
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
fix: remove dead generated ai code
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Deprecate pro mode
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-${{ matrix.template-types }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}-${{ matrix.template-types }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# create-llama
|
||||
|
||||
## 0.5.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1df8cfb: Split artifacts use case to document generator and code generator
|
||||
- 1b5a519: chore: improve dev experience with nodemon
|
||||
- b3eb0ba: Fix typing check issue
|
||||
- 556f33c: fix chromadb dependency issue
|
||||
- 2451539: fix: remove dead generated ai code
|
||||
- 7a70390: Deprecate pro mode
|
||||
|
||||
## 0.5.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.5.13",
|
||||
"version": "0.5.14",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
+10
-20
@@ -1,5 +1,5 @@
|
||||
import { extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, LLM, MessageContent, Settings } from "llamaindex";
|
||||
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
@@ -12,12 +12,6 @@ import {
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
const workflow = createCodeArtifactWorkflow(reqBody);
|
||||
|
||||
return workflow;
|
||||
};
|
||||
|
||||
export const RequirementSchema = z.object({
|
||||
next_step: z.enum(["answering", "coding"]),
|
||||
language: z.string().nullable().optional(),
|
||||
@@ -71,32 +65,28 @@ const artifactEvent = workflowEvent<{
|
||||
};
|
||||
}>();
|
||||
|
||||
export function createCodeArtifactWorkflow(reqBody: any, llm?: LLM) {
|
||||
if (!llm) {
|
||||
llm = Settings.llm;
|
||||
}
|
||||
export function workflowFactory(reqBody: any) {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({
|
||||
llm,
|
||||
chatHistory: reqBody.chatHistory,
|
||||
}),
|
||||
memory: new ChatMemoryBuffer({ llm }),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.put({
|
||||
role: "user",
|
||||
content: userInput,
|
||||
});
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
return planEvent.with({
|
||||
userInput: userInput,
|
||||
});
|
||||
|
||||
+10
-20
@@ -1,5 +1,5 @@
|
||||
import { extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, LLM, MessageContent, Settings } from "llamaindex";
|
||||
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
@@ -12,12 +12,6 @@ import {
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
const workflow = createDocumentArtifactWorkflow(reqBody);
|
||||
|
||||
return workflow;
|
||||
};
|
||||
|
||||
export const DocumentRequirementSchema = z.object({
|
||||
type: z.enum(["markdown", "html"]),
|
||||
title: z.string(),
|
||||
@@ -74,32 +68,28 @@ const artifactEvent = workflowEvent<{
|
||||
};
|
||||
}>();
|
||||
|
||||
export function createDocumentArtifactWorkflow(reqBody: any, llm?: LLM) {
|
||||
if (!llm) {
|
||||
llm = Settings.llm;
|
||||
}
|
||||
export function workflowFactory(reqBody: any) {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({
|
||||
llm,
|
||||
chatHistory: reqBody.chatHistory,
|
||||
}),
|
||||
memory: new ChatMemoryBuffer({ llm }),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.put({
|
||||
role: "user",
|
||||
content: userInput,
|
||||
});
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
return planEvent.with({
|
||||
userInput,
|
||||
context: state.lastArtifact
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src/**/*.ts"],
|
||||
"exec": "nodemon --exec tsx src/index.ts",
|
||||
"ext": "js ts",
|
||||
"ignore": ["src/app/workflow_*.ts"]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"generate": "tsx src/generate.ts datasource",
|
||||
"generate:datasource": "tsx src/generate.ts datasource",
|
||||
"generate:ui": "tsx src/generate.ts ui",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"dev": "nodemon",
|
||||
"start": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,6 +20,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/server
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f072308: feat: add dev mode UI
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
+128
-5
@@ -4,10 +4,10 @@ LlamaIndexServer is a Next.js-based application that allows you to quickly launc
|
||||
|
||||
## Features
|
||||
|
||||
- Serving a workflow as a chatbot
|
||||
- Add a sophisticated chatbot UI to your LlamaIndex workflow
|
||||
- Edit code and document artifacts in an OpenAI Canvas-style UI
|
||||
- Extendable UI components for events and headers
|
||||
- Built on Next.js for high performance and easy API development
|
||||
- Optional built-in chat UI with extendable UI components
|
||||
- Prebuilt development code
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -21,9 +21,11 @@ Create an `index.ts` file and add the following code:
|
||||
|
||||
```ts
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { wiki } from "@llamaindex/tools"; // or any other tool
|
||||
|
||||
const createWorkflow = () => agent({ tools: [wiki()] });
|
||||
const createWorkflow = () => agent({ tools: [wiki()], llm: openai("gpt-4o") });
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
@@ -34,6 +36,8 @@ new LlamaIndexServer({
|
||||
}).start();
|
||||
```
|
||||
|
||||
The `createWorkflow` function is a factory function that creates an [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) with a tool that retrieves information from Wikipedia in this case. For more details, read about the [Workflow factory contract](#workflow-factory-contract).
|
||||
|
||||
## Running the Server
|
||||
|
||||
In the same directory as `index.ts`, run the following command to start the server:
|
||||
@@ -54,16 +58,75 @@ curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json
|
||||
|
||||
The `LlamaIndexServer` accepts the following configuration options:
|
||||
|
||||
- `workflow`: A callable function that creates a workflow instance for each request
|
||||
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
|
||||
- `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`)
|
||||
- `dev_mode`: When enabled, you can update workflow code in the UI and see the changes immediately. It's currently in beta and only supports updating workflow code at `app/src/workflow.ts`. Please start server in dev mode (`npm run dev`) to use see this reload feature enabled.
|
||||
|
||||
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).
|
||||
|
||||
## Workflow factory contract
|
||||
|
||||
The `workflow` provided will be called for each chat request to initialize a new workflow instance. The contract of the generated workflow must be the same as for the [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow).
|
||||
|
||||
This means that the workflow must handle a `startAgentEvent` event, which is the entry point of the workflow and contains the following information in it's `data` property:
|
||||
|
||||
```typescript
|
||||
{
|
||||
userInput: MessageContent;
|
||||
chatHistory?: ChatMessage[] | undefined;
|
||||
};
|
||||
```
|
||||
|
||||
The `userInput` is the latest user message and the `chatHistory` is the list of messages exchanged between the user and the workflow so far.
|
||||
|
||||
Furthermore, the workflow must stop with a `stopAgentEvent` event to mark the end of the workflow. In between, the workflow can emit [UI events](##AI-generated-UI-Components) to render custom UI components and [Artifact events](##Sending-Artifacts-to-the-UI) to send structured data like generated documents or code snippets to the UI.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { ChatMemoryBuffer, type ChatMessage, Settings } from "llamaindex";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { wiki } from "@llamaindex/tools";
|
||||
|
||||
Settings.llm = openai("gpt-4o");
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
const workflow = createWorkflow();
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { state, sendEvent } = getContext();
|
||||
const messages = data.chatHistory;
|
||||
|
||||
const toolCallResponse = await chatWithTools(
|
||||
Settings.llm,
|
||||
[wiki()],
|
||||
messages,
|
||||
);
|
||||
|
||||
// using result from tool call and use `sendEvent` to emit the next event...
|
||||
});
|
||||
|
||||
// define more workflow handling logic here...
|
||||
|
||||
// Finally stop with a `stopAgentEvent` event to mark the end of the workflow.
|
||||
// return stopAgentEvent.with({
|
||||
// result: "This is the end!",
|
||||
// });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
```
|
||||
|
||||
To generate sophisticated examples of workflows, you best use the [create-llama](https://github.com/run-llama/create-llama) project.
|
||||
|
||||
## AI-generated UI Components
|
||||
|
||||
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
|
||||
@@ -137,6 +200,66 @@ new LlamaIndexServer({
|
||||
}).start();
|
||||
```
|
||||
|
||||
## Sending Artifacts to the UI
|
||||
|
||||
In addition to UI events for custom components, LlamaIndex Server supports a special `ArtifactEvent` to send structured data like generated documents or code snippets to the UI. These artifacts are displayed in a dedicated "Canvas" panel in the chat interface.
|
||||
|
||||
### Artifact Event Structure
|
||||
|
||||
To send an artifact, your workflow needs to emit an event with `type: "artifact"`. The `data` payload of this event should include:
|
||||
|
||||
- `type`: A string indicating the type of artifact (e.g., `"document"`, `"code"`).
|
||||
- `created_at`: A timestamp (e.g., `Date.now()`) indicating when the artifact was created.
|
||||
- `data`: An object containing the specific details of the artifact. The structure of this object depends on the artifact `type`.
|
||||
|
||||
### Defining and Sending an ArtifactEvent
|
||||
|
||||
First, define your artifact event using `workflowEvent` from `@llamaindex/workflow`:
|
||||
|
||||
```typescript
|
||||
import { workflowEvent } from "@llamaindex/workflow";
|
||||
|
||||
// Example for a document artifact
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact"; // Must be "artifact"
|
||||
data: {
|
||||
type: "document"; // Custom type for your artifact (e.g., "document", "code")
|
||||
created_at: number;
|
||||
data: {
|
||||
// Specific data for the document artifact type
|
||||
title: string;
|
||||
content: string;
|
||||
type: "markdown" | "html"; // document format
|
||||
};
|
||||
};
|
||||
}>();
|
||||
```
|
||||
|
||||
Then, within your workflow logic, use `sendEvent` (obtained from `getContext()`) to emit the event:
|
||||
|
||||
```typescript
|
||||
// Assuming 'sendEvent' is available in your workflow handler
|
||||
// and 'documentDetails' contains the content for the artifact.
|
||||
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact", // This top-level type must be "artifact"
|
||||
data: {
|
||||
type: "document", // This is your specific artifact type
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
title: "My Generated Document",
|
||||
content: "# Hello World
|
||||
This is a markdown document.",
|
||||
type: "markdown",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the [ChatCanvasPanel](/packages/server/next/app/components/ui/chat/canvas/panel.tsx) by a renderer depending on the artifact type. For type `document` this is using the [DocumentArtifactViewer](https://github.com/run-llama/chat-ui/blob/bacb75fc6edceacf742fba18632404a2483b5a81/packages/chat-ui/src/chat/canvas/artifacts/document.tsx#L17).
|
||||
|
||||
## Default Endpoints and Features
|
||||
|
||||
### Chat Endpoint
|
||||
|
||||
@@ -6,7 +6,7 @@ This directory contains examples of how to use the LlamaIndex Server.
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
npx tsx simple-workflow/calculator.ts
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## Open browser at http://localhost:3000
|
||||
|
||||
@@ -39,5 +39,5 @@ new LlamaIndexServer({
|
||||
appTitle: "LlamaIndex App",
|
||||
starterQuestions: ["What is the color of the dog?"],
|
||||
},
|
||||
port: 4100,
|
||||
port: 3000,
|
||||
}).start();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
This example shows how to use the dev mode of the server.
|
||||
|
||||
First, we need to set `devMode` to `true` in the `uiConfig` of the server.
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
appTitle: "Calculator",
|
||||
devMode: true,
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
```
|
||||
|
||||
Export OpenAI API key and start the server in dev mode.
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<your-openai-api-key>
|
||||
npx nodemon --exec tsx index.ts
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { workflowFactory } from "./src/app/workflow";
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
appTitle: "Calculator",
|
||||
devMode: true,
|
||||
starterQuestions: [
|
||||
"What is the weather in Tokyo?",
|
||||
"What is the weather in New York?",
|
||||
],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -0,0 +1,16 @@
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
return agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "weather",
|
||||
description: "Get the weather in a specific city",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: ({ city }) => `The weather in ${city} is sunny`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsx simple-workflow/calculator.ts"
|
||||
"dev": "nodemon --exec tsx simple-workflow/calculator.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "^0.2.0",
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ new LlamaIndexServer({
|
||||
appTitle: "Calculator",
|
||||
starterQuestions: ["1 + 1", "2 + 2"],
|
||||
},
|
||||
port: 4000,
|
||||
port: 3000,
|
||||
}).start();
|
||||
|
||||
@@ -13,6 +13,7 @@ import CustomChatMessages from "./chat-messages";
|
||||
import { DynamicEventsErrors } from "./custom/events/dynamic-events-errors";
|
||||
import { fetchComponentDefinitions } from "./custom/events/loader";
|
||||
import { ComponentDef } from "./custom/events/types";
|
||||
import { DevModePanel } from "./dev-mode-panel";
|
||||
|
||||
export default function ChatSection() {
|
||||
const handler = useChat({
|
||||
@@ -35,12 +36,13 @@ export default function ChatSection() {
|
||||
<ChatHeader />
|
||||
<ChatUI
|
||||
handler={handler}
|
||||
className="flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
|
||||
className="relative flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
|
||||
>
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ChatSectionPanel />
|
||||
<ChatCanvasPanel />
|
||||
</ResizablePanelGroup>
|
||||
<DevModePanel />
|
||||
</ChatUI>
|
||||
</div>
|
||||
<ChatInjection />
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CodeEditor,
|
||||
fileExtensionToEditorLang,
|
||||
} from "@llamaindex/chat-ui/widgets";
|
||||
import { AlertCircle, Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import { getConfig } from "../lib/utils";
|
||||
|
||||
const API_PATH = "/api/dev/files/workflow";
|
||||
const POLLING_TIMEOUT = 30_000; // 30 seconds
|
||||
|
||||
type WorkflowFile = {
|
||||
last_modified: number;
|
||||
file_path: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export function DevModePanel() {
|
||||
const devModeEnabled = getConfig("DEV_MODE");
|
||||
if (!devModeEnabled) return null;
|
||||
return <DevModePanelComp />;
|
||||
}
|
||||
|
||||
function DevModePanelComp() {
|
||||
const [devModeOpen, setDevModeOpen] = useState(false);
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [fetchingError, setFetchingError] = useState<string | null>();
|
||||
const [workflowFile, setWorkflowFile] = useState<WorkflowFile | null>(null);
|
||||
|
||||
const [updatedCode, setUpdatedCode] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [pollingError, setPollingError] = useState<string | null>(null);
|
||||
|
||||
async function fetchWorkflowCode() {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
const response = await fetch(API_PATH);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail ?? "Unknown error");
|
||||
}
|
||||
|
||||
setWorkflowFile(data);
|
||||
setFetchingError(null);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
setFetchingError(errorMessage);
|
||||
console.warn("Error fetching workflow code:", error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartingWorkflow() {
|
||||
if (!workflowFile) return;
|
||||
|
||||
const initialLastModified = workflowFile.last_modified;
|
||||
setIsPolling(true);
|
||||
setPollingError(null);
|
||||
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
// interval refetching the updated workflow code
|
||||
const poll = async () => {
|
||||
if (Date.now() - pollStartTime > POLLING_TIMEOUT) {
|
||||
setPollingError(
|
||||
`Server not responding after ${POLLING_TIMEOUT / 1000} seconds.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pollResponse = await fetch(API_PATH);
|
||||
const pollData = (await pollResponse.json()) as WorkflowFile;
|
||||
if (pollData.last_modified !== initialLastModified) {
|
||||
setWorkflowFile(pollData);
|
||||
setUpdatedCode(pollData.content);
|
||||
setIsPolling(false);
|
||||
setPollingError(null);
|
||||
setDevModeOpen(false);
|
||||
} else {
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.info("Polling error", error);
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
|
||||
const handleResetCode = () => {
|
||||
setUpdatedCode(workflowFile?.content ?? null);
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
const handleSaveCode = async () => {
|
||||
if (!workflowFile) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const response = await fetch(API_PATH, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: updatedCode,
|
||||
file_path: workflowFile.file_path,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail ?? "Unknown error");
|
||||
}
|
||||
setSaveError(null);
|
||||
await restartingWorkflow();
|
||||
} catch (error) {
|
||||
console.warn("Error saving workflow code:", error);
|
||||
setSaveError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error happened when saving workflow code",
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (devModeOpen) {
|
||||
fetchWorkflowCode();
|
||||
}
|
||||
}, [devModeOpen]);
|
||||
|
||||
const codeEditorLanguage = fileExtensionToEditorLang(
|
||||
workflowFile?.file_path.split(".").pop() ?? "",
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setDevModeOpen(!devModeOpen)}
|
||||
className="fixed right-2 top-1/2 origin-right -translate-y-1/2 rotate-90 transform rounded-l-md shadow-md transition-transform hover:-translate-x-1"
|
||||
>
|
||||
Dev Mode
|
||||
</Button>
|
||||
|
||||
{isPolling && (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
{!pollingError && (
|
||||
<>
|
||||
<Loader2 className="mb-4 h-16 w-16 animate-spin text-white" />
|
||||
<p className="text-lg font-semibold text-white">
|
||||
Applying changes and restarting server...
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-300">
|
||||
Please wait for a while then you can start chatting with the
|
||||
updated workflow.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{pollingError && (
|
||||
<div className="bg-destructive/20 text-destructive-foreground mt-4 max-w-md rounded-md p-4 text-center">
|
||||
<div className="mb-2 flex items-center justify-center gap-2">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<h6 className="text-sm font-medium">Server Starting Error</h6>
|
||||
</div>
|
||||
<p className="text-sm">{pollingError}</p>
|
||||
|
||||
<p className="text-sm">
|
||||
Please reload the page and check server logs.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`border-border fixed right-0 top-0 z-10 h-full w-full border-l shadow-xl transition-all duration-300 ease-in-out ${
|
||||
devModeOpen ? "translate-x-0 bg-black/50" : "translate-x-full"
|
||||
}`}
|
||||
onClick={() => setDevModeOpen(false)}
|
||||
>
|
||||
<div
|
||||
className={`bg-background ml-auto flex h-full w-[800px] flex-col p-4`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Workflow Editor</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{isFetching ? (
|
||||
"Loading..."
|
||||
) : workflowFile ? (
|
||||
<>
|
||||
Edit the code of <b>{workflowFile.file_path}</b> and save to
|
||||
apply changes to your workflow.
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setDevModeOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{fetchingError ? (
|
||||
<div className="bg-destructive/10 text-destructive/70 mb-4 flex items-center gap-2 rounded-md p-4">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<p className="text-sm font-medium">{fetchingError}</p>
|
||||
</div>
|
||||
) : (
|
||||
<CodeEditor
|
||||
code={updatedCode ?? workflowFile?.content ?? ""}
|
||||
onChange={setUpdatedCode}
|
||||
language={codeEditorLanguage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col">
|
||||
{saveError && (
|
||||
<div className="bg-destructive/10 text-destructive/70 mb-4 rounded-md p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<h6 className="text-sm font-medium">Error Saving Code</h6>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-sm">{saveError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mr-2"
|
||||
onClick={handleResetCode}
|
||||
>
|
||||
Reset Code
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveCode}
|
||||
disabled={isSaving || !updatedCode || !workflowFile}
|
||||
>
|
||||
Save & Restart Server
|
||||
{isSaving && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/server",
|
||||
"description": "LlamaIndex Server",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@llamaindex/chat-ui": "0.4.3",
|
||||
"@llamaindex/chat-ui": "0.4.4",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
import { sendJSONResponse } from "../utils/request";
|
||||
import { parseRequestBody, sendJSONResponse } from "../utils/request";
|
||||
|
||||
export const handleServeFiles = async (
|
||||
req: IncomingMessage,
|
||||
@@ -21,3 +23,84 @@ export const handleServeFiles = async (
|
||||
return sendJSONResponse(res, 404, { error: "File not found" });
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_WORKFLOW_FILE_PATH = "src/app/workflow.ts"; // TODO: we can make it as a parameter in server later
|
||||
|
||||
export const getWorkflowFile = async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
|
||||
) => {
|
||||
const fileExists = await promisify(fs.exists)(filePath);
|
||||
if (!fileExists) {
|
||||
return sendJSONResponse(res, 404, {
|
||||
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
|
||||
});
|
||||
}
|
||||
|
||||
const content = await promisify(fs.readFile)(filePath, "utf-8");
|
||||
const last_modified = fs.statSync(filePath).mtime.getTime();
|
||||
sendJSONResponse(res, 200, { content, file_path: filePath, last_modified });
|
||||
};
|
||||
|
||||
export const updateWorkflowFile = async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
|
||||
) => {
|
||||
const body = await parseRequestBody(req);
|
||||
const { content } = body as { content: string };
|
||||
|
||||
const fileExists = await promisify(fs.exists)(filePath);
|
||||
if (!fileExists) {
|
||||
return sendJSONResponse(res, 404, {
|
||||
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedFilePath = path.resolve(DEFAULT_WORKFLOW_FILE_PATH);
|
||||
const result = await validateTypeScriptFile(resolvedFilePath, content);
|
||||
|
||||
if (!result.isValid) {
|
||||
return sendJSONResponse(res, 400, {
|
||||
detail: result.errors.join("\n"),
|
||||
});
|
||||
}
|
||||
|
||||
await promisify(fs.writeFile)(filePath, content);
|
||||
sendJSONResponse(res, 200, { content });
|
||||
} catch (error) {
|
||||
console.error("Error updating workflow file:", error);
|
||||
sendJSONResponse(res, 500, { error: "Failed to update workflow file" });
|
||||
}
|
||||
};
|
||||
|
||||
// use typescript package to validate the file syntax and imports
|
||||
async function validateTypeScriptFile(filePath: string, content: string) {
|
||||
// Update workflow file directly will cause the server restart immediately.
|
||||
// So we create a temporary file with the same content in the same directory as the workflow file
|
||||
// This file will be used to validate the file syntax and imports. It will be deleted after validation.
|
||||
const tempFilePath = path.join(
|
||||
path.dirname(filePath),
|
||||
`workflow_${Date.now()}.ts`,
|
||||
);
|
||||
fs.writeFileSync(tempFilePath, content);
|
||||
|
||||
const errors = [];
|
||||
try {
|
||||
const tscCommand = `npx tsc ${tempFilePath} --noEmit --skipLibCheck true`;
|
||||
await promisify(exec)(tscCommand);
|
||||
} catch (error) {
|
||||
const errorMessage = (error as { stdout: string })?.stdout;
|
||||
errors.push(errorMessage);
|
||||
} finally {
|
||||
// Clean up temporary file
|
||||
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors: errors,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,8 +9,13 @@ 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 {
|
||||
getWorkflowFile,
|
||||
handleServeFiles,
|
||||
updateWorkflowFile,
|
||||
} from "./handlers/files";
|
||||
import type { LlamaIndexServerOptions } from "./types";
|
||||
|
||||
const nextDir = path.join(__dirname, "..", "server");
|
||||
const configFile = path.join(__dirname, "..", "server", "public", "config.js");
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
@@ -44,6 +49,7 @@ export class LlamaIndexServer {
|
||||
? "/api/chat/config/llamacloud"
|
||||
: undefined;
|
||||
const componentsApi = this.componentsDir ? "/api/components" : undefined;
|
||||
const devMode = uiConfig?.devMode ?? false;
|
||||
|
||||
// content in javascript format
|
||||
const content = `
|
||||
@@ -52,7 +58,8 @@ export class LlamaIndexServer {
|
||||
APP_TITLE: ${JSON.stringify(appTitle)},
|
||||
LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
|
||||
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
|
||||
COMPONENTS_API: ${JSON.stringify(componentsApi)}
|
||||
COMPONENTS_API: ${JSON.stringify(componentsApi)},
|
||||
DEV_MODE: ${JSON.stringify(devMode)}
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(configFile, content);
|
||||
@@ -96,6 +103,14 @@ export class LlamaIndexServer {
|
||||
return getLlamaCloudConfig(req, res);
|
||||
}
|
||||
|
||||
if (pathname === "/api/dev/files/workflow" && req.method === "GET") {
|
||||
return getWorkflowFile(req, res);
|
||||
}
|
||||
|
||||
if (pathname === "/api/dev/files/workflow" && req.method === "PUT") {
|
||||
return updateWorkflowFile(req, res);
|
||||
}
|
||||
|
||||
const handle = this.app.getRequestHandler();
|
||||
handle(req, res, parsedUrl);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ export type UIConfig = {
|
||||
starterQuestions?: string[];
|
||||
componentsDir?: string;
|
||||
llamaCloudIndexSelector?: boolean;
|
||||
devMode?: boolean;
|
||||
};
|
||||
|
||||
export type LlamaIndexServerOptions = NextAppOptions & {
|
||||
|
||||
Generated
+88
-22
@@ -181,8 +181,8 @@ importers:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
|
||||
'@llamaindex/chat-ui':
|
||||
specifier: 0.4.3
|
||||
version: 0.4.3(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
specifier: 0.4.4
|
||||
version: 0.4.4(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@llamaindex/env':
|
||||
specifier: ^0.1.29
|
||||
version: 0.1.29
|
||||
@@ -392,6 +392,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20.10.3
|
||||
version: 20.17.30
|
||||
nodemon:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
tsx:
|
||||
specifier: ^4.7.2
|
||||
version: 4.19.3
|
||||
@@ -1177,8 +1180,8 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@llamaindex/chat-ui@0.4.3':
|
||||
resolution: {integrity: sha512-JeS5pzEXZRRzTL70lHRtbWbaPjeZuMMHlqkTNBgYqc2eMM+o9VMOzVKKsXoYfOeE+wunqJd+cBrtkECGEmhRZg==}
|
||||
'@llamaindex/chat-ui@0.4.4':
|
||||
resolution: {integrity: sha512-sE3mJxlmAV3eiIaqOnioUNYYBLJJL8sy2mdFP4xXDf3PfkcEn0wT2Om/ButpVgIXHlPG9lONtv8mzw7hWq2Atg==}
|
||||
peerDependencies:
|
||||
react: ^18.2.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
@@ -3882,6 +3885,10 @@ packages:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-flag@3.0.0:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3987,6 +3994,9 @@ packages:
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore-by-default@1.0.1:
|
||||
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -4805,6 +4815,11 @@ packages:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
nodemon@3.1.10:
|
||||
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
nopt@5.0.0:
|
||||
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5258,6 +5273,9 @@ packages:
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
@@ -5674,6 +5692,10 @@ packages:
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
@@ -5832,6 +5854,10 @@ packages:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5928,6 +5954,10 @@ packages:
|
||||
to-vfile@6.1.0:
|
||||
resolution: {integrity: sha512-BxX8EkCxOAZe+D/ToHdDsJcVI4HqQfmw0tCkp31zf3dNP/XWIAjU4CmeuSwsSoOzOTqHPOL0KUzyZqJplkD0Qw==}
|
||||
|
||||
touch@3.1.1:
|
||||
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
||||
hasBin: true
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
@@ -6018,6 +6048,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
underscore@1.13.7:
|
||||
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||
|
||||
@@ -6462,7 +6495,7 @@ snapshots:
|
||||
'@babel/parser': 7.27.0
|
||||
'@babel/template': 7.27.0
|
||||
'@babel/types': 7.27.0
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -6891,7 +6924,7 @@ snapshots:
|
||||
'@eslint/config-array@0.19.2':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.6
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -6909,7 +6942,7 @@ snapshots:
|
||||
'@eslint/eslintrc@3.3.1':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
espree: 10.3.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@@ -7113,7 +7146,7 @@ snapshots:
|
||||
next: 15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
zod: 3.24.3
|
||||
|
||||
'@llamaindex/chat-ui@0.4.3(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@llamaindex/chat-ui@0.4.4(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@codemirror/lang-css': 6.3.1
|
||||
'@codemirror/lang-html': 6.4.9
|
||||
@@ -8531,7 +8564,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.31.0
|
||||
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
|
||||
'@typescript-eslint/visitor-keys': 8.31.0
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
@@ -8546,7 +8579,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.31.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
@@ -8559,7 +8592,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.31.0
|
||||
'@typescript-eslint/visitor-keys': 8.31.0
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
@@ -8743,7 +8776,7 @@ snapshots:
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -9101,7 +9134,7 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
chownr@1.1.4:
|
||||
optional: true
|
||||
@@ -9297,9 +9330,11 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.0:
|
||||
debug@4.4.0(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 5.5.0
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
@@ -9665,7 +9700,7 @@ snapshots:
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
get-tsconfig: 4.10.0
|
||||
is-bun-module: 2.0.0
|
||||
@@ -9789,7 +9824,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.3.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
@@ -10168,6 +10203,8 @@ snapshots:
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@3.0.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-property-descriptors@1.0.2:
|
||||
@@ -10278,7 +10315,7 @@ snapshots:
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -10304,6 +10341,8 @@ snapshots:
|
||||
ieee754@1.2.1:
|
||||
optional: true
|
||||
|
||||
ignore-by-default@1.0.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
@@ -10664,7 +10703,7 @@ snapshots:
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
commander: 13.1.0
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
execa: 8.0.1
|
||||
lilconfig: 3.1.3
|
||||
listr2: 8.3.2
|
||||
@@ -11115,7 +11154,7 @@ snapshots:
|
||||
micromark@3.2.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
decode-named-character-reference: 1.1.0
|
||||
micromark-core-commonmark: 1.1.0
|
||||
micromark-factory-space: 1.1.0
|
||||
@@ -11260,6 +11299,19 @@ snapshots:
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
nodemon@3.1.10:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.7.1
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.1
|
||||
undefsafe: 2.0.5
|
||||
|
||||
nopt@5.0.0:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
@@ -11672,6 +11724,8 @@ snapshots:
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
pump@3.0.2:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
@@ -12235,6 +12289,10 @@ snapshots:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.1
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
@@ -12393,6 +12451,10 @@ snapshots:
|
||||
client-only: 0.0.1
|
||||
react: 19.1.0
|
||||
|
||||
supports-color@5.5.0:
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -12489,6 +12551,8 @@ snapshots:
|
||||
is-buffer: 2.0.5
|
||||
vfile: 4.2.1
|
||||
|
||||
touch@3.1.1: {}
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tree-sitter@0.22.4:
|
||||
@@ -12593,6 +12657,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
underscore@1.13.7: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
@@ -12820,7 +12886,7 @@ snapshots:
|
||||
vite-node@2.1.9(@types/node@20.17.30)(lightningcss@1.29.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 1.1.2
|
||||
vite: 5.4.18(@types/node@20.17.30)(lightningcss@1.29.2)
|
||||
@@ -12855,7 +12921,7 @@ snapshots:
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.2.0
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
expect-type: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
pathe: 1.1.2
|
||||
@@ -12886,7 +12952,7 @@ snapshots:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
commander: 9.5.0
|
||||
debug: 4.4.0
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ app = LlamaIndexServer(
|
||||
|
||||
The LlamaIndexServer accepts the following configuration parameters:
|
||||
|
||||
- `workflow_factory`: A callable that creates a workflow instance for each request
|
||||
- `workflow_factory`: A callable that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
|
||||
- `logger`: Optional logger instance (defaults to uvicorn logger)
|
||||
- `use_default_routers`: Whether to include default routers (chat, static file serving)
|
||||
- `env`: Environment setting ('dev' enables CORS and UI by default)
|
||||
@@ -83,10 +83,36 @@ The LlamaIndexServer accepts the following configuration parameters:
|
||||
- `ui_path`: Path for downloaded UI static files (default: ".ui")
|
||||
- `component_dir`: The directory for custom UI components rendering events emitted by the workflow. The default is None, which does not render custom UI components.
|
||||
- `llamacloud_index_selector`: Whether to show the LlamaCloud index selector in the chat UI (default: False). Requires `LLAMA_CLOUD_API_KEY` to be set.
|
||||
- `dev_mode`: When enabled, you can update workflow code in the UI and see the changes immediately. It's currently in beta and only supports updating workflow code at `app/workflow.py`. You might also need to set `env="dev"` and start the server with the reload feature enabled.
|
||||
- `verbose`: Enable verbose logging
|
||||
- `api_prefix`: API route prefix (default: "/api")
|
||||
- `server_url`: The deployment URL of the server (default is None)
|
||||
|
||||
## Workflow factory contract
|
||||
|
||||
The `workflow_factory` provided will be called for each chat request to initialize a new workflow instance. Additionally, we provide the [ChatRequest](https://github.com/run-llama/create-llama/blob/afe9e9fc16427d20e1dfb635a45e7ed4b46285cb/python/llama-index-server/llama_index/server/api/models.py#L32) object, which includes the request information that is helpful for initializing the workflow. For example:
|
||||
```python
|
||||
def create_workflow(chat_request: ChatRequest) -> Workflow:
|
||||
# using messages from the chat request to initialize the workflow
|
||||
return MyCustomWorkflow(chat_request.messages)
|
||||
```
|
||||
|
||||
Your workflow will be executed once for each chat request with the following input parameters are included in workflow's `StartEvent`:
|
||||
- `user_msg` [str]: The current user message
|
||||
- `chat_history` [list[[ChatMessage](https://docs.llamaindex.ai/en/stable/api_reference/prompts/#llama_index.core.prompts.ChatMessage)]]: All the previous messages of the conversation
|
||||
|
||||
Example:
|
||||
```python
|
||||
@step
|
||||
def handle_start_event(ev: StartEvent) -> MyNextEvent:
|
||||
user_msg = ev.user_msg
|
||||
chat_history = ev.chat_history
|
||||
...
|
||||
```
|
||||
|
||||
Your workflows can emit `UIEvent` events to render [Custom UI Components](https://github.com/run-llama/create-llama/blob/main/python/llama-index-server/docs/custom_ui_component.md) in the chat UI to improve the user experience.
|
||||
Furthermore, you can send `ArtifactEvent` events to render code or document [Artifacts](https://github.com/run-llama/create-llama/blob/main/python/llama-index-server/docs/custom_artifact_event.md) in a dedicated Canvas panel in the chat UI.
|
||||
|
||||
## Default Routers and Features
|
||||
|
||||
### Chat Router
|
||||
@@ -107,11 +133,6 @@ When enabled, the server provides a chat interface at the root path (`/`) with:
|
||||
- Real-time chat interface
|
||||
- API endpoint integration
|
||||
|
||||
### Custom UI Components
|
||||
|
||||
You can add custom UI components for your workflow by providing `component_dir` config and adding custom .jsx or .tsx files to the directory.
|
||||
See [Custom UI Components](https://github.com/run-llama/create-llama/blob/main/llama-index-server/docs/custom_ui_component.md) for more details.
|
||||
|
||||
## Development Mode
|
||||
|
||||
In development mode (`env="dev"`), the server:
|
||||
@@ -120,6 +141,20 @@ In development mode (`env="dev"`), the server:
|
||||
- Automatically includes the chat UI
|
||||
- Provides more verbose logging
|
||||
|
||||
### Workflow Editor (Beta)
|
||||
|
||||
In development mode, you can set `dev_mode` to `True` in the UI configuration to enable the workflow editor, which allows you to edit the workflow code directly in the browser.
|
||||
|
||||
```python
|
||||
app = LlamaIndexServer(
|
||||
workflow_factory=create_workflow,
|
||||
env="dev",
|
||||
ui_config={"dev_mode": True},
|
||||
)
|
||||
```
|
||||
|
||||
**Note**: The workflow editor is currently in beta and only supports updating LlamaIndexServer projects created with [create-llama](https://github.com/run-llama/create-llama/). You also need to start the server via `fastapi dev` so that the server can hot reload the workflow code.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The server provides the following default endpoints:
|
||||
@@ -130,11 +165,10 @@ The server provides the following default endpoints:
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always provide a workflow factory that creates fresh workflow instances
|
||||
2. Use environment variables for sensitive configuration
|
||||
3. Enable verbose logging during development
|
||||
4. Configure CORS appropriately for your deployment environment
|
||||
5. Use starter questions to guide users in the chat UI
|
||||
1. Use environment variables for sensitive configuration
|
||||
2. Enable verbose logging during development
|
||||
3. Configure CORS appropriately for your deployment environment
|
||||
4. Use starter questions to guide users in the chat UI
|
||||
|
||||
## Getting Started with a New Project
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Sending Artifacts to the UI
|
||||
|
||||
In addition to UI events for custom components, LlamaIndex Server supports a special `ArtifactEvent` to send structured data like generated documents or code snippets to the UI. These artifacts are displayed in a dedicated "Canvas" panel in the chat interface.
|
||||
|
||||
## Artifact Event Structure
|
||||
|
||||
To send an artifact, your workflow needs to emit an event with `type: "artifact"`. The `data` payload of this event should include:
|
||||
|
||||
- `type`: An `ArtifactType` enum indicating the type of artifact (e.g., `ArtifactType.DOCUMENT`, `ArtifactType.CODE`).
|
||||
- `created_at`: A timestamp (e.g., `int(time.time())`) indicating when the artifact was created.
|
||||
- `data`: An object containing the specific details of the artifact. The structure of this object depends on the artifact `type`. For example, `DocumentArtifactData` or `CodeArtifactData`.
|
||||
|
||||
## Defining and Sending an ArtifactEvent
|
||||
|
||||
First, import the necessary classes:
|
||||
|
||||
```python
|
||||
import time
|
||||
from llama_index.server.api.models import (
|
||||
Artifact,
|
||||
ArtifactEvent,
|
||||
ArtifactType,
|
||||
DocumentArtifactData,
|
||||
# CodeArtifactData, # Import if sending code artifacts
|
||||
)
|
||||
```
|
||||
|
||||
Then, within your workflow logic, use `ctx.write_event_to_stream` to emit the event. Here's an example of sending a document artifact, taken from [document_workflow.py](/python/llama-index-server/examples/artifact/document_workflow.py):
|
||||
|
||||
```python
|
||||
# Assuming 'ctx' is the workflow Context and 'content' is a markdown string
|
||||
|
||||
ctx.write_event_to_stream(
|
||||
ArtifactEvent(
|
||||
data=Artifact(
|
||||
type=ArtifactType.DOCUMENT,
|
||||
created_at=int(time.time()),
|
||||
data=DocumentArtifactData(
|
||||
title="My cooking recipes",
|
||||
content=content,
|
||||
type="markdown",
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the Canvas panel by a renderer depending on the artifact type. For `ArtifactType.DOCUMENT`, this uses a `DocumentArtifactViewer`.
|
||||
|
||||
## Available Artifact Types
|
||||
|
||||
LlamaIndex Server currently supports the following artifact types:
|
||||
|
||||
- `ArtifactType.DOCUMENT`: For text-based documents like Markdown or HTML.
|
||||
- `data` should be an instance of `DocumentArtifactData` which includes `title: str`, `content: str`, and `type: Literal["markdown", "html"]`.
|
||||
- `ArtifactType.CODE`: For code snippets.
|
||||
- `data` should be an instance of `CodeArtifactData` which includes `title: str`, `code: str`, and `language: str`.
|
||||
|
||||
Ensure you provide the correct data model corresponding to the `ArtifactType` you are sending. You can find these data models in `llama_index.server.api.models`.
|
||||
@@ -0,0 +1,38 @@
|
||||
# A simple chat app
|
||||
|
||||
This guide explains how to set up and use the LlamaIndex server with a simple chatbot agent.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [uv](https://github.com/astral-sh/uv) installed (a fast Python package manager and runner)
|
||||
- An OpenAI API key
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Set the OpenAI API Key**
|
||||
|
||||
Export your OpenAI API key as an environment variable:
|
||||
|
||||
```sh
|
||||
export OPENAI_API_KEY=your_openai_api_key_here
|
||||
```
|
||||
|
||||
2. **Run the Server Using uv**
|
||||
|
||||
Start the server with the following command:
|
||||
|
||||
```sh
|
||||
uv run workflow.py
|
||||
```
|
||||
|
||||
This will launch the FastAPI server using the workflow defined in `main.py`.
|
||||
|
||||
3. **Access the Application**
|
||||
|
||||
Open your browser and go to:
|
||||
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
You will see the LlamaIndex chat app UI, where you can interact with the agent.
|
||||
@@ -0,0 +1,14 @@
|
||||
from typing import Optional
|
||||
|
||||
from llama_index.core.agent.workflow import AgentWorkflow
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.server.api.models import ChatRequest
|
||||
|
||||
|
||||
def create_workflow(chat_request: Optional[ChatRequest] = None) -> AgentWorkflow:
|
||||
return AgentWorkflow.from_tools_or_functions(
|
||||
tools_or_functions=[],
|
||||
llm=Settings.llm or OpenAI(model="gpt-4o-mini"),
|
||||
system_prompt="You are a helpful assistant that can tell a joke about Llama.",
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
from app.workflow import create_workflow
|
||||
from fastapi import FastAPI
|
||||
|
||||
from llama_index.server import LlamaIndexServer, UIConfig
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = LlamaIndexServer(
|
||||
workflow_factory=create_workflow,
|
||||
ui_config=UIConfig(
|
||||
app_title="Artifact",
|
||||
starter_questions=[
|
||||
"Tell me a funny joke.",
|
||||
"Tell me some jokes about AI.",
|
||||
],
|
||||
component_dir="components",
|
||||
dev_mode=True, # To show the dev UI, should disable this in production
|
||||
),
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
@@ -1,4 +1,9 @@
|
||||
from llama_index.server.api.routers.chat import chat_router
|
||||
from llama_index.server.api.routers.ui import custom_components_router
|
||||
from llama_index.server.api.routers.dev import dev_router
|
||||
|
||||
__all__ = ["chat_router", "custom_components_router"]
|
||||
__all__ = [
|
||||
"chat_router",
|
||||
"custom_components_router",
|
||||
"dev_router",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from llama_index.server.settings import server_settings
|
||||
from llama_index.server.utils.workflow_validation import validate_workflow_file
|
||||
|
||||
|
||||
class WorkflowFile(BaseModel):
|
||||
last_modified: int
|
||||
file_path: str = Field(
|
||||
default="app/workflow.py",
|
||||
description="Relative path to the workflow file",
|
||||
)
|
||||
content: str
|
||||
|
||||
|
||||
class WorkflowFileUpdate(BaseModel):
|
||||
content: str
|
||||
file_path: str = Field(
|
||||
default="app/workflow.py",
|
||||
description="Relative path to the workflow file",
|
||||
)
|
||||
|
||||
|
||||
class WorkflowValidationResult(BaseModel):
|
||||
valid: bool
|
||||
error: str
|
||||
|
||||
|
||||
def dev_router() -> APIRouter:
|
||||
# Use a prefix here to avoid conflicts with other routers
|
||||
# but we probably don't need to do this
|
||||
router = APIRouter(prefix="/dev", tags=["dev"])
|
||||
|
||||
default_workflow_file_path = "app/workflow.py"
|
||||
|
||||
@router.get("/files/workflow")
|
||||
async def get_workflow_file() -> WorkflowFile:
|
||||
"""
|
||||
Fetch the current workflow code
|
||||
"""
|
||||
# Check if the file exists
|
||||
if not os.path.exists(default_workflow_file_path):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Dev mode is currently in beta. It only supports updating workflow file at 'app/workflow.py'",
|
||||
)
|
||||
stat = os.stat(default_workflow_file_path)
|
||||
with open(default_workflow_file_path, "r") as f:
|
||||
return WorkflowFile(
|
||||
last_modified=int(stat.st_mtime),
|
||||
file_path=default_workflow_file_path,
|
||||
content=f.read(),
|
||||
)
|
||||
|
||||
@router.post("/files/workflow/validate")
|
||||
async def validate_workflow(file: WorkflowFileUpdate) -> WorkflowValidationResult:
|
||||
"""
|
||||
Validate the current workflow code
|
||||
"""
|
||||
try:
|
||||
if file.file_path != default_workflow_file_path:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Updating {file.file_path} is not allowed"
|
||||
)
|
||||
validate_workflow_file(
|
||||
workflow_content=file.content,
|
||||
factory_signature=server_settings.workflow_factory_signature,
|
||||
)
|
||||
return WorkflowValidationResult(valid=True, error="")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.put("/files/workflow")
|
||||
async def put_workflow_file(update: WorkflowFileUpdate) -> None:
|
||||
"""
|
||||
Update the current workflow code
|
||||
"""
|
||||
# Validations
|
||||
if update.file_path != default_workflow_file_path:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Updating {update.file_path} is not allowed"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as tmp:
|
||||
tmp.write(update.content)
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
# Validate workflow file using the actual callable name from the workflow_factory
|
||||
factory_func_name = server_settings.workflow_factory_signature
|
||||
validate_workflow_file(
|
||||
workflow_path=tmp_path, factory_signature=factory_func_name
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
# If all checks pass, overwrite the real file
|
||||
with open(default_workflow_file_path, "w") as f:
|
||||
f.write(update.content)
|
||||
|
||||
return router
|
||||
@@ -10,7 +10,11 @@ from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from llama_index.core.workflow import Workflow
|
||||
from llama_index.server.api.routers import chat_router, custom_components_router
|
||||
from llama_index.server.api.routers import (
|
||||
chat_router,
|
||||
custom_components_router,
|
||||
dev_router,
|
||||
)
|
||||
from llama_index.server.chat_ui import download_chat_ui
|
||||
from llama_index.server.settings import server_settings
|
||||
|
||||
@@ -33,6 +37,9 @@ class UIConfig(BaseModel):
|
||||
component_dir: Optional[str] = Field(
|
||||
default=None, description="The directory to custom UI components code"
|
||||
)
|
||||
dev_mode: bool = Field(
|
||||
default=False, description="Whether to enable the UI dev mode"
|
||||
)
|
||||
|
||||
def get_config_content(self) -> str:
|
||||
return json.dumps(
|
||||
@@ -46,6 +53,7 @@ class UIConfig(BaseModel):
|
||||
"COMPONENTS_API": f"{server_settings.api_url}/components"
|
||||
if self.component_dir
|
||||
else None,
|
||||
"DEV_MODE": self.dev_mode,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
@@ -100,24 +108,33 @@ class LlamaIndexServer(FastAPI):
|
||||
server_settings.set_url(server_url)
|
||||
if api_prefix:
|
||||
server_settings.set_api_prefix(api_prefix)
|
||||
|
||||
if self.use_default_routers:
|
||||
self.add_default_routers()
|
||||
server_settings.set_workflow_factory(workflow_factory.__name__)
|
||||
|
||||
if str(env).lower() == "dev":
|
||||
self.allow_cors("*")
|
||||
if self.ui_config.enabled is None:
|
||||
self.ui_config.enabled = True
|
||||
|
||||
else:
|
||||
if self.ui_config.enabled and self.ui_config.dev_mode:
|
||||
raise ValueError(
|
||||
"UI dev mode requires the environment variable for LlamaIndexServer to be set to 'dev' and start the FastAPI app in dev mode."
|
||||
)
|
||||
if self.ui_config.enabled is None:
|
||||
self.ui_config.enabled = False
|
||||
|
||||
# Routers
|
||||
if self.use_default_routers:
|
||||
self.add_default_routers()
|
||||
|
||||
# Should mount ui at the end
|
||||
if self.ui_config.enabled:
|
||||
self.mount_ui()
|
||||
|
||||
# Default routers
|
||||
def add_default_routers(self) -> None:
|
||||
self.add_chat_router()
|
||||
if self.ui_config.enabled and self.ui_config.dev_mode:
|
||||
self.include_router(dev_router(), prefix=server_settings.api_prefix)
|
||||
self.mount_data_dir()
|
||||
self.mount_output_dir()
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ class ServerSettings(BaseSettings):
|
||||
default="/api",
|
||||
description="The prefix for the API endpoints",
|
||||
)
|
||||
workflow_factory_signature: str = Field(
|
||||
default="",
|
||||
description="The signature of the workflow factory function",
|
||||
)
|
||||
|
||||
@property
|
||||
def file_server_url_prefix(self) -> str:
|
||||
@@ -40,6 +44,9 @@ class ServerSettings(BaseSettings):
|
||||
self.api_prefix = v
|
||||
self.validate_api_prefix(v) # type: ignore
|
||||
|
||||
def set_workflow_factory(self, v: str) -> None:
|
||||
self.workflow_factory_signature = v
|
||||
|
||||
class Config:
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Utilities for validating workflow.py files (syntax and import checks only).
|
||||
"""
|
||||
|
||||
import ast
|
||||
import importlib.util
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def validate_workflow_file(
|
||||
workflow_path: Optional[str] = None,
|
||||
workflow_content: Optional[str] = None,
|
||||
factory_signature: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Validate that the workflow file is syntactically correct, can be imported, and defines a callable factory function with the given name.
|
||||
Raises an exception if invalid.
|
||||
"""
|
||||
if workflow_path is None and workflow_content is None:
|
||||
raise ValueError("Either workflow_path or workflow_content must be provided")
|
||||
|
||||
# 1. Syntax check
|
||||
if workflow_path is not None:
|
||||
with open(workflow_path, "r") as f:
|
||||
content = f.read()
|
||||
else:
|
||||
if workflow_content is None:
|
||||
raise ValueError(
|
||||
"workflow_content must be provided if workflow_path is not specified"
|
||||
)
|
||||
content = workflow_content
|
||||
try:
|
||||
ast.parse(content)
|
||||
except SyntaxError as e:
|
||||
raise ValueError(f"Syntax error in workflow: {e}")
|
||||
|
||||
# 2. Import check (will catch missing modules, etc.)
|
||||
spec = importlib.util.spec_from_file_location("workflow", workflow_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ValueError(f"Could not load module specification for {workflow_path}")
|
||||
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(mod)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Import error: {e}")
|
||||
|
||||
# 3. Contract validation: require the given factory function name
|
||||
if factory_signature:
|
||||
if not hasattr(mod, factory_signature):
|
||||
raise ValueError(f"Missing required function: '{factory_signature}'")
|
||||
obj = getattr(mod, factory_signature)
|
||||
if not callable(obj):
|
||||
raise ValueError(f"'{factory_signature}' is not callable")
|
||||
Generated
+2678
-2677
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user