diff --git a/.husky/pre-commit b/.husky/pre-commit index 513a9dc4..50e113d0 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,8 @@ yarn lint-staged + +MD_FILES=$(git diff --cached --name-only | grep -E '^docs/.+\.md$' | tr '\n' ' ') +if [ -n "$MD_FILES" ]; then + yarn docs:links -- $MD_FILES +fi + CI=true yarn copyright:check && yarn docs:check && yarn lint && yarn format && yarn ts:check && yarn run test:unit diff --git a/docs/workflows.md b/docs/workflows.md index b1c844c0..57df3728 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -89,7 +89,7 @@ console.info(`Counter:`, response.result); _Source: [examples/workflows/nesting.ts](/examples/workflows/nesting.ts)_ -### Content Creator Agent +### Agent Delegation @@ -180,7 +180,183 @@ for await (const { prompt } of reader) { _Source: [examples/workflows/agent.ts](/examples/workflows/agent.ts)_ -### Multi Agents +### Multi-agent Content Creator + + + +```ts +import "dotenv/config"; +import { z } from "zod"; +import { Workflow } from "bee-agent-framework/experimental/workflows/workflow"; +import { BeeAgent } from "bee-agent-framework/agents/bee/agent"; +import { UnconstrainedMemory } from "bee-agent-framework/memory/unconstrainedMemory"; +import { createConsoleReader } from "examples/helpers/io.js"; +import { BaseMessage } from "bee-agent-framework/llms/primitives/message"; +import { JsonDriver } from "bee-agent-framework/llms/drivers/json"; +import { isEmpty, pick } from "remeda"; +import { LLMTool } from "bee-agent-framework/tools/llm"; +import { GoogleSearchTool } from "bee-agent-framework/tools/search/googleSearch"; +import { GroqChatLLM } from "bee-agent-framework/adapters/groq/chat"; + +const schema = z.object({ + input: z.string(), + output: z.string().optional(), + + topic: z.string().optional(), + notes: z.array(z.string()).default([]), + plan: z.string().optional(), + draft: z.string().optional(), +}); + +const workflow = new Workflow({ + schema, + outputSchema: schema.required({ output: true }), +}) + .addStep("preprocess", async (state) => { + const llm = new GroqChatLLM(); + const driver = new JsonDriver(llm); + + const { parsed } = await driver.generate( + schema.pick({ topic: true, notes: true }).or( + z.object({ + error: z + .string() + .describe("Use when the input query does not make sense or you need clarification."), + }), + ), + [ + BaseMessage.of({ + role: `user`, + text: [ + "Your task is to rewrite the user query so that it guides the content planner and editor to craft a blog post that perfectly aligns with the user's needs. Notes should be used only if the user complains about something.", + "If the user query does ", + "", + ...[state.topic && ["# Previous Topic", state.topic, ""]], + ...[!isEmpty(state.notes) && ["# Previous Notes", ...state.notes, ""]], + "# User Query", + state.input || "empty", + ] + .filter(Boolean) + .join("\n"), + }), + ], + ); + + return "error" in parsed + ? { update: { output: parsed.error }, next: Workflow.END } + : { update: pick(parsed, ["notes", "topic"]) }; + }) + .addStrictStep("planner", schema.required({ topic: true }), async (state) => { + const llm = new GroqChatLLM(); + const agent = new BeeAgent({ + llm, + memory: new UnconstrainedMemory(), + tools: [new GoogleSearchTool(), new LLMTool({ llm })], + }); + + agent.emitter.on("update", (data) => { + console.info(data.update); + }); + + const { result } = await agent.run({ + prompt: [ + `You are a Content Planner. Your task is to write a content plan for "${state.topic}" topic in Markdown format.`, + ``, + `# Objectives`, + `1. Prioritize the latest trends, key players, and noteworthy news.`, + `2. Identify the target audience, considering their interests and pain points.`, + `3. Develop a detailed content outline including introduction, key points, and a call to action.`, + `4. Include SEO keywords and relevant sources.`, + ``, + ...[!isEmpty(state.notes) && ["# Notes", ...state.notes, ""]], + `Provide a structured output that covers the mentioned sections.`, + ].join("\n"), + }); + + return { + update: { + plan: result.text, + }, + }; + }) + .addStrictStep("writer", schema.required({ plan: true }), async (state) => { + const llm = new GroqChatLLM(); + const output = await llm.generate([ + BaseMessage.of({ + role: `system`, + text: [ + `You are a Content Writer. Your task is to write a compelling blog post based on the provided context.`, + ``, + `# Context`, + `${state.plan}`, + ``, + `# Objectives`, + `- An engaging introduction`, + `- Insightful body paragraphs (2-3 per section)`, + `- Properly named sections/subtitles`, + `- A summarizing conclusion`, + `- Format: Markdown`, + ``, + ...[!isEmpty(state.notes) && ["# Notes", ...state.notes, ""]], + `Ensure the content flows naturally, incorporates SEO keywords, and is well-structured.`, + ].join("\n"), + }), + ]); + + return { + update: { draft: output.getTextContent() }, + }; + }) + .addStrictStep("editor", schema.required({ draft: true }), async (state) => { + const llm = new GroqChatLLM(); + const output = await llm.generate([ + BaseMessage.of({ + role: `system`, + text: [ + `You are an Editor. Your task is to transform the following draft blog post to a final version.`, + ``, + `# Draft`, + `${state.draft}`, + ``, + `# Objectives`, + `- Fix Grammatical errors`, + `- Journalistic best practices`, + ``, + ...[!isEmpty(state.notes) && ["# Notes", ...state.notes, ""]], + ``, + `IMPORTANT: The final version must not contain any editor's comments.`, + ].join("\n"), + }), + ]); + + return { + update: { output: output.getTextContent() }, + }; + }); + +let lastResult = {} as Workflow.output; +const reader = createConsoleReader(); +for await (const { prompt } of reader) { + const { result } = await workflow + .run({ + input: prompt, + notes: lastResult?.notes, + topic: lastResult?.topic, + }) + .observe((emitter) => { + emitter.on("start", ({ step, run }) => { + reader.write(`-> ▶️ ${step}`, JSON.stringify(run.state).substring(0, 200).concat("...")); + }); + }); + + lastResult = result; + reader.write("🤖 Answer", lastResult.output); +} +``` + +_Source: [examples/workflows/contentCreator.ts](/examples/workflows/contentCreator.ts)_ + +### Multi Agents Workflows @@ -192,27 +368,32 @@ import { OpenMeteoTool } from "bee-agent-framework/tools/weather/openMeteo"; import { WikipediaTool } from "bee-agent-framework/tools/search/wikipedia"; import { AgentWorkflow } from "bee-agent-framework/experimental/workflows/agent"; import { BaseMessage, Role } from "bee-agent-framework/llms/primitives/message"; -import { GroqChatLLM } from "bee-agent-framework/adapters/groq/chat"; +import { WatsonXChatLLM } from "bee-agent-framework/adapters/watsonx/chat"; const workflow = new AgentWorkflow(); +const llm = WatsonXChatLLM.fromPreset("meta-llama/llama-3-3-70b-instruct", { + apiKey: process.env.WATSONX_API_KEY, + projectId: process.env.WATSONX_PROJECT_ID, +}); + workflow.addAgent({ name: "WeatherForecaster", instructions: "You are a weather assistant. Respond only if you can provide a useful answer.", tools: [new OpenMeteoTool()], - llm: new GroqChatLLM(), + llm, execution: { maxIterations: 3 }, }); workflow.addAgent({ name: "Researcher", instructions: "You are a researcher assistant. Respond only if you can provide a useful answer.", tools: [new WikipediaTool()], - llm: new GroqChatLLM(), + llm, }); workflow.addAgent({ name: "Solver", instructions: "Your task is to provide the most useful final answer based on the assistants' responses which all are relevant. Ignore those where assistant do not know.", - llm: new GroqChatLLM(), + llm, }); const reader = createConsoleReader(); @@ -232,7 +413,10 @@ for await (const { prompt } of reader) { reader.write(`-> ${data.step}`, data.response?.update?.finalAnswer ?? "-"); }); }); - await memory.addMany(result.newMessages); + + // await memory.addMany(result.newMessages); // save intermediate steps + final answer + await memory.addMany(result.newMessages.slice(-1)); // save only the final answer + reader.write(`Agent 🤖`, result.finalAnswer); } ``` diff --git a/examples/workflows/multiAgents.ts b/examples/workflows/multiAgents.ts index b069f0b8..c38472cf 100644 --- a/examples/workflows/multiAgents.ts +++ b/examples/workflows/multiAgents.ts @@ -5,27 +5,32 @@ import { OpenMeteoTool } from "bee-agent-framework/tools/weather/openMeteo"; import { WikipediaTool } from "bee-agent-framework/tools/search/wikipedia"; import { AgentWorkflow } from "bee-agent-framework/experimental/workflows/agent"; import { BaseMessage, Role } from "bee-agent-framework/llms/primitives/message"; -import { GroqChatLLM } from "bee-agent-framework/adapters/groq/chat"; +import { WatsonXChatLLM } from "bee-agent-framework/adapters/watsonx/chat"; const workflow = new AgentWorkflow(); +const llm = WatsonXChatLLM.fromPreset("meta-llama/llama-3-3-70b-instruct", { + apiKey: process.env.WATSONX_API_KEY, + projectId: process.env.WATSONX_PROJECT_ID, +}); + workflow.addAgent({ name: "WeatherForecaster", instructions: "You are a weather assistant. Respond only if you can provide a useful answer.", tools: [new OpenMeteoTool()], - llm: new GroqChatLLM(), + llm, execution: { maxIterations: 3 }, }); workflow.addAgent({ name: "Researcher", instructions: "You are a researcher assistant. Respond only if you can provide a useful answer.", tools: [new WikipediaTool()], - llm: new GroqChatLLM(), + llm, }); workflow.addAgent({ name: "Solver", instructions: "Your task is to provide the most useful final answer based on the assistants' responses which all are relevant. Ignore those where assistant do not know.", - llm: new GroqChatLLM(), + llm, }); const reader = createConsoleReader(); @@ -45,6 +50,9 @@ for await (const { prompt } of reader) { reader.write(`-> ${data.step}`, data.response?.update?.finalAnswer ?? "-"); }); }); - await memory.addMany(result.newMessages); + + // await memory.addMany(result.newMessages); // save intermediate steps + final answer + await memory.addMany(result.newMessages.slice(-1)); // save only the final answer + reader.write(`Agent 🤖`, result.finalAnswer); } diff --git a/package.json b/package.json index 1692363e..9831d03d 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,7 @@ "lint-staged": { "{src,tests,scripts}/**/*": "yarn copyright", "*": "prettier --ignore-unknown --write", - "*.{ts,js}": "eslint --fix", - "docs/**/*.md": "yarn docs:links" + "*.{ts,js}": "eslint --fix" }, "prettier": { "printWidth": 100,