mirror of
https://github.com/run-llama/template-workflow-document-qa.git
synced 2026-06-30 21:47:58 -04:00
edits (#1)
* adding test-proj * add github * Add py tests/lints * reformats * hmm * fix formats * m * m
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# Copy this to .env and set any necessary secrets
|
||||
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
__pycache__
|
||||
workflows.db
|
||||
.venv
|
||||
+4
-16
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
+124
-124
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 }}"
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# Copy this to .env and set any necessary secrets
|
||||
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
__pycache__
|
||||
workflows.db
|
||||
.venv
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
def test_placeholder():
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
# uses pnpm
|
||||
pnpm-lock.yaml
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import tailwind from "@tailwindcss/postcss";
|
||||
|
||||
export default {
|
||||
plugins: [tailwind()],
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
export function toHumanResponseRawEvent(str: string) {
|
||||
return {
|
||||
__is_pydantic: true,
|
||||
value: { _data: { response: str } },
|
||||
qualified_name: "workflows.events.HumanResponseEvent",
|
||||
};
|
||||
}
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Vendored
+15
@@ -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;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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),
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -10,5 +10,5 @@ export default function App() {
|
||||
<Home />
|
||||
</ApiProvider>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+121
-65
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -7,7 +7,5 @@ import "./index.css";
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -13,4 +13,3 @@ interface ImportMetaEnv {
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user