Skip to content

Commit 081c871

Browse files
chore: copied doc-workflow example from CL
1 parent 361a685 commit 081c871

File tree

2 files changed

+481
-0
lines changed

2 files changed

+481
-0
lines changed
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import { ChatMemoryBuffer, ChatMessage, LLM, MessageContent } from "llamaindex";
2+
3+
import {
4+
agentStreamEvent,
5+
createStatefulMiddleware,
6+
createWorkflow,
7+
startAgentEvent,
8+
stopAgentEvent,
9+
workflowEvent,
10+
} from "@llamaindex/workflow";
11+
12+
import { z } from "zod";
13+
14+
export const DocumentRequirementSchema = z.object({
15+
type: z.enum(["markdown", "html"]),
16+
title: z.string(),
17+
requirement: z.string(),
18+
});
19+
20+
export type DocumentRequirement = z.infer<typeof DocumentRequirementSchema>;
21+
22+
export const UIEventSchema = z.object({
23+
type: z.literal("ui_event"),
24+
data: z.object({
25+
state: z
26+
.enum(["plan", "generate", "completed"])
27+
.describe(
28+
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
29+
),
30+
requirement: z
31+
.string()
32+
.optional()
33+
.describe(
34+
"An optional requirement creating or updating a document, if applicable.",
35+
),
36+
}),
37+
});
38+
export type UIEvent = z.infer<typeof UIEventSchema>;
39+
export const uiEvent = workflowEvent<UIEvent>();
40+
41+
const planEvent = workflowEvent<{
42+
userInput: MessageContent;
43+
context?: string | undefined;
44+
}>();
45+
46+
const generateArtifactEvent = workflowEvent<{
47+
requirement: DocumentRequirement;
48+
}>();
49+
50+
const synthesizeAnswerEvent = workflowEvent<{
51+
requirement: DocumentRequirement;
52+
generatedArtifact: string;
53+
}>();
54+
55+
const ArtifactSchema = z.object({
56+
type: z.literal("artifact"),
57+
data: z.object({
58+
type: z.literal("document"),
59+
data: z.object({
60+
title: z.string(),
61+
content: z.string(),
62+
type: z.string(),
63+
}),
64+
created_at: z.number(),
65+
}),
66+
});
67+
type Artifact = z.infer<typeof ArtifactSchema>;
68+
export const artifactEvent = workflowEvent<Artifact>();
69+
70+
export function createDocumentArtifactWorkflow(
71+
llm: LLM,
72+
chatHistory: ChatMessage[],
73+
lastArtifact: Artifact | undefined,
74+
) {
75+
const { withState, getContext } = createStatefulMiddleware(() => {
76+
return {
77+
memory: new ChatMemoryBuffer({
78+
llm,
79+
chatHistory: chatHistory,
80+
}),
81+
lastArtifact: lastArtifact,
82+
};
83+
});
84+
const workflow = withState(createWorkflow());
85+
86+
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
87+
// Prepare chat history
88+
const { state } = getContext();
89+
// Put user input to the memory
90+
if (!userInput) {
91+
throw new Error("Missing user input to start the workflow");
92+
}
93+
state.memory.put({
94+
role: "user",
95+
content: userInput,
96+
});
97+
return planEvent.with({
98+
userInput,
99+
context: state.lastArtifact
100+
? JSON.stringify(state.lastArtifact)
101+
: undefined,
102+
});
103+
});
104+
105+
workflow.handle([planEvent], async ({ data: planData }) => {
106+
const { sendEvent } = getContext();
107+
const { state } = getContext();
108+
sendEvent(
109+
uiEvent.with({
110+
type: "ui_event",
111+
data: {
112+
state: "plan",
113+
},
114+
}),
115+
);
116+
const user_msg = planData.userInput;
117+
const context = planData.context
118+
? `## The context is: \n${planData.context}\n`
119+
: "";
120+
const prompt = `
121+
You are a documentation analyst responsible for analyzing the user's request and providing requirements for document generation or update.
122+
Follow these instructions:
123+
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
124+
2. From the user's request, provide requirements for the next step of the document generation or update.
125+
3. Do not be verbose; only return the requirements for the next step of the document generation or update.
126+
4. Only the following document types are allowed: "markdown", "html".
127+
5. The requirement should be in the following format:
128+
\`\`\`json
129+
{
130+
"type": "markdown" | "html",
131+
"title": string,
132+
"requirement": string
133+
}
134+
\`\`\`
135+
136+
## Example:
137+
User request: Create a project guideline document.
138+
You should return:
139+
\`\`\`json
140+
{
141+
"type": "markdown",
142+
"title": "Project Guideline",
143+
"requirement": "Generate a Markdown document that outlines the project goals, deliverables, and timeline. Include sections for introduction, objectives, deliverables, and timeline."
144+
}
145+
\`\`\`
146+
147+
User request: Add a troubleshooting section to the guideline.
148+
You should return:
149+
\`\`\`json
150+
{
151+
"type": "markdown",
152+
"title": "Project Guideline",
153+
"requirement": "Add a 'Troubleshooting' section at the end of the document with common issues and solutions."
154+
}
155+
\`\`\`
156+
157+
${context}
158+
159+
Now, please plan for the user's request:
160+
${user_msg}
161+
`;
162+
163+
const response = await llm.complete({
164+
prompt,
165+
});
166+
// Parse the response to DocumentRequirement
167+
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
168+
if (!jsonBlock) {
169+
throw new Error("No JSON block found in the response.");
170+
}
171+
const requirement = DocumentRequirementSchema.parse(
172+
JSON.parse(jsonBlock[1]),
173+
);
174+
state.memory.put({
175+
role: "assistant",
176+
content: `Planning for the document generation: \n${response.text}`,
177+
});
178+
return generateArtifactEvent.with({
179+
requirement,
180+
});
181+
});
182+
183+
workflow.handle(
184+
[generateArtifactEvent],
185+
async ({ data: { requirement } }) => {
186+
const { sendEvent } = getContext();
187+
const { state } = getContext();
188+
189+
sendEvent(
190+
uiEvent.with({
191+
type: "ui_event",
192+
data: {
193+
state: "generate",
194+
requirement: requirement.requirement,
195+
},
196+
}),
197+
);
198+
199+
const previousArtifact = state.lastArtifact
200+
? JSON.stringify(state.lastArtifact)
201+
: "";
202+
const requirementStr = JSON.stringify(requirement);
203+
204+
const prompt = `
205+
You are a skilled technical writer who can help users with documentation.
206+
You are given a task to generate or update a document for a given requirement.
207+
208+
## Follow these instructions:
209+
**1. Carefully read the user's requirements.**
210+
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
211+
If the previous document is provided:
212+
+ Carefully analyze the document with the request to make the right changes.
213+
+ Avoid making unnecessary changes from the previous document if the request is not to rewrite it from scratch.
214+
**2. For document requests:**
215+
- If the user does not specify a type, default to Markdown.
216+
- Ensure the document is clear, well-structured, and grammatically correct.
217+
- Only generate content relevant to the user's request—do not add extra boilerplate.
218+
**3. Do not be verbose in your response.**
219+
- No other text or comments; only return the document content wrapped by the appropriate code block (\`\`\`markdown or \`\`\`html).
220+
- If the user's request is to update the document, only return the updated document.
221+
**4. Only the following types are allowed: "markdown", "html".**
222+
**5. If there is no change to the document, return the reason without any code block.**
223+
224+
## Example:
225+
\`\`\`markdown
226+
# Project Guideline
227+
228+
## Introduction
229+
...
230+
\`\`\`
231+
232+
The previous content is:
233+
${previousArtifact}
234+
235+
Now, please generate the document for the following requirement:
236+
${requirementStr}
237+
`;
238+
239+
const response = await llm.complete({
240+
prompt,
241+
});
242+
243+
// Extract the document from the response
244+
const docMatch = response.text.match(/```(markdown|html)([\s\S]*)```/);
245+
const generatedContent = response.text;
246+
247+
if (docMatch) {
248+
const content = docMatch[2].trim();
249+
const docType = docMatch[1] as "markdown" | "html";
250+
251+
// Put the generated document to the memory
252+
state.memory.put({
253+
role: "assistant",
254+
content: `Generated document: \n${response.text}`,
255+
});
256+
257+
// To show the Canvas panel for the artifact
258+
sendEvent(
259+
artifactEvent.with({
260+
type: "artifact",
261+
data: {
262+
type: "document",
263+
created_at: Date.now(),
264+
data: {
265+
title: requirement.title,
266+
content: content,
267+
type: docType,
268+
},
269+
},
270+
}),
271+
);
272+
}
273+
274+
return synthesizeAnswerEvent.with({
275+
requirement,
276+
generatedArtifact: generatedContent,
277+
});
278+
},
279+
);
280+
281+
workflow.handle([synthesizeAnswerEvent], async ({ data }) => {
282+
const { sendEvent } = getContext();
283+
const { state } = getContext();
284+
285+
const chatHistory = await state.memory.getMessages();
286+
const messages = [
287+
...chatHistory,
288+
{
289+
role: "system" as const,
290+
content: `
291+
Your responsibility is to explain the work to the user.
292+
If there is no document to update, explain the reason.
293+
If the document is updated, just summarize what changed. Don't need to include the whole document again in the response.
294+
`,
295+
},
296+
];
297+
298+
const responseStream = await llm.chat({
299+
messages,
300+
stream: true,
301+
});
302+
303+
sendEvent(
304+
uiEvent.with({
305+
type: "ui_event",
306+
data: {
307+
state: "completed",
308+
requirement: data.requirement.requirement,
309+
},
310+
}),
311+
);
312+
313+
let response = "";
314+
for await (const chunk of responseStream) {
315+
response += chunk.delta;
316+
sendEvent(
317+
agentStreamEvent.with({
318+
delta: chunk.delta,
319+
response: "",
320+
currentAgentName: "assistant",
321+
raw: chunk,
322+
}),
323+
);
324+
}
325+
326+
return stopAgentEvent.with({
327+
result: response,
328+
});
329+
});
330+
331+
return workflow;
332+
}

0 commit comments

Comments
 (0)