* adding test-proj

* add github

* Add py tests/lints

* reformats

* hmm

* fix formats

* m

* m
This commit is contained in:
Adrian Lyjak
2025-09-18 14:36:38 -04:00
committed by GitHub
parent 24059471af
commit 215aa6929b
52 changed files with 1805 additions and 327 deletions
-5
View File
@@ -1,5 +0,0 @@
# LlamaCloud API configuration
LLAMA_CLOUD_API_KEY=llx-your-api-key-here
# OpenAI API configuration
OPENAI_API_KEY=sk-your-openai-api-key-here
+2
View File
@@ -0,0 +1,2 @@
# Copy this to .env and set any necessary secrets
OPENAI_API_KEY=sk-your-openai-api-key-here
+69
View File
@@ -0,0 +1,69 @@
name: Check Template Regeneration
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
check-template:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Run regeneration check
run: uv run copier/copy_utils.py check-regeneration
check-python:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Run Python checks
run: uv run hatch run all-check
working-directory: test-proj
check-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable Corepack
run: corepack enable
- name: Activate pnpm version
working-directory: test-proj/ui
run: corepack prepare --activate
- name: Run UI checks
run: pnpm run all-check
working-directory: test-proj/ui
+4
View File
@@ -0,0 +1,4 @@
.env
__pycache__
workflows.db
.venv
+4 -16
View File
@@ -8,17 +8,10 @@ project_name:
Project name must contain only letters, numbers, and dashes
{% endif %}
llama_project_id:
project_title:
type: str
help: What is your Llama Cloud project ID?
default: ""
required: true
llama_org_id:
type: str
help: What is your Llama Cloud organization ID?
default: ""
required: true
help: What is the title of your project? This will be used in the UI Title Bar.
default: "{{ project_name.replace('-', ' ').title() }}"
# computed variables
project_name_snake:
@@ -26,15 +19,10 @@ project_name_snake:
default: "{{ project_name.replace('-', '_') }}"
when: false
project_title:
type: str
default: "{{ (project_name.replace('-', ' '))[:1] | upper ~ (project_name.replace('-', ' '))[1:] }}"
when: false
_exclude:
- "test-proj"
- ".git"
- ".github"
- "copier"
- "CONTRIBUTING.md"
- "copier.yaml"
- "copier.yaml"
+1
View File
@@ -44,6 +44,7 @@ def run_copier_quietly(src_path: str, dst_path: str, data: Dict[str, str]) -> No
data=data,
unsafe=True,
quiet=True,
vcs_ref="HEAD",
)
-8
View File
@@ -1,8 +0,0 @@
env_files:
- ".env"
llama_cloud: true
workflows:
upload: "document-qa.qa_workflows:DocumentUploadWorkflow"
chat: "document-qa.qa_workflows:ChatWorkflow"
ui:
directory: ui
-8
View File
@@ -1,8 +0,0 @@
env_files:
- ".env"
llama_cloud: true
workflows:
upload: "{{project_name_snake}}.qa_workflows:DocumentUploadWorkflow"
chat: "{{project_name_snake}}.qa_workflows:ChatWorkflow"
ui:
directory: ui
-25
View File
@@ -1,25 +0,0 @@
[project]
name = "document-qa"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Terry Zhao", email = "terry@runllama.ai" }
]
requires-python = ">=3.12"
dependencies = [
"llama-index-workflows>=2.2.0",
"python-cowsay>=1.2.1",
"llama-cloud-services>=0.6.0",
"llama-index-core>=0.12.0",
"llama-index-llms-openai>=0.3.0",
"llama-index-embeddings-openai>=0.3.0",
"python-dotenv>=1.0.1",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = []
+34 -12
View File
@@ -1,20 +1,17 @@
[project]
name = "{{project_name_snake}}"
name = "{{ project_name_snake }}"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Terry Zhao", email = "terry@runllama.ai" }
]
authors = []
requires-python = ">=3.12"
dependencies = [
"llama-index-workflows>=2.2.0",
"python-cowsay>=1.2.1",
"llama-cloud-services>=0.6.0",
"llama-index-core>=0.12.0",
"llama-index-llms-openai>=0.3.0",
"llama-index-embeddings-openai>=0.3.0",
"python-dotenv>=1.0.1",
"llama-index-workflows>=2.2.0,<3.0.0",
"llama-cloud-services>=0.6.68",
"llama-index-core>=0.14.0",
"llama-index-llms-openai>=0.5.6",
"llama-index-embeddings-openai>=0.5.1",
"python-dotenv>=1.1.1",
]
[build-system]
@@ -22,5 +19,30 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = []
dev = [
"hatch>=1.14.1",
"pytest>=8.4.2",
"ruff>=0.13.0",
"ty>=0.0.1a20",
]
[tool.hatch.envs.default.scripts]
"format" = "ruff format ."
"format-check" = "ruff format --check ."
"lint" = "ruff check --fix ."
"lint-check" = ["ruff check ."]
typecheck = "ty check src"
test = "pytest"
"all-check" = ["format-check", "lint-check", "test"]
"all-fix" = ["format", "lint", "test"]
[tool.llamadeploy]
env-files = [".env"]
llama_cloud = true
[tool.llamadeploy.ui]
directory = "./ui"
[tool.llamadeploy.workflows]
upload = "test_proj.qa_workflows:upload"
chat = "test_proj.qa_workflows:chat"
+52
View File
@@ -0,0 +1,52 @@
import functools
import os
import httpx
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud_services import LlamaParse
# deployed agents may infer their name from the deployment name
# Note: Make sure that an agent deployment with this name actually exists
# otherwise calls to get or set data will fail. You may need to adjust the `or `
# name for development
DEPLOYMENT_NAME = os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
# required for all llama cloud calls
LLAMA_CLOUD_API_KEY = os.environ["LLAMA_CLOUD_API_KEY"]
# get this in case running against a different environment than production
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
LLAMA_CLOUD_PROJECT_ID = os.getenv("LLAMA_DEPLOY_PROJECT_ID")
INDEX_NAME = "document_qa_index"
def get_custom_client() -> httpx.AsyncClient:
return httpx.AsyncClient(
timeout=60,
headers={"Project-Id": LLAMA_CLOUD_PROJECT_ID}
if LLAMA_CLOUD_PROJECT_ID
else None,
)
@functools.cache
def get_llama_cloud_client() -> AsyncLlamaCloud:
return AsyncLlamaCloud(
base_url=LLAMA_CLOUD_BASE_URL,
token=LLAMA_CLOUD_API_KEY,
httpx_client=get_custom_client(),
)
@functools.cache
def get_llama_parse_client() -> LlamaParse:
return LlamaParse(
parse_mode="parse_page_with_agent",
model="openai-gpt-4-1-mini",
high_res_ocr=True,
adaptive_long_table=True,
outlined_table_extraction=True,
output_tables_as_HTML=True,
result_type="markdown",
api_key=LLAMA_CLOUD_API_KEY,
project_id=LLAMA_CLOUD_PROJECT_ID,
custom_client=get_custom_client(),
)
@@ -1,27 +1,35 @@
import logging
import os
import uuid
import httpx
from llama_cloud.types import RetrievalMode
import tempfile
from llama_index.core.chat_engine.types import BaseChatEngine, ChatMode
from workflows import Workflow, step, Context
from workflows.events import StartEvent, StopEvent, Event, InputRequiredEvent, HumanResponseEvent
from workflows.events import (
StartEvent,
StopEvent,
Event,
InputRequiredEvent,
HumanResponseEvent,
)
from workflows.retry_policy import ConstantDelayRetryPolicy
from workflows.server import WorkflowServer
from llama_cloud_services import LlamaParse, LlamaCloudIndex
from llama_cloud_services import LlamaCloudIndex
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core.memory import ChatMemoryBuffer
from dotenv import load_dotenv
from .clients import get_custom_client, get_llama_cloud_client
from .config import PROJECT_ID, ORGANIZATION_ID
# Load environment variables
load_dotenv()
from .clients import (
LLAMA_CLOUD_API_KEY,
LLAMA_CLOUD_BASE_URL,
get_custom_client,
get_llama_cloud_client,
get_llama_parse_client,
LLAMA_CLOUD_PROJECT_ID,
)
logger = logging.getLogger(__name__)
@@ -30,57 +38,45 @@ class FileEvent(StartEvent):
file_id: str
index_name: str
class DownloadFileEvent(Event):
file_id: str
class FileDownloadedEvent(Event):
file_id: str
file_path: str
filename: str
class ChatEvent(StartEvent):
index_name: str
session_id: str
# Configure LLM and embedding model
Settings.llm = OpenAI(model="gpt-4", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
custom_client = get_custom_client()
class DocumentUploadWorkflow(Workflow):
"""Workflow to upload and index documents using LlamaParse and LlamaCloud Index"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Get API key with validation
api_key = os.getenv("LLAMA_CLOUD_API_KEY")
if not api_key:
logger.warning("Warning: LLAMA_CLOUD_API_KEY not found in environment. Document upload will not work.")
self.parser = None
else:
# Initialize LlamaParse with recommended settings
logger.info(f"Initializing LlamaParse with API key: {api_key}")
self.parser = LlamaParse(
parse_mode="parse_page_with_agent",
model="openai-gpt-4-1-mini",
high_res_ocr=True,
adaptive_long_table=True,
outlined_table_extraction=True,
output_tables_as_HTML=True,
result_type="markdown",
api_key=api_key,
project_id=PROJECT_ID,
organization_id=ORGANIZATION_ID,
custom_client=custom_client
)
# Initialize LlamaParse with recommended settings
self.parser = get_llama_parse_client()
@step(retry_policy=ConstantDelayRetryPolicy(maximum_attempts=3, delay=10))
async def run_file(self, event: FileEvent, ctx: Context) -> DownloadFileEvent:
logger.info(f"Running file {event.file_id}")
await ctx.store.set("index_name", event.index_name)
return DownloadFileEvent(file_id=event.file_id)
@step(retry_policy=ConstantDelayRetryPolicy(maximum_attempts=3, delay=10))
async def download_file(
self, event: DownloadFileEvent, ctx: Context
@@ -114,78 +110,61 @@ class DocumentUploadWorkflow(Workflow):
logger.error(f"Error downloading file {event.file_id}: {e}", exc_info=True)
raise e
@step
async def parse_document(self, ev: FileDownloadedEvent, ctx: Context) -> StopEvent:
"""Parse document and index it to LlamaCloud"""
try:
logger.info(f"Parsing document {ev.file_id}")
# Check if parser is initialized
if not self.parser:
return StopEvent(result={
"success": False,
"error": "LLAMA_CLOUD_API_KEY not configured. Please set it in your .env file."
})
# Get file path or content from event
file_path = ev.file_path
file_name = file_path.split("/")[-1]
index_name = await ctx.store.get("index_name")
# Parse the document
if file_path:
# Parse from file path
result = await self.parser.aparse(file_path)
# Get parsed documents
documents = result.get_text_documents()
# Create or connect to LlamaCloud Index
try:
logger.info(f"Connecting to existing index {index_name}")
# Try to connect to existing index
index = LlamaCloudIndex(
name=index_name,
project_id=PROJECT_ID,
organization_id=ORGANIZATION_ID,
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
custom_client=custom_client
)
for document in documents:
index.insert(document)
except Exception:
# Create new index if doesn't exist
logger.info(f"Creating new index {index_name}")
index = LlamaCloudIndex.from_documents(
documents=documents,
name=index_name,
project_id=PROJECT_ID,
organization_id=ORGANIZATION_ID,
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
show_progress=True,
custom_client=custom_client
)
return StopEvent(result={
"success": True,
"index_name": index_name,
"index_url": f"https://cloud.llamaindex.ai/projects/{PROJECT_ID}/indexes/{index.id}",
"document_count": len(documents),
"file_name": file_name,
"message": f"Successfully indexed {len(documents)} documents to '{index_name}'"
})
index = LlamaCloudIndex.create_index(
documents=documents,
name=index_name,
project_id=LLAMA_CLOUD_PROJECT_ID,
api_key=LLAMA_CLOUD_API_KEY,
base_url=LLAMA_CLOUD_BASE_URL,
show_progress=True,
custom_client=custom_client,
)
# Insert documents to index
logger.info(f"Inserting {len(documents)} documents to {index_name}")
for document in documents:
index.insert(document)
return StopEvent(
result={
"success": True,
"index_name": index_name,
"document_count": len(documents),
"index_url": f"https://cloud.llamaindex.ai/projects/{LLAMA_CLOUD_PROJECT_ID}/indexes/{index.id}",
"file_name": file_name,
"message": f"Successfully indexed {len(documents)} documents to '{index_name}'",
}
)
except Exception as e:
logger.error(e.stack_trace)
return StopEvent(result={
"success": False,
"error": str(e),
"stack_trace": e.stack_trace
})
return StopEvent(
result={"success": False, "error": str(e), "stack_trace": e.stack_trace}
)
class ChatResponseEvent(Event):
"""Event emitted when chat engine generates a response"""
response: str
sources: list
query: str
@@ -193,6 +172,7 @@ class ChatResponseEvent(Event):
class ChatDeltaEvent(Event):
"""Streaming delta for incremental response output"""
delta: str
@@ -201,7 +181,9 @@ class ChatWorkflow(Workflow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.chat_engines: dict[str, BaseChatEngine] = {} # Cache chat engines per index
self.chat_engines: dict[
str, BaseChatEngine
] = {} # Cache chat engines per index
@step
async def initialize_chat(self, ev: ChatEvent, ctx: Context) -> InputRequiredEvent:
@@ -225,10 +207,10 @@ class ChatWorkflow(Workflow):
# Connect to LlamaCloud Index
index = LlamaCloudIndex(
name=index_name,
project_id=PROJECT_ID,
organization_id=ORGANIZATION_ID,
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
custom_client=custom_client
project_id=LLAMA_CLOUD_PROJECT_ID,
api_key=LLAMA_CLOUD_API_KEY,
base_url=LLAMA_CLOUD_BASE_URL,
async_httpx_client=custom_client,
)
# Create chat engine with memory
@@ -252,13 +234,17 @@ class ChatWorkflow(Workflow):
)
except Exception as e:
return StopEvent(result={
"success": False,
"error": f"Failed to initialize chat: {str(e)}"
})
return StopEvent(
result={
"success": False,
"error": f"Failed to initialize chat: {str(e)}",
}
)
@step
async def process_user_response(self, ev: HumanResponseEvent, ctx: Context) -> InputRequiredEvent | HumanResponseEvent | StopEvent | None:
async def process_user_response(
self, ev: HumanResponseEvent, ctx: Context
) -> InputRequiredEvent | HumanResponseEvent | StopEvent | None:
"""Process user input and generate response"""
try:
logger.info(f"Processing user response {ev.response}")
@@ -268,13 +254,17 @@ class ChatWorkflow(Workflow):
# Check for exit command
if user_input.lower() == "exit":
logger.info(f"User input is exit")
conversation_history = await ctx.store.get("conversation_history", default=[])
return StopEvent(result={
"success": True,
"message": "Chat session ended.",
"conversation_history": conversation_history
})
logger.info("User input is exit")
conversation_history = await ctx.store.get(
"conversation_history", default=[]
)
return StopEvent(
result={
"success": True,
"message": "Chat session ended.",
"conversation_history": conversation_history,
}
)
# Get session info from context
index_name = await ctx.store.get("index_name")
@@ -295,29 +285,43 @@ class ChatWorkflow(Workflow):
# Extract source nodes for citations
sources = []
if hasattr(stream_response, 'source_nodes'):
if hasattr(stream_response, "source_nodes"):
for node in stream_response.source_nodes:
sources.append({
"text": node.text[:200] + "..." if len(node.text) > 200 else node.text,
"score": node.score if hasattr(node, 'score') else None,
"metadata": node.metadata if hasattr(node, 'metadata') else {}
})
sources.append(
{
"text": node.text[:200] + "..."
if len(node.text) > 200
else node.text,
"score": node.score if hasattr(node, "score") else None,
"metadata": node.metadata
if hasattr(node, "metadata")
else {},
}
)
# Update conversation history
conversation_history = await ctx.store.get("conversation_history", default=[])
conversation_history.append({
"query": user_input,
"response": full_text.strip() if full_text else str(stream_response),
"sources": sources
})
conversation_history = await ctx.store.get(
"conversation_history", default=[]
)
conversation_history.append(
{
"query": user_input,
"response": full_text.strip()
if full_text
else str(stream_response),
"sources": sources,
}
)
await ctx.store.set("conversation_history", conversation_history)
# After streaming completes, emit a summary response event to stream for frontend/main printing
ctx.write_event_to_stream(ChatResponseEvent(
response=full_text.strip() if full_text else str(stream_response),
sources=sources,
query=user_input,
))
ctx.write_event_to_stream(
ChatResponseEvent(
response=full_text.strip() if full_text else str(stream_response),
sources=sources,
query=user_input,
)
)
# Prompt for next input
return InputRequiredEvent(
@@ -325,14 +329,10 @@ class ChatWorkflow(Workflow):
)
except Exception as e:
return StopEvent(result={
"success": False,
"error": f"Error processing query: {str(e)}"
})
return StopEvent(
result={"success": False, "error": f"Error processing query: {str(e)}"}
)
# Create workflow server
app = WorkflowServer()
app.add_workflow("upload", DocumentUploadWorkflow(timeout=300))
app.add_workflow("chat", ChatWorkflow(timeout=None))
upload = DocumentUploadWorkflow(timeout=None)
chat = ChatWorkflow(timeout=None)
-34
View File
@@ -1,34 +0,0 @@
import functools
import os
import httpx
import dotenv
from llama_cloud.client import AsyncLlamaCloud
dotenv.load_dotenv()
# deployed agents may infer their name from the deployment name
# Note: Make sure that an agent deployment with this name actually exists
# otherwise calls to get or set data will fail. You may need to adjust the `or `
# name for development
agent_name = os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
agent_name_or_default = agent_name or "test-proj"
# required for all llama cloud calls
api_key = os.environ["LLAMA_CLOUD_API_KEY"]
# get this in case running against a different environment than production
base_url = os.getenv("LLAMA_CLOUD_BASE_URL")
project_id = os.getenv("LLAMA_DEPLOY_PROJECT_ID")
def get_custom_client():
return httpx.AsyncClient(
timeout=60, headers={"Project-Id": project_id} if project_id else None
)
@functools.lru_cache(maxsize=None)
def get_llama_cloud_client():
return AsyncLlamaCloud(
base_url=base_url,
token=api_key,
httpx_client=get_custom_client(),
)
@@ -1,2 +0,0 @@
PROJECT_ID = "{{ llama_project_id }}"
ORGANIZATION_ID = "{{ llama_org_id }}"
+6
View File
@@ -0,0 +1,6 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: '2405947'
_src_path: .
llama_org_id: asdf
llama_project_id: asdf
project_name: test-proj
+2
View File
@@ -0,0 +1,2 @@
# Copy this to .env and set any necessary secrets
OPENAI_API_KEY=sk-your-openai-api-key-here
+4
View File
@@ -0,0 +1,4 @@
.env
__pycache__
workflows.db
.venv
+17
View File
@@ -0,0 +1,17 @@
# Document Q&A Application
A document question-answering application built with LlamaIndex workflows and LlamaCloud services.
This application uses LlamaDeploy. For more information see [the docs](https://developers.llamaindex.ai/python/cloud/llamadeploy/getting-started)
# Getting Started
1. install `uv` if you haven't `brew install uv`
2. run `uvx llamactl serve`
3. Visit http://localhost:4501/docs and see workflow APIs
# Organization
- `src` contains python workflow sources. The name of the deployment here is defined as `document-qa`requests. See http://localhost:4501/docs for openAPI docs
+48
View File
@@ -0,0 +1,48 @@
[project]
name = "test_proj"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = []
requires-python = ">=3.12"
dependencies = [
"llama-index-workflows>=2.2.0,<3.0.0",
"llama-cloud-services>=0.6.68",
"llama-index-core>=0.14.0",
"llama-index-llms-openai>=0.5.6",
"llama-index-embeddings-openai>=0.5.1",
"python-dotenv>=1.1.1",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"hatch>=1.14.1",
"pytest>=8.4.2",
"ruff>=0.13.0",
"ty>=0.0.1a20",
]
[tool.hatch.envs.default.scripts]
"format" = "ruff format ."
"format-check" = "ruff format --check ."
"lint" = "ruff check --fix ."
"lint-check" = ["ruff check ."]
typecheck = "ty check src"
test = "pytest"
"all-check" = ["format-check", "lint-check", "test"]
"all-fix" = ["format", "lint", "test"]
[tool.llamadeploy]
env-files = [".env"]
llama_cloud = true
[tool.llamadeploy.ui]
directory = "./ui"
[tool.llamadeploy.workflows]
upload = "test_proj.qa_workflows:upload"
chat = "test_proj.qa_workflows:chat"
View File
+52
View File
@@ -0,0 +1,52 @@
import functools
import os
import httpx
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud_services import LlamaParse
# deployed agents may infer their name from the deployment name
# Note: Make sure that an agent deployment with this name actually exists
# otherwise calls to get or set data will fail. You may need to adjust the `or `
# name for development
DEPLOYMENT_NAME = os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
# required for all llama cloud calls
LLAMA_CLOUD_API_KEY = os.environ["LLAMA_CLOUD_API_KEY"]
# get this in case running against a different environment than production
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
LLAMA_CLOUD_PROJECT_ID = os.getenv("LLAMA_DEPLOY_PROJECT_ID")
INDEX_NAME = "document_qa_index"
def get_custom_client() -> httpx.AsyncClient:
return httpx.AsyncClient(
timeout=60,
headers={"Project-Id": LLAMA_CLOUD_PROJECT_ID}
if LLAMA_CLOUD_PROJECT_ID
else None,
)
@functools.cache
def get_llama_cloud_client() -> AsyncLlamaCloud:
return AsyncLlamaCloud(
base_url=LLAMA_CLOUD_BASE_URL,
token=LLAMA_CLOUD_API_KEY,
httpx_client=get_custom_client(),
)
@functools.cache
def get_llama_parse_client() -> LlamaParse:
return LlamaParse(
parse_mode="parse_page_with_agent",
model="openai-gpt-4-1-mini",
high_res_ocr=True,
adaptive_long_table=True,
outlined_table_extraction=True,
output_tables_as_HTML=True,
result_type="markdown",
api_key=LLAMA_CLOUD_API_KEY,
project_id=LLAMA_CLOUD_PROJECT_ID,
custom_client=get_custom_client(),
)
+338
View File
@@ -0,0 +1,338 @@
import logging
import os
import httpx
from llama_cloud.types import RetrievalMode
import tempfile
from llama_index.core.chat_engine.types import BaseChatEngine, ChatMode
from workflows import Workflow, step, Context
from workflows.events import (
StartEvent,
StopEvent,
Event,
InputRequiredEvent,
HumanResponseEvent,
)
from workflows.retry_policy import ConstantDelayRetryPolicy
from llama_cloud_services import LlamaCloudIndex
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core.memory import ChatMemoryBuffer
from .clients import (
LLAMA_CLOUD_API_KEY,
LLAMA_CLOUD_BASE_URL,
get_custom_client,
get_llama_cloud_client,
get_llama_parse_client,
LLAMA_CLOUD_PROJECT_ID,
)
logger = logging.getLogger(__name__)
class FileEvent(StartEvent):
file_id: str
index_name: str
class DownloadFileEvent(Event):
file_id: str
class FileDownloadedEvent(Event):
file_id: str
file_path: str
filename: str
class ChatEvent(StartEvent):
index_name: str
session_id: str
# Configure LLM and embedding model
Settings.llm = OpenAI(model="gpt-4", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
custom_client = get_custom_client()
class DocumentUploadWorkflow(Workflow):
"""Workflow to upload and index documents using LlamaParse and LlamaCloud Index"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Get API key with validation
# Initialize LlamaParse with recommended settings
self.parser = get_llama_parse_client()
@step(retry_policy=ConstantDelayRetryPolicy(maximum_attempts=3, delay=10))
async def run_file(self, event: FileEvent, ctx: Context) -> DownloadFileEvent:
logger.info(f"Running file {event.file_id}")
await ctx.store.set("index_name", event.index_name)
return DownloadFileEvent(file_id=event.file_id)
@step(retry_policy=ConstantDelayRetryPolicy(maximum_attempts=3, delay=10))
async def download_file(
self, event: DownloadFileEvent, ctx: Context
) -> FileDownloadedEvent:
"""Download the file reference from the cloud storage"""
logger.info(f"Downloading file {event.file_id}")
try:
file_metadata = await get_llama_cloud_client().files.get_file(
id=event.file_id
)
file_url = await get_llama_cloud_client().files.read_file_content(
event.file_id
)
temp_dir = tempfile.gettempdir()
filename = file_metadata.name
file_path = os.path.join(temp_dir, filename)
client = httpx.AsyncClient()
# Report progress to the UI
logger.info(f"Downloading file {file_url.url} to {file_path}")
async with client.stream("GET", file_url.url) as response:
with open(file_path, "wb") as f:
async for chunk in response.aiter_bytes():
f.write(chunk)
logger.info(f"Downloaded file {file_url.url} to {file_path}")
return FileDownloadedEvent(
file_id=event.file_id, file_path=file_path, filename=filename
)
except Exception as e:
logger.error(f"Error downloading file {event.file_id}: {e}", exc_info=True)
raise e
@step
async def parse_document(self, ev: FileDownloadedEvent, ctx: Context) -> StopEvent:
"""Parse document and index it to LlamaCloud"""
try:
logger.info(f"Parsing document {ev.file_id}")
# Get file path or content from event
file_path = ev.file_path
file_name = file_path.split("/")[-1]
index_name = await ctx.store.get("index_name")
# Parse the document
if file_path:
# Parse from file path
result = await self.parser.aparse(file_path)
# Get parsed documents
documents = result.get_text_documents()
# Create or connect to LlamaCloud Index
index = LlamaCloudIndex.create_index(
documents=documents,
name=index_name,
project_id=LLAMA_CLOUD_PROJECT_ID,
api_key=LLAMA_CLOUD_API_KEY,
base_url=LLAMA_CLOUD_BASE_URL,
show_progress=True,
custom_client=custom_client,
)
# Insert documents to index
logger.info(f"Inserting {len(documents)} documents to {index_name}")
for document in documents:
index.insert(document)
return StopEvent(
result={
"success": True,
"index_name": index_name,
"document_count": len(documents),
"index_url": f"https://cloud.llamaindex.ai/projects/{LLAMA_CLOUD_PROJECT_ID}/indexes/{index.id}",
"file_name": file_name,
"message": f"Successfully indexed {len(documents)} documents to '{index_name}'",
}
)
except Exception as e:
logger.error(e.stack_trace)
return StopEvent(
result={"success": False, "error": str(e), "stack_trace": e.stack_trace}
)
class ChatResponseEvent(Event):
"""Event emitted when chat engine generates a response"""
response: str
sources: list
query: str
class ChatDeltaEvent(Event):
"""Streaming delta for incremental response output"""
delta: str
class ChatWorkflow(Workflow):
"""Workflow to handle continuous chat queries against indexed documents"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.chat_engines: dict[
str, BaseChatEngine
] = {} # Cache chat engines per index
@step
async def initialize_chat(self, ev: ChatEvent, ctx: Context) -> InputRequiredEvent:
"""Initialize the chat session and request first input"""
try:
logger.info(f"Initializing chat {ev.index_name}")
index_name = ev.index_name
session_id = ev.session_id
# Store session info in context
await ctx.store.set("index_name", index_name)
await ctx.store.set("session_id", session_id)
await ctx.store.set("conversation_history", [])
# Create cache key for chat engine
cache_key = f"{index_name}_{session_id}"
# Initialize chat engine if not exists
if cache_key not in self.chat_engines:
logger.info(f"Initializing chat engine {cache_key}")
# Connect to LlamaCloud Index
index = LlamaCloudIndex(
name=index_name,
project_id=LLAMA_CLOUD_PROJECT_ID,
api_key=LLAMA_CLOUD_API_KEY,
base_url=LLAMA_CLOUD_BASE_URL,
async_httpx_client=custom_client,
)
# Create chat engine with memory
memory = ChatMemoryBuffer.from_defaults(token_limit=3900)
self.chat_engines[cache_key] = index.as_chat_engine(
chat_mode=ChatMode.CONTEXT,
memory=memory,
llm=Settings.llm,
context_prompt=(
"You are a helpful assistant that answers questions based on the provided documents. "
"Always cite specific information from the documents when answering. "
"If you cannot find the answer in the documents, say so clearly."
),
verbose=False,
retriever_mode=RetrievalMode.CHUNKS,
)
# Request first user input
return InputRequiredEvent(
prefix="Chat initialized. Ask a question (or type 'exit' to quit): "
)
except Exception as e:
return StopEvent(
result={
"success": False,
"error": f"Failed to initialize chat: {str(e)}",
}
)
@step
async def process_user_response(
self, ev: HumanResponseEvent, ctx: Context
) -> InputRequiredEvent | HumanResponseEvent | StopEvent | None:
"""Process user input and generate response"""
try:
logger.info(f"Processing user response {ev.response}")
user_input = ev.response.strip()
logger.info(f"User input: {user_input}")
# Check for exit command
if user_input.lower() == "exit":
logger.info("User input is exit")
conversation_history = await ctx.store.get(
"conversation_history", default=[]
)
return StopEvent(
result={
"success": True,
"message": "Chat session ended.",
"conversation_history": conversation_history,
}
)
# Get session info from context
index_name = await ctx.store.get("index_name")
session_id = await ctx.store.get("session_id")
cache_key = f"{index_name}_{session_id}"
# Get chat engine
chat_engine = self.chat_engines[cache_key]
# Process query with chat engine (streaming)
stream_response = await chat_engine.astream_chat(user_input)
full_text = ""
# Emit streaming deltas to the event stream
async for token in stream_response.async_response_gen():
full_text += token
ctx.write_event_to_stream(ChatDeltaEvent(delta=token))
# Extract source nodes for citations
sources = []
if hasattr(stream_response, "source_nodes"):
for node in stream_response.source_nodes:
sources.append(
{
"text": node.text[:200] + "..."
if len(node.text) > 200
else node.text,
"score": node.score if hasattr(node, "score") else None,
"metadata": node.metadata
if hasattr(node, "metadata")
else {},
}
)
# Update conversation history
conversation_history = await ctx.store.get(
"conversation_history", default=[]
)
conversation_history.append(
{
"query": user_input,
"response": full_text.strip()
if full_text
else str(stream_response),
"sources": sources,
}
)
await ctx.store.set("conversation_history", conversation_history)
# After streaming completes, emit a summary response event to stream for frontend/main printing
ctx.write_event_to_stream(
ChatResponseEvent(
response=full_text.strip() if full_text else str(stream_response),
sources=sources,
query=user_input,
)
)
# Prompt for next input
return InputRequiredEvent(
prefix="\nAsk another question (or type 'exit' to quit): "
)
except Exception as e:
return StopEvent(
result={"success": False, "error": f"Error processing query: {str(e)}"}
)
upload = DocumentUploadWorkflow(timeout=None)
chat = ChatWorkflow(timeout=None)
+2
View File
@@ -0,0 +1,2 @@
def test_placeholder():
pass
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
# uses pnpm
pnpm-lock.yaml
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quick Start UI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
{
"name": "test-proj-ui",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "tsc --noEmit",
"format": "prettier --write src",
"format-check": "prettier --check src",
"all-check": "pnpm i && pnpm run lint && pnpm run format-check && pnpm run build",
"all-fix": "pnpm i && pnpm run lint && pnpm run format && pnpm run build"
},
"dependencies": {
"@llamaindex/ui": "^1.0.2",
"@llamaindex/workflows-client": "^1.2.0",
"@radix-ui/themes": "^3.2.1",
"llama-cloud-services": "^0.3.6",
"lucide-react": "^0.544.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tw-animate-css": "^1.3.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.3.4",
"dotenv": "^17.2.2",
"eslint": "^9",
"prettier": "^3.6.2",
"tailwindcss": "^4",
"typescript": "^5",
"vite": "^5.4.8"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}
+5
View File
@@ -0,0 +1,5 @@
import tailwind from "@tailwindcss/postcss";
export default {
plugins: [tailwind()],
};
+14
View File
@@ -0,0 +1,14 @@
import { ApiProvider } from "@llamaindex/ui";
import Home from "./pages/Home";
import { Theme } from "@radix-ui/themes";
import { clients } from "@/libs/clients";
export default function App() {
return (
<Theme>
<ApiProvider clients={clients}>
<Home />
</ApiProvider>
</Theme>
);
}
+492
View File
@@ -0,0 +1,492 @@
// This is a temporary chatbot component that is used to test the chatbot functionality.
// LlamaIndex will replace it with better chatbot component.
import { useState, useRef, useEffect, FormEvent, KeyboardEvent } from "react";
import {
Send,
Loader2,
Bot,
User,
MessageSquare,
Trash2,
RefreshCw,
} from "lucide-react";
import {
Button,
Input,
ScrollArea,
Card,
CardContent,
cn,
useWorkflowTaskCreate,
useWorkflowTask,
} from "@llamaindex/ui";
import { AGENT_NAME } from "../libs/config";
import { toHumanResponseRawEvent } from "@/libs/utils";
type Role = "user" | "assistant";
interface Message {
id: string;
role: Role;
content: string;
timestamp: Date;
error?: boolean;
}
export default function ChatBot() {
const { createTask } = useWorkflowTaskCreate();
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [handlerId, setHandlerId] = useState<string | null>(null);
const lastProcessedEventIndexRef = useRef<number>(0);
const [canSend, setCanSend] = useState<boolean>(false);
const streamingMessageIndexRef = useRef<number | null>(null);
// Deployment + auth setup
const deployment = AGENT_NAME || "document-qa";
const platformToken = (import.meta as any).env?.VITE_LLAMA_CLOUD_API_KEY as
| string
| undefined;
const projectId = (import.meta as any).env?.VITE_LLAMA_DEPLOY_PROJECT_ID as
| string
| undefined;
const defaultIndexName =
(import.meta as any).env?.VITE_DEFAULT_INDEX_NAME || "document_qa_index";
const sessionIdRef = useRef<string>(
`chat-${Math.random().toString(36).slice(2)}-${Date.now()}`,
);
// UI text defaults
const title = "AI Document Assistant";
const placeholder = "Ask me anything about your documents...";
const welcomeMessage =
"Welcome! 👋 Upload a document with the control above, then ask questions here.";
// Helper functions for message management
const appendMessage = (role: Role, msg: string): void => {
setMessages((prev) => {
const id = `${role}-stream-${Date.now()}`;
const idx = prev.length;
streamingMessageIndexRef.current = idx;
return [
...prev,
{
id,
role,
content: msg,
timestamp: new Date(),
},
];
});
};
const updateMessage = (index: number, message: string) => {
setMessages((prev) => {
if (index < 0 || index >= prev.length) return prev;
const copy = [...prev];
const existing = copy[index];
copy[index] = { ...existing, content: message };
return copy;
});
};
// Initialize with welcome message
useEffect(() => {
if (messages.length === 0) {
const welcomeMsg: Message = {
id: "welcome",
role: "assistant",
content: welcomeMessage,
timestamp: new Date(),
};
setMessages([welcomeMsg]);
}
}, []);
// Create chat task on init
useEffect(() => {
(async () => {
if (!handlerId) {
const handler = await createTask("chat", {
index_name: defaultIndexName,
session_id: sessionIdRef.current,
});
setHandlerId(handler.handler_id);
}
})();
}, []);
// Subscribe to task/events using hook (auto stream when handler exists)
const { events } = useWorkflowTask(handlerId ?? "", Boolean(handlerId));
// Process streamed events into messages
useEffect(() => {
if (!events || events.length === 0) return;
let startIdx = lastProcessedEventIndexRef.current;
if (startIdx < 0) startIdx = 0;
if (startIdx >= events.length) return;
for (let i = startIdx; i < events.length; i++) {
const ev: any = events[i];
const type = ev?.type as string | undefined;
const rawData = ev?.data as any;
if (!type) continue;
const data = (rawData && (rawData._data ?? rawData)) as any;
if (type.includes("ChatDeltaEvent")) {
const delta: string = data?.delta ?? "";
if (!delta) continue;
if (streamingMessageIndexRef.current === null) {
appendMessage("assistant", delta);
} else {
const idx = streamingMessageIndexRef.current;
const current = messages[idx!]?.content ?? "";
if (current === "Thinking...") {
updateMessage(idx!, delta);
} else {
updateMessage(idx!, current + delta);
}
}
} else if (type.includes("ChatResponseEvent")) {
// finalize current stream
streamingMessageIndexRef.current = null;
} else if (type.includes("InputRequiredEvent")) {
// ready for next user input; enable send
setCanSend(true);
setIsLoading(false);
inputRef.current?.focus();
} else if (type.includes("StopEvent")) {
// finished; no summary bubble needed (chat response already streamed)
}
}
lastProcessedEventIndexRef.current = events.length;
}, [events, messages]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// No manual SSE cleanup needed
const getCommonHeaders = () => ({
...(platformToken ? { authorization: `Bearer ${platformToken}` } : {}),
...(projectId ? { "Project-Id": projectId } : {}),
});
const startChatIfNeeded = async (): Promise<string> => {
if (handlerId) return handlerId;
const handler = await createTask("chat", {
index_name: defaultIndexName,
session_id: sessionIdRef.current,
});
setHandlerId(handler.handler_id);
return handler.handler_id;
};
// Removed manual SSE ensureEventStream; hook handles streaming
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const trimmedInput = input.trim();
if (!trimmedInput || isLoading || !canSend) return;
// Add user message
const userMessage: Message = {
id: `user-${Date.now()}`,
role: "user",
content: trimmedInput,
timestamp: new Date(),
};
const newMessages = [...messages, userMessage];
setMessages(newMessages);
setInput("");
setIsLoading(true);
setCanSend(false);
// Immediately create an assistant placeholder to avoid visual gap before deltas
if (streamingMessageIndexRef.current === null) {
appendMessage("assistant", "Thinking...");
}
try {
// Ensure chat handler exists (created on init)
const hid = await startChatIfNeeded();
// Send user input as HumanResponseEvent
const postRes = await fetch(`/deployments/${deployment}/events/${hid}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...getCommonHeaders(),
},
body: JSON.stringify({
event: JSON.stringify(toHumanResponseRawEvent(trimmedInput)),
}),
});
if (!postRes.ok) {
throw new Error(
`Failed to send message: ${postRes.status} ${postRes.statusText}`,
);
}
// The assistant reply will be streamed by useWorkflowTask and appended incrementally
} catch (err) {
console.error("Chat error:", err);
// Add error message
const errorMessage: Message = {
id: `error-${Date.now()}`,
role: "assistant",
content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : "Unknown error"}. Please try again.`,
timestamp: new Date(),
error: true,
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
// Focus back on input
inputRef.current?.focus();
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
// Submit on Enter (without Shift)
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any);
}
};
const clearChat = () => {
setMessages([
{
id: "welcome",
role: "assistant" as const,
content: welcomeMessage,
timestamp: new Date(),
},
]);
setInput("");
inputRef.current?.focus();
};
const retryLastMessage = () => {
const lastUserMessage = messages.filter((m) => m.role === "user").pop();
if (lastUserMessage) {
// Remove the last assistant message if it was an error
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === "assistant" && lastMessage.error) {
setMessages((prev) => prev.slice(0, -1));
}
setInput(lastUserMessage.content);
inputRef.current?.focus();
}
};
return (
<div
className={cn(
"flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-lg",
)}
>
{/* Header */}
<div className="px-4 py-3 border-b dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<h3 className="font-semibold text-gray-900 dark:text-white">
{title}
</h3>
{isLoading && (
<span className="text-xs text-gray-500 dark:text-gray-400">
Thinking...
</span>
)}
</div>
<div className="flex items-center gap-2">
{messages.some((m) => m.error) && (
<button
onClick={retryLastMessage}
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Retry last message"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
{messages.length > 0 && (
<button
onClick={clearChat}
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Clear chat"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-4 overflow-y-auto">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full min-h-[300px]">
<div className="text-center">
<Bot className="w-12 h-12 text-gray-400 dark:text-gray-600 mx-auto mb-3" />
<p className="text-gray-500 dark:text-gray-400 mb-2">
No messages yet
</p>
<p className="text-sm text-gray-400 dark:text-gray-500">
Start a conversation!
</p>
</div>
</div>
) : (
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={cn(
"flex gap-3",
message.role === "user" ? "justify-end" : "justify-start",
)}
>
{message.role !== "user" && (
<div
className={cn(
"w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0",
message.error
? "bg-red-100 dark:bg-red-900"
: "bg-blue-100 dark:bg-blue-900",
)}
>
<Bot
className={cn(
"w-5 h-5",
message.error
? "text-red-600 dark:text-red-400"
: "text-blue-600 dark:text-blue-400",
)}
/>
</div>
)}
<div
className={cn(
"max-w-[70%]",
message.role === "user" ? "order-1" : "order-2",
)}
>
<Card
className={cn(
"py-0",
message.role === "user"
? "bg-blue-600 text-white border-blue-600"
: message.error
? "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
: "bg-gray-50 dark:bg-gray-700",
)}
>
<CardContent className="p-3">
<p
className={cn(
"whitespace-pre-wrap text-sm",
message.error && "text-red-700 dark:text-red-400",
)}
>
{message.content}
</p>
<p
className={cn(
"text-xs mt-1 opacity-70",
message.role === "user"
? "text-blue-100"
: message.error
? "text-red-500 dark:text-red-400"
: "text-gray-500 dark:text-gray-400",
)}
>
{message.timestamp.toLocaleTimeString()}
</p>
</CardContent>
</Card>
</div>
{message.role === "user" && (
<div className="w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center flex-shrink-0 order-2">
<User className="w-5 h-5 text-gray-600 dark:text-gray-400" />
</div>
)}
</div>
))}
{isLoading && (
<div className="flex gap-3 justify-start">
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center">
<Bot className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<Card className="bg-gray-50 dark:bg-gray-700 py-0">
<CardContent className="p-3">
<div className="flex items-center gap-2">
<div className="flex gap-1">
<span
className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"
style={{ animationDelay: "0ms" }}
></span>
<span
className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"
style={{ animationDelay: "150ms" }}
></span>
<span
className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"
style={{ animationDelay: "300ms" }}
></span>
</div>
</div>
</CardContent>
</Card>
</div>
)}
<div ref={messagesEndRef} />
</div>
)}
</ScrollArea>
{/* Input */}
<div className="border-t dark:border-gray-700 p-4">
<form onSubmit={handleSubmit} className="flex gap-2">
<Input
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isLoading}
className="flex-1"
autoFocus
/>
<Button
type="submit"
disabled={!canSend || isLoading || !input.trim()}
size="icon"
title="Send message"
>
{!canSend || isLoading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
</Button>
</form>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-2">
Press Enter to send Shift+Enter for new line
</p>
</div>
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+39
View File
@@ -0,0 +1,39 @@
import {
ApiClients,
cloudApiClient,
createWorkflowClient,
createWorkflowConfig,
} from "@llamaindex/ui";
import { AGENT_NAME } from "./config";
const platformToken = import.meta.env.VITE_LLAMA_CLOUD_API_KEY;
const apiBaseUrl = import.meta.env.VITE_LLAMA_CLOUD_BASE_URL;
const projectId = import.meta.env.VITE_LLAMA_DEPLOY_PROJECT_ID;
// Configure the platform client
cloudApiClient.setConfig({
...(apiBaseUrl && { baseUrl: apiBaseUrl }),
headers: {
// optionally use a backend API token scoped to a project. For local development,
...(platformToken && { authorization: `Bearer ${platformToken}` }),
// This header is required for requests to correctly scope to the agent's project
// when authenticating with a user cookie
...(projectId && { "Project-Id": projectId }),
},
});
const workflowsClient = createWorkflowClient(
createWorkflowConfig({
baseUrl: `/deployments/${AGENT_NAME}/`,
headers: {
...(platformToken && { authorization: `Bearer ${platformToken}` }),
},
}),
);
const clients: ApiClients = {
workflowsClient: workflowsClient,
cloudApiClient: cloudApiClient,
};
export { clients };
@@ -1,2 +1,2 @@
export const APP_TITLE = "Test Project";
export const AGENT_NAME = import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME;
export const APP_TITLE = "Test Proj";
export const AGENT_NAME = import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME;
+7
View File
@@ -0,0 +1,7 @@
export function toHumanResponseRawEvent(str: string) {
return {
__is_pydantic: true,
value: { _data: { response: str } },
qualified_name: "workflows.events.HumanResponseEvent",
};
}
+11
View File
@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "@llamaindex/ui/styles.css";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+48
View File
@@ -0,0 +1,48 @@
import ChatBot from "../components/ChatBot";
import { WorkflowTrigger } from "@llamaindex/ui";
import { APP_TITLE } from "../libs/config";
export default function Home() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<header className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
{APP_TITLE}
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Upload documents and ask questions about them
</p>
</header>
<div>
<div className="flex mb-4">
<WorkflowTrigger
workflowName="upload"
inputFields={[
{
key: "index_name",
label: "Index Name",
placeholder: "e.g. document_qa_index",
required: true,
},
]}
customWorkflowInput={(files, fieldValues) => {
return {
file_id: files[0].fileId,
index_name: fieldValues.index_name,
};
}}
/>
</div>
<div>
<div className="h-[700px]">
<ChatBot />
</div>
</div>
</div>
</div>
</div>
);
}
+15
View File
@@ -0,0 +1,15 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_LLAMA_CLOUD_API_KEY?: string;
readonly VITE_LLAMA_CLOUD_BASE_URL?: string;
// injected from llama_deploy
readonly VITE_LLAMA_DEPLOY_BASE_PATH: string;
readonly VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME: string;
readonly VITE_LLAMA_DEPLOY_PROJECT_ID: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
+49
View File
@@ -0,0 +1,49 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import dotenv from "dotenv";
dotenv.config({ path: '../.env' });
// https://vitejs.dev/config/
export default defineConfig(({}) => {
const deploymentId = process.env.LLAMA_DEPLOY_DEPLOYMENT_URL_ID;
const basePath = process.env.LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH;
const projectId = process.env.LLAMA_DEPLOY_PROJECT_ID;
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
const baseUrl = process.env.LLAMA_CLOUD_BASE_URL;
const apiKey = process.env.LLAMA_CLOUD_API_KEY;
return {
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
port: port,
host: true,
},
build: {
outDir: "dist",
sourcemap: true,
},
base: basePath,
define: {
"import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME":
JSON.stringify(deploymentId),
"import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH": JSON.stringify(basePath),
...(projectId && {
"import.meta.env.VITE_LLAMA_DEPLOY_PROJECT_ID":
JSON.stringify(projectId),
}),
...(baseUrl && {
"import.meta.env.VITE_LLAMA_CLOUD_BASE_URL": JSON.stringify(baseUrl),
}),
...(apiKey && {
"import.meta.env.VITE_LLAMA_CLOUD_API_KEY": JSON.stringify(apiKey),
}),
},
};
});
+9 -3
View File
@@ -5,14 +5,19 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "tsc --noEmit",
"format": "prettier --write src",
"format-check": "prettier --check src",
"all-check": "pnpm i && pnpm run lint && pnpm run format-check && pnpm run build",
"all-fix": "pnpm i && pnpm run lint && pnpm run format && pnpm run build"
},
"dependencies": {
"@llamaindex/ui": "^1.0.2",
"@llamaindex/workflows-client": "^1.2.0",
"llama-cloud-services": "^0.3.6",
"@radix-ui/themes": "^3.2.1",
"llama-cloud-services": "^0.3.6",
"lucide-react": "^0.544.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -27,6 +32,7 @@
"@vitejs/plugin-react": "^4.3.4",
"dotenv": "^17.2.2",
"eslint": "^9",
"prettier": "^3.6.2",
"tailwindcss": "^4",
"typescript": "^5",
"vite": "^5.4.8"
+2 -2
View File
@@ -10,5 +10,5 @@ export default function App() {
<Home />
</ApiProvider>
</Theme>
)
}
);
}
+121 -65
View File
@@ -1,8 +1,25 @@
// This is a temporary chatbot component that is used to test the chatbot functionality.
// LlamaIndex will replace it with better chatbot component.
import { useState, useRef, useEffect, FormEvent, KeyboardEvent } from "react";
import { Send, Loader2, Bot, User, MessageSquare, Trash2, RefreshCw } from "lucide-react";
import { Button, Input, ScrollArea, Card, CardContent, cn, useWorkflowTaskCreate, useWorkflowTask } from "@llamaindex/ui";
import {
Send,
Loader2,
Bot,
User,
MessageSquare,
Trash2,
RefreshCw,
} from "lucide-react";
import {
Button,
Input,
ScrollArea,
Card,
CardContent,
cn,
useWorkflowTaskCreate,
useWorkflowTask,
} from "@llamaindex/ui";
import { AGENT_NAME } from "../libs/config";
import { toHumanResponseRawEvent } from "@/libs/utils";
@@ -28,19 +45,27 @@ export default function ChatBot() {
// Deployment + auth setup
const deployment = AGENT_NAME || "document-qa";
const platformToken = (import.meta as any).env?.VITE_LLAMA_CLOUD_API_KEY as string | undefined;
const projectId = (import.meta as any).env?.VITE_LLAMA_DEPLOY_PROJECT_ID as string | undefined;
const defaultIndexName = (import.meta as any).env?.VITE_DEFAULT_INDEX_NAME || "document_qa_index";
const sessionIdRef = useRef<string>(`chat-${Math.random().toString(36).slice(2)}-${Date.now()}`);
const platformToken = (import.meta as any).env?.VITE_LLAMA_CLOUD_API_KEY as
| string
| undefined;
const projectId = (import.meta as any).env?.VITE_LLAMA_DEPLOY_PROJECT_ID as
| string
| undefined;
const defaultIndexName =
(import.meta as any).env?.VITE_DEFAULT_INDEX_NAME || "document_qa_index";
const sessionIdRef = useRef<string>(
`chat-${Math.random().toString(36).slice(2)}-${Date.now()}`,
);
// UI text defaults
const title = "AI Document Assistant";
const placeholder = "Ask me anything about your documents...";
const welcomeMessage = "Welcome! 👋 Upload a document with the control above, then ask questions here.";
const welcomeMessage =
"Welcome! 👋 Upload a document with the control above, then ask questions here.";
// Helper functions for message management
const appendMessage = (role: Role, msg: string): void => {
setMessages(prev => {
setMessages((prev) => {
const id = `${role}-stream-${Date.now()}`;
const idx = prev.length;
streamingMessageIndexRef.current = idx;
@@ -57,7 +82,7 @@ export default function ChatBot() {
};
const updateMessage = (index: number, message: string) => {
setMessages(prev => {
setMessages((prev) => {
if (index < 0 || index >= prev.length) return prev;
const copy = [...prev];
const existing = copy[index];
@@ -73,7 +98,7 @@ export default function ChatBot() {
id: "welcome",
role: "assistant",
content: welcomeMessage,
timestamp: new Date()
timestamp: new Date(),
};
setMessages([welcomeMsg]);
}
@@ -176,7 +201,7 @@ export default function ChatBot() {
id: `user-${Date.now()}`,
role: "user",
content: trimmedInput,
timestamp: new Date()
timestamp: new Date(),
};
const newMessages = [...messages, userMessage];
@@ -202,11 +227,13 @@ export default function ChatBot() {
...getCommonHeaders(),
},
body: JSON.stringify({
event: JSON.stringify(toHumanResponseRawEvent(trimmedInput))
event: JSON.stringify(toHumanResponseRawEvent(trimmedInput)),
}),
});
if (!postRes.ok) {
throw new Error(`Failed to send message: ${postRes.status} ${postRes.statusText}`);
throw new Error(
`Failed to send message: ${postRes.status} ${postRes.statusText}`,
);
}
// The assistant reply will be streamed by useWorkflowTask and appended incrementally
@@ -219,10 +246,10 @@ export default function ChatBot() {
role: "assistant",
content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : "Unknown error"}. Please try again.`,
timestamp: new Date(),
error: true
error: true,
};
setMessages(prev => [...prev, errorMessage]);
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
// Focus back on input
@@ -232,7 +259,7 @@ export default function ChatBot() {
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
// Submit on Enter (without Shift)
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any);
}
@@ -244,20 +271,20 @@ export default function ChatBot() {
id: "welcome",
role: "assistant" as const,
content: welcomeMessage,
timestamp: new Date()
}
timestamp: new Date(),
},
]);
setInput("");
inputRef.current?.focus();
};
const retryLastMessage = () => {
const lastUserMessage = messages.filter(m => m.role === "user").pop();
const lastUserMessage = messages.filter((m) => m.role === "user").pop();
if (lastUserMessage) {
// Remove the last assistant message if it was an error
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === "assistant" && lastMessage.error) {
setMessages(prev => prev.slice(0, -1));
setMessages((prev) => prev.slice(0, -1));
}
setInput(lastUserMessage.content);
inputRef.current?.focus();
@@ -265,19 +292,27 @@ export default function ChatBot() {
};
return (
<div className={cn("flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-lg")}>
<div
className={cn(
"flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-lg",
)}
>
{/* Header */}
<div className="px-4 py-3 border-b dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<h3 className="font-semibold text-gray-900 dark:text-white">{title}</h3>
<h3 className="font-semibold text-gray-900 dark:text-white">
{title}
</h3>
{isLoading && (
<span className="text-xs text-gray-500 dark:text-gray-400">Thinking...</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Thinking...
</span>
)}
</div>
<div className="flex items-center gap-2">
{messages.some(m => m.error) && (
{messages.some((m) => m.error) && (
<button
onClick={retryLastMessage}
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
@@ -320,51 +355,63 @@ export default function ChatBot() {
key={message.id}
className={cn(
"flex gap-3",
message.role === "user" ? "justify-end" : "justify-start"
message.role === "user" ? "justify-end" : "justify-start",
)}
>
{message.role !== "user" && (
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0",
message.error
? "bg-red-100 dark:bg-red-900"
: "bg-blue-100 dark:bg-blue-900"
)}>
<Bot className={cn(
"w-5 h-5",
<div
className={cn(
"w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0",
message.error
? "text-red-600 dark:text-red-400"
: "text-blue-600 dark:text-blue-400"
)} />
? "bg-red-100 dark:bg-red-900"
: "bg-blue-100 dark:bg-blue-900",
)}
>
<Bot
className={cn(
"w-5 h-5",
message.error
? "text-red-600 dark:text-red-400"
: "text-blue-600 dark:text-blue-400",
)}
/>
</div>
)}
<div className={cn(
"max-w-[70%]",
message.role === "user" ? "order-1" : "order-2"
)}>
<Card className={cn(
"py-0",
message.role === "user"
? "bg-blue-600 text-white border-blue-600"
: message.error
? "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
: "bg-gray-50 dark:bg-gray-700"
)}>
<div
className={cn(
"max-w-[70%]",
message.role === "user" ? "order-1" : "order-2",
)}
>
<Card
className={cn(
"py-0",
message.role === "user"
? "bg-blue-600 text-white border-blue-600"
: message.error
? "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
: "bg-gray-50 dark:bg-gray-700",
)}
>
<CardContent className="p-3">
<p className={cn(
"whitespace-pre-wrap text-sm",
message.error && "text-red-700 dark:text-red-400"
)}>
<p
className={cn(
"whitespace-pre-wrap text-sm",
message.error && "text-red-700 dark:text-red-400",
)}
>
{message.content}
</p>
<p className={cn(
"text-xs mt-1 opacity-70",
message.role === "user"
? "text-blue-100"
: message.error
? "text-red-500 dark:text-red-400"
: "text-gray-500 dark:text-gray-400"
)}>
<p
className={cn(
"text-xs mt-1 opacity-70",
message.role === "user"
? "text-blue-100"
: message.error
? "text-red-500 dark:text-red-400"
: "text-gray-500 dark:text-gray-400",
)}
>
{message.timestamp.toLocaleTimeString()}
</p>
</CardContent>
@@ -387,9 +434,18 @@ export default function ChatBot() {
<CardContent className="p-3">
<div className="flex items-center gap-2">
<div className="flex gap-1">
<span className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></span>
<span className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></span>
<span className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></span>
<span
className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"
style={{ animationDelay: "0ms" }}
></span>
<span
className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"
style={{ animationDelay: "150ms" }}
></span>
<span
className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"
style={{ animationDelay: "300ms" }}
></span>
</div>
</div>
</CardContent>
@@ -420,7 +476,7 @@ export default function ChatBot() {
size="icon"
title="Send message"
>
{(!canSend || isLoading) ? (
{!canSend || isLoading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
@@ -433,4 +489,4 @@ export default function ChatBot() {
</div>
</div>
);
}
}
+14 -7
View File
@@ -1,4 +1,9 @@
import { ApiClients, cloudApiClient, createWorkflowClient, createWorkflowConfig } from "@llamaindex/ui";
import {
ApiClients,
cloudApiClient,
createWorkflowClient,
createWorkflowConfig,
} from "@llamaindex/ui";
import { AGENT_NAME } from "./config";
const platformToken = import.meta.env.VITE_LLAMA_CLOUD_API_KEY;
@@ -17,12 +22,14 @@ cloudApiClient.setConfig({
},
});
const workflowsClient = createWorkflowClient(createWorkflowConfig({
baseUrl: `/deployments/${AGENT_NAME}/`,
headers: {
...(platformToken && { authorization: `Bearer ${platformToken}` }),
}
}));
const workflowsClient = createWorkflowClient(
createWorkflowConfig({
baseUrl: `/deployments/${AGENT_NAME}/`,
headers: {
...(platformToken && { authorization: `Bearer ${platformToken}` }),
},
}),
);
const clients: ApiClients = {
workflowsClient: workflowsClient,
+1 -1
View File
@@ -1,2 +1,2 @@
export const APP_TITLE = "{{ project_title }}"
export const APP_TITLE = "{{ project_title }}";
export const AGENT_NAME = import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME;
+6 -2
View File
@@ -1,3 +1,7 @@
export function toHumanResponseRawEvent(str: string) {
return { __is_pydantic: true, value: { _data: { response: str } }, qualified_name: "workflows.events.HumanResponseEvent" }
}
return {
__is_pydantic: true,
value: { _data: { response: str } },
qualified_name: "workflows.events.HumanResponseEvent",
};
}
+1 -3
View File
@@ -7,7 +7,5 @@ import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
</React.StrictMode>,
);
+9 -7
View File
@@ -20,12 +20,14 @@ export default function Home() {
<div className="flex mb-4">
<WorkflowTrigger
workflowName="upload"
inputFields={[{
key: "index_name",
label: "Index Name",
placeholder: "e.g. document_qa_index",
required: true,
}]}
inputFields={[
{
key: "index_name",
label: "Index Name",
placeholder: "e.g. document_qa_index",
required: true,
},
]}
customWorkflowInput={(files, fieldValues) => {
return {
file_id: files[0].fileId,
@@ -43,4 +45,4 @@ export default function Home() {
</div>
</div>
);
}
}
-1
View File
@@ -13,4 +13,3 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}