{ "cells": [ { "cell_type": "markdown", "id": "c8a8e2eb8cec31f0", "metadata": {}, "source": [ "# Agent Executor: Blog Writing\n", "\n", "This recipe demonstrates how to build an Agent Executor using Mirascope to automate the process of researching and writing a blog post. We'll create a system that combines a researcher agent and a writer tool, orchestrated by an executor agent.\n", "\n", "
\n", "

Mirascope Concepts Used

\n", "\n", "
\n", "\n", "
\n", "

Background

\n", "

\n", "Agent-based systems in AI involve creating autonomous agents that can perform tasks or make decisions. In this recipe, we're using multiple agents (researcher and writer) coordinated by an executor to create a blog post. This approach allows for a more modular and potentially more effective content creation process.\n", "

\n", "
\n", "\n", "## System Architecture\n", "\n", "```mermaid\n", "flowchart TD\n", " AE[Agent Executor]\n", " R[Researcher.research]\n", " WID[_write_initial_draft]\n", " OAPI[OpenAI API]\n", " \n", " subgraph Researcher\n", " RA[Researcher Agent]\n", " WS[Web Search]\n", " PW[Parse Webpage]\n", " end\n", " \n", " AE --> R\n", " AE --> WID\n", " R --> RA\n", " RA --> WS\n", " RA --> PW\n", " \n", " WS -.-> OAPI\n", " PW -.-> OAPI\n", " RA -.-> OAPI\n", " WID -.-> OAPI\n", " AE -.-> OAPI\n", "\n", " classDef agent fill:#e1d5e7,stroke:#9673a6,stroke-width:2px;\n", " classDef tool fill:#fff2cc,stroke:#d6b656,stroke-width:2px;\n", " classDef api fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px;\n", " \n", " class AE,RA agent;\n", " class R,WID,WS,PW tool;\n", " class OAPI api;\n", "```" ] }, { "cell_type": "markdown", "id": "69347545", "metadata": {}, "source": [ "## Setup\n", "\n", "To set up our environment, first let's install all of the packages we will use:" ] }, { "cell_type": "code", "execution_count": null, "id": "50ef7bbb", "metadata": {}, "outputs": [], "source": [ "!pip install \"mirascope[openai]\" requests beautifulsoup4 duckduckgo-search tenacity" ] }, { "cell_type": "code", "execution_count": null, "id": "d2bf2619", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "os.environ[\"OPENAI_API_KEY\"] = \"YOUR_API_KEY\"\n", "# Set the appropriate API key for the provider you're using" ] }, { "cell_type": "markdown", "id": "3d67c0bf186c57d", "metadata": {}, "source": [ "\n", "Make sure to also set your `OPENAI_API_KEY` if you haven't already.\n", "\n", "## Implementing the `BaseAgent`\n", "\n", "First, let's create a base `OpenAIAgent` class that we can later subclass to implement specialized agents:\n", "\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "10cdf092e8dfcaa", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T16:48:00.495865Z", "start_time": "2024-09-30T16:47:59.075903Z" } }, "outputs": [], "source": [ "from abc import abstractmethod\n", "\n", "from mirascope.core import BaseMessageParam, openai\n", "from pydantic import BaseModel\n", "\n", "\n", "class OpenAIAgent(BaseModel):\n", " history: list[BaseMessageParam | openai.OpenAIMessageParam] = []\n", "\n", " @abstractmethod\n", " def _step(self, prompt: str) -> openai.OpenAIStream: ...\n", "\n", " def run(self, prompt: str) -> str:\n", " stream = self._step(prompt)\n", " result, tools_and_outputs = \"\", []\n", "\n", " for chunk, tool in stream:\n", " if tool:\n", " tools_and_outputs.append((tool, tool.call()))\n", " else:\n", " result += chunk.content\n", " print(chunk.content, end=\"\", flush=True)\n", " if stream.user_message_param:\n", " self.history.append(stream.user_message_param)\n", " self.history.append(stream.message_param)\n", " if tools_and_outputs:\n", " self.history += stream.tool_message_params(tools_and_outputs)\n", " return self.run(\"\")\n", " print(\"\\n\")\n", " return result" ] }, { "cell_type": "markdown", "id": "37b5736127849a17", "metadata": {}, "source": [ "\n", "Note that the `_step` function is marked as an abstract method that each subclass will need to implement.\n", "\n", "## Research Agent\n", "\n", "The first step to writing a good blog post is researching your topic, so let's create an agent that can search the internet and summarize relevant information that we can later consume when writing the post.\n", "\n", "### Web Search Tool\n", "\n", "We can use the `duckduckgo-search` package (with no API key!) to perform some basic keyword search on the internet. Note that we are including `self` as an argument so that we can access the state of the `Researcher` agent we will build. This enables easier configuration.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "3d3f9307fd9d6969", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T16:48:03.944251Z", "start_time": "2024-09-30T16:48:03.924591Z" } }, "outputs": [], "source": [ "import inspect\n", "\n", "from duckduckgo_search import DDGS\n", "\n", "\n", "class ResearcherBase(OpenAIAgent):\n", " max_results: int = 10\n", "\n", " def web_search(self, text: str) -> str:\n", " \"\"\"Search the web for the given text.\n", "\n", " Args:\n", " text: The text to search for.\n", "\n", " Returns:\n", " The search results for the given text formatted as newline separated\n", " dictionaries with keys 'title', 'href', and 'body'.\n", " \"\"\"\n", " try:\n", " results = DDGS(proxy=None).text(text, max_results=self.max_results)\n", " return \"\\n\\n\".join(\n", " [\n", " inspect.cleandoc(\n", " \"\"\"\n", " title: {title}\n", " href: {href}\n", " body: {body}\n", " \"\"\"\n", " ).format(**result)\n", " for result in results\n", " ]\n", " )\n", " except Exception as e:\n", " return f\"{type(e)}: Failed to search the web for text\"" ] }, { "cell_type": "markdown", "id": "4bd3a837e857de1a", "metadata": {}, "source": [ "\n", "### Parsing HTML Content\n", "\n", "Our `web_search` tool only returns search results -- not the actual content of the webpages found at the href results of our search. While we could deterministically parse every web page returned, let's instead provide our researcher with a tool for parsing the content. The value of this approach is that we can greatly increase the number of search results and let the researcher decide which of the results are worth parsing and using.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "47b2c1928511af5d", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T16:48:18.602910Z", "start_time": "2024-09-30T16:48:18.508014Z" } }, "outputs": [], "source": [ "import requests\n", "from bs4 import BeautifulSoup\n", "\n", "\n", "class ResearcherBaseWithParser(ResearcherBase):\n", " def parse_webpage(self, link: str) -> str:\n", " \"\"\"Parse the paragraphs of the webpage found at `link`.\n", "\n", " Args:\n", " link: The URL of the webpage.\n", "\n", " Returns:\n", " The parsed paragraphs of the webpage, separated by newlines.\n", " \"\"\"\n", " try:\n", " response = requests.get(link)\n", " soup = BeautifulSoup(response.content, \"html.parser\")\n", " return \"\\n\".join([p.text for p in soup.find_all(\"p\")])\n", " except Exception as e:\n", " return f\"{type(e)}: Failed to parse content from URL\"" ] }, { "cell_type": "markdown", "id": "ae471cffb23540ae", "metadata": {}, "source": [ "\n", "### Researcher Step Function\n", "\n", "Now that we have our tools we're ready to implement the `_step` method of our researcher where the majority of the remaining work lies in engineering the prompt:\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "ac3bed2e23e17ba3", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T16:48:20.913271Z", "start_time": "2024-09-30T16:48:20.905823Z" } }, "outputs": [], "source": [ "from mirascope.core import prompt_template\n", "\n", "\n", "class ResearcherBaseWithStep(ResearcherBaseWithParser):\n", " @openai.call(\"gpt-4o-mini\", stream=True)\n", " @prompt_template(\n", " \"\"\"\n", " SYSTEM:\n", " Your task is to research a topic and summarize the information you find.\n", " This information will be given to a writer (user) to create a blog post.\n", "\n", " You have access to the following tools:\n", " - `web_search`: Search the web for information. Limit to max {self.max_results}\n", " results.\n", " - `parse_webpage`: Parse the content of a webpage.\n", "\n", " When calling the `web_search` tool, the `body` is simply the body of the search\n", " result. You MUST then call the `parse_webpage` tool to get the actual content\n", " of the webpage. It is up to you to determine which search results to parse.\n", "\n", " Once you have gathered all of the information you need, generate a writeup that\n", " strikes the right balance between brevity and completeness. The goal is to\n", " provide as much information to the writer as possible without overwhelming them.\n", "\n", " MESSAGES: {self.history}\n", " USER: {prompt}\n", " \"\"\"\n", " )\n", " def _step(self, prompt: str) -> openai.OpenAIDynamicConfig:\n", " return {\"tools\": [self.web_search, self.parse_webpage]}" ] }, { "cell_type": "markdown", "id": "e098b7bdd82cc422", "metadata": {}, "source": [ "\n", "### Implementing a `research` tool method\n", "\n", "While we could use the `run` method from our `OpenAIAgent` as a tool, there is value in further engineering our prompt by providing good descriptions (and names!) for the tools we use. Putting everything together, we can expose a `research` method that we can later use as a tool in our agent executor:\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "9339d45bd43e72ba", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T16:48:24.907871Z", "start_time": "2024-09-30T16:48:24.897143Z" } }, "outputs": [], "source": [ "class Researcher(ResearcherBaseWithStep):\n", " def research(self, prompt: str) -> str:\n", " \"\"\"Research a topic and summarize the information found.\n", "\n", " Args:\n", " prompt: The user prompt to guide the research. The content of this prompt\n", " is directly responsible for the quality of the research, so it is\n", " crucial that the prompt be clear and concise.\n", "\n", " Returns:\n", " The results of the research.\n", " \"\"\"\n", " print(\"RESEARCHING...\")\n", " result = self.run(prompt)\n", " print(\"RESEARCH COMPLETE!\")\n", " return result" ] }, { "cell_type": "markdown", "id": "900964223fd27633", "metadata": {}, "source": [ "\n", "## Writing An Initial Draft\n", "\n", "The next step when writing a blog is to write an initial draft and critique it. We can then incorporate the feedback from the critique to iteratively improve the post. Let's make a call to an LLM to write this first draft as well as critique it:\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "ccbffabdb7e9eb17", "metadata": { "ExecuteTime": { "end_time": "2024-09-30T16:48:28.635646Z", "start_time": "2024-09-30T16:48:28.625336Z" } }, "outputs": [], "source": [ "from mirascope.integrations.tenacity import collect_errors\n", "from pydantic import ValidationError\n", "from tenacity import retry, wait_exponential\n", "\n", "\n", "class AgentExecutorBase(OpenAIAgent):\n", " researcher: Researcher = Researcher()\n", " num_paragraphs: int = 4\n", "\n", " class InitialDraft(BaseModel):\n", " draft: str\n", " critique: str\n", "\n", " @staticmethod\n", " def parse_initial_draft(response: InitialDraft) -> str:\n", " return f\"Draft: {response.draft}\\nCritique: {response.critique}\"\n", "\n", " @retry(\n", " wait=wait_exponential(multiplier=1, min=4, max=10),\n", " after=collect_errors(ValidationError),\n", " )\n", " @openai.call(\n", " \"gpt-4o-mini\", response_model=InitialDraft, output_parser=parse_initial_draft\n", " )\n", " @prompt_template(\n", " \"\"\"\n", " SYSTEM:\n", " Your task is to write the initial draft for a blog post based on the information\n", " provided to you by the researcher, which will be a summary of the information\n", " they found on the internet.\n", "\n", " Along with the draft, you will also write a critique of your own work. This\n", " critique is crucial for improving the quality of the draft in subsequent\n", " iterations. Ensure that the critique is thoughtful, constructive, and specific.\n", " It should strike the right balance between comprehensive and concise feedback.\n", "\n", " If for any reason you deem that the research is insufficient or unclear, you can\n", " request that additional research be conducted by the researcher. Make sure that\n", " your request is specific, clear, and concise.\n", "\n", " MESSAGES: {self.history}\n", " USER:\n", " {previous_errors}\n", " {prompt}\n", " \"\"\"\n", " )\n", " def _write_initial_draft(\n", " self, prompt: str, *, errors: list[ValidationError] | None = None\n", " ) -> openai.OpenAIDynamicConfig:\n", " \"\"\"Writes the initial draft of a blog post along with a self-critique.\n", "\n", " Args:\n", " prompt: The user prompt to guide the writing process. The content of this\n", " prompt is directly responsible for the quality of the blog post, so it\n", " is crucial that the prompt be clear and concise.\n", "\n", " Returns:\n", " The initial draft of the blog post along with a self-critique.\n", " \"\"\"\n", " return {\n", " \"computed_fields\": {\n", " \"previous_errors\": f\"Previous Errors: {errors}\" if errors else None\n", " }\n", " }" ] }, { "cell_type": "markdown", "id": "3fecfec7741c840", "metadata": {}, "source": [ "\n", "There are a few things worth noting here:\n", "\n", "- We are again using `self` for convenient access to the containing class' state. In this case we expect to put this function inside of our executor and want to give access to the conversation history -- particularly the results of the researcher.\n", "- We are using `response_model` to extract specifically the `draft` and `critique` fields.\n", "- We are using an output parser `parse_initial_draft` to parse the `InitialDraft` class into a format that is friendly for using tools (`str`).\n", "- We are using `tenacity` in order to retry should the call fail to properly generate an `InitialDraft` instance, reinserting the list of previous errors into each subsequent call.\n", "\n", "## Agent Executor\n", "\n", "Now we just need to put it all together into our `AgentExecutor` class, write our `_step` function, and run it!\n" ] }, { "cell_type": "code", "execution_count": null, "id": "46c7ee829effc51a", "metadata": {}, "outputs": [], "source": [ "class AgentExecutor(AgentExecutorBase):\n", " @openai.call(\"gpt-4o-mini\", stream=True)\n", " @prompt_template(\n", " \"\"\"\n", " SYSTEM:\n", " Your task is to facilitate the collaboration between the researcher and the\n", " blog writer. The researcher will provide the blog writer with the information\n", " they need to write a blog post, and the blog writer will draft and critique the\n", " blog post until they reach a final iteration they are satisfied with.\n", "\n", " To access the researcher and writer, you have the following tools:\n", " - `research`: Prompt the researcher to perform research.\n", " - `_write_initial_draft`: Write an initial draft with a self-critique\n", "\n", " You will need to manage the flow of information between the researcher and the\n", " blog writer, ensuring that the information provided is clear, concise, and\n", " relevant to the task at hand.\n", "\n", " The final blog post MUST have EXACTLY {self.num_paragraphs} paragraphs.\n", "\n", " MESSAGES: {self.history}\n", " USER: {prompt}\n", " \"\"\"\n", " )\n", " def _step(self, prompt: str) -> openai.OpenAIDynamicConfig:\n", " return {\"tools\": [self.researcher.research, self._write_initial_draft]}\n", "\n", "\n", "agent = AgentExecutor()\n", "print(\"STARTING AGENT EXECUTION...\")\n", "agent.run(\"Help me write a blog post about LLMs and structured outputs.\")" ] }, { "cell_type": "markdown", "id": "ecd1b2711b6cedd7", "metadata": {}, "source": [ "\n", "
\n", "

Additional Real-World Applications

\n", "
    \n", "
  1. \n", "

    Automated Content Marketing:

    \n", "
      \n", "
    • Create a system that generates targeted blog posts for different customer segments based on current market trends and company data.
    • \n", "
    • Example: An e-commerce platform could use this to write product category overviews, incorporating latest fashion trends and customer preferences.
    • \n", "
    \n", "
  2. \n", "
  3. \n", "

    Technical Documentation Generation:

    \n", "
      \n", "
    • Develop an agent that researches API changes, new features, and community feedback to automatically update and expand technical documentation.
    • \n", "
    • Example: A software company could use this to keep their SDK documentation up-to-date with each new release.
    • \n", "
    \n", "
  4. \n", "
  5. \n", "

    Personalized Learning Content:

    \n", "
      \n", "
    • Build an educational tool that creates customized study materials based on a student's learning style, current knowledge, and learning goals.
    • \n", "
    • Example: An online learning platform could generate personalized course summaries and practice exercises for each student.
    • \n", "
    \n", "
  6. \n", "
  7. \n", "

    Automated News Summary and Analysis:

    \n", "
      \n", "
    • Create a system that gathers news from various sources, summarizes key points, and generates analytical pieces on trending topics.
    • \n", "
    • Example: A news agency could use this to produce daily briefings on complex, evolving stories like economic trends or geopolitical events.
    • \n", "
    \n", "
  8. \n", "
  9. \n", "

    Scientific Literature Review Assistant:

    \n", "
      \n", "
    • Develop an agent that can scan recent publications in a specific field, summarize key findings, and draft literature review sections for research papers.
    • \n", "
    • Example: Researchers could use this to stay updated on the latest developments in their field and to assist in writing comprehensive literature reviews.
    • \n", "
    \n", "
  10. \n", "
  11. \n", "

    Legal Document Drafting:

    \n", "
      \n", "
    • Create a system that researches relevant case law and regulations to assist in drafting legal documents like contracts or briefs.
    • \n", "
    • Example: A law firm could use this to generate first drafts of standard contracts, incorporating the latest legal precedents and regulations.
    • \n", "
    \n", "
  12. \n", "
  13. \n", "

    Product Description Generator:

    \n", "
      \n", "
    • Build an agent that researches product features, customer reviews, and market trends to write engaging and informative product descriptions.
    • \n", "
    • Example: An online marketplace could use this to automatically generate or update descriptions for thousands of products.
    • \n", "
    \n", "
  14. \n", "
  15. \n", "

    Travel Guide Creation:

    \n", "
      \n", "
    • Develop a system that researches destinations, local attractions, and traveler reviews to create personalized travel guides.
    • \n", "
    • Example: A travel company could use this to generate custom itineraries and destination guides based on a traveler's preferences and budget.
    • \n", "
    \n", "
  16. \n", "
\n", "
\n", "\n", "When adapting this recipe, consider:\n", "\n", "- Implement a feedback loop where the executor can request additional research or revisions.\n", "- Add more specialized agents, such as an editor or fact-checker.\n", "- Incorporate user feedback into the writing process.\n", "- Extend the system to handle multiple blog post formats or styles.\n", "- Implement caching for research results to improve efficiency for similar topics.\n", "- Adjusting the prompts and system messages to fit your specific use case or writing style.\n", "- Experimenting with different LLM models for various tasks (research vs. writing).\n", "- Implementing error handling and logging for production use.\n", "- Optimizing the web search and parsing functions for better performance and reliability.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 5 }