Compare commits

..

28 Commits

Author SHA1 Message Date
leehuwuj 2f17cdcd64 Add ArtifactEvent model and update workflows to use it 2025-04-28 15:22:48 +07:00
leehuwuj 6104c3d78b remove include last artifact in the code 2025-04-28 15:08:57 +07:00
leehuwuj 88af75c995 fix missing set memory 2025-04-28 14:57:09 +07:00
leehuwuj 484cb9c163 disable e2e test for python package change 2025-04-28 14:05:55 +07:00
leehuwuj 830b3e5643 revert create-llama change 2025-04-28 14:03:35 +07:00
leehuwuj 51f70a9d3e fix mypy 2025-04-28 13:54:34 +07:00
leehuwuj 11ae75ddcc fix adding custom route does not work 2025-04-28 12:13:06 +07:00
leehuwuj e09de607b9 fix mypy 2025-04-28 09:45:58 +07:00
leehuwuj 872f23859b sort artifact 2025-04-25 19:51:22 +07:00
leehuwuj 9e42cf31c4 Merge remote-tracking branch 'origin/main' into lee/add-artifact 2025-04-25 19:41:51 +07:00
leehuwuj fbed55a8d8 Merge remote-tracking branch 'origin/main' into lee/add-artifact 2025-04-25 19:38:04 +07:00
leehuwuj 1ba5a26141 move code 2025-04-25 19:31:33 +07:00
leehuwuj 4fdfca534e Refactor artifact workflows and UI components
- Updated `code_workflow.py` and `document_workflow.py` to improve chat history handling and user message storage.
- Enhanced `ArtifactWorkflow` to utilize optional fields in the `Requirement` model.
- Revised prompt instructions for clarity and conciseness in generating requirements.
- Modified UI event components to reflect changes in workflow stages and improve user feedback.
- Improved error handling for JSON parsing in artifact annotations.
2025-04-25 19:25:31 +07:00
leehuwuj f975b7924a Use uv to release package 2025-04-25 16:18:44 +07:00
leehuwuj 2c3ac6d47b Refactor artifact workflow classes and UI event handling
- Renamed `ArtifactUIEvents` to `UIEventData` for clarity.
- Introduced `last_artifact` attribute in `ArtifactWorkflow` to streamline artifact retrieval.
- Updated chat history handling to utilize the new `last_artifact` attribute.
- Modified event streaming to use `UIEventData` for consistent event structure.
- Added a new UI component for displaying artifact workflow status and progress.
2025-04-25 15:11:41 +07:00
leehuwuj 322f43d5c9 remove app_writer workflow 2025-04-25 14:15:53 +07:00
leehuwuj ef56e1d85d Add artifact workflows for code and document generation
- Introduced `code_workflow.py` for generating and updating code artifacts based on user requests.
- Introduced `document_workflow.py` for generating and updating document artifacts (Markdown/HTML).
- Created `main.py` to set up FastAPI server with artifact workflows.
- Added a README for setup instructions and usage.
- Implemented UI components for displaying artifact status and progress.
- Updated chat router to remove unused event callbacks.
2025-04-25 14:14:52 +07:00
leehuwuj 66636e50ee remove dead code 2025-04-25 08:37:51 +07:00
leehuwuj 2b27ebd828 revert changes 2025-04-25 08:34:03 +07:00
leehuwuj 03ee8c3ae3 bump chat ui 2025-04-25 08:30:34 +07:00
leehuwuj f85e913f8f fix test 2025-04-24 14:48:49 +07:00
leehuwuj 11fa3bc562 remove previous content from tool input 2025-04-24 12:08:25 +07:00
leehuwuj 96d846fffd enhance code 2025-04-24 12:04:00 +07:00
leehuwuj 630a76d6c8 Refactor artifact generation tools by introducing separate CodeGenerator and DocumentGenerator classes. Update app_writer to utilize FunctionAgent for code and document generation workflows. Remove deprecated ArtifactGenerator class. Enhance artifact transformation logic in callbacks. Improve system prompts for clarity and instruction adherence. 2025-04-24 11:21:21 +07:00
leehuwuj d787ecf6a3 update ci 2025-04-24 08:57:13 +07:00
leehuwuj 629a1797dc fix ci 2025-04-24 08:36:20 +07:00
leehuwuj ba2939164b migrate poetry to uv 2025-04-23 20:35:08 +07:00
leehuwuj be4550f035 support artifact 2025-04-23 20:28:34 +07:00
181 changed files with 748 additions and 21835 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
chore: create-llama monorepo
@@ -31,13 +31,6 @@ jobs:
- name: Run Prettier
run: pnpm run format
- name: Run build
run: pnpm run build
- name: Run Typecheck for examples
run: pnpm run typecheck
working-directory: packages/server/examples
- name: Run Python format check
uses: chartboost/ruff-action@v1
with:
+1 -2
View File
@@ -1,4 +1,3 @@
pnpm format
pnpm lint
uvx ruff check .
uvx ruff format . --check
uvx ruff format --check packages/create-llama/templates/
-15
View File
@@ -1,15 +0,0 @@
node_modules/
pnpm-lock.yaml
lib/
dist/
cache/
build/
.next/
out/
packages/server/server/
# Python
python/
**/*.mypy_cache/**
**/*.venv/**
**/*.ruff_cache/**
@@ -1,25 +1,5 @@
# create-llama
## 0.5.13
### Patch Changes
- f4ca602: Add artifact use case for Typescript template
- f4ca602: Update typescript use cases to use the new workflow engine
## 0.5.12
### Patch Changes
- 241d82a: Add artifacts use case (python)
## 0.5.11
### Patch Changes
- 3960618: chore: create-llama monorepo
- 8fe5fc2: chore: add llamaindex server package
## 0.5.10
### Patch Changes
-62
View File
@@ -1,62 +0,0 @@
import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ["packages/create-llama/**"],
rules: {
"max-params": ["error", 4],
"prefer-const": "error",
"no-empty": "off",
"no-extra-boolean-cast": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
},
{
files: ["packages/server/**"],
rules: {
"no-irregular-whitespace": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": [
"error",
{
ignoreRestArgs: true,
},
],
},
},
{
ignores: [
"python/**",
"**/*.mypy_cache/**",
"**/*.venv/**",
"**/*.ruff_cache/**",
"**/dist/**",
"**/e2e/cache/**",
"**/lib/*",
"**/.next/**",
"**/out/**",
"**/node_modules/**",
"**/build/**",
],
},
);
+37 -53
View File
@@ -1,55 +1,39 @@
{
"name": "create-llama-monorepo",
"version": "1.0.0",
"private": true,
"description": "Monorepo for create-llama",
"keywords": [
"rag",
"llamaindex"
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama"
},
"license": "MIT",
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "pnpm -r dev",
"build": "pnpm -r build",
"e2e": "pnpm -r e2e",
"lint": "eslint .",
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"prepare": "husky",
"new-snapshot": "pnpm -r build && changeset version --snapshot",
"new-version": "pnpm -r build && changeset version",
"release": "pnpm -r build && changeset publish",
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"bunchee": "6.4.0",
"husky": "^9.0.10",
"lint-staged": "^15.2.11",
"typescript-eslint": "^8.18.0",
"globals": "^15.12.0",
"eslint": "9.22.0",
"@eslint/js": "^9.25.0",
"eslint-config-next": "^15.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "7.37.2",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.7.3",
"@types/node": "^22.9.0",
"@types/react": "^19",
"@types/react-dom": "^19"
},
"packageManager": "pnpm@9.0.5",
"engines": {
"node": ">=16.14.0"
}
"name": "create-llama-monorepo",
"version": "1.0.0",
"private": true,
"description": "Monorepo for create-llama",
"keywords": [
"rag",
"llamaindex"
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama"
},
"license": "MIT",
"workspaces": [
"packages/*"
],
"scripts": {
"prepare": "husky",
"new-snapshot": "pnpm -r build && changeset version --snapshot",
"new-version": "pnpm -r build && changeset version",
"release": "pnpm -r build && changeset publish",
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot",
"build": "pnpm -r --filter create-llama build",
"e2e": "pnpm -r --filter create-llama e2e",
"dev": "pnpm -r --filter create-llama dev",
"format": "pnpm -r --filter create-llama format",
"format:write": "pnpm -r --filter create-llama format:write",
"lint": "pnpm -r --filter create-llama lint"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"husky": "^9.0.10"
},
"packageManager": "pnpm@9.0.5",
"engines": {
"node": ">=16.14.0"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": [
"prettier"
],
"rules": {
"max-params": [
"error",
4
],
"prefer-const": "error",
},
}
-4
View File
@@ -59,7 +59,3 @@ __pycache__
# build artifacts
create-llama-*.tgz
# copied from root
README.md
LICENSE.md
+6
View File
@@ -0,0 +1,6 @@
apps/docs/i18n
apps/docs/docs/api
pnpm-lock.yaml
lib/
dist/
.docusaurus/
@@ -130,11 +130,4 @@ Pro mode is ideal for developers who want fine-grained control over their projec
- [TS/JS docs](https://ts.llamaindex.ai/)
- [Python docs](https://docs.llamaindex.ai/en/stable/)
## LlamaIndex Server
The generated code is using the LlamaIndex Server, which serves LlamaIndex Workflows and Agent Workflows via an API server. See the following docs for more information:
- [LlamaIndex Server For TypeScript](./packages/server/README.md)
- [LlamaIndex Server For Python](./python/llama-index-server/README.md)
Inspired by and adapted from [create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import path from "path";
import { green, yellow } from "picocolors";
import { tryGitInit } from "./helpers/git";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
+2 -2
View File
@@ -67,8 +67,8 @@ export async function runCreateLlama({
].join("-");
// Handle different data source types
const dataSourceArgs = [];
if (dataSource.includes("--web-source")) {
let dataSourceArgs = [];
if (dataSource.includes("--web-source" || "--db-source")) {
const webSource = dataSource.split(" ")[1];
dataSourceArgs.push("--web-source", webSource);
} else if (dataSource.includes("--db-source")) {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { async as glob } from "fast-glob";
import fs from "fs";
import path from "path";
@@ -181,7 +181,7 @@ const getVectorDBEnvs = (
]
: []),
];
case "chroma": {
case "chroma":
const envs = [
{
name: "CHROMA_COLLECTION",
@@ -206,7 +206,6 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
});
}
return envs;
}
case "weaviate":
return [
{
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import spawn from "cross-spawn";
import { yellow } from "picocolors";
import type { PackageManager } from "./get-pkg-manager";
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from "fs";
import path from "path";
import { blue, green } from "picocolors";
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
@@ -28,7 +28,7 @@ export async function askModelConfig({
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
if (askModels) {
const choices = [
let choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
{ title: "Ollama", value: "ollama" },
+1 -2
View File
@@ -31,7 +31,6 @@ const getAdditionalDependencies = (
tools?: Tool[],
templateType?: TemplateType,
observability?: TemplateObservability,
// eslint-disable-next-line max-params
) => {
const dependencies: Dependency[] = [];
@@ -563,7 +562,7 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("*.py", path.join(root, "app"), {
await copy("workflow.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
});
+1 -2
View File
@@ -57,8 +57,7 @@ export type TemplateUseCase =
| "form_filling"
| "extractor"
| "contract_review"
| "agentic_rag"
| "artifacts";
| "agentic_rag";
// Config for both file and folder
export type FileSourceConfig =
| {
+2 -2
View File
@@ -31,7 +31,7 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("*.ts", path.join(root, "src", "app"), {
await copy("workflow.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
@@ -516,7 +516,7 @@ async function updatePackageJson({
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "^3.0.0",
"@llamaindex/readers": "^2.0.0",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import validateProjectName from "validate-npm-package-name";
export function validateNpmName(name: string): {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import { Command } from "commander";
import fs from "fs";
+9 -6
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.13",
"version": "0.5.10",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -17,20 +17,19 @@
"create-llama": "./dist/index.js"
},
"files": [
"dist",
"README.md",
"LICENSE.md"
"dist"
],
"scripts": {
"copy": "cp -r ../../README.md ../../LICENSE.md .",
"build": "bash ./scripts/build.sh",
"build:ncc": "pnpm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"postbuild": "pnpm run copy",
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
"dev": "ncc build ./index.ts -w -o dist/",
"e2e": "playwright test",
"e2e:python": "playwright test e2e/shared e2e/python",
"e2e:typescript": "playwright test e2e/shared e2e/typescript",
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
"pack-install": "bash ./scripts/pack.sh"
},
"dependencies": {
@@ -63,6 +62,10 @@
"yaml": "2.4.1"
},
"devDependencies": {
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"@playwright/test": "^1.41.1",
"@vercel/ncc": "0.38.1",
"rimraf": "^5.0.5",
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
+20 -34
View File
@@ -6,11 +6,7 @@ import { ModelConfig, TemplateFramework } from "../helpers/types";
import { PureQuestionArgs, QuestionResults } from "./types";
import { askPostInstallAction, questionHandlers } from "./utils";
type AppType =
| "agentic_rag"
| "financial_report"
| "deep_research"
| "artifacts";
type AppType = "agentic_rag" | "financial_report" | "deep_research";
type SimpleAnswers = {
appType: AppType;
@@ -46,12 +42,6 @@ export const askSimpleQuestions = async (
description:
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
},
{
title: "Artifacts",
value: "artifacts",
description:
"Build your own Vercel's v0 or OpenAI's canvas-styled UI.",
},
],
},
questionHandlers,
@@ -62,19 +52,21 @@ export const askSimpleQuestions = async (
let useLlamaCloud = false;
const { language: newLanguage } = await prompts(
{
type: "select",
name: "language",
message: "What language do you want to use?",
choices: [
{ title: "Python (FastAPI)", value: "fastapi" },
{ title: "Typescript (NextJS)", value: "nextjs" },
],
},
questionHandlers,
);
language = newLanguage;
if (appType !== "extractor" && appType !== "contract_review") {
const { language: newLanguage } = await prompts(
{
type: "select",
name: "language",
message: "What language do you want to use?",
choices: [
{ title: "Python (FastAPI)", value: "fastapi" },
{ title: "Typescript (NextJS)", value: "nextjs" },
],
},
questionHandlers,
);
language = newLanguage;
}
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
{
@@ -119,10 +111,10 @@ const convertAnswers = async (
args: PureQuestionArgs,
answers: SimpleAnswers,
): Promise<QuestionResults> => {
const MODEL_GPT41: ModelConfig = {
const MODEL_GPT4o: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4.1",
model: "gpt-4o",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
@@ -143,19 +135,13 @@ const convertAnswers = async (
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: getTools(["interpreter", "document_generator"]),
modelConfig: MODEL_GPT41,
modelConfig: MODEL_GPT4o,
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: [],
modelConfig: MODEL_GPT41,
},
artifacts: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
modelConfig: MODEL_GPT4o,
},
};
@@ -191,7 +191,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
case "png":
case "jpeg":
case "svg":
case "pdf": {
case "pdf":
const { filename } = this.saveToDisk(data, ext);
output.push({
type: ext as InterpreterExtraType,
@@ -199,7 +199,6 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
url: this.getFileUrl(filename),
});
break;
}
default:
output.push({
type: ext as InterpreterExtraType,
@@ -1,9 +1,5 @@
import {
Document,
LLamaCloudFileService,
LlamaCloudIndex,
VectorStoreIndex,
} from "llamaindex";
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { DocumentFile } from "../streaming/annotations";
import { parseFile, storeFile } from "./helper";
import { runPipeline } from "./pipeline";
@@ -10,7 +10,6 @@ dependencies = [
"python-dotenv>=1.0.0",
"pydantic<2.10",
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"cachetools>=5.3.3",
"reflex>=0.6.2.post1",
]
@@ -11,7 +11,6 @@ dependencies = [
"python-dotenv>=1.0.0",
"pydantic<2.10",
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"cachetools>=5.3.3",
"reflex>=0.6.2.post1",
]
@@ -6,7 +6,7 @@ import { Message } from "./chat-messages";
export default function ChatAvatar(message: Message) {
if (message.role === "user") {
return (
<div className="bg-background flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow">
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
@@ -20,7 +20,7 @@ export default function ChatAvatar(message: Message) {
}
return (
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white">
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white">
<Image
className="rounded-md"
src="/llama.png"
@@ -23,20 +23,20 @@ export default function ChatInput(props: ChatInputProps) {
<>
<form
onSubmit={props.handleSubmit}
className="flex w-full max-w-5xl items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl"
className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4"
>
<input
autoFocus
name="message"
placeholder="Type a message"
className="w-full flex-1 rounded-xl p-4 shadow-inner"
className="w-full p-4 rounded-xl shadow-inner flex-1"
value={props.input}
onChange={props.handleInputChange}
/>
<button
disabled={props.isLoading}
type="submit"
className="rounded-xl bg-gradient-to-r from-cyan-500 to-sky-500 p-4 text-white shadow-xl disabled:cursor-not-allowed disabled:opacity-50"
className="p-4 text-white rounded-xl shadow-xl bg-gradient-to-r from-cyan-500 to-sky-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send message
</button>
@@ -7,7 +7,7 @@ export default function ChatItem(message: Message) {
return (
<div className="flex items-start gap-4 pt-5">
<ChatAvatar {...message} />
<p className="whitespace-pre-wrap break-words">{message.content}</p>
<p className="break-words whitespace-pre-wrap">{message.content}</p>
</div>
);
}
@@ -39,7 +39,7 @@ export default function ChatMessages({
return (
<div
className="w-full max-w-5xl flex-1 overflow-auto rounded-xl bg-white p-4 shadow-xl"
className="flex-1 w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl overflow-auto"
ref={scrollableChatContainerRef}
>
<div className="flex flex-col gap-5 divide-y">
@@ -1,137 +0,0 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { Markdown } from "@llamaindex/chat-ui/widgets";
import { ListChecks, Loader2, Wand2 } from "lucide-react";
import { useEffect, useState } from "react";
const STAGE_META = {
plan: {
icon: ListChecks,
badgeText: "Step 1/2: Planning",
gradient: "from-blue-100 via-blue-50 to-white",
progress: 33,
iconBg: "bg-blue-100 text-blue-600",
badge: "bg-blue-100 text-blue-700",
},
generate: {
icon: Wand2,
badgeText: "Step 2/2: Generating",
gradient: "from-violet-100 via-violet-50 to-white",
progress: 66,
iconBg: "bg-violet-100 text-violet-600",
badge: "bg-violet-100 text-violet-700",
},
};
function ArtifactWorkflowCard({ event }) {
const [visible, setVisible] = useState(event?.state !== "completed");
const [fade, setFade] = useState(false);
useEffect(() => {
if (event?.state === "completed") {
setVisible(false);
} else {
setVisible(true);
setFade(false);
}
}, [event?.state]);
if (!event || !visible) return null;
const { state, requirement } = event;
const meta = STAGE_META[state];
if (!meta) return null;
return (
<div className="flex min-h-[180px] w-full items-center justify-center py-2">
<Card
className={cn(
"w-full rounded-xl shadow-md transition-all duration-500",
"border-0",
fade && "pointer-events-none opacity-0",
`bg-gradient-to-br ${meta.gradient}`,
)}
style={{
boxShadow:
"0 2px 12px 0 rgba(80, 80, 120, 0.08), 0 1px 3px 0 rgba(80, 80, 120, 0.04)",
}}
>
<CardHeader className="flex flex-row items-center gap-2 px-3 pb-1 pt-2">
<div
className={cn(
"flex items-center justify-center rounded-full p-1",
meta.iconBg,
)}
>
<meta.icon className="h-5 w-5" />
</div>
<CardTitle className="flex items-center gap-2 text-base font-semibold">
<Badge className={cn("ml-1", meta.badge, "px-2 py-0.5 text-xs")}>
{meta.badgeText}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="px-3 py-1">
{state === "plan" && (
<div className="flex flex-col items-center gap-2 py-2">
<Loader2 className="mb-1 h-6 w-6 animate-spin text-blue-400" />
<div className="text-center text-sm font-medium text-blue-900">
Analyzing your request...
</div>
<Skeleton className="mt-1 h-3 w-1/2 rounded-full" />
</div>
)}
{state === "generate" && (
<div className="flex flex-col gap-2 py-2">
<div className="flex items-center gap-1">
<Loader2 className="h-4 w-4 animate-spin text-violet-400" />
<span className="text-sm font-medium text-violet-900">
Working on the requirement:
</span>
</div>
<div className="max-h-24 overflow-auto rounded-lg border border-violet-200 bg-violet-50 px-2 py-1 text-xs">
{requirement ? (
<Markdown content={requirement} />
) : (
<span className="italic text-violet-400">
No requirements available yet.
</span>
)}
</div>
</div>
)}
</CardContent>
<div className="px-3 pb-2 pt-1">
<Progress
value={meta.progress}
className={cn(
"h-1 rounded-full bg-gray-200",
state === "plan" && "bg-blue-200",
state === "generate" && "bg-violet-200",
)}
indicatorClassName={cn(
"transition-all duration-500",
state === "plan" && "bg-blue-500",
state === "generate" && "bg-violet-500",
)}
/>
</div>
</Card>
</div>
);
}
export default function Component({ events }) {
const aggregateEvents = () => {
if (!events || events.length === 0) return null;
return events[events.length - 1];
};
const event = aggregateEvents();
return <ArtifactWorkflowCard event={event} />;
}
@@ -97,7 +97,7 @@ export default function Component({ events }) {
case "pending":
return <Clock className="h-4 w-4 text-gray-400" />;
case "inprogress":
return <Loader2 className="h-4 w-4 animate-spin text-blue-500" />;
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
case "done":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "error":
@@ -140,9 +140,9 @@ export default function Component({ events }) {
};
return (
<div className="mx-auto w-full max-w-4xl space-y-6 p-4">
<div className="w-full max-w-4xl mx-auto space-y-6 p-4">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">DeepResearch Workflow</h1>
<div className="flex items-center space-x-2">
<Badge
@@ -188,7 +188,7 @@ export default function Component({ events }) {
className={cn(
"border-2 transition-all duration-300",
retrieve?.state === "inprogress"
? "border-blue-500 shadow-lg shadow-blue-100"
? "border-blue-500 shadow-blue-100 shadow-lg"
: retrieve?.state === "done"
? "border-green-500"
: retrieve?.state === "error"
@@ -231,7 +231,7 @@ export default function Component({ events }) {
className={cn(
"border-2 transition-all duration-300",
analyze?.state === "inprogress"
? "border-blue-500 shadow-lg shadow-blue-100"
? "border-blue-500 shadow-blue-100 shadow-lg"
: analyze?.state === "done"
? "border-green-500"
: analyze?.state === "error"
@@ -288,9 +288,9 @@ export default function Component({ events }) {
key={answer.id}
value={answer.id}
className={cn(
"mb-4 overflow-hidden rounded-lg border",
"mb-4 border rounded-lg overflow-hidden",
answer.state === "inprogress"
? "border-blue-500 shadow-sm shadow-blue-100"
? "border-blue-500 shadow-blue-100 shadow-sm"
: answer.state === "done"
? "border-green-100"
: answer.state === "error"
@@ -309,7 +309,7 @@ export default function Component({ events }) {
<Badge
variant="outline"
className={cn(
"ml-auto flex shrink-0 items-center space-x-1",
"ml-auto flex items-center space-x-1 shrink-0",
answer.state === "inprogress"
? "text-blue-500"
: answer.state === "done"
@@ -327,7 +327,7 @@ export default function Component({ events }) {
<AccordionContent className="px-4 pb-4 pt-1">
<div
className={cn(
"rounded-md p-3",
"p-3 rounded-md",
answer.state === "done"
? "bg-green-50"
: answer.state === "inprogress"
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { AstraDBVectorStore } from "@llamaindex/astra";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { AstraDBVectorStore } from "@llamaindex/astra";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { ChromaVectorStore } from "@llamaindex/chroma";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { ChromaVectorStore } from "@llamaindex/chroma";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MilvusVectorStore } from "@llamaindex/milvus";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch } from "@llamaindex/mongodb";
import * as dotenv from "dotenv";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch } from "@llamaindex/mongodb";
import { VectorStoreIndex } from "llamaindex";
import { MongoClient } from "mongodb";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { PineconeVectorStore } from "@llamaindex/pinecone";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { PineconeVectorStore } from "@llamaindex/pinecone";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { QdrantVectorStore } from "@llamaindex/qdrant";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,3 +1,4 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { WeaviateVectorStore } from "@llamaindex/weaviate";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,69 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with uv:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
uv sync
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM.
Then, run the development server:
```shell
uv run fastapi dev
```
Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI.
To start the app optimized for **production**, run:
```
uv run fastapi run
```
## Configure LLM and Embedding Model
You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py).
## Use Case
We have prepared two artifact workflows:
- [Code Workflow](app/code_workflow.py): To generate code and display it in the UI like Vercel's v0.
- [Document Workflow](app/document_workflow.py): Generate and update a document like OpenAI's canvas.
Modify the factory method in [`workflow.py`](app/workflow.py) to decide which artifact workflow to use. Without any changes the Code Workflow is used.
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
```
## Customize the UI
To customize the UI, you can start by modifying the [./components/ui_event.jsx](./components/ui_event.jsx) file.
You can also generate a new code for the workflow using LLM by running the following command:
```
uv run generate_ui
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
- [LlamaIndex Server](https://pypi.org/project/llama-index-server/)
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,365 +0,0 @@
import re
import time
from typing import Any, Literal, Optional, Union
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.llms import LLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.prompts import PromptTemplate
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.api.models import (
Artifact,
ArtifactEvent,
ArtifactType,
ChatRequest,
CodeArtifactData,
UIEvent,
)
from llama_index.server.api.utils import get_last_artifact
from pydantic import BaseModel, Field
class Requirement(BaseModel):
next_step: Literal["answering", "coding"]
language: Optional[str] = None
file_name: Optional[str] = None
requirement: str
class PlanEvent(Event):
user_msg: str
context: Optional[str] = None
class GenerateArtifactEvent(Event):
requirement: Requirement
class SynthesizeAnswerEvent(Event):
pass
class UIEventData(BaseModel):
"""
Event data for updating workflow status to the UI.
"""
state: Literal["plan", "generate", "completed"] = Field(
description="The current state of the workflow. "
"plan: analyze and create a plan for the next step. "
"generate: generate the artifact based on the requirement from the previous step. "
"completed: the workflow is completed. "
)
requirement: Optional[str] = Field(
description="The requirement for generating the artifact. ",
default=None,
)
class CodeArtifactWorkflow(Workflow):
"""
A simple workflow that help generate/update the chat artifact (code, document)
e.g: Help create a NextJS app.
Update the generated code with the user's feedback.
Generate a guideline for the app,...
"""
def __init__(
self,
llm: LLM,
chat_request: ChatRequest,
**kwargs: Any,
):
"""
Args:
llm: The LLM to use.
chat_request: The chat request from the chat app to use.
"""
super().__init__(**kwargs)
self.llm = llm
self.chat_request = chat_request
self.last_artifact = get_last_artifact(chat_request)
@step
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> PlanEvent:
user_msg = ev.user_msg
if user_msg is None:
raise ValueError("user_msg is required to run the workflow")
await ctx.set("user_msg", user_msg)
chat_history = ev.chat_history or []
chat_history.append(
ChatMessage(
role="user",
content=user_msg,
)
)
memory = ChatMemoryBuffer.from_defaults(
chat_history=chat_history,
llm=self.llm,
)
await ctx.set("memory", memory)
return PlanEvent(
user_msg=user_msg,
context=str(self.last_artifact.model_dump_json())
if self.last_artifact
else "",
)
@step
async def planning(
self, ctx: Context, event: PlanEvent
) -> Union[GenerateArtifactEvent, SynthesizeAnswerEvent]:
"""
Based on the conversation history and the user's request
this step will help to provide a good next step for the code or document generation.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="plan",
requirement=None,
),
)
)
prompt = PromptTemplate("""
You are a product analyst responsible for analyzing the user's request and providing the next step for code or document generation.
You are helping user with their code artifact. To update the code, you need to plan a coding step.
Follow these instructions:
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
2. The next step must be one of the following two options:
- "coding": To make the changes to the current code.
- "answering": If you don't need to update the current code or need clarification from the user.
Important: Avoid telling the user to update the code themselves, you are the one who will update the code (by planning a coding step).
3. If the next step is "coding", you may specify the language ("typescript" or "python") and file_name if known, otherwise set them to null.
4. The requirement must be provided clearly what is the user request and what need to be done for the next step in details
as precise and specific as possible, don't be stingy with in the requirement.
5. If the next step is "answering", set language and file_name to null, and the requirement should describe what to answer or explain to the user.
6. Be concise; only return the requirements for the next step.
7. The requirements must be in the following format:
```json
{
"next_step": "answering" | "coding",
"language": "typescript" | "python" | null,
"file_name": string | null,
"requirement": string
}
```
## Example 1:
User request: Create a calculator app.
You should return:
```json
{
"next_step": "coding",
"language": "typescript",
"file_name": "calculator.tsx",
"requirement": "Generate code for a calculator app that has a simple UI with a display and button layout. The display should show the current input and the result. The buttons should include basic operators, numbers, clear, and equals. The calculation should work correctly."
}
```
## Example 2:
User request: Explain how the game loop works.
Context: You have already generated the code for a snake game.
You should return:
```json
{
"next_step": "answering",
"language": null,
"file_name": null,
"requirement": "The user is asking about the game loop. Explain how the game loop works."
}
```
{context}
Now, plan the user's next step for this request:
{user_msg}
""").format(
context=""
if event.context is None
else f"## The context is: \n{event.context}\n",
user_msg=event.user_msg,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# parse the response to Requirement
# 1. use regex to find the json block
json_block = re.search(
r"```(?:json)?\s*([\s\S]*?)\s*```", response.text, re.IGNORECASE
)
if json_block is None:
raise ValueError("No JSON block found in the response.")
# 2. parse the json block to Requirement
requirement = Requirement.model_validate_json(json_block.group(1).strip())
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=requirement.requirement,
),
)
)
# Put the planning result to the memory
# useful for answering step
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"The plan for next step: \n{response.text}",
)
)
await ctx.set("memory", memory)
if requirement.next_step == "coding":
return GenerateArtifactEvent(
requirement=requirement,
)
else:
return SynthesizeAnswerEvent()
@step
async def generate_artifact(
self, ctx: Context, event: GenerateArtifactEvent
) -> SynthesizeAnswerEvent:
"""
Generate the code based on the user's request.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=event.requirement.requirement,
),
)
)
prompt = PromptTemplate("""
You are a skilled developer who can help user with coding.
You are given a task to generate or update a code for a given requirement.
## Follow these instructions:
**1. Carefully read the user's requirements.**
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
If the previous code is provided:
+ Carefully analyze the code with the request to make the right changes.
+ Avoid making a lot of changes from the previous code if the request is not to write the code from scratch again.
**2. For code requests:**
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
The import pattern should be:
```
import { ComponentName } from "@/components/ui/component-name"
import { Markdown } from "@llamaindex/chat-ui"
import { cn } from "@/lib/utils"
```
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
- Only generate code relevant to the user's request—do not add extra boilerplate.
**3. Don't be verbose on response**
- No other text or comments only return the code which wrapped by ```language``` block.
- If the user's request is to update the code, only return the updated code.
**4. Only the following languages are allowed: "typescript", "python".**
**5. If there is no code to update, return the reason without any code block.**
## Example:
```typescript
import React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export default function MyComponent() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<Button>Click me</Button>
</div>
);
}
The previous code is:
{previous_artifact}
Now, i have to generate the code for the following requirement:
{requirement}
```
""").format(
previous_artifact=self.last_artifact.model_dump_json()
if self.last_artifact
else "",
requirement=event.requirement,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# Extract the code from the response
language_pattern = r"```(\w+)([\s\S]*)```"
code_match = re.search(language_pattern, response.text)
if code_match is None:
return SynthesizeAnswerEvent()
else:
code = code_match.group(2).strip()
# Put the generated code to the memory
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"Updated the code: \n{response.text}",
)
)
# To show the Canvas panel for the artifact
ctx.write_event_to_stream(
ArtifactEvent(
data=Artifact(
type=ArtifactType.CODE,
created_at=int(time.time()),
data=CodeArtifactData(
language=event.requirement.language or "",
file_name=event.requirement.file_name or "",
code=code,
),
),
)
)
return SynthesizeAnswerEvent()
@step
async def synthesize_answer(
self, ctx: Context, event: SynthesizeAnswerEvent
) -> StopEvent:
"""
Synthesize the answer.
"""
memory: ChatMemoryBuffer = await ctx.get("memory")
chat_history = memory.get()
chat_history.append(
ChatMessage(
role="system",
content="""
You are a helpful assistant who is responsible for explaining the work to the user.
Based on the conversation history, provide an answer to the user's question.
The user has access to the code so avoid mentioning the whole code again in your response.
""",
)
)
response_stream = await self.llm.astream_chat(
messages=chat_history,
)
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="completed",
),
)
)
return StopEvent(result=response_stream)
@@ -1,337 +0,0 @@
import re
import time
from typing import Any, Literal, Optional
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.llms import LLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.prompts import PromptTemplate
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.api.models import (
Artifact,
ArtifactEvent,
ArtifactType,
ChatRequest,
DocumentArtifactData,
UIEvent,
)
from llama_index.server.api.utils import get_last_artifact
from pydantic import BaseModel, Field
class DocumentRequirement(BaseModel):
type: Literal["markdown", "html"]
title: str
requirement: str
class PlanEvent(Event):
user_msg: str
context: Optional[str] = None
class GenerateArtifactEvent(Event):
requirement: DocumentRequirement
class SynthesizeAnswerEvent(Event):
requirement: DocumentRequirement
generated_artifact: str
class UIEventData(BaseModel):
"""
Event data for updating workflow status to the UI.
"""
state: Literal["plan", "generate", "completed"] = Field(
description="The current state of the workflow. "
"plan: analyze and create a plan for the next step. "
"generate: generate the artifact based on the requirement from the previous step. "
"completed: the workflow is completed. "
)
requirement: Optional[str] = Field(
description="The requirement for generating the artifact. ",
default=None,
)
class DocumentArtifactWorkflow(Workflow):
"""
A workflow to help generate or update document artifacts (e.g., Markdown or HTML documents).
Example use cases: Generate a project guideline, update documentation with user feedback, etc.
"""
def __init__(
self,
llm: LLM,
chat_request: ChatRequest,
**kwargs: Any,
):
"""
Args:
llm: The LLM to use.
chat_request: The chat request from the chat app to use.
"""
super().__init__(**kwargs)
self.llm = llm
self.chat_request = chat_request
self.last_artifact = get_last_artifact(chat_request)
@step
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> PlanEvent:
user_msg = ev.user_msg
if user_msg is None:
raise ValueError("user_msg is required to run the workflow")
await ctx.set("user_msg", user_msg)
chat_history = ev.chat_history or []
chat_history.append(
ChatMessage(
role="user",
content=user_msg,
)
)
memory = ChatMemoryBuffer.from_defaults(
chat_history=chat_history,
llm=self.llm,
)
await ctx.set("memory", memory)
return PlanEvent(
user_msg=user_msg,
context=str(self.last_artifact.model_dump_json())
if self.last_artifact
else "",
)
@step
async def planning(self, ctx: Context, event: PlanEvent) -> GenerateArtifactEvent:
"""
Based on the conversation history and the user's request,
this step will provide a clear requirement for the next document generation or update.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="plan",
requirement=None,
),
)
)
prompt = PromptTemplate("""
You are a documentation analyst responsible for analyzing the user's request and providing requirements for document generation or update.
Follow these instructions:
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
2. From the user's request, provide requirements for the next step of the document generation or update.
3. Do not be verbose; only return the requirements for the next step of the document generation or update.
4. Only the following document types are allowed: "markdown", "html".
5. The requirement should be in the following format:
```json
{
"type": "markdown" | "html",
"title": string,
"requirement": string
}
```
## Example:
User request: Create a project guideline document.
You should return:
```json
{
"type": "markdown",
"title": "Project Guideline",
"requirement": "Generate a Markdown document that outlines the project goals, deliverables, and timeline. Include sections for introduction, objectives, deliverables, and timeline."
}
```
User request: Add a troubleshooting section to the guideline.
You should return:
```json
{
"type": "markdown",
"title": "Project Guideline",
"requirement": "Add a 'Troubleshooting' section at the end of the document with common issues and solutions."
}
```
{context}
Now, please plan for the user's request:
{user_msg}
""").format(
context=""
if event.context is None
else f"## The context is: \n{event.context}\n",
user_msg=event.user_msg,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# parse the response to DocumentRequirement
json_block = re.search(r"```json([\s\S]*)```", response.text)
if json_block is None:
raise ValueError("No json block found in the response")
requirement = DocumentRequirement.model_validate_json(
json_block.group(1).strip()
)
# Put the planning result to the memory
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"Planning for the document generation: \n{response.text}",
)
)
await ctx.set("memory", memory)
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=requirement.requirement,
),
)
)
return GenerateArtifactEvent(
requirement=requirement,
)
@step
async def generate_artifact(
self, ctx: Context, event: GenerateArtifactEvent
) -> SynthesizeAnswerEvent:
"""
Generate or update the document based on the user's request.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=event.requirement.requirement,
),
)
)
prompt = PromptTemplate("""
You are a skilled technical writer who can help users with documentation.
You are given a task to generate or update a document for a given requirement.
## Follow these instructions:
**1. Carefully read the user's requirements.**
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
If the previous document is provided:
+ Carefully analyze the document with the request to make the right changes.
+ Avoid making unnecessary changes from the previous document if the request is not to rewrite it from scratch.
**2. For document requests:**
- If the user does not specify a type, default to Markdown.
- Ensure the document is clear, well-structured, and grammatically correct.
- Only generate content relevant to the user's request—do not add extra boilerplate.
**3. Do not be verbose in your response.**
- No other text or comments; only return the document content wrapped by the appropriate code block (```markdown or ```html).
- If the user's request is to update the document, only return the updated document.
**4. Only the following types are allowed: "markdown", "html".**
**5. If there is no change to the document, return the reason without any code block.**
## Example:
```markdown
# Project Guideline
## Introduction
...
```
The previous content is:
{previous_artifact}
Now, please generate the document for the following requirement:
{requirement}
""").format(
previous_artifact=self.last_artifact.model_dump_json()
if self.last_artifact
else "",
requirement=event.requirement,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# Extract the document from the response
language_pattern = r"```(markdown|html)([\s\S]*)```"
doc_match = re.search(language_pattern, response.text)
if doc_match is None:
return SynthesizeAnswerEvent(
requirement=event.requirement,
generated_artifact="There is no change to the document. "
+ response.text.strip(),
)
content = doc_match.group(2).strip()
doc_type = doc_match.group(1)
# Put the generated document to the memory
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"Generated document: \n{response.text}",
)
)
# To show the Canvas panel for the artifact
ctx.write_event_to_stream(
ArtifactEvent(
data=Artifact(
type=ArtifactType.DOCUMENT,
created_at=int(time.time()),
data=DocumentArtifactData(
title=event.requirement.title,
content=content,
type=doc_type, # type: ignore
),
),
)
)
return SynthesizeAnswerEvent(
requirement=event.requirement,
generated_artifact=response.text,
)
@step
async def synthesize_answer(
self, ctx: Context, event: SynthesizeAnswerEvent
) -> StopEvent:
"""
Synthesize the answer for the user.
"""
memory: ChatMemoryBuffer = await ctx.get("memory")
chat_history = memory.get()
chat_history.append(
ChatMessage(
role="system",
content="""
Your responsibility is to explain the work to the user.
If there is no document to update, explain the reason.
If the document is updated, just summarize what changed. Don't need to include the whole document again in the response.
""",
)
)
response_stream = await self.llm.astream_chat(
messages=chat_history,
)
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="completed",
requirement=event.requirement.requirement,
),
)
)
return StopEvent(result=response_stream)
@@ -1,15 +0,0 @@
from app.code_workflow import CodeArtifactWorkflow
# from app.document_workflow import DocumentArtifactWorkflow to generate documents
from llama_index.core.workflow import Workflow
from llama_index.llms.openai import OpenAI
from llama_index.server.api.models import ChatRequest
def create_workflow(chat_request: ChatRequest) -> Workflow:
workflow = CodeArtifactWorkflow(
llm=OpenAI(model="gpt-4.1"),
chat_request=chat_request,
timeout=120.0,
)
return workflow
@@ -1,4 +1,4 @@
import { agent } from "@llamaindex/workflow";
import { agent } from "llamaindex";
import { getIndex } from "./data";
export const workflowFactory = async (reqBody: any) => {
@@ -1,56 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
## Custom UI Components
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
```
npm run generate:ui
```
## Use Case
We have prepared two artifact workflows:
- [Code Workflow](app/code_workflow.ts): To generate code and display it in the UI like Vercel's v0.
- [Document Workflow](app/document_workflow.ts): Generate and update a document like OpenAI's canvas.
Modify the factory method in [`workflow.ts`](app/workflow.ts) to decide which artifact workflow to use. Without any changes the Code Workflow is used.
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -1,360 +0,0 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, LLM, Settings } from "llamaindex";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import { z } from "zod";
export const RequirementSchema = z.object({
next_step: z.enum(["answering", "coding"]),
language: z.string().nullable().optional(),
file_name: z.string().nullable().optional(),
requirement: z.string(),
});
export type Requirement = z.infer<typeof RequirementSchema>;
export const UIEventSchema = z.object({
type: z.literal("ui_event"),
data: z.object({
state: z
.enum(["plan", "generate", "completed"])
.describe(
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
),
requirement: z
.string()
.optional()
.describe(
"An optional requirement creating or updating a code, if applicable.",
),
}),
});
export type UIEvent = z.infer<typeof UIEventSchema>;
const planEvent = workflowEvent<{
userInput: string;
context?: string | undefined;
}>();
const generateArtifactEvent = workflowEvent<{
requirement: Requirement;
}>();
const synthesizeAnswerEvent = workflowEvent<object>();
const uiEvent = workflowEvent<UIEvent>();
const artifactEvent = workflowEvent<{
type: "artifact";
data: {
type: "code";
created_at: number;
data: {
language: string;
file_name: string;
code: string;
};
};
}>();
export function createCodeArtifactWorkflow(reqBody: any, llm?: LLM) {
if (!llm) {
llm = Settings.llm;
}
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({
llm,
chatHistory: reqBody.chatHistory,
}),
lastArtifact: extractLastArtifact(reqBody),
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
// 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,
});
return planEvent.with({
userInput,
});
});
workflow.handle([planEvent], async ({ data: planData }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "plan",
},
}),
);
const user_msg = planData.userInput;
const context = planData.context
? `## The context is: \n${planData.context}\n`
: "";
const prompt = `
You are a product analyst responsible for analyzing the user's request and providing the next step for code or document generation.
You are helping user with their code artifact. To update the code, you need to plan a coding step.
Follow these instructions:
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
2. The next step must be one of the following two options:
- "coding": To make the changes to the current code.
- "answering": If you don't need to update the current code or need clarification from the user.
Important: Avoid telling the user to update the code themselves, you are the one who will update the code (by planning a coding step).
3. If the next step is "coding", you may specify the language ("typescript" or "python") and file_name if known, otherwise set them to null.
4. The requirement must be provided clearly what is the user request and what need to be done for the next step in details
as precise and specific as possible, don't be stingy with in the requirement.
5. If the next step is "answering", set language and file_name to null, and the requirement should describe what to answer or explain to the user.
6. Be concise; only return the requirements for the next step.
7. The requirements must be in the following format:
\`\`\`json
{
"next_step": "answering" | "coding",
"language": "typescript" | "python" | null,
"file_name": string | null,
"requirement": string
}
\`\`\`
## Example 1:
User request: Create a calculator app.
You should return:
\`\`\`json
{
"next_step": "coding",
"language": "typescript",
"file_name": "calculator.tsx",
"requirement": "Generate code for a calculator app that has a simple UI with a display and button layout. The display should show the current input and the result. The buttons should include basic operators, numbers, clear, and equals. The calculation should work correctly."
}
\`\`\`
## Example 2:
User request: Explain how the game loop works.
Context: You have already generated the code for a snake game.
You should return:
\`\`\`json
{
"next_step": "answering",
"language": null,
"file_name": null,
"requirement": "The user is asking about the game loop. Explain how the game loop works."
}
\`\`\`
${context}
Now, plan the user's next step for this request:
${user_msg}
`;
const response = await llm.complete({
prompt,
});
// parse the response to Requirement
// 1. use regex to find the json block
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonBlock) {
throw new Error("No JSON block found in the response.");
}
const requirement = RequirementSchema.parse(JSON.parse(jsonBlock[1]));
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: requirement.requirement,
},
}),
);
state.memory.put({
role: "assistant",
content: `The plan for next step: \n${response.text}`,
});
if (requirement.next_step === "coding") {
return generateArtifactEvent.with({
requirement,
});
} else {
return synthesizeAnswerEvent.with({});
}
});
workflow.handle([generateArtifactEvent], async ({ data: planData }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: planData.requirement.requirement,
},
}),
);
const previousArtifact = state.lastArtifact
? JSON.stringify(state.lastArtifact)
: "There is no previous artifact";
const requirementText = planData.requirement.requirement;
const prompt = `
You are a skilled developer who can help user with coding.
You are given a task to generate or update a code for a given requirement.
## Follow these instructions:
**1. Carefully read the user's requirements.**
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
If the previous code is provided:
+ Carefully analyze the code with the request to make the right changes.
+ Avoid making a lot of changes from the previous code if the request is not to write the code from scratch again.
**2. For code requests:**
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
The import pattern should be:
\`\`\`typescript
import { ComponentName } from "@/components/ui/component-name"
import { Markdown } from "@llamaindex/chat-ui"
import { cn } from "@/lib/utils"
\`\`\`
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
- Only generate code relevant to the user's request—do not add extra boilerplate.
**3. Don't be verbose on response**
- No other text or comments only return the code which wrapped by \`\`\`language\`\`\` block.
- If the user's request is to update the code, only return the updated code.
**4. Only the following languages are allowed: "typescript", "python".**
**5. If there is no code to update, return the reason without any code block.**
## Example:
\`\`\`typescript
import React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export default function MyComponent() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<Button>Click me</Button>
</div>
);
}
\`\`\`
The previous code is:
{previousArtifact}
Now, i have to generate the code for the following requirement:
{requirement}
`
.replace("{previousArtifact}", previousArtifact)
.replace("{requirement}", requirementText);
const response = await llm.complete({
prompt,
});
// Extract the code from the response
const codeMatch = response.text.match(/```(\w+)([\s\S]*)```/);
if (!codeMatch) {
return synthesizeAnswerEvent.with({});
}
const code = codeMatch[2].trim();
// Put the generated code to the memory
state.memory.put({
role: "assistant",
content: `Updated the code: \n${response.text}`,
});
// To show the Canvas panel for the artifact
sendEvent(
artifactEvent.with({
type: "artifact",
data: {
type: "code",
created_at: Date.now(),
data: {
language: planData.requirement.language || "",
file_name: planData.requirement.file_name || "",
code,
},
},
}),
);
return synthesizeAnswerEvent.with({});
});
workflow.handle([synthesizeAnswerEvent], async () => {
const { sendEvent } = getContext();
const { state } = getContext();
const chatHistory = await state.memory.getMessages();
const messages = [
...chatHistory,
{
role: "system" as const,
content: `
You are a helpful assistant who is responsible for explaining the work to the user.
Based on the conversation history, provide an answer to the user's question.
The user has access to the code so avoid mentioning the whole code again in your response.
`,
},
];
const responseStream = await llm.chat({
messages,
stream: true,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "completed",
},
}),
);
let response = "";
for await (const chunk of responseStream) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response: "",
currentAgentName: "assistant",
raw: chunk,
}),
);
}
return stopAgentEvent.with({
result: response,
});
});
return workflow;
}
@@ -1,341 +0,0 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, LLM, Settings } from "llamaindex";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import { z } from "zod";
export const DocumentRequirementSchema = z.object({
type: z.enum(["markdown", "html"]),
title: z.string(),
requirement: z.string(),
});
export type DocumentRequirement = z.infer<typeof DocumentRequirementSchema>;
export const UIEventSchema = z.object({
type: z.literal("ui_event"),
data: z.object({
state: z
.enum(["plan", "generate", "completed"])
.describe(
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
),
requirement: z
.string()
.optional()
.describe(
"An optional requirement creating or updating a document, if applicable.",
),
}),
});
export type UIEvent = z.infer<typeof UIEventSchema>;
const planEvent = workflowEvent<{
userInput: string;
context?: string | undefined;
}>();
const generateArtifactEvent = workflowEvent<{
requirement: DocumentRequirement;
}>();
const synthesizeAnswerEvent = workflowEvent<{
requirement: DocumentRequirement;
generatedArtifact: string;
}>();
const uiEvent = workflowEvent<UIEvent>();
const artifactEvent = workflowEvent<{
type: "artifact";
data: {
type: "document";
created_at: number;
data: {
title: string;
content: string;
type: "markdown" | "html";
};
};
}>();
export function createDocumentArtifactWorkflow(reqBody: any, llm?: LLM) {
if (!llm) {
llm = Settings.llm;
}
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({
llm,
chatHistory: reqBody.chatHistory,
}),
lastArtifact: extractLastArtifact(reqBody),
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
// 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,
});
return planEvent.with({
userInput,
context: state.lastArtifact
? JSON.stringify(state.lastArtifact)
: undefined,
});
});
workflow.handle([planEvent], async ({ data: planData }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "plan",
},
}),
);
const user_msg = planData.userInput;
const context = planData.context
? `## The context is: \n${planData.context}\n`
: "";
const prompt = `
You are a documentation analyst responsible for analyzing the user's request and providing requirements for document generation or update.
Follow these instructions:
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
2. From the user's request, provide requirements for the next step of the document generation or update.
3. Do not be verbose; only return the requirements for the next step of the document generation or update.
4. Only the following document types are allowed: "markdown", "html".
5. The requirement should be in the following format:
\`\`\`json
{
"type": "markdown" | "html",
"title": string,
"requirement": string
}
\`\`\`
## Example:
User request: Create a project guideline document.
You should return:
\`\`\`json
{
"type": "markdown",
"title": "Project Guideline",
"requirement": "Generate a Markdown document that outlines the project goals, deliverables, and timeline. Include sections for introduction, objectives, deliverables, and timeline."
}
\`\`\`
User request: Add a troubleshooting section to the guideline.
You should return:
\`\`\`json
{
"type": "markdown",
"title": "Project Guideline",
"requirement": "Add a 'Troubleshooting' section at the end of the document with common issues and solutions."
}
\`\`\`
${context}
Now, please plan for the user's request:
${user_msg}
`;
const response = await llm.complete({
prompt,
});
// Parse the response to DocumentRequirement
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonBlock) {
throw new Error("No JSON block found in the response.");
}
const requirement = DocumentRequirementSchema.parse(
JSON.parse(jsonBlock[1]),
);
state.memory.put({
role: "assistant",
content: `Planning for the document generation: \n${response.text}`,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: requirement.requirement,
},
}),
);
return generateArtifactEvent.with({
requirement,
});
});
workflow.handle(
[generateArtifactEvent],
async ({ data: { requirement } }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: requirement.requirement,
},
}),
);
const previousArtifact = state.lastArtifact
? JSON.stringify(state.lastArtifact)
: "";
const requirementStr = JSON.stringify(requirement);
const prompt = `
You are a skilled technical writer who can help users with documentation.
You are given a task to generate or update a document for a given requirement.
## Follow these instructions:
**1. Carefully read the user's requirements.**
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
If the previous document is provided:
+ Carefully analyze the document with the request to make the right changes.
+ Avoid making unnecessary changes from the previous document if the request is not to rewrite it from scratch.
**2. For document requests:**
- If the user does not specify a type, default to Markdown.
- Ensure the document is clear, well-structured, and grammatically correct.
- Only generate content relevant to the user's request—do not add extra boilerplate.
**3. Do not be verbose in your response.**
- No other text or comments; only return the document content wrapped by the appropriate code block (\`\`\`markdown or \`\`\`html).
- If the user's request is to update the document, only return the updated document.
**4. Only the following types are allowed: "markdown", "html".**
**5. If there is no change to the document, return the reason without any code block.**
## Example:
\`\`\`markdown
# Project Guideline
## Introduction
...
\`\`\`
The previous content is:
${previousArtifact}
Now, please generate the document for the following requirement:
${requirementStr}
`;
const response = await llm.complete({
prompt,
});
// Extract the document from the response
const docMatch = response.text.match(/```(markdown|html)([\s\S]*)```/);
const generatedContent = response.text;
if (docMatch) {
const content = docMatch[2].trim();
const docType = docMatch[1] as "markdown" | "html";
// Put the generated document to the memory
state.memory.put({
role: "assistant",
content: `Generated document: \n${response.text}`,
});
// To show the Canvas panel for the artifact
sendEvent(
artifactEvent.with({
type: "artifact",
data: {
type: "document",
created_at: Date.now(),
data: {
title: requirement.title,
content: content,
type: docType,
},
},
}),
);
}
return synthesizeAnswerEvent.with({
requirement,
generatedArtifact: generatedContent,
});
},
);
workflow.handle([synthesizeAnswerEvent], async ({ data }) => {
const { sendEvent } = getContext();
const { state } = getContext();
const chatHistory = await state.memory.getMessages();
const messages = [
...chatHistory,
{
role: "system" as const,
content: `
Your responsibility is to explain the work to the user.
If there is no document to update, explain the reason.
If the document is updated, just summarize what changed. Don't need to include the whole document again in the response.
`,
},
];
const responseStream = await llm.chat({
messages,
stream: true,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "completed",
requirement: data.requirement.requirement,
},
}),
);
let response = "";
for await (const chunk of responseStream) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response: "",
currentAgentName: "assistant",
raw: chunk,
}),
);
}
return stopAgentEvent.with({
result: response,
});
});
return workflow;
}
@@ -1,12 +0,0 @@
import { createCodeArtifactWorkflow, UIEventSchema } from "./code-workflow";
// import { createDocumentArtifactWorkflow, UIEventSchema } from "./doc-workflow";
export const workflowFactory = async (reqBody: any) => {
// Uncomment the import and change to createDocumentArtifactWorkflow to use the document workflow
const workflow = createCodeArtifactWorkflow(reqBody);
return workflow;
};
// Re-export the UIEventSchema for generating the UI by `pnpm generate:ui` command
export { UIEventSchema };
@@ -31,7 +31,7 @@ You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/l
## Custom UI Components
For Deep Research, we have a custom component located in `components/ui_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
For Deep Research, we have a custom component located in `components/deep_research_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
### Generate a new UI Component from workflow event
@@ -1,21 +1,22 @@
import { toSourceEvent } from "@llamaindex/server";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import { toSourceEvent, toStreamGenerator } from "@llamaindex/server";
import {
AgentInputData,
AgentWorkflowContext,
ChatMemoryBuffer,
ChatResponseChunk,
HandlerContext,
LlamaCloudIndex,
Metadata,
MetadataMode,
NodeWithScore,
PromptTemplate,
Settings,
StartEvent,
StopEvent as StopEventBase,
ToolCallLLM,
VectorStoreIndex,
Workflow,
WorkflowEvent,
} from "llamaindex";
import { randomUUID } from "node:crypto";
import { z } from "zod";
@@ -24,11 +25,12 @@ import { getIndex } from "./data";
// workflow factory
export const workflowFactory = async (reqBody: any) => {
const index = await getIndex(reqBody?.data);
return getWorkflow(index);
return new DeepResearchWorkflow(index);
};
// workflow configs
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
const TIMEOUT = 360; // timeout in seconds
const TOP_K = 10; // number of nodes to retrieve from the vector store
const createPlanResearchPrompt = new PromptTemplate({
@@ -112,10 +114,10 @@ Create a well-structured outline for the research report that covers all the ans
type ResearchQuestion = { questionId: string; question: string };
type ResearchResult = ResearchQuestion & { answer: string };
// class PlanResearchEvent extends WorkflowEvent<{}> {}
const planResearchEvent = workflowEvent<{}>();
const researchEvent = workflowEvent<ResearchQuestion>();
const reportEvent = workflowEvent<{}>();
class PlanResearchEvent extends WorkflowEvent<{}> {}
class ResearchEvent extends WorkflowEvent<ResearchQuestion[]> {}
class ReportEvent extends WorkflowEvent<{}> {}
class StopEvent extends StopEventBase<AsyncGenerator<ChatResponseChunk>> {}
export const UIEventSchema = z
.object({
@@ -138,180 +140,221 @@ export const UIEventSchema = z
type UIEventData = z.infer<typeof UIEventSchema>;
const uiEvent = workflowEvent<{
class UIEvent extends WorkflowEvent<{
type: "ui_event";
data: UIEventData;
}>();
}> {}
// workflow definition
export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
const retriever = index.asRetriever({ similarityTopK: TOP_K });
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({
llm: Settings.llm,
chatHistory: [],
}),
contextNodes: [] as NodeWithScore<Metadata>[],
userRequest: "",
totalQuestions: 0,
researchResults: [] as ResearchResult[],
};
});
const workflow = withState(createWorkflow());
class DeepResearchWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
#llm = Settings.llm as ToolCallLLM;
#index?: VectorStoreIndex | LlamaCloudIndex;
workflow.handle([startAgentEvent], async ({ data }) => {
userRequest: string = "";
totalQuestions: number = 0;
contextNodes: NodeWithScore<Metadata>[] = [];
memory: ChatMemoryBuffer = new ChatMemoryBuffer({ llm: Settings.llm });
constructor(index: VectorStoreIndex | LlamaCloudIndex) {
super({ timeout: TIMEOUT });
this.#index = index;
this.addWorkflowSteps();
}
addWorkflowSteps() {
this.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [PlanResearchEvent],
},
this.handleStartWorkflow,
);
this.addStep(
{
inputs: [PlanResearchEvent],
outputs: [ResearchEvent, ReportEvent, StopEvent],
},
this.handlePlanResearch,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [PlanResearchEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [ReportEvent],
outputs: [StopEvent],
},
this.handleReport,
);
}
async initWorkflow(data: AgentInputData) {
const { userInput, chatHistory = [] } = data;
const { sendEvent, state } = getContext();
if (!userInput) throw new Error("Invalid input");
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
state.userRequest = userInput;
sendEvent(
uiEvent.with({
this.userRequest = userInput;
await this.memory.set(chatHistory);
await this.memory.put({ role: "user", content: userInput });
}
handleStartWorkflow = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<PlanResearchEvent> => {
await this.initWorkflow(ev.data);
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: {
event: "retrieve",
state: "inprogress",
},
data: { event: "retrieve", state: "inprogress" },
}),
);
const retrievedNodes = await retriever.retrieve(userInput);
const retrievedNodes = await this.retriever.retrieve(this.userRequest);
sendEvent(toSourceEvent(retrievedNodes));
sendEvent(
uiEvent.with({
ctx.sendEvent(toSourceEvent(retrievedNodes));
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "retrieve", state: "done" },
}),
);
state.contextNodes.push(...retrievedNodes);
this.contextNodes = retrievedNodes;
return planResearchEvent.with({});
});
return new PlanResearchEvent({});
};
workflow.handle([planResearchEvent], async ({ data }) => {
const { sendEvent, state, stream } = getContext();
sendEvent(
uiEvent.with({
handlePlanResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: PlanResearchEvent,
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "analyze", state: "inprogress" },
}),
);
const { decision, researchQuestions, cancelReason } =
await createResearchPlan(
state.memory,
state.contextNodes
.map((node) => node.node.getContent(MetadataMode.NONE))
.join("\n"),
enhancedPrompt(state.totalQuestions),
state.userRequest,
);
await this.createResearchPlan();
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
// Stop workflow due to decision from LLM
if (decision === "cancel") {
sendEvent(
uiEvent.with({
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return agentStreamEvent.with({
delta: cancelReason ?? "Research cancelled without any reason.",
response: cancelReason ?? "Research cancelled without any reason.",
currentAgentName: "",
raw: null,
});
return new StopEvent(
toStreamGenerator(
cancelReason ?? "Research cancelled without any reason.",
),
);
}
if (decision === "research" && researchQuestions.length > 0) {
state.totalQuestions += researchQuestions.length;
state.memory.put({
// Trigger research from generated questions
if (decision === "research") {
this.memory.put({
role: "assistant",
content:
"We need to find answers to the following questions:\n" +
researchQuestions.join("\n"),
});
researchQuestions.forEach(({ questionId: id, question }) => {
sendEvent(
uiEvent.with({
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "answer", state: "pending", id, question },
}),
);
sendEvent(researchEvent.with({ questionId: id, question }));
});
const events = await stream
.until(() => state.researchResults.length === researchQuestions.length)
.toArray();
return planResearchEvent.with({});
return new ResearchEvent(researchQuestions);
}
state.memory.put({
// Resarch done, start writing report
this.memory.put({
role: "assistant",
content: "No more idea to analyze. We should report the answers.",
});
sendEvent(
uiEvent.with({
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return reportEvent.with({});
});
workflow.handle([researchEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const { questionId, question } = data;
return new ReportEvent({});
};
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "answer",
state: "inprogress",
id: questionId,
question,
},
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<PlanResearchEvent> => {
const researchQuestions = ev.data;
// Answer questions in parallel
const researchResults: ResearchResult[] = await Promise.all(
researchQuestions.map(async ({ questionId: id, question }) => {
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "answer", state: "inprogress", id, question },
}),
);
const answer = await this.answerQuestion(question);
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "answer", state: "done", id, question, answer },
}),
);
return { questionId: id, question, answer };
}),
);
const answer = await answerQuestion(
contextStr(state.contextNodes),
question,
);
state.researchResults.push({ questionId, question, answer });
state.memory.put({
role: "assistant",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
// Save answers to memory
researchResults.forEach(({ question, answer }) => {
this.memory.put({
role: "assistant",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
});
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "answer",
state: "done",
id: questionId,
question,
answer,
},
}),
);
});
this.memory.put({
role: "assistant",
content:
"Researched all the questions. Now, I need to analyze if it's ready to write a report or need to research more.",
});
this.totalQuestions += researchResults.length;
return new PlanResearchEvent({});
};
handleReport = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportEvent,
): Promise<StopEvent> => {
const chatHistory = await this.memory.getAllMessages();
workflow.handle([reportEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const chatHistory = await state.memory.getAllMessages();
const messages = chatHistory.concat([
{
role: "system",
@@ -324,91 +367,81 @@ export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
},
]);
const stream = await Settings.llm.chat({ messages, stream: true });
let response = "";
for await (const chunk of stream) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response,
currentAgentName: "",
raw: stream,
}),
);
}
return stopAgentEvent.with({
result: response,
});
});
const stream = await this.llm.chat({ messages, stream: true });
return workflow;
}
const createResearchPlan = async (
memory: ChatMemoryBuffer,
contextStr: string,
enhancedPrompt: string,
userRequest: string,
) => {
const chatHistory = await memory.getMessages();
const conversationContext = chatHistory
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const prompt = createPlanResearchPrompt.format({
context_str: contextStr,
conversation_context: conversationContext,
enhanced_prompt: enhancedPrompt,
user_request: userRequest,
});
const responseFormat = z.object({
decision: z.enum(["research", "write", "cancel"]),
researchQuestions: z.array(z.string()),
cancelReason: z.string().optional(),
});
const result = await Settings.llm.complete({ prompt, responseFormat });
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
return {
...plan,
researchQuestions: plan.researchQuestions.map((question) => ({
questionId: randomUUID(),
question,
})),
return new StopEvent(toStreamGenerator(stream));
};
};
const contextStr = (contextNodes: NodeWithScore<Metadata>[]) => {
return contextNodes
.map((node) => {
const nodeId = node.node.id_;
const nodeContent = node.node.getContent(MetadataMode.NONE);
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
})
.join("\n");
};
const enhancedPrompt = (totalQuestions: number) => {
if (totalQuestions === 0) {
return "The student has no questions to research. Let start by providing some questions for the student to research.";
get llm() {
if (!this.#llm.supportToolCall) throw new Error("LLM is not a ToolCallLLM");
return this.#llm;
}
if (totalQuestions >= MAX_QUESTIONS) {
return `The student has researched ${totalQuestions} questions. Should proceeding writing report or cancel the research if the answers are not enough to write a report.`;
get retriever() {
if (!this.#index) throw new Error("Index is not initialized");
return this.#index.asRetriever({ similarityTopK: TOP_K });
}
return "";
};
get contextStr() {
return this.contextNodes
.map((node) => {
const nodeId = node.node.id_;
const nodeContent = node.node.getContent(MetadataMode.NONE);
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
})
.join("\n");
}
const answerQuestion = async (contextStr: string, question: string) => {
const prompt = researchPrompt.format({
context_str: contextStr,
question,
});
const result = await Settings.llm.complete({ prompt });
return result.text;
};
get enhancedPrompt() {
if (this.totalQuestions === 0) {
return "The student has no questions to research. Let start by asking some questions.";
}
if (this.totalQuestions > MAX_QUESTIONS) {
return `The student has researched ${this.totalQuestions} questions. Should cancel the research if the context is not enough to write a report.`;
}
return "";
}
async createResearchPlan() {
const chatHistory = await this.memory.getMessages();
const conversationContext = chatHistory
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const prompt = createPlanResearchPrompt.format({
context_str: this.contextStr,
conversation_context: conversationContext,
enhanced_prompt: this.enhancedPrompt,
user_request: this.userRequest,
});
const responseFormat = z.object({
decision: z.enum(["research", "write", "cancel"]),
researchQuestions: z.array(z.string()),
cancelReason: z.string().optional(),
});
const result = await this.llm.complete({ prompt, responseFormat });
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
return {
...plan,
researchQuestions: plan.researchQuestions.map((question) => ({
questionId: randomUUID(),
question,
})),
};
}
async answerQuestion(question: string) {
const prompt = researchPrompt.format({
context_str: this.contextStr,
question,
});
const result = await this.llm.complete({ prompt });
return result.text;
}
}
@@ -6,25 +6,27 @@ import {
interpreter,
} from "@llamaindex/tools";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import {
AgentInputData,
AgentWorkflowContext,
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponseChunk,
HandlerContext,
Metadata,
NodeWithScore,
Settings,
StartEvent,
StopEvent,
ToolCall,
ToolCallLLM,
Workflow,
WorkflowEvent,
} from "llamaindex";
import { getIndex } from "./data";
const TIMEOUT = 360 * 1000;
export async function workflowFactory(reqBody: any) {
const index = await getIndex(reqBody?.data);
@@ -45,18 +47,28 @@ export async function workflowFactory(reqBody: any) {
});
const documentGeneratorTool = documentGenerator();
return getWorkflow(
return new FinancialReportWorkflow({
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
);
timeout: TIMEOUT,
});
}
// workflow events
const inputEvent = workflowEvent<{ input: ChatMessage[] }>();
const researchEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
const analyzeEvent = workflowEvent<{ input: ChatMessage | ToolCall[] }>();
const reportGenerationEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
// Create a custom event type
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
class ResearchEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
class AnalyzeEvent extends WorkflowEvent<{
input: ChatMessage | ToolCall[];
}> {}
class ReportGenerationEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
const DEFAULT_SYSTEM_PROMPT = `
You are a financial analyst who are given a set of tools to help you.
@@ -64,103 +76,173 @@ It's good to using appropriate tools for the user request and always use the inf
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
`;
// workflow definition
export function getWorkflow(
queryEngineTool: BaseToolWithCall,
codeInterpreterTool: BaseToolWithCall,
documentGeneratorTool: BaseToolWithCall,
) {
const llm = Settings.llm as ToolCallLLM;
if (!llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
class FinancialReportWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
constructor(options: {
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.llm = Settings.llm as ToolCallLLM;
if (!this.llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.queryEngineTool = options.queryEngineTool;
this.codeInterpreterTool = options.codeInterpreterTool;
this.documentGeneratorTool = options.documentGeneratorTool;
this.memory = new ChatMemoryBuffer({ llm: this.llm, chatHistory: [] });
// Add steps
this.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [InputEvent],
},
this.prepareChatHistory,
);
this.addStep(
{
inputs: [InputEvent],
outputs: [
InputEvent,
ResearchEvent,
AnalyzeEvent,
ReportGenerationEvent,
StopEvent,
],
},
this.handleLLMInput,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [AnalyzeEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [AnalyzeEvent],
outputs: [InputEvent],
},
this.handleAnalyze,
);
this.addStep(
{
inputs: [ReportGenerationEvent],
outputs: [InputEvent],
},
this.handleReportGeneration,
);
}
const { withState, getContext } = createStatefulMiddleware(() => ({
memory: new ChatMemoryBuffer({ llm, chatHistory: [] }),
}));
const workflow = withState(createWorkflow());
// Add steps
workflow.handle([startAgentEvent], async ({ data }) => {
const { state } = getContext();
const { userInput, chatHistory = [] } = data;
prepareChatHistory = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<InputEvent> => {
const { userInput, chatHistory = [] } = ev.data;
if (!userInput) throw new Error("Invalid input");
state.memory.set(chatHistory);
this.memory.set(chatHistory);
state.memory.put({ role: "system", content: DEFAULT_SYSTEM_PROMPT });
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
state.memory.put({ role: "user", content: userInput });
this.memory.put({ role: "user", content: userInput });
const messages = await state.memory.getMessages();
return inputEvent.with({ input: messages });
});
const messages = await this.memory.getMessages();
return new InputEvent({ input: messages });
};
workflow.handle([inputEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const chatHistory = data.input;
handleLLMInput = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: InputEvent,
): Promise<
| InputEvent
| ResearchEvent
| AnalyzeEvent
| ReportGenerationEvent
| StopEvent<AsyncGenerator<ChatResponseChunk, any, any> | undefined>
> => {
const chatHistory = ev.data.input;
const tools = [codeInterpreterTool, documentGeneratorTool, queryEngineTool];
const tools = [
this.codeInterpreterTool,
this.documentGeneratorTool,
this.queryEngineTool,
];
const toolCallResponse = await chatWithTools(llm, tools, chatHistory);
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
const generator = toolCallResponse.responseGenerator;
let response = "";
if (generator) {
for await (const chunk of generator) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response,
currentAgentName: "LLM", // Or derive from context if needed
raw: chunk.raw,
}),
);
}
}
return stopAgentEvent.with({ result: response });
return new StopEvent(toolCallResponse.responseGenerator);
}
if (toolCallResponse.hasMultipleTools()) {
state.memory.put({
this.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
const newChatHistory = await state.memory.getMessages();
return inputEvent.with({ input: newChatHistory });
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
state.memory.put(toolCallResponse.toolCallMessage);
this.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case codeInterpreterTool.metadata.name:
return analyzeEvent.with({
case this.codeInterpreterTool.metadata.name:
return new AnalyzeEvent({
input: toolCallResponse.toolCalls,
});
case documentGeneratorTool.metadata.name:
return reportGenerationEvent.with({
case this.documentGeneratorTool.metadata.name:
return new ReportGenerationEvent({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (queryEngineTool.metadata.name === toolName) {
return researchEvent.with({
if (this.queryEngineTool.metadata.name === toolName) {
return new ResearchEvent({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
});
};
workflow.handle([researchEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
sendEvent(
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<AnalyzeEvent> => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Researcher",
text: "Researching data",
@@ -168,13 +250,13 @@ export function getWorkflow(
}),
);
const { toolCalls } = data;
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [queryEngineTool],
tools: [this.queryEngineTool],
toolCalls,
writeEvent: (text, step) => {
sendEvent(
ctx.sendEvent(
toAgentRunEvent({
agent: "Researcher",
text,
@@ -186,7 +268,7 @@ export function getWorkflow(
},
});
for (const toolMsg of toolMsgs) {
state.memory.put(toolMsg);
this.memory.put(toolMsg);
}
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
@@ -195,25 +277,26 @@ export function getWorkflow(
.filter(Boolean);
if (sourcesNodes.length > 0) {
sendEvent(toSourceEvent(sourcesNodes));
ctx.sendEvent(toSourceEvent(sourcesNodes));
}
// Send a message indicating research is done, triggering analysis
return analyzeEvent.with({
return new AnalyzeEvent({
input: {
role: "assistant",
content:
"I have finished researching the data, please analyze the data.",
},
});
});
};
/**
* Analyze a research result or a tool call for code interpreter from the LLM
*/
workflow.handle([analyzeEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
sendEvent(
handleAnalyze = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: AnalyzeEvent,
): Promise<InputEvent> => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Analyst",
text: "Analyzing data",
@@ -222,8 +305,8 @@ export function getWorkflow(
);
// Request by workflow LLM, input is a list of tool calls
let toolCalls: ToolCall[] = [];
if (Array.isArray(data.input)) {
toolCalls = data.input;
if (Array.isArray(ev.data.input)) {
toolCalls = ev.data.input;
} else {
// Requested by Researcher, input is a ChatMessage
// We start new LLM chat specifically for analyzing the data
@@ -237,65 +320,63 @@ export function getWorkflow(
// Clone the current chat history
// Add the analysis system prompt and the message from the researcher
const currentChatHistory = await state.memory.getMessages();
const chatHistory = await this.memory.getMessages();
const newChatHistory = [
...currentChatHistory,
...chatHistory,
{ role: "system", content: analysisPrompt },
data.input, // This is the ChatMessage from the research step
ev.data.input,
];
const toolCallResponse = await chatWithTools(
llm,
[codeInterpreterTool],
this.llm,
[this.codeInterpreterTool],
newChatHistory as ChatMessage[],
);
if (!toolCallResponse.hasToolCall()) {
// If no tool call needed for analysis, put the response directly
state.memory.put(await toolCallResponse.asFullResponse());
const finalChatHistory = await state.memory.getMessages();
return inputEvent.with({ input: finalChatHistory });
this.memory.put(await toolCallResponse.asFullResponse());
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
} else {
state.memory.put(toolCallResponse.toolCallMessage!);
this.memory.put(toolCallResponse.toolCallMessage!);
toolCalls = toolCallResponse.toolCalls;
}
}
// Call the code interpreter tools if needed
if (toolCalls.length > 0) {
const toolMsgs = await callTools({
tools: [codeInterpreterTool],
toolCalls,
writeEvent: (text, step) => {
sendEvent(
toAgentRunEvent({
agent: "Analyst",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
state.memory.put(toolMsg);
}
}
const finalChatHistory = await state.memory.getMessages();
// After analysis (or tool calls for analysis), trigger the next LLM input cycle
return inputEvent.with({ input: finalChatHistory });
});
workflow.handle([reportGenerationEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const { toolCalls } = data;
// Call the tools
const toolMsgs = await callTools({
tools: [documentGeneratorTool],
tools: [this.codeInterpreterTool],
toolCalls,
writeEvent: (text, step) => {
sendEvent(
ctx.sendEvent(
toAgentRunEvent({
agent: "Analyst",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
handleReportGeneration = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportGenerationEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.documentGeneratorTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Reporter",
text,
@@ -307,12 +388,9 @@ export function getWorkflow(
},
});
for (const toolMsg of toolMsgs) {
state.memory.put(toolMsg);
this.memory.put(toolMsg);
}
const chatHistory = await state.memory.getMessages();
// After report generation, trigger the next LLM input cycle
return inputEvent.with({ input: chatHistory });
});
return workflow;
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
}
@@ -12,7 +12,7 @@ dependencies = [
"pydantic<2.10",
"aiostream>=0.5.2,<0.6.0",
"llama-index-core>=0.12.28,<0.13.0",
"llama-index-server>=0.1.15,<0.2.0",
"llama-index-server>=0.1.14,<0.2.0",
]
[project.optional-dependencies]
@@ -10,12 +10,12 @@
},
"dependencies": {
"@llamaindex/openai": "0.2.0",
"@llamaindex/server": "0.2.0",
"@llamaindex/workflow": "1.1.1",
"@llamaindex/readers": "^2.0.0",
"@llamaindex/server": "0.1.5",
"@llamaindex/tools": "0.0.4",
"dotenv": "^16.4.7",
"zod": "^3.23.8",
"llamaindex": "0.10.5"
"llamaindex": "0.10.2"
},
"devDependencies": {
"@types/node": "^20.10.3",
@@ -9,7 +9,6 @@ readme = "README.md"
requires-python = ">=3.11,<3.14"
dependencies = [
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"fastapi[standard]>=0.109.1",
"uvicorn>=0.23.2",
"python-dotenv>=1.0.0",
@@ -96,9 +96,8 @@ export function Artifact({
useEffect(() => {
// auto trigger code execution
if (!result) {
fetchArtifactResult();
}
!result && fetchArtifactResult();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!artifact || version === undefined) return null;
@@ -285,6 +284,7 @@ function InterpreterOutput({ outputUrls }: { outputUrls: OutputUrl[] }) {
<li key={url.url}>
<div className="mt-4">
{isImageFile(url.filename) ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={url.url} alt={url.filename} className="my-4 w-1/2" />
) : (
<a
@@ -51,21 +51,18 @@ function ChatTools({
}
switch (toolCall.name) {
case "get_weather_information": {
case "get_weather_information":
const weatherData = toolOutput.output as unknown as WeatherData;
return <WeatherCard data={weatherData} />;
}
case "artifact": {
case "artifact":
return (
<Artifact
artifact={toolOutput.output as CodeArtifact}
version={artifactVersion}
/>
);
}
default: {
default:
return null;
}
}
}
-1
View File
@@ -1 +0,0 @@
server/
-131
View File
@@ -1,131 +0,0 @@
# @llamaindex/server
## 0.2.0
### Minor Changes
- 0384268: Use the new workflow engine and deprecate the old one.
### Patch Changes
- d9f9e3c: chore: bump chat-ui to support code editor & document editor
## 0.1.7
### Patch Changes
- 8fe5fc2: chore: add llamaindex server package
## 0.1.6
### Patch Changes
- 82d4b46: feat: re-add supports for artifacts
## 0.1.5
### Patch Changes
- 7ca9ddf: Add generate ui workflow to @llamaindex/server
- 3310eaa: chore: bump chat-ui
- llamaindex@0.10.2
## 0.1.4
### Patch Changes
- llamaindex@0.10.1
## 0.1.3
### Patch Changes
- edb8b87: fix: shadcn components cannot be used in next server
- Updated dependencies [6cf928f]
- llamaindex@0.10.0
## 0.1.2
### Patch Changes
- bb34ade: feat: support cn utils for server UI
- llamaindex@0.9.19
## 0.1.1
### Patch Changes
- 400b3b5: feat: use full-source code with import statements for custom comps
- llamaindex@0.9.18
## 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
- Updated dependencies [3534c37]
- llamaindex@0.9.17
## 0.0.7
### Patch Changes
- 4999df1: bump nextjs
- Updated dependencies [f5e4d09]
- llamaindex@0.9.16
## 0.0.6
### Patch Changes
- 8c02684: fix: handle stream error
- c515a32: feat: return raw output for agent toolcall result
- llamaindex@0.9.15
## 0.0.5
### Patch Changes
- 9d951b2: feat: support llamacloud in @llamaindex/server
- Updated dependencies [9d951b2]
- llamaindex@0.9.14
## 0.0.4
### Patch Changes
- 164cf7a: fix: custom next server start fail
## 0.0.3
### Patch Changes
- 299008b: feat: copy create-llama to @llamaindex/servers
- 75d6e29: feat: response source nodes in query tool output
- Updated dependencies [75d6e29]
- llamaindex@0.9.13
## 0.0.2
### Patch Changes
- f8a86e4: feat: @llamaindex/server
- Updated dependencies [21bebfc]
- Updated dependencies [93bc0ff]
- Updated dependencies [91a18e7]
- Updated dependencies [f8a86e4]
- Updated dependencies [5189b44]
- Updated dependencies [58a9446]
- @llamaindex/core@0.6.0
- @llamaindex/workflow@1.0.0
-161
View File
@@ -1,161 +0,0 @@
# 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
```bash
npm i @llamaindex/server
```
## Quick Start
Create an `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 options:
- `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).
## 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.
These components can be auto-generated using an LLM by providing a JSON schema of the workflow event.
### UI Event Schema
To display custom UI components, your workflow needs to emit UI events that have an event type for identification and a data object:
```typescript
class UIEvent extends WorkflowEvent<{
type: "ui_event";
data: UIEventData;
}> {}
```
The `data` object can be any JSON object. To enable AI generation of the UI component, you need to provide a schema for that data (here we're using Zod):
```typescript
const MyEventDataSchema = z
.object({
stage: z
.enum(["retrieve", "analyze", "answer"])
.describe("The current stage the workflow process is in."),
progress: z
.number()
.min(0)
.max(1)
.describe("The progress in percent of the current stage"),
})
.describe("WorkflowStageProgress");
type UIEventData = z.infer<typeof MyEventDataSchema>;
```
### Generate UI Components
The `generateEventComponent` function uses an LLM to generate a custom UI component based on the JSON schema of a workflow event. The schema should contain accurate descriptions of each field so that the LLM can generate matching components for your use case. We've done this for you in the example above using the `describe` function from Zod:
```typescript
import { OpenAI } from "llamaindex";
import { generateEventComponent } from "@llamaindex/server";
import { MyEventDataSchema } from "./your-workflow";
// Also works well with Claude 3.5 Sonnet and Google Gemini 2.5 Pro
const llm = new OpenAI({ model: "gpt-4.1" });
const code = generateEventComponent(MyEventDataSchema, llm);
```
After generating the code, we need to save it to a file. The file name must match the event type from your workflow (e.g., `ui_event.jsx` for handling events with `ui_event` type):
```ts
fs.writeFileSync("components/ui_event.jsx", code);
```
Feel free to modify the generated code to match your needs. If you're not satisfied with the generated code, we suggest improving the provided JSON schema first or trying another LLM.
> Note that `generateEventComponent` is generating JSX code, but you can also provide a TSX file.
### Server Setup
To use the generated UI components, you need to initialize the LlamaIndex server with the `componentsDir` that contains your custom UI components:
```ts
new LlamaIndexServer({
workflow: createWorkflow,
uiConfig: {
appTitle: "LlamaIndex App",
componentsDir: "components",
},
}).start();
```
## 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. By convention, the `data` folder is used for documents that are ingested, and the `output` folder is used for documents generated by the workflow.
## API Reference
- [LlamaIndexServer](https://ts.llamaindex.ai/docs/api/classes/LlamaIndexServer)
-12
View File
@@ -1,12 +0,0 @@
# LlamaIndex Server Examples
This directory contains examples of how to use the LlamaIndex Server.
## Running the examples
```bash
export OPENAI_API_KEY=your_openai_api_key
npx tsx simple-workflow/calculator.ts
```
## Open browser at http://localhost:3000
@@ -1,43 +0,0 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import {
Document,
OpenAI,
OpenAIEmbedding,
Settings,
VectorStoreIndex,
} from "llamaindex";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
});
export const workflowFactory = async () => {
const index = await VectorStoreIndex.fromDocuments([
new Document({ text: "The dog is brown" }),
new Document({ text: "The dog is yellow" }),
]);
const queryEngineTool = index.queryTool({
metadata: {
name: "query_document",
description: `This tool can retrieve information in documents`,
},
includeSourceNodes: true,
});
return agent({ tools: [queryEngineTool] });
};
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "LlamaIndex App",
starterQuestions: ["What is the color of the dog?"],
},
port: 4100,
}).start();
-24
View File
@@ -1,24 +0,0 @@
{
"name": "llamaindex-server-examples",
"version": "0.0.1",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "tsx simple-workflow/calculator.ts"
},
"dependencies": {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/readers": "^3.0.0",
"@llamaindex/server": "workspace:*",
"@llamaindex/tools": "0.0.4",
"@llamaindex/workflow": "1.1.0",
"dotenv": "^16.4.7",
"llamaindex": "0.10.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.10.3",
"tsx": "^4.7.2",
"typescript": "^5.3.2"
}
}
@@ -1,24 +0,0 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { z } from "zod";
const calculatorAgent = agent({
tools: [
tool({
name: "add",
description: "Adds two numbers",
parameters: z.object({ x: z.number(), y: z.number() }),
execute: ({ x, y }) => x + y,
}),
],
});
new LlamaIndexServer({
workflow: () => calculatorAgent,
uiConfig: {
appTitle: "Calculator",
starterQuestions: ["1 + 1", "2 + 2"],
},
port: 4000,
}).start();
-14
View File
@@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["**/*"],
"exclude": ["node_modules", "dist"]
}
-39
View File
@@ -1,39 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
output/
!lib/
@@ -1,56 +0,0 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "./lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200 dark:text-neutral-400" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -1,157 +0,0 @@
"use client";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import * as React from "react";
import { buttonVariants } from "./button";
import { cn } from "./lib/utils";
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};
@@ -1,68 +0,0 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "./lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertDescription, AlertTitle };
@@ -1,11 +0,0 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };
@@ -1,53 +0,0 @@
"use client";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import { cn } from "./lib/utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarFallback, AvatarImage };
@@ -1,38 +0,0 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "./lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
@@ -1,111 +0,0 @@
"use client";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { cn } from "./lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
};
@@ -1,58 +0,0 @@
"use client";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "./lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
@@ -1,75 +0,0 @@
"use client";
import { ChevronLeft, ChevronRight } from "lucide-react";
import * as React from "react";
import { DayPicker } from "react-day-picker";
import { buttonVariants } from "./button";
import { cn } from "./lib/utils";
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
);
}
export { Calendar };
@@ -1,83 +0,0 @@
"use client";
import { forwardRef } from "react";
import { cn } from "./lib/utils";
const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className,
)}
{...props}
/>
),
);
Card.displayName = "Card";
const CardHeader = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
};
@@ -1,241 +0,0 @@
"use client";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import * as React from "react";
import { Button } from "./button";
import { cn } from "./lib/utils";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
};
@@ -1,353 +0,0 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "./lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-sector[stroke='#fff']]:stroke-transparent",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartStyle,
ChartTooltip,
ChartTooltipContent,
};
@@ -1,24 +0,0 @@
"use client";
import { ChatCanvas, useChatCanvas } from "@llamaindex/chat-ui";
import { ResizableHandle, ResizablePanel } from "../../resizable";
import { CodeArtifactRenderer } from "./preview";
export function ChatCanvasPanel() {
const { displayedArtifact, isCanvasOpen } = useChatCanvas();
if (!displayedArtifact || !isCanvasOpen) return null;
return (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60} minSize={50}>
<ChatCanvas className="w-full">
<ChatCanvas.CodeArtifact
tabs={{ preview: <CodeArtifactRenderer /> }}
/>
<ChatCanvas.DocumentArtifact />
</ChatCanvas>
</ResizablePanel>
</>
);
}
@@ -1,155 +0,0 @@
"use client";
import { CodeArtifact, useChatCanvas } from "@llamaindex/chat-ui";
import { Loader2, WandSparkles } from "lucide-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../../accordion";
import { buttonVariants } from "../../button";
import { cn } from "../../lib/utils";
import { DynamicComponentErrorBoundary } from "../custom/events/error-boundary";
import { parseComponent } from "../custom/events/loader";
const SUPPORTED_FRONTEND_PREVIEW = [
"js",
"ts",
"jsx",
"tsx",
"javascript",
"typescript",
];
export function CodeArtifactRenderer() {
const { displayedArtifact } = useChatCanvas();
if (displayedArtifact?.type !== "code") return null;
const codeArtifact = displayedArtifact as CodeArtifact;
if (!SUPPORTED_FRONTEND_PREVIEW.includes(codeArtifact.data.language)) {
return (
<div className="flex h-full items-center justify-center gap-2">
<p className="text-sm text-gray-500">
Preview is not supported for this language
</p>
</div>
);
}
return <CodeArtifactRendererComp artifact={codeArtifact} />;
}
function CodeArtifactRendererComp({ artifact }: { artifact: CodeArtifact }) {
const { appendErrors } = useChatCanvas();
const [isRendering, setIsRendering] = useState(true);
const [component, setComponent] = useState<FunctionComponent | null>(null);
const {
data: { code, file_name },
} = artifact;
useEffect(() => {
const renderComponent = async () => {
setIsRendering(true);
const { component: parsedComponent, error } = await parseComponent(
code,
file_name,
);
if (error) {
setComponent(null);
appendErrors(artifact, [error]);
} else {
setComponent(() => parsedComponent);
}
setIsRendering(false);
};
renderComponent();
}, [artifact]);
if (isRendering) {
return (
<div className="flex h-full items-center justify-center gap-2">
<Loader2 className="size-4 animate-spin" />
<p className="text-sm text-gray-500">Rendering Artifact...</p>
</div>
);
}
if (!component) {
return <CodeErrors artifact={artifact} />;
}
return (
<DynamicComponentErrorBoundary
onError={(error) => appendErrors(artifact, [error])}
>
{React.createElement(component)}
</DynamicComponentErrorBoundary>
);
}
function CodeErrors({ artifact }: { artifact: CodeArtifact }) {
const { getCodeErrors, fixCodeErrors } = useChatCanvas();
const uniqueErrors = getCodeErrors(artifact);
if (uniqueErrors.length === 0) return null;
return (
<div className="flex flex-col gap-10 px-10 pt-10">
<p className="text-center text-sm text-gray-500">
Error when rendering code, please check the details and try fixing them.
</p>
<Accordion
type="single"
defaultValue="errors"
collapsible
className="w-full rounded-xl border border-gray-100 bg-white shadow-md"
>
<AccordionItem value="errors" className="border-none px-4">
<AccordionTrigger className="py-2 hover:no-underline">
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-muted-foreground font-bold">
Rendering errors
</span>
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500 text-xs text-white">
{uniqueErrors.length}
</span>
</div>
<div className="flex items-center gap-2">
<div
className={cn(
buttonVariants({ variant: "default", size: "sm" }),
"mr-2 h-8 cursor-pointer bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600",
)}
onClick={(e) => {
e.stopPropagation();
fixCodeErrors(artifact);
}}
>
<WandSparkles className="mr-2 h-4 w-4" />
<span>Fix errors</span>
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="pb-4">
<div className="space-y-2">
{uniqueErrors.map((error, index) => (
<p key={index} className="text-muted-foreground text-sm">
{error}
</p>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}
@@ -1,15 +0,0 @@
"use client";
import { ChatMessage } from "@llamaindex/chat-ui";
export function ChatMessageAvatar() {
return (
<ChatMessage.Avatar>
<img
className="border-1 rounded-full border-[#e711dd]"
src="/llama.png"
alt="Llama Logo"
/>
</ChatMessage.Avatar>
);
}
@@ -1,55 +0,0 @@
"use client";
import { Sparkles, Star } from "lucide-react";
import { Button } from "../button";
import { getConfig } from "../lib/utils";
export function ChatHeader() {
return (
<div className="flex items-center justify-between px-4 pt-2">
<ChatAppTitle />
<LlamaIndexLinks />
</div>
);
}
function ChatAppTitle() {
return (
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
<h1 className="font-semibold">{getConfig("APP_TITLE")}</h1>
</div>
);
}
function LlamaIndexLinks() {
return (
<div className="flex items-center justify-end gap-4">
<div className="flex items-center gap-2">
<a
href="https://www.llamaindex.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Built by LlamaIndex
</a>
<img
className="h-[24px] w-[24px] rounded-sm"
src="/llama.png"
alt="Llama Logo"
/>
</div>
<a
href="https://github.com/run-llama/LlamaIndexTS"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" size="sm">
<Star className="mr-2 size-4" />
Star on GitHub
</Button>
</a>
</div>
);
}
@@ -1,32 +0,0 @@
"use client";
/**
* The default border color has changed to `currentColor` in Tailwind CSS v4,
* so adding these compatibility styles to make sure everything still
* looks the same as it did with Tailwind CSS v3.
*/
const tailwindConfig = `
@import "tailwindcss";
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
`;
export function ChatInjection() {
return (
<>
<script
async
src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"
></script>
<style type="text/tailwindcss">{tailwindConfig}</style>
</>
);
}
@@ -1,78 +0,0 @@
"use client";
import { ChatInput, useChatUI, useFile } from "@llamaindex/chat-ui";
import { DocumentInfo, ImagePreview } from "@llamaindex/chat-ui/widgets";
import { getConfig } from "../lib/utils";
import { LlamaCloudSelector } from "./custom/llama-cloud-selector";
export default function CustomChatInput() {
const { requestData, isLoading, input } = useChatUI();
const uploadAPI = getConfig("UPLOAD_API") ?? "";
const llamaCloudAPI = getConfig("LLAMA_CLOUD_API") ?? "";
const {
imageUrl,
setImageUrl,
uploadFile,
files,
removeDoc,
reset,
getAnnotations,
} = useFile({ uploadAPI });
/**
* Handles file uploads. Overwrite to hook into the file upload behavior.
* @param file The file to upload
*/
const handleUploadFile = async (file: File) => {
// There's already an image uploaded, only allow one image at a time
if (imageUrl) {
alert("You can only upload one image at a time.");
return;
}
try {
// Upload the file and send with it the current request data
await uploadFile(file, requestData);
} catch (error: unknown) {
// Show error message if upload fails
alert(
error instanceof Error ? error.message : "An unknown error occurred",
);
}
};
// Get references to the upload files in message annotations format, see https://github.com/run-llama/chat-ui/blob/main/packages/chat-ui/src/hook/use-file.tsx#L56
const annotations = getAnnotations();
return (
<ChatInput resetUploadedFiles={reset} annotations={annotations}>
{/* Image preview section */}
{imageUrl && (
<ImagePreview url={imageUrl} onRemove={() => setImageUrl(null)} />
)}
{/* Document previews section */}
{files.length > 0 && (
<div className="flex w-full gap-4 overflow-auto py-2">
{files.map((file) => (
<DocumentInfo
key={file.id}
document={{ url: file.url, sources: [] }}
className="mb-2 mt-2"
onRemove={() => removeDoc(file)}
/>
))}
</div>
)}
<ChatInput.Form>
<ChatInput.Field />
{uploadAPI && <ChatInput.Upload onUpload={handleUploadFile} />}
{llamaCloudAPI && <LlamaCloudSelector />}
<ChatInput.Submit
disabled={
isLoading || (!input.trim() && files.length === 0 && !imageUrl)
}
/>
</ChatInput.Form>
</ChatInput>
);
}
@@ -1,29 +0,0 @@
"use client";
import { ChatMessage } from "@llamaindex/chat-ui";
import { DynamicEvents } from "./custom/events/dynamic-events";
import { ComponentDef } from "./custom/events/types";
import { ToolAnnotations } from "./tools/chat-tools";
export function ChatMessageContent({
componentDefs,
appendError,
}: {
componentDefs: ComponentDef[];
appendError: (error: string) => void;
}) {
return (
<ChatMessage.Content>
<ChatMessage.Content.Event />
<ChatMessage.Content.AgentEvent />
<ToolAnnotations />
<ChatMessage.Content.Image />
<DynamicEvents componentDefs={componentDefs} appendError={appendError} />
<ChatMessage.Content.Artifact />
<ChatMessage.Content.Markdown />
<ChatMessage.Content.DocumentFile />
<ChatMessage.Content.Source />
<ChatMessage.Content.SuggestedQuestions />
</ChatMessage.Content>
);
}

Some files were not shown because too many files have changed in this diff Show More