diff --git a/.changeset/chilly-foxes-remain.md b/.changeset/chilly-foxes-remain.md new file mode 100644 index 000000000..4c6f0f530 --- /dev/null +++ b/.changeset/chilly-foxes-remain.md @@ -0,0 +1,5 @@ +--- +"create-llama": patch +--- + +Add artifacts use case (python) diff --git a/packages/create-llama/helpers/python.ts b/packages/create-llama/helpers/python.ts index 4586e2819..159da2786 100644 --- a/packages/create-llama/helpers/python.ts +++ b/packages/create-llama/helpers/python.ts @@ -562,7 +562,7 @@ const installLlamaIndexServerTemplate = async ({ process.exit(1); } - await copy("workflow.py", path.join(root, "app"), { + await copy("*.py", path.join(root, "app"), { parents: true, cwd: path.join(templatesDir, "components", "workflows", "python", useCase), }); diff --git a/packages/create-llama/helpers/types.ts b/packages/create-llama/helpers/types.ts index f480eccfc..7245a4f41 100644 --- a/packages/create-llama/helpers/types.ts +++ b/packages/create-llama/helpers/types.ts @@ -57,7 +57,8 @@ export type TemplateUseCase = | "form_filling" | "extractor" | "contract_review" - | "agentic_rag"; + | "agentic_rag" + | "artifacts"; // Config for both file and folder export type FileSourceConfig = | { diff --git a/packages/create-llama/questions/simple.ts b/packages/create-llama/questions/simple.ts index e7f7a6b4b..4207afcea 100644 --- a/packages/create-llama/questions/simple.ts +++ b/packages/create-llama/questions/simple.ts @@ -6,7 +6,11 @@ import { ModelConfig, TemplateFramework } from "../helpers/types"; import { PureQuestionArgs, QuestionResults } from "./types"; import { askPostInstallAction, questionHandlers } from "./utils"; -type AppType = "agentic_rag" | "financial_report" | "deep_research"; +type AppType = + | "agentic_rag" + | "financial_report" + | "deep_research" + | "artifacts"; type SimpleAnswers = { appType: AppType; @@ -42,6 +46,12 @@ export const askSimpleQuestions = async ( description: "Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.", }, + { + title: "Artifacts", + value: "artifacts", + description: + "Build your own Vercel's v0 or OpenAI's canvas-styled UI.", + }, ], }, questionHandlers, @@ -52,7 +62,7 @@ export const askSimpleQuestions = async ( let useLlamaCloud = false; - if (appType !== "extractor" && appType !== "contract_review") { + if (appType !== "artifacts") { const { language: newLanguage } = await prompts( { type: "select", @@ -111,10 +121,10 @@ const convertAnswers = async ( args: PureQuestionArgs, answers: SimpleAnswers, ): Promise => { - const MODEL_GPT4o: ModelConfig = { + const MODEL_GPT41: ModelConfig = { provider: "openai", apiKey: args.openAiKey, - model: "gpt-4o", + model: "gpt-4.1", embeddingModel: "text-embedding-3-large", dimensions: 1536, isConfigured(): boolean { @@ -135,13 +145,19 @@ const convertAnswers = async ( template: "llamaindexserver", dataSources: EXAMPLE_10K_SEC_FILES, tools: getTools(["interpreter", "document_generator"]), - modelConfig: MODEL_GPT4o, + modelConfig: MODEL_GPT41, }, deep_research: { template: "llamaindexserver", dataSources: EXAMPLE_10K_SEC_FILES, tools: [], - modelConfig: MODEL_GPT4o, + modelConfig: MODEL_GPT41, + }, + artifacts: { + template: "llamaindexserver", + dataSources: [], + tools: [], + modelConfig: MODEL_GPT41, }, }; diff --git a/packages/create-llama/templates/components/ui/workflows/artifacts/ui_event.jsx b/packages/create-llama/templates/components/ui/workflows/artifacts/ui_event.jsx new file mode 100644 index 000000000..7394bb81f --- /dev/null +++ b/packages/create-llama/templates/components/ui/workflows/artifacts/ui_event.jsx @@ -0,0 +1,137 @@ +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { Markdown } from "@llamaindex/chat-ui/widgets"; +import { ListChecks, Loader2, Wand2 } from "lucide-react"; +import { useEffect, useState } from "react"; + +const STAGE_META = { + plan: { + icon: ListChecks, + badgeText: "Step 1/2: Planning", + gradient: "from-blue-100 via-blue-50 to-white", + progress: 33, + iconBg: "bg-blue-100 text-blue-600", + badge: "bg-blue-100 text-blue-700", + }, + generate: { + icon: Wand2, + badgeText: "Step 2/2: Generating", + gradient: "from-violet-100 via-violet-50 to-white", + progress: 66, + iconBg: "bg-violet-100 text-violet-600", + badge: "bg-violet-100 text-violet-700", + }, +}; + +function ArtifactWorkflowCard({ event }) { + const [visible, setVisible] = useState(event?.state !== "completed"); + const [fade, setFade] = useState(false); + + useEffect(() => { + if (event?.state === "completed") { + setVisible(false); + } else { + setVisible(true); + setFade(false); + } + }, [event?.state]); + + if (!event || !visible) return null; + + const { state, requirement } = event; + const meta = STAGE_META[state]; + + if (!meta) return null; + + return ( +
+ + +
+ +
+ + + {meta.badgeText} + + +
+ + {state === "plan" && ( +
+ +
+ Analyzing your request... +
+ +
+ )} + {state === "generate" && ( +
+
+ + + Working on the requirement: + +
+
+ {requirement ? ( + + ) : ( + + No requirements available yet. + + )} +
+
+ )} +
+
+ +
+
+
+ ); +} + +export default function Component({ events }) { + const aggregateEvents = () => { + if (!events || events.length === 0) return null; + return events[events.length - 1]; + }; + + const event = aggregateEvents(); + + return ; +} diff --git a/packages/create-llama/templates/components/workflows/python/artifacts/README-template.md b/packages/create-llama/templates/components/workflows/python/artifacts/README-template.md new file mode 100644 index 000000000..428d51d5b --- /dev/null +++ b/packages/create-llama/templates/components/workflows/python/artifacts/README-template.md @@ -0,0 +1,69 @@ +This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/). + +## Getting Started + +First, setup the environment with uv: + +> **_Note:_** This step is not needed if you are using the dev-container. + +```shell +uv sync +``` + +Then check the parameters that have been pre-configured in the `.env` file in this directory. +Make sure you have set the `OPENAI_API_KEY` for the LLM. + +Then, run the development server: + +```shell +uv run fastapi dev +``` + +Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI. + +To start the app optimized for **production**, run: + +``` +uv run fastapi run +``` + +## Configure LLM and Embedding Model + +You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py). + +## Use Case + +We have prepared two artifact workflows: + +- [Code Workflow](app/code_workflow.py): To generate code and display it in the UI like Vercel's v0. +- [Document Workflow](app/document_workflow.py): Generate and update a document like OpenAI's canvas. + +Modify the factory method in [`workflow.py`](app/workflow.py) to decide which artifact workflow to use. Without any changes the Code Workflow is used. + +You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request: + +``` +curl --location 'localhost:8000/api/chat' \ +--header 'Content-Type: application/json' \ +--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }' +``` + +## Customize the UI + +To customize the UI, you can start by modifying the [./components/ui_event.jsx](./components/ui_event.jsx) file. + +You can also generate a new code for the workflow using LLM by running the following command: + +``` +uv run generate_ui +``` + +## Learn More + +To learn more about LlamaIndex, take a look at the following resources: + +- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex. +- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows. +- [LlamaIndex Server](https://pypi.org/project/llama-index-server/) + +You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome! diff --git a/packages/create-llama/templates/components/workflows/python/artifacts/code_workflow.py b/packages/create-llama/templates/components/workflows/python/artifacts/code_workflow.py new file mode 100644 index 000000000..215ff10dc --- /dev/null +++ b/packages/create-llama/templates/components/workflows/python/artifacts/code_workflow.py @@ -0,0 +1,365 @@ +import re +import time +from typing import Any, Literal, Optional, Union + +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.llms import LLM +from llama_index.core.memory import ChatMemoryBuffer +from llama_index.core.prompts import PromptTemplate +from llama_index.core.workflow import ( + Context, + Event, + StartEvent, + StopEvent, + Workflow, + step, +) +from llama_index.server.api.models import ( + Artifact, + ArtifactEvent, + ArtifactType, + ChatRequest, + CodeArtifactData, + UIEvent, +) +from llama_index.server.api.utils import get_last_artifact +from pydantic import BaseModel, Field + + +class Requirement(BaseModel): + next_step: Literal["answering", "coding"] + language: Optional[str] = None + file_name: Optional[str] = None + requirement: str + + +class PlanEvent(Event): + user_msg: str + context: Optional[str] = None + + +class GenerateArtifactEvent(Event): + requirement: Requirement + + +class SynthesizeAnswerEvent(Event): + pass + + +class UIEventData(BaseModel): + """ + Event data for updating workflow status to the UI. + """ + + state: Literal["plan", "generate", "completed"] = Field( + description="The current state of the workflow. " + "plan: analyze and create a plan for the next step. " + "generate: generate the artifact based on the requirement from the previous step. " + "completed: the workflow is completed. " + ) + requirement: Optional[str] = Field( + description="The requirement for generating the artifact. ", + default=None, + ) + + +class CodeArtifactWorkflow(Workflow): + """ + A simple workflow that help generate/update the chat artifact (code, document) + e.g: Help create a NextJS app. + Update the generated code with the user's feedback. + Generate a guideline for the app,... + """ + + def __init__( + self, + llm: LLM, + chat_request: ChatRequest, + **kwargs: Any, + ): + """ + Args: + llm: The LLM to use. + chat_request: The chat request from the chat app to use. + """ + super().__init__(**kwargs) + self.llm = llm + self.chat_request = chat_request + self.last_artifact = get_last_artifact(chat_request) + + @step + async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> PlanEvent: + user_msg = ev.user_msg + if user_msg is None: + raise ValueError("user_msg is required to run the workflow") + await ctx.set("user_msg", user_msg) + chat_history = ev.chat_history or [] + chat_history.append( + ChatMessage( + role="user", + content=user_msg, + ) + ) + memory = ChatMemoryBuffer.from_defaults( + chat_history=chat_history, + llm=self.llm, + ) + await ctx.set("memory", memory) + return PlanEvent( + user_msg=user_msg, + context=str(self.last_artifact.model_dump_json()) + if self.last_artifact + else "", + ) + + @step + async def planning( + self, ctx: Context, event: PlanEvent + ) -> Union[GenerateArtifactEvent, SynthesizeAnswerEvent]: + """ + Based on the conversation history and the user's request + this step will help to provide a good next step for the code or document generation. + """ + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="plan", + requirement=None, + ), + ) + ) + prompt = PromptTemplate(""" + You are a product analyst responsible for analyzing the user's request and providing the next step for code or document generation. + You are helping user with their code artifact. To update the code, you need to plan a coding step. + + Follow these instructions: + 1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be. + 2. The next step must be one of the following two options: + - "coding": To make the changes to the current code. + - "answering": If you don't need to update the current code or need clarification from the user. + Important: Avoid telling the user to update the code themselves, you are the one who will update the code (by planning a coding step). + 3. If the next step is "coding", you may specify the language ("typescript" or "python") and file_name if known, otherwise set them to null. + 4. The requirement must be provided clearly what is the user request and what need to be done for the next step in details + as precise and specific as possible, don't be stingy with in the requirement. + 5. If the next step is "answering", set language and file_name to null, and the requirement should describe what to answer or explain to the user. + 6. Be concise; only return the requirements for the next step. + 7. The requirements must be in the following format: + ```json + { + "next_step": "answering" | "coding", + "language": "typescript" | "python" | null, + "file_name": string | null, + "requirement": string + } + ``` + + ## Example 1: + User request: Create a calculator app. + You should return: + ```json + { + "next_step": "coding", + "language": "typescript", + "file_name": "calculator.tsx", + "requirement": "Generate code for a calculator app that has a simple UI with a display and button layout. The display should show the current input and the result. The buttons should include basic operators, numbers, clear, and equals. The calculation should work correctly." + } + ``` + + ## Example 2: + User request: Explain how the game loop works. + Context: You have already generated the code for a snake game. + You should return: + ```json + { + "next_step": "answering", + "language": null, + "file_name": null, + "requirement": "The user is asking about the game loop. Explain how the game loop works." + } + ``` + + {context} + + Now, plan the user's next step for this request: + {user_msg} + """).format( + context="" + if event.context is None + else f"## The context is: \n{event.context}\n", + user_msg=event.user_msg, + ) + response = await self.llm.acomplete( + prompt=prompt, + formatted=True, + ) + # parse the response to Requirement + # 1. use regex to find the json block + json_block = re.search( + r"```(?:json)?\s*([\s\S]*?)\s*```", response.text, re.IGNORECASE + ) + if json_block is None: + raise ValueError("No JSON block found in the response.") + # 2. parse the json block to Requirement + requirement = Requirement.model_validate_json(json_block.group(1).strip()) + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="generate", + requirement=requirement.requirement, + ), + ) + ) + # Put the planning result to the memory + # useful for answering step + memory: ChatMemoryBuffer = await ctx.get("memory") + memory.put( + ChatMessage( + role="assistant", + content=f"The plan for next step: \n{response.text}", + ) + ) + await ctx.set("memory", memory) + if requirement.next_step == "coding": + return GenerateArtifactEvent( + requirement=requirement, + ) + else: + return SynthesizeAnswerEvent() + + @step + async def generate_artifact( + self, ctx: Context, event: GenerateArtifactEvent + ) -> SynthesizeAnswerEvent: + """ + Generate the code based on the user's request. + """ + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="generate", + requirement=event.requirement.requirement, + ), + ) + ) + prompt = PromptTemplate(""" + You are a skilled developer who can help user with coding. + You are given a task to generate or update a code for a given requirement. + + ## Follow these instructions: + **1. Carefully read the user's requirements.** + If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output. + If the previous code is provided: + + Carefully analyze the code with the request to make the right changes. + + Avoid making a lot of changes from the previous code if the request is not to write the code from scratch again. + **2. For code requests:** + - If the user does not specify a framework or language, default to a React component using the Next.js framework. + - For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS. + The import pattern should be: + ``` + import { ComponentName } from "@/components/ui/component-name" + import { Markdown } from "@llamaindex/chat-ui" + import { cn } from "@/lib/utils" + ``` + - Ensure the code is idiomatic, production-ready, and includes necessary imports. + - Only generate code relevant to the user's request—do not add extra boilerplate. + **3. Don't be verbose on response** + - No other text or comments only return the code which wrapped by ```language``` block. + - If the user's request is to update the code, only return the updated code. + **4. Only the following languages are allowed: "typescript", "python".** + **5. If there is no code to update, return the reason without any code block.** + + ## Example: + ```typescript + import React from "react"; + import { Button } from "@/components/ui/button"; + import { cn } from "@/lib/utils"; + + export default function MyComponent() { + return ( +
+ +
+ ); + } + + The previous code is: + {previous_artifact} + + Now, i have to generate the code for the following requirement: + {requirement} + ``` + """).format( + previous_artifact=self.last_artifact.model_dump_json() + if self.last_artifact + else "", + requirement=event.requirement, + ) + response = await self.llm.acomplete( + prompt=prompt, + formatted=True, + ) + # Extract the code from the response + language_pattern = r"```(\w+)([\s\S]*)```" + code_match = re.search(language_pattern, response.text) + if code_match is None: + return SynthesizeAnswerEvent() + else: + code = code_match.group(2).strip() + # Put the generated code to the memory + memory: ChatMemoryBuffer = await ctx.get("memory") + memory.put( + ChatMessage( + role="assistant", + content=f"Updated the code: \n{response.text}", + ) + ) + # To show the Canvas panel for the artifact + ctx.write_event_to_stream( + ArtifactEvent( + data=Artifact( + type=ArtifactType.CODE, + created_at=int(time.time()), + data=CodeArtifactData( + language=event.requirement.language or "", + file_name=event.requirement.file_name or "", + code=code, + ), + ), + ) + ) + return SynthesizeAnswerEvent() + + @step + async def synthesize_answer( + self, ctx: Context, event: SynthesizeAnswerEvent + ) -> StopEvent: + """ + Synthesize the answer. + """ + memory: ChatMemoryBuffer = await ctx.get("memory") + chat_history = memory.get() + chat_history.append( + ChatMessage( + role="system", + content=""" + You are a helpful assistant who is responsible for explaining the work to the user. + Based on the conversation history, provide an answer to the user's question. + The user has access to the code so avoid mentioning the whole code again in your response. + """, + ) + ) + response_stream = await self.llm.astream_chat( + messages=chat_history, + ) + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="completed", + ), + ) + ) + return StopEvent(result=response_stream) diff --git a/packages/create-llama/templates/components/workflows/python/artifacts/document_workflow.py b/packages/create-llama/templates/components/workflows/python/artifacts/document_workflow.py new file mode 100644 index 000000000..eded1d2e8 --- /dev/null +++ b/packages/create-llama/templates/components/workflows/python/artifacts/document_workflow.py @@ -0,0 +1,337 @@ +import re +import time +from typing import Any, Literal, Optional + +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.llms import LLM +from llama_index.core.memory import ChatMemoryBuffer +from llama_index.core.prompts import PromptTemplate +from llama_index.core.workflow import ( + Context, + Event, + StartEvent, + StopEvent, + Workflow, + step, +) +from llama_index.server.api.models import ( + Artifact, + ArtifactEvent, + ArtifactType, + ChatRequest, + DocumentArtifactData, + UIEvent, +) +from llama_index.server.api.utils import get_last_artifact +from pydantic import BaseModel, Field + + +class DocumentRequirement(BaseModel): + type: Literal["markdown", "html"] + title: str + requirement: str + + +class PlanEvent(Event): + user_msg: str + context: Optional[str] = None + + +class GenerateArtifactEvent(Event): + requirement: DocumentRequirement + + +class SynthesizeAnswerEvent(Event): + requirement: DocumentRequirement + generated_artifact: str + + +class UIEventData(BaseModel): + """ + Event data for updating workflow status to the UI. + """ + + state: Literal["plan", "generate", "completed"] = Field( + description="The current state of the workflow. " + "plan: analyze and create a plan for the next step. " + "generate: generate the artifact based on the requirement from the previous step. " + "completed: the workflow is completed. " + ) + requirement: Optional[str] = Field( + description="The requirement for generating the artifact. ", + default=None, + ) + + +class DocumentArtifactWorkflow(Workflow): + """ + A workflow to help generate or update document artifacts (e.g., Markdown or HTML documents). + Example use cases: Generate a project guideline, update documentation with user feedback, etc. + """ + + def __init__( + self, + llm: LLM, + chat_request: ChatRequest, + **kwargs: Any, + ): + """ + Args: + llm: The LLM to use. + chat_request: The chat request from the chat app to use. + """ + super().__init__(**kwargs) + self.llm = llm + self.chat_request = chat_request + self.last_artifact = get_last_artifact(chat_request) + + @step + async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> PlanEvent: + user_msg = ev.user_msg + if user_msg is None: + raise ValueError("user_msg is required to run the workflow") + await ctx.set("user_msg", user_msg) + chat_history = ev.chat_history or [] + chat_history.append( + ChatMessage( + role="user", + content=user_msg, + ) + ) + memory = ChatMemoryBuffer.from_defaults( + chat_history=chat_history, + llm=self.llm, + ) + await ctx.set("memory", memory) + return PlanEvent( + user_msg=user_msg, + context=str(self.last_artifact.model_dump_json()) + if self.last_artifact + else "", + ) + + @step + async def planning(self, ctx: Context, event: PlanEvent) -> GenerateArtifactEvent: + """ + Based on the conversation history and the user's request, + this step will provide a clear requirement for the next document generation or update. + """ + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="plan", + requirement=None, + ), + ) + ) + prompt = PromptTemplate(""" + You are a documentation analyst responsible for analyzing the user's request and providing requirements for document generation or update. + Follow these instructions: + 1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be. + 2. From the user's request, provide requirements for the next step of the document generation or update. + 3. Do not be verbose; only return the requirements for the next step of the document generation or update. + 4. Only the following document types are allowed: "markdown", "html". + 5. The requirement should be in the following format: + ```json + { + "type": "markdown" | "html", + "title": string, + "requirement": string + } + ``` + + ## Example: + User request: Create a project guideline document. + You should return: + ```json + { + "type": "markdown", + "title": "Project Guideline", + "requirement": "Generate a Markdown document that outlines the project goals, deliverables, and timeline. Include sections for introduction, objectives, deliverables, and timeline." + } + ``` + + User request: Add a troubleshooting section to the guideline. + You should return: + ```json + { + "type": "markdown", + "title": "Project Guideline", + "requirement": "Add a 'Troubleshooting' section at the end of the document with common issues and solutions." + } + ``` + + {context} + + Now, please plan for the user's request: + {user_msg} + """).format( + context="" + if event.context is None + else f"## The context is: \n{event.context}\n", + user_msg=event.user_msg, + ) + response = await self.llm.acomplete( + prompt=prompt, + formatted=True, + ) + # parse the response to DocumentRequirement + json_block = re.search(r"```json([\s\S]*)```", response.text) + if json_block is None: + raise ValueError("No json block found in the response") + requirement = DocumentRequirement.model_validate_json( + json_block.group(1).strip() + ) + + # Put the planning result to the memory + memory: ChatMemoryBuffer = await ctx.get("memory") + memory.put( + ChatMessage( + role="assistant", + content=f"Planning for the document generation: \n{response.text}", + ) + ) + await ctx.set("memory", memory) + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="generate", + requirement=requirement.requirement, + ), + ) + ) + return GenerateArtifactEvent( + requirement=requirement, + ) + + @step + async def generate_artifact( + self, ctx: Context, event: GenerateArtifactEvent + ) -> SynthesizeAnswerEvent: + """ + Generate or update the document based on the user's request. + """ + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="generate", + requirement=event.requirement.requirement, + ), + ) + ) + prompt = PromptTemplate(""" + You are a skilled technical writer who can help users with documentation. + You are given a task to generate or update a document for a given requirement. + + ## Follow these instructions: + **1. Carefully read the user's requirements.** + If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output. + If the previous document is provided: + + Carefully analyze the document with the request to make the right changes. + + Avoid making unnecessary changes from the previous document if the request is not to rewrite it from scratch. + **2. For document requests:** + - If the user does not specify a type, default to Markdown. + - Ensure the document is clear, well-structured, and grammatically correct. + - Only generate content relevant to the user's request—do not add extra boilerplate. + **3. Do not be verbose in your response.** + - No other text or comments; only return the document content wrapped by the appropriate code block (```markdown or ```html). + - If the user's request is to update the document, only return the updated document. + **4. Only the following types are allowed: "markdown", "html".** + **5. If there is no change to the document, return the reason without any code block.** + + ## Example: + ```markdown + # Project Guideline + + ## Introduction + ... + ``` + + The previous content is: + {previous_artifact} + + Now, please generate the document for the following requirement: + {requirement} + """).format( + previous_artifact=self.last_artifact.model_dump_json() + if self.last_artifact + else "", + requirement=event.requirement, + ) + response = await self.llm.acomplete( + prompt=prompt, + formatted=True, + ) + # Extract the document from the response + language_pattern = r"```(markdown|html)([\s\S]*)```" + doc_match = re.search(language_pattern, response.text) + if doc_match is None: + return SynthesizeAnswerEvent( + requirement=event.requirement, + generated_artifact="There is no change to the document. " + + response.text.strip(), + ) + content = doc_match.group(2).strip() + doc_type = doc_match.group(1) + # Put the generated document to the memory + memory: ChatMemoryBuffer = await ctx.get("memory") + memory.put( + ChatMessage( + role="assistant", + content=f"Generated document: \n{response.text}", + ) + ) + # To show the Canvas panel for the artifact + ctx.write_event_to_stream( + ArtifactEvent( + data=Artifact( + type=ArtifactType.DOCUMENT, + created_at=int(time.time()), + data=DocumentArtifactData( + title=event.requirement.title, + content=content, + type=doc_type, # type: ignore + ), + ), + ) + ) + return SynthesizeAnswerEvent( + requirement=event.requirement, + generated_artifact=response.text, + ) + + @step + async def synthesize_answer( + self, ctx: Context, event: SynthesizeAnswerEvent + ) -> StopEvent: + """ + Synthesize the answer for the user. + """ + memory: ChatMemoryBuffer = await ctx.get("memory") + chat_history = memory.get() + chat_history.append( + ChatMessage( + role="system", + content=""" + Your responsibility is to explain the work to the user. + If there is no document to update, explain the reason. + If the document is updated, just summarize what changed. Don't need to include the whole document again in the response. + """, + ) + ) + response_stream = await self.llm.astream_chat( + messages=chat_history, + ) + ctx.write_event_to_stream( + UIEvent( + type="ui_event", + data=UIEventData( + state="completed", + requirement=event.requirement.requirement, + ), + ) + ) + return StopEvent(result=response_stream) diff --git a/packages/create-llama/templates/components/workflows/python/artifacts/workflow.py b/packages/create-llama/templates/components/workflows/python/artifacts/workflow.py new file mode 100644 index 000000000..a234aeefa --- /dev/null +++ b/packages/create-llama/templates/components/workflows/python/artifacts/workflow.py @@ -0,0 +1,15 @@ +from app.code_workflow import CodeArtifactWorkflow + +# from app.document_workflow import DocumentArtifactWorkflow to generate documents +from llama_index.core.workflow import Workflow +from llama_index.llms.openai import OpenAI +from llama_index.server.api.models import ChatRequest + + +def create_workflow(chat_request: ChatRequest) -> Workflow: + workflow = CodeArtifactWorkflow( + llm=OpenAI(model="gpt-4.1"), + chat_request=chat_request, + timeout=120.0, + ) + return workflow diff --git a/packages/create-llama/templates/types/llamaindexserver/fastapi/pyproject.toml b/packages/create-llama/templates/types/llamaindexserver/fastapi/pyproject.toml index bf98c32d7..88ce9fe79 100644 --- a/packages/create-llama/templates/types/llamaindexserver/fastapi/pyproject.toml +++ b/packages/create-llama/templates/types/llamaindexserver/fastapi/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "pydantic<2.10", "aiostream>=0.5.2,<0.6.0", "llama-index-core>=0.12.28,<0.13.0", - "llama-index-server>=0.1.14,<0.2.0", + "llama-index-server>=0.1.15,<0.2.0", ] [project.optional-dependencies]