diff --git a/.changeset/flat-singers-share.md b/.changeset/flat-singers-share.md
new file mode 100644
index 0000000000000000000000000000000000000000..fce3d6e92da6aef3649fc5f1a24d078d191dbd5f
--- /dev/null
+++ b/.changeset/flat-singers-share.md
@@ -0,0 +1,5 @@
+---
+"create-llama": patch
+---
+
+Add publisher agent to multi-agents for generating documents (PDF and HTML)
diff --git a/.changeset/gorgeous-penguins-shout.md b/.changeset/gorgeous-penguins-shout.md
new file mode 100644
index 0000000000000000000000000000000000000000..26dba19408adb4619ea09dde0ed5ba7d9d78287b
--- /dev/null
+++ b/.changeset/gorgeous-penguins-shout.md
@@ -0,0 +1,5 @@
+---
+"create-llama": patch
+---
+
+Allow tool selection for multi-agents (Python and TS)
diff --git a/e2e/python/resolve_dependencies.spec.ts b/e2e/python/resolve_dependencies.spec.ts
index 96864ac061feb0206fc825e0aeb98899b9dacd66..6d778a1f8e38cb97f7ef256886468a18458d383e 100644
--- a/e2e/python/resolve_dependencies.spec.ts
+++ b/e2e/python/resolve_dependencies.spec.ts
@@ -33,6 +33,7 @@ if (
   const toolOptions = [
     "wikipedia.WikipediaToolSpec",
     "google.GoogleSearchToolSpec",
+    "document_generator",
   ];
 
   const dataSources = [
diff --git a/e2e/utils.ts b/e2e/utils.ts
index 361ad7c6c5fe7b2773a9e293314f96f9fd30b2c2..956872cfcb4f720cc6a6e66d1b2b7b789e45eb7d 100644
--- a/e2e/utils.ts
+++ b/e2e/utils.ts
@@ -109,7 +109,9 @@ export async function runCreateLlama({
   if (appType) {
     commandArgs.push(appType);
   }
-  if (!useLlamaParse) {
+  if (useLlamaParse) {
+    commandArgs.push("--use-llama-parse");
+  } else {
     commandArgs.push("--no-llama-parse");
   }
 
diff --git a/helpers/env-variables.ts b/helpers/env-variables.ts
index 11beb0e853b887620960d105f9d2c7521e151e69..49e0ab87848c320fbed84b706ef573a32e5c9966 100644
--- a/helpers/env-variables.ts
+++ b/helpers/env-variables.ts
@@ -426,34 +426,35 @@ const getToolEnvs = (tools?: Tool[]): EnvVar[] => {
 const getSystemPromptEnv = (
   tools?: Tool[],
   dataSources?: TemplateDataSource[],
-  framework?: TemplateFramework,
+  template?: TemplateType,
 ): EnvVar[] => {
   const defaultSystemPrompt =
     "You are a helpful assistant who helps users with their questions.";
 
+  const systemPromptEnv: EnvVar[] = [];
   // build tool system prompt by merging all tool system prompts
-  let toolSystemPrompt = "";
-  tools?.forEach((tool) => {
-    const toolSystemPromptEnv = tool.envVars?.find(
-      (env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
-    );
-    if (toolSystemPromptEnv) {
-      toolSystemPrompt += toolSystemPromptEnv.value + "\n";
-    }
-  });
+  // multiagent template doesn't need system prompt
+  if (template !== "multiagent") {
+    let toolSystemPrompt = "";
+    tools?.forEach((tool) => {
+      const toolSystemPromptEnv = tool.envVars?.find(
+        (env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
+      );
+      if (toolSystemPromptEnv) {
+        toolSystemPrompt += toolSystemPromptEnv.value + "\n";
+      }
+    });
 
-  const systemPrompt = toolSystemPrompt
-    ? `\"${toolSystemPrompt}\"`
-    : defaultSystemPrompt;
+    const systemPrompt = toolSystemPrompt
+      ? `\"${toolSystemPrompt}\"`
+      : defaultSystemPrompt;
 
-  const systemPromptEnv = [
-    {
+    systemPromptEnv.push({
       name: "SYSTEM_PROMPT",
       description: "The system prompt for the AI model.",
       value: systemPrompt,
-    },
-  ];
-
+    });
+  }
   if (tools?.length == 0 && (dataSources?.length ?? 0 > 0)) {
     const citationPrompt = `'You have provided information from a knowledge base that has been passed to you in nodes of information.
 Each node has useful metadata such as node ID, file name, page, etc.
@@ -559,7 +560,7 @@ export const createBackendEnvFile = async (
     ...getToolEnvs(opts.tools),
     ...getTemplateEnvs(opts.template),
     ...getObservabilityEnvs(opts.observability),
-    ...getSystemPromptEnv(opts.tools, opts.dataSources, opts.framework),
+    ...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
   ];
   // Render and write env file
   const content = renderEnvVar(envVars);
diff --git a/helpers/python.ts b/helpers/python.ts
index f5dac282bbf67d9c39844f285da8d0fee3ff8510..4af474342d86e508307ffecfb5220f78cb947891 100644
--- a/helpers/python.ts
+++ b/helpers/python.ts
@@ -364,7 +364,12 @@ export const installPythonTemplate = async ({
   | "modelConfig"
 >) => {
   console.log("\nInitializing Python project with template:", template, "\n");
-  const templatePath = path.join(templatesDir, "types", template, framework);
+  let templatePath;
+  if (template === "extractor") {
+    templatePath = path.join(templatesDir, "types", "extractor", framework);
+  } else {
+    templatePath = path.join(templatesDir, "types", "streaming", framework);
+  }
   await copy("**", root, {
     parents: true,
     cwd: templatePath,
@@ -401,23 +406,42 @@ export const installPythonTemplate = async ({
       cwd: path.join(compPath, "services", "python"),
     });
   }
-
-  if (template === "streaming") {
-    // For the streaming template only:
+  // Copy engine code
+  if (template === "streaming" || template === "multiagent") {
     // Select and copy engine code based on data sources and tools
     let engine;
-    if (dataSources.length > 0 && (!tools || tools.length === 0)) {
-      console.log("\nNo tools selected - use optimized context chat engine\n");
-      engine = "chat";
-    } else {
+    // Multiagent always uses agent engine
+    if (template === "multiagent") {
       engine = "agent";
+    } else {
+      // For streaming, use chat engine by default
+      // Unless tools are selected, in which case use agent engine
+      if (dataSources.length > 0 && (!tools || tools.length === 0)) {
+        console.log(
+          "\nNo tools selected - use optimized context chat engine\n",
+        );
+        engine = "chat";
+      } else {
+        engine = "agent";
+      }
     }
+
+    // Copy engine code
     await copy("**", enginePath, {
       parents: true,
       cwd: path.join(compPath, "engines", "python", engine),
     });
   }
 
+  if (template === "multiagent") {
+    // Copy multi-agent code
+    await copy("**", path.join(root), {
+      parents: true,
+      cwd: path.join(compPath, "multiagent", "python"),
+      rename: assetRelocator,
+    });
+  }
+
   console.log("Adding additional dependencies");
 
   const addOnDependencies = getAdditionalDependencies(
diff --git a/helpers/tools.ts b/helpers/tools.ts
index a635e3fd1f38e1eb38d6476df568ef245131d0a6..97bde8b619af2a00f7d486b7eeeb7a1ac54e3c5c 100644
--- a/helpers/tools.ts
+++ b/helpers/tools.ts
@@ -110,6 +110,29 @@ For better results, you can specify the region parameter to get results from a s
       },
     ],
   },
+  {
+    display: "Document generator",
+    name: "document_generator",
+    supportedFrameworks: ["fastapi", "nextjs", "express"],
+    dependencies: [
+      {
+        name: "xhtml2pdf",
+        version: "^0.2.14",
+      },
+      {
+        name: "markdown",
+        version: "^3.7",
+      },
+    ],
+    type: ToolType.LOCAL,
+    envVars: [
+      {
+        name: TOOL_SYSTEM_PROMPT_ENV_VAR,
+        description: "System prompt for document generator tool.",
+        value: `If user request for a report or a post, use document generator tool to create a file and reply with the link to the file.`,
+      },
+    ],
+  },
   {
     display: "Code Interpreter",
     name: "interpreter",
diff --git a/helpers/typescript.ts b/helpers/typescript.ts
index ffebae4a391d565b4e1f1c15e8a34c732284e259..90d2079e89a0b6ee5cdea47af52d2f512bc7a28b 100644
--- a/helpers/typescript.ts
+++ b/helpers/typescript.ts
@@ -157,7 +157,10 @@ export const installTSTemplate = async ({
   // Select and copy engine code based on data sources and tools
   let engine;
   tools = tools ?? [];
-  if (dataSources.length > 0 && tools.length === 0) {
+  // multiagent template always uses agent engine
+  if (template === "multiagent") {
+    engine = "agent";
+  } else if (dataSources.length > 0 && tools.length === 0) {
     console.log("\nNo tools selected - use optimized context chat engine\n");
     engine = "chat";
   } else {
diff --git a/questions.ts b/questions.ts
index 3619447cdb9e4aa23c393e6a5c588e343ca3bcd2..81061f83f7bf67d1b1ba6a675efd863cccdcc54e 100644
--- a/questions.ts
+++ b/questions.ts
@@ -141,12 +141,10 @@ export const getDataSourceChoices = (
     });
   }
   if (selectedDataSource === undefined || selectedDataSource.length === 0) {
-    if (template !== "multiagent") {
-      choices.push({
-        title: "No datasource",
-        value: "none",
-      });
-    }
+    choices.push({
+      title: "No datasource",
+      value: "none",
+    });
     choices.push({
       title:
         process.platform !== "linux"
@@ -734,8 +732,10 @@ export const askQuestions = async (
     }
   }
 
-  if (!program.tools && program.template === "streaming") {
-    // TODO: allow to select tools also for multi-agent framework
+  if (
+    !program.tools &&
+    (program.template === "streaming" || program.template === "multiagent")
+  ) {
     if (ciInfo.isCI) {
       program.tools = getPrefOrDefault("tools");
     } else {
diff --git a/templates/components/engines/python/agent/engine.py b/templates/components/engines/python/agent/engine.py
index 22a30d0e98862adb2cb44ae448aae94b70842e37..c71d370472439f228cbee53ccd3a8564beffdab3 100644
--- a/templates/components/engines/python/agent/engine.py
+++ b/templates/components/engines/python/agent/engine.py
@@ -8,7 +8,7 @@ from llama_index.core.settings import Settings
 from llama_index.core.tools.query_engine import QueryEngineTool
 
 
-def get_chat_engine(filters=None, params=None, event_handlers=None):
+def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
     system_prompt = os.getenv("SYSTEM_PROMPT")
     top_k = int(os.getenv("TOP_K", 0))
     tools = []
diff --git a/templates/components/engines/python/agent/tools/__init__.py b/templates/components/engines/python/agent/tools/__init__.py
index f24d988dbdb91ad6d8527c9d95ca18d3604d9d5d..6b2184328b966a7884fe413a2e9016bcdbbb9b4c 100644
--- a/templates/components/engines/python/agent/tools/__init__.py
+++ b/templates/components/engines/python/agent/tools/__init__.py
@@ -1,8 +1,9 @@
+import importlib
 import os
+
 import yaml
-import importlib
-from llama_index.core.tools.tool_spec.base import BaseToolSpec
 from llama_index.core.tools.function_tool import FunctionTool
+from llama_index.core.tools.tool_spec.base import BaseToolSpec
 
 
 class ToolType:
@@ -40,14 +41,26 @@ class ToolFactory:
             raise ValueError(f"Failed to load tool {tool_name}: {e}")
 
     @staticmethod
-    def from_env() -> list[FunctionTool]:
-        tools = []
+    def from_env(
+        map_result: bool = False,
+    ) -> list[FunctionTool] | dict[str, FunctionTool]:
+        """
+        Load tools from the configured file.
+        Params:
+            - use_map: if True, return map of tool name and the tool itself
+        """
+        if map_result:
+            tools = {}
+        else:
+            tools = []
         if os.path.exists("config/tools.yaml"):
             with open("config/tools.yaml", "r") as f:
                 tool_configs = yaml.safe_load(f)
                 for tool_type, config_entries in tool_configs.items():
                     for tool_name, config in config_entries.items():
-                        tools.extend(
-                            ToolFactory.load_tools(tool_type, tool_name, config)
-                        )
+                        tool = ToolFactory.load_tools(tool_type, tool_name, config)
+                        if map_result:
+                            tools[tool_name] = tool
+                        else:
+                            tools.extend(tool)
         return tools
diff --git a/templates/components/engines/python/agent/tools/document_generator.py b/templates/components/engines/python/agent/tools/document_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..5609f1467ab9dc59ebbf774677b2d462611ab570
--- /dev/null
+++ b/templates/components/engines/python/agent/tools/document_generator.py
@@ -0,0 +1,229 @@
+import logging
+import os
+import re
+from enum import Enum
+from io import BytesIO
+
+from llama_index.core.tools.function_tool import FunctionTool
+
+OUTPUT_DIR = "output/tools"
+
+
+class DocumentType(Enum):
+    PDF = "pdf"
+    HTML = "html"
+
+
+COMMON_STYLES = """
+body {
+    font-family: Arial, sans-serif;
+    line-height: 1.3;
+    color: #333;
+}
+h1, h2, h3, h4, h5, h6 {
+    margin-top: 1em;
+    margin-bottom: 0.5em;
+}
+p {
+    margin-bottom: 0.7em;
+}
+code {
+    background-color: #f4f4f4;
+    padding: 2px 4px;
+    border-radius: 4px;
+}
+pre {
+    background-color: #f4f4f4;
+    padding: 10px;
+    border-radius: 4px;
+    overflow-x: auto;
+}
+table {
+    border-collapse: collapse;
+    width: 100%;
+    margin-bottom: 1em;
+}
+th, td {
+    border: 1px solid #ddd;
+    padding: 8px;
+    text-align: left;
+}
+th {
+    background-color: #f2f2f2;
+    font-weight: bold;
+}
+"""
+
+HTML_SPECIFIC_STYLES = """
+body {
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 20px;
+}
+"""
+
+PDF_SPECIFIC_STYLES = """
+@page {
+    size: letter;
+    margin: 2cm;
+}
+body {
+    font-size: 11pt;
+}
+h1 { font-size: 18pt; }
+h2 { font-size: 16pt; }
+h3 { font-size: 14pt; }
+h4, h5, h6 { font-size: 12pt; }
+pre, code {
+    font-family: Courier, monospace;
+    font-size: 0.9em;
+}
+"""
+
+HTML_TEMPLATE = """
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <style>
+        {common_styles}
+        {specific_styles}
+    </style>
+</head>
+<body>
+    {content}
+</body>
+</html>
+"""
+
+
+class DocumentGenerator:
+    @classmethod
+    def _generate_html_content(cls, original_content: str) -> str:
+        """
+        Generate HTML content from the original markdown content.
+        """
+        try:
+            import markdown
+        except ImportError:
+            raise ImportError(
+                "Failed to import required modules. Please install markdown."
+            )
+
+        # Convert markdown to HTML with fenced code and table extensions
+        html_content = markdown.markdown(
+            original_content, extensions=["fenced_code", "tables"]
+        )
+        return html_content
+
+    @classmethod
+    def _generate_pdf(cls, html_content: str) -> BytesIO:
+        """
+        Generate a PDF from the HTML content.
+        """
+        try:
+            from xhtml2pdf import pisa
+        except ImportError:
+            raise ImportError(
+                "Failed to import required modules. Please install xhtml2pdf."
+            )
+
+        pdf_html = HTML_TEMPLATE.format(
+            common_styles=COMMON_STYLES,
+            specific_styles=PDF_SPECIFIC_STYLES,
+            content=html_content,
+        )
+
+        buffer = BytesIO()
+        pdf = pisa.pisaDocument(
+            BytesIO(pdf_html.encode("UTF-8")), buffer, encoding="UTF-8"
+        )
+
+        if pdf.err:
+            logging.error(f"PDF generation failed: {pdf.err}")
+            raise ValueError("PDF generation failed")
+
+        buffer.seek(0)
+        return buffer
+
+    @classmethod
+    def _generate_html(cls, html_content: str) -> str:
+        """
+        Generate a complete HTML document with the given HTML content.
+        """
+        return HTML_TEMPLATE.format(
+            common_styles=COMMON_STYLES,
+            specific_styles=HTML_SPECIFIC_STYLES,
+            content=html_content,
+        )
+
+    @classmethod
+    def generate_document(
+        cls, original_content: str, document_type: str, file_name: str
+    ) -> str:
+        """
+        To generate document as PDF or HTML file.
+        Parameters:
+            original_content: str (markdown style)
+            document_type: str (pdf or html) specify the type of the file format based on the use case
+            file_name: str (name of the document file) must be a valid file name, no extensions needed
+        Returns:
+            str (URL to the document file): A file URL ready to serve.
+        """
+        try:
+            document_type = DocumentType(document_type.lower())
+        except ValueError:
+            raise ValueError(
+                f"Invalid document type: {document_type}. Must be 'pdf' or 'html'."
+            )
+        # Always generate html content first
+        html_content = cls._generate_html_content(original_content)
+
+        # Based on the type of document, generate the corresponding file
+        if document_type == DocumentType.PDF:
+            content = cls._generate_pdf(html_content)
+            file_extension = "pdf"
+        elif document_type == DocumentType.HTML:
+            content = BytesIO(cls._generate_html(html_content).encode("utf-8"))
+            file_extension = "html"
+        else:
+            raise ValueError(f"Unexpected document type: {document_type}")
+
+        file_name = cls._validate_file_name(file_name)
+        file_path = os.path.join(OUTPUT_DIR, f"{file_name}.{file_extension}")
+
+        cls._write_to_file(content, file_path)
+
+        file_url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}"
+        return file_url
+
+    @staticmethod
+    def _write_to_file(content: BytesIO, file_path: str):
+        """
+        Write the content to a file.
+        """
+        try:
+            os.makedirs(os.path.dirname(file_path), exist_ok=True)
+            with open(file_path, "wb") as file:
+                file.write(content.getvalue())
+        except Exception as e:
+            raise e
+
+    @staticmethod
+    def _validate_file_name(file_name: str) -> str:
+        """
+        Validate the file name.
+        """
+        # Don't allow directory traversal
+        if os.path.isabs(file_name):
+            raise ValueError("File name is not allowed.")
+        # Don't allow special characters
+        if re.match(r"^[a-zA-Z0-9_.-]+$", file_name):
+            return file_name
+        else:
+            raise ValueError("File name is not allowed to contain special characters.")
+
+
+def get_tools(**kwargs):
+    return [FunctionTool.from_defaults(DocumentGenerator.generate_document)]
diff --git a/templates/components/engines/python/agent/tools/duckduckgo.py b/templates/components/engines/python/agent/tools/duckduckgo.py
index b63612a7755f9a5876949244b3fd715f5b34f94b..ec0f633267bc5c3cd4dc9d78840cc3c6d61ca4f6 100644
--- a/templates/components/engines/python/agent/tools/duckduckgo.py
+++ b/templates/components/engines/python/agent/tools/duckduckgo.py
@@ -32,5 +32,37 @@ def duckduckgo_search(
     return results
 
 
+def duckduckgo_image_search(
+    query: str,
+    region: str = "wt-wt",
+    max_results: int = 10,
+):
+    """
+    Use this function to search for images in DuckDuckGo.
+    Args:
+        query (str): The query to search in DuckDuckGo.
+        region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
+        max_results Optional(int): The maximum number of results to be returned. Default is 10.
+    """
+    try:
+        from duckduckgo_search import DDGS
+    except ImportError:
+        raise ImportError(
+            "duckduckgo_search package is required to use this function."
+            "Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
+        )
+    params = {
+        "keywords": query,
+        "region": region,
+        "max_results": max_results,
+    }
+    with DDGS() as ddg:
+        results = list(ddg.images(**params))
+    return results
+
+
 def get_tools(**kwargs):
-    return [FunctionTool.from_defaults(duckduckgo_search)]
+    return [
+        FunctionTool.from_defaults(duckduckgo_search),
+        FunctionTool.from_defaults(duckduckgo_image_search),
+    ]
diff --git a/templates/components/engines/python/agent/tools/img_gen.py b/templates/components/engines/python/agent/tools/img_gen.py
index 966e95d0d09d1fcf5b18d1ff979dbd5e9cbd5723..8c2ae7bc042cc04e146f608cfe1cea42dff558fb 100644
--- a/templates/components/engines/python/agent/tools/img_gen.py
+++ b/templates/components/engines/python/agent/tools/img_gen.py
@@ -1,10 +1,11 @@
+import logging
 import os
 import uuid
-import logging
-import requests
 from typing import Optional
-from pydantic import BaseModel, Field
+
+import requests
 from llama_index.core.tools import FunctionTool
+from pydantic import BaseModel, Field
 
 logger = logging.getLogger(__name__)
 
@@ -26,7 +27,7 @@ class ImageGeneratorToolOutput(BaseModel):
 
 class ImageGeneratorTool:
     _IMG_OUTPUT_FORMAT = "webp"
-    _IMG_OUTPUT_DIR = "output/tool"
+    _IMG_OUTPUT_DIR = "output/tools"
     _IMG_GEN_API = "https://api.stability.ai/v2beta/stable-image/generate/core"
 
     def __init__(self, api_key: str = None):
diff --git a/templates/components/engines/python/agent/tools/interpreter.py b/templates/components/engines/python/agent/tools/interpreter.py
index 8e701c58f2bf438a58aae5d50964c1b78918e8a9..0f4c10b95e51ec4d7722ed79f5edb9010f4349af 100644
--- a/templates/components/engines/python/agent/tools/interpreter.py
+++ b/templates/components/engines/python/agent/tools/interpreter.py
@@ -1,13 +1,13 @@
-import os
-import logging
 import base64
+import logging
+import os
 import uuid
-from pydantic import BaseModel
-from typing import List, Dict, Optional
-from llama_index.core.tools import FunctionTool
+from typing import Dict, List, Optional
+
 from e2b_code_interpreter import CodeInterpreter
 from e2b_code_interpreter.models import Logs
-
+from llama_index.core.tools import FunctionTool
+from pydantic import BaseModel
 
 logger = logging.getLogger(__name__)
 
@@ -26,7 +26,7 @@ class E2BToolOutput(BaseModel):
 
 
 class E2BCodeInterpreter:
-    output_dir = "output/tool"
+    output_dir = "output/tools"
 
     def __init__(self, api_key: str = None):
         if api_key is None:
diff --git a/templates/components/engines/python/chat/engine.py b/templates/components/engines/python/chat/engine.py
index cb7e00827d9195898de2d675935acd220f59cd07..83c73cc65bc5cf3ae7160bfad4f2aca5ff6fdf84 100644
--- a/templates/components/engines/python/chat/engine.py
+++ b/templates/components/engines/python/chat/engine.py
@@ -9,7 +9,7 @@ from llama_index.core.memory import ChatMemoryBuffer
 from llama_index.core.settings import Settings
 
 
-def get_chat_engine(filters=None, params=None, event_handlers=None):
+def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
     system_prompt = os.getenv("SYSTEM_PROMPT")
     citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
     top_k = int(os.getenv("TOP_K", 0))
diff --git a/templates/components/engines/typescript/agent/tools/document-generator.ts b/templates/components/engines/typescript/agent/tools/document-generator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b630db2c5657a3c6d4d0a6c496d9791839157802
--- /dev/null
+++ b/templates/components/engines/typescript/agent/tools/document-generator.ts
@@ -0,0 +1,142 @@
+import { JSONSchemaType } from "ajv";
+import { BaseTool, ToolMetadata } from "llamaindex";
+import { marked } from "marked";
+import path from "node:path";
+import { saveDocument } from "../../llamaindex/documents/helper";
+
+const OUTPUT_DIR = "output/tools";
+
+type DocumentParameter = {
+  originalContent: string;
+  fileName: string;
+};
+
+const DEFAULT_METADATA: ToolMetadata<JSONSchemaType<DocumentParameter>> = {
+  name: "document_generator",
+  description:
+    "Generate HTML document from markdown content. Return a file url to the document",
+  parameters: {
+    type: "object",
+    properties: {
+      originalContent: {
+        type: "string",
+        description: "The original markdown content to convert.",
+      },
+      fileName: {
+        type: "string",
+        description: "The name of the document file (without extension).",
+      },
+    },
+    required: ["originalContent", "fileName"],
+  },
+};
+
+const COMMON_STYLES = `
+  body {
+    font-family: Arial, sans-serif;
+    line-height: 1.3;
+    color: #333;
+  }
+  h1, h2, h3, h4, h5, h6 {
+    margin-top: 1em;
+    margin-bottom: 0.5em;
+  }
+  p {
+    margin-bottom: 0.7em;
+  }
+  code {
+    background-color: #f4f4f4;
+    padding: 2px 4px;
+    border-radius: 4px;
+  }
+  pre {
+    background-color: #f4f4f4;
+    padding: 10px;
+    border-radius: 4px;
+    overflow-x: auto;
+  }
+  table {
+    border-collapse: collapse;
+    width: 100%;
+    margin-bottom: 1em;
+  }
+  th, td {
+    border: 1px solid #ddd;
+    padding: 8px;
+    text-align: left;
+  }
+  th {
+    background-color: #f2f2f2;
+    font-weight: bold;
+  }
+  img {
+    max-width: 90%;
+    height: auto;
+    display: block;
+    margin: 1em auto;
+    border-radius: 10px;
+  }
+`;
+
+const HTML_SPECIFIC_STYLES = `
+  body {
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 20px;
+  }
+`;
+
+const HTML_TEMPLATE = `
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <style>
+        ${COMMON_STYLES}
+        ${HTML_SPECIFIC_STYLES}
+    </style>
+</head>
+<body>
+    {{content}}
+</body>
+</html>
+`;
+
+export interface DocumentGeneratorParams {
+  metadata?: ToolMetadata<JSONSchemaType<DocumentParameter>>;
+}
+
+export class DocumentGenerator implements BaseTool<DocumentParameter> {
+  metadata: ToolMetadata<JSONSchemaType<DocumentParameter>>;
+
+  constructor(params: DocumentGeneratorParams) {
+    this.metadata = params.metadata ?? DEFAULT_METADATA;
+  }
+
+  private static async generateHtmlContent(
+    originalContent: string,
+  ): Promise<string> {
+    return await marked(originalContent);
+  }
+
+  private static generateHtmlDocument(htmlContent: string): string {
+    return HTML_TEMPLATE.replace("{{content}}", htmlContent);
+  }
+
+  async call(input: DocumentParameter): Promise<string> {
+    const { originalContent, fileName } = input;
+
+    const htmlContent =
+      await DocumentGenerator.generateHtmlContent(originalContent);
+    const fileContent = DocumentGenerator.generateHtmlDocument(htmlContent);
+
+    const filePath = path.join(OUTPUT_DIR, `${fileName}.html`);
+
+    return `URL: ${await saveDocument(filePath, fileContent)}`;
+  }
+}
+
+export function getTools(): BaseTool[] {
+  return [new DocumentGenerator({})];
+}
diff --git a/templates/components/engines/typescript/agent/tools/duckduckgo.ts b/templates/components/engines/typescript/agent/tools/duckduckgo.ts
index 19423e35314af2e3f237b24b5346b21576ee3bc1..419ff90acfe7366987432245e6a2e0757adc451f 100644
--- a/templates/components/engines/typescript/agent/tools/duckduckgo.ts
+++ b/templates/components/engines/typescript/agent/tools/duckduckgo.ts
@@ -5,15 +5,19 @@ import { BaseTool, ToolMetadata } from "llamaindex";
 export type DuckDuckGoParameter = {
   query: string;
   region?: string;
+  maxResults?: number;
 };
 
 export type DuckDuckGoToolParams = {
   metadata?: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
 };
 
-const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>> = {
-  name: "duckduckgo",
-  description: "Use this function to search for any query in DuckDuckGo.",
+const DEFAULT_SEARCH_METADATA: ToolMetadata<
+  JSONSchemaType<DuckDuckGoParameter>
+> = {
+  name: "duckduckgo_search",
+  description:
+    "Use this function to search for information (only text) in the internet using DuckDuckGo.",
   parameters: {
     type: "object",
     properties: {
@@ -27,6 +31,12 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>> = {
           "Optional, The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...",
         nullable: true,
       },
+      maxResults: {
+        type: "number",
+        description:
+          "Optional, The maximum number of results to be returned. Default is 10.",
+        nullable: true,
+      },
     },
     required: ["query"],
   },
@@ -42,15 +52,18 @@ export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
   metadata: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
 
   constructor(params: DuckDuckGoToolParams) {
-    this.metadata = params.metadata ?? DEFAULT_META_DATA;
+    this.metadata = params.metadata ?? DEFAULT_SEARCH_METADATA;
   }
 
   async call(input: DuckDuckGoParameter) {
-    const { query, region } = input;
+    const { query, region, maxResults = 10 } = input;
     const options = region ? { region } : {};
+    // Temporarily sleep to reduce overloading the DuckDuckGo
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+
     const searchResults = await search(query, options);
 
-    return searchResults.results.map((result) => {
+    return searchResults.results.slice(0, maxResults).map((result) => {
       return {
         title: result.title,
         description: result.description,
@@ -59,3 +72,7 @@ export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
     });
   }
 }
+
+export function getTools() {
+  return [new DuckDuckGoSearchTool({})];
+}
diff --git a/templates/components/engines/typescript/agent/tools/img-gen.ts b/templates/components/engines/typescript/agent/tools/img-gen.ts
index 05cc12aba303e1cf72d4f7ca72aa5eb30cec0774..d24d5567bc870bc7bbe2666b2f04e83b28ee10e5 100644
--- a/templates/components/engines/typescript/agent/tools/img-gen.ts
+++ b/templates/components/engines/typescript/agent/tools/img-gen.ts
@@ -37,7 +37,7 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>> = {
 
 export class ImgGeneratorTool implements BaseTool<ImgGeneratorParameter> {
   readonly IMG_OUTPUT_FORMAT = "webp";
-  readonly IMG_OUTPUT_DIR = "output/tool";
+  readonly IMG_OUTPUT_DIR = "output/tools";
   readonly IMG_GEN_API =
     "https://api.stability.ai/v2beta/stable-image/generate/core";
 
diff --git a/templates/components/engines/typescript/agent/tools/index.ts b/templates/components/engines/typescript/agent/tools/index.ts
index c442a315d5e2d3aba915e7be73d9949c1e6256d9..b29af04843bb54108b05a8c0d6c8758c270c6f48 100644
--- a/templates/components/engines/typescript/agent/tools/index.ts
+++ b/templates/components/engines/typescript/agent/tools/index.ts
@@ -1,5 +1,9 @@
 import { BaseToolWithCall } from "llamaindex";
 import { ToolsFactory } from "llamaindex/tools/ToolsFactory";
+import {
+  DocumentGenerator,
+  DocumentGeneratorParams,
+} from "./document-generator";
 import { DuckDuckGoSearchTool, DuckDuckGoToolParams } from "./duckduckgo";
 import { ImgGeneratorTool, ImgGeneratorToolParams } from "./img-gen";
 import { InterpreterTool, InterpreterToolParams } from "./interpreter";
@@ -43,6 +47,9 @@ const toolFactory: Record<string, ToolCreator> = {
   img_gen: async (config: unknown) => {
     return [new ImgGeneratorTool(config as ImgGeneratorToolParams)];
   },
+  document_generator: async (config: unknown) => {
+    return [new DocumentGenerator(config as DocumentGeneratorParams)];
+  },
 };
 
 async function createLocalTools(
diff --git a/templates/components/engines/typescript/agent/tools/interpreter.ts b/templates/components/engines/typescript/agent/tools/interpreter.ts
index 6870e5487bd2066b269aedf6642dce89ac606fe4..24573c2051d28fe2b64cfe9591af1d1bbfcf0045 100644
--- a/templates/components/engines/typescript/agent/tools/interpreter.ts
+++ b/templates/components/engines/typescript/agent/tools/interpreter.ts
@@ -56,7 +56,7 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
 };
 
 export class InterpreterTool implements BaseTool<InterpreterParameter> {
-  private readonly outputDir = "output/tool";
+  private readonly outputDir = "output/tools";
   private apiKey?: string;
   private fileServerURLPrefix?: string;
   metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
diff --git a/templates/types/multiagent/fastapi/README-template.md b/templates/components/multiagent/python/README-template.md
similarity index 100%
rename from templates/types/multiagent/fastapi/README-template.md
rename to templates/components/multiagent/python/README-template.md
diff --git a/templates/types/multiagent/fastapi/app/agents/multi.py b/templates/components/multiagent/python/app/agents/multi.py
similarity index 89%
rename from templates/types/multiagent/fastapi/app/agents/multi.py
rename to templates/components/multiagent/python/app/agents/multi.py
index 9a04a3da89010893fe7fd53d8a2cbb574a3086bb..1150385020d0f53f224474f403c3f837ca4ff36b 100644
--- a/templates/types/multiagent/fastapi/app/agents/multi.py
+++ b/templates/components/multiagent/python/app/agents/multi.py
@@ -8,7 +8,7 @@ from app.agents.single import (
 )
 from llama_index.core.tools.types import ToolMetadata, ToolOutput
 from llama_index.core.tools.utils import create_schema_from_function
-from llama_index.core.workflow import Context, Workflow
+from llama_index.core.workflow import Context, StopEvent, Workflow
 
 
 class AgentCallTool(ContextAwareTool):
@@ -25,7 +25,11 @@ class AgentCallTool(ContextAwareTool):
             name=name,
             description=(
                 f"Use this tool to delegate a sub task to the {agent.name} agent."
-                + (f" The agent is an {agent.role}." if agent.role else "")
+                + (
+                    f" The agent is an {agent.description}."
+                    if agent.description
+                    else ""
+                )
             ),
             fn_schema=fn_schema,
         )
@@ -35,7 +39,8 @@ class AgentCallTool(ContextAwareTool):
         handler = self.agent.run(input=input)
         # bubble all events while running the agent to the calling agent
         async for ev in handler.stream_events():
-            ctx.write_event_to_stream(ev)
+            if type(ev) is not StopEvent:
+                ctx.write_event_to_stream(ev)
         ret: AgentRunResult = await handler
         response = ret.response.message.content
         return ToolOutput(
diff --git a/templates/types/multiagent/fastapi/app/agents/planner.py b/templates/components/multiagent/python/app/agents/planner.py
similarity index 86%
rename from templates/types/multiagent/fastapi/app/agents/planner.py
rename to templates/components/multiagent/python/app/agents/planner.py
index 07306f2ba7eb80e2dee6d4b29eb2d48a56f6b3de..ce9ba01ee7b85e9285058bc68af2014a88e7cb29 100644
--- a/templates/types/multiagent/fastapi/app/agents/planner.py
+++ b/templates/components/multiagent/python/app/agents/planner.py
@@ -11,6 +11,7 @@ from llama_index.core.agent.runner.planner import (
     SubTask,
 )
 from llama_index.core.bridge.pydantic import ValidationError
+from llama_index.core.chat_engine.types import ChatMessage
 from llama_index.core.llms.function_calling import FunctionCallingLLM
 from llama_index.core.prompts import PromptTemplate
 from llama_index.core.settings import Settings
@@ -24,6 +25,18 @@ from llama_index.core.workflow import (
     step,
 )
 
+INITIAL_PLANNER_PROMPT = """\
+Think step-by-step. Given a conversation, set of tools and a user request. Your responsibility is to create a plan to complete the task.
+The plan must adapt with the user request and the conversation. It's fine to just start with needed tasks first and asking user for the next step approval.
+
+The tools available are:
+{tools_str}
+
+Conversation: {chat_history}
+
+Overall Task: {task}
+"""
+
 
 class ExecutePlanEvent(Event):
     pass
@@ -62,14 +75,21 @@ class StructuredPlannerAgent(Workflow):
         tools: List[BaseTool] | None = None,
         timeout: float = 360.0,
         refine_plan: bool = False,
+        chat_history: Optional[List[ChatMessage]] = None,
         **kwargs: Any,
     ) -> None:
         super().__init__(*args, timeout=timeout, **kwargs)
         self.name = name
         self.refine_plan = refine_plan
+        self.chat_history = chat_history
 
         self.tools = tools or []
-        self.planner = Planner(llm=llm, tools=self.tools, verbose=self._verbose)
+        self.planner = Planner(
+            llm=llm,
+            tools=self.tools,
+            initial_plan_prompt=INITIAL_PLANNER_PROMPT,
+            verbose=self._verbose,
+        )
         # The executor is keeping the memory of all tool calls and decides to call the right tool for the task
         self.executor = FunctionCallingAgent(
             name="executor",
@@ -89,7 +109,9 @@ class StructuredPlannerAgent(Workflow):
         ctx.data["streaming"] = getattr(ev, "streaming", False)
         ctx.data["task"] = ev.input
 
-        plan_id, plan = await self.planner.create_plan(input=ev.input)
+        plan_id, plan = await self.planner.create_plan(
+            input=ev.input, chat_history=self.chat_history
+        )
         ctx.data["act_plan_id"] = plan_id
 
         # inform about the new plan
@@ -106,11 +128,12 @@ class StructuredPlannerAgent(Workflow):
             ctx.data["act_plan_id"]
         )
 
-        ctx.data["num_sub_tasks"] = len(upcoming_sub_tasks)
-        # send an event per sub task
-        events = [SubTaskEvent(sub_task=sub_task) for sub_task in upcoming_sub_tasks]
-        for event in events:
-            ctx.send_event(event)
+        if upcoming_sub_tasks:
+            # Execute only the first sub-task
+            # otherwise the executor will get over-lapping messages
+            # alternatively, we could use one executor for all sub tasks
+            next_sub_task = upcoming_sub_tasks[0]
+            return SubTaskEvent(sub_task=next_sub_task)
 
         return None
 
@@ -120,7 +143,7 @@ class StructuredPlannerAgent(Workflow):
     ) -> SubTaskResultEvent:
         if self._verbose:
             print(f"=== Executing sub task: {ev.sub_task.name} ===")
-        is_last_tasks = ctx.data["num_sub_tasks"] == self.get_remaining_subtasks(ctx)
+        is_last_tasks = self.get_remaining_subtasks(ctx) == 1
         # TODO: streaming only works without plan refining
         streaming = is_last_tasks and ctx.data["streaming"] and not self.refine_plan
         handler = self.executor.run(
@@ -142,22 +165,17 @@ class StructuredPlannerAgent(Workflow):
     async def gather_results(
         self, ctx: Context, ev: SubTaskResultEvent
     ) -> ExecutePlanEvent | StopEvent:
-        # wait for all sub tasks to finish
-        num_sub_tasks = ctx.data["num_sub_tasks"]
-        results = ctx.collect_events(ev, [SubTaskResultEvent] * num_sub_tasks)
-        if results is None:
-            return None
+        result = ev
 
         upcoming_sub_tasks = self.get_upcoming_sub_tasks(ctx)
         # if no more tasks to do, stop workflow and send result of last step
         if upcoming_sub_tasks == 0:
-            return StopEvent(result=results[-1].result)
+            return StopEvent(result=result.result)
 
         if self.refine_plan:
-            # store all results for refining the plan
+            # store the result for refining the plan
             ctx.data["results"] = ctx.data.get("results", {})
-            for result in results:
-                ctx.data["results"][result.sub_task.name] = result.result
+            ctx.data["results"][result.sub_task.name] = result.result
 
             new_plan = await self.planner.refine_plan(
                 ctx.data["task"], ctx.data["act_plan_id"], ctx.data["results"]
@@ -213,7 +231,9 @@ class Planner:
             plan_refine_prompt = PromptTemplate(plan_refine_prompt)
         self.plan_refine_prompt = plan_refine_prompt
 
-    async def create_plan(self, input: str) -> Tuple[str, Plan]:
+    async def create_plan(
+        self, input: str, chat_history: Optional[List[ChatMessage]] = None
+    ) -> Tuple[str, Plan]:
         tools = self.tools
         tools_str = ""
         for tool in tools:
@@ -225,6 +245,7 @@ class Planner:
                 self.initial_plan_prompt,
                 tools_str=tools_str,
                 task=input,
+                chat_history=chat_history,
             )
         except (ValueError, ValidationError):
             if self.verbose:
diff --git a/templates/types/multiagent/fastapi/app/agents/single.py b/templates/components/multiagent/python/app/agents/single.py
similarity index 97%
rename from templates/types/multiagent/fastapi/app/agents/single.py
rename to templates/components/multiagent/python/app/agents/single.py
index b47662f8d928a33870e8220ecba3e16f979f6e11..a598bdf65306788ffa55b9af72fa41150769f687 100644
--- a/templates/types/multiagent/fastapi/app/agents/single.py
+++ b/templates/components/multiagent/python/app/agents/single.py
@@ -5,10 +5,8 @@ from llama_index.core.llms import ChatMessage, ChatResponse
 from llama_index.core.llms.function_calling import FunctionCallingLLM
 from llama_index.core.memory import ChatMemoryBuffer
 from llama_index.core.settings import Settings
-from llama_index.core.tools import ToolOutput, ToolSelection
+from llama_index.core.tools import FunctionTool, ToolOutput, ToolSelection
 from llama_index.core.tools.types import BaseTool
-from llama_index.core.tools import FunctionTool
-
 from llama_index.core.workflow import (
     Context,
     Event,
@@ -64,14 +62,14 @@ class FunctionCallingAgent(Workflow):
         timeout: float = 360.0,
         name: str,
         write_events: bool = True,
-        role: Optional[str] = None,
+        description: str | None = None,
         **kwargs: Any,
     ) -> None:
         super().__init__(*args, verbose=verbose, timeout=timeout, **kwargs)
         self.tools = tools or []
         self.name = name
-        self.role = role
         self.write_events = write_events
+        self.description = description
 
         if llm is None:
             llm = Settings.llm
diff --git a/templates/components/multiagent/python/app/api/routers/chat.py b/templates/components/multiagent/python/app/api/routers/chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..23135c8093c818a52ef49d6e487e8701e3acf3ce
--- /dev/null
+++ b/templates/components/multiagent/python/app/api/routers/chat.py
@@ -0,0 +1,46 @@
+import logging
+
+from app.api.routers.events import EventCallbackHandler
+from app.api.routers.models import (
+    ChatData,
+)
+from app.api.routers.vercel_response import VercelStreamResponse
+from app.engine import get_chat_engine
+from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
+
+chat_router = r = APIRouter()
+
+logger = logging.getLogger("uvicorn")
+
+
+@r.post("")
+async def chat(
+    request: Request,
+    data: ChatData,
+    background_tasks: BackgroundTasks,
+):
+    try:
+        last_message_content = data.get_last_message_content()
+        messages = data.get_history_messages(include_agent_messages=True)
+
+        event_handler = EventCallbackHandler()
+        # The chat API supports passing private document filters and chat params
+        # but agent workflow does not support them yet
+        # ignore chat params and use all documents for now
+        # TODO: generate filters based on doc_ids
+        # TODO: use chat params
+        engine = get_chat_engine(chat_history=messages)
+
+        event_handler = engine.run(input=last_message_content, streaming=True)
+        return VercelStreamResponse(
+            request=request,
+            chat_data=data,
+            event_handler=event_handler,
+            events=engine.stream_events(),
+        )
+    except Exception as e:
+        logger.exception("Error in chat engine", exc_info=True)
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=f"Error in chat engine: {e}",
+        ) from e
diff --git a/templates/types/multiagent/fastapi/app/api/routers/vercel_response.py b/templates/components/multiagent/python/app/api/routers/vercel_response.py
similarity index 66%
rename from templates/types/multiagent/fastapi/app/api/routers/vercel_response.py
rename to templates/components/multiagent/python/app/api/routers/vercel_response.py
index 29bcf852bc94e65816bbe9b71ebeb346bb3697dd..12082496ed2a48bc27ec3a5944dd6fbcfe50829f 100644
--- a/templates/types/multiagent/fastapi/app/api/routers/vercel_response.py
+++ b/templates/components/multiagent/python/app/api/routers/vercel_response.py
@@ -1,6 +1,6 @@
 import json
 import logging
-from asyncio import Task
+from abc import ABC
 from typing import AsyncGenerator, List
 
 from aiostream import stream
@@ -13,93 +13,80 @@ from fastapi.responses import StreamingResponse
 logger = logging.getLogger("uvicorn")
 
 
-class VercelStreamResponse(StreamingResponse):
+class VercelStreamResponse(StreamingResponse, ABC):
     """
-    Class to convert the response from the chat engine to the streaming format expected by Vercel
+    Base class to convert the response from the chat engine to the streaming format expected by Vercel
     """
 
     TEXT_PREFIX = "0:"
     DATA_PREFIX = "8:"
 
-    @classmethod
-    def convert_text(cls, token: str):
-        # Escape newlines and double quotes to avoid breaking the stream
-        token = json.dumps(token)
-        return f"{cls.TEXT_PREFIX}{token}\n"
+    def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs):
+        self.request = request
 
-    @classmethod
-    def convert_data(cls, data: dict):
-        data_str = json.dumps(data)
-        return f"{cls.DATA_PREFIX}[{data_str}]\n"
+        stream = self._create_stream(request, chat_data, *args, **kwargs)
+        content = self.content_generator(stream)
 
-    def __init__(
-        self,
-        request: Request,
-        task: Task[AgentRunResult | AsyncGenerator],
-        events: AsyncGenerator[AgentRunEvent, None],
-        chat_data: ChatData,
-        verbose: bool = True,
-    ):
-        content = VercelStreamResponse.content_generator(
-            request, task, events, chat_data, verbose
-        )
         super().__init__(content=content)
 
-    @classmethod
-    async def content_generator(
-        cls,
+    async def content_generator(self, stream):
+        is_stream_started = False
+
+        async with stream.stream() as streamer:
+            async for output in streamer:
+                if not is_stream_started:
+                    is_stream_started = True
+                    # Stream a blank message to start the stream
+                    yield self.convert_text("")
+
+                yield output
+
+                if await self.request.is_disconnected():
+                    break
+
+    def _create_stream(
+        self,
         request: Request,
-        task: Task[AgentRunResult | AsyncGenerator],
-        events: AsyncGenerator[AgentRunEvent, None],
         chat_data: ChatData,
+        event_handler: AgentRunResult | AsyncGenerator,
+        events: AsyncGenerator[AgentRunEvent, None],
         verbose: bool = True,
     ):
         # Yield the text response
         async def _chat_response_generator():
-            result = await task
+            result = await event_handler
             final_response = ""
 
             if isinstance(result, AgentRunResult):
                 for token in result.response.message.content:
                     final_response += token
-                    yield cls.convert_text(token)
+                    yield self.convert_text(token)
 
             if isinstance(result, AsyncGenerator):
                 async for token in result:
                     final_response += token.delta
-                    yield cls.convert_text(token.delta)
+                    yield self.convert_text(token.delta)
 
             # Generate next questions if next question prompt is configured
-            question_data = await cls._generate_next_questions(
+            question_data = await self._generate_next_questions(
                 chat_data.messages, final_response
             )
             if question_data:
-                yield cls.convert_data(question_data)
+                yield self.convert_data(question_data)
 
             # TODO: stream sources
 
         # Yield the events from the event handler
         async def _event_generator():
-            async for event in events():
-                event_response = cls._event_to_response(event)
+            async for event in events:
+                event_response = self._event_to_response(event)
                 if verbose:
                     logger.debug(event_response)
                 if event_response is not None:
-                    yield cls.convert_data(event_response)
+                    yield self.convert_data(event_response)
 
         combine = stream.merge(_chat_response_generator(), _event_generator())
-
-        is_stream_started = False
-        async with combine.stream() as streamer:
-            if not is_stream_started:
-                is_stream_started = True
-                # Stream a blank message to start the stream
-                yield cls.convert_text("")
-
-            async for output in streamer:
-                yield output
-                if await request.is_disconnected():
-                    break
+        return combine
 
     @staticmethod
     def _event_to_response(event: AgentRunEvent) -> dict:
@@ -108,6 +95,17 @@ class VercelStreamResponse(StreamingResponse):
             "data": {"agent": event.name, "text": event.msg},
         }
 
+    @classmethod
+    def convert_text(cls, token: str):
+        # Escape newlines and double quotes to avoid breaking the stream
+        token = json.dumps(token)
+        return f"{cls.TEXT_PREFIX}{token}\n"
+
+    @classmethod
+    def convert_data(cls, data: dict):
+        data_str = json.dumps(data)
+        return f"{cls.DATA_PREFIX}[{data_str}]\n"
+
     @staticmethod
     async def _generate_next_questions(chat_history: List[Message], response: str):
         questions = await NextQuestionSuggestion.suggest_next_questions(
diff --git a/templates/types/multiagent/fastapi/app/examples/factory.py b/templates/components/multiagent/python/app/engine/engine.py
similarity index 81%
rename from templates/types/multiagent/fastapi/app/examples/factory.py
rename to templates/components/multiagent/python/app/engine/engine.py
index 2a376a32770a2ebb0eefe364c4eeb72607734a96..e9563975d57a61267d25be404efd6460b135f29e 100644
--- a/templates/types/multiagent/fastapi/app/examples/factory.py
+++ b/templates/components/multiagent/python/app/engine/engine.py
@@ -1,20 +1,20 @@
 import logging
+import os
 from typing import List, Optional
+
 from app.examples.choreography import create_choreography
 from app.examples.orchestrator import create_orchestrator
 from app.examples.workflow import create_workflow
-
-
-from llama_index.core.workflow import Workflow
 from llama_index.core.chat_engine.types import ChatMessage
-
-
-import os
+from llama_index.core.workflow import Workflow
 
 logger = logging.getLogger("uvicorn")
 
 
-def create_agent(chat_history: Optional[List[ChatMessage]] = None) -> Workflow:
+def get_chat_engine(
+    chat_history: Optional[List[ChatMessage]] = None, **kwargs
+) -> Workflow:
+    # TODO: the EXAMPLE_TYPE could be passed as a chat config parameter?
     agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
     match agent_type:
         case "choreography":
diff --git a/templates/components/multiagent/python/app/examples/choreography.py b/templates/components/multiagent/python/app/examples/choreography.py
new file mode 100644
index 0000000000000000000000000000000000000000..13da60e53f820160a15ed75829c6d7b1172460df
--- /dev/null
+++ b/templates/components/multiagent/python/app/examples/choreography.py
@@ -0,0 +1,32 @@
+from textwrap import dedent
+from typing import List, Optional
+
+from app.agents.multi import AgentCallingAgent
+from app.agents.single import FunctionCallingAgent
+from app.examples.publisher import create_publisher
+from app.examples.researcher import create_researcher
+from llama_index.core.chat_engine.types import ChatMessage
+
+
+def create_choreography(chat_history: Optional[List[ChatMessage]] = None):
+    researcher = create_researcher(chat_history)
+    publisher = create_publisher(chat_history)
+    reviewer = FunctionCallingAgent(
+        name="reviewer",
+        description="expert in reviewing blog posts, needs a written post to review",
+        system_prompt="You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. If the post is good, you can say 'The post is good.'",
+        chat_history=chat_history,
+    )
+    return AgentCallingAgent(
+        name="writer",
+        agents=[researcher, reviewer, publisher],
+        description="expert in writing blog posts, needs researched information and images to write a blog post",
+        system_prompt=dedent("""
+            You are an expert in writing blog posts. You are given a task to write a blog post. Before starting to write the post, consult the researcher agent to get the information you need. Don't make up any information yourself.
+            After creating a draft for the post, send it to the reviewer agent to receive feedback and make sure to incorporate the feedback from the reviewer.
+            You can consult the reviewer and researcher a maximum of two times. Your output should contain only the blog post.
+            Finally, always request the publisher to create a document (PDF, HTML) and publish the blog post.
+        """),
+        # TODO: add chat_history support to AgentCallingAgent
+        # chat_history=chat_history,
+    )
diff --git a/templates/components/multiagent/python/app/examples/orchestrator.py b/templates/components/multiagent/python/app/examples/orchestrator.py
new file mode 100644
index 0000000000000000000000000000000000000000..8786dcd3f9231ed98be2391dc25d610ae3a1844f
--- /dev/null
+++ b/templates/components/multiagent/python/app/examples/orchestrator.py
@@ -0,0 +1,40 @@
+from textwrap import dedent
+from typing import List, Optional
+
+from app.agents.multi import AgentOrchestrator
+from app.agents.single import FunctionCallingAgent
+from app.examples.publisher import create_publisher
+from app.examples.researcher import create_researcher
+from llama_index.core.chat_engine.types import ChatMessage
+
+
+def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None):
+    researcher = create_researcher(chat_history)
+    writer = FunctionCallingAgent(
+        name="writer",
+        description="expert in writing blog posts, need information and images to write a post",
+        system_prompt=dedent("""
+            You are an expert in writing blog posts.
+            You are given a task to write a blog post. Do not make up any information yourself.
+            If you don't have the necessary information to write a blog post, reply "I need information about the topic to write the blog post".
+            If you need to use images, reply "I need images about the topic to write the blog post". Do not use any dummy images made up by you.
+            If you have all the information needed, write the blog post.
+        """),
+        chat_history=chat_history,
+    )
+    reviewer = FunctionCallingAgent(
+        name="reviewer",
+        description="expert in reviewing blog posts, needs a written blog post to review",
+        system_prompt=dedent("""
+            You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post and fix any issues found yourself. You must output a final blog post.
+            A post must include at least one valid image. If not, reply "I need images about the topic to write the blog post". An image URL starting with "example" or "your website" is not valid.
+            Especially check for logical inconsistencies and proofread the post for grammar and spelling errors.
+        """),
+        chat_history=chat_history,
+    )
+    publisher = create_publisher(chat_history)
+    return AgentOrchestrator(
+        agents=[writer, reviewer, researcher, publisher],
+        refine_plan=False,
+        chat_history=chat_history,
+    )
diff --git a/templates/components/multiagent/python/app/examples/publisher.py b/templates/components/multiagent/python/app/examples/publisher.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a170d88680d01b18523a5804ca78a24199fa244
--- /dev/null
+++ b/templates/components/multiagent/python/app/examples/publisher.py
@@ -0,0 +1,35 @@
+from textwrap import dedent
+from typing import List, Tuple
+
+from app.agents.single import FunctionCallingAgent
+from app.engine.tools import ToolFactory
+from llama_index.core.chat_engine.types import ChatMessage
+from llama_index.core.tools import FunctionTool
+
+
+def get_publisher_tools() -> Tuple[List[FunctionTool], str, str]:
+    tools = []
+    # Get configured tools from the tools.yaml file
+    configured_tools = ToolFactory.from_env(map_result=True)
+    if "document_generator" in configured_tools.keys():
+        tools.extend(configured_tools["document_generator"])
+        prompt_instructions = dedent("""
+            Normally, reply the blog post content to the user directly. 
+            But if user requested to generate a file, use the document_generator tool to generate the file and reply the link to the file.
+        """)
+        description = "Expert in publishing the blog post, able to publish the blog post in PDF or HTML format."
+    else:
+        prompt_instructions = "You don't have a tool to generate document. Please reply the content directly."
+        description = "Expert in publishing the blog post"
+    return tools, prompt_instructions, description
+
+
+def create_publisher(chat_history: List[ChatMessage]):
+    tools, prompt_instructions, description = get_publisher_tools()
+    return FunctionCallingAgent(
+        name="publisher",
+        tools=tools,
+        description=description,
+        system_prompt=prompt_instructions,
+        chat_history=chat_history,
+    )
diff --git a/templates/components/multiagent/python/app/examples/researcher.py b/templates/components/multiagent/python/app/examples/researcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..6efa70e9edc0ab23b25e1fbe6a1738a5fddeab05
--- /dev/null
+++ b/templates/components/multiagent/python/app/examples/researcher.py
@@ -0,0 +1,82 @@
+import os
+from textwrap import dedent
+from typing import List
+
+from app.agents.single import FunctionCallingAgent
+from app.engine.index import get_index
+from app.engine.tools import ToolFactory
+from llama_index.core.chat_engine.types import ChatMessage
+from llama_index.core.tools import QueryEngineTool, ToolMetadata
+
+
+def _create_query_engine_tool() -> QueryEngineTool:
+    """
+    Provide an agent worker that can be used to query the index.
+    """
+    index = get_index()
+    if index is None:
+        return None
+    top_k = int(os.getenv("TOP_K", 0))
+    query_engine = index.as_query_engine(
+        **({"similarity_top_k": top_k} if top_k != 0 else {})
+    )
+    return QueryEngineTool(
+        query_engine=query_engine,
+        metadata=ToolMetadata(
+            name="query_index",
+            description="""
+                Use this tool to retrieve information about the text corpus from the index.
+            """,
+        ),
+    )
+
+
+def _get_research_tools() -> QueryEngineTool:
+    """
+    Researcher take responsibility for retrieving information.
+    Try init wikipedia or duckduckgo tool if available.
+    """
+    tools = []
+    query_engine_tool = _create_query_engine_tool()
+    if query_engine_tool is not None:
+        tools.append(query_engine_tool)
+    researcher_tool_names = ["duckduckgo", "wikipedia.WikipediaToolSpec"]
+    configured_tools = ToolFactory.from_env(map_result=True)
+    for tool_name, tool in configured_tools.items():
+        if tool_name in researcher_tool_names:
+            tools.extend(tool)
+    return tools
+
+
+def create_researcher(chat_history: List[ChatMessage]):
+    """
+    Researcher is an agent that take responsibility for using tools to complete a given task.
+    """
+    tools = _get_research_tools()
+    return FunctionCallingAgent(
+        name="researcher",
+        tools=tools,
+        description="expert in retrieving any unknown content or searching for images from the internet",
+        system_prompt=dedent("""
+            You are a researcher agent. You are given a research task.
+            
+            If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
+            Otherwise, you must use tools to retrieve information or images needed for the task.
+
+            It's normal for the task to include some ambiguity. You must always think carefully about the context of the user's request to understand what are the main content needs to be retrieved.
+            Example:
+                Request: "Create a blog post about the history of the internet, write in English and publish in PDF format."
+                ->Though: The main content is "history of the internet", while "write in English and publish in PDF format" is a requirement for other agents.
+                Your task: Look for information in English about the history of the Internet.
+                This is not your task: Create a blog post or look for how to create a PDF.
+
+                Next request: "Publish the blog post in HTML format."
+                ->Though: User just asking for a format change, the previous content is still valid.
+                Your task: Return the previous content of the post to the writer. No need to do any research.
+                This is not your task: Look for how to create an HTML file.
+
+            If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
+            If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history."
+        """),
+        chat_history=chat_history,
+    )
diff --git a/templates/components/multiagent/python/app/examples/workflow.py b/templates/components/multiagent/python/app/examples/workflow.py
new file mode 100644
index 0000000000000000000000000000000000000000..9fffb45001e37aaef4dabe36f838a2235a3d950a
--- /dev/null
+++ b/templates/components/multiagent/python/app/examples/workflow.py
@@ -0,0 +1,250 @@
+from textwrap import dedent
+from typing import AsyncGenerator, List, Optional
+from llama_index.core.settings import Settings
+from llama_index.core.prompts import PromptTemplate
+
+from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
+from app.examples.publisher import create_publisher
+from app.examples.researcher import create_researcher
+from llama_index.core.chat_engine.types import ChatMessage
+from llama_index.core.workflow import (
+    Context,
+    Event,
+    StartEvent,
+    StopEvent,
+    Workflow,
+    step,
+)
+
+
+def create_workflow(chat_history: Optional[List[ChatMessage]] = None):
+    researcher = create_researcher(
+        chat_history=chat_history,
+    )
+    publisher = create_publisher(
+        chat_history=chat_history,
+    )
+    writer = FunctionCallingAgent(
+        name="writer",
+        description="expert in writing blog posts, need information and images to write a post.",
+        system_prompt=dedent(
+            """
+            You are an expert in writing blog posts.
+            You are given the task of writing a blog post based on research content provided by the researcher agent. Do not invent any information yourself. 
+            It's important to read the entire conversation history to write the blog post accurately.
+            If you receive a review from the reviewer, update the post according to the feedback and return the new post content.
+            If the user requests an update with new information but no research content is provided, you must respond with: "I don't have any research content to write about."
+            If the content is not valid (e.g., broken link, broken image, etc.), do not use it.
+            It's normal for the task to include some ambiguity, so you must define the user's initial request to write the post correctly.
+            If you update the post based on the reviewer's feedback, first explain what changes you made to the post, then provide the new post content. Do not include the reviewer's comments.
+            Example:
+                Task: "Here is the information I found about the history of the internet: 
+                Create a blog post about the history of the internet, write in English, and publish in PDF format."
+                -> Your task: Use the research content {...} to write a blog post in English.
+                -> This is not your task: Create a PDF
+                Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
+        """
+        ),
+        chat_history=chat_history,
+    )
+    reviewer = FunctionCallingAgent(
+        name="reviewer",
+        description="expert in reviewing blog posts, needs a written blog post to review.",
+        system_prompt=dedent(
+            """
+            You are an expert in reviewing blog posts.
+            You are given a task to review a blog post. As a reviewer, it's important that your review aligns with the user's request. Please focus on the user's request when reviewing the post.
+            Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement.
+            Furthermore, proofread the post for grammar and spelling errors.
+            Only if the post is good enough for publishing should you return 'The post is good.' In all other cases, return your review.
+            It's normal for the task to include some ambiguity, so you must define the user's initial request to review the post correctly.
+            Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
+            Example:
+                Task: "Create a blog post about the history of the internet, write in English and publish in PDF format."
+                -> Your task: Review whether the main content of the post is about the history of the internet and if it is written in English.
+                -> This is not your task: Create blog post, create PDF, write in English.
+        """
+        ),
+        chat_history=chat_history,
+    )
+    workflow = BlogPostWorkflow(
+        timeout=360, chat_history=chat_history
+    )  # Pass chat_history here
+    workflow.add_workflows(
+        researcher=researcher,
+        writer=writer,
+        reviewer=reviewer,
+        publisher=publisher,
+    )
+    return workflow
+
+
+class ResearchEvent(Event):
+    input: str
+
+
+class WriteEvent(Event):
+    input: str
+    is_good: bool = False
+
+
+class ReviewEvent(Event):
+    input: str
+
+
+class PublishEvent(Event):
+    input: str
+
+
+class BlogPostWorkflow(Workflow):
+    def __init__(
+        self, timeout: int = 360, chat_history: Optional[List[ChatMessage]] = None
+    ):
+        super().__init__(timeout=timeout)
+        self.chat_history = chat_history or []
+
+    @step()
+    async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent | PublishEvent:
+        # set streaming
+        ctx.data["streaming"] = getattr(ev, "streaming", False)
+        # start the workflow with researching about a topic
+        ctx.data["task"] = ev.input
+        ctx.data["user_input"] = ev.input
+
+        # Decision-making process
+        decision = await self._decide_workflow(ev.input, self.chat_history)
+
+        if decision != "publish":
+            return ResearchEvent(input=f"Research for this task: {ev.input}")
+        else:
+            chat_history_str = "\n".join(
+                [f"{msg.role}: {msg.content}" for msg in self.chat_history]
+            )
+            return PublishEvent(
+                input=f"Please publish content based on the chat history\n{chat_history_str}\n\n and task: {ev.input}"
+            )
+
+    async def _decide_workflow(
+        self, input: str, chat_history: List[ChatMessage]
+    ) -> str:
+        prompt_template = PromptTemplate(
+            "Given the following chat history and new task, decide whether to publish based on existing information.\n"
+            "Chat history:\n{chat_history}\n"
+            "New task: {input}\n"
+            "Decision (respond with either 'not_publish' or 'publish'):"
+        )
+
+        chat_history_str = "\n".join(
+            [f"{msg.role}: {msg.content}" for msg in chat_history]
+        )
+        prompt = prompt_template.format(chat_history=chat_history_str, input=input)
+
+        output = await Settings.llm.acomplete(prompt)
+        decision = output.text.strip().lower()
+
+        return "publish" if decision == "publish" else "research"
+
+    @step()
+    async def research(
+        self, ctx: Context, ev: ResearchEvent, researcher: FunctionCallingAgent
+    ) -> WriteEvent:
+        result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input)
+        content = result.response.message.content
+        return WriteEvent(
+            input=f"Write a blog post given this task: {ctx.data['task']} using this research content: {content}"
+        )
+
+    @step()
+    async def write(
+        self, ctx: Context, ev: WriteEvent, writer: FunctionCallingAgent
+    ) -> ReviewEvent | StopEvent:
+        MAX_ATTEMPTS = 2
+        ctx.data["attempts"] = ctx.data.get("attempts", 0) + 1
+        too_many_attempts = ctx.data["attempts"] > MAX_ATTEMPTS
+        if too_many_attempts:
+            ctx.write_event_to_stream(
+                AgentRunEvent(
+                    name=writer.name,
+                    msg=f"Too many attempts ({MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.",
+                )
+            )
+        if ev.is_good or too_many_attempts:
+            # too many attempts or the blog post is good - stream final response if requested
+            result = await self.run_agent(
+                ctx, writer, ev.input, streaming=ctx.data["streaming"]
+            )
+            return StopEvent(result=result)
+        result: AgentRunResult = await self.run_agent(ctx, writer, ev.input)
+        ctx.data["result"] = result
+        return ReviewEvent(input=result.response.message.content)
+
+    @step()
+    async def review(
+        self, ctx: Context, ev: ReviewEvent, reviewer: FunctionCallingAgent
+    ) -> WriteEvent:
+        result: AgentRunResult = await self.run_agent(ctx, reviewer, ev.input)
+        review = result.response.message.content
+        old_content = ctx.data["result"].response.message.content
+        post_is_good = "post is good" in review.lower()
+        ctx.write_event_to_stream(
+            AgentRunEvent(
+                name=reviewer.name,
+                msg=f"The post is {'not ' if not post_is_good else ''}good enough for publishing. Sending back to the writer{' for publication.' if post_is_good else '.'}",
+            )
+        )
+        if post_is_good:
+            return WriteEvent(
+                input=f"You're blog post is ready for publication. Please respond with just the blog post. Blog post: ```{old_content}```",
+                is_good=True,
+            )
+        else:
+            return WriteEvent(
+                input=dedent(
+                    f"""
+                    Improve the writing of a given blog post by using a given review.
+                    Blog post:
+                    ```
+                    {old_content}
+                    ``` 
+
+                    Review: 
+                    ```
+                    {review}
+                    ```
+                    """
+                ),
+            )
+
+    @step()
+    async def publish(
+        self,
+        ctx: Context,
+        ev: PublishEvent,
+        publisher: FunctionCallingAgent,
+    ) -> StopEvent:
+        try:
+            result: AgentRunResult = await self.run_agent(ctx, publisher, ev.input)
+            return StopEvent(result=result)
+        except Exception as e:
+            ctx.write_event_to_stream(
+                AgentRunEvent(
+                    name=publisher.name,
+                    msg=f"Error publishing: {e}",
+                )
+            )
+            return StopEvent(result=None)
+
+    async def run_agent(
+        self,
+        ctx: Context,
+        agent: FunctionCallingAgent,
+        input: str,
+        streaming: bool = False,
+    ) -> AgentRunResult | AsyncGenerator:
+        handler = agent.run(input=input, streaming=streaming)
+        # bubble all events while running the executor to the planner
+        async for event in handler.stream_events():
+            # Don't write the StopEvent from sub task to the stream
+            if type(event) is not StopEvent:
+                ctx.write_event_to_stream(event)
+        return await handler
diff --git a/templates/components/multiagent/typescript/workflow/agents.ts b/templates/components/multiagent/typescript/workflow/agents.ts
index 7abaa6d0ab4dc0913f89eaa7d21959be97fe3c38..b62bd360ad03b9960261ce538f72065b2bef9dd3 100644
--- a/templates/components/multiagent/typescript/workflow/agents.ts
+++ b/templates/components/multiagent/typescript/workflow/agents.ts
@@ -1,33 +1,38 @@
-import { ChatMessage, QueryEngineTool } from "llamaindex";
-import { getDataSource } from "../engine";
+import { ChatMessage } from "llamaindex";
 import { FunctionCallingAgent } from "./single-agent";
-
-const getQueryEngineTool = async () => {
-  const index = await getDataSource();
-  if (!index) {
-    throw new Error(
-      "StorageContext is empty - call 'npm run generate' to generate the storage first.",
-    );
-  }
-
-  const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
-  return new QueryEngineTool({
-    queryEngine: index.asQueryEngine({
-      similarityTopK: topK,
-    }),
-    metadata: {
-      name: "query_index",
-      description: `Use this tool to retrieve information about the text corpus from the index.`,
-    },
-  });
-};
+import { lookupTools } from "./tools";
 
 export const createResearcher = async (chatHistory: ChatMessage[]) => {
+  const tools = await lookupTools([
+    "query_index",
+    "wikipedia_tool",
+    "duckduckgo_search",
+    "image_generator",
+  ]);
+
   return new FunctionCallingAgent({
     name: "researcher",
-    tools: [await getQueryEngineTool()],
-    systemPrompt:
-      "You are a researcher agent. You are given a researching task. You must use your tools to complete the research.",
+    tools: tools,
+    systemPrompt: `You are a researcher agent. You are given a research task.
+            
+If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
+Otherwise, you must use tools to retrieve information or images needed for the task.
+
+It's normal for the task to include some ambiguity. You must always think carefully about the context of the user's request to understand what are the main content needs to be retrieved.
+Example:
+    Request: "Create a blog post about the history of the internet, write in English and publish in PDF format."
+    ->Though: The main content is "history of the internet", while "write in English and publish in PDF format" is a requirement for other agents.
+    Your task: Look for information in English about the history of the Internet.
+    This is not your task: Create a blog post or look for how to create a PDF.
+
+    Next request: "Publish the blog post in HTML format."
+    ->Though: User just asking for a format change, the previous content is still valid.
+    Your task: Return the previous content of the post to the writer. No need to do any research.
+    This is not your task: Look for how to create an HTML file.
+
+If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
+If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history.
+`,
     chatHistory,
   });
 };
@@ -35,8 +40,20 @@ export const createResearcher = async (chatHistory: ChatMessage[]) => {
 export const createWriter = (chatHistory: ChatMessage[]) => {
   return new FunctionCallingAgent({
     name: "writer",
-    systemPrompt:
-      "You are an expert in writing blog posts. You are given a task to write a blog post. Don't make up any information yourself.",
+    systemPrompt: `You are an expert in writing blog posts.
+You are given the task of writing a blog post based on research content provided by the researcher agent. Do not invent any information yourself. 
+It's important to read the entire conversation history to write the blog post accurately.
+If you receive a review from the reviewer, update the post according to the feedback and return the new post content.
+If the user requests an update with new information but no research content is provided, you must respond with: "I don't have any research content to write about."
+If the content is not valid (e.g., broken link, broken image, etc.), do not use it.
+It's normal for the task to include some ambiguity, so you must define the user's initial request to write the post correctly.
+If you update the post based on the reviewer's feedback, first explain what changes you made to the post, then provide the new post content. Do not include the reviewer's comments.
+Example:
+    Task: "Here is the information I found about the history of the internet: 
+    Create a blog post about the history of the internet, write in English, and publish in PDF format."
+    -> Your task: Use the research content {...} to write a blog post in English.
+    -> This is not your task: Create a PDF
+    Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.`,
     chatHistory,
   });
 };
@@ -44,8 +61,34 @@ export const createWriter = (chatHistory: ChatMessage[]) => {
 export const createReviewer = (chatHistory: ChatMessage[]) => {
   return new FunctionCallingAgent({
     name: "reviewer",
-    systemPrompt:
-      "You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. Only if the post is good enough for publishing, then you MUST return 'The post is good.'. In all other cases return your review.",
+    systemPrompt: `You are an expert in reviewing blog posts.
+You are given a task to review a blog post. As a reviewer, it's important that your review aligns with the user's request. Please focus on the user's request when reviewing the post.
+Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement.
+Furthermore, proofread the post for grammar and spelling errors.
+Only if the post is good enough for publishing should you return 'The post is good.' In all other cases, return your review.
+It's normal for the task to include some ambiguity, so you must define the user's initial request to review the post correctly.
+Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
+Example:
+    Task: "Create a blog post about the history of the internet, write in English and publish in PDF format."
+    -> Your task: Review whether the main content of the post is about the history of the internet and if it is written in English.
+    -> This is not your task: Create blog post, create PDF, write in English.`,
+    chatHistory,
+  });
+};
+
+export const createPublisher = async (chatHistory: ChatMessage[]) => {
+  const tools = await lookupTools(["document_generator"]);
+  let systemPrompt = `You are an expert in publishing blog posts. You are given a task to publish a blog post. 
+If the writer says that there was an error, you should reply with the error and not publish the post.`;
+  if (tools.length > 0) {
+    systemPrompt = `${systemPrompt}. 
+If the user requests to generate a file, use the document_generator tool to generate the file and reply with the link to the file.
+Otherwise, simply return the content of the post.`;
+  }
+  return new FunctionCallingAgent({
+    name: "publisher",
+    tools: tools,
+    systemPrompt: systemPrompt,
     chatHistory,
   });
 };
diff --git a/templates/components/multiagent/typescript/workflow/factory.ts b/templates/components/multiagent/typescript/workflow/factory.ts
index 8853b08b24ea0c4e26e05f2645a13ac010a9cd61..01161303bd509b8dcd70c3d9071fa043d221287d 100644
--- a/templates/components/multiagent/typescript/workflow/factory.ts
+++ b/templates/components/multiagent/typescript/workflow/factory.ts
@@ -6,7 +6,12 @@ import {
   WorkflowEvent,
 } from "@llamaindex/core/workflow";
 import { ChatMessage, ChatResponseChunk } from "llamaindex";
-import { createResearcher, createReviewer, createWriter } from "./agents";
+import {
+  createPublisher,
+  createResearcher,
+  createReviewer,
+  createWriter,
+} from "./agents";
 import { AgentInput, AgentRunEvent } from "./type";
 
 const TIMEOUT = 360 * 1000;
@@ -18,8 +23,49 @@ class WriteEvent extends WorkflowEvent<{
   isGood: boolean;
 }> {}
 class ReviewEvent extends WorkflowEvent<{ input: string }> {}
+class PublishEvent extends WorkflowEvent<{ input: string }> {}
+
+const prepareChatHistory = (chatHistory: ChatMessage[]) => {
+  // By default, the chat history only contains the assistant and user messages
+  // all the agents messages are stored in annotation data which is not visible to the LLM
+
+  const MAX_AGENT_MESSAGES = 10;
+
+  // Construct a new agent message from agent messages
+  // Get annotations from assistant messages
+  const agentAnnotations = chatHistory
+    .filter((msg) => msg.role === "assistant")
+    .flatMap((msg) => msg.annotations || [])
+    .filter((annotation) => annotation.type === "agent")
+    .slice(-MAX_AGENT_MESSAGES);
+
+  const agentMessages = agentAnnotations
+    .map(
+      (annotation) =>
+        `\n<${annotation.data.agent}>\n${annotation.data.text}\n</${annotation.data.agent}>`,
+    )
+    .join("\n");
+
+  const agentContent = agentMessages
+    ? "Here is the previous conversation of agents:\n" + agentMessages
+    : "";
+
+  if (agentContent) {
+    const agentMessage: ChatMessage = {
+      role: "assistant",
+      content: agentContent,
+    };
+    return [
+      ...chatHistory.slice(0, -1),
+      agentMessage,
+      chatHistory.slice(-1)[0],
+    ];
+  }
+  return chatHistory;
+};
 
 export const createWorkflow = (chatHistory: ChatMessage[]) => {
+  const chatHistoryWithAgentMessages = prepareChatHistory(chatHistory);
   const runAgent = async (
     context: Context,
     agent: Workflow,
@@ -42,7 +88,7 @@ export const createWorkflow = (chatHistory: ChatMessage[]) => {
   };
 
   const research = async (context: Context, ev: ResearchEvent) => {
-    const researcher = await createResearcher(chatHistory);
+    const researcher = await createResearcher(chatHistoryWithAgentMessages);
     const researchRes = await runAgent(context, researcher, {
       message: ev.data.input,
     });
@@ -66,17 +112,12 @@ export const createWorkflow = (chatHistory: ChatMessage[]) => {
     }
 
     if (ev.data.isGood || tooManyAttempts) {
-      // The text is ready for publication, we just use the writer to stream the output
-      const writer = createWriter(chatHistory);
-      const content = context.get("result");
-
-      return (await runAgent(context, writer, {
-        message: `You're blog post is ready for publication. Please respond with just the blog post. Blog post: \`\`\`${content}\`\`\``,
-        streaming: true,
-      })) as unknown as StopEvent<AsyncGenerator<ChatResponseChunk>>;
+      return new PublishEvent({
+        input: "Please help me to publish the blog post.",
+      });
     }
 
-    const writer = createWriter(chatHistory);
+    const writer = createWriter(chatHistoryWithAgentMessages);
     const writeRes = await runAgent(context, writer, {
       message: ev.data.input,
     });
@@ -86,7 +127,7 @@ export const createWorkflow = (chatHistory: ChatMessage[]) => {
   };
 
   const review = async (context: Context, ev: ReviewEvent) => {
-    const reviewer = createReviewer(chatHistory);
+    const reviewer = createReviewer(chatHistoryWithAgentMessages);
     const reviewRes = await reviewer.run(
       new StartEvent<AgentInput>({ input: { message: ev.data.input } }),
     );
@@ -123,11 +164,24 @@ export const createWorkflow = (chatHistory: ChatMessage[]) => {
     });
   };
 
+  const publish = async (context: Context, ev: PublishEvent) => {
+    const publisher = await createPublisher(chatHistoryWithAgentMessages);
+
+    const publishResult = await runAgent(context, publisher, {
+      message: `${ev.data.input}`,
+      streaming: true,
+    });
+    return publishResult as unknown as StopEvent<
+      AsyncGenerator<ChatResponseChunk>
+    >;
+  };
+
   const workflow = new Workflow({ timeout: TIMEOUT, validate: true });
   workflow.addStep(StartEvent, start, { outputs: ResearchEvent });
   workflow.addStep(ResearchEvent, research, { outputs: WriteEvent });
-  workflow.addStep(WriteEvent, write, { outputs: [ReviewEvent, StopEvent] });
+  workflow.addStep(WriteEvent, write, { outputs: [ReviewEvent, PublishEvent] });
   workflow.addStep(ReviewEvent, review, { outputs: WriteEvent });
+  workflow.addStep(PublishEvent, publish, { outputs: StopEvent });
 
   return workflow;
 };
diff --git a/templates/components/multiagent/typescript/workflow/tools.ts b/templates/components/multiagent/typescript/workflow/tools.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ac4e5fb98ba48dd5cf6c64dfef02952e91cb27a9
--- /dev/null
+++ b/templates/components/multiagent/typescript/workflow/tools.ts
@@ -0,0 +1,52 @@
+import fs from "fs/promises";
+import { BaseToolWithCall, QueryEngineTool } from "llamaindex";
+import path from "path";
+import { getDataSource } from "../engine";
+import { createTools } from "../engine/tools/index";
+
+const getQueryEngineTool = async (): Promise<QueryEngineTool | null> => {
+  const index = await getDataSource();
+  if (!index) {
+    return null;
+  }
+
+  const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
+  return new QueryEngineTool({
+    queryEngine: index.asQueryEngine({
+      similarityTopK: topK,
+    }),
+    metadata: {
+      name: "query_index",
+      description: `Use this tool to retrieve information about the text corpus from the index.`,
+    },
+  });
+};
+
+export const getAvailableTools = async () => {
+  const configFile = path.join("config", "tools.json");
+  let toolConfig: any;
+  const tools: BaseToolWithCall[] = [];
+  try {
+    toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
+  } catch (e) {
+    console.info(`Could not read ${configFile} file. Using no tools.`);
+  }
+  if (toolConfig) {
+    tools.push(...(await createTools(toolConfig)));
+  }
+  const queryEngineTool = await getQueryEngineTool();
+  if (queryEngineTool) {
+    tools.push(queryEngineTool);
+  }
+
+  return tools;
+};
+
+export const lookupTools = async (
+  toolNames: string[],
+): Promise<BaseToolWithCall[]> => {
+  const availableTools = await getAvailableTools();
+  return availableTools.filter((tool) =>
+    toolNames.includes(tool.metadata.name),
+  );
+};
diff --git a/templates/types/multiagent/fastapi/app/api/__init__.py b/templates/types/multiagent/fastapi/app/api/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/templates/types/multiagent/fastapi/app/api/routers/__init__.py b/templates/types/multiagent/fastapi/app/api/routers/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/templates/types/multiagent/fastapi/app/api/routers/chat.py b/templates/types/multiagent/fastapi/app/api/routers/chat.py
deleted file mode 100644
index 2b7a5636ffa29ed3f758cd8ded9c6f0897f06f80..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/api/routers/chat.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import logging
-
-from app.api.routers.models import (
-    ChatData,
-)
-from app.api.routers.vercel_response import VercelStreamResponse
-from app.examples.factory import create_agent
-from fastapi import APIRouter, HTTPException, Request, status
-from llama_index.core.workflow import Workflow
-
-chat_router = r = APIRouter()
-
-logger = logging.getLogger("uvicorn")
-
-
-@r.post("")
-async def chat(
-    request: Request,
-    data: ChatData,
-):
-    try:
-        last_message_content = data.get_last_message_content()
-        messages = data.get_history_messages()
-        # TODO: generate filters based on doc_ids
-        # for now just use all documents
-        # doc_ids = data.get_chat_document_ids()
-        # TODO: use params
-        # params = data.data or {}
-
-        agent: Workflow = create_agent(chat_history=messages)
-        handler = agent.run(input=last_message_content, streaming=True)
-
-        return VercelStreamResponse(request, handler, agent.stream_events, data)
-    except Exception as e:
-        logger.exception("Error in agent", exc_info=True)
-        raise HTTPException(
-            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
-            detail=f"Error in agent: {e}",
-        ) from e
diff --git a/templates/types/multiagent/fastapi/app/api/routers/chat_config.py b/templates/types/multiagent/fastapi/app/api/routers/chat_config.py
deleted file mode 100644
index 8d926e50bab944d9baff2ae2cb681ab6d48d4cef..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/api/routers/chat_config.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import logging
-import os
-
-from fastapi import APIRouter
-
-from app.api.routers.models import ChatConfig
-
-
-config_router = r = APIRouter()
-
-logger = logging.getLogger("uvicorn")
-
-
-@r.get("")
-async def chat_config() -> ChatConfig:
-    starter_questions = None
-    conversation_starters = os.getenv("CONVERSATION_STARTERS")
-    if conversation_starters and conversation_starters.strip():
-        starter_questions = conversation_starters.strip().split("\n")
-    return ChatConfig(starter_questions=starter_questions)
-
-
-try:
-    from app.engine.service import LLamaCloudFileService
-
-    logger.info("LlamaCloud is configured. Adding /config/llamacloud route.")
-
-    @r.get("/llamacloud")
-    async def chat_llama_cloud_config():
-        projects = LLamaCloudFileService.get_all_projects_with_pipelines()
-        pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
-        project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
-        pipeline_config = None
-        if pipeline and project:
-            pipeline_config = {
-                "pipeline": pipeline,
-                "project": project,
-            }
-        return {
-            "projects": projects,
-            "pipeline": pipeline_config,
-        }
-
-except ImportError:
-    logger.debug(
-        "LlamaCloud is not configured. Skipping adding /config/llamacloud route."
-    )
-    pass
diff --git a/templates/types/multiagent/fastapi/app/api/routers/models.py b/templates/types/multiagent/fastapi/app/api/routers/models.py
deleted file mode 100644
index 29648608fec20fff2679857df7bd8e4927858d49..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/api/routers/models.py
+++ /dev/null
@@ -1,227 +0,0 @@
-import logging
-import os
-from typing import Any, Dict, List, Literal, Optional
-
-from llama_index.core.llms import ChatMessage, MessageRole
-from llama_index.core.schema import NodeWithScore
-from pydantic import BaseModel, Field, validator
-from pydantic.alias_generators import to_camel
-
-from app.config import DATA_DIR
-
-logger = logging.getLogger("uvicorn")
-
-
-class FileContent(BaseModel):
-    type: Literal["text", "ref"]
-    # If the file is pure text then the value is be a string
-    # otherwise, it's a list of document IDs
-    value: str | List[str]
-
-
-class File(BaseModel):
-    id: str
-    content: FileContent
-    filename: str
-    filesize: int
-    filetype: str
-
-
-class AnnotationFileData(BaseModel):
-    files: List[File] = Field(
-        default=[],
-        description="List of files",
-    )
-
-    class Config:
-        json_schema_extra = {
-            "example": {
-                "csvFiles": [
-                    {
-                        "content": "Name, Age\nAlice, 25\nBob, 30",
-                        "filename": "example.csv",
-                        "filesize": 123,
-                        "id": "123",
-                        "type": "text/csv",
-                    }
-                ]
-            }
-        }
-        alias_generator = to_camel
-
-
-class Annotation(BaseModel):
-    type: str
-    data: AnnotationFileData | List[str]
-
-    def to_content(self) -> str | None:
-        if self.type == "document_file":
-            # We only support generating context content for CSV files for now
-            csv_files = [file for file in self.data.files if file.filetype == "csv"]
-            if len(csv_files) > 0:
-                return "Use data from following CSV raw content\n" + "\n".join(
-                    [f"```csv\n{csv_file.content.value}\n```" for csv_file in csv_files]
-                )
-        else:
-            logger.warning(
-                f"The annotation {self.type} is not supported for generating context content"
-            )
-        return None
-
-
-class Message(BaseModel):
-    role: MessageRole
-    content: str
-    annotations: List[Annotation] | None = None
-
-
-class ChatData(BaseModel):
-    messages: List[Message]
-    data: Any = None
-
-    class Config:
-        json_schema_extra = {
-            "example": {
-                "messages": [
-                    {
-                        "role": "user",
-                        "content": "What standards for letters exist?",
-                    }
-                ]
-            }
-        }
-
-    @validator("messages")
-    def messages_must_not_be_empty(cls, v):
-        if len(v) == 0:
-            raise ValueError("Messages must not be empty")
-        return v
-
-    def get_last_message_content(self) -> str:
-        """
-        Get the content of the last message along with the data content if available.
-        Fallback to use data content from previous messages
-        """
-        if len(self.messages) == 0:
-            raise ValueError("There is not any message in the chat")
-        last_message = self.messages[-1]
-        message_content = last_message.content
-        for message in reversed(self.messages):
-            if message.role == MessageRole.USER and message.annotations is not None:
-                annotation_contents = filter(
-                    None,
-                    [annotation.to_content() for annotation in message.annotations],
-                )
-                if not annotation_contents:
-                    continue
-                annotation_text = "\n".join(annotation_contents)
-                message_content = f"{message_content}\n{annotation_text}"
-                break
-        return message_content
-
-    def get_history_messages(self) -> List[ChatMessage]:
-        """
-        Get the history messages
-        """
-        return [
-            ChatMessage(role=message.role, content=message.content)
-            for message in self.messages[:-1]
-        ]
-
-    def is_last_message_from_user(self) -> bool:
-        return self.messages[-1].role == MessageRole.USER
-
-    def get_chat_document_ids(self) -> List[str]:
-        """
-        Get the document IDs from the chat messages
-        """
-        document_ids: List[str] = []
-        for message in self.messages:
-            if message.role == MessageRole.USER and message.annotations is not None:
-                for annotation in message.annotations:
-                    if (
-                        annotation.type == "document_file"
-                        and annotation.data.files is not None
-                    ):
-                        for fi in annotation.data.files:
-                            if fi.content.type == "ref":
-                                document_ids += fi.content.value
-        return list(set(document_ids))
-
-
-class SourceNodes(BaseModel):
-    id: str
-    metadata: Dict[str, Any]
-    score: Optional[float]
-    text: str
-    url: Optional[str]
-
-    @classmethod
-    def from_source_node(cls, source_node: NodeWithScore):
-        metadata = source_node.node.metadata
-        url = cls.get_url_from_metadata(metadata)
-
-        return cls(
-            id=source_node.node.node_id,
-            metadata=metadata,
-            score=source_node.score,
-            text=source_node.node.text,  # type: ignore
-            url=url,
-        )
-
-    @classmethod
-    def get_url_from_metadata(cls, metadata: Dict[str, Any]) -> str:
-        url_prefix = os.getenv("FILESERVER_URL_PREFIX")
-        if not url_prefix:
-            logger.warning(
-                "Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
-            )
-        file_name = metadata.get("file_name")
-
-        if file_name and url_prefix:
-            # file_name exists and file server is configured
-            pipeline_id = metadata.get("pipeline_id")
-            if pipeline_id:
-                # file is from LlamaCloud
-                file_name = f"{pipeline_id}${file_name}"
-                return f"{url_prefix}/output/llamacloud/{file_name}"
-            is_private = metadata.get("private", "false") == "true"
-            if is_private:
-                # file is a private upload
-                return f"{url_prefix}/output/uploaded/{file_name}"
-            # file is from calling the 'generate' script
-            # Get the relative path of file_path to data_dir
-            file_path = metadata.get("file_path")
-            data_dir = os.path.abspath(DATA_DIR)
-            if file_path and data_dir:
-                relative_path = os.path.relpath(file_path, data_dir)
-                return f"{url_prefix}/data/{relative_path}"
-        # fallback to URL in metadata (e.g. for websites)
-        return metadata.get("URL")
-
-    @classmethod
-    def from_source_nodes(cls, source_nodes: List[NodeWithScore]):
-        return [cls.from_source_node(node) for node in source_nodes]
-
-
-class Result(BaseModel):
-    result: Message
-    nodes: List[SourceNodes]
-
-
-class ChatConfig(BaseModel):
-    starter_questions: Optional[List[str]] = Field(
-        default=None,
-        description="List of starter questions",
-        serialization_alias="starterQuestions",
-    )
-
-    class Config:
-        json_schema_extra = {
-            "example": {
-                "starterQuestions": [
-                    "What standards for letters exist?",
-                    "What are the requirements for a letter to be considered a letter?",
-                ]
-            }
-        }
diff --git a/templates/types/multiagent/fastapi/app/api/routers/upload.py b/templates/types/multiagent/fastapi/app/api/routers/upload.py
deleted file mode 100644
index ccc03004b4cb6955b77b97b67e458ad174600cfb..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/api/routers/upload.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import logging
-from typing import List, Any
-
-from fastapi import APIRouter, HTTPException
-from pydantic import BaseModel
-
-from app.api.services.file import PrivateFileService
-
-file_upload_router = r = APIRouter()
-
-logger = logging.getLogger("uvicorn")
-
-
-class FileUploadRequest(BaseModel):
-    base64: str
-    filename: str
-    params: Any = None
-
-
-@r.post("")
-def upload_file(request: FileUploadRequest) -> List[str]:
-    try:
-        logger.info("Processing file")
-        return PrivateFileService.process_file(
-            request.filename, request.base64, request.params
-        )
-    except Exception as e:
-        logger.error(f"Error processing file: {e}", exc_info=True)
-        raise HTTPException(status_code=500, detail="Error processing file")
diff --git a/templates/types/multiagent/fastapi/app/config.py b/templates/types/multiagent/fastapi/app/config.py
deleted file mode 100644
index 29fa8d9a28fa2fc5ae9502639c1452cf8ae15e4b..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/config.py
+++ /dev/null
@@ -1 +0,0 @@
-DATA_DIR = "data"
diff --git a/templates/types/multiagent/fastapi/app/examples/choreography.py b/templates/types/multiagent/fastapi/app/examples/choreography.py
deleted file mode 100644
index aa7c197d62f220830bfe71f36104027508deb046..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/examples/choreography.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from typing import List, Optional
-from app.agents.single import FunctionCallingAgent
-from app.agents.multi import AgentCallingAgent
-from app.examples.researcher import create_researcher
-from llama_index.core.chat_engine.types import ChatMessage
-
-
-def create_choreography(chat_history: Optional[List[ChatMessage]] = None):
-    researcher = create_researcher(chat_history)
-    reviewer = FunctionCallingAgent(
-        name="reviewer",
-        role="expert in reviewing blog posts",
-        system_prompt="You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. If the post is good, you can say 'The post is good.'",
-        chat_history=chat_history,
-    )
-    return AgentCallingAgent(
-        name="writer",
-        agents=[researcher, reviewer],
-        role="expert in writing blog posts",
-        system_prompt="""You are an expert in writing blog posts. You are given a task to write a blog post. Before starting to write the post, consult the researcher agent to get the information you need. Don't make up any information yourself.
-        After creating a draft for the post, send it to the reviewer agent to receive some feedback and make sure to incorporate the feedback from the reviewer.
-        You can consult the reviewer and researcher maximal two times. Your output should just contain the blog post.""",
-        # TODO: add chat_history support to AgentCallingAgent
-        # chat_history=chat_history,
-    )
diff --git a/templates/types/multiagent/fastapi/app/examples/orchestrator.py b/templates/types/multiagent/fastapi/app/examples/orchestrator.py
deleted file mode 100644
index 9f91512412558ce3d6fdb4f0e2e8707db14bf8e1..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/examples/orchestrator.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import List, Optional
-from app.agents.single import FunctionCallingAgent
-from app.agents.multi import AgentOrchestrator
-from app.examples.researcher import create_researcher
-
-from llama_index.core.chat_engine.types import ChatMessage
-
-
-def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None):
-    researcher = create_researcher(chat_history)
-    writer = FunctionCallingAgent(
-        name="writer",
-        role="expert in writing blog posts",
-        system_prompt="""You are an expert in writing blog posts. You are given a task to write a blog post. Don't make up any information yourself. If you don't have the necessary information to write a blog post, reply "I need information about the topic to write the blog post". If you have all the information needed, write the blog post.""",
-        chat_history=chat_history,
-    )
-    reviewer = FunctionCallingAgent(
-        name="reviewer",
-        role="expert in reviewing blog posts",
-        system_prompt="""You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post and fix the issues found yourself. You must output a final blog post.
-        Especially check for logical inconsistencies and proofread the post for grammar and spelling errors.""",
-        chat_history=chat_history,
-    )
-    return AgentOrchestrator(
-        agents=[writer, reviewer, researcher],
-        refine_plan=False,
-    )
diff --git a/templates/types/multiagent/fastapi/app/examples/researcher.py b/templates/types/multiagent/fastapi/app/examples/researcher.py
deleted file mode 100644
index ed6819d4d688aa409599116921cc4ab6dc97cb69..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/examples/researcher.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import os
-from typing import List
-from llama_index.core.tools import QueryEngineTool, ToolMetadata
-from app.agents.single import FunctionCallingAgent
-from app.engine.index import get_index
-
-from llama_index.core.chat_engine.types import ChatMessage
-
-
-def get_query_engine_tool() -> QueryEngineTool:
-    """
-    Provide an agent worker that can be used to query the index.
-    """
-    index = get_index()
-    if index is None:
-        raise ValueError("Index not found. Please create an index first.")
-    top_k = int(os.getenv("TOP_K", 0))
-    query_engine = index.as_query_engine(
-        **({"similarity_top_k": top_k} if top_k != 0 else {})
-    )
-    return QueryEngineTool(
-        query_engine=query_engine,
-        metadata=ToolMetadata(
-            name="query_index",
-            description="""
-                Use this tool to retrieve information about the text corpus from the index.
-            """,
-        ),
-    )
-
-
-def create_researcher(chat_history: List[ChatMessage]):
-    return FunctionCallingAgent(
-        name="researcher",
-        tools=[get_query_engine_tool()],
-        role="expert in retrieving any unknown content",
-        system_prompt="You are a researcher agent. You are given a researching task. You must use your tools to complete the research.",
-        chat_history=chat_history,
-    )
diff --git a/templates/types/multiagent/fastapi/app/examples/workflow.py b/templates/types/multiagent/fastapi/app/examples/workflow.py
deleted file mode 100644
index c92f96ab9c92dbce65852f2f72920b6b7f35597d..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/examples/workflow.py
+++ /dev/null
@@ -1,139 +0,0 @@
-from typing import AsyncGenerator, List, Optional
-
-from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
-from app.examples.researcher import create_researcher
-from llama_index.core.chat_engine.types import ChatMessage
-from llama_index.core.workflow import (
-    Context,
-    Event,
-    StartEvent,
-    StopEvent,
-    Workflow,
-    step,
-)
-
-
-def create_workflow(chat_history: Optional[List[ChatMessage]] = None):
-    researcher = create_researcher(
-        chat_history=chat_history,
-    )
-    writer = FunctionCallingAgent(
-        name="writer",
-        role="expert in writing blog posts",
-        system_prompt="""You are an expert in writing blog posts. You are given a task to write a blog post. Don't make up any information yourself.""",
-        chat_history=chat_history,
-    )
-    reviewer = FunctionCallingAgent(
-        name="reviewer",
-        role="expert in reviewing blog posts",
-        system_prompt="You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. Only if the post is good enough for publishing, then you MUST return 'The post is good.'. In all other cases return your review.",
-        chat_history=chat_history,
-    )
-    workflow = BlogPostWorkflow(timeout=360)
-    workflow.add_workflows(researcher=researcher, writer=writer, reviewer=reviewer)
-    return workflow
-
-
-class ResearchEvent(Event):
-    input: str
-
-
-class WriteEvent(Event):
-    input: str
-    is_good: bool = False
-
-
-class ReviewEvent(Event):
-    input: str
-
-
-class BlogPostWorkflow(Workflow):
-    @step()
-    async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent:
-        # set streaming
-        ctx.data["streaming"] = getattr(ev, "streaming", False)
-        # start the workflow with researching about a topic
-        ctx.data["task"] = ev.input
-        return ResearchEvent(input=f"Research for this task: {ev.input}")
-
-    @step()
-    async def research(
-        self, ctx: Context, ev: ResearchEvent, researcher: FunctionCallingAgent
-    ) -> WriteEvent:
-        result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input)
-        content = result.response.message.content
-        return WriteEvent(
-            input=f"Write a blog post given this task: {ctx.data['task']} using this research content: {content}"
-        )
-
-    @step()
-    async def write(
-        self, ctx: Context, ev: WriteEvent, writer: FunctionCallingAgent
-    ) -> ReviewEvent | StopEvent:
-        MAX_ATTEMPTS = 2
-        ctx.data["attempts"] = ctx.data.get("attempts", 0) + 1
-        too_many_attempts = ctx.data["attempts"] > MAX_ATTEMPTS
-        if too_many_attempts:
-            ctx.write_event_to_stream(
-                AgentRunEvent(
-                    name=writer.name,
-                    msg=f"Too many attempts ({MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.",
-                )
-            )
-        if ev.is_good or too_many_attempts:
-            # too many attempts or the blog post is good - stream final response if requested
-            result = await self.run_agent(
-                ctx, writer, ev.input, streaming=ctx.data["streaming"]
-            )
-            return StopEvent(result=result)
-        result: AgentRunResult = await self.run_agent(ctx, writer, ev.input)
-        ctx.data["result"] = result
-        return ReviewEvent(input=result.response.message.content)
-
-    @step()
-    async def review(
-        self, ctx: Context, ev: ReviewEvent, reviewer: FunctionCallingAgent
-    ) -> WriteEvent:
-        result: AgentRunResult = await self.run_agent(ctx, reviewer, ev.input)
-        review = result.response.message.content
-        old_content = ctx.data["result"].response.message.content
-        post_is_good = "post is good" in review.lower()
-        ctx.write_event_to_stream(
-            AgentRunEvent(
-                name=reviewer.name,
-                msg=f"The post is {'not ' if not post_is_good else ''}good enough for publishing. Sending back to the writer{' for publication.' if post_is_good else '.'}",
-            )
-        )
-        if post_is_good:
-            return WriteEvent(
-                input=f"You're blog post is ready for publication. Please respond with just the blog post. Blog post: ```{old_content}```",
-                is_good=True,
-            )
-        else:
-            return WriteEvent(
-                input=f"""Improve the writing of a given blog post by using a given review.
-Blog post:
-```
-{old_content}
-``` 
-
-Review: 
-```
-{review}
-```"""
-            )
-
-    async def run_agent(
-        self,
-        ctx: Context,
-        agent: FunctionCallingAgent,
-        input: str,
-        streaming: bool = False,
-    ) -> AgentRunResult | AsyncGenerator:
-        handler = agent.run(input=input, streaming=streaming)
-        # bubble all events while running the executor to the planner
-        async for event in handler.stream_events():
-            # Don't write the StopEvent from sub task to the stream
-            if type(event) is not StopEvent:
-                ctx.write_event_to_stream(event)
-        return await handler
diff --git a/templates/types/multiagent/fastapi/app/observability.py b/templates/types/multiagent/fastapi/app/observability.py
deleted file mode 100644
index 28019c37e9daad2afce7cc75bb43672e5c6e43cd..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/observability.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def init_observability():
-    pass
diff --git a/templates/types/multiagent/fastapi/app/utils.py b/templates/types/multiagent/fastapi/app/utils.py
deleted file mode 100644
index ac43ccbb36843974c610fc937b5b23b2f25d47da..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/app/utils.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import os
-
-
-def load_from_env(var: str, throw_error: bool = True) -> str:
-    res = os.getenv(var)
-    if res is None and throw_error:
-        raise ValueError(f"Missing environment variable: {var}")
-    return res
diff --git a/templates/types/multiagent/fastapi/gitignore b/templates/types/multiagent/fastapi/gitignore
deleted file mode 100644
index ae22d348e213803cd983db0c3fbcab39e87f0608..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-__pycache__
-storage
-.env
-output
diff --git a/templates/types/multiagent/fastapi/main.py b/templates/types/multiagent/fastapi/main.py
deleted file mode 100644
index 11395a07982ebb8acc89d803815d7eaeb9ee0c52..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/main.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# flake8: noqa: E402
-import os
-from dotenv import load_dotenv
-
-from app.config import DATA_DIR
-
-load_dotenv()
-
-import logging
-
-import uvicorn
-from app.api.routers.chat import chat_router
-from app.api.routers.chat_config import config_router
-from app.api.routers.upload import file_upload_router
-from app.observability import init_observability
-from app.settings import init_settings
-from fastapi import FastAPI
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import RedirectResponse
-from fastapi.staticfiles import StaticFiles
-
-app = FastAPI()
-
-init_settings()
-init_observability()
-
-
-environment = os.getenv("ENVIRONMENT", "dev")  # Default to 'development' if not set
-logger = logging.getLogger("uvicorn")
-
-if environment == "dev":
-    logger.warning("Running in development mode - allowing CORS for all origins")
-    app.add_middleware(
-        CORSMiddleware,
-        allow_origins=["*"],
-        allow_credentials=True,
-        allow_methods=["*"],
-        allow_headers=["*"],
-    )
-
-    # Redirect to documentation page when accessing base URL
-    @app.get("/")
-    async def redirect_to_docs():
-        return RedirectResponse(url="/docs")
-
-
-def mount_static_files(directory, path):
-    if os.path.exists(directory):
-        logger.info(f"Mounting static files '{directory}' at '{path}'")
-        app.mount(
-            path,
-            StaticFiles(directory=directory, check_dir=False),
-            name=f"{directory}-static",
-        )
-
-
-# Mount the data files to serve the file viewer
-mount_static_files(DATA_DIR, "/api/files/data")
-# Mount the output files from tools
-mount_static_files("output", "/api/files/output")
-
-app.include_router(chat_router, prefix="/api/chat")
-app.include_router(config_router, prefix="/api/chat/config")
-app.include_router(file_upload_router, prefix="/api/chat/upload")
-
-
-if __name__ == "__main__":
-    app_host = os.getenv("APP_HOST", "0.0.0.0")
-    app_port = int(os.getenv("APP_PORT", "8000"))
-    reload = True if environment == "dev" else False
-
-    uvicorn.run(app="main:app", host=app_host, port=app_port, reload=reload)
diff --git a/templates/types/multiagent/fastapi/pyproject.toml b/templates/types/multiagent/fastapi/pyproject.toml
deleted file mode 100644
index 5c779f27c934c862e1902d29e712c231f4b18c5c..0000000000000000000000000000000000000000
--- a/templates/types/multiagent/fastapi/pyproject.toml
+++ /dev/null
@@ -1,27 +0,0 @@
-[tool]
-[tool.poetry]
-name = "app"
-version = "0.1.0"
-description = ""
-authors = ["Marcus Schiesser <mail@marcusschiesser.de>"]
-readme = "README.md"
-
-[tool.poetry.scripts]
-generate = "app.engine.generate:generate_datasource"
-
-[tool.poetry.dependencies]
-python = ">=3.11,<3.13"
-llama-index-agent-openai = ">=0.3.0,<0.4.0"
-llama-index = "0.11.11"
-fastapi = "^0.112.2"
-python-dotenv = "^1.0.0"
-uvicorn = { extras = ["standard"], version = "^0.23.2" }
-cachetools = "^5.3.3"
-aiostream = "^0.5.2"
-
-[tool.poetry.dependencies.docx2txt]
-version = "^0.8"
-
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
diff --git a/templates/types/streaming/express/package.json b/templates/types/streaming/express/package.json
index 084f39c8610251b25e31e4f52216d31122f2563c..39d23f857409a9912c0714652b50b60d7b52c266 100644
--- a/templates/types/streaming/express/package.json
+++ b/templates/types/streaming/express/package.json
@@ -27,7 +27,8 @@
     "@e2b/code-interpreter": "^0.0.5",
     "got": "^14.4.1",
     "@apidevtools/swagger-parser": "^10.1.0",
-    "formdata-node": "^6.0.3"
+    "formdata-node": "^6.0.3",
+    "marked": "^14.1.2"
   },
   "devDependencies": {
     "@types/cors": "^2.8.16",
diff --git a/templates/types/streaming/fastapi/app/api/routers/chat.py b/templates/types/streaming/fastapi/app/api/routers/chat.py
index 39894361a8d21d0f95bfe1231b0b09b373b73995..48876efb6f9b4faaf98dc23bddc50fd3d5c04f81 100644
--- a/templates/types/streaming/fastapi/app/api/routers/chat.py
+++ b/templates/types/streaming/fastapi/app/api/routers/chat.py
@@ -1,8 +1,8 @@
 import logging
 from typing import List
 
-from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
-from llama_index.core.chat_engine.types import BaseChatEngine, NodeWithScore
+from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
+from llama_index.core.chat_engine.types import NodeWithScore
 from llama_index.core.llms import MessageRole
 
 from app.api.routers.events import EventCallbackHandler
@@ -58,11 +58,19 @@ async def chat(
 @r.post("/request")
 async def chat_request(
     data: ChatData,
-    chat_engine: BaseChatEngine = Depends(get_chat_engine),
 ) -> Result:
     last_message_content = data.get_last_message_content()
     messages = data.get_history_messages()
 
+    doc_ids = data.get_chat_document_ids()
+    filters = generate_filters(doc_ids)
+    params = data.data or {}
+    logger.info(
+        f"Creating chat engine with filters: {str(filters)}",
+    )
+
+    chat_engine = get_chat_engine(filters=filters, params=params)
+
     response = await chat_engine.achat(last_message_content, messages)
     return Result(
         result=Message(role=MessageRole.ASSISTANT, content=response.response),
diff --git a/templates/types/streaming/fastapi/app/api/routers/models.py b/templates/types/streaming/fastapi/app/api/routers/models.py
index 29648608fec20fff2679857df7bd8e4927858d49..123f97ba950886bfeb0fd45984932b272be50959 100644
--- a/templates/types/streaming/fastapi/app/api/routers/models.py
+++ b/templates/types/streaming/fastapi/app/api/routers/models.py
@@ -50,9 +50,14 @@ class AnnotationFileData(BaseModel):
         alias_generator = to_camel
 
 
+class AgentAnnotation(BaseModel):
+    agent: str
+    text: str
+
+
 class Annotation(BaseModel):
     type: str
-    data: AnnotationFileData | List[str]
+    data: AnnotationFileData | List[str] | AgentAnnotation
 
     def to_content(self) -> str | None:
         if self.type == "document_file":
@@ -119,14 +124,48 @@ class ChatData(BaseModel):
                 break
         return message_content
 
-    def get_history_messages(self) -> List[ChatMessage]:
+    def _get_agent_messages(self, max_messages: int = 10) -> List[str]:
+        """
+        Construct agent messages from the annotations in the chat messages
+        """
+        agent_messages = []
+        for message in self.messages:
+            if (
+                message.role == MessageRole.ASSISTANT
+                and message.annotations is not None
+            ):
+                for annotation in message.annotations:
+                    if annotation.type == "agent" and isinstance(
+                        annotation.data, AgentAnnotation
+                    ):
+                        text = annotation.data.text
+                        agent_messages.append(
+                            f"\nAgent: {annotation.data.agent}\nsaid: {text}\n"
+                        )
+                        if len(agent_messages) >= max_messages:
+                            break
+        return agent_messages
+
+    def get_history_messages(
+        self, include_agent_messages: bool = False
+    ) -> List[ChatMessage]:
         """
         Get the history messages
         """
-        return [
+        chat_messages = [
             ChatMessage(role=message.role, content=message.content)
             for message in self.messages[:-1]
         ]
+        if include_agent_messages:
+            agent_messages = self._get_agent_messages(max_messages=5)
+            if len(agent_messages) > 0:
+                message = ChatMessage(
+                    role=MessageRole.ASSISTANT,
+                    content="Previous agent events: \n" + "\n".join(agent_messages),
+                )
+                chat_messages.append(message)
+
+        return chat_messages
 
     def is_last_message_from_user(self) -> bool:
         return self.messages[-1].role == MessageRole.USER
diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-agent-events.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-agent-events.tsx
index 618dd0645b98990c5d3c1e162ef5b954706c5ec9..8fea31dfab2aafc37ad5b16952f8bd4227953e64 100644
--- a/templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-agent-events.tsx
+++ b/templates/types/streaming/nextjs/app/components/ui/chat/chat-message/chat-agent-events.tsx
@@ -17,6 +17,7 @@ const AgentIcons: Record<string, LucideIcon> = {
   researcher: icons.ScanSearch,
   writer: icons.PenLine,
   reviewer: icons.MessageCircle,
+  publisher: icons.BookCheck,
 };
 
 type MergedEvent = {
diff --git a/templates/types/streaming/nextjs/package.json b/templates/types/streaming/nextjs/package.json
index 775bee983b0be827aac83904d234dd948e884d8c..38ced85143fb05929bbe8903d55d819cd3049fd5 100644
--- a/templates/types/streaming/nextjs/package.json
+++ b/templates/types/streaming/nextjs/package.json
@@ -42,7 +42,8 @@
     "tailwind-merge": "^2.1.0",
     "tiktoken": "^1.0.15",
     "uuid": "^9.0.1",
-    "vaul": "^0.9.1"
+    "vaul": "^0.9.1",
+    "marked": "^14.1.2"
   },
   "devDependencies": {
     "@types/node": "^20.10.3",