mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-02 19:14:28 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad5912b41f | |||
| 76502d28e7 | |||
| f4ca602da5 | |||
| d304554f33 |
@@ -31,6 +31,13 @@ jobs:
|
||||
- name: Run Prettier
|
||||
run: pnpm run format
|
||||
|
||||
- name: Run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run Typecheck for examples
|
||||
run: pnpm run typecheck
|
||||
working-directory: packages/server/examples
|
||||
|
||||
- name: Run Python format check
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# create-llama
|
||||
|
||||
## 0.5.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f4ca602: Add artifact use case for Typescript template
|
||||
- f4ca602: Update typescript use cases to use the new workflow engine
|
||||
|
||||
## 0.5.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -31,7 +31,7 @@ const installLlamaIndexServerTemplate = async ({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await copy("workflow.ts", path.join(root, "src", "app"), {
|
||||
await copy("*.ts", path.join(root, "src", "app"), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
@@ -516,7 +516,7 @@ async function updatePackageJson({
|
||||
if (backend) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/readers": "^2.0.0",
|
||||
"@llamaindex/readers": "^3.0.0",
|
||||
};
|
||||
|
||||
if (vectorDb && vectorDb in vectorDbDependencies) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.5.12",
|
||||
"version": "0.5.13",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
@@ -62,21 +62,19 @@ export const askSimpleQuestions = async (
|
||||
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (appType !== "artifacts") {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
|
||||
+6
-2
@@ -1,5 +1,9 @@
|
||||
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import {
|
||||
Document,
|
||||
LLamaCloudFileService,
|
||||
LlamaCloudIndex,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
import { parseFile, storeFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
@@ -10,6 +10,7 @@ dependencies = [
|
||||
"python-dotenv>=1.0.0",
|
||||
"pydantic<2.10",
|
||||
"llama-index>=0.12.1",
|
||||
"llama-parse>=0.6.21,<0.7.0",
|
||||
"cachetools>=5.3.3",
|
||||
"reflex>=0.6.2.post1",
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencies = [
|
||||
"python-dotenv>=1.0.0",
|
||||
"pydantic<2.10",
|
||||
"llama-index>=0.12.1",
|
||||
"llama-parse>=0.6.21,<0.7.0",
|
||||
"cachetools>=5.3.3",
|
||||
"reflex>=0.6.2.post1",
|
||||
]
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { LlamaCloudIndex } from "llamaindex";
|
||||
|
||||
type LlamaCloudDataSourceParams = {
|
||||
llamaCloudPipeline?: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { LlamaCloudIndex } from "llamaindex";
|
||||
|
||||
type LlamaCloudDataSourceParams = {
|
||||
llamaCloudPipeline?: {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { agent } from "llamaindex";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
|
||||
|
||||
## Configure LLM and Embedding Model
|
||||
|
||||
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
|
||||
|
||||
## Custom UI Components
|
||||
|
||||
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
|
||||
|
||||
```
|
||||
npm run generate:ui
|
||||
```
|
||||
|
||||
## Use Case
|
||||
|
||||
We have prepared two artifact workflows:
|
||||
|
||||
- [Code Workflow](app/code_workflow.ts): To generate code and display it in the UI like Vercel's v0.
|
||||
- [Document Workflow](app/document_workflow.ts): Generate and update a document like OpenAI's canvas.
|
||||
|
||||
Modify the factory method in [`workflow.ts`](app/workflow.ts) 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:3000) or you can test the `/api/chat` endpoint with the following curl request:
|
||||
|
||||
```shell
|
||||
curl --location 'localhost:3000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
|
||||
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
import { extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, LLM, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const RequirementSchema = z.object({
|
||||
next_step: z.enum(["answering", "coding"]),
|
||||
language: z.string().nullable().optional(),
|
||||
file_name: z.string().nullable().optional(),
|
||||
requirement: z.string(),
|
||||
});
|
||||
|
||||
export type Requirement = z.infer<typeof RequirementSchema>;
|
||||
|
||||
export const UIEventSchema = z.object({
|
||||
type: z.literal("ui_event"),
|
||||
data: z.object({
|
||||
state: z
|
||||
.enum(["plan", "generate", "completed"])
|
||||
.describe(
|
||||
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
|
||||
),
|
||||
requirement: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"An optional requirement creating or updating a code, if applicable.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UIEvent = z.infer<typeof UIEventSchema>;
|
||||
const planEvent = workflowEvent<{
|
||||
userInput: string;
|
||||
context?: string | undefined;
|
||||
}>();
|
||||
|
||||
const generateArtifactEvent = workflowEvent<{
|
||||
requirement: Requirement;
|
||||
}>();
|
||||
|
||||
const synthesizeAnswerEvent = workflowEvent<object>();
|
||||
|
||||
const uiEvent = workflowEvent<UIEvent>();
|
||||
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact";
|
||||
data: {
|
||||
type: "code";
|
||||
created_at: number;
|
||||
data: {
|
||||
language: string;
|
||||
file_name: string;
|
||||
code: string;
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
export function createCodeArtifactWorkflow(reqBody: any, llm?: LLM) {
|
||||
if (!llm) {
|
||||
llm = Settings.llm;
|
||||
}
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({
|
||||
llm,
|
||||
chatHistory: reqBody.chatHistory,
|
||||
}),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.put({
|
||||
role: "user",
|
||||
content: userInput,
|
||||
});
|
||||
return planEvent.with({
|
||||
userInput,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle([planEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "plan",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const user_msg = planData.userInput;
|
||||
const context = planData.context
|
||||
? `## The context is: \n${planData.context}\n`
|
||||
: "";
|
||||
const prompt = `
|
||||
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}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
// parse the response to Requirement
|
||||
// 1. use regex to find the json block
|
||||
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonBlock) {
|
||||
throw new Error("No JSON block found in the response.");
|
||||
}
|
||||
const requirement = RequirementSchema.parse(JSON.parse(jsonBlock[1]));
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `The plan for next step: \n${response.text}`,
|
||||
});
|
||||
|
||||
if (requirement.next_step === "coding") {
|
||||
return generateArtifactEvent.with({
|
||||
requirement,
|
||||
});
|
||||
} else {
|
||||
return synthesizeAnswerEvent.with({});
|
||||
}
|
||||
});
|
||||
|
||||
workflow.handle([generateArtifactEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: planData.requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const previousArtifact = state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: "There is no previous artifact";
|
||||
const requirementText = planData.requirement.requirement;
|
||||
|
||||
const prompt = `
|
||||
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:
|
||||
\`\`\`typescript
|
||||
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 (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<Button>Click me</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
The previous code is:
|
||||
{previousArtifact}
|
||||
|
||||
Now, i have to generate the code for the following requirement:
|
||||
{requirement}
|
||||
`
|
||||
.replace("{previousArtifact}", previousArtifact)
|
||||
.replace("{requirement}", requirementText);
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
|
||||
// Extract the code from the response
|
||||
const codeMatch = response.text.match(/```(\w+)([\s\S]*)```/);
|
||||
if (!codeMatch) {
|
||||
return synthesizeAnswerEvent.with({});
|
||||
}
|
||||
|
||||
const code = codeMatch[2].trim();
|
||||
|
||||
// Put the generated code to the memory
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Updated the code: \n${response.text}`,
|
||||
});
|
||||
|
||||
// To show the Canvas panel for the artifact
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact",
|
||||
data: {
|
||||
type: "code",
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
language: planData.requirement.language || "",
|
||||
file_name: planData.requirement.file_name || "",
|
||||
code,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return synthesizeAnswerEvent.with({});
|
||||
});
|
||||
|
||||
workflow.handle([synthesizeAnswerEvent], async () => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
const messages = [
|
||||
...chatHistory,
|
||||
{
|
||||
role: "system" as const,
|
||||
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.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const responseStream = await llm.chat({
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "completed",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let response = "";
|
||||
for await (const chunk of responseStream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response: "",
|
||||
currentAgentName: "assistant",
|
||||
raw: chunk,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
import { extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, LLM, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const DocumentRequirementSchema = z.object({
|
||||
type: z.enum(["markdown", "html"]),
|
||||
title: z.string(),
|
||||
requirement: z.string(),
|
||||
});
|
||||
|
||||
export type DocumentRequirement = z.infer<typeof DocumentRequirementSchema>;
|
||||
|
||||
export const UIEventSchema = z.object({
|
||||
type: z.literal("ui_event"),
|
||||
data: z.object({
|
||||
state: z
|
||||
.enum(["plan", "generate", "completed"])
|
||||
.describe(
|
||||
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
|
||||
),
|
||||
requirement: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"An optional requirement creating or updating a document, if applicable.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UIEvent = z.infer<typeof UIEventSchema>;
|
||||
|
||||
const planEvent = workflowEvent<{
|
||||
userInput: string;
|
||||
context?: string | undefined;
|
||||
}>();
|
||||
|
||||
const generateArtifactEvent = workflowEvent<{
|
||||
requirement: DocumentRequirement;
|
||||
}>();
|
||||
|
||||
const synthesizeAnswerEvent = workflowEvent<{
|
||||
requirement: DocumentRequirement;
|
||||
generatedArtifact: string;
|
||||
}>();
|
||||
|
||||
const uiEvent = workflowEvent<UIEvent>();
|
||||
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact";
|
||||
data: {
|
||||
type: "document";
|
||||
created_at: number;
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
type: "markdown" | "html";
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
export function createDocumentArtifactWorkflow(reqBody: any, llm?: LLM) {
|
||||
if (!llm) {
|
||||
llm = Settings.llm;
|
||||
}
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({
|
||||
llm,
|
||||
chatHistory: reqBody.chatHistory,
|
||||
}),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.put({
|
||||
role: "user",
|
||||
content: userInput,
|
||||
});
|
||||
return planEvent.with({
|
||||
userInput,
|
||||
context: state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle([planEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "plan",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const user_msg = planData.userInput;
|
||||
const context = planData.context
|
||||
? `## The context is: \n${planData.context}\n`
|
||||
: "";
|
||||
const prompt = `
|
||||
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}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
// Parse the response to DocumentRequirement
|
||||
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonBlock) {
|
||||
throw new Error("No JSON block found in the response.");
|
||||
}
|
||||
const requirement = DocumentRequirementSchema.parse(
|
||||
JSON.parse(jsonBlock[1]),
|
||||
);
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Planning for the document generation: \n${response.text}`,
|
||||
});
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return generateArtifactEvent.with({
|
||||
requirement,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle(
|
||||
[generateArtifactEvent],
|
||||
async ({ data: { requirement } }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const previousArtifact = state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: "";
|
||||
const requirementStr = JSON.stringify(requirement);
|
||||
|
||||
const prompt = `
|
||||
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:
|
||||
${previousArtifact}
|
||||
|
||||
Now, please generate the document for the following requirement:
|
||||
${requirementStr}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
|
||||
// Extract the document from the response
|
||||
const docMatch = response.text.match(/```(markdown|html)([\s\S]*)```/);
|
||||
const generatedContent = response.text;
|
||||
|
||||
if (docMatch) {
|
||||
const content = docMatch[2].trim();
|
||||
const docType = docMatch[1] as "markdown" | "html";
|
||||
|
||||
// Put the generated document to the memory
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Generated document: \n${response.text}`,
|
||||
});
|
||||
|
||||
// To show the Canvas panel for the artifact
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact",
|
||||
data: {
|
||||
type: "document",
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
title: requirement.title,
|
||||
content: content,
|
||||
type: docType,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return synthesizeAnswerEvent.with({
|
||||
requirement,
|
||||
generatedArtifact: generatedContent,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
workflow.handle([synthesizeAnswerEvent], async ({ data }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
const messages = [
|
||||
...chatHistory,
|
||||
{
|
||||
role: "system" as const,
|
||||
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.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const responseStream = await llm.chat({
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "completed",
|
||||
requirement: data.requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let response = "";
|
||||
for await (const chunk of responseStream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response: "",
|
||||
currentAgentName: "assistant",
|
||||
raw: chunk,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createCodeArtifactWorkflow, UIEventSchema } from "./code-workflow";
|
||||
// import { createDocumentArtifactWorkflow, UIEventSchema } from "./doc-workflow";
|
||||
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
// Uncomment the import and change to createDocumentArtifactWorkflow to use the document workflow
|
||||
const workflow = createCodeArtifactWorkflow(reqBody);
|
||||
|
||||
return workflow;
|
||||
};
|
||||
|
||||
// Re-export the UIEventSchema for generating the UI by `pnpm generate:ui` command
|
||||
export { UIEventSchema };
|
||||
+1
-1
@@ -31,7 +31,7 @@ You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/l
|
||||
|
||||
## Custom UI Components
|
||||
|
||||
For Deep Research, we have a custom component located in `components/deep_research_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
|
||||
For Deep Research, we have a custom component located in `components/ui_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
|
||||
|
||||
### Generate a new UI Component from workflow event
|
||||
|
||||
|
||||
+210
-243
@@ -1,22 +1,21 @@
|
||||
import { toSourceEvent, toStreamGenerator } from "@llamaindex/server";
|
||||
import { toSourceEvent } from "@llamaindex/server";
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
AgentInputData,
|
||||
AgentWorkflowContext,
|
||||
ChatMemoryBuffer,
|
||||
ChatResponseChunk,
|
||||
HandlerContext,
|
||||
LlamaCloudIndex,
|
||||
Metadata,
|
||||
MetadataMode,
|
||||
NodeWithScore,
|
||||
PromptTemplate,
|
||||
Settings,
|
||||
StartEvent,
|
||||
StopEvent as StopEventBase,
|
||||
ToolCallLLM,
|
||||
VectorStoreIndex,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
@@ -25,12 +24,11 @@ import { getIndex } from "./data";
|
||||
// workflow factory
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
const index = await getIndex(reqBody?.data);
|
||||
return new DeepResearchWorkflow(index);
|
||||
return getWorkflow(index);
|
||||
};
|
||||
|
||||
// workflow configs
|
||||
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
|
||||
const TIMEOUT = 360; // timeout in seconds
|
||||
const TOP_K = 10; // number of nodes to retrieve from the vector store
|
||||
|
||||
const createPlanResearchPrompt = new PromptTemplate({
|
||||
@@ -114,10 +112,10 @@ Create a well-structured outline for the research report that covers all the ans
|
||||
type ResearchQuestion = { questionId: string; question: string };
|
||||
type ResearchResult = ResearchQuestion & { answer: string };
|
||||
|
||||
class PlanResearchEvent extends WorkflowEvent<{}> {}
|
||||
class ResearchEvent extends WorkflowEvent<ResearchQuestion[]> {}
|
||||
class ReportEvent extends WorkflowEvent<{}> {}
|
||||
class StopEvent extends StopEventBase<AsyncGenerator<ChatResponseChunk>> {}
|
||||
// class PlanResearchEvent extends WorkflowEvent<{}> {}
|
||||
const planResearchEvent = workflowEvent<{}>();
|
||||
const researchEvent = workflowEvent<ResearchQuestion>();
|
||||
const reportEvent = workflowEvent<{}>();
|
||||
|
||||
export const UIEventSchema = z
|
||||
.object({
|
||||
@@ -140,221 +138,180 @@ export const UIEventSchema = z
|
||||
|
||||
type UIEventData = z.infer<typeof UIEventSchema>;
|
||||
|
||||
class UIEvent extends WorkflowEvent<{
|
||||
const uiEvent = workflowEvent<{
|
||||
type: "ui_event";
|
||||
data: UIEventData;
|
||||
}> {}
|
||||
}>();
|
||||
|
||||
// workflow definition
|
||||
class DeepResearchWorkflow extends Workflow<
|
||||
AgentWorkflowContext,
|
||||
AgentInputData,
|
||||
string
|
||||
> {
|
||||
#llm = Settings.llm as ToolCallLLM;
|
||||
#index?: VectorStoreIndex | LlamaCloudIndex;
|
||||
export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
|
||||
const retriever = index.asRetriever({ similarityTopK: TOP_K });
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({
|
||||
llm: Settings.llm,
|
||||
chatHistory: [],
|
||||
}),
|
||||
contextNodes: [] as NodeWithScore<Metadata>[],
|
||||
userRequest: "",
|
||||
totalQuestions: 0,
|
||||
researchResults: [] as ResearchResult[],
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
userRequest: string = "";
|
||||
totalQuestions: number = 0;
|
||||
contextNodes: NodeWithScore<Metadata>[] = [];
|
||||
memory: ChatMemoryBuffer = new ChatMemoryBuffer({ llm: Settings.llm });
|
||||
|
||||
constructor(index: VectorStoreIndex | LlamaCloudIndex) {
|
||||
super({ timeout: TIMEOUT });
|
||||
this.#index = index;
|
||||
this.addWorkflowSteps();
|
||||
}
|
||||
|
||||
addWorkflowSteps() {
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInputData>],
|
||||
outputs: [PlanResearchEvent],
|
||||
},
|
||||
this.handleStartWorkflow,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [PlanResearchEvent],
|
||||
outputs: [ResearchEvent, ReportEvent, StopEvent],
|
||||
},
|
||||
this.handlePlanResearch,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ResearchEvent],
|
||||
outputs: [PlanResearchEvent],
|
||||
},
|
||||
this.handleResearch,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ReportEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
this.handleReport,
|
||||
);
|
||||
}
|
||||
|
||||
async initWorkflow(data: AgentInputData) {
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
const { sendEvent, state } = getContext();
|
||||
if (!userInput) throw new Error("Invalid input");
|
||||
|
||||
this.userRequest = userInput;
|
||||
|
||||
await this.memory.set(chatHistory);
|
||||
await this.memory.put({ role: "user", content: userInput });
|
||||
}
|
||||
|
||||
handleStartWorkflow = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: StartEvent<AgentInputData>,
|
||||
): Promise<PlanResearchEvent> => {
|
||||
await this.initWorkflow(ev.data);
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
state.userRequest = userInput;
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "retrieve", state: "inprogress" },
|
||||
data: {
|
||||
event: "retrieve",
|
||||
state: "inprogress",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const retrievedNodes = await this.retriever.retrieve(this.userRequest);
|
||||
const retrievedNodes = await retriever.retrieve(userInput);
|
||||
|
||||
ctx.sendEvent(toSourceEvent(retrievedNodes));
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
sendEvent(toSourceEvent(retrievedNodes));
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "retrieve", state: "done" },
|
||||
}),
|
||||
);
|
||||
|
||||
this.contextNodes = retrievedNodes;
|
||||
state.contextNodes.push(...retrievedNodes);
|
||||
|
||||
return new PlanResearchEvent({});
|
||||
};
|
||||
return planResearchEvent.with({});
|
||||
});
|
||||
|
||||
handlePlanResearch = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: PlanResearchEvent,
|
||||
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
workflow.handle([planResearchEvent], async ({ data }) => {
|
||||
const { sendEvent, state, stream } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "inprogress" },
|
||||
}),
|
||||
);
|
||||
|
||||
const { decision, researchQuestions, cancelReason } =
|
||||
await this.createResearchPlan();
|
||||
await createResearchPlan(
|
||||
state.memory,
|
||||
state.contextNodes
|
||||
.map((node) => node.node.getContent(MetadataMode.NONE))
|
||||
.join("\n"),
|
||||
enhancedPrompt(state.totalQuestions),
|
||||
state.userRequest,
|
||||
);
|
||||
|
||||
// Stop workflow due to decision from LLM
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
if (decision === "cancel") {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
return new StopEvent(
|
||||
toStreamGenerator(
|
||||
cancelReason ?? "Research cancelled without any reason.",
|
||||
),
|
||||
);
|
||||
return agentStreamEvent.with({
|
||||
delta: cancelReason ?? "Research cancelled without any reason.",
|
||||
response: cancelReason ?? "Research cancelled without any reason.",
|
||||
currentAgentName: "",
|
||||
raw: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger research from generated questions
|
||||
if (decision === "research") {
|
||||
this.memory.put({
|
||||
if (decision === "research" && researchQuestions.length > 0) {
|
||||
state.totalQuestions += researchQuestions.length;
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"We need to find answers to the following questions:\n" +
|
||||
researchQuestions.join("\n"),
|
||||
});
|
||||
|
||||
researchQuestions.forEach(({ questionId: id, question }) => {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "pending", id, question },
|
||||
}),
|
||||
);
|
||||
sendEvent(researchEvent.with({ questionId: id, question }));
|
||||
});
|
||||
|
||||
return new ResearchEvent(researchQuestions);
|
||||
const events = await stream
|
||||
.until(() => state.researchResults.length === researchQuestions.length)
|
||||
.toArray();
|
||||
return planResearchEvent.with({});
|
||||
}
|
||||
|
||||
// Resarch done, start writing report
|
||||
this.memory.put({
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: "No more idea to analyze. We should report the answers.",
|
||||
});
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
return reportEvent.with({});
|
||||
});
|
||||
|
||||
return new ReportEvent({});
|
||||
};
|
||||
workflow.handle([researchEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const { questionId, question } = data;
|
||||
|
||||
handleResearch = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ResearchEvent,
|
||||
): Promise<PlanResearchEvent> => {
|
||||
const researchQuestions = ev.data;
|
||||
|
||||
// Answer questions in parallel
|
||||
const researchResults: ResearchResult[] = await Promise.all(
|
||||
researchQuestions.map(async ({ questionId: id, question }) => {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "inprogress", id, question },
|
||||
}),
|
||||
);
|
||||
|
||||
const answer = await this.answerQuestion(question);
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "done", id, question, answer },
|
||||
}),
|
||||
);
|
||||
|
||||
return { questionId: id, question, answer };
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
event: "answer",
|
||||
state: "inprogress",
|
||||
id: questionId,
|
||||
question,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Save answers to memory
|
||||
researchResults.forEach(({ question, answer }) => {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
|
||||
});
|
||||
});
|
||||
const answer = await answerQuestion(
|
||||
contextStr(state.contextNodes),
|
||||
question,
|
||||
);
|
||||
state.researchResults.push({ questionId, question, answer });
|
||||
|
||||
this.memory.put({
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Researched all the questions. Now, I need to analyze if it's ready to write a report or need to research more.",
|
||||
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
|
||||
});
|
||||
|
||||
this.totalQuestions += researchResults.length;
|
||||
|
||||
return new PlanResearchEvent({});
|
||||
};
|
||||
|
||||
handleReport = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ReportEvent,
|
||||
): Promise<StopEvent> => {
|
||||
const chatHistory = await this.memory.getAllMessages();
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
event: "answer",
|
||||
state: "done",
|
||||
id: questionId,
|
||||
question,
|
||||
answer,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
workflow.handle([reportEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const chatHistory = await state.memory.getAllMessages();
|
||||
const messages = chatHistory.concat([
|
||||
{
|
||||
role: "system",
|
||||
@@ -367,81 +324,91 @@ class DeepResearchWorkflow extends Workflow<
|
||||
},
|
||||
]);
|
||||
|
||||
const stream = await this.llm.chat({ messages, stream: true });
|
||||
|
||||
return new StopEvent(toStreamGenerator(stream));
|
||||
};
|
||||
|
||||
get llm() {
|
||||
if (!this.#llm.supportToolCall) throw new Error("LLM is not a ToolCallLLM");
|
||||
return this.#llm;
|
||||
}
|
||||
|
||||
get retriever() {
|
||||
if (!this.#index) throw new Error("Index is not initialized");
|
||||
return this.#index.asRetriever({ similarityTopK: TOP_K });
|
||||
}
|
||||
|
||||
get contextStr() {
|
||||
return this.contextNodes
|
||||
.map((node) => {
|
||||
const nodeId = node.node.id_;
|
||||
const nodeContent = node.node.getContent(MetadataMode.NONE);
|
||||
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
get enhancedPrompt() {
|
||||
if (this.totalQuestions === 0) {
|
||||
return "The student has no questions to research. Let start by asking some questions.";
|
||||
const stream = await Settings.llm.chat({ messages, stream: true });
|
||||
let response = "";
|
||||
for await (const chunk of stream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response,
|
||||
currentAgentName: "",
|
||||
raw: stream,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.totalQuestions > MAX_QUESTIONS) {
|
||||
return `The student has researched ${this.totalQuestions} questions. Should cancel the research if the context is not enough to write a report.`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async createResearchPlan() {
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
|
||||
const conversationContext = chatHistory
|
||||
.map((message) => `${message.role}: ${message.content}`)
|
||||
.join("\n");
|
||||
|
||||
const prompt = createPlanResearchPrompt.format({
|
||||
context_str: this.contextStr,
|
||||
conversation_context: conversationContext,
|
||||
enhanced_prompt: this.enhancedPrompt,
|
||||
user_request: this.userRequest,
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
const responseFormat = z.object({
|
||||
decision: z.enum(["research", "write", "cancel"]),
|
||||
researchQuestions: z.array(z.string()),
|
||||
cancelReason: z.string().optional(),
|
||||
});
|
||||
|
||||
const result = await this.llm.complete({ prompt, responseFormat });
|
||||
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
|
||||
|
||||
return {
|
||||
...plan,
|
||||
researchQuestions: plan.researchQuestions.map((question) => ({
|
||||
questionId: randomUUID(),
|
||||
question,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async answerQuestion(question: string) {
|
||||
const prompt = researchPrompt.format({
|
||||
context_str: this.contextStr,
|
||||
question,
|
||||
});
|
||||
const result = await this.llm.complete({ prompt });
|
||||
return result.text;
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
|
||||
const createResearchPlan = async (
|
||||
memory: ChatMemoryBuffer,
|
||||
contextStr: string,
|
||||
enhancedPrompt: string,
|
||||
userRequest: string,
|
||||
) => {
|
||||
const chatHistory = await memory.getMessages();
|
||||
|
||||
const conversationContext = chatHistory
|
||||
.map((message) => `${message.role}: ${message.content}`)
|
||||
.join("\n");
|
||||
|
||||
const prompt = createPlanResearchPrompt.format({
|
||||
context_str: contextStr,
|
||||
conversation_context: conversationContext,
|
||||
enhanced_prompt: enhancedPrompt,
|
||||
user_request: userRequest,
|
||||
});
|
||||
|
||||
const responseFormat = z.object({
|
||||
decision: z.enum(["research", "write", "cancel"]),
|
||||
researchQuestions: z.array(z.string()),
|
||||
cancelReason: z.string().optional(),
|
||||
});
|
||||
|
||||
const result = await Settings.llm.complete({ prompt, responseFormat });
|
||||
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
|
||||
|
||||
return {
|
||||
...plan,
|
||||
researchQuestions: plan.researchQuestions.map((question) => ({
|
||||
questionId: randomUUID(),
|
||||
question,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const contextStr = (contextNodes: NodeWithScore<Metadata>[]) => {
|
||||
return contextNodes
|
||||
.map((node) => {
|
||||
const nodeId = node.node.id_;
|
||||
const nodeContent = node.node.getContent(MetadataMode.NONE);
|
||||
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
const enhancedPrompt = (totalQuestions: number) => {
|
||||
if (totalQuestions === 0) {
|
||||
return "The student has no questions to research. Let start by providing some questions for the student to research.";
|
||||
}
|
||||
|
||||
if (totalQuestions >= MAX_QUESTIONS) {
|
||||
return `The student has researched ${totalQuestions} questions. Should proceeding writing report or cancel the research if the answers are not enough to write a report.`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const answerQuestion = async (contextStr: string, question: string) => {
|
||||
const prompt = researchPrompt.format({
|
||||
context_str: contextStr,
|
||||
question,
|
||||
});
|
||||
const result = await Settings.llm.complete({ prompt });
|
||||
return result.text;
|
||||
};
|
||||
|
||||
+133
-211
@@ -6,27 +6,25 @@ import {
|
||||
interpreter,
|
||||
} from "@llamaindex/tools";
|
||||
import {
|
||||
AgentInputData,
|
||||
AgentWorkflowContext,
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
ChatResponseChunk,
|
||||
HandlerContext,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Settings,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
|
||||
export async function workflowFactory(reqBody: any) {
|
||||
const index = await getIndex(reqBody?.data);
|
||||
|
||||
@@ -47,28 +45,18 @@ export async function workflowFactory(reqBody: any) {
|
||||
});
|
||||
const documentGeneratorTool = documentGenerator();
|
||||
|
||||
return new FinancialReportWorkflow({
|
||||
return getWorkflow(
|
||||
queryEngineTool,
|
||||
codeInterpreterTool,
|
||||
documentGeneratorTool,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// Create a custom event type
|
||||
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
class AnalyzeEvent extends WorkflowEvent<{
|
||||
input: ChatMessage | ToolCall[];
|
||||
}> {}
|
||||
|
||||
class ReportGenerationEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
// workflow events
|
||||
const inputEvent = workflowEvent<{ input: ChatMessage[] }>();
|
||||
const researchEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
|
||||
const analyzeEvent = workflowEvent<{ input: ChatMessage | ToolCall[] }>();
|
||||
const reportGenerationEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT = `
|
||||
You are a financial analyst who are given a set of tools to help you.
|
||||
@@ -76,173 +64,103 @@ It's good to using appropriate tools for the user request and always use the inf
|
||||
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
|
||||
`;
|
||||
|
||||
class FinancialReportWorkflow extends Workflow<
|
||||
AgentWorkflowContext,
|
||||
AgentInputData,
|
||||
string
|
||||
> {
|
||||
llm: ToolCallLLM;
|
||||
memory: ChatMemoryBuffer;
|
||||
queryEngineTool: BaseToolWithCall;
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
|
||||
constructor(options: {
|
||||
queryEngineTool: BaseToolWithCall;
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({
|
||||
verbose: options?.verbose ?? false,
|
||||
timeout: options?.timeout ?? 360,
|
||||
});
|
||||
|
||||
this.llm = Settings.llm as ToolCallLLM;
|
||||
if (!this.llm.supportToolCall) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
this.queryEngineTool = options.queryEngineTool;
|
||||
this.codeInterpreterTool = options.codeInterpreterTool;
|
||||
|
||||
this.documentGeneratorTool = options.documentGeneratorTool;
|
||||
this.memory = new ChatMemoryBuffer({ llm: this.llm, chatHistory: [] });
|
||||
|
||||
// Add steps
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInputData>],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.prepareChatHistory,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [InputEvent],
|
||||
outputs: [
|
||||
InputEvent,
|
||||
ResearchEvent,
|
||||
AnalyzeEvent,
|
||||
ReportGenerationEvent,
|
||||
StopEvent,
|
||||
],
|
||||
},
|
||||
this.handleLLMInput,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ResearchEvent],
|
||||
outputs: [AnalyzeEvent],
|
||||
},
|
||||
this.handleResearch,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [AnalyzeEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleAnalyze,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ReportGenerationEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleReportGeneration,
|
||||
);
|
||||
// workflow definition
|
||||
export function getWorkflow(
|
||||
queryEngineTool: BaseToolWithCall,
|
||||
codeInterpreterTool: BaseToolWithCall,
|
||||
documentGeneratorTool: BaseToolWithCall,
|
||||
) {
|
||||
const llm = Settings.llm as ToolCallLLM;
|
||||
if (!llm.supportToolCall) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
const { withState, getContext } = createStatefulMiddleware(() => ({
|
||||
memory: new ChatMemoryBuffer({ llm, chatHistory: [] }),
|
||||
}));
|
||||
|
||||
prepareChatHistory = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: StartEvent<AgentInputData>,
|
||||
): Promise<InputEvent> => {
|
||||
const { userInput, chatHistory = [] } = ev.data;
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
// Add steps
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { state } = getContext();
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
if (!userInput) throw new Error("Invalid input");
|
||||
|
||||
this.memory.set(chatHistory);
|
||||
state.memory.set(chatHistory);
|
||||
|
||||
if (this.systemPrompt) {
|
||||
this.memory.put({ role: "system", content: this.systemPrompt });
|
||||
}
|
||||
state.memory.put({ role: "system", content: DEFAULT_SYSTEM_PROMPT });
|
||||
|
||||
this.memory.put({ role: "user", content: userInput });
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
const messages = await this.memory.getMessages();
|
||||
return new InputEvent({ input: messages });
|
||||
};
|
||||
const messages = await state.memory.getMessages();
|
||||
return inputEvent.with({ input: messages });
|
||||
});
|
||||
|
||||
handleLLMInput = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: InputEvent,
|
||||
): Promise<
|
||||
| InputEvent
|
||||
| ResearchEvent
|
||||
| AnalyzeEvent
|
||||
| ReportGenerationEvent
|
||||
| StopEvent<AsyncGenerator<ChatResponseChunk, any, any> | undefined>
|
||||
> => {
|
||||
const chatHistory = ev.data.input;
|
||||
workflow.handle([inputEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const chatHistory = data.input;
|
||||
|
||||
const tools = [
|
||||
this.codeInterpreterTool,
|
||||
this.documentGeneratorTool,
|
||||
this.queryEngineTool,
|
||||
];
|
||||
const tools = [codeInterpreterTool, documentGeneratorTool, queryEngineTool];
|
||||
|
||||
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
|
||||
const toolCallResponse = await chatWithTools(llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
const generator = toolCallResponse.responseGenerator;
|
||||
let response = "";
|
||||
if (generator) {
|
||||
for await (const chunk of generator) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response,
|
||||
currentAgentName: "LLM", // Or derive from context if needed
|
||||
raw: chunk.raw,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return stopAgentEvent.with({ result: response });
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
this.memory.put({
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
const newChatHistory = await state.memory.getMessages();
|
||||
return inputEvent.with({ input: newChatHistory });
|
||||
}
|
||||
|
||||
// Put the LLM tool call message into the memory
|
||||
// And trigger the next step according to the tool call
|
||||
if (toolCallResponse.toolCallMessage) {
|
||||
this.memory.put(toolCallResponse.toolCallMessage);
|
||||
state.memory.put(toolCallResponse.toolCallMessage);
|
||||
}
|
||||
const toolName = toolCallResponse.getToolNames()[0];
|
||||
switch (toolName) {
|
||||
case this.codeInterpreterTool.metadata.name:
|
||||
return new AnalyzeEvent({
|
||||
case codeInterpreterTool.metadata.name:
|
||||
return analyzeEvent.with({
|
||||
input: toolCallResponse.toolCalls,
|
||||
});
|
||||
case this.documentGeneratorTool.metadata.name:
|
||||
return new ReportGenerationEvent({
|
||||
case documentGeneratorTool.metadata.name:
|
||||
return reportGenerationEvent.with({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
default:
|
||||
if (this.queryEngineTool.metadata.name === toolName) {
|
||||
return new ResearchEvent({
|
||||
if (queryEngineTool.metadata.name === toolName) {
|
||||
return researchEvent.with({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
handleResearch = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ResearchEvent,
|
||||
): Promise<AnalyzeEvent> => {
|
||||
ctx.sendEvent(
|
||||
workflow.handle([researchEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text: "Researching data",
|
||||
@@ -250,13 +168,13 @@ class FinancialReportWorkflow extends Workflow<
|
||||
}),
|
||||
);
|
||||
|
||||
const { toolCalls } = ev.data;
|
||||
const { toolCalls } = data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.queryEngineTool],
|
||||
tools: [queryEngineTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
ctx.sendEvent(
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text,
|
||||
@@ -268,7 +186,7 @@ class FinancialReportWorkflow extends Workflow<
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
state.memory.put(toolMsg);
|
||||
}
|
||||
|
||||
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
|
||||
@@ -277,26 +195,25 @@ class FinancialReportWorkflow extends Workflow<
|
||||
.filter(Boolean);
|
||||
|
||||
if (sourcesNodes.length > 0) {
|
||||
ctx.sendEvent(toSourceEvent(sourcesNodes));
|
||||
sendEvent(toSourceEvent(sourcesNodes));
|
||||
}
|
||||
|
||||
return new AnalyzeEvent({
|
||||
// Send a message indicating research is done, triggering analysis
|
||||
return analyzeEvent.with({
|
||||
input: {
|
||||
role: "assistant",
|
||||
content:
|
||||
"I have finished researching the data, please analyze the data.",
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Analyze a research result or a tool call for code interpreter from the LLM
|
||||
*/
|
||||
handleAnalyze = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: AnalyzeEvent,
|
||||
): Promise<InputEvent> => {
|
||||
ctx.sendEvent(
|
||||
workflow.handle([analyzeEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text: "Analyzing data",
|
||||
@@ -305,8 +222,8 @@ class FinancialReportWorkflow extends Workflow<
|
||||
);
|
||||
// Request by workflow LLM, input is a list of tool calls
|
||||
let toolCalls: ToolCall[] = [];
|
||||
if (Array.isArray(ev.data.input)) {
|
||||
toolCalls = ev.data.input;
|
||||
if (Array.isArray(data.input)) {
|
||||
toolCalls = data.input;
|
||||
} else {
|
||||
// Requested by Researcher, input is a ChatMessage
|
||||
// We start new LLM chat specifically for analyzing the data
|
||||
@@ -320,63 +237,65 @@ class FinancialReportWorkflow extends Workflow<
|
||||
|
||||
// Clone the current chat history
|
||||
// Add the analysis system prompt and the message from the researcher
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
const currentChatHistory = await state.memory.getMessages();
|
||||
const newChatHistory = [
|
||||
...chatHistory,
|
||||
...currentChatHistory,
|
||||
{ role: "system", content: analysisPrompt },
|
||||
ev.data.input,
|
||||
data.input, // This is the ChatMessage from the research step
|
||||
];
|
||||
const toolCallResponse = await chatWithTools(
|
||||
this.llm,
|
||||
[this.codeInterpreterTool],
|
||||
llm,
|
||||
[codeInterpreterTool],
|
||||
newChatHistory as ChatMessage[],
|
||||
);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
this.memory.put(await toolCallResponse.asFullResponse());
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
// If no tool call needed for analysis, put the response directly
|
||||
state.memory.put(await toolCallResponse.asFullResponse());
|
||||
const finalChatHistory = await state.memory.getMessages();
|
||||
return inputEvent.with({ input: finalChatHistory });
|
||||
} else {
|
||||
this.memory.put(toolCallResponse.toolCallMessage!);
|
||||
state.memory.put(toolCallResponse.toolCallMessage!);
|
||||
toolCalls = toolCallResponse.toolCalls;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the tools
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.codeInterpreterTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
ctx.sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
// Call the code interpreter tools if needed
|
||||
if (toolCalls.length > 0) {
|
||||
const toolMsgs = await callTools({
|
||||
tools: [codeInterpreterTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
state.memory.put(toolMsg);
|
||||
}
|
||||
}
|
||||
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
};
|
||||
const finalChatHistory = await state.memory.getMessages();
|
||||
// After analysis (or tool calls for analysis), trigger the next LLM input cycle
|
||||
return inputEvent.with({ input: finalChatHistory });
|
||||
});
|
||||
|
||||
handleReportGeneration = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ReportGenerationEvent,
|
||||
): Promise<InputEvent> => {
|
||||
const { toolCalls } = ev.data;
|
||||
workflow.handle([reportGenerationEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const { toolCalls } = data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.documentGeneratorTool],
|
||||
tools: [documentGeneratorTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
ctx.sendEvent(
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Reporter",
|
||||
text,
|
||||
@@ -388,9 +307,12 @@ class FinancialReportWorkflow extends Workflow<
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
state.memory.put(toolMsg);
|
||||
}
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
};
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
// After report generation, trigger the next LLM input cycle
|
||||
return inputEvent.with({ input: chatHistory });
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "0.2.0",
|
||||
"@llamaindex/readers": "^2.0.0",
|
||||
"@llamaindex/server": "0.1.5",
|
||||
"@llamaindex/server": "0.2.0",
|
||||
"@llamaindex/workflow": "1.1.1",
|
||||
"@llamaindex/tools": "0.0.4",
|
||||
"dotenv": "^16.4.7",
|
||||
"zod": "^3.23.8",
|
||||
"llamaindex": "0.10.2"
|
||||
"llamaindex": "0.10.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
|
||||
@@ -9,6 +9,7 @@ readme = "README.md"
|
||||
requires-python = ">=3.11,<3.14"
|
||||
dependencies = [
|
||||
"llama-index>=0.12.1",
|
||||
"llama-parse>=0.6.21,<0.7.0",
|
||||
"fastapi[standard]>=0.109.1",
|
||||
"uvicorn>=0.23.2",
|
||||
"python-dotenv>=1.0.0",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# LlamaIndex Server Examples
|
||||
|
||||
This directory contains examples of how to use the LlamaIndex Server.
|
||||
|
||||
## Running the examples
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
npx tsx simple-workflow/calculator.ts
|
||||
```
|
||||
|
||||
## Open browser at http://localhost:3000
|
||||
@@ -0,0 +1,43 @@
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import {
|
||||
Document,
|
||||
OpenAI,
|
||||
OpenAIEmbedding,
|
||||
Settings,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
const index = await VectorStoreIndex.fromDocuments([
|
||||
new Document({ text: "The dog is brown" }),
|
||||
new Document({ text: "The dog is yellow" }),
|
||||
]);
|
||||
|
||||
const queryEngineTool = index.queryTool({
|
||||
metadata: {
|
||||
name: "query_document",
|
||||
description: `This tool can retrieve information in documents`,
|
||||
},
|
||||
includeSourceNodes: true,
|
||||
});
|
||||
|
||||
return agent({ tools: [queryEngineTool] });
|
||||
};
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
starterQuestions: ["What is the color of the dog?"],
|
||||
},
|
||||
port: 4100,
|
||||
}).start();
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "llamaindex-server-examples",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsx simple-workflow/calculator.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "^0.2.0",
|
||||
"@llamaindex/readers": "^3.0.0",
|
||||
"@llamaindex/server": "workspace:*",
|
||||
"@llamaindex/tools": "0.0.4",
|
||||
"@llamaindex/workflow": "1.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"llamaindex": "0.10.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
const calculatorAgent = agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "add",
|
||||
description: "Adds two numbers",
|
||||
parameters: z.object({ x: z.number(), y: z.number() }),
|
||||
execute: ({ x, y }) => x + y,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: () => calculatorAgent,
|
||||
uiConfig: {
|
||||
appTitle: "Calculator",
|
||||
starterQuestions: ["1 + 1", "2 + 2"],
|
||||
},
|
||||
port: 4000,
|
||||
}).start();
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+904
-18
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "packages/server/examples"
|
||||
|
||||
Reference in New Issue
Block a user