support code generation of event components using an LLM (Python) (#557)

---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
Huu Le
2025-04-15 18:23:06 +07:00
committed by GitHub
parent 1514a555d5
commit 7c3b279417
8 changed files with 154 additions and 96 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Support code generation of event components using an LLM (Python)
@@ -49,6 +49,16 @@ curl --location 'localhost:8000/api/chat' \
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
```
## Customize the UI
To customize the UI, you can start by modifying the [./components/ui_event.jsx](./components/ui_event.jsx) file.
You can also generate a new code for the workflow using LLM by running the following command:
```
poetry run generate:ui
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -23,8 +23,7 @@ from llama_index.core.workflow import (
Workflow,
step,
)
from llama_index.server.api.models import SourceNodesEvent
from llama_index.server.api.models import ChatRequest
from llama_index.server.api.models import ChatRequest, SourceNodesEvent, UIEvent
from pydantic import BaseModel, Field
logger = logging.getLogger("uvicorn")
@@ -66,20 +65,29 @@ class ReportEvent(Event):
# Events that are streamed to the frontend and rendered there
class DeepResearchEventData(BaseModel):
event: Literal["retrieve", "analyze", "answer"]
state: Literal["pending", "inprogress", "done", "error"]
id: Optional[str] = None
question: Optional[str] = None
answer: Optional[str] = None
class UIEventData(BaseModel):
"""
Events for DeepResearch workflow which has 3 main stages:
- Retrieve: Retrieve information from the knowledge base.
- Analyze: Analyze the retrieved information and provide list of questions for answering.
- Answer: Answering the provided questions. There are multiple answer events, each with its own id that is used to display the answer for a particular question.
"""
class DataEvent(Event):
type: Literal["deep_research_event"]
data: DeepResearchEventData
def to_response(self):
return self.model_dump()
id: Optional[str] = Field(default=None, description="The id of the event")
event: Literal["retrieve", "analyze", "answer"] = Field(
default="retrieve", description="The event type"
)
state: Literal["pending", "inprogress", "done", "error"] = Field(
default="pending", description="The state of the event"
)
question: Optional[str] = Field(
default=None,
description="Used by answer event to display the question",
)
answer: Optional[str] = Field(
default=None,
description="Used by answer event to display the answer of the question",
)
class DeepResearchWorkflow(Workflow):
@@ -137,12 +145,12 @@ class DeepResearchWorkflow(Workflow):
]
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "inprogress",
},
UIEvent(
type="ui_event",
data=UIEventData(
event="retrieve",
state="inprogress",
),
)
)
retriever = self.index.as_retriever(
@@ -151,12 +159,12 @@ class DeepResearchWorkflow(Workflow):
nodes = retriever.retrieve(self.user_request)
self.context_nodes.extend(nodes) # type: ignore
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "done",
},
UIEvent(
type="ui_event",
data=UIEventData(
event="retrieve",
state="done",
),
)
)
# Send source nodes to the stream
@@ -177,12 +185,12 @@ class DeepResearchWorkflow(Workflow):
"""
logger.info("Analyzing the retrieved information")
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "inprogress",
},
UIEvent(
type="ui_event",
data=UIEventData(
event="analyze",
state="inprogress",
),
)
)
total_questions = await ctx.get("total_questions")
@@ -194,12 +202,12 @@ class DeepResearchWorkflow(Workflow):
)
if res.decision == "cancel":
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
UIEvent(
type="ui_event",
data=UIEventData(
event="analyze",
state="done",
),
)
)
return StopEvent(
@@ -210,12 +218,12 @@ class DeepResearchWorkflow(Workflow):
# It's a LLM hallucination.
if total_questions == 0:
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
UIEvent(
type="ui_event",
data=UIEventData(
event="analyze",
state="done",
),
)
)
return StopEvent(
@@ -245,15 +253,15 @@ class DeepResearchWorkflow(Workflow):
for question in res.research_questions:
question_id = str(uuid.uuid4())
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "pending",
"id": question_id,
"question": question,
"answer": None,
},
UIEvent(
type="ui_event",
data=UIEventData(
event="answer",
state="pending",
id=question_id,
question=question,
answer=None,
),
)
)
ctx.send_event(
@@ -264,12 +272,12 @@ class DeepResearchWorkflow(Workflow):
)
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
UIEvent(
type="ui_event",
data=UIEventData(
event="analyze",
state="done",
),
)
)
return None
@@ -280,14 +288,14 @@ class DeepResearchWorkflow(Workflow):
Answer the question
"""
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "inprogress",
"id": ev.question_id,
"question": ev.question,
},
UIEvent(
type="ui_event",
data=UIEventData(
event="answer",
state="inprogress",
id=ev.question_id,
question=ev.question,
),
)
)
try:
@@ -299,15 +307,15 @@ class DeepResearchWorkflow(Workflow):
logger.error(f"Error answering question {ev.question}: {e}")
answer = f"Got error when answering the question: {ev.question}"
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "done",
"id": ev.question_id,
"question": ev.question,
"answer": answer,
},
UIEvent(
type="ui_event",
data=UIEventData(
event="answer",
state="done",
id=ev.question_id,
question=ev.question,
answer=answer,
),
)
)
@@ -128,7 +128,7 @@ type DeepResearchEventData = {
};
class DeepResearchEvent extends WorkflowEvent<{
type: "deep_research_event";
type: "ui_event";
data: DeepResearchEventData;
}> {}
@@ -201,7 +201,7 @@ class DeepResearchWorkflow extends Workflow<
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "retrieve", state: "inprogress" },
}),
);
@@ -212,7 +212,7 @@ class DeepResearchWorkflow extends Workflow<
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "retrieve", state: "done" },
}),
);
@@ -228,7 +228,7 @@ class DeepResearchWorkflow extends Workflow<
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "analyze", state: "inprogress" },
}),
);
@@ -240,7 +240,7 @@ class DeepResearchWorkflow extends Workflow<
if (decision === "cancel") {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
@@ -263,7 +263,7 @@ class DeepResearchWorkflow extends Workflow<
researchQuestions.forEach(({ questionId: id, question }) => {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "answer", state: "pending", id, question },
}),
);
@@ -280,7 +280,7 @@ class DeepResearchWorkflow extends Workflow<
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
@@ -299,7 +299,7 @@ class DeepResearchWorkflow extends Workflow<
researchQuestions.map(async ({ questionId: id, question }) => {
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "answer", state: "inprogress", id, question },
}),
);
@@ -308,7 +308,7 @@ class DeepResearchWorkflow extends Workflow<
ctx.sendEvent(
new DeepResearchEvent({
type: "deep_research_event",
type: "ui_event",
data: { event: "answer", state: "done", id, question, answer },
}),
);
@@ -1,19 +1,24 @@
import logging
import os
from app.index import STORAGE_DIR
from app.settings import init_settings
from dotenv import load_dotenv
from llama_index.core.indices import (
VectorStoreIndex,
)
from llama_index.core.readers import SimpleDirectoryReader
from llama_index.llms.openai import OpenAI
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_datasource():
def generate_index():
"""
Index the documents in the data directory.
"""
from app.index import STORAGE_DIR
from app.settings import init_settings
from llama_index.core.indices import (
VectorStoreIndex,
)
from llama_index.core.readers import SimpleDirectoryReader
load_dotenv()
init_settings()
@@ -31,3 +36,28 @@ def generate_datasource():
# store it for later
index.storage_context.persist(STORAGE_DIR)
logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}")
def generate_ui_for_workflow():
"""
Generate UI for UIEventData event in app/workflow.py
"""
import asyncio
from main import COMPONENT_DIR
# To generate UI components for additional event types,
# import the corresponding data model (e.g., MyCustomEventData)
# and run the generate_ui_for_workflow function with the imported model.
# Make sure the output filename of the generated UI component matches the event type (here `ui_event`)
try:
from app.workflow import UIEventData
except ImportError:
raise ImportError("Couldn't generate UI component for the current workflow.")
from llama_index.server.gen_ui import generate_event_component
# works also well with Claude 3.7 Sonnet or Gemini Pro 2.5
llm = OpenAI(model="gpt-4.1")
code = asyncio.run(generate_event_component(event_cls=UIEventData, llm=llm))
with open(f"{COMPONENT_DIR}/ui_event.jsx", "w") as f:
f.write(code)
@@ -9,6 +9,9 @@ from llama_index.server import LlamaIndexServer, UIConfig
logger = logging.getLogger("uvicorn")
# A path to a directory where the customized UI code is stored
COMPONENT_DIR = "components"
def create_app():
env = os.environ.get("APP_ENV")
@@ -16,7 +19,7 @@ def create_app():
app = LlamaIndexServer(
workflow_factory=create_workflow, # A factory function that creates a new workflow for each request
ui_config=UIConfig(
component_dir="components",
component_dir=COMPONENT_DIR,
app_title="Chat App",
),
env=env,
@@ -7,7 +7,9 @@ authors = ["Marcus Schiesser <mail@marcusschiesser.de>"]
readme = "README.md"
[tool.poetry.scripts]
generate = "generate:generate_datasource"
"generate" = "generate:generate_index"
"generate:index" = "generate:generate_index"
"generate:ui" = "generate:generate_ui_for_workflow"
dev = "main:run('dev')"
prod = "main:run('prod')"
@@ -17,7 +19,7 @@ python-dotenv = "^1.0.0"
pydantic = "<2.10"
aiostream = "^0.5.2"
llama-index-core = "^0.12.28"
llama-index-server = "^0.1.10"
llama-index-server = "^0.1.12"
[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"