From 11eea23752bb964ce6c8cdfd8ec5883a50b741d5 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Tue, 22 Oct 2024 12:20:03 +0700 Subject: [PATCH 01/13] init code for multi-agent financial report --- helpers/datasources.ts | 15 + helpers/index.ts | 19 +- helpers/types.ts | 2 +- questions/simple.ts | 12 +- .../multiagent/python/README-template.md | 14 +- .../multiagent/python/app/engine/engine.py | 8 +- .../python/app/examples/choreography.py | 34 --- .../python/app/examples/orchestrator.py | 44 --- .../python/app/examples/publisher.py | 35 --- .../python/app/examples/researcher.py | 86 ------ .../python/app/examples/workflow.py | 265 ------------------ .../app/financial_report/agents/analyst.py | 34 +++ .../app/financial_report/agents/reporter.py | 43 +++ .../app/financial_report/agents/researcher.py | 102 +++++++ .../app/financial_report/choreography.py | 27 ++ .../app/financial_report/orchestrator.py | 18 ++ .../python/app/financial_report/workflow.py | 176 ++++++++++++ 17 files changed, 451 insertions(+), 483 deletions(-) delete mode 100644 templates/components/multiagent/python/app/examples/choreography.py delete mode 100644 templates/components/multiagent/python/app/examples/orchestrator.py delete mode 100644 templates/components/multiagent/python/app/examples/publisher.py delete mode 100644 templates/components/multiagent/python/app/examples/researcher.py delete mode 100644 templates/components/multiagent/python/app/examples/workflow.py create mode 100644 templates/components/multiagent/python/app/financial_report/agents/analyst.py create mode 100644 templates/components/multiagent/python/app/financial_report/agents/reporter.py create mode 100644 templates/components/multiagent/python/app/financial_report/agents/researcher.py create mode 100644 templates/components/multiagent/python/app/financial_report/choreography.py create mode 100644 templates/components/multiagent/python/app/financial_report/orchestrator.py create mode 100644 templates/components/multiagent/python/app/financial_report/workflow.py diff --git a/helpers/datasources.ts b/helpers/datasources.ts index dc80207e4..74a64451b 100644 --- a/helpers/datasources.ts +++ b/helpers/datasources.ts @@ -11,6 +11,21 @@ export const EXAMPLE_FILE: TemplateDataSource = { }, }; +export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [ + { + type: "file", + config: { + path: "https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf", + }, + }, + { + type: "file", + config: { + path: "https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf", + }, + }, +]; + export function getDataSources( files?: string, exampleFile?: boolean, diff --git a/helpers/index.ts b/helpers/index.ts index 56d07808f..c0d583ed3 100644 --- a/helpers/index.ts +++ b/helpers/index.ts @@ -96,6 +96,12 @@ async function generateContextData( } } +const downloadFile = async (url: string, destPath: string) => { + const response = await fetch(url); + const fileBuffer = await response.arrayBuffer(); + await fsExtra.writeFile(destPath, Buffer.from(fileBuffer)); +}; + const prepareContextData = async ( root: string, dataSources: TemplateDataSource[], @@ -103,12 +109,17 @@ const prepareContextData = async ( await makeDir(path.join(root, "data")); for (const dataSource of dataSources) { const dataSourceConfig = dataSource?.config as FileSourceConfig; - // Copy local data const dataPath = dataSourceConfig.path; - const destPath = path.join(root, "data", path.basename(dataPath)); - console.log("Copying data from path:", dataPath); - await fsExtra.copy(dataPath, destPath); + // If the path is URLs, download the data and save it to the data directory + if (dataPath.startsWith("http")) { + console.log("Downloading file from URL:", dataPath); + await downloadFile(dataPath, destPath); + } else { + // Copy local data + console.log("Copying data from path:", dataPath); + await fsExtra.copy(dataPath, destPath); + } } }; diff --git a/helpers/types.ts b/helpers/types.ts index 89dd96644..c8b58bf28 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -50,7 +50,7 @@ export type TemplateDataSourceType = "file" | "web" | "db"; export type TemplateObservability = "none" | "traceloop" | "llamatrace"; // Config for both file and folder export type FileSourceConfig = { - path: string; + path: string; // Could be an URL to download the data while creating the project }; export type WebSourceConfig = { baseUrl?: string; diff --git a/questions/simple.ts b/questions/simple.ts index 29930c1ae..9a5ce6b00 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -1,5 +1,5 @@ import prompts from "prompts"; -import { EXAMPLE_FILE } from "../helpers/datasources"; +import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources"; import { askModelConfig } from "../helpers/providers"; import { getTools } from "../helpers/tools"; import { ModelConfig, TemplateFramework } from "../helpers/types"; @@ -11,7 +11,8 @@ type AppType = | "code_artifact" | "multiagent" | "extractor" - | "data_scientist"; + | "data_scientist" + | "financial_report"; type SimpleAnswers = { appType: AppType; @@ -31,6 +32,7 @@ export const askSimpleQuestions = async ( choices: [ { title: "Agentic RAG", value: "rag" }, { title: "Data Scientist", value: "data_scientist" }, + { title: "Financial Report", value: "financial_report" }, { title: "Code Artifact Agent", value: "code_artifact" }, { title: "Multi-Agent Report Gen", value: "multiagent" }, { title: "Structured extraction", value: "extractor" }, @@ -130,6 +132,12 @@ const convertAnswers = async ( dataSources: [], modelConfig: MODEL_GPT4o, }, + financial_report: { + template: "multiagent", + tools: getTools(["duckduckgo", "document_generator"]), + frontend: true, + dataSources: EXAMPLE_10K_SEC_FILES, + }, code_artifact: { template: "streaming", tools: getTools(["artifact"]), diff --git a/templates/components/multiagent/python/README-template.md b/templates/components/multiagent/python/README-template.md index 761f19a4d..dcc8081e7 100644 --- a/templates/components/multiagent/python/README-template.md +++ b/templates/components/multiagent/python/README-template.md @@ -5,14 +5,14 @@ This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [W This example is using three agents to generate a blog post: - a researcher that retrieves content via a RAG pipeline, -- a writer that specializes in writing blog posts and -- a reviewer that is reviewing the blog post. +- an analyst that analyzes the financial data, +- a reporter that writes the financial report. There are three different methods how the agents can interact to reach their goal: -1. [Choreography](./app/examples/choreography.py) - the agents decide themselves to delegate a task to another agent -1. [Orchestrator](./app/examples/orchestrator.py) - a central orchestrator decides which agent should execute a task -1. [Explicit Workflow](./app/examples/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks +1. [Choreography](./app/financial_report/choreography.py) - the agents decide themselves to delegate a task to another agent +1. [Orchestrator](./app/financial_report/orchestrator.py) - a central orchestrator decides which agent should execute a task +1. [Explicit Workflow](./app/financial_report/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks ## Getting Started @@ -46,10 +46,10 @@ You can test the endpoint with the following curl request: ``` curl --location 'localhost:8000/api/chat' \ --header 'Content-Type: application/json' \ ---data '{ "messages": [{ "role": "user", "content": "Write a blog post about physical standards for letters" }] }' +--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla." }] }' ``` -You can start editing the API by modifying `app/api/routers/chat.py` or `app/examples/workflow.py`. The API auto-updates as you save the files. +You can start editing the API by modifying `app/api/routers/chat.py` or `app/financial_report/workflow.py`. The API auto-updates as you save the files. Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API. diff --git a/templates/components/multiagent/python/app/engine/engine.py b/templates/components/multiagent/python/app/engine/engine.py index 506179a02..3341139f8 100644 --- a/templates/components/multiagent/python/app/engine/engine.py +++ b/templates/components/multiagent/python/app/engine/engine.py @@ -2,9 +2,9 @@ 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 app.financial_report.choreography import create_choreography +from app.financial_report.orchestrator import create_orchestrator +from app.financial_report.workflow import create_workflow from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.workflow import Workflow @@ -24,6 +24,4 @@ def get_chat_engine( case _: agent = create_workflow(chat_history, **kwargs) - logger.info(f"Using agent pattern: {agent_type}") - return agent diff --git a/templates/components/multiagent/python/app/examples/choreography.py b/templates/components/multiagent/python/app/examples/choreography.py deleted file mode 100644 index b194c20b7..000000000 --- a/templates/components/multiagent/python/app/examples/choreography.py +++ /dev/null @@ -1,34 +0,0 @@ -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, **kwargs): - researcher = create_researcher(chat_history, **kwargs) - 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 deleted file mode 100644 index 7cf74c0cb..000000000 --- a/templates/components/multiagent/python/app/examples/orchestrator.py +++ /dev/null @@ -1,44 +0,0 @@ -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, **kwargs): - researcher = create_researcher(chat_history, **kwargs) - 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 deleted file mode 100644 index 2a170d886..000000000 --- a/templates/components/multiagent/python/app/examples/publisher.py +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index abcc2da38..000000000 --- a/templates/components/multiagent/python/app/examples/researcher.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -from textwrap import dedent -from typing import List - -from app.agents.single import FunctionCallingAgent -from app.engine.index import IndexConfig, 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(params=None) -> QueryEngineTool: - """ - Provide an agent worker that can be used to query the index. - """ - # Add query tool if index exists - index_config = IndexConfig(**(params or {})) - index = get_index(index_config) - 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(**kwargs) -> QueryEngineTool: - """ - Researcher take responsibility for retrieving information. - Try init wikipedia or duckduckgo tool if available. - """ - tools = [] - query_engine_tool = _create_query_engine_tool(**kwargs) - 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], **kwargs): - """ - Researcher is an agent that take responsibility for using tools to complete a given task. - """ - tools = _get_research_tools(**kwargs) - 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 deleted file mode 100644 index 89bd6dbaf..000000000 --- a/templates/components/multiagent/python/app/examples/workflow.py +++ /dev/null @@ -1,265 +0,0 @@ -from textwrap import dedent -from typing import AsyncGenerator, List, Optional - -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.prompts import PromptTemplate -from llama_index.core.settings import Settings -from llama_index.core.workflow import ( - Context, - Event, - StartEvent, - StopEvent, - Workflow, - step, -) - - -def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs): - researcher = create_researcher( - chat_history=chat_history, - **kwargs, - ) - 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 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( - dedent( - """ - You are an expert in decision-making, helping people write and publish blog posts. - If the user is asking for a file or to publish content, respond with 'publish'. - If the user requests to write or update a blog post, respond with 'not_publish'. - - Here is the chat history: - {chat_history} - - The current user request is: - {input} - - Given the chat history and the new user request, decide whether to publish based on existing information. - 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, - f"Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: {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/python/app/financial_report/agents/analyst.py b/templates/components/multiagent/python/app/financial_report/agents/analyst.py new file mode 100644 index 000000000..42b302553 --- /dev/null +++ b/templates/components/multiagent/python/app/financial_report/agents/analyst.py @@ -0,0 +1,34 @@ +from textwrap import dedent +from typing import List, Tuple + +from app.agents.single import FunctionCallingAgent +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.tools import FunctionTool + + +def _get_analyst_params() -> Tuple[List[type[FunctionTool]], str, str]: + tools = [] + prompt_instructions = dedent( + """ + You are an expert in analyzing financial data. + You are given a task and a set of financial data to analyze. Your task is to analyze the financial data and return a report. + Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find. + Construct the analysis in a textual format like tables would be great! + Don't need to synthesize the data, just analyze and provide your findings. + Always use the provided information, don't make up any information yourself. + """ + ) + description = "Expert in analyzing financial data." + return tools, prompt_instructions, description + + +def create_analyst(chat_history: List[ChatMessage]): + tools, prompt_instructions, description = _get_analyst_params() + + return FunctionCallingAgent( + name="analyst", + tools=tools, + description=description, + system_prompt=dedent(prompt_instructions), + chat_history=chat_history, + ) diff --git a/templates/components/multiagent/python/app/financial_report/agents/reporter.py b/templates/components/multiagent/python/app/financial_report/agents/reporter.py new file mode 100644 index 000000000..a9698d326 --- /dev/null +++ b/templates/components/multiagent/python/app/financial_report/agents/reporter.py @@ -0,0 +1,43 @@ +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 BaseTool + + +def _get_reporter_params( + chat_history: List[ChatMessage], +) -> Tuple[List[type[BaseTool]], str, str]: + tools: List[type[BaseTool]] = [] + description = "Expert in representing a financial report" + prompt_instructions = dedent( + """ + You are a report generation assistant tasked with producing a well-formatted report given parsed context. + Given a comprehensive analysis of the user request, your task is to synthesize the information and return a well-formatted report. + + ## Instructions + You are responsible for representing the analysis in a well-formatted report. If tables or visualizations provided, add them to the right sections that are most relevant. + Finally, the report should be presented in markdown format. + """ + ) + configured_tools = ToolFactory.from_env(map_result=True) + if "document_generator" in configured_tools: # type: ignore + tools.extend(configured_tools["document_generator"]) # type: ignore + prompt_instructions += ( + "\nYou are also able to generate a file document (PDF/HTML) of the report." + ) + description += " and generate a file document (PDF/HTML) of the report." + return tools, description, prompt_instructions + + +def create_reporter(chat_history: List[ChatMessage]): + tools, description, prompt_instructions = _get_reporter_params(chat_history) + return FunctionCallingAgent( + name="reporter", + tools=tools, + description=description, + system_prompt=prompt_instructions, + chat_history=chat_history, + ) diff --git a/templates/components/multiagent/python/app/financial_report/agents/researcher.py b/templates/components/multiagent/python/app/financial_report/agents/researcher.py new file mode 100644 index 000000000..f1147bf0f --- /dev/null +++ b/templates/components/multiagent/python/app/financial_report/agents/researcher.py @@ -0,0 +1,102 @@ +import os +from textwrap import dedent +from typing import List + +from app.agents.single import FunctionCallingAgent +from app.engine.index import IndexConfig, get_index +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.tools import BaseTool, QueryEngineTool, ToolMetadata +from llama_index.indices.managed.llama_cloud import LlamaCloudIndex + + +def _create_query_engine_tools(params=None) -> list[type[BaseTool]]: + """ + Provide an agent worker that can be used to query the index. + """ + # Add query tool if index exists + index_config = IndexConfig(**(params or {})) + index = get_index(index_config) + if index is None: + return None + + top_k = int(os.getenv("TOP_K", 5)) + + # Construct query engine tools + tools = [] + # If index is LlamaCloudIndex, we need to add chunk and doc retriever tools + if isinstance(index, LlamaCloudIndex): + # Document retriever + doc_retriever = index.as_query_engine( + retriever_mode="files_via_content", + similarity_top_k=top_k, + ) + chunk_retriever = index.as_query_engine( + retriever_mode="chunks", + similarity_top_k=top_k, + ) + tools.append( + QueryEngineTool( + query_engine=doc_retriever, + metadata=ToolMetadata( + name="document_retriever", + description=dedent( + """ + Document retriever that retrieves entire documents from the corpus. + ONLY use for research questions that may require searching over entire research reports. + Will be slower and more expensive than chunk-level retrieval but may be necessary. + """ + ), + ), + ) + ) + tools.append( + QueryEngineTool( + query_engine=chunk_retriever, + metadata=ToolMetadata( + name="chunk_retriever", + description=dedent( + """ + Retrieves a small set of relevant document chunks from the corpus. + Use for research questions that want to look up specific facts from the knowledge corpus, + and need entire documents. + """ + ), + ), + ) + ) + else: + query_engine = index.as_query_engine( + **({"similarity_top_k": top_k} if top_k != 0 else {}) + ) + tools.append( + QueryEngineTool( + query_engine=query_engine, + metadata=ToolMetadata( + name="retrieve_information", + description="Use this tool to retrieve information about the text corpus from the index.", + ), + ) + ) + return tools + + +def create_researcher(chat_history: List[ChatMessage], **kwargs): + """ + Researcher is an agent that take responsibility for using tools to complete a given task. + """ + tools = _create_query_engine_tools(**kwargs) + + return FunctionCallingAgent( + name="researcher", + tools=tools, + description="expert in retrieving any unknown content from the corpus", + system_prompt=dedent( + """ + You are a researcher agent. You are responsible for retrieving information from the corpus. + ## Instructions + + Don't synthesize the information, just return the whole retrieved information. + + Don't need to retrieve the information that is already provided in the chat history and response with: "There is no new information, please reuse the information from the conversation." + """ + ), + chat_history=chat_history, + ) diff --git a/templates/components/multiagent/python/app/financial_report/choreography.py b/templates/components/multiagent/python/app/financial_report/choreography.py new file mode 100644 index 000000000..df08179f0 --- /dev/null +++ b/templates/components/multiagent/python/app/financial_report/choreography.py @@ -0,0 +1,27 @@ +from textwrap import dedent +from typing import List, Optional + +from app.agents.multi import AgentCallingAgent +from app.financial_report.agents.analyst import create_analyst +from app.financial_report.agents.researcher import create_researcher +from llama_index.core.chat_engine.types import ChatMessage + + +def create_choreography(chat_history: Optional[List[ChatMessage]] = None, **kwargs): + researcher = create_researcher(chat_history, **kwargs) + analyst = create_analyst(chat_history) + return AgentCallingAgent( + name="reporter", + agents=[researcher, analyst], + description="expert in writing financial reports, needs researched information and images to write a financial report", + system_prompt=dedent( + """ + You are an expert in writing financial reports. You are given a task to write a financial report. + Before starting to write the report, consult the researcher and analyst agents to get the information you need. + Finally, create a report with the information you have gathered in markdown format. + Don't make up any information yourself. + """ + ), + # TODO: add chat_history support to AgentCallingAgent + # chat_history=chat_history, + ) diff --git a/templates/components/multiagent/python/app/financial_report/orchestrator.py b/templates/components/multiagent/python/app/financial_report/orchestrator.py new file mode 100644 index 000000000..f103cc31d --- /dev/null +++ b/templates/components/multiagent/python/app/financial_report/orchestrator.py @@ -0,0 +1,18 @@ +from typing import List, Optional + +from app.agents.multi import AgentOrchestrator +from app.financial_report.agents.analyst import create_analyst +from app.financial_report.agents.reporter import create_reporter +from app.financial_report.agents.researcher import create_researcher +from llama_index.core.chat_engine.types import ChatMessage + + +def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None, **kwargs): + researcher = create_researcher(chat_history, **kwargs) + analyst = create_analyst(chat_history) + reporter = create_reporter(chat_history) + return AgentOrchestrator( + agents=[researcher, analyst, reporter], + refine_plan=False, + chat_history=chat_history, + ) diff --git a/templates/components/multiagent/python/app/financial_report/workflow.py b/templates/components/multiagent/python/app/financial_report/workflow.py new file mode 100644 index 000000000..d909d865b --- /dev/null +++ b/templates/components/multiagent/python/app/financial_report/workflow.py @@ -0,0 +1,176 @@ +from textwrap import dedent +from typing import AsyncGenerator, List, Optional + +from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent +from app.financial_report.agents.analyst import create_analyst +from app.financial_report.agents.reporter import create_reporter +from app.financial_report.agents.researcher import create_researcher +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.prompts import PromptTemplate +from llama_index.core.settings import Settings +from llama_index.core.workflow import ( + Context, + Event, + StartEvent, + StopEvent, + Workflow, + step, +) + + +def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs): + researcher = create_researcher( + chat_history=chat_history, + **kwargs, + ) + + analyst = create_analyst(chat_history=chat_history) + + reporter = create_reporter(chat_history=chat_history) + + workflow = FinancialReportWorkflow(timeout=360, chat_history=chat_history) + + workflow.add_workflows( + researcher=researcher, + analyst=analyst, + reporter=reporter, + ) + return workflow + + +class ResearchEvent(Event): + input: str + + +class AnalyzeEvent(Event): + input: str + is_good: bool = False + + +class ReportEvent(Event): + input: str + + +class FinancialReportWorkflow(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 | ReportEvent: + # 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 ReportEvent( + input=f"Create a report 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: + # TODO: Refactor this by using prompt generation + prompt_template = PromptTemplate( + dedent( + """ + You are an expert in decision-making, helping people create financial reports for the provided data. + If the conversation already has a report and user wants to get a file/document of the report, respond with 'publish'. + Otherwise, respond with 'research'. + + Here is the chat history: + {chat_history} + + The current user request is: + {input} + + Given the chat history and the new user request, decide whether to create a report based on existing information. + 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 + ) -> AnalyzeEvent: + result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input) + content = result.response.message.content + return AnalyzeEvent( + input=dedent( + f""" + Given the following research content: + {content} + Provide a comprehensive analysis of the data for the user's request: {ctx.data["task"]} + """ + ) + ) + + @step() + async def analyze( + self, ctx: Context, ev: AnalyzeEvent, analyst: FunctionCallingAgent + ) -> ReportEvent | StopEvent: + result: AgentRunResult = await self.run_agent(ctx, analyst, ev.input) + content = result.response.message.content + return ReportEvent( + input=dedent( + f""" + Given the following analysis: + {content} + Create a report for the user's request: {ctx.data["task"]} + """ + ) + ) + + @step() + async def report( + self, ctx: Context, ev: ReportEvent, reporter: FunctionCallingAgent + ) -> StopEvent: + try: + result: AgentRunResult = await self.run_agent(ctx, reporter, ev.input) + return StopEvent(result=result) + except Exception as e: + ctx.write_event_to_stream( + AgentRunEvent( + name=reporter.name, + msg=f"Error creating a report: {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 From 5838a519962f9485cf7a580fb8aa9b3aacec59fa Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 10:15:11 +0700 Subject: [PATCH 02/13] refactor code --- create-app.ts | 2 + e2e/shared/multiagent_template.spec.ts | 112 ++++---- e2e/utils.ts | 5 + helpers/datasources.ts | 8 +- helpers/index.ts | 25 +- helpers/python.ts | 19 ++ helpers/types.ts | 11 +- index.ts | 9 + questions/simple.ts | 53 ++-- .../agents/python/blog/choreography.py | 34 +++ .../agents/python/blog/orchestrator.py | 44 +++ .../agents/python/blog/publisher.py | 35 +++ .../agents/python/blog/researcher.py | 86 ++++++ .../components/agents/python/blog/workflow.py | 265 ++++++++++++++++++ .../python/financial_report}/analyst.py | 2 +- .../python}/financial_report/choreography.py | 6 +- .../python}/financial_report/orchestrator.py | 8 +- .../python/financial_report}/reporter.py | 2 +- .../python/financial_report}/researcher.py | 2 +- .../python}/financial_report/workflow.py | 8 +- .../python/app/api/routers/vercel_response.py | 2 +- .../multiagent/python/app/engine/engine.py | 6 +- .../python/app/{agents => workflows}/multi.py | 0 .../app/{agents => workflows}/planner.py | 2 +- .../app/{agents => workflows}/single.py | 0 25 files changed, 642 insertions(+), 104 deletions(-) create mode 100644 templates/components/agents/python/blog/choreography.py create mode 100644 templates/components/agents/python/blog/orchestrator.py create mode 100644 templates/components/agents/python/blog/publisher.py create mode 100644 templates/components/agents/python/blog/researcher.py create mode 100644 templates/components/agents/python/blog/workflow.py rename templates/components/{multiagent/python/app/financial_report/agents => agents/python/financial_report}/analyst.py (96%) rename templates/components/{multiagent/python/app => agents/python}/financial_report/choreography.py (85%) rename templates/components/{multiagent/python/app => agents/python}/financial_report/orchestrator.py (66%) rename templates/components/{multiagent/python/app/financial_report/agents => agents/python/financial_report}/reporter.py (97%) rename templates/components/{multiagent/python/app/financial_report/agents => agents/python/financial_report}/researcher.py (98%) rename templates/components/{multiagent/python/app => agents/python}/financial_report/workflow.py (95%) rename templates/components/multiagent/python/app/{agents => workflows}/multi.py (100%) rename templates/components/multiagent/python/app/{agents => workflows}/planner.py (99%) rename templates/components/multiagent/python/app/{agents => workflows}/single.py (100%) diff --git a/create-app.ts b/create-app.ts index 7e00e0bab..144fdc7e2 100644 --- a/create-app.ts +++ b/create-app.ts @@ -41,6 +41,7 @@ export async function createApp({ tools, useLlamaParse, observability, + agents, }: InstallAppArgs): Promise { const root = path.resolve(appPath); @@ -86,6 +87,7 @@ export async function createApp({ tools, useLlamaParse, observability, + agents, }; if (frontend) { diff --git a/e2e/shared/multiagent_template.spec.ts b/e2e/shared/multiagent_template.spec.ts index c330b2c95..f470b3492 100644 --- a/e2e/shared/multiagent_template.spec.ts +++ b/e2e/shared/multiagent_template.spec.ts @@ -18,68 +18,72 @@ const templateUI: TemplateUI = "shadcn"; const templatePostInstallAction: TemplatePostInstallAction = "runApp"; const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend"; const userMessage = "Write a blog post about physical standards for letters"; +const templateAgents = ["financial_report", "blog"]; -test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => { - test.skip( - process.platform !== "linux" || process.env.DATASOURCE === "--no-files", - "The multiagent template currently only works with files. We also only run on Linux to speed up tests.", - ); - let port: number; - let externalPort: number; - let cwd: string; - let name: string; - let appProcess: ChildProcess; - // Only test without using vector db for now - const vectorDb = "none"; +for (const agents of templateAgents) { + test.describe(`Test multiagent template ${agents} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => { + test.skip( + process.platform !== "linux" || process.env.DATASOURCE === "--no-files", + "The multiagent template currently only works with files. We also only run on Linux to speed up tests.", + ); + let port: number; + let externalPort: number; + let cwd: string; + let name: string; + let appProcess: ChildProcess; + // Only test without using vector db for now + const vectorDb = "none"; - test.beforeAll(async () => { - port = Math.floor(Math.random() * 10000) + 10000; - externalPort = port + 1; - cwd = await createTestDir(); - const result = await runCreateLlama({ - cwd, - templateType: "multiagent", - templateFramework, - dataSource, - vectorDb, - port, - externalPort, - postInstallAction: templatePostInstallAction, - templateUI, - appType, + test.beforeAll(async () => { + port = Math.floor(Math.random() * 10000) + 10000; + externalPort = port + 1; + cwd = await createTestDir(); + const result = await runCreateLlama({ + cwd, + templateType: "multiagent", + templateFramework, + dataSource, + vectorDb, + port, + externalPort, + postInstallAction: templatePostInstallAction, + templateUI, + appType, + agents, + }); + name = result.projectName; + appProcess = result.appProcess; }); - name = result.projectName; - appProcess = result.appProcess; - }); - test("App folder should exist", async () => { - const dirExists = fs.existsSync(path.join(cwd, name)); - expect(dirExists).toBeTruthy(); - }); + test("App folder should exist", async () => { + const dirExists = fs.existsSync(path.join(cwd, name)); + expect(dirExists).toBeTruthy(); + }); - test("Frontend should have a title", async ({ page }) => { - await page.goto(`http://localhost:${port}`); - await expect(page.getByText("Built by LlamaIndex")).toBeVisible(); - }); + test("Frontend should have a title", async ({ page }) => { + await page.goto(`http://localhost:${port}`); + await expect(page.getByText("Built by LlamaIndex")).toBeVisible(); + }); - test("Frontend should be able to submit a message and receive the start of a streamed response", async ({ - page, - }) => { - await page.goto(`http://localhost:${port}`); - await page.fill("form textarea", userMessage); + test("Frontend should be able to submit a message and receive the start of a streamed response", async ({ + page, + }) => { + await page.goto(`http://localhost:${port}`); + await page.fill("form textarea", userMessage); - const responsePromise = page.waitForResponse((res) => - res.url().includes("/api/chat"), - ); + const responsePromise = page.waitForResponse((res) => + res.url().includes("/api/chat"), + ); - await page.click("form button[type=submit]"); + await page.click("form button[type=submit]"); - const response = await responsePromise; - expect(response.ok()).toBeTruthy(); - }); + const response = await responsePromise; + expect(response.ok()).toBeTruthy(); + }); - // clean processes - test.afterAll(async () => { - appProcess?.kill(); + // clean processes + test.afterAll(async () => { + appProcess?.kill(); + }); }); -}); +} diff --git a/e2e/utils.ts b/e2e/utils.ts index 44a40d11a..a835f0f20 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -34,6 +34,7 @@ export type RunCreateLlamaOptions = { tools?: string; useLlamaParse?: boolean; observability?: string; + agents?: string; }; export async function runCreateLlama({ @@ -52,6 +53,7 @@ export async function runCreateLlama({ tools, useLlamaParse, observability, + agents, }: RunCreateLlamaOptions): Promise { if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) { throw new Error( @@ -119,6 +121,9 @@ export async function runCreateLlama({ if (observability) { commandArgs.push("--observability", observability); } + if (templateType === "multiagent" && agents) { + commandArgs.push("--agents", agents); + } const command = commandArgs.join(" "); console.log(`running command '${command}' in ${cwd}`); diff --git a/helpers/datasources.ts b/helpers/datasources.ts index 74a64451b..80b936b0d 100644 --- a/helpers/datasources.ts +++ b/helpers/datasources.ts @@ -15,13 +15,17 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [ { type: "file", config: { - path: "https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf", + url: new URL( + "https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf", + ), }, }, { type: "file", config: { - path: "https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf", + url: new URL( + "https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf", + ), }, }, ]; diff --git a/helpers/index.ts b/helpers/index.ts index c0d583ed3..8525455d4 100644 --- a/helpers/index.ts +++ b/helpers/index.ts @@ -109,16 +109,27 @@ const prepareContextData = async ( await makeDir(path.join(root, "data")); for (const dataSource of dataSources) { const dataSourceConfig = dataSource?.config as FileSourceConfig; - const dataPath = dataSourceConfig.path; - const destPath = path.join(root, "data", path.basename(dataPath)); // If the path is URLs, download the data and save it to the data directory - if (dataPath.startsWith("http")) { - console.log("Downloading file from URL:", dataPath); - await downloadFile(dataPath, destPath); + if ("url" in dataSourceConfig) { + console.log( + "Downloading file from URL:", + dataSourceConfig.url.toString(), + ); + const destPath = path.join( + root, + "data", + path.basename(dataSourceConfig.url.toString()), + ); + await downloadFile(dataSourceConfig.url.toString(), destPath); } else { // Copy local data - console.log("Copying data from path:", dataPath); - await fsExtra.copy(dataPath, destPath); + console.log("Copying data from path:", dataSourceConfig.path); + const destPath = path.join( + root, + "data", + path.basename(dataSourceConfig.path), + ); + await fsExtra.copy(dataSourceConfig.path, destPath); } } }; diff --git a/helpers/python.ts b/helpers/python.ts index 58908276b..648416ad1 100644 --- a/helpers/python.ts +++ b/helpers/python.ts @@ -362,6 +362,7 @@ export const installPythonTemplate = async ({ postInstallAction, observability, modelConfig, + agents, }: Pick< InstallTemplateArgs, | "root" @@ -373,6 +374,7 @@ export const installPythonTemplate = async ({ | "postInstallAction" | "observability" | "modelConfig" + | "agents" >) => { console.log("\nInitializing Python project with template:", template, "\n"); let templatePath; @@ -443,6 +445,23 @@ export const installPythonTemplate = async ({ cwd: path.join(compPath, "engines", "python", engine), }); + // Copy agent code + if (template === "multiagent") { + if (agents) { + await copy("**", path.join(root, "app", "agents"), { + parents: true, + cwd: path.join(compPath, "agents", "python", agents), + }); + } else { + console.log( + red( + "There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.", + ), + ); + process.exit(1); + } + } + // Copy router code await copyRouterCode(root, tools ?? []); } diff --git a/helpers/types.ts b/helpers/types.ts index c8b58bf28..d00f70e67 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -49,9 +49,13 @@ export type TemplateDataSource = { export type TemplateDataSourceType = "file" | "web" | "db"; export type TemplateObservability = "none" | "traceloop" | "llamatrace"; // Config for both file and folder -export type FileSourceConfig = { - path: string; // Could be an URL to download the data while creating the project -}; +export type FileSourceConfig = + | { + path: string; + } + | { + url: URL; + }; export type WebSourceConfig = { baseUrl?: string; prefix?: string; @@ -94,4 +98,5 @@ export interface InstallTemplateArgs { postInstallAction?: TemplatePostInstallAction; tools?: Tool[]; observability?: TemplateObservability; + agents?: string; } diff --git a/index.ts b/index.ts index 6ee0edeb1..568b7b921 100644 --- a/index.ts +++ b/index.ts @@ -208,6 +208,13 @@ const program = new Command(packageJson.name) `, false, ) + .option( + "--agents ", + ` + + Select which agents to use for the multi-agent template (e.g: financial_report, blog). +`, + ) .allowUnknownOption() .parse(process.argv); @@ -322,6 +329,8 @@ async function run(): Promise { const answers = await askQuestions(options as unknown as QuestionArgs); + console.log("answers", answers); + await createApp({ ...answers, appPath: resolvedProjectPath, diff --git a/questions/simple.ts b/questions/simple.ts index 9a5ce6b00..c74ada21e 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -11,8 +11,9 @@ type AppType = | "code_artifact" | "multiagent" | "extractor" - | "data_scientist" - | "financial_report"; + | "data_scientist"; + +type AgentTemplate = "financial_report" | "blog"; type SimpleAnswers = { appType: AppType; @@ -32,9 +33,8 @@ export const askSimpleQuestions = async ( choices: [ { title: "Agentic RAG", value: "rag" }, { title: "Data Scientist", value: "data_scientist" }, - { title: "Financial Report", value: "financial_report" }, { title: "Code Artifact Agent", value: "code_artifact" }, - { title: "Multi-Agent Report Gen", value: "multiagent" }, + { title: "Multi-Agent Financial Report", value: "multiagent" }, { title: "Structured extraction", value: "extractor" }, ], }, @@ -99,6 +99,31 @@ export const askSimpleQuestions = async ( return results; }; +const getDefaultAgentTemplateParams = (agentTemplate: AgentTemplate) => { + if (agentTemplate === "financial_report") { + return { + agents: "financial_report", + tools: getTools(["document_generator", "duckduckgo"]), + dataSources: EXAMPLE_10K_SEC_FILES, + frontend: true, + }; + } else if (agentTemplate === "blog") { + return { + agents: "blog", + tools: getTools([ + "document_generator", + "wikipedia.WikipediaToolSpec", + "duckduckgo", + "img_gen", + ]), + dataSources: [EXAMPLE_FILE], + frontend: true, + }; + } else { + throw new Error(`Unknown agent template: ${agentTemplate}`); + } +}; + const convertAnswers = async ( args: PureQuestionArgs, answers: SimpleAnswers, @@ -115,7 +140,10 @@ const convertAnswers = async ( }; const lookup: Record< AppType, - Pick & { + Pick< + QuestionResults, + "template" | "tools" | "frontend" | "dataSources" | "agents" + > & { modelConfig?: ModelConfig; } > = { @@ -132,12 +160,6 @@ const convertAnswers = async ( dataSources: [], modelConfig: MODEL_GPT4o, }, - financial_report: { - template: "multiagent", - tools: getTools(["duckduckgo", "document_generator"]), - frontend: true, - dataSources: EXAMPLE_10K_SEC_FILES, - }, code_artifact: { template: "streaming", tools: getTools(["artifact"]), @@ -147,14 +169,7 @@ const convertAnswers = async ( }, multiagent: { template: "multiagent", - tools: getTools([ - "document_generator", - "wikipedia.WikipediaToolSpec", - "duckduckgo", - "img_gen", - ]), - frontend: true, - dataSources: [EXAMPLE_FILE], + ...getDefaultAgentTemplateParams("financial_report"), }, extractor: { template: "extractor", diff --git a/templates/components/agents/python/blog/choreography.py b/templates/components/agents/python/blog/choreography.py new file mode 100644 index 000000000..ce43388e0 --- /dev/null +++ b/templates/components/agents/python/blog/choreography.py @@ -0,0 +1,34 @@ +from textwrap import dedent +from typing import List, Optional + +from app.agents.publisher import create_publisher +from app.agents.researcher import create_researcher +from app.workflows.multi import AgentCallingAgent +from app.workflows.single import FunctionCallingAgent +from llama_index.core.chat_engine.types import ChatMessage + + +def create_choreography(chat_history: Optional[List[ChatMessage]] = None, **kwargs): + researcher = create_researcher(chat_history, **kwargs) + 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/agents/python/blog/orchestrator.py b/templates/components/agents/python/blog/orchestrator.py new file mode 100644 index 000000000..c4593ea44 --- /dev/null +++ b/templates/components/agents/python/blog/orchestrator.py @@ -0,0 +1,44 @@ +from textwrap import dedent +from typing import List, Optional + +from app.agents.publisher import create_publisher +from app.agents.researcher import create_researcher +from app.workflows.multi import AgentOrchestrator +from app.workflows.single import FunctionCallingAgent +from llama_index.core.chat_engine.types import ChatMessage + + +def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None, **kwargs): + researcher = create_researcher(chat_history, **kwargs) + 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/agents/python/blog/publisher.py b/templates/components/agents/python/blog/publisher.py new file mode 100644 index 000000000..2a170d886 --- /dev/null +++ b/templates/components/agents/python/blog/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/agents/python/blog/researcher.py b/templates/components/agents/python/blog/researcher.py new file mode 100644 index 000000000..3b9ba5ed6 --- /dev/null +++ b/templates/components/agents/python/blog/researcher.py @@ -0,0 +1,86 @@ +import os +from textwrap import dedent +from typing import List + +from app.engine.index import IndexConfig, get_index +from app.engine.tools import ToolFactory +from app.workflows.single import FunctionCallingAgent +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.tools import QueryEngineTool, ToolMetadata + + +def _create_query_engine_tool(params=None) -> QueryEngineTool: + """ + Provide an agent worker that can be used to query the index. + """ + # Add query tool if index exists + index_config = IndexConfig(**(params or {})) + index = get_index(index_config) + 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(**kwargs) -> QueryEngineTool: + """ + Researcher take responsibility for retrieving information. + Try init wikipedia or duckduckgo tool if available. + """ + tools = [] + query_engine_tool = _create_query_engine_tool(**kwargs) + 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], **kwargs): + """ + Researcher is an agent that take responsibility for using tools to complete a given task. + """ + tools = _get_research_tools(**kwargs) + 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/agents/python/blog/workflow.py b/templates/components/agents/python/blog/workflow.py new file mode 100644 index 000000000..c7bfbe51e --- /dev/null +++ b/templates/components/agents/python/blog/workflow.py @@ -0,0 +1,265 @@ +from textwrap import dedent +from typing import AsyncGenerator, List, Optional + +from app.agents.publisher import create_publisher +from app.agents.researcher import create_researcher +from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.prompts import PromptTemplate +from llama_index.core.settings import Settings +from llama_index.core.workflow import ( + Context, + Event, + StartEvent, + StopEvent, + Workflow, + step, +) + + +def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs): + researcher = create_researcher( + chat_history=chat_history, + **kwargs, + ) + 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 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( + dedent( + """ + You are an expert in decision-making, helping people write and publish blog posts. + If the user is asking for a file or to publish content, respond with 'publish'. + If the user requests to write or update a blog post, respond with 'not_publish'. + + Here is the chat history: + {chat_history} + + The current user request is: + {input} + + Given the chat history and the new user request, decide whether to publish based on existing information. + 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, + f"Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: {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/python/app/financial_report/agents/analyst.py b/templates/components/agents/python/financial_report/analyst.py similarity index 96% rename from templates/components/multiagent/python/app/financial_report/agents/analyst.py rename to templates/components/agents/python/financial_report/analyst.py index 42b302553..aa573a1c7 100644 --- a/templates/components/multiagent/python/app/financial_report/agents/analyst.py +++ b/templates/components/agents/python/financial_report/analyst.py @@ -1,7 +1,7 @@ from textwrap import dedent from typing import List, Tuple -from app.agents.single import FunctionCallingAgent +from app.workflows.single import FunctionCallingAgent from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.tools import FunctionTool diff --git a/templates/components/multiagent/python/app/financial_report/choreography.py b/templates/components/agents/python/financial_report/choreography.py similarity index 85% rename from templates/components/multiagent/python/app/financial_report/choreography.py rename to templates/components/agents/python/financial_report/choreography.py index df08179f0..2c919b10e 100644 --- a/templates/components/multiagent/python/app/financial_report/choreography.py +++ b/templates/components/agents/python/financial_report/choreography.py @@ -1,9 +1,9 @@ from textwrap import dedent from typing import List, Optional -from app.agents.multi import AgentCallingAgent -from app.financial_report.agents.analyst import create_analyst -from app.financial_report.agents.researcher import create_researcher +from app.agents.analyst import create_analyst +from app.agents.researcher import create_researcher +from app.workflows.multi import AgentCallingAgent from llama_index.core.chat_engine.types import ChatMessage diff --git a/templates/components/multiagent/python/app/financial_report/orchestrator.py b/templates/components/agents/python/financial_report/orchestrator.py similarity index 66% rename from templates/components/multiagent/python/app/financial_report/orchestrator.py rename to templates/components/agents/python/financial_report/orchestrator.py index f103cc31d..3548b5c28 100644 --- a/templates/components/multiagent/python/app/financial_report/orchestrator.py +++ b/templates/components/agents/python/financial_report/orchestrator.py @@ -1,9 +1,9 @@ from typing import List, Optional -from app.agents.multi import AgentOrchestrator -from app.financial_report.agents.analyst import create_analyst -from app.financial_report.agents.reporter import create_reporter -from app.financial_report.agents.researcher import create_researcher +from app.agents.analyst import create_analyst +from app.agents.reporter import create_reporter +from app.agents.researcher import create_researcher +from app.workflows.multi import AgentOrchestrator from llama_index.core.chat_engine.types import ChatMessage diff --git a/templates/components/multiagent/python/app/financial_report/agents/reporter.py b/templates/components/agents/python/financial_report/reporter.py similarity index 97% rename from templates/components/multiagent/python/app/financial_report/agents/reporter.py rename to templates/components/agents/python/financial_report/reporter.py index a9698d326..b758ab024 100644 --- a/templates/components/multiagent/python/app/financial_report/agents/reporter.py +++ b/templates/components/agents/python/financial_report/reporter.py @@ -1,8 +1,8 @@ from textwrap import dedent from typing import List, Tuple -from app.agents.single import FunctionCallingAgent from app.engine.tools import ToolFactory +from app.workflows.single import FunctionCallingAgent from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.tools import BaseTool diff --git a/templates/components/multiagent/python/app/financial_report/agents/researcher.py b/templates/components/agents/python/financial_report/researcher.py similarity index 98% rename from templates/components/multiagent/python/app/financial_report/agents/researcher.py rename to templates/components/agents/python/financial_report/researcher.py index f1147bf0f..0dd75073f 100644 --- a/templates/components/multiagent/python/app/financial_report/agents/researcher.py +++ b/templates/components/agents/python/financial_report/researcher.py @@ -2,8 +2,8 @@ from textwrap import dedent from typing import List -from app.agents.single import FunctionCallingAgent from app.engine.index import IndexConfig, get_index +from app.workflows.single import FunctionCallingAgent from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.tools import BaseTool, QueryEngineTool, ToolMetadata from llama_index.indices.managed.llama_cloud import LlamaCloudIndex diff --git a/templates/components/multiagent/python/app/financial_report/workflow.py b/templates/components/agents/python/financial_report/workflow.py similarity index 95% rename from templates/components/multiagent/python/app/financial_report/workflow.py rename to templates/components/agents/python/financial_report/workflow.py index d909d865b..d4e81ccec 100644 --- a/templates/components/multiagent/python/app/financial_report/workflow.py +++ b/templates/components/agents/python/financial_report/workflow.py @@ -1,10 +1,10 @@ from textwrap import dedent from typing import AsyncGenerator, List, Optional -from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent -from app.financial_report.agents.analyst import create_analyst -from app.financial_report.agents.reporter import create_reporter -from app.financial_report.agents.researcher import create_researcher +from app.agents.analyst import create_analyst +from app.agents.reporter import create_reporter +from app.agents.researcher import create_researcher +from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.prompts import PromptTemplate from llama_index.core.settings import Settings diff --git a/templates/components/multiagent/python/app/api/routers/vercel_response.py b/templates/components/multiagent/python/app/api/routers/vercel_response.py index 82f2e7056..02cb61592 100644 --- a/templates/components/multiagent/python/app/api/routers/vercel_response.py +++ b/templates/components/multiagent/python/app/api/routers/vercel_response.py @@ -4,9 +4,9 @@ from typing import AsyncGenerator, List from aiostream import stream -from app.agents.single import AgentRunEvent, AgentRunResult from app.api.routers.models import ChatData, Message from app.api.services.suggestion import NextQuestionSuggestion +from app.workflows.single import AgentRunEvent, AgentRunResult from fastapi import Request from fastapi.responses import StreamingResponse diff --git a/templates/components/multiagent/python/app/engine/engine.py b/templates/components/multiagent/python/app/engine/engine.py index 3341139f8..9c455c544 100644 --- a/templates/components/multiagent/python/app/engine/engine.py +++ b/templates/components/multiagent/python/app/engine/engine.py @@ -2,9 +2,9 @@ import os from typing import List, Optional -from app.financial_report.choreography import create_choreography -from app.financial_report.orchestrator import create_orchestrator -from app.financial_report.workflow import create_workflow +from app.agents.choreography import create_choreography +from app.agents.orchestrator import create_orchestrator +from app.agents.workflow import create_workflow from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.workflow import Workflow diff --git a/templates/components/multiagent/python/app/agents/multi.py b/templates/components/multiagent/python/app/workflows/multi.py similarity index 100% rename from templates/components/multiagent/python/app/agents/multi.py rename to templates/components/multiagent/python/app/workflows/multi.py diff --git a/templates/components/multiagent/python/app/agents/planner.py b/templates/components/multiagent/python/app/workflows/planner.py similarity index 99% rename from templates/components/multiagent/python/app/agents/planner.py rename to templates/components/multiagent/python/app/workflows/planner.py index 8bb4fd721..339bb739b 100644 --- a/templates/components/multiagent/python/app/agents/planner.py +++ b/templates/components/multiagent/python/app/workflows/planner.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union -from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent +from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent from llama_index.core.agent.runner.planner import ( DEFAULT_INITIAL_PLAN_PROMPT, DEFAULT_PLAN_REFINE_PROMPT, diff --git a/templates/components/multiagent/python/app/agents/single.py b/templates/components/multiagent/python/app/workflows/single.py similarity index 100% rename from templates/components/multiagent/python/app/agents/single.py rename to templates/components/multiagent/python/app/workflows/single.py From 33e4bdc942052071509169ebb077ec676d15595d Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 10:20:09 +0700 Subject: [PATCH 03/13] fix testing --- index.ts | 2 -- templates/components/agents/python/blog/publisher.py | 2 +- templates/components/multiagent/python/app/workflows/multi.py | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index 568b7b921..de7f5b649 100644 --- a/index.ts +++ b/index.ts @@ -329,8 +329,6 @@ async function run(): Promise { const answers = await askQuestions(options as unknown as QuestionArgs); - console.log("answers", answers); - await createApp({ ...answers, appPath: resolvedProjectPath, diff --git a/templates/components/agents/python/blog/publisher.py b/templates/components/agents/python/blog/publisher.py index 2a170d886..5dfc6c778 100644 --- a/templates/components/agents/python/blog/publisher.py +++ b/templates/components/agents/python/blog/publisher.py @@ -1,8 +1,8 @@ from textwrap import dedent from typing import List, Tuple -from app.agents.single import FunctionCallingAgent from app.engine.tools import ToolFactory +from app.workflows.single import FunctionCallingAgent from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.tools import FunctionTool diff --git a/templates/components/multiagent/python/app/workflows/multi.py b/templates/components/multiagent/python/app/workflows/multi.py index 115038502..5ec62d0db 100644 --- a/templates/components/multiagent/python/app/workflows/multi.py +++ b/templates/components/multiagent/python/app/workflows/multi.py @@ -1,7 +1,7 @@ from typing import Any, List -from app.agents.planner import StructuredPlannerAgent -from app.agents.single import ( +from app.workflows.planner import StructuredPlannerAgent +from app.workflows.single import ( AgentRunResult, ContextAwareTool, FunctionCallingAgent, From 37fa08ebf650ffc11a0550e42b2905e0ff3124d1 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 10:28:55 +0700 Subject: [PATCH 04/13] add back interpreter tool --- questions/simple.ts | 2 +- .../agents/python/financial_report/analyst.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/questions/simple.ts b/questions/simple.ts index c74ada21e..04ebaf3ef 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -103,7 +103,7 @@ const getDefaultAgentTemplateParams = (agentTemplate: AgentTemplate) => { if (agentTemplate === "financial_report") { return { agents: "financial_report", - tools: getTools(["document_generator", "duckduckgo"]), + tools: getTools(["document_generator", "interpreter"]), dataSources: EXAMPLE_10K_SEC_FILES, frontend: true, }; diff --git a/templates/components/agents/python/financial_report/analyst.py b/templates/components/agents/python/financial_report/analyst.py index aa573a1c7..5a838d66f 100644 --- a/templates/components/agents/python/financial_report/analyst.py +++ b/templates/components/agents/python/financial_report/analyst.py @@ -1,6 +1,7 @@ from textwrap import dedent from typing import List, Tuple +from app.engine.tools import ToolFactory from app.workflows.single import FunctionCallingAgent from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.tools import FunctionTool @@ -18,7 +19,16 @@ def _get_analyst_params() -> Tuple[List[type[FunctionTool]], str, str]: Always use the provided information, don't make up any information yourself. """ ) - description = "Expert in analyzing financial data." + configured_tools = ToolFactory.from_env(map_result=True) + # Check if the interpreter tool is configured + if "interpreter" in configured_tools.keys(): + tools.extend(configured_tools["interpreter"]) + prompt_instructions += dedent(""" + You are able to visualize the financial data using code interpreter tool. + It's very useful to create and include visualizations to the report (make sure you include the right code and data for the visualization). + Never include any code into the report, just the visualization. + """) + description = "Expert in analyzing financial data, able to visualize the financial data using code interpreter tool." return tools, prompt_instructions, description From ee93981178f915655397de68edc5866639c83d45 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 10:39:50 +0700 Subject: [PATCH 05/13] fix test --- .../components/agents/python/financial_report/analyst.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/components/agents/python/financial_report/analyst.py b/templates/components/agents/python/financial_report/analyst.py index 5a838d66f..f86b10d92 100644 --- a/templates/components/agents/python/financial_report/analyst.py +++ b/templates/components/agents/python/financial_report/analyst.py @@ -19,6 +19,7 @@ def _get_analyst_params() -> Tuple[List[type[FunctionTool]], str, str]: Always use the provided information, don't make up any information yourself. """ ) + description = "Expert in analyzing financial data" configured_tools = ToolFactory.from_env(map_result=True) # Check if the interpreter tool is configured if "interpreter" in configured_tools.keys(): @@ -28,7 +29,9 @@ def _get_analyst_params() -> Tuple[List[type[FunctionTool]], str, str]: It's very useful to create and include visualizations to the report (make sure you include the right code and data for the visualization). Never include any code into the report, just the visualization. """) - description = "Expert in analyzing financial data, able to visualize the financial data using code interpreter tool." + description += ( + ", able to visualize the financial data using code interpreter tool." + ) return tools, prompt_instructions, description From 0339b6528a0cb02a4101d11b642600397178ae9a Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 13:55:15 +0700 Subject: [PATCH 06/13] reduce finance code --- helpers/python.ts | 2 +- helpers/types.ts | 3 +- questions/simple.ts | 13 ++-- .../agents/python/blog/README-template.md | 64 +++++++++++++++++++ .../blog/{ => app/agents}/choreography.py | 0 .../blog/{ => app/agents}/orchestrator.py | 0 .../python/blog/{ => app/agents}/publisher.py | 0 .../blog/{ => app/agents}/researcher.py | 0 .../python/blog/{ => app/agents}/workflow.py | 0 .../python/blog}/app/engine/engine.py | 2 + .../financial_report}/README-template.md | 16 ----- .../{ => app/agents}/analyst.py | 0 .../{ => app/agents}/reporter.py | 0 .../{ => app/agents}/researcher.py | 0 .../{ => app/agents}/workflow.py | 0 .../financial_report/app/engine/engine.py | 15 +++++ .../python/financial_report/choreography.py | 27 -------- .../python/financial_report/orchestrator.py | 18 ------ .../python/app/api/routers/vercel_response.py | 1 - 19 files changed, 91 insertions(+), 70 deletions(-) create mode 100644 templates/components/agents/python/blog/README-template.md rename templates/components/agents/python/blog/{ => app/agents}/choreography.py (100%) rename templates/components/agents/python/blog/{ => app/agents}/orchestrator.py (100%) rename templates/components/agents/python/blog/{ => app/agents}/publisher.py (100%) rename templates/components/agents/python/blog/{ => app/agents}/researcher.py (100%) rename templates/components/agents/python/blog/{ => app/agents}/workflow.py (100%) rename templates/components/{multiagent/python => agents/python/blog}/app/engine/engine.py (94%) rename templates/components/{multiagent/python => agents/python/financial_report}/README-template.md (69%) rename templates/components/agents/python/financial_report/{ => app/agents}/analyst.py (100%) rename templates/components/agents/python/financial_report/{ => app/agents}/reporter.py (100%) rename templates/components/agents/python/financial_report/{ => app/agents}/researcher.py (100%) rename templates/components/agents/python/financial_report/{ => app/agents}/workflow.py (100%) create mode 100644 templates/components/agents/python/financial_report/app/engine/engine.py delete mode 100644 templates/components/agents/python/financial_report/choreography.py delete mode 100644 templates/components/agents/python/financial_report/orchestrator.py diff --git a/helpers/python.ts b/helpers/python.ts index 648416ad1..6403a058f 100644 --- a/helpers/python.ts +++ b/helpers/python.ts @@ -448,7 +448,7 @@ export const installPythonTemplate = async ({ // Copy agent code if (template === "multiagent") { if (agents) { - await copy("**", path.join(root, "app", "agents"), { + await copy("**", path.join(root), { parents: true, cwd: path.join(compPath, "agents", "python", agents), }); diff --git a/helpers/types.ts b/helpers/types.ts index d00f70e67..aca2667f7 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -48,6 +48,7 @@ export type TemplateDataSource = { }; export type TemplateDataSourceType = "file" | "web" | "db"; export type TemplateObservability = "none" | "traceloop" | "llamatrace"; +export type TemplateAgent = "financial_report" | "blog"; // Config for both file and folder export type FileSourceConfig = | { @@ -98,5 +99,5 @@ export interface InstallTemplateArgs { postInstallAction?: TemplatePostInstallAction; tools?: Tool[]; observability?: TemplateObservability; - agents?: string; + agents?: TemplateAgent; } diff --git a/questions/simple.ts b/questions/simple.ts index 04ebaf3ef..98668e626 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -2,7 +2,11 @@ import prompts from "prompts"; import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources"; import { askModelConfig } from "../helpers/providers"; import { getTools } from "../helpers/tools"; -import { ModelConfig, TemplateFramework } from "../helpers/types"; +import { + ModelConfig, + TemplateAgent, + TemplateFramework, +} from "../helpers/types"; import { PureQuestionArgs, QuestionResults } from "./types"; import { askPostInstallAction, questionHandlers } from "./utils"; @@ -13,8 +17,6 @@ type AppType = | "extractor" | "data_scientist"; -type AgentTemplate = "financial_report" | "blog"; - type SimpleAnswers = { appType: AppType; language: TemplateFramework; @@ -99,17 +101,15 @@ export const askSimpleQuestions = async ( return results; }; -const getDefaultAgentTemplateParams = (agentTemplate: AgentTemplate) => { +const getDefaultAgentTemplateParams = (agentTemplate: TemplateAgent) => { if (agentTemplate === "financial_report") { return { - agents: "financial_report", tools: getTools(["document_generator", "interpreter"]), dataSources: EXAMPLE_10K_SEC_FILES, frontend: true, }; } else if (agentTemplate === "blog") { return { - agents: "blog", tools: getTools([ "document_generator", "wikipedia.WikipediaToolSpec", @@ -169,6 +169,7 @@ const convertAnswers = async ( }, multiagent: { template: "multiagent", + agents: "financial_report" as TemplateAgent, ...getDefaultAgentTemplateParams("financial_report"), }, extractor: { diff --git a/templates/components/agents/python/blog/README-template.md b/templates/components/agents/python/blog/README-template.md new file mode 100644 index 000000000..162de0c8a --- /dev/null +++ b/templates/components/agents/python/blog/README-template.md @@ -0,0 +1,64 @@ +## Overview + +This example is using three agents to generate a blog post: + +- a researcher that retrieves content via a RAG pipeline, +- a writer that specializes in writing blog posts and +- a reviewer that is reviewing the blog post. + +There are three different methods how the agents can interact to reach their goal: + +1. [Choreography](./app/examples/choreography.py) - the agents decide themselves to delegate a task to another agent +1. [Orchestrator](./app/examples/orchestrator.py) - a central orchestrator decides which agent should execute a task +1. [Explicit Workflow](./app/examples/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks + +## Getting Started + +First, setup the environment with poetry: + +> **_Note:_** This step is not needed if you are using the dev-container. + +```shell +poetry install +``` + +Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider). +Second, generate the embeddings of the documents in the `./data` directory: + +```shell +poetry run generate +``` + +Third, run the development server: + +```shell +poetry run python main.py +``` + +Per default, the example is using the explicit workflow. You can change the example by setting the `EXAMPLE_TYPE` environment variable to `choreography` or `orchestrator`. +The example provides one streaming API endpoint `/api/chat`. +You can test the endpoint with the following curl request: + +``` +curl --location 'localhost:8000/api/chat' \ +--header 'Content-Type: application/json' \ +--data '{ "messages": [{ "role": "user", "content": "Write a blog post about physical standards for letters" }] }' +``` + +You can start editing the API by modifying `app/api/routers/chat.py` or `app/examples/workflow.py`. The API auto-updates as you save the files. + +Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API. + +The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`: + +``` +ENVIRONMENT=prod poetry run python main.py +``` + +## Learn More + +To learn more about LlamaIndex, take a look at the following resources: + +- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex. +- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows. + You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome! diff --git a/templates/components/agents/python/blog/choreography.py b/templates/components/agents/python/blog/app/agents/choreography.py similarity index 100% rename from templates/components/agents/python/blog/choreography.py rename to templates/components/agents/python/blog/app/agents/choreography.py diff --git a/templates/components/agents/python/blog/orchestrator.py b/templates/components/agents/python/blog/app/agents/orchestrator.py similarity index 100% rename from templates/components/agents/python/blog/orchestrator.py rename to templates/components/agents/python/blog/app/agents/orchestrator.py diff --git a/templates/components/agents/python/blog/publisher.py b/templates/components/agents/python/blog/app/agents/publisher.py similarity index 100% rename from templates/components/agents/python/blog/publisher.py rename to templates/components/agents/python/blog/app/agents/publisher.py diff --git a/templates/components/agents/python/blog/researcher.py b/templates/components/agents/python/blog/app/agents/researcher.py similarity index 100% rename from templates/components/agents/python/blog/researcher.py rename to templates/components/agents/python/blog/app/agents/researcher.py diff --git a/templates/components/agents/python/blog/workflow.py b/templates/components/agents/python/blog/app/agents/workflow.py similarity index 100% rename from templates/components/agents/python/blog/workflow.py rename to templates/components/agents/python/blog/app/agents/workflow.py diff --git a/templates/components/multiagent/python/app/engine/engine.py b/templates/components/agents/python/blog/app/engine/engine.py similarity index 94% rename from templates/components/multiagent/python/app/engine/engine.py rename to templates/components/agents/python/blog/app/engine/engine.py index 9c455c544..78a79c459 100644 --- a/templates/components/multiagent/python/app/engine/engine.py +++ b/templates/components/agents/python/blog/app/engine/engine.py @@ -24,4 +24,6 @@ def get_chat_engine( case _: agent = create_workflow(chat_history, **kwargs) + logger.info(f"Using agent pattern: {agent_type}") + return agent diff --git a/templates/components/multiagent/python/README-template.md b/templates/components/agents/python/financial_report/README-template.md similarity index 69% rename from templates/components/multiagent/python/README-template.md rename to templates/components/agents/python/financial_report/README-template.md index dcc8081e7..c571a7d38 100644 --- a/templates/components/multiagent/python/README-template.md +++ b/templates/components/agents/python/financial_report/README-template.md @@ -1,19 +1,5 @@ This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/). -## Overview - -This example is using three agents to generate a blog post: - -- a researcher that retrieves content via a RAG pipeline, -- an analyst that analyzes the financial data, -- a reporter that writes the financial report. - -There are three different methods how the agents can interact to reach their goal: - -1. [Choreography](./app/financial_report/choreography.py) - the agents decide themselves to delegate a task to another agent -1. [Orchestrator](./app/financial_report/orchestrator.py) - a central orchestrator decides which agent should execute a task -1. [Explicit Workflow](./app/financial_report/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks - ## Getting Started First, setup the environment with poetry: @@ -38,8 +24,6 @@ Third, run the development server: poetry run python main.py ``` -Per default, the example is using the explicit workflow. You can change the example by setting the `EXAMPLE_TYPE` environment variable to `choreography` or `orchestrator`. - The example provides one streaming API endpoint `/api/chat`. You can test the endpoint with the following curl request: diff --git a/templates/components/agents/python/financial_report/analyst.py b/templates/components/agents/python/financial_report/app/agents/analyst.py similarity index 100% rename from templates/components/agents/python/financial_report/analyst.py rename to templates/components/agents/python/financial_report/app/agents/analyst.py diff --git a/templates/components/agents/python/financial_report/reporter.py b/templates/components/agents/python/financial_report/app/agents/reporter.py similarity index 100% rename from templates/components/agents/python/financial_report/reporter.py rename to templates/components/agents/python/financial_report/app/agents/reporter.py diff --git a/templates/components/agents/python/financial_report/researcher.py b/templates/components/agents/python/financial_report/app/agents/researcher.py similarity index 100% rename from templates/components/agents/python/financial_report/researcher.py rename to templates/components/agents/python/financial_report/app/agents/researcher.py diff --git a/templates/components/agents/python/financial_report/workflow.py b/templates/components/agents/python/financial_report/app/agents/workflow.py similarity index 100% rename from templates/components/agents/python/financial_report/workflow.py rename to templates/components/agents/python/financial_report/app/agents/workflow.py diff --git a/templates/components/agents/python/financial_report/app/engine/engine.py b/templates/components/agents/python/financial_report/app/engine/engine.py new file mode 100644 index 000000000..c2372e7ca --- /dev/null +++ b/templates/components/agents/python/financial_report/app/engine/engine.py @@ -0,0 +1,15 @@ +import logging +from typing import List, Optional + +from app.agents.workflow import create_workflow +from llama_index.core.chat_engine.types import ChatMessage +from llama_index.core.workflow import Workflow + +logger = logging.getLogger("uvicorn") + + +def get_chat_engine( + chat_history: Optional[List[ChatMessage]] = None, **kwargs +) -> Workflow: + agent_workflow = create_workflow(chat_history, **kwargs) + return agent_workflow diff --git a/templates/components/agents/python/financial_report/choreography.py b/templates/components/agents/python/financial_report/choreography.py deleted file mode 100644 index 2c919b10e..000000000 --- a/templates/components/agents/python/financial_report/choreography.py +++ /dev/null @@ -1,27 +0,0 @@ -from textwrap import dedent -from typing import List, Optional - -from app.agents.analyst import create_analyst -from app.agents.researcher import create_researcher -from app.workflows.multi import AgentCallingAgent -from llama_index.core.chat_engine.types import ChatMessage - - -def create_choreography(chat_history: Optional[List[ChatMessage]] = None, **kwargs): - researcher = create_researcher(chat_history, **kwargs) - analyst = create_analyst(chat_history) - return AgentCallingAgent( - name="reporter", - agents=[researcher, analyst], - description="expert in writing financial reports, needs researched information and images to write a financial report", - system_prompt=dedent( - """ - You are an expert in writing financial reports. You are given a task to write a financial report. - Before starting to write the report, consult the researcher and analyst agents to get the information you need. - Finally, create a report with the information you have gathered in markdown format. - Don't make up any information yourself. - """ - ), - # TODO: add chat_history support to AgentCallingAgent - # chat_history=chat_history, - ) diff --git a/templates/components/agents/python/financial_report/orchestrator.py b/templates/components/agents/python/financial_report/orchestrator.py deleted file mode 100644 index 3548b5c28..000000000 --- a/templates/components/agents/python/financial_report/orchestrator.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import List, Optional - -from app.agents.analyst import create_analyst -from app.agents.reporter import create_reporter -from app.agents.researcher import create_researcher -from app.workflows.multi import AgentOrchestrator -from llama_index.core.chat_engine.types import ChatMessage - - -def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None, **kwargs): - researcher = create_researcher(chat_history, **kwargs) - analyst = create_analyst(chat_history) - reporter = create_reporter(chat_history) - return AgentOrchestrator( - agents=[researcher, analyst, reporter], - refine_plan=False, - chat_history=chat_history, - ) diff --git a/templates/components/multiagent/python/app/api/routers/vercel_response.py b/templates/components/multiagent/python/app/api/routers/vercel_response.py index 02cb61592..2c23b6a75 100644 --- a/templates/components/multiagent/python/app/api/routers/vercel_response.py +++ b/templates/components/multiagent/python/app/api/routers/vercel_response.py @@ -28,7 +28,6 @@ def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs): super().__init__(content=content) async def content_generator(self, event_handler, events): - logger.info("Starting content_generator") stream = self._create_stream( self.request, self.chat_data, event_handler, events ) From c3213990b867f30817b38ccb1b4a7081da202026 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 14:02:06 +0700 Subject: [PATCH 07/13] wordings --- helpers/types.ts | 4 ++-- questions/simple.ts | 6 +++--- .../agents/python/financial_report/app/engine/engine.py | 3 --- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/helpers/types.ts b/helpers/types.ts index aca2667f7..30a2ab357 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -48,7 +48,7 @@ export type TemplateDataSource = { }; export type TemplateDataSourceType = "file" | "web" | "db"; export type TemplateObservability = "none" | "traceloop" | "llamatrace"; -export type TemplateAgent = "financial_report" | "blog"; +export type TemplateAgents = "financial_report" | "blog"; // Config for both file and folder export type FileSourceConfig = | { @@ -99,5 +99,5 @@ export interface InstallTemplateArgs { postInstallAction?: TemplatePostInstallAction; tools?: Tool[]; observability?: TemplateObservability; - agents?: TemplateAgent; + agents?: TemplateAgents; } diff --git a/questions/simple.ts b/questions/simple.ts index 98668e626..ad140d909 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -4,7 +4,7 @@ import { askModelConfig } from "../helpers/providers"; import { getTools } from "../helpers/tools"; import { ModelConfig, - TemplateAgent, + TemplateAgents, TemplateFramework, } from "../helpers/types"; import { PureQuestionArgs, QuestionResults } from "./types"; @@ -101,7 +101,7 @@ export const askSimpleQuestions = async ( return results; }; -const getDefaultAgentTemplateParams = (agentTemplate: TemplateAgent) => { +const getDefaultAgentTemplateParams = (agentTemplate: TemplateAgents) => { if (agentTemplate === "financial_report") { return { tools: getTools(["document_generator", "interpreter"]), @@ -169,7 +169,7 @@ const convertAnswers = async ( }, multiagent: { template: "multiagent", - agents: "financial_report" as TemplateAgent, + agents: "financial_report" as TemplateAgents, ...getDefaultAgentTemplateParams("financial_report"), }, extractor: { diff --git a/templates/components/agents/python/financial_report/app/engine/engine.py b/templates/components/agents/python/financial_report/app/engine/engine.py index c2372e7ca..0ea21ab29 100644 --- a/templates/components/agents/python/financial_report/app/engine/engine.py +++ b/templates/components/agents/python/financial_report/app/engine/engine.py @@ -1,12 +1,9 @@ -import logging from typing import List, Optional from app.agents.workflow import create_workflow from llama_index.core.chat_engine.types import ChatMessage from llama_index.core.workflow import Workflow -logger = logging.getLogger("uvicorn") - def get_chat_engine( chat_history: Optional[List[ChatMessage]] = None, **kwargs From 40caa56c9c22df45285dfaa6d765ab4e5e1c2586 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 14:20:02 +0700 Subject: [PATCH 08/13] limit support for fastapi --- helpers/python.ts | 1 + questions/simple.ts | 4 +++- .../agents/python/financial_report/README-template.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/helpers/python.ts b/helpers/python.ts index 6403a058f..6305739a2 100644 --- a/helpers/python.ts +++ b/helpers/python.ts @@ -451,6 +451,7 @@ export const installPythonTemplate = async ({ await copy("**", path.join(root), { parents: true, cwd: path.join(compPath, "agents", "python", agents), + rename: assetRelocator, }); } else { console.log( diff --git a/questions/simple.ts b/questions/simple.ts index ad140d909..5408e0108 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -46,7 +46,9 @@ export const askSimpleQuestions = async ( let language: TemplateFramework = "fastapi"; let llamaCloudKey = args.llamaCloudKey; let useLlamaCloud = false; - if (appType !== "extractor") { + // Default multiagent with financial report use case only supports Python + // TODO: Add support for Typescript frameworks + if (appType !== "extractor" && appType !== "multiagent") { const { language: newLanguage } = await prompts( { type: "select", diff --git a/templates/components/agents/python/financial_report/README-template.md b/templates/components/agents/python/financial_report/README-template.md index c571a7d38..ba6d24fba 100644 --- a/templates/components/agents/python/financial_report/README-template.md +++ b/templates/components/agents/python/financial_report/README-template.md @@ -10,7 +10,7 @@ First, setup the environment with poetry: poetry install ``` -Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider). +Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider and `E2B_API_KEY` for the [E2B's code interpreter tool](https://e2b.dev/docs)). Second, generate the embeddings of the documents in the `./data` directory: @@ -30,7 +30,7 @@ You can test the endpoint with the following curl request: ``` curl --location 'localhost:8000/api/chat' \ --header 'Content-Type: application/json' \ ---data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla." }] }' +--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }' ``` You can start editing the API by modifying `app/api/routers/chat.py` or `app/financial_report/workflow.py`. The API auto-updates as you save the files. From fb352c99ba8cd6d5f42b70e5dedae2f7a2877dcc Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 14:23:56 +0700 Subject: [PATCH 09/13] add changeset --- .changeset/spicy-apricots-double.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spicy-apricots-double.md diff --git a/.changeset/spicy-apricots-double.md b/.changeset/spicy-apricots-double.md new file mode 100644 index 000000000..24b490816 --- /dev/null +++ b/.changeset/spicy-apricots-double.md @@ -0,0 +1,5 @@ +--- +"create-llama": patch +--- + +Add financial report as the default use case in the multi-agent template (Python). From c11f53d9dbd311b6a868c1c6dbfa9ecdb714fd34 Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 14:36:51 +0700 Subject: [PATCH 10/13] minor typing enhance --- questions/simple.ts | 2 +- .../python/financial_report/app/agents/researcher.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/questions/simple.ts b/questions/simple.ts index 5408e0108..8350166a9 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -171,7 +171,7 @@ const convertAnswers = async ( }, multiagent: { template: "multiagent", - agents: "financial_report" as TemplateAgents, + agents: "financial_report", ...getDefaultAgentTemplateParams("financial_report"), }, extractor: { diff --git a/templates/components/agents/python/financial_report/app/agents/researcher.py b/templates/components/agents/python/financial_report/app/agents/researcher.py index 0dd75073f..4d1459a5f 100644 --- a/templates/components/agents/python/financial_report/app/agents/researcher.py +++ b/templates/components/agents/python/financial_report/app/agents/researcher.py @@ -1,6 +1,6 @@ import os from textwrap import dedent -from typing import List +from typing import List, Optional from app.engine.index import IndexConfig, get_index from app.workflows.single import FunctionCallingAgent @@ -9,7 +9,7 @@ from llama_index.indices.managed.llama_cloud import LlamaCloudIndex -def _create_query_engine_tools(params=None) -> list[type[BaseTool]]: +def _create_query_engine_tools(params=None) -> Optional[list[type[BaseTool]]]: """ Provide an agent worker that can be used to query the index. """ @@ -86,6 +86,9 @@ def create_researcher(chat_history: List[ChatMessage], **kwargs): """ tools = _create_query_engine_tools(**kwargs) + if tools is None: + raise ValueError("No tools found for researcher agent") + return FunctionCallingAgent( name="researcher", tools=tools, From e390a147e843954997b4a7c906c754d23cc7d94e Mon Sep 17 00:00:00 2001 From: leehuwuj Date: Wed, 23 Oct 2024 14:58:12 +0700 Subject: [PATCH 11/13] fix missing data source question and tunning prompt --- questions/simple.ts | 35 ++++++++++--------- .../financial_report/app/agents/reporter.py | 1 + .../financial_report/app/agents/workflow.py | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/questions/simple.ts b/questions/simple.ts index 8350166a9..2dddc154c 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -46,22 +46,25 @@ export const askSimpleQuestions = async ( let language: TemplateFramework = "fastapi"; let llamaCloudKey = args.llamaCloudKey; let useLlamaCloud = false; - // Default multiagent with financial report use case only supports Python - // TODO: Add support for Typescript frameworks - if (appType !== "extractor" && appType !== "multiagent") { - const { language: newLanguage } = await prompts( - { - type: "select", - name: "language", - message: "What language do you want to use?", - choices: [ - { title: "Python (FastAPI)", value: "fastapi" }, - { title: "Typescript (NextJS)", value: "nextjs" }, - ], - }, - questionHandlers, - ); - language = newLanguage; + + if (appType !== "extractor") { + // Default multiagent with financial report use case only supports Python + // TODO: Add support for Typescript frameworks + if (appType !== "multiagent") { + const { language: newLanguage } = await prompts( + { + type: "select", + name: "language", + message: "What language do you want to use?", + choices: [ + { title: "Python (FastAPI)", value: "fastapi" }, + { title: "Typescript (NextJS)", value: "nextjs" }, + ], + }, + questionHandlers, + ); + language = newLanguage; + } const { useLlamaCloud: newUseLlamaCloud } = await prompts( { diff --git a/templates/components/agents/python/financial_report/app/agents/reporter.py b/templates/components/agents/python/financial_report/app/agents/reporter.py index b758ab024..b1337bb03 100644 --- a/templates/components/agents/python/financial_report/app/agents/reporter.py +++ b/templates/components/agents/python/financial_report/app/agents/reporter.py @@ -19,6 +19,7 @@ def _get_reporter_params( ## Instructions You are responsible for representing the analysis in a well-formatted report. If tables or visualizations provided, add them to the right sections that are most relevant. + Use only the provided information to create the report. Do not make up any information yourself. Finally, the report should be presented in markdown format. """ ) diff --git a/templates/components/agents/python/financial_report/app/agents/workflow.py b/templates/components/agents/python/financial_report/app/agents/workflow.py index d4e81ccec..897a69be7 100644 --- a/templates/components/agents/python/financial_report/app/agents/workflow.py +++ b/templates/components/agents/python/financial_report/app/agents/workflow.py @@ -87,7 +87,7 @@ async def _decide_workflow( dedent( """ You are an expert in decision-making, helping people create financial reports for the provided data. - If the conversation already has a report and user wants to get a file/document of the report, respond with 'publish'. + If the user doesn't need to add or update anything, respond with 'publish'. Otherwise, respond with 'research'. Here is the chat history: From ade4564424b5bdaacdd397846a6b4811a7b18265 Mon Sep 17 00:00:00 2001 From: Huu Le <39040748+leehuwuj@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:18:05 +0700 Subject: [PATCH 12/13] Update templates/components/agents/python/financial_report/app/agents/workflow.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../agents/python/financial_report/app/agents/workflow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/components/agents/python/financial_report/app/agents/workflow.py b/templates/components/agents/python/financial_report/app/agents/workflow.py index 897a69be7..d995bab2e 100644 --- a/templates/components/agents/python/financial_report/app/agents/workflow.py +++ b/templates/components/agents/python/financial_report/app/agents/workflow.py @@ -44,7 +44,6 @@ class ResearchEvent(Event): class AnalyzeEvent(Event): input: str - is_good: bool = False class ReportEvent(Event): From 297b95a26bf807d568d183ad8adb1b5fd5edbb52 Mon Sep 17 00:00:00 2001 From: Marcus Schiesser Date: Wed, 23 Oct 2024 15:23:59 +0700 Subject: [PATCH 13/13] use GPT4o model for financial report gen --- questions/simple.ts | 47 +++++++++++++-------------------------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/questions/simple.ts b/questions/simple.ts index 2dddc154c..195e30d8c 100644 --- a/questions/simple.ts +++ b/questions/simple.ts @@ -2,18 +2,14 @@ import prompts from "prompts"; import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources"; import { askModelConfig } from "../helpers/providers"; import { getTools } from "../helpers/tools"; -import { - ModelConfig, - TemplateAgents, - TemplateFramework, -} from "../helpers/types"; +import { ModelConfig, TemplateFramework } from "../helpers/types"; import { PureQuestionArgs, QuestionResults } from "./types"; import { askPostInstallAction, questionHandlers } from "./utils"; type AppType = | "rag" | "code_artifact" - | "multiagent" + | "financial_report_agent" | "extractor" | "data_scientist"; @@ -35,8 +31,11 @@ export const askSimpleQuestions = async ( choices: [ { title: "Agentic RAG", value: "rag" }, { title: "Data Scientist", value: "data_scientist" }, + { + title: "Financial Report Generator (using Workflows)", + value: "financial_report_agent", + }, { title: "Code Artifact Agent", value: "code_artifact" }, - { title: "Multi-Agent Financial Report", value: "multiagent" }, { title: "Structured extraction", value: "extractor" }, ], }, @@ -48,9 +47,9 @@ export const askSimpleQuestions = async ( let useLlamaCloud = false; if (appType !== "extractor") { - // Default multiagent with financial report use case only supports Python + // Default financial report agent use case only supports Python // TODO: Add support for Typescript frameworks - if (appType !== "multiagent") { + if (appType !== "financial_report_agent") { const { language: newLanguage } = await prompts( { type: "select", @@ -106,29 +105,6 @@ export const askSimpleQuestions = async ( return results; }; -const getDefaultAgentTemplateParams = (agentTemplate: TemplateAgents) => { - if (agentTemplate === "financial_report") { - return { - tools: getTools(["document_generator", "interpreter"]), - dataSources: EXAMPLE_10K_SEC_FILES, - frontend: true, - }; - } else if (agentTemplate === "blog") { - return { - tools: getTools([ - "document_generator", - "wikipedia.WikipediaToolSpec", - "duckduckgo", - "img_gen", - ]), - dataSources: [EXAMPLE_FILE], - frontend: true, - }; - } else { - throw new Error(`Unknown agent template: ${agentTemplate}`); - } -}; - const convertAnswers = async ( args: PureQuestionArgs, answers: SimpleAnswers, @@ -172,10 +148,13 @@ const convertAnswers = async ( dataSources: [], modelConfig: MODEL_GPT4o, }, - multiagent: { + financial_report_agent: { template: "multiagent", agents: "financial_report", - ...getDefaultAgentTemplateParams("financial_report"), + tools: getTools(["document_generator", "interpreter"]), + dataSources: EXAMPLE_10K_SEC_FILES, + frontend: true, + modelConfig: MODEL_GPT4o, }, extractor: { template: "extractor",