mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-18 13:05:55 -04:00
support code generation of event components using an LLM (Python) (#557)
--------- Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user