Compare commits

...

15 Commits

Author SHA1 Message Date
github-actions[bot] ad5912b41f Release 0.5.13 (#605)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-09 17:31:53 +07:00
Marcus Schiesser 76502d28e7 chore: remove changeset 2025-05-09 17:29:50 +07:00
Huu Le f4ca602da5 feat: Add artifact use case and use new the workflow for Typescript (#595)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-05-09 17:20:30 +07:00
Thuc Pham d304554f33 feat: add examples package for easily testing workflow (#599) 2025-05-08 17:15:00 +07:00
github-actions[bot] 8dce9f913d Release 0.2.0 (#591)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-08 17:14:11 +07:00
Marcus Schiesser c62096c516 fix: server packages (#598) 2025-05-08 16:34:51 +07:00
Huu Le 0384268543 Support the new workflow for @llamaindex/server (#592)
* use the new llama-flow workflow

* update create-llama

* update deep research workflow to use llama-flow

* update ts config to avoid overhead checking python

* Refactor workflows to use startAgentEvent and remove workflowInputEvent. Clean up unused imports and improve error handling for missing user input.

* Refactor runWorkflow to utilize AgentInputData and improve error handling for missing user input. Replace workflowInputEvent with startAgentEvent and enhance chat history management. Add callbacks for suggested questions event.

* Implement code artifact workflow and update TypeScript helpers. Introduce new `code_workflow.ts` for managing code generation and updates, and create a factory function in `workflow.ts`. Modify TypeScript helper to copy all `.ts` files instead of just `workflow.ts`. Update chat handler to utilize `AgentInputData` for improved data handling.

* Refactor runWorkflow to utilize the run function for workflow execution, replacing the previous context creation method. This change simplifies the workflow stream initialization and enhances code clarity.

* Refactor workflows to replace stopEvent with stopAgentEvent and enhance event handling in code_workflow.ts and workflow.ts. Update grammar in enhancedPrompt for clarity and improve response handling in agentStreamEvent.

* Refactor financial report workflow to streamline event handling and improve memory management. Replace custom event classes with workflowEvent for better clarity and maintainability. Update workflow definition to utilize getWorkflow function, enhancing code organization and readability.

* Add document and code artifact workflows with event handling improvements

- Introduced `doc-workflow.ts` for managing document generation and updates.
- Created `code-workflow.ts` for code artifact management.
- Enhanced event handling with `workflowEvent` for better clarity and maintainability.
- Updated `README-template.md` to include setup instructions and use cases for new workflows.
- Modified `workflow.ts` to allow switching between code and document workflows.
- Improved grammar and clarity in prompts and comments throughout the code.

* Refactor workflow.ts to replace ReadableStream with TransformStream for improved event handling. Introduce workflowToEngineResponseStream function to streamline the processing of workflow events and enhance error handling. Update return statement in runWorkflow to utilize the new stream implementation.

* add changesets

* Remove redundant totalQuestions update in getWorkflow function to streamline event processing.

* Migrate workflow types to @llamaindex/workflow package and update imports

* Replace @llama-flow/core with @llamaindex/workflow and update stream handling

* update workflows

* update import for agentic rag

* fix wrong import

* init new stream method

* Refactor stream handling in workflow.ts and stream.ts to utilize WorkflowStream type. Update processWorkflowStream function for improved event processing and clarity. Enhance imports from @llamaindex/workflow.

* Refactor stream handling in request.ts and stream.ts to improve type usage and error handling. Update toDataStreamResponse function to toDataStream and enhance callback functionality for better stream management in workflow.ts.

* Refactor server.ts, types.ts, and chat.ts to streamline workflow type usage and improve error handling. Update toDataStream function in stream.ts for better data streaming and processing. Enhance imports from @llamaindex/workflow for consistency.

* Enhance chat handler to include suggested questions functionality. Refactor toDataStream in stream.ts to support callback options for onStart, onText, and onFinal events. Export generateNextQuestions function in suggestion.ts for improved accessibility.

* Refactor workflow imports in deep_research and financial_report templates to enhance consistency and organization. Update package.json to include @llamaindex/workflow version 1.1.0. Remove commented-out code in gen-ui.ts for cleaner implementation.

* remove log

* fix incorrect toolcall llm check

* relock

* revert changes on create-llama
2025-05-07 17:15:07 +07:00
Thuc Pham d9f9e3c1c3 chore: bump chat-ui to support code editor & document editor (#594) 2025-05-06 16:56:26 +07:00
Thuc Pham 1357c423a3 chore: move lint & prettier configs to root (#590)
* chore: move lint & prettier configs to root

* update prettier config

* fix: format

* use bunchee in root

* move typescript packages to root

* apply recommened typescript rules for create-llama and fix lints

* apply prettier-plugin-tailwindcss to auto sort tailwind classnames

* Create ninety-goats-draw.md
2025-04-29 16:45:57 +07:00
github-actions[bot] 8105aa70b6 Release 0.5.12 (#589)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-29 15:48:08 +07:00
Marcus Schiesser 23a90625d1 chore: add ruff check 2025-04-29 15:47:13 +07:00
Marcus Schiesser ac789bcb8d chore: check python format 2025-04-29 15:42:10 +07:00
Huu Le 241d82a87d feat: add create-llama artifacts template (python) (#586)
* add artifact template for python

* Add artifact workflows for code and document generation

- Introduced `CodeArtifactWorkflow` and `DocumentArtifactWorkflow` classes to handle code and document artifacts respectively.
- Updated README to include instructions for modifying the factory method to select the appropriate workflow.
- Enhanced clarity in class documentation and improved naming conventions for better understanding.

* bump packages

* fix wrong name

* add ts workflows

* revert change for TS

* docs: fix docs

* add metadata fields

---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-04-29 14:22:16 +07:00
github-actions[bot] b16cfd873b chore(release): bump llama-index-server version to 0.1.15 (#576)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-04-28 15:55:05 +07:00
Huu Le 3130cdf18d Add support for artifact in llama-index-server (#580)
* support artifact

* migrate poetry to uv

* fix ci

* update ci

* 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.

* enhance code

* remove previous content from tool input

* fix test

* bump chat ui

* revert changes

* remove dead code

* 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.

* remove app_writer workflow

* 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.

* Use uv to release package

* 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.

* move code

* Merge remote-tracking branch 'origin/main' into lee/add-artifact

* sort artifact

* fix mypy

* fix adding custom route does not work

* fix mypy

* revert create-llama change

* disable e2e test for python package change

* fix missing set memory

* remove include last artifact in the code

* Add ArtifactEvent model and update workflows to use it
2025-04-28 15:49:20 +07:00
144 changed files with 10696 additions and 7636 deletions
+2
View File
@@ -4,10 +4,12 @@ on:
branches: [main]
paths-ignore:
- "python/llama-index-server/**"
- ".github/workflows/*llama_index_server.yml"
pull_request:
branches: [main]
paths-ignore:
- "python/llama-index-server/**"
- ".github/workflows/*llama_index_server.yml"
jobs:
e2e-python:
@@ -31,6 +31,13 @@ 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:
@@ -22,7 +22,8 @@ jobs:
working-directory: ./python/llama-index-server
if: |
github.event_name == 'push' &&
!startsWith(github.ref, 'refs/heads/release/llama-index-server-v')
!startsWith(github.ref, 'refs/heads/release/llama-index-server-v') &&
!contains(github.event.head_commit.message, 'Release: llama-index-server v')
steps:
- name: Checkout Repository
@@ -30,17 +31,19 @@ jobs:
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
shell: bash
run: uv sync --all-extras --dev
- name: Setup Git
run: |
@@ -48,15 +51,17 @@ jobs:
git config --global user.name "github-actions[bot]"
- name: Bump patch version
shell: bash
run: |
poetry version patch
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version $(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version | awk -F. '{$NF = $NF + 1;}1' OFS=.)
git add pyproject.toml
git commit -m "chore(release): bump version to $(poetry version -s)"
git commit -m "chore(release): bump llama-index-server version to $(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)"
- name: Get current version
id: get_version
shell: bash
run: |
version=$(poetry version -s)
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Create Release PR
@@ -91,31 +96,34 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
shell: bash
run: uv sync --all-extras
- name: Get current version
id: get_version
shell: bash
run: |
version=$(poetry version -s)
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Build and publish to PyPI
uses: JRubics/poetry-publish@v2.1
with:
python_version: "3.11"
pypi_token: ${{ secrets.PYPI_TOKEN }}
package_directory: "python/llama-index-server"
poetry_install_options: "--without dev"
- name: Build package
shell: bash
run: uv build --no-sources
- name: Publish to PyPI
shell: bash
run: uv publish --token ${{ secrets.PYPI_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
+25 -37
View File
@@ -4,7 +4,6 @@ on:
pull_request:
env:
POETRY_VERSION: "1.8.3"
PYTHON_VERSION: "3.9"
jobs:
@@ -21,29 +20,23 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up python ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Configure Poetry
run: |
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
poetry env use python
- name: Install dependencies
shell: bash
run: poetry install --with dev
run: uv sync --all-extras --dev
- name: Run unit tests
shell: bash
run: |
poetry run pytest tests
run: uv run pytest tests
type-check:
name: Type Check
@@ -54,28 +47,23 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "poetry"
- name: Configure Poetry
run: |
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
poetry env use python
- name: Install dependencies
shell: bash
run: poetry install --with dev
run: uv sync --all-extras --dev
- name: Run mypy
shell: bash
run: poetry run mypy llama_index
run: uv run mypy llama_index
build:
needs: [unit-test, type-check]
@@ -85,25 +73,25 @@ jobs:
working-directory: python/llama-index-server
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Clear python cache
- name: Install build package
shell: bash
run: poetry cache clear --all pypi
- name: Build package
shell: bash
run: poetry build
- name: Test installing built package
shell: bash
run: python -m pip install .
run: uv sync --all-extras
- name: Test import
shell: bash
working-directory: ${{ vars.RUNNER_TEMP }}
run: python -c "from llama_index.server import LlamaIndexServer"
run: uv run python -c "from llama_index.server import LlamaIndexServer"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
+2 -1
View File
@@ -1,3 +1,4 @@
pnpm format
pnpm lint
uvx ruff format --check packages/create-llama/templates/
uvx ruff check .
uvx ruff format . --check
+15
View File
@@ -0,0 +1,15 @@
node_modules/
pnpm-lock.yaml
lib/
dist/
cache/
build/
.next/
out/
packages/server/server/
# Python
python/
**/*.mypy_cache/**
**/*.venv/**
**/*.ruff_cache/**
@@ -18,6 +18,21 @@ export default tseslint.config(
},
},
{
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",
@@ -31,7 +46,12 @@ export default tseslint.config(
},
{
ignores: [
"python/**",
"**/*.mypy_cache/**",
"**/*.venv/**",
"**/*.ruff_cache/**",
"**/dist/**",
"**/e2e/cache/**",
"**/lib/*",
"**/.next/**",
"**/out/**",
+53 -37
View File
@@ -1,39 +1,55 @@
{
"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 build",
"e2e": "pnpm -r e2e",
"dev": "pnpm -r dev",
"format": "pnpm -r format",
"format:write": "pnpm -r format:write",
"lint": "pnpm -r lint"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"husky": "^9.0.10"
},
"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": {
"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"
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"extends": [
"prettier"
],
"rules": {
"max-params": [
"error",
4
],
"prefer-const": "error",
},
}
-6
View File
@@ -1,6 +0,0 @@
apps/docs/i18n
apps/docs/docs/api
pnpm-lock.yaml
lib/
dist/
.docusaurus/
+13
View File
@@ -1,5 +1,18 @@
# 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
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import path from "path";
import { green, yellow } from "picocolors";
import { tryGitInit } from "./helpers/git";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -1,4 +1,3 @@
/* 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
let dataSourceArgs = [];
if (dataSource.includes("--web-source" || "--db-source")) {
const dataSourceArgs = [];
if (dataSource.includes("--web-source")) {
const webSource = dataSource.split(" ")[1];
dataSourceArgs.push("--web-source", webSource);
} else if (dataSource.includes("--db-source")) {
-1
View File
@@ -1,4 +1,3 @@
/* 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,6 +206,7 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
});
}
return envs;
}
case "weaviate":
return [
{
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import spawn from "cross-spawn";
import { yellow } from "picocolors";
import type { PackageManager } from "./get-pkg-manager";
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from "fs";
import path from "path";
import { blue, green } from "picocolors";
-1
View File
@@ -1,4 +1,3 @@
/* 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) {
let choices = [
const choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
{ title: "Ollama", value: "ollama" },
+2 -1
View File
@@ -31,6 +31,7 @@ const getAdditionalDependencies = (
tools?: Tool[],
templateType?: TemplateType,
observability?: TemplateObservability,
// eslint-disable-next-line max-params
) => {
const dependencies: Dependency[] = [];
@@ -562,7 +563,7 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("workflow.py", path.join(root, "app"), {
await copy("*.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
});
+2 -1
View File
@@ -57,7 +57,8 @@ export type TemplateUseCase =
| "form_filling"
| "extractor"
| "contract_review"
| "agentic_rag";
| "agentic_rag"
| "artifacts";
// 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("workflow.ts", path.join(root, "src", "app"), {
await copy("*.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": "^2.0.0",
"@llamaindex/readers": "^3.0.0",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import validateProjectName from "validate-npm-package-name";
export function validateNpmName(name: string): {
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import { Command } from "commander";
import fs from "fs";
+1 -8
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.11",
"version": "0.5.13",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -31,9 +31,6 @@
"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": {
@@ -66,10 +63,6 @@
"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,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
+34 -20
View File
@@ -6,7 +6,11 @@ import { ModelConfig, TemplateFramework } from "../helpers/types";
import { PureQuestionArgs, QuestionResults } from "./types";
import { askPostInstallAction, questionHandlers } from "./utils";
type AppType = "agentic_rag" | "financial_report" | "deep_research";
type AppType =
| "agentic_rag"
| "financial_report"
| "deep_research"
| "artifacts";
type SimpleAnswers = {
appType: AppType;
@@ -42,6 +46,12 @@ 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,
@@ -52,21 +62,19 @@ export const askSimpleQuestions = async (
let useLlamaCloud = false;
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 { 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(
{
@@ -111,10 +119,10 @@ const convertAnswers = async (
args: PureQuestionArgs,
answers: SimpleAnswers,
): Promise<QuestionResults> => {
const MODEL_GPT4o: ModelConfig = {
const MODEL_GPT41: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4o",
model: "gpt-4.1",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
@@ -135,13 +143,19 @@ const convertAnswers = async (
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: getTools(["interpreter", "document_generator"]),
modelConfig: MODEL_GPT4o,
modelConfig: MODEL_GPT41,
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: [],
modelConfig: MODEL_GPT4o,
modelConfig: MODEL_GPT41,
},
artifacts: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
};
@@ -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,6 +199,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
url: this.getFileUrl(filename),
});
break;
}
default:
output.push({
type: ext as InterpreterExtraType,
@@ -1,5 +1,9 @@
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import {
Document,
LLamaCloudFileService,
LlamaCloudIndex,
VectorStoreIndex,
} from "llamaindex";
import { DocumentFile } from "../streaming/annotations";
import { parseFile, storeFile } from "./helper";
import { runPipeline } from "./pipeline";
@@ -10,6 +10,7 @@ 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,6 +11,7 @@ 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="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
<div className="bg-background flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow">
<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 items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4"
className="flex w-full max-w-5xl items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl"
>
<input
autoFocus
name="message"
placeholder="Type a message"
className="w-full p-4 rounded-xl shadow-inner flex-1"
className="w-full flex-1 rounded-xl p-4 shadow-inner"
value={props.input}
onChange={props.handleInputChange}
/>
<button
disabled={props.isLoading}
type="submit"
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"
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"
>
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="break-words whitespace-pre-wrap">{message.content}</p>
<p className="whitespace-pre-wrap break-words">{message.content}</p>
</div>
);
}
@@ -39,7 +39,7 @@ export default function ChatMessages({
return (
<div
className="flex-1 w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl overflow-auto"
className="w-full max-w-5xl flex-1 overflow-auto rounded-xl bg-white p-4 shadow-xl"
ref={scrollableChatContainerRef}
>
<div className="flex flex-col gap-5 divide-y">
@@ -0,0 +1,137 @@
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 text-blue-500 animate-spin" />;
return <Loader2 className="h-4 w-4 animate-spin text-blue-500" />;
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="w-full max-w-4xl mx-auto space-y-6 p-4">
<div className="mx-auto w-full max-w-4xl space-y-6 p-4">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="mb-6 flex items-center justify-between">
<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-blue-100 shadow-lg"
? "border-blue-500 shadow-lg shadow-blue-100"
: 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-blue-100 shadow-lg"
? "border-blue-500 shadow-lg shadow-blue-100"
: 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 border rounded-lg overflow-hidden",
"mb-4 overflow-hidden rounded-lg border",
answer.state === "inprogress"
? "border-blue-500 shadow-blue-100 shadow-sm"
? "border-blue-500 shadow-sm shadow-blue-100"
: 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 items-center space-x-1 shrink-0",
"ml-auto flex shrink-0 items-center space-x-1",
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(
"p-3 rounded-md",
"rounded-md p-3",
answer.state === "done"
? "bg-green-50"
: answer.state === "inprogress"
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { LlamaCloudIndex } from "llamaindex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { AstraDBVectorStore } from "@llamaindex/astra";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { AstraDBVectorStore } from "@llamaindex/astra";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { ChromaVectorStore } from "@llamaindex/chroma";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* 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/cloud/LlamaCloudIndex";
import { LlamaCloudIndex } from "llamaindex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MilvusVectorStore } from "@llamaindex/milvus";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch } from "@llamaindex/mongodb";
import * as dotenv from "dotenv";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch } from "@llamaindex/mongodb";
import { VectorStoreIndex } from "llamaindex";
import { MongoClient } from "mongodb";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { PineconeVectorStore } from "@llamaindex/pinecone";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { PineconeVectorStore } from "@llamaindex/pinecone";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { QdrantVectorStore } from "@llamaindex/qdrant";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { WeaviateVectorStore } from "@llamaindex/weaviate";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -0,0 +1,69 @@
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!
@@ -0,0 +1,365 @@
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)
@@ -0,0 +1,337 @@
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)
@@ -0,0 +1,15 @@
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";
import { agent } from "@llamaindex/workflow";
import { getIndex } from "./data";
export const workflowFactory = async (reqBody: any) => {
@@ -0,0 +1,56 @@
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!
@@ -0,0 +1,360 @@
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;
}
@@ -0,0 +1,341 @@
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;
}
@@ -0,0 +1,12 @@
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/deep_research_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/ui_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,22 +1,21 @@
import { toSourceEvent, toStreamGenerator } from "@llamaindex/server";
import { toSourceEvent } from "@llamaindex/server";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
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";
@@ -25,12 +24,11 @@ import { getIndex } from "./data";
// workflow factory
export const workflowFactory = async (reqBody: any) => {
const index = await getIndex(reqBody?.data);
return new DeepResearchWorkflow(index);
return getWorkflow(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({
@@ -114,10 +112,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<{}> {}
class ResearchEvent extends WorkflowEvent<ResearchQuestion[]> {}
class ReportEvent extends WorkflowEvent<{}> {}
class StopEvent extends StopEventBase<AsyncGenerator<ChatResponseChunk>> {}
// class PlanResearchEvent extends WorkflowEvent<{}> {}
const planResearchEvent = workflowEvent<{}>();
const researchEvent = workflowEvent<ResearchQuestion>();
const reportEvent = workflowEvent<{}>();
export const UIEventSchema = z
.object({
@@ -140,221 +138,180 @@ export const UIEventSchema = z
type UIEventData = z.infer<typeof UIEventSchema>;
class UIEvent extends WorkflowEvent<{
const uiEvent = workflowEvent<{
type: "ui_event";
data: UIEventData;
}> {}
}>();
// workflow definition
class DeepResearchWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
#llm = Settings.llm as ToolCallLLM;
#index?: VectorStoreIndex | LlamaCloudIndex;
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());
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) {
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
const { sendEvent, state } = getContext();
if (!userInput) throw new Error("Invalid input");
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({
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
state.userRequest = userInput;
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "retrieve", state: "inprogress" },
data: {
event: "retrieve",
state: "inprogress",
},
}),
);
const retrievedNodes = await this.retriever.retrieve(this.userRequest);
const retrievedNodes = await retriever.retrieve(userInput);
ctx.sendEvent(toSourceEvent(retrievedNodes));
ctx.sendEvent(
new UIEvent({
sendEvent(toSourceEvent(retrievedNodes));
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "retrieve", state: "done" },
}),
);
this.contextNodes = retrievedNodes;
state.contextNodes.push(...retrievedNodes);
return new PlanResearchEvent({});
};
return planResearchEvent.with({});
});
handlePlanResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: PlanResearchEvent,
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
ctx.sendEvent(
new UIEvent({
workflow.handle([planResearchEvent], async ({ data }) => {
const { sendEvent, state, stream } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "inprogress" },
}),
);
const { decision, researchQuestions, cancelReason } =
await this.createResearchPlan();
await createResearchPlan(
state.memory,
state.contextNodes
.map((node) => node.node.getContent(MetadataMode.NONE))
.join("\n"),
enhancedPrompt(state.totalQuestions),
state.userRequest,
);
// Stop workflow due to decision from LLM
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
if (decision === "cancel") {
ctx.sendEvent(
new UIEvent({
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return new StopEvent(
toStreamGenerator(
cancelReason ?? "Research cancelled without any reason.",
),
);
return agentStreamEvent.with({
delta: cancelReason ?? "Research cancelled without any reason.",
response: cancelReason ?? "Research cancelled without any reason.",
currentAgentName: "",
raw: null,
});
}
// Trigger research from generated questions
if (decision === "research") {
this.memory.put({
if (decision === "research" && researchQuestions.length > 0) {
state.totalQuestions += researchQuestions.length;
state.memory.put({
role: "assistant",
content:
"We need to find answers to the following questions:\n" +
researchQuestions.join("\n"),
});
researchQuestions.forEach(({ questionId: id, question }) => {
ctx.sendEvent(
new UIEvent({
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "answer", state: "pending", id, question },
}),
);
sendEvent(researchEvent.with({ questionId: id, question }));
});
return new ResearchEvent(researchQuestions);
const events = await stream
.until(() => state.researchResults.length === researchQuestions.length)
.toArray();
return planResearchEvent.with({});
}
// Resarch done, start writing report
this.memory.put({
state.memory.put({
role: "assistant",
content: "No more idea to analyze. We should report the answers.",
});
ctx.sendEvent(
new UIEvent({
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return reportEvent.with({});
});
return new ReportEvent({});
};
workflow.handle([researchEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const { questionId, question } = data;
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 };
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "answer",
state: "inprogress",
id: questionId,
question,
},
}),
);
// Save answers to memory
researchResults.forEach(({ question, answer }) => {
this.memory.put({
role: "assistant",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
});
});
const answer = await answerQuestion(
contextStr(state.contextNodes),
question,
);
state.researchResults.push({ questionId, question, answer });
this.memory.put({
state.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.",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
});
this.totalQuestions += researchResults.length;
return new PlanResearchEvent({});
};
handleReport = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportEvent,
): Promise<StopEvent> => {
const chatHistory = await this.memory.getAllMessages();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "answer",
state: "done",
id: questionId,
question,
answer,
},
}),
);
});
workflow.handle([reportEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const chatHistory = await state.memory.getAllMessages();
const messages = chatHistory.concat([
{
role: "system",
@@ -367,81 +324,91 @@ class DeepResearchWorkflow extends Workflow<
},
]);
const stream = await this.llm.chat({ messages, stream: true });
return new StopEvent(toStreamGenerator(stream));
};
get llm() {
if (!this.#llm.supportToolCall) throw new Error("LLM is not a ToolCallLLM");
return this.#llm;
}
get retriever() {
if (!this.#index) throw new Error("Index is not initialized");
return this.#index.asRetriever({ similarityTopK: TOP_K });
}
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");
}
get enhancedPrompt() {
if (this.totalQuestions === 0) {
return "The student has no questions to research. Let start by asking some questions.";
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,
}),
);
}
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,
return stopAgentEvent.with({
result: response,
});
});
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;
}
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,
})),
};
};
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.";
}
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.`;
}
return "";
};
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;
};
@@ -6,27 +6,25 @@ import {
interpreter,
} from "@llamaindex/tools";
import {
AgentInputData,
AgentWorkflowContext,
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import {
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);
@@ -47,28 +45,18 @@ export async function workflowFactory(reqBody: any) {
});
const documentGeneratorTool = documentGenerator();
return new FinancialReportWorkflow({
return getWorkflow(
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
timeout: TIMEOUT,
});
);
}
// 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[];
}> {}
// workflow events
const inputEvent = workflowEvent<{ input: ChatMessage[] }>();
const researchEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
const analyzeEvent = workflowEvent<{ input: ChatMessage | ToolCall[] }>();
const reportGenerationEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
const DEFAULT_SYSTEM_PROMPT = `
You are a financial analyst who are given a set of tools to help you.
@@ -76,173 +64,103 @@ 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.
`;
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,
);
// 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");
}
const { withState, getContext } = createStatefulMiddleware(() => ({
memory: new ChatMemoryBuffer({ llm, chatHistory: [] }),
}));
prepareChatHistory = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<InputEvent> => {
const { userInput, chatHistory = [] } = ev.data;
const workflow = withState(createWorkflow());
// Add steps
workflow.handle([startAgentEvent], async ({ data }) => {
const { state } = getContext();
const { userInput, chatHistory = [] } = data;
if (!userInput) throw new Error("Invalid input");
this.memory.set(chatHistory);
state.memory.set(chatHistory);
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
state.memory.put({ role: "system", content: DEFAULT_SYSTEM_PROMPT });
this.memory.put({ role: "user", content: userInput });
state.memory.put({ role: "user", content: userInput });
const messages = await this.memory.getMessages();
return new InputEvent({ input: messages });
};
const messages = await state.memory.getMessages();
return inputEvent.with({ input: messages });
});
handleLLMInput = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: InputEvent,
): Promise<
| InputEvent
| ResearchEvent
| AnalyzeEvent
| ReportGenerationEvent
| StopEvent<AsyncGenerator<ChatResponseChunk, any, any> | undefined>
> => {
const chatHistory = ev.data.input;
workflow.handle([inputEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const chatHistory = data.input;
const tools = [
this.codeInterpreterTool,
this.documentGeneratorTool,
this.queryEngineTool,
];
const tools = [codeInterpreterTool, documentGeneratorTool, queryEngineTool];
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
const toolCallResponse = await chatWithTools(llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
return new StopEvent(toolCallResponse.responseGenerator);
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 });
}
if (toolCallResponse.hasMultipleTools()) {
this.memory.put({
state.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
const newChatHistory = await state.memory.getMessages();
return inputEvent.with({ input: newChatHistory });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
this.memory.put(toolCallResponse.toolCallMessage);
state.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case this.codeInterpreterTool.metadata.name:
return new AnalyzeEvent({
case codeInterpreterTool.metadata.name:
return analyzeEvent.with({
input: toolCallResponse.toolCalls,
});
case this.documentGeneratorTool.metadata.name:
return new ReportGenerationEvent({
case documentGeneratorTool.metadata.name:
return reportGenerationEvent.with({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (this.queryEngineTool.metadata.name === toolName) {
return new ResearchEvent({
if (queryEngineTool.metadata.name === toolName) {
return researchEvent.with({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
};
});
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<AnalyzeEvent> => {
ctx.sendEvent(
workflow.handle([researchEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
sendEvent(
toAgentRunEvent({
agent: "Researcher",
text: "Researching data",
@@ -250,13 +168,13 @@ class FinancialReportWorkflow extends Workflow<
}),
);
const { toolCalls } = ev.data;
const { toolCalls } = data;
const toolMsgs = await callTools({
tools: [this.queryEngineTool],
tools: [queryEngineTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
sendEvent(
toAgentRunEvent({
agent: "Researcher",
text,
@@ -268,7 +186,7 @@ class FinancialReportWorkflow extends Workflow<
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
state.memory.put(toolMsg);
}
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
@@ -277,26 +195,25 @@ class FinancialReportWorkflow extends Workflow<
.filter(Boolean);
if (sourcesNodes.length > 0) {
ctx.sendEvent(toSourceEvent(sourcesNodes));
sendEvent(toSourceEvent(sourcesNodes));
}
return new AnalyzeEvent({
// Send a message indicating research is done, triggering analysis
return analyzeEvent.with({
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
*/
handleAnalyze = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: AnalyzeEvent,
): Promise<InputEvent> => {
ctx.sendEvent(
workflow.handle([analyzeEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
sendEvent(
toAgentRunEvent({
agent: "Analyst",
text: "Analyzing data",
@@ -305,8 +222,8 @@ class FinancialReportWorkflow extends Workflow<
);
// Request by workflow LLM, input is a list of tool calls
let toolCalls: ToolCall[] = [];
if (Array.isArray(ev.data.input)) {
toolCalls = ev.data.input;
if (Array.isArray(data.input)) {
toolCalls = data.input;
} else {
// Requested by Researcher, input is a ChatMessage
// We start new LLM chat specifically for analyzing the data
@@ -320,63 +237,65 @@ class FinancialReportWorkflow extends Workflow<
// Clone the current chat history
// Add the analysis system prompt and the message from the researcher
const chatHistory = await this.memory.getMessages();
const currentChatHistory = await state.memory.getMessages();
const newChatHistory = [
...chatHistory,
...currentChatHistory,
{ role: "system", content: analysisPrompt },
ev.data.input,
data.input, // This is the ChatMessage from the research step
];
const toolCallResponse = await chatWithTools(
this.llm,
[this.codeInterpreterTool],
llm,
[codeInterpreterTool],
newChatHistory as ChatMessage[],
);
if (!toolCallResponse.hasToolCall()) {
this.memory.put(await toolCallResponse.asFullResponse());
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
// 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 });
} else {
this.memory.put(toolCallResponse.toolCallMessage!);
state.memory.put(toolCallResponse.toolCallMessage!);
toolCalls = toolCallResponse.toolCalls;
}
}
// Call the tools
const toolMsgs = await callTools({
tools: [this.codeInterpreterTool],
toolCalls,
writeEvent: (text, step) => {
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);
// 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 chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
const finalChatHistory = await state.memory.getMessages();
// After analysis (or tool calls for analysis), trigger the next LLM input cycle
return inputEvent.with({ input: finalChatHistory });
});
handleReportGeneration = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportGenerationEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
workflow.handle([reportGenerationEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const { toolCalls } = data;
const toolMsgs = await callTools({
tools: [this.documentGeneratorTool],
tools: [documentGeneratorTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
sendEvent(
toAgentRunEvent({
agent: "Reporter",
text,
@@ -388,9 +307,12 @@ class FinancialReportWorkflow extends Workflow<
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
state.memory.put(toolMsg);
}
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
const chatHistory = await state.memory.getMessages();
// After report generation, trigger the next LLM input cycle
return inputEvent.with({ input: chatHistory });
});
return workflow;
}
@@ -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.14,<0.2.0",
"llama-index-server>=0.1.15,<0.2.0",
]
[project.optional-dependencies]
@@ -10,12 +10,12 @@
},
"dependencies": {
"@llamaindex/openai": "0.2.0",
"@llamaindex/readers": "^2.0.0",
"@llamaindex/server": "0.1.5",
"@llamaindex/server": "0.2.0",
"@llamaindex/workflow": "1.1.1",
"@llamaindex/tools": "0.0.4",
"dotenv": "^16.4.7",
"zod": "^3.23.8",
"llamaindex": "0.10.2"
"llamaindex": "0.10.5"
},
"devDependencies": {
"@types/node": "^20.10.3",
@@ -9,6 +9,7 @@ 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,8 +96,9 @@ export function Artifact({
useEffect(() => {
// auto trigger code execution
!result && fetchArtifactResult();
// eslint-disable-next-line react-hooks/exhaustive-deps
if (!result) {
fetchArtifactResult();
}
}, []);
if (!artifact || version === undefined) return null;
@@ -284,7 +285,6 @@ 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,18 +51,21 @@ 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;
}
}
}
-7
View File
@@ -1,7 +0,0 @@
lib/
dist/
server/
next/.next/
next/out/
node_modules/
build/
+10
View File
@@ -1,5 +1,15 @@
# @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
+12
View File
@@ -0,0 +1,12 @@
# 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
@@ -0,0 +1,43 @@
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
@@ -0,0 +1,24 @@
{
"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"
}
}
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -48,7 +48,7 @@ const AccordionContent = React.forwardRef<
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
@@ -54,7 +54,7 @@ function AlertDialogContent({
<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 top-[50%] left-[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",
"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}
@@ -15,7 +15,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className,
)}
{...props}
@@ -35,7 +35,7 @@ const CardTitle = forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("leading-none font-semibold tracking-tight", className)}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
@@ -187,7 +187,7 @@ function CarouselPrevious({
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
@@ -217,7 +217,7 @@ function CarouselNext({
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
@@ -55,7 +55,7 @@ function ChartContainer({
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 flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
"[&_.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}
@@ -202,7 +202,7 @@ function ChartTooltipContent({
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
"border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
@@ -6,7 +6,7 @@ export function ChatMessageAvatar() {
return (
<ChatMessage.Avatar>
<img
className="rounded-full border-1 border-[#e711dd]"
className="border-1 rounded-full border-[#e711dd]"
src="/llama.png"
alt="Llama Logo"
/>
@@ -57,7 +57,7 @@ export default function CustomChatInput() {
<DocumentInfo
key={file.id}
document={{ url: file.url, sources: [] }}
className="mt-2 mb-2"
className="mb-2 mt-2"
onRemove={() => removeDoc(file)}
/>
))}
@@ -72,7 +72,7 @@ function ChatSectionPanel() {
}, []);
return (
<ResizablePanel defaultSize={40} minSize={30} className="mx-auto max-w-1/2">
<ResizablePanel defaultSize={40} minSize={30} className="max-w-1/2 mx-auto">
<div className="flex h-full min-w-0 flex-1 flex-col gap-4">
<DynamicEventsErrors
errors={uniqueErrors}
@@ -14,7 +14,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer size-4 shrink-0 rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -45,7 +45,7 @@ function CommandDialog({
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@@ -66,7 +66,7 @@ function CommandInput({
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -83,7 +83,7 @@ function CommandList({
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
"max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden",
className,
)}
{...props}
@@ -140,7 +140,7 @@ function CommandItem({
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -66,7 +66,7 @@ function ContextMenuSubTrigger({
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm data-[inset]:pl-8 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -85,7 +85,7 @@ function ContextMenuSubContent({
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
@@ -102,7 +102,7 @@ function ContextMenuContent({
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
@@ -126,7 +126,7 @@ function ContextMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -144,7 +144,7 @@ function ContextMenuCheckboxItem({
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
checked={checked}
@@ -169,7 +169,7 @@ function ContextMenuRadioItem({
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -57,13 +57,13 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="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 top-[50%] left-[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",
"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}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@@ -102,7 +102,7 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
className={cn("text-lg font-semibold leading-none", className)}
{...props}
/>
);
@@ -42,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
checked={checked}
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm data-[inset]:pl-8",
className,
)}
{...props}
@@ -230,7 +230,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
@@ -32,7 +32,7 @@ function HoverCardContent({
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin) outline-hidden z-50 w-64 rounded-md border p-4 shadow-md",
className,
)}
{...props}
@@ -51,7 +51,7 @@ function InputOTPSlot({
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input shadow-xs relative flex h-9 w-9 items-center justify-center border-y border-r text-sm outline-none transition-all first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className,
)}
{...props}
@@ -10,7 +10,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
@@ -13,7 +13,7 @@ function Label({
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className,
)}
{...props}
@@ -14,7 +14,7 @@ function Menubar({
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
"bg-background shadow-xs flex h-9 items-center gap-1 rounded-md border p-1",
className,
)}
{...props}
@@ -56,7 +56,7 @@ function MenubarTrigger({
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex select-none items-center rounded-sm px-2 py-1 text-sm font-medium",
className,
)}
{...props}
@@ -79,7 +79,7 @@ function MenubarContent({
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin) z-50 min-w-[12rem] overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
@@ -103,7 +103,7 @@ function MenubarItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -121,7 +121,7 @@ function MenubarCheckboxItem({
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground rounded-xs outline-hidden relative flex cursor-default select-none items-center gap-2 py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
checked={checked}
@@ -146,7 +146,7 @@ function MenubarRadioItem({
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground rounded-xs outline-hidden relative flex cursor-default select-none items-center gap-2 py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
@@ -229,7 +229,7 @@ function MenubarSubTrigger({
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[inset]:pl-8",
className,
)}
{...props}
@@ -248,7 +248,7 @@ function MenubarSubContent({
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
@@ -92,8 +92,8 @@ function NavigationMenuContent({
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 left-0 top-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200",
className,
)}
{...props}
@@ -108,7 +108,7 @@ function NavigationMenuViewport({
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center",
"absolute left-0 top-full isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
@@ -131,7 +131,7 @@ function NavigationMenuLink({
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm outline-none transition-all focus-visible:outline-1 focus-visible:ring-[3px] [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -30,7 +30,7 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md",
className,
)}
{...props}

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