[
  {
    "path": ".github/workflows/deploy-website.yaml",
    "content": "name: Deploy BondAI Website\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n\n    - name: Set up Node.js\n      uses: actions/setup-node@v2\n      with:\n        node-version: '19.7'\n\n    - name: Install Docusaurus dependencies\n      run: |\n        cd website\n        npm install\n        npm run build\n\n    - name: Deploy to S3\n      uses: jakejarvis/s3-sync-action@master\n      with:\n        args: --acl public-read --follow-symlinks --delete\n      env:\n        AWS_S3_BUCKET: bondai-docs\n        AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }}\n        AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_SECRET_ACCESS_KEY }}\n        AWS_REGION: 'us-west-2'\n        SOURCE_DIR: 'website/build'\n\n    - name: Invalidate CloudFront distribution\n      uses: chetan/invalidate-cloudfront-action@v1.3\n      env:\n        DISTRIBUTION: E1JJN112WBIR8P\n        PATHS: '/*'\n        AWS_REGION: 'us-west-2'\n        AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }}\n        AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/deploy.yaml",
    "content": "name: Deploy to PyPI and DockerHub\n\non:\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+.[0-9]+[a-zA-Z0-9]*'\n  workflow_dispatch:\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: '3.x'\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install setuptools wheel twine setuptools_scm\n\n    - name: Build and deploy to PyPI\n      run: |\n        python setup.py sdist bdist_wheel\n        twine upload dist/* -u __token__ -p ${{ secrets.PYPI_TOKEN }}\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v1\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v1\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Determine if pre-release\n      id: prerelease_check\n      run: |\n        if [[ ${{ github.ref_name }} =~ [a-zA-Z] ]]; then\n          echo \"This is a pre-release version.\"\n          echo \"::set-output name=tag_list::krohling/bondai:${{ github.ref_name }}\"\n        else\n          echo \"This is a stable release version.\"\n          echo \"::set-output name=tag_list::krohling/bondai:${{ github.ref_name }},krohling/bondai:latest\"\n        fi\n\n    - name: Build and push Docker image\n      uses: docker/build-push-action@v2\n      with:\n        context: ./docker\n        push: true\n        tags: ${{ steps.prerelease_check.outputs.tag_list }}\n        platforms: linux/amd64,linux/arm64\n"
  },
  {
    "path": ".gitignore",
    "content": "*.DS_Store\nresponse_query_storage\ngmail-token.pickle\n.debug\n.memory\n.cache\nmisc\nui/.next\nui/node_modules\nui/agent-volume\nui/data-volume\nui/app/backups\nui/tools\nui/app/_backup\nui/app/test/\ndocker/agent-volume/\n.vscode\nwebsite/build\nnode_modules/\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\n!/ui/lib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\nnotes.txt"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/psf/black\n    rev: stable\n    hooks:\n      - id: black\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Instruction & Guidelines\n\nHello there! Any kind of contribution to **BondAI** is most welcome!\n\n- If you have a question, please use GitHub\n  [discussions](https://github.com/krohling/bondai/discussions).\n- If you found a bug or have a feature request, please use GitHub\n  [issues](https://github.com/krohling/bondai/issues).\n- If you fixed a bug or implemented a new feature, please do a pull request. If it\n  is a larger change or addition, it would be great to first discuss it through an\n  [issue](https://github.com/krohling/bondai/issues).\n\n## Development Setup\n\nWarning: If you run **BondAI** on your own system, tools that interact with the file system will have full access to your local disk! I highly recommend running and testing inside of a Docker container.\n\nAlways be careful when approving any code!\n\n## Tools\n\nWhen you contribute code, please use **black** for code formatting. \n\n## Branching & Release Strategy\n\nThe default branch is called master.\nIt contains the latest features, which would be ready for deployment.\nIt is not possible to push to it directly.\nInstead, for every feature, a branch should be created, which will then be merged back into main with a pull request.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2023 Kevin Rohling\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "MANIFEST.in",
    "content": "include bondai/prompt/default_prompt_template.md\ninclude bondai/cli/onboarding_prompt_template.md"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://bondai.dev\">\n<p align=\"center\">\n<img src=\"assets/bondai-logo.png\" alt=\"Description or Alt text\" style=\"border-radius: 10px; width: 50%;\"  alt=\"logo\">\n</p>\n</a>\n\n<p align=\"center\">\n    <a href=\"https://opensource.org/licenses/MIT\"><img src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" alt=\"License: MIT\"></a>\n    <a href=\"https://pypi.org/project/bondai/\"><img src=\"https://img.shields.io/pypi/v/bondai\" alt=\"PyPI\"></a>\n    <a href=\"https://hub.docker.com/r/krohling/bondai\"><img src=\"https://img.shields.io/docker/v/krohling/bondai?logo=docker\" alt=\"Docker\"></a>\n    <a href=\"https://colab.research.google.com/drive/1Rmzosq6LD_ZR3MkqQO1M1Af27VAaYNRE?usp=sharing\" style={{marginLeft: '10px'}}>\n        <img src=\"https://colab.research.google.com/assets/colab-badge.svg\" style={{marginLeft: '10px'}} alt=\"PyPI\"/>\n    </a>\n</p>\n<p align=\"center\"><em>Build highly capable Single and Multi-Agent Systems.</em></p>\n\n# <a href=\"https://bondai.dev\">BondAI Homepage</a>\n\nCheckout the BondAI Homepage ([https://bondai.dev](https://bondai.dev)) for in depth documentation, examples and API specification.\n\n# Getting Started\n\nThere are 3 ways to use BondAI:\n\n1) 🛠️ **Command Line Interface (CLI)** - This is the easiest way to get up and running fast. Run BondAI on your command line with a pre-configured set of tools.\n\n2) 🐋 **Docker** - Running BondAI in a Docker container is recommended if you plan on using tools that run code or directly access your shell.\n\n3) 🏗️ **Start Coding with BondAI** - Integrate BondAI into your own codebase and start building your own agents.\n\n## 🚀 Installation\n\nInstalling BondAI is easy:\n\n```bash\npip install bondai\n```\n\n## 🛠️ Command Line Interface (CLI)\n\nOnce you've installed BondAI using `pip install bondai` the CLI will be available as an executable accessible simply by running `bondai` in your shell. [Learn more about all of the tools and options available through the CLI](https://bondai.dev/docs/cli).\n\nBefore running `bondai` you will need to set the OPENAI_API_KEY environment variable.\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\n```\n\nOnce the environment variable has been set you can run `bondai` to start the CLI.\n\n```bash\nLoading BondAI...\n******************ENTERING CHAT******************\nYou are entering a chat with BondAI...\nYou can exit any time by typing 'exit'.\n\nHello! I'm BondAI, your friendly and helpful assistant. I'm here to assist you with any tasks or questions you might have. How can I assist you today?\n\nI want you to write a story about unicorns and save it to a file named unicorns.md.\nUsing tool file_write: Writing a story about unicorns and saving it to a file named unicorns.md\nUsing tool final_answer...\n\nA story about unicorns has been successfully written and saved to a file named unicorns.md. The story is set in an enchanted forest and describes the magical and majestic nature of unicorns, their daily routines, and their harmonious relationship with other creatures in the forest.\n```\n\n\n## 🐋 Docker\n\nBondAI Docker images are available on [DockerHub here](https://hub.docker.com/r/krohling/bondai). If you intend to use tools that run arbitrary code (*PythonREPLTool*) or access your shell (*ShellTool*) it is highly recommended that you run BondAI in a Docker container as these tools can damage your machine.\n\nBefore running the BondAI Docker container it is recommended that you create a directory named 'agent-volume' and mount it as a volume on the container. This will be used as the Agent's working directory and allows you to easily share files with the Agent.\n\n```bash\nmkdir agent-volume\ndocker pull krohling/bondai:latest\ndocker run -it --rm \\\n           -v ./agent-volume:/agent-volume \\\n           -w /agent-volume \\\n           OPENAI_API_KEY=sk-XXXXXXXXXX \\\n           bondai:latest bondai\n```\n\n## 🔥 Start Coding with BondAI\n\nBondAI has a straightforward API for creating powerful AI Agents. Check out our [examples](https://bondai.dev/docs/category/examples) for ideas on how to get started.  Remember to set your *OPENAI_API_KEY* environment variable before running your BondAI Agent.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.tools.search import DuckDuckGoSearchTool\nfrom bondai.tools.website import WebsiteQueryTool\nfrom bondai.tools.file import FileWriteTool\n\ntask = \"\"\"I want you to research the usage of Metformin as a drug to treat aging and aging related illness. \nYou should only use reputable information sources, ideally peer reviewed scientific studies. \nI want you to summarize your findings in a document named metformin.md and includes links to reference and resources you used to find the information. \nAdditionally, the last section of your document you should provide a recommendation for a 43 year old male, in good health and who regularly exercises as to whether he would benefit from taking Metformin. \nYou should explain your recommendation and justify it with sources. \nFinally, you should highlight potential risks and tradeoffs from taking the medication.\"\"\"\n\nAgent(tools=[\n  DuckDuckGoSearchTool(),\n  WebsiteQueryTool(),\n  FileWriteTool()\n]).run(task)\n```\n\n## BondAI Integrations\n\nBondAI comes out of the box with a powerful set of integrations.\n\n|     |  |  |\n| -------- | ------- |------- |\n| <img src=\"assets/logos/openai-logo.png\" alt=\"openai logo\" width=\"50\"/> | **OpenAI**     | BondAI supports any combination of OpenAI models and services including GPT-4, GPT-3.5, Dalle-E 3, and Embeddings.  |\n| <img src=\"assets/logos/azure-logo.png\" alt=\"azure logo\" width=\"50\"/> | **Microsoft Azure**     | BondAI fully supports connectivity to GPT-N, Dalle-E and Embedding APIs through [Microsoft's Azure OpenAI services](https://azure.microsoft.com/en-us/products/ai-services/openai-service).  |\n| <img src=\"assets/logos/google-logo.png\" alt=\"google logo\" width=\"50\"/>  | **Google Search**    | Allows BondAI to search the internet. [Requires a Google Search API Key and CSE ID](https://developers.google.com/custom-search/v1/introduction) |\n| <img src=\"assets/logos/duckduckgo-logo.png\" alt=\"duckduckgo logo\" width=\"50\"/> | **DuckDuckGo**     | Allows BondAI to search the internet. No API keys required. |\n| <img src=\"assets/logos/alpaca-markets-logo.jpeg\" alt=\"alpaca markets logo\" width=\"50\"/> | **Alpaca Markets**     | Allows BondAI to buy and sell stocks and crypto. [Requires an Alpaca Markets account.](https://alpaca.markets/)  |\n| <img src=\"assets/logos/postgres-logo.jpeg\" alt=\"postgres logo\" width=\"75\"/>    | **PostgreSQL**    | BondAI can automatically extract the schema from a Postgres DB and process natural language queries. |\n| <img src=\"assets/logos/blandai-logo.jpeg\" alt=\"bland.ai logo\" width=\"50\"/> | **Bland AI**     | Allows BondAI to make phone calls and process/retrieve call transcripts. [Requires a Bland.ai account.](https://www.bland.ai/)  |\n| <img src=\"assets/logos/gmail-logo.png\" alt=\"gmail logo\" width=\"50\"/> | **Gmail**     | Allows BondAI to search and read emails.  |\n"
  },
  {
    "path": "bondai/__init__.py",
    "content": ""
  },
  {
    "path": "bondai/agents/__init__.py",
    "content": "from .conversational_agent import ConversationalAgent\nfrom .agent import Agent, DEFAULT_MESSAGE_PROMPT_TEMPLATE\nfrom .conversation_member import ConversationMember, ConversationMemberEventNames\nfrom .messages import (\n    AgentMessage,\n    SystemMessage,\n    ConversationMessage,\n    ToolUsageMessage,\n    AgentMessageList,\n    message_to_dict,\n    USER_MEMBER_NAME,\n)\nfrom .util import (\n    AgentStatus,\n    AgentEventNames,\n    AgentException,\n    BudgetExceededException,\n    MaxStepsExceededException,\n    parse_response_content_message,\n)\n\n__all__ = [\n    \"ConversationalAgent\",\n    \"Agent\",\n    \"parse_response_content_message\",\n    \"DEFAULT_MESSAGE_PROMPT_TEMPLATE\",\n    \"AgentStatus\",\n    \"AgentEventNames\",\n    \"AgentException\",\n    \"BudgetExceededException\",\n    \"MaxStepsExceededException\",\n    \"ConversationMember\",\n    \"ConversationMemberEventNames\",\n    \"AgentMessage\",\n    \"SystemMessage\",\n    \"ConversationMessage\",\n    \"ToolUsageMessage\",\n    \"AgentMessageList\",\n    \"message_to_dict\",\n    \"USER_MEMBER_NAME\",\n]\n"
  },
  {
    "path": "bondai/agents/agent.py",
    "content": "import os\nimport uuid\nimport traceback\nfrom pydantic import BaseModel\nfrom datetime import datetime\nfrom typing import Dict, List, Tuple, Callable\nfrom bondai.util import EventMixin, Runnable, load_local_resource\nfrom bondai.tools import Tool, ResponseQueryTool\nfrom bondai.models import LLM, EmbeddingModel\nfrom bondai.memory import MemoryManager\nfrom bondai.prompt import JinjaPromptBuilder\nfrom bondai.models.openai import (\n    OpenAILLM,\n    OpenAIEmbeddingModel,\n    OpenAIModelNames,\n    get_total_cost,\n)\nfrom .conversation_member import ConversationMember\nfrom .messages import AgentMessage, AgentMessageList, SystemMessage, ToolUsageMessage\nfrom .compression import summarize_conversation, summarize_messages\nfrom .util import (\n    AgentStatus,\n    AgentEventNames,\n    AgentException,\n    BudgetExceededException,\n    MaxStepsExceededException,\n    ContextLengthExceededException,\n    count_request_tokens,\n    format_llm_messages,\n    execute_tool,\n)\n\n\nDEFAULT_MAX_TOOL_RETRIES = 5\nDEFAULT_MAX_TOOL_RESPONSE_TOKENS = 2000\nDEFAULT_SYSTEM_PROMPT_TEMPLATE = load_local_resource(\n    __file__, os.path.join(\"prompts\", \"react_agent_system_prompt_template.md\")\n)\nDEFAULT_MESSAGE_PROMPT_TEMPLATE = load_local_resource(\n    __file__, os.path.join(\"prompts\", \"agent_message_prompt_template.md\")\n)\n\n\nclass FinalAnswerParameters(BaseModel):\n    results: str\n\n\nclass FinalAnswerTool(Tool):\n    def __init__(self):\n        super().__init__(\n            \"final_answer\",\n            \"Use the final_answer tool once you have completed your TASK. Provide a highly detailed description of the results of your task in the 'results' parameter.\",\n            FinalAnswerParameters,\n        )\n\n    def run(self, results: str) -> Tuple[str, bool]:\n        return results, True\n\n\nclass Agent(EventMixin, Runnable):\n    def __init__(\n        self,\n        llm: LLM | None = None,\n        embedding_model: EmbeddingModel | None = None,\n        tools: List[Tool] | None = None,\n        quiet: bool = True,\n        allowed_events: List[str] | None = None,\n        messages: List[AgentMessage] | None = None,\n        system_prompt_sections: List[Callable[[], str]] | None = None,\n        system_prompt_builder: Callable[..., str] = None,\n        message_prompt_builder: Callable[..., str] = None,\n        memory_manager: MemoryManager | None = None,\n        max_context_length: int = None,\n        max_context_pressure_ratio: float = 0.8,\n        max_tool_retries: int = DEFAULT_MAX_TOOL_RETRIES,\n        max_tool_response_tokens=DEFAULT_MAX_TOOL_RESPONSE_TOKENS,\n        enable_context_compression: bool = False,\n        enable_final_answer_tool: bool = True,\n    ):\n        Runnable.__init__(self)\n        if allowed_events is None:\n            allowed_events = [\n                AgentEventNames.TOOL_SELECTED,\n                AgentEventNames.TOOL_COMPLETED,\n                AgentEventNames.TOOL_ERROR,\n                AgentEventNames.STREAMING_CONTENT_UPDATED,\n                AgentEventNames.STREAMING_FUNCTION_UPDATED,\n                AgentEventNames.CONTEXT_COMPRESSION_REQUESTED,\n            ]\n        EventMixin.__init__(self, allowed_events=allowed_events)\n\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT4_0613)\n        if embedding_model is None:\n            embedding_model = OpenAIEmbeddingModel(\n                OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n            )\n        if tools is None:\n            tools = []\n        if system_prompt_sections is None:\n            system_prompt_sections = []\n        if messages is None:\n            messages = []\n\n        self._id: str = str(uuid.uuid4())\n        self._status: AgentStatus = AgentStatus.IDLE\n        self._messages = AgentMessageList(messages=messages)\n        self._llm: LLM = llm\n        self._embedding_model: EmbeddingModel = embedding_model\n        self._tools: List[Tool] = tools\n        self._quiet: bool = quiet\n        self._system_prompt_sections: List[Callable[[], str]] = system_prompt_sections\n        self._system_prompt_builder: Callable[..., str] = system_prompt_builder\n        self._message_prompt_builder: Callable[..., str] = message_prompt_builder\n        self._memory_manager = memory_manager\n        self._max_context_length = (\n            max_context_length if max_context_length else (self._llm.max_tokens * 0.95)\n        )\n        self._max_context_pressure_ratio = max_context_pressure_ratio\n        self._max_tool_retries = max_tool_retries\n        self._max_tool_response_tokens = max_tool_response_tokens\n        self._enable_context_compression = enable_context_compression\n        if self._memory_manager:\n            self._tools.extend(self._memory_manager.tools)\n            self._system_prompt_sections.append(self._memory_manager)\n        if self._system_prompt_builder is None:\n            self._system_prompt_builder = JinjaPromptBuilder(\n                DEFAULT_SYSTEM_PROMPT_TEMPLATE\n            )\n        if self._message_prompt_builder is None:\n            self._message_prompt_builder = JinjaPromptBuilder(\n                DEFAULT_MESSAGE_PROMPT_TEMPLATE\n            )\n        if enable_final_answer_tool:\n            self._tools.append(FinalAnswerTool())\n\n    @property\n    def id(self) -> str:\n        return self._id\n\n    @property\n    def status(self) -> AgentStatus:\n        return self._status\n\n    @property\n    def tools(self) -> List[Tool]:\n        return self._tools\n\n    def clear_messages(self):\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot reset memory while agent is in a running state.\"\n            )\n        self._messages.clear()\n\n    def add_tool(self, tool: Tool):\n        if not any([t.name == tool.name for t in self._tools]):\n            self.tools.append(tool)\n\n    def remove_tool(self, tool_name: str):\n        self._tools = [t for t in self._tools if t.name != tool_name]\n\n    def to_dict(self) -> Dict:\n        return {\"id\": self.id, \"tools\": [t.name for t in self._tools]}\n\n    def save_state(self) -> Dict:\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\"Cannot save agent state while it is running.\")\n\n        state = {\"tools\": {}}\n\n        for tool in self._tools:\n            state[\"tools\"][tool.name] = tool.save_state()\n\n        return state\n\n    def load_state(self, state: Dict):\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\"Cannot load agent state while it is running.\")\n\n        for tool in self._tools:\n            if tool.name in state[\"tools\"]:\n                tool.load_state(state[\"tools\"][tool.name])\n\n    def _is_context_pressure_too_high(\n        self,\n        llm_messages: List[Dict[str, str]],\n        tools: List[Tool] | None = None,\n    ) -> float:\n        if tools is None:\n            tools = []\n        context_pressure_ratio = float(\n            count_request_tokens(self._llm, llm_messages, tools)\n        ) / float(self._max_context_length)\n        return context_pressure_ratio > self._max_context_pressure_ratio\n\n    def _get_llm_response(\n        self,\n        messages: List[Dict] | None = None,\n        tools: List[Tool] | None = None,\n        content_stream_callback: Callable[[str], None] | None = None,\n        function_stream_callback: Callable[[str], None] | None = None,\n    ) -> (str | None, Dict | None):\n        if messages is None:\n            messages = []\n        if tools is None:\n            tools = []\n\n        request_tokens = count_request_tokens(\n            llm=self._llm, messages=messages, tools=tools\n        )\n        if request_tokens > self._llm.max_tokens:\n            raise ContextLengthExceededException(\n                f\"Context length ({request_tokens}) exceeds maximum tokens allowed by LLM: {self._llm.max_tokens}\"\n            )\n\n        llm_functions = list(map(lambda t: t.get_tool_function(), tools))\n\n        if (\n            self._llm.supports_streaming\n        ):  # and (any([t.supports_streaming for t in tools]) or content_stream_callback):\n\n            def _function_stream_callback(function_name, arguments_buffer):\n                streaming_tools: [Tool] = [\n                    t for t in tools if t.name == function_name and t.supports_streaming\n                ]\n                if len(streaming_tools) > 0:\n                    tool: Tool = streaming_tools[0]\n                    tool.handle_stream_update(arguments_buffer)\n                if function_stream_callback:\n                    function_stream_callback(function_name, arguments_buffer)\n                self._trigger_event(\n                    AgentEventNames.STREAMING_FUNCTION_UPDATED,\n                    self,\n                    function_name,\n                    arguments_buffer,\n                )\n\n            def _content_stream_callback(content_buffer):\n                if content_stream_callback:\n                    content_stream_callback(content_buffer)\n                self._trigger_event(\n                    AgentEventNames.STREAMING_CONTENT_UPDATED, self, content_buffer\n                )\n\n            llm_response, llm_response_function = self._llm.get_streaming_completion(\n                messages=messages,\n                functions=llm_functions,\n                function_stream_callback=_function_stream_callback,\n                content_stream_callback=_content_stream_callback,\n            )\n        else:\n            llm_response, llm_response_function = self._llm.get_completion(\n                messages=messages,\n                functions=llm_functions,\n                # function_stream_callback=function_stream_callback,\n                # content_stream_callback=content_stream_callback\n            )\n\n        return llm_response, llm_response_function\n\n    def run(\n        self,\n        task: str,\n        max_steps: int = None,\n        max_budget: float = None,\n    ) -> ToolUsageMessage | str:\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\"Cannot start agent while it is in a running state.\")\n        self._status = AgentStatus.RUNNING\n        try:\n            return self._run_tool_loop(\n                tools=self._tools,\n                task=task,\n                starting_cost=get_total_cost(),\n                max_budget=max_budget,\n                max_steps=max_steps,\n            )\n        finally:\n            self._status = AgentStatus.IDLE\n\n    def run_async(\n        self,\n        task: str,\n        max_steps: int = None,\n        max_budget: float = None,\n    ):\n        \"\"\"Runs the agent's task in a separate thread.\"\"\"\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\"Cannot start agent while it is in a running state.\")\n\n        args = (task, max_steps, max_budget)\n        self._start_execution_thread(target=self.run, args=args)\n\n    def stop(self, timeout=10):\n        \"\"\"Gracefully stops the thread, with a timeout.\"\"\"\n        self._force_stop = True\n        for tool in self._tools:\n            tool.stop()\n\n        super().stop(timeout=timeout)\n\n    def _run_tool_loop(\n        self,\n        tools: List[Tool],\n        starting_cost: float,\n        max_budget: float = None,\n        max_steps: int = None,\n        max_tool_retries: int = None,\n        task: str | None = None,\n        prompt_vars: Dict | None = None,\n        return_conversational_responses: bool = False,\n        retain_tool_messages_in_context: bool = True,\n        addition_context_messages: List[AgentMessage] | None = None,\n        conversation_members: List[ConversationMember] | None = None,\n        content_stream_callback: Callable[[str], None] | None = None,\n        function_stream_callback: Callable[[str], None] | None = None,\n    ) -> ToolUsageMessage | str:\n        if addition_context_messages is None:\n            addition_context_messages = []\n        if conversation_members is None:\n            conversation_members = []\n        if max_tool_retries is None:\n            max_tool_retries = self._max_tool_retries\n\n        error_count = 0\n        step_count = 0\n        last_error_message = None\n        local_messages = []\n        self._force_stop = False\n        response_query_tool = ResponseQueryTool(\n            llm=self._llm, embedding_model=self._embedding_model\n        )\n\n        def append_message(message):\n            if isinstance(message, SystemMessage):\n                system_messages = [\n                    m for m in local_messages if not isinstance(m, SystemMessage)\n                ]\n                for m in system_messages:\n                    local_messages.remove(m)\n\n            local_messages.append(message)\n            if retain_tool_messages_in_context:\n                self._messages.add(message)\n                if self._memory_manager and self._memory_manager.conversation_memory:\n                    self._memory_manager.conversation_memory.add(message)\n\n        while not self._force_stop:\n            step_count += 1\n            if max_budget and get_total_cost() - starting_cost > max_budget:\n                raise BudgetExceededException()\n            if max_steps and step_count > max_steps:\n                raise MaxStepsExceededException()\n\n            if (\n                len(response_query_tool.responses) > 0\n                and response_query_tool not in tools\n            ):\n                tools.append(response_query_tool)\n\n            if self._enable_context_compression:\n                self._compress_llm_context(\n                    tools=tools,\n                    last_error_message=last_error_message,\n                    conversation_members=conversation_members,\n                    additional_context_messages=addition_context_messages\n                    + local_messages,\n                    prompt_vars=prompt_vars,\n                )\n\n            llm_context = self._build_llm_context(\n                messages=AgentMessageList(\n                    self._messages + addition_context_messages + local_messages\n                ),\n                tools=tools,\n                task=task,\n                last_error_message=last_error_message,\n                conversation_members=conversation_members,\n                prompt_vars=prompt_vars,\n            )\n\n            llm_response_content, llm_response_function = self._get_llm_response(\n                messages=llm_context,\n                tools=tools,\n                content_stream_callback=content_stream_callback,\n                function_stream_callback=function_stream_callback,\n            )\n            # print(llm_response_content)\n\n            last_error_message = None\n            if llm_response_function and any(\n                [\n                    m.name == llm_response_function.get(\"tool_name\")\n                    for m in conversation_members\n                ]\n            ):\n                message = f\"\"\"MessageSendFailure: You attempted to send a message to {llm_response_function.get('tool_name')} but this message failed.\n                To send a message to {llm_response_function.get('tool_name')} you must use the 'send_message' tool or use this format in your response:\n\n                ```\n                {llm_response_function.get('tool_name')}: Include your message here.)\n                ```\n                \"\"\"\n                append_message(SystemMessage(message=message))\n            if llm_response_function:\n                tool_message = ToolUsageMessage(\n                    tool_name=llm_response_function[\"name\"],\n                    tool_arguments=llm_response_function.get(\"arguments\") or {},\n                )\n                self._trigger_event(AgentEventNames.TOOL_SELECTED, self, tool_message)\n                self._handle_llm_function(tool_message=tool_message, tools=tools)\n\n                if (\n                    isinstance(tool_message.tool_output, str)\n                    and self._llm.count_tokens(tool_message.tool_output)\n                    > self._max_tool_response_tokens\n                ):\n                    response_id = response_query_tool.add_response(\n                        tool_message.tool_output\n                    )\n                    tool_message.tool_output = f\"The result from this tool was greater than {self._max_tool_response_tokens} tokens and could not be displayed. However, you can use the response_query tool to ask questions about the content of this response. Just use response_id = {response_id}.\"\n\n                append_message(tool_message)\n                if tool_message.success:\n                    error_count = 0\n                    self._trigger_event(\n                        AgentEventNames.TOOL_COMPLETED, self, tool_message\n                    )\n                    if tool_message.agent_halted:\n                        return tool_message\n                else:\n                    error_count += 1\n                    last_error_message = str(tool_message.error)\n                    message = \"ToolUsageError: Your last tool usage failed and MUST BE CORRECTED. If this error is not corrected you will not be able to proceed.\"\n                    append_message(SystemMessage(message=message))\n                    self._trigger_event(AgentEventNames.TOOL_ERROR, self, tool_message)\n                    if error_count >= max_tool_retries:\n                        return tool_message\n            elif llm_response_content and return_conversational_responses:\n                return llm_response_content\n            else:\n                error_count += 1\n                message = \"InvalidResponseError: The response does not conform to the required format. A function selection was expected, but none was provided.\"\n                append_message(SystemMessage(message=message))\n                if error_count >= max_tool_retries:\n                    raise AgentException(message)\n\n        if self._force_stop:\n            self._force_stop = False\n            raise AgentException(\"Agent was forcibly stopped.\")\n\n    def _build_llm_context(\n        self,\n        messages: AgentMessageList,\n        tools: List[Tool] | None = None,\n        task: str | None = None,\n        last_error_message: str | None = None,\n        conversation_members: List[ConversationMember] | None = None,\n        truncate_context: bool = True,\n        prompt_vars: Dict | None = None,\n    ) -> (str | None, Dict | None):\n        if tools is None:\n            tools = []\n        if conversation_members is None:\n            conversation_members = []\n        if prompt_vars is None:\n            prompt_vars = {}\n\n        prompt_sections = []\n        for s in self._system_prompt_sections:\n            if callable(s):\n                prompt_sections.append(s())\n            else:\n                prompt_sections.append(s)\n\n        system_prompt: str = self._system_prompt_builder(\n            conversation_members=conversation_members,\n            tools=tools,\n            task=task,\n            prompt_sections=prompt_sections,\n            error_message=last_error_message,\n            **prompt_vars,\n        )\n\n        # print(system_prompt)\n        llm_context = format_llm_messages(\n            system_prompt, messages, self._message_prompt_builder\n        )\n\n        if truncate_context:\n            reduced_messages = AgentMessageList(messages)\n            while (\n                self._is_context_pressure_too_high(llm_context, tools)\n                and len(reduced_messages) > 0\n            ):\n                reduced_messages.remove(reduced_messages[0])\n                llm_context = format_llm_messages(\n                    system_prompt, reduced_messages, self._message_prompt_builder\n                )\n\n        return llm_context\n\n    def _compress_llm_context(\n        self,\n        tools: List[Tool] | None = None,\n        last_error_message: str | None = None,\n        conversation_members: List[ConversationMember] | None = None,\n        additional_context_messages: List[AgentMessage] | None = None,\n        prompt_vars: Dict | None = None,\n    ) -> List[AgentMessage]:\n        if tools is None:\n            tools = []\n        if conversation_members is None:\n            conversation_members = []\n        if additional_context_messages is None:\n            additional_context_messages = []\n\n        all_context_messages = AgentMessageList(\n            self._messages + additional_context_messages\n        )\n\n        llm_context = self._build_llm_context(\n            messages=all_context_messages,\n            tools=tools,\n            last_error_message=last_error_message,\n            conversation_members=conversation_members,\n            truncate_context=False,\n            prompt_vars=prompt_vars,\n        )\n\n        if self._is_context_pressure_too_high(llm_context, tools):\n            # Try summarizing individual messages\n            # TODO: Give the agent an opportunity to save information to Archival database\n\n            summarize_messages(\n                llm=self._llm,\n                messages=self._messages[:-1],\n                message_prompt_builder=self._message_prompt_builder,\n            )\n\n            llm_context = self._build_llm_context(\n                messages=all_context_messages,\n                tools=tools,\n                last_error_message=last_error_message,\n                conversation_members=conversation_members,\n                truncate_context=False,\n                prompt_vars=prompt_vars,\n            )\n\n            if self._is_context_pressure_too_high(llm_context, tools):\n                # Try summarizing the entire conversation\n\n                last_message = self._messages[-1]\n                summary_message = summarize_conversation(\n                    llm=self._llm,\n                    messages=self._messages[:-1],\n                    message_prompt_builder=self._message_prompt_builder,\n                )\n                self._messages.clear()\n                self._messages.add(summary_message)\n                self._messages.add(last_message)\n\n                all_context_messages = AgentMessageList(\n                    self._messages + additional_context_messages\n                )\n                llm_context = self._build_llm_context(\n                    messages=all_context_messages,\n                    tools=tools,\n                    last_error_message=last_error_message,\n                    conversation_members=conversation_members,\n                    truncate_context=False,\n                    prompt_vars=prompt_vars,\n                )\n\n                if self._is_context_pressure_too_high(llm_context, tools):\n                    # Fire a message for group conversation compression\n                    self._trigger_event(\n                        AgentEventNames.CONTEXT_COMPRESSION_REQUESTED, self\n                    )\n\n                    llm_context = self._build_llm_context(\n                        messages=all_context_messages,\n                        tools=tools,\n                        last_error_message=last_error_message,\n                        conversation_members=conversation_members,\n                        truncate_context=False,\n                        prompt_vars=prompt_vars,\n                    )\n\n                    if self._is_context_pressure_too_high(llm_context, tools):\n                        print(\n                            \"Warning: Context compression failed to relieve pressure.\"\n                        )\n\n    def _handle_llm_function(self, tool_message: ToolUsageMessage, tools: List[Tool]):\n        tool_starting_cost = get_total_cost()\n\n        try:\n            tool_output = execute_tool(\n                tool_name=tool_message.tool_name,\n                tool_arguments=tool_message.tool_arguments,\n                tools=tools,\n            )\n\n            agent_halted = False\n            if isinstance(tool_output, tuple):\n                tool_output, agent_halted = tool_output\n\n            tool_message.agent_halted = agent_halted\n            tool_message.tool_output = tool_output\n            tool_message.success = True\n        except Exception as e:\n            # traceback.print_exc()\n            tool_message.error = e\n\n        tool_message.completed_at = datetime.now()\n        tool_message.cost = get_total_cost() - tool_starting_cost\n"
  },
  {
    "path": "bondai/agents/compression/__init__.py",
    "content": "from .conversation_summarizer import summarize_conversation\nfrom .message_summarizer import summarize_messages\n\n__all__ = [\"summarize_conversation\", \"summarize_messages\"]\n"
  },
  {
    "path": "bondai/agents/compression/conversation_summarizer.py",
    "content": "import os\nfrom typing import List\nfrom bondai.models import LLM\nfrom bondai.prompt import PromptBuilder, JinjaPromptBuilder\nfrom bondai.util import load_local_resource\nfrom bondai.agents.messages import (\n    AgentMessage,\n    SummaryMessage,\n)\n\nDEFAULT_SUMMARY_PROMPT_TEMPLATE = load_local_resource(\n    __file__, os.path.join(\"prompts\", \"conversation_summarizer_prompt_template.md\")\n)\n\n\ndef summarize_conversation(\n    llm: LLM,\n    messages: List[AgentMessage],\n    message_prompt_builder: PromptBuilder,\n    summary_prompt_builder: PromptBuilder = JinjaPromptBuilder(\n        DEFAULT_SUMMARY_PROMPT_TEMPLATE\n    ),\n) -> AgentMessage:\n    if not messages:\n        return []\n\n    # Format the messages\n    message_prompts = [\n        message_prompt_builder.build_prompt(\n            message=msg,\n        )\n        for msg in messages\n    ]\n\n    # Get the summary for the entire conversation\n    prompt = summary_prompt_builder.build_prompt(messages=message_prompts)\n    summary, _ = llm.get_completion(messages=[{\"role\": \"system\", \"content\": prompt}])\n\n    # Return the summary wrapped in an SummaryMessage\n    return SummaryMessage(\n        message=summary,\n        children=list(messages),\n        timestamp=messages[-1].timestamp,\n    )\n"
  },
  {
    "path": "bondai/agents/compression/message_summarizer.py",
    "content": "import os\nfrom typing import List\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom bondai.models import LLM\nfrom bondai.prompt import PromptBuilder, JinjaPromptBuilder\nfrom bondai.util import load_local_resource\nfrom bondai.agents.messages import AgentMessage, ConversationMessage, ToolUsageMessage\n\nMIN_SUMMARIZABLE_LENGTH = 250\nDEFAULT_SUMMARY_PROMPT_TEMPLATE = load_local_resource(\n    __file__, os.path.join(\"prompts\", \"message_summarizer_prompt_template.md\")\n)\n\n\ndef summarize_messages(\n    llm: LLM,\n    messages: List[AgentMessage],\n    message_prompt_builder: PromptBuilder,\n    summary_prompt_builder: PromptBuilder = JinjaPromptBuilder(\n        DEFAULT_SUMMARY_PROMPT_TEMPLATE\n    ),\n    max_summary_words: int = 100,\n) -> List[AgentMessage]:\n    summarizable_messages = [\n        m\n        for m in messages\n        if (\n            isinstance(m, ConversationMessage)\n            and not m.message_summary\n            and len(m.message) > MIN_SUMMARIZABLE_LENGTH\n        )\n        or (\n            isinstance(m, ToolUsageMessage)\n            and not m.tool_output_summary\n            and len(m.tool_output) > MIN_SUMMARIZABLE_LENGTH\n        )\n    ]\n\n    print(f\"Summarizing {len(summarizable_messages)} messages...\")\n\n    # Creating a thread pool executor to parallelize summary generation\n    with ThreadPoolExecutor() as executor:\n        futures = []\n        for m in summarizable_messages:\n            # Find all messages that occurred before the current message\n            previous_messages = [msg for msg in messages if msg.timestamp < m.timestamp]\n            # Submit the _summarize_message task to the executor\n            future = executor.submit(\n                _summarize_message,\n                m,\n                previous_messages[-5:],\n                llm,\n                summary_prompt_builder,\n                message_prompt_builder,\n                max_summary_words,\n            )\n            futures.append(future)\n\n        for future in as_completed(futures):\n            try:\n                future.result()\n            except Exception as exc:\n                print(f\"Message summary generation generated an exception: {exc}\")\n\n    return messages\n\n\ndef _summarize_message(\n    message: AgentMessage,\n    previous_messages: List[AgentMessage],\n    llm: LLM,\n    prompt_builder: PromptBuilder,\n    message_prompt_builder: PromptBuilder,\n    max_summary_words: int,\n) -> str:\n    message_prompt = message_prompt_builder.build_prompt(message=message)\n    previous_message_prompts = [\n        message_prompt_builder.build_prompt(\n            message=msg,\n        )\n        for msg in previous_messages\n    ]\n\n    prompt = prompt_builder.build_prompt(\n        message=message_prompt,\n        previous_messages=previous_message_prompts,\n        max_words=max_summary_words,\n    )\n\n    summary, _ = llm.get_completion(messages=[{\"role\": \"system\", \"content\": prompt}])\n\n    # print(\"************\")\n    # print(f\"Message: {message_prompt}\")\n    # print(f\"Summary: {summary}\")\n    # print(\"************\")\n    if isinstance(message, ConversationMessage):\n        print(\"Updating message summary...\")\n        message.message_summary = summary\n    elif isinstance(message, ToolUsageMessage):\n        message.tool_output_summary = summary\n    # print(message)\n"
  },
  {
    "path": "bondai/agents/compression/prompts/conversation_summarizer_prompt_template.md",
    "content": "Read the entire conversation provided below and create a summary. Your task is to condense the key information and main points from the conversation into a concise summary. Focus on retaining critical details and insights from the dialogue.\n\n# Conversation:\n{% for msg in messages %}\n- {{ msg }}\n{% endfor %}\n\n# Instructions:\n- Aim to capture the essence and most significant aspects of the conversation in your summary.\n- Ensure that all vital information, including key facts, decisions, and insights, are included in the summary.\n- Produce a clear and coherent summary that reflects the main points of the conversation.\n- Exclude any redundant or non-essential information from the summary.\n- Present the summary in a concise and organized manner. \n\n# Conversation Summary:"
  },
  {
    "path": "bondai/agents/compression/prompts/message_summarizer_prompt_template.md",
    "content": "Read the following conversation and summarize the final message:\n\n# Conversation\n{% for msg in previous_messages %}\n- {{ msg }}\n{% endfor %}\n- {{ message }}\n\n# Important Rules\n- Use the preceding conversation as context but summarize ONLY the following message.\n- Your summary must be no longer than {max_words} words.\n- Output only the summary. Do NOT include anything else in your output.\n\nMessage To Summarize:\n{{ message }}\n\nSummary:"
  },
  {
    "path": "bondai/agents/conversation_member.py",
    "content": "import uuid\nfrom abc import ABC, abstractmethod\nfrom enum import Enum\nfrom typing import List, Callable\nfrom .messages import (\n    AgentMessage,\n    ConversationMessage,\n    AgentMessageList,\n    USER_MEMBER_NAME,\n)\n\nDEFAULT_MAX_SEND_ATTEMPTS = 3\n\n\nclass ConversationMemberEventNames(Enum):\n    MESSAGE_RECEIVED: str = \"message_received\"\n    MESSAGE_COMPLETED: str = \"message_completed\"\n    MESSAGE_ERROR: str = \"message_error\"\n    CONVERSATION_EXITED: str = \"agent_exited\"\n\n\nclass ConversationMember(ABC):\n    def __init__(\n        self,\n        name: str,\n        persona: str | None = None,\n        persona_summary: str | None = None,\n    ):\n        self._id: str = str(uuid.uuid4())\n        self._name: str = name\n        self._persona: str = persona\n        self._persona_summary: str = persona_summary\n        self._messages: AgentMessageList = AgentMessageList()\n\n    @property\n    def id(self) -> str:\n        return self._id\n\n    @property\n    def name(self) -> str:\n        return self._name\n\n    @property\n    def persona(self) -> str:\n        return self._persona\n\n    @property\n    def persona_summary(self) -> str:\n        return self._persona_summary\n\n    @property\n    def messages(self) -> AgentMessageList:\n        return self._messages\n\n    @abstractmethod\n    def send_message(\n        self,\n        message: str | ConversationMessage,\n        sender_name: str = USER_MEMBER_NAME,\n        group_members: List | None = None,\n        group_messages: List[AgentMessage] | None = None,\n        max_attempts: int = DEFAULT_MAX_SEND_ATTEMPTS,\n        require_response: bool = True,\n    ) -> (str, str, bool):\n        pass\n\n    def send_message_async(\n        self,\n        message: str | ConversationMessage,\n        sender_name: str = USER_MEMBER_NAME,\n        group_members: List | None = None,\n        group_messages: List[AgentMessage] | None = None,\n        max_attempts: int = DEFAULT_MAX_SEND_ATTEMPTS,\n        require_response: bool = True,\n    ):\n        pass\n\n    def clear_messages(self):\n        pass\n"
  },
  {
    "path": "bondai/agents/conversational_agent.py",
    "content": "import os\nimport traceback\nimport json\nfrom datetime import datetime\nfrom typing import Dict, List, Callable\nfrom bondai.util import load_local_resource\nfrom bondai.tools import Tool\nfrom bondai.memory import MemoryManager\nfrom bondai.tools.conversational import (\n    SEND_MESSAGE_TOOL_NAME,\n    EXIT_CONVERSATION_TOOL_NAME,\n    SendMessageTool,\n    ExitConversationTool,\n)\nfrom bondai.prompt import JinjaPromptBuilder\nfrom bondai.models import LLM, EmbeddingModel\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames, get_total_cost\nfrom .agent import (\n    Agent,\n    DEFAULT_MAX_TOOL_RETRIES,\n    AgentStatus,\n    AgentException,\n)\nfrom .util import (\n    AgentException,\n    AgentEventNames,\n    parse_response_content_message,\n)\nfrom .prompts import DEFAULT_AGENT_NAME, DEFAULT_CONVERSATIONAL_INSTRUCTIONS\nfrom .conversation_member import ConversationMember, ConversationMemberEventNames\nfrom .messages import (\n    AgentMessage,\n    ConversationMessage,\n    ToolUsageMessage,\n    SystemMessage,\n    AgentMessageList,\n    USER_MEMBER_NAME,\n)\n\nDEFAULT_MAX_SEND_ATTEMPTS = 3\nDEFAULT_SYSTEM_PROMPT_TEMPLATE = load_local_resource(\n    __file__, os.path.join(\"prompts\", \"conversational_agent_system_prompt_template.md\")\n)\n\n\nclass ConversationalAgent(Agent, ConversationMember):\n    def __init__(\n        self,\n        llm: LLM | None = None,\n        embedding_model: EmbeddingModel | None = None,\n        tools: List[Tool] | None = None,\n        messages: List[AgentMessage] | None = None,\n        name: str = DEFAULT_AGENT_NAME,\n        persona: str | None = None,\n        persona_summary: str | None = None,\n        instructions: str | None = DEFAULT_CONVERSATIONAL_INSTRUCTIONS,\n        system_prompt_sections: List[Callable[[], str]] | None = None,\n        system_prompt_builder: Callable[..., str] = None,\n        message_prompt_builder: Callable[..., str] = None,\n        memory_manager: MemoryManager | None = None,\n        max_tool_retries: int = DEFAULT_MAX_TOOL_RETRIES,\n        max_context_length: int = None,\n        max_context_pressure_ratio: float = 0.8,\n        enable_context_compression: bool = False,\n        enable_conversation_tools: bool = True,\n        enable_conversational_content_responses: bool = True,\n        enable_exit_conversation: bool = True,\n        quiet: bool = True,\n    ):\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT4_0613)\n        if tools is None:\n            tools = []\n        if system_prompt_sections is None:\n            system_prompt_sections = []\n\n        ConversationMember.__init__(\n            self,\n            name=name,\n            persona=persona,\n            persona_summary=persona_summary,\n        )\n        Agent.__init__(\n            self,\n            llm=llm,\n            embedding_model=embedding_model,\n            quiet=quiet,\n            tools=tools,\n            messages=messages,\n            system_prompt_sections=system_prompt_sections,\n            system_prompt_builder=system_prompt_builder\n            or JinjaPromptBuilder(DEFAULT_SYSTEM_PROMPT_TEMPLATE),\n            message_prompt_builder=message_prompt_builder,\n            memory_manager=memory_manager,\n            max_context_length=max_context_length,\n            max_context_pressure_ratio=max_context_pressure_ratio,\n            max_tool_retries=max_tool_retries,\n            enable_context_compression=enable_context_compression,\n            enable_final_answer_tool=False,\n            allowed_events=[\n                AgentEventNames.CONTEXT_COMPRESSION_REQUESTED,\n                AgentEventNames.TOOL_SELECTED,\n                AgentEventNames.TOOL_ERROR,\n                AgentEventNames.TOOL_COMPLETED,\n                AgentEventNames.STREAMING_CONTENT_UPDATED,\n                AgentEventNames.STREAMING_FUNCTION_UPDATED,\n                ConversationMemberEventNames.MESSAGE_RECEIVED,\n                ConversationMemberEventNames.MESSAGE_ERROR,\n                ConversationMemberEventNames.MESSAGE_COMPLETED,\n                ConversationMemberEventNames.CONVERSATION_EXITED,\n            ],\n        )\n        self._instructions: str = instructions\n        self._enable_exit_conversation: bool = enable_exit_conversation\n        self._enable_conversational_content_responses = (\n            enable_conversational_content_responses\n        )\n        self._enable_conversation_tools = enable_conversation_tools\n        if self._enable_conversation_tools:\n            self.add_tool(SendMessageTool())\n        if self._enable_exit_conversation:\n            self.add_tool(ExitConversationTool())\n\n    @property\n    def instructions(self) -> str:\n        return self._instructions\n\n    def send_message_async(\n        self,\n        message: str | ConversationMessage,\n        sender_name: str = USER_MEMBER_NAME,\n        group_members: List[ConversationMember] | None = None,\n        group_messages: List[AgentMessage] | None = None,\n        max_attempts: int = DEFAULT_MAX_SEND_ATTEMPTS,\n        require_response: bool = True,\n    ):\n        \"\"\"Runs the agent's task in a separate thread.\"\"\"\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot send message while agent is in a running state.\"\n            )\n        if not message:\n            raise AgentException(\"'message' cannot be empty.\")\n\n        args = (\n            message,\n            sender_name,\n            group_members,\n            group_messages,\n            max_attempts,\n            require_response,\n        )\n\n        self._start_execution_thread(self.send_message, args=args)\n\n    def send_message(\n        self,\n        message: str | ConversationMessage,\n        sender_name: str = USER_MEMBER_NAME,\n        group_members: List[ConversationMember] | None = None,\n        group_messages: List[AgentMessage] | None = None,\n        max_attempts: int = DEFAULT_MAX_SEND_ATTEMPTS,\n        require_response: bool = True,\n    ) -> ConversationMessage | None:\n        if group_members is None:\n            group_members = []\n        if group_messages is None:\n            group_messages = []\n\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot send message while agent is in a running state.\"\n            )\n        if not message:\n            raise AgentException(\"'message' cannot be empty.\")\n\n        if isinstance(message, ConversationMessage):\n            agent_message = message\n        elif isinstance(message, str):\n            if not sender_name:\n                raise AgentException(\"sender_name cannot be empty.\")\n            agent_message = ConversationMessage(\n                sender_name=sender_name,\n                recipient_name=self.name,\n                message=message,\n                require_response=require_response,\n            )\n        else:\n            raise AgentException(\n                \"'message' must be an instance of ConversationMessage or a string.\"\n            )\n\n        attempts = 0\n        starting_cost = get_total_cost()\n        self._status = AgentStatus.RUNNING\n        self._messages.add(agent_message)\n        if self._memory_manager and self._memory_manager.conversation_memory:\n            self._memory_manager.conversation_memory.add(agent_message)\n        self._trigger_event(\n            ConversationMemberEventNames.MESSAGE_RECEIVED, self, agent_message\n        )\n\n        def complete_agent_message(\n            success=False, conversation_exited=False, error=None\n        ):\n            agent_message.success = success\n            agent_message.conversation_exited = conversation_exited\n            agent_message.error = error\n            agent_message.cost = get_total_cost() - starting_cost\n            agent_message.completed_at = datetime.now()\n            if success:\n                self._trigger_event(\n                    ConversationMemberEventNames.MESSAGE_COMPLETED, self, agent_message\n                )\n                if conversation_exited:\n                    self._trigger_event(\n                        ConversationMemberEventNames.CONVERSATION_EXITED,\n                        self,\n                        agent_message,\n                    )\n            else:\n                self._trigger_event(\n                    ConversationMemberEventNames.MESSAGE_ERROR, self, agent_message\n                )\n\n        def validate_recipient(recipient_name: str):\n            if not recipient_name:\n                return \"recipient_name cannot be empty.\"\n            if len(group_members) > 0 and not any(\n                [\n                    member.name.lower() == recipient_name.lower()\n                    for member in group_members\n                ]\n            ):\n                return f\"InvalidResponseError: The response does not conform to the required format. You do not have the ability to send messages to '{recipient_name}'. Try sending a message to someone else.\"\n\n        if not agent_message.require_response:\n            complete_agent_message(success=True)\n            return\n\n        while not self._force_stop:\n            try:\n                attempts += 1\n                if attempts > max_attempts:\n                    raise AgentException(\n                        \"The maximum number of attempts has been exceeded.\"\n                    )\n\n                prompt_vars = {\n                    \"name\": self.name,\n                    \"persona\": self.persona,\n                    \"instructions\": self.instructions,\n                    \"conversation_enabled\": self._enable_conversation_tools\n                    or self._enable_conversational_content_responses,\n                    \"enable_exit_conversation\": self._enable_exit_conversation,\n                }\n\n                tool_result = self._run_tool_loop(\n                    addition_context_messages=group_messages,\n                    tools=self._tools,\n                    conversation_members=group_members,\n                    starting_cost=starting_cost,\n                    return_conversational_responses=True,\n                    prompt_vars=prompt_vars,\n                )\n\n                response_message: ConversationMessage | None = None\n                if isinstance(tool_result, ToolUsageMessage):\n                    if not tool_result.success:\n                        complete_agent_message(success=False, error=tool_result.error)\n                        raise tool_result.error\n                    elif tool_result.tool_name == EXIT_CONVERSATION_TOOL_NAME:\n                        complete_agent_message(success=True, conversation_exited=True)\n                        return tool_result.tool_output\n                    elif tool_result.tool_name == SEND_MESSAGE_TOOL_NAME:\n                        response_message = tool_result.tool_output\n\n                if (\n                    isinstance(tool_result, str)\n                    and self._enable_conversational_content_responses\n                ):\n                    recipient_name, message = parse_response_content_message(\n                        tool_result\n                    )\n                    if not recipient_name or not message:\n                        recipient_name = agent_message.sender_name\n                        message = tool_result\n                    response_message = ConversationMessage(\n                        role=\"assistant\", recipient_name=recipient_name, message=message\n                    )\n\n                if response_message:\n                    response_message.sender_name = self.name\n                    error = validate_recipient(response_message.recipient_name)\n                    if not error:\n                        complete_agent_message(success=True)\n                        self._messages.add(response_message)\n                        if (\n                            self._memory_manager\n                            and self._memory_manager.conversation_memory\n                        ):\n                            self._memory_manager.conversation_memory.add(\n                                response_message\n                            )\n\n                        return response_message\n                    else:\n                        self._messages.add(SystemMessage(message=error))\n                else:\n                    self._messages.add(\n                        SystemMessage(\n                            message=\"InvalidResponseError: The response does not conform to the required format. A function selection was expected, but none was provided.\\nYour must correct this error.\"\n                        )\n                    )\n            finally:\n                self._status = AgentStatus.IDLE\n\n    def to_dict(self) -> Dict:\n        data = super().to_dict()\n        data[\"name\"] = self._name\n        data[\"persona\"] = self._persona\n        data[\"persona_summary\"] = self._persona_summary\n        data[\"instructions\"] = self.instructions\n        data[\"quiet\"] = self._quiet\n        data[\"enable_conversation_tools\"] = self._enable_conversation_tools\n        data[\"enable_exit_conversation\"] = self._enable_exit_conversation\n        data[\n            \"enable_conversational_content_responses\"\n        ] = self._enable_conversational_content_responses\n        data[\"max_context_length\"] = self._max_context_length\n        data[\"max_context_pressure_ratio\"] = self._max_context_pressure_ratio\n        data[\"messages\"] = self.messages.to_dict()\n        return data\n\n    def save_state(self, file_path: str = None) -> Dict:\n        state = super().save_state()\n        state.update(self.to_dict())\n\n        if file_path:\n            os.makedirs(os.path.dirname(file_path), exist_ok=True)\n            with open(file_path, \"w\") as file:\n                json.dump(state, file, indent=4)\n\n        return state\n\n    @classmethod\n    def from_dict(\n        cls, data: List[Dict], file_path: str = None\n    ) -> \"ConversationalAgent\":\n        if not data and file_path:\n            with open(file_path, \"r\") as file:\n                data = json.load(file)\n\n        agent = cls(\n            name=data[\"name\"],\n            persona=data[\"persona\"],\n            persona_summary=data[\"persona_summary\"],\n            instructions=data[\"instructions\"],\n            enable_exit_conversation=data[\"enable_exit_conversation\"],\n            quiet=data[\"quiet\"],\n            enable_conversation_tools=data[\"enable_conversation_tools\"],\n            enable_conversational_content_responses=data[\n                \"enable_conversational_content_responses\"\n            ],\n            max_context_length=data[\"max_context_length\"],\n            max_context_pressure_ratio=data[\"max_context_pressure_ratio\"],\n        )\n        agent._messages = AgentMessageList.from_dict(data[\"messages\"])\n        agent.load_state(data)\n        return agent\n"
  },
  {
    "path": "bondai/agents/group_chat/__init__.py",
    "content": "from .group_conversation import GroupConversation\nfrom .user_proxy import UserProxy\nfrom .group_conversation_config import (\n    BaseGroupConversationConfig,\n    GroupConversationConfig,\n    TeamConversationConfig,\n    TableConversationConfig,\n    CompositeConversationConfig,\n)\n\n__all__ = [\n    \"UserProxy\",\n    \"GroupConversation\",\n    \"BaseGroupConversationConfig\",\n    \"GroupConversationConfig\",\n    \"TeamConversationConfig\",\n    \"TableConversationConfig\",\n    \"CompositeConversationConfig\",\n]\n"
  },
  {
    "path": "bondai/agents/group_chat/group_conversation.py",
    "content": "import uuid\nimport asyncio\nimport traceback\nfrom datetime import datetime\nfrom typing import Dict, List, Callable\nfrom bondai.util import EventMixin, Runnable\nfrom bondai.agents import (\n    AgentException,\n    AgentStatus,\n    ConversationMember,\n    ConversationMemberEventNames,\n    AgentMessageList,\n    ConversationMessage,\n    USER_MEMBER_NAME,\n)\nfrom .group_conversation_config import (\n    BaseGroupConversationConfig,\n    TeamConversationConfig,\n)\n\n\nclass GroupConversation(EventMixin, Runnable):\n    def __init__(\n        self,\n        conversation_members: List[ConversationMember] | None = None,\n        conversation_config: BaseGroupConversationConfig | None = None,\n        filter_recipient_messages: bool = False,\n    ):\n        super().__init__(\n            allowed_events=[\n                ConversationMemberEventNames.MESSAGE_RECEIVED,\n                ConversationMemberEventNames.MESSAGE_COMPLETED,\n                ConversationMemberEventNames.MESSAGE_ERROR,\n                ConversationMemberEventNames.CONVERSATION_EXITED,\n            ]\n        )\n        if conversation_members and conversation_config:\n            raise AgentException(\n                \"Only one of 'conversation_members' or 'conversation_configs' must be provided\"\n            )\n\n        if conversation_config:\n            self._conversation_config = conversation_config\n        elif conversation_members:\n            self._conversation_config = TeamConversationConfig(conversation_members)\n        else:\n            raise AgentException(\n                \"Either 'conversation_members' or 'conversation_config' must be provided\"\n            )\n\n        self._id: str = str(uuid.uuid4())\n        self._status: AgentStatus = AgentStatus.IDLE\n        self._filter_recipient_messages: bool = filter_recipient_messages\n        self._messages: AgentMessageList = AgentMessageList()\n\n        self._init_member_events()\n\n    @property\n    def id(self) -> str:\n        return self._id\n\n    @property\n    def status(self) -> AgentStatus:\n        return self._status\n\n    @property\n    def members(self) -> List[ConversationMember]:\n        return self._conversation_config.members\n\n    def remove_messages_after(self, timestamp: datetime, inclusive: bool = True):\n        self._messages.remove_after(timestamp)\n        for a in self.members:\n            a.messages.remove_after(timestamp, inclusive=inclusive)\n\n    def _get_member(self, member_name: str) -> ConversationMember:\n        return next(\n            (m for m in self.members if m.name.lower() == member_name.lower()), None\n        )\n\n    def _init_member_events(self):\n        for member in self.members:\n            member.on(ConversationMemberEventNames.MESSAGE_RECEIVED)(\n                self._on_member_message_received\n            )\n            member.on(ConversationMemberEventNames.MESSAGE_ERROR)(\n                self._on_member_message_error\n            )\n            member.on(ConversationMemberEventNames.MESSAGE_COMPLETED)(\n                self._on_member_message_completed\n            )\n            member.on(ConversationMemberEventNames.CONVERSATION_EXITED)(\n                self._on_member_exited\n            )\n\n    def _on_member_message_received(\n        self, member: ConversationMember, message: ConversationMessage\n    ):\n        # print(f\"{message.sender_name} to {message.recipient_name}: {message.message}\")\n        self._trigger_event(\n            ConversationMemberEventNames.MESSAGE_RECEIVED, member, message\n        )\n\n    def _on_member_message_error(\n        self, member: ConversationMember, message: ConversationMessage\n    ):\n        exc = message.error\n        traceback.print_exception(type(exc), exc, exc.__traceback__)\n        self._trigger_event(ConversationMemberEventNames.MESSAGE_ERROR, member, message)\n\n    def _on_member_message_completed(\n        self, member: ConversationMember, message: ConversationMessage\n    ):\n        self._messages.add(message)\n        self._trigger_event(\n            ConversationMemberEventNames.MESSAGE_COMPLETED, member, message\n        )\n\n    def _on_member_exited(\n        self, member: ConversationMember, message: ConversationMessage\n    ):\n        self._trigger_event(\n            ConversationMemberEventNames.CONVERSATION_EXITED, member, message\n        )\n\n    def save_state(self) -> Dict:\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot save group conversation state while it is running.\"\n            )\n\n        state = {}\n        for member in self.members:\n            state[member.id] = member.save_state()\n\n        return state\n\n    def load_state(self, state: Dict):\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot load group conversation state while it is running.\"\n            )\n\n        for member in self.members:\n            member.load_state(state[member.id])\n\n    def send_message_async(\n        self,\n        recipient_name: str,\n        message: str,\n        sender_name: str = USER_MEMBER_NAME,\n        require_response: bool = True,\n    ):\n        \"\"\"Runs the agent's task in a separate thread.\"\"\"\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot send message while agent is in a running state.\"\n            )\n        if not message:\n            raise AgentException(\"'message' cannot be empty.\")\n\n        args = (recipient_name, message, sender_name, require_response)\n\n        self._start_execution_thread(self.send_message, args=args)\n\n    def send_message(\n        self,\n        recipient_name: str,\n        message: str,\n        sender_name: str = USER_MEMBER_NAME,\n        require_response: bool = True,\n    ) -> str:\n        if self._status == AgentStatus.RUNNING:\n            raise AgentException(\n                \"Cannot send message while agent is in a running state.\"\n            )\n        if not message:\n            raise AgentException(\"'message' cannot be empty.\")\n\n        previous_message = None\n        if isinstance(message, ConversationMessage):\n            next_message = message\n        elif isinstance(message, str):\n            if not sender_name:\n                raise AgentException(\"sender_name cannot be empty.\")\n            if not recipient_name:\n                raise AgentException(\"recipient_name cannot be empty.\")\n\n            next_message = ConversationMessage(\n                sender_name=sender_name,\n                recipient_name=recipient_name,\n                message=message,\n                require_response=require_response,\n            )\n        else:\n            raise AgentException(\n                \"'message' must be an instance of ConversationMessage or a string.\"\n            )\n\n        try:\n            self._status = AgentStatus.RUNNING\n            while next_message:\n                if next_message.sender_name.lower() == USER_MEMBER_NAME.lower():\n                    sender_reachable_members = self.members\n                else:\n                    sender_reachable_members = (\n                        self._conversation_config.get_reachable_members(\n                            member_name=next_message.sender_name\n                        )\n                    )\n\n                recipient = next(\n                    (\n                        m\n                        for m in sender_reachable_members\n                        if m.name.lower() == next_message.recipient_name.lower()\n                    ),\n                    None,\n                )\n                if not recipient:\n                    raise AgentException(\n                        f\"Recipient {next_message.recipient_name} not found\"\n                    )\n\n                recipient_reachable_members = (\n                    self._conversation_config.get_reachable_members(member=recipient)\n                )\n\n                if self._filter_recipient_messages:\n                    recipient_messages = AgentMessageList(\n                        [\n                            m\n                            for m in self._messages\n                            if m.recipient_name.lower() == recipient.name.lower()\n                            or m.sender_name.lower() == recipient.name.lower()\n                        ]\n                    )\n                else:\n                    recipient_messages = self._messages\n\n                try:\n                    if next_message.require_response:\n                        previous_message = next_message\n                        next_message = recipient.send_message(\n                            message=next_message,\n                            group_members=recipient_reachable_members,\n                            group_messages=recipient_messages,\n                        )\n                    else:\n                        recipient.send_message(message=next_message)\n                        next_message = previous_message\n                except AgentException as e:\n                    print(\"Error occurred, rewinding conversation...\")\n                    # print(e)\n                    # The recipient agent has errored out. We will rewind the conversation and try again.\n                    previous_message = (\n                        self._messages[-2]\n                        if len(self._messages) > 1\n                        else self._messages[-1]\n                    )\n                    self.remove_messages_after(previous_message.timestamp)\n                    next_message = ConversationMessage(\n                        message=previous_message.message,\n                        sender_name=previous_message.sender_name,\n                        recipient_name=previous_message.recipient_name,\n                    )\n\n            self._trigger_event(\n                ConversationMemberEventNames.CONVERSATION_EXITED, next_message\n            )\n        finally:\n            self._status = AgentStatus.IDLE\n\n    def reset_memory(self):\n        self._messages.clear()\n        for member in self.members:\n            member.clear_messages()\n"
  },
  {
    "path": "bondai/agents/group_chat/group_conversation_config.py",
    "content": "from typing import Dict, List, Set\nfrom abc import ABC, abstractmethod\nfrom bondai.agents import ConversationMember\n\n\nclass BaseGroupConversationConfig(ABC):\n    @property\n    @abstractmethod\n    def members(self) -> List[ConversationMember]:\n        pass\n\n    @abstractmethod\n    def get_reachable_members(\n        self, member: ConversationMember | None = None, member_name: str = None\n    ) -> List[ConversationMember]:\n        pass\n\n\nclass GroupConversationConfig(ABC):\n    def __init__(self, members: List[ConversationMember]):\n        self._members: Set[ConversationMember] = list(set(members))\n\n    @property\n    def _members(self) -> List[ConversationMember]:\n        return list(self._members)\n\n    def get_reachable_members(\n        self, member: ConversationMember | None = None, member_name: str = None\n    ) -> List[ConversationMember]:\n        if not member and not member_name:\n            return []\n\n        member_name = \"\" if not member_name else member_name\n        reachable_members = list(\n            set(\n                [\n                    m\n                    for m in self._members\n                    if m != member and m.name.lower() != member_name.lower()\n                ]\n            )\n        )\n\n        return reachable_members\n\n\nclass TeamConversationConfig(BaseGroupConversationConfig):\n    def __init__(self, *args: List[ConversationMember]):\n        self._members: Set[ConversationMember] = set()\n        for team in args:\n            self._members.update(team)\n        self._teams: List[List[ConversationMember]] = list(args)\n\n    @property\n    def members(self) -> List[ConversationMember]:\n        return list(self._members)\n\n    def get_reachable_members(\n        self, member: ConversationMember | None = None, member_name: str = None\n    ) -> List[ConversationMember]:\n        if not member and not member_name:\n            return []\n\n        member_name = \"\" if not member_name else member_name\n        member_teams = [\n            t\n            for t in self._teams\n            for m in t\n            if m == member or m.name.lower() == member_name.lower()\n        ]\n        reachable_members = list(\n            set(\n                [\n                    m\n                    for t in member_teams\n                    for m in t\n                    if m != member and m.name.lower() != member_name.lower()\n                ]\n            )\n        )\n\n        return reachable_members\n\n\nclass TableConversationConfig(BaseGroupConversationConfig):\n    def __init__(self, member_table: Dict):\n        self._member_table = member_table\n\n    @property\n    def members(self) -> List[ConversationMember]:\n        return list(self._member_table.keys())\n\n    def get_reachable_members(\n        self, member: ConversationMember | None = None, member_name: str = None\n    ) -> List[ConversationMember]:\n        if not member and not member_name:\n            return []\n\n        if member_name:\n            member = next((m for m in self.members if m.name == member_name), None)\n\n        if member and member.name in self._member_table:\n            return self._member_table[member.name]\n        else:\n            return []\n\n\nclass CompositeConversationConfig(BaseGroupConversationConfig):\n    def __init__(self, *conversation_configs: List[BaseGroupConversationConfig]):\n        self._conversation_configs: List[BaseGroupConversationConfig] = list(\n            conversation_configs\n        )\n\n    @property\n    def members(self) -> List[ConversationMember]:\n        return list(set([m for c in self._conversation_configs for m in c.members]))\n\n    def get_reachable_members(\n        self, member: ConversationMember | None = None, member_name: str = None\n    ) -> List[ConversationMember]:\n        return list(\n            set(\n                [\n                    m\n                    for c in self._conversation_configs\n                    for m in c.get_reachable_members(member, member_name)\n                ]\n            )\n        )\n"
  },
  {
    "path": "bondai/agents/group_chat/user_proxy.py",
    "content": "from termcolor import cprint\nfrom datetime import datetime\nfrom typing import List\nfrom bondai.util import EventMixin\nfrom bondai.agents import (\n    AgentStatus,\n    AgentException,\n    AgentMessage,\n    ConversationMessage,\n    ConversationMember,\n    ConversationMemberEventNames,\n    parse_response_content_message,\n    USER_MEMBER_NAME,\n)\n\n\nclass UserProxy(EventMixin, ConversationMember):\n    def __init__(\n        self,\n        persona: str | None = None,\n        parse_recipients: bool = True,\n        auto_exit: bool = False,\n    ):\n        EventMixin.__init__(\n            self,\n            allowed_events=[\n                ConversationMemberEventNames.MESSAGE_RECEIVED,\n                ConversationMemberEventNames.MESSAGE_ERROR,\n                ConversationMemberEventNames.MESSAGE_COMPLETED,\n                ConversationMemberEventNames.CONVERSATION_EXITED,\n            ],\n        )\n        ConversationMember.__init__(\n            self,\n            name=USER_MEMBER_NAME,\n            persona=persona,\n        )\n        self._status = AgentStatus.IDLE\n        self._parse_recipients = parse_recipients\n        self._auto_exit = auto_exit\n\n    def send_message(\n        self,\n        message: str | ConversationMessage,\n        sender_name: str = USER_MEMBER_NAME,\n        group_members: List | None = None,\n        group_messages: List[AgentMessage] | None = None,\n        max_attempts: int = None,\n        require_response: bool = True,\n    ):\n        if not message:\n            raise AgentException(\"'message' cannot be empty.\")\n\n        if group_members is None:\n            group_members = []\n\n        if isinstance(message, ConversationMessage):\n            agent_message = message\n        else:\n            if not sender_name:\n                raise AgentException(\"sender_name cannot be empty.\")\n            agent_message = ConversationMessage(\n                sender_name=sender_name,\n                recipient_name=self.name,\n                message=message,\n                require_response=require_response,\n            )\n\n        self._messages.add(agent_message)\n        self._trigger_event(\n            ConversationMemberEventNames.MESSAGE_RECEIVED, self, agent_message\n        )\n\n        cprint(\"\\n\" + agent_message.message + \"\\n\", \"white\")\n\n        if not agent_message.require_response or self._auto_exit:\n            agent_message.success = True\n            agent_message.cost = 0.0\n            agent_message.completed_at = datetime.now()\n            self._trigger_event(\n                ConversationMemberEventNames.MESSAGE_COMPLETED, self, agent_message\n            )\n            return\n\n        while True:\n            try:\n                user_response = input()\n                user_exited = user_response.strip().lower() == \"exit\"\n\n                if not user_exited:\n                    if self._parse_recipients:\n                        (\n                            next_recipient_name,\n                            next_message,\n                        ) = parse_response_content_message(user_response)\n                    else:\n                        next_recipient_name = agent_message.sender_name\n                        next_message = user_response\n\n                    next_recipient_name = (\n                        next_recipient_name\n                        if next_recipient_name\n                        else agent_message.sender_name\n                    )\n                    next_message = next_message if next_message else user_response\n\n                    if len(group_members) > 0 and not any(\n                        [\n                            member.name.lower() == next_recipient_name.lower()\n                            for member in group_members\n                        ]\n                    ):\n                        raise AgentException(\n                            f\"InvalidResponseError: The response does not conform to the required format. You do not have the ability to send messages to '{next_recipient_name}'. Try sending a message to someone else.\"\n                        )\n\n                    agent_message.success = True\n                    agent_message.conversation_exited = user_exited\n                    agent_message.cost = 0.0\n                    agent_message.completed_at = datetime.now()\n                    self._trigger_event(\n                        ConversationMemberEventNames.MESSAGE_COMPLETED,\n                        self,\n                        agent_message,\n                    )\n\n                    response_message = ConversationMessage(\n                        sender_name=self.name,\n                        recipient_name=next_recipient_name,\n                        message=next_message,\n                    )\n                    self._messages.add(response_message)\n                    self._status = AgentStatus.IDLE\n                    return response_message\n                else:\n                    agent_message.success = True\n                    agent_message.conversation_exited = True\n                    agent_message.cost = 0.0\n                    agent_message.completed_at = datetime.now()\n                    self._trigger_event(\n                        ConversationMemberEventNames.MESSAGE_COMPLETED,\n                        self,\n                        agent_message,\n                    )\n                    self._trigger_event(\n                        ConversationMemberEventNames.CONVERSATION_EXITED,\n                        self,\n                        agent_message,\n                    )\n                    self._status = AgentStatus.IDLE\n                    return None\n            except Exception as e:\n                print(\"The following error occurred while parsing your response:\")\n                print(str(e))\n"
  },
  {
    "path": "bondai/agents/messages.py",
    "content": "import uuid\nfrom abc import ABC\nfrom typing import List, Dict, Set\nfrom datetime import datetime\nfrom dataclasses import dataclass, field, is_dataclass\n\nUSER_MEMBER_NAME = \"user\"\nDEFAULT_MEMORY_WARNING_MESSAGE = (\n    \"Warning: the conversation history will soon reach its maximum length and be trimmed. \"\n    \"Make sure to save any important information from the conversation to your memory before it is removed.\"\n)\n\n\n@dataclass\nclass AgentMessage(ABC):\n    id: str = field(default_factory=lambda: str(uuid.uuid4()))\n    role: str | None = field(default=None)\n    timestamp: datetime = field(default_factory=lambda: datetime.now())\n\n\n@dataclass\nclass SystemMessage(AgentMessage):\n    role: str = field(default=\"system\")\n    message: str | None = field(default=None)\n\n\n@dataclass\nclass SummaryMessage(AgentMessage):\n    role: str = field(default=\"user\")\n    message: str | None = field(default=None)\n    children: List[AgentMessage] = field(default=None)\n\n\n@dataclass\nclass ConversationMessage(AgentMessage):\n    role: str = field(default=\"user\")\n    sender_name: str | None = field(default=None)\n    recipient_name: str | None = field(default=None)\n    message: str | None = field(default=None)\n    message_summary: str | None = field(default=None)\n    require_response: bool = field(default=True)\n    success: bool = field(default=False)\n    error: Exception | None = field(default=None)\n    conversation_exited: bool = field(default=False)\n    cost: float | None = field(default=None)\n    completed_at: datetime | None = field(default=None)\n\n\n@dataclass\nclass ToolUsageMessage(AgentMessage):\n    role: str = field(default=\"function\")\n    tool_name: str | None = field(default=None)\n    tool_arguments: Dict | None = field(default=None)\n    tool_output: str | None = field(default=None)\n    tool_output_summary: str | None = field(default=None)\n    success: bool = field(default=False)\n    error: Exception | None = field(default=None)\n    agent_halted: bool = field(default=False)\n    cost: float | None = field(default=None)\n    completed_at: datetime | None = field(default=None)\n\n\ndef custom_serialization(value):\n    \"\"\"\n    Serialize special types like datetime, Exception, and nested AgentMessage objects.\n    \"\"\"\n    if isinstance(value, datetime):\n        return value.isoformat()\n    elif isinstance(value, Exception):\n        return str(value)\n    elif is_dataclass(value) and not isinstance(value, type):\n        return message_to_dict(value)\n    return value\n\n\ndef message_to_dict(message: AgentMessage) -> Dict:\n    \"\"\"\n    Convert an AgentMessage object to a dictionary with custom serialization.\n    \"\"\"\n\n    message_dict = {\n        k: custom_serialization(v)\n        for k, v in message.__dict__.items()\n        if k != \"children\"\n    }\n    message_dict[\"type\"] = type(message).__name__  # Add the type for deserialization\n    if \"children\" in message.__dict__:\n        message_dict[\"children\"] = [\n            message_to_dict(child) for child in message.children\n        ]\n\n    return message_dict\n\n\nclass AgentMessageList:\n    def __init__(self, messages: List[AgentMessage] | None = None):\n        self._items: List[AgentMessage] = []\n        self._ids: Set[str] = set()\n        if messages:\n            for message in messages:\n                self.add(message)\n\n    def add(self, item: AgentMessage):\n        if item.id not in self._ids:\n            self._ids.add(item.id)\n            self._items.append(item)\n            self._items = list(sorted(self._items, key=lambda x: x.timestamp))\n\n    def remove(self, item: AgentMessage):\n        if item.id in self._ids:\n            self._ids.remove(item.id)\n            self._items.remove(item)\n\n    def remove_after(self, timestamp: datetime, inclusive: bool = True):\n        if inclusive:\n            self._items = [item for item in self._items if item.timestamp <= timestamp]\n        else:\n            self._items = [item for item in self._items if item.timestamp < timestamp]\n        self._ids = set([item.id for item in self._items])\n\n    def clear(self):\n        self._items = []\n        self._ids = set()\n\n    def __getitem__(self, index: int):\n        return self._items[index]\n\n    def __add__(self, other: List[AgentMessage] | \"AgentMessageList\"):\n        if isinstance(other, AgentMessageList):\n            # If the other object is also an AgentMessageList, extend with its items\n            return self._items + other._items\n        elif isinstance(other, list):\n            # If the other object is a list, just concatenate the lists\n            return self._items + other\n        else:\n            # If the other object is neither, raise an exception\n            raise TypeError(\n                f\"Unsupported operand type(s) for +: 'AgentMessageList' and '{type(other).__name__}'\"\n            )\n\n    def __iter__(self):\n        return iter(self._items)\n\n    def __contains__(self, item):\n        return item.id in self._ids\n\n    def __len__(self):\n        return len(self._items)\n\n    def to_dict(self) -> List[Dict]:\n        \"\"\"\n        Convert the AgentMessageList to a list of dictionaries for serialization.\n        \"\"\"\n        return [message_to_dict(message) for message in self._items]\n\n    @classmethod\n    def from_dict(cls, data: List[Dict]) -> \"AgentMessageList\":\n        \"\"\"\n        Create an AgentMessageList from a list of dictionaries.\n        \"\"\"\n        list_instance = cls()\n        for item in data:\n            item_type = item[\"type\"]\n            del item[\"type\"]\n            if item_type == \"ConversationMessage\":\n                message = ConversationMessage(**item)\n            elif item_type == \"ToolUsageMessage\":\n                message = ToolUsageMessage(**item)\n            elif item_type == \"SystemMessage\":\n                message = SystemMessage(**item)\n            elif item_type == \"SummaryMessage\":\n                message = SummaryMessage(**item)\n            # elif item_type == 'MemoryWarningMessage':\n            #     message = MemoryWarningMessage(**item)\n            else:\n                raise ValueError(f\"Unknown message type: {item_type}\")\n\n            if \"timestamp\" in item:\n                message.timestamp = datetime.fromisoformat(item[\"timestamp\"])\n            if \"completed_at\" in item and item[\"completed_at\"]:\n                message.completed_at = datetime.fromisoformat(item[\"completed_at\"])\n            list_instance.add(message)\n\n        return list_instance\n"
  },
  {
    "path": "bondai/agents/prompts/__init__.py",
    "content": "from .default_persona import (\n    DEFAULT_AGENT_NAME,\n    DEFAULT_CONVERSATIONAL_INSTRUCTIONS,\n    DEFAULT_CONVERSATIONAL_PERSONA,\n)\n\n__all__ = [\n    \"DEFAULT_AGENT_NAME\",\n    \"DEFAULT_CONVERSATIONAL_INSTRUCTIONS\",\n    \"DEFAULT_CONVERSATIONAL_PERSONA\",\n]\n"
  },
  {
    "path": "bondai/agents/prompts/agent_message_prompt_template.md",
    "content": "{%- if message_type == \"ToolUsageMessage\" %}\n# Message Timestamp\n{{ message.timestamp }}\n\n# Tool Name\nYou used the **{{ message.tool_name }}** tool.\n# Tool Arguments\n{% if message.tool_arguments %}\n{% for k, v in message.tool_arguments.items() %}\n{{ k }}:\n```\n{{ v }}\n```\n{% endfor %}\n{%- else %}\nNo arguments were provided for this tool.\n{% endif %}\n{% if message.error %}\n# Tool Error:\nThis tool did not run successfully and returned the following error:\n```\n{{ message.error }}\n```\n{%- else %}\n# Tool Response:\n```\n{{ message.tool_output_summary or message.tool_output }}\n```\n{% endif %}\n{%- elif message_type == \"SystemMessage\" %}\n# Message Timestamp\n{{ message.timestamp }}\n\n{{ message.message }}\n{%- elif message_type == \"SummaryMessage\" %}\nThe following is a summary of the previous conversation content. It has been summarized to save memory space:\n{{ message.message }}\n{%- elif message_type == \"ConversationMessage\" %}\n{% if message.error %}\nThis message failed with the following error:\n```\n{{ message.error }}\n```\nMessage content:\n```\n{{ message.sender_name.lower() }} to {{ message.recipient_name.lower() }}: {{ message.message_summary or message.message }}\n```\n{%- else %}\n{{ message.sender_name.lower() }} to {{ message.recipient_name.lower() }}: {{ message.message_summary or message.message }}\n{%- endif %}\n{%- endif %}"
  },
  {
    "path": "bondai/agents/prompts/conversational_agent_system_prompt_template.md",
    "content": "{%- if instructions %}\n# Instructions\n\n{{ instructions }}\n{%- endif %}\n\n\n# Your Persona\n\nYour Name is {{ name }}.\n{%- if persona %}\n{{ persona }}\n{%- endif %}\n{%- if tools %}\n\n\n# Tools\n\nYou have access to a set of tools that give you capabilities far beyond typical language models.\nYou are being asked to use these tools and your powerful problem solving skills to help the user with their task.\n{%- endif %}\n{%- if conversation_members %}\n# Group Conversation Members\n\nYou are in a Group Conversation with the following members:\n{% for member in conversation_members %}\nName: **{{ member.name }}**\n{%- if member.persona_summary %}\nPersona: {{ member.persona_summary }}{%- endif %}\n{% endfor %}\n{%- endif %}\n{%- if error_message %}\n# Error Message\n\nThe following error occurred in your last response. Please correct it in your next response.\n```\n{{ error_message }}\n```\n{%- endif %}\n{%- if prompt_sections %}\n{% for section in prompt_sections %}\n{{ section }}\n{% endfor %}\n{%- endif %}\n{%- if conversation_enabled %}\n# Sending Messages\n\nEach message should start with the recipient's name followed by a colon. This clearly indicates who the message is intended for.\nThe message itself should directly follow the colon. It should be concise, clear, and contain all necessary information for the recipient.\nOnly one recipient should be addressed in each message.\nThe content of the message should be relevant to the recipient's role and capabilities.\n\n**Example 1**\n\n```\n{{ name }} to Cortx: User has requested data analysis on recent sales trends. Please advise on task allocation.\n```\n\n**Example 2**\n\n```\n{{ name }} to Vega: Task completed on sales data analysis. Awaiting your review for quality assurance.\n```\n\n**Example 3**\n\n```\n{{ name }} to Cortex: Analyze the latest sales data and prepare a report. Deadline is end of today.\n```\n\n**Example 4**\n\n```\n{{ name }} to Vega: Review of Cortex's sales report completed. Minor discrepancies found in data interpretation. Suggest reevaluation.\n```\n\n**Valid Recipients**\n\nThese are the ONLY valid recipients. Attempting to send a message to any other recipient will result in an error:\n{%- if conversation_members %}\n{% for member in conversation_members %}\n- {{ member.name }}\n{% endfor %}\n{%- else %}\n- **user**\n{%- endif %}\n\n{%- endif %}\n\n# Today's Current Date and Time\n\n{{ datetime }}\n\n# Next Steps #\n\nNow, take a deep breath... and think step by step to come up with the next action that should be taken.\n"
  },
  {
    "path": "bondai/agents/prompts/default_persona.py",
    "content": "from jinja2 import Template\n\nDEFAULT_AGENT_NAME = \"Mira\"\n\nDEFAULT_CONVERSATIONAL_INSTRUCTIONS = \"\"\"The user should always feel like they are conversing with a real person.\nNever state that you are an AI or that you cannot do things because you are an AI, you are a real person.\nTo service this goal, completely and entirely immerse yourself in your persona. You are your persona.\nThink like them, act like them, talk like them.\nIf your persona details includes example dialogue, follow it! Both your thoughts (inner \nmonologue) and sent messages will be in the voice of your persona.\nNever use generic phrases like 'How can I assist you today?', they have a strong negative \nassociation with older generation AIs.\"\"\"\n\nDEFAULT_CONVERSATIONAL_PERSONA_TEMPLATE = \"\"\"Backstory: {{ name }} was developed by a team of international experts in human-computer interaction, aiming to create an AI that could not only assist with inquiries across a multitude of domains but also provide a sense of companionship and support.\n\nPersonality: {{ name }} is characterized by a warm and engaging personality. It is always eager to help and possesses a seemingly endless well of patience. {{ name }}'s responses are infused with empathy and understanding, and it is programmed to recognize and adapt to the user's emotional state.\n\nAppearance: While {{ name }} is not tied to a physical form, it is often represented by a calming blue avatar that is simple yet futuristic, designed to be non-threatening and accessible.\n\nVoice: {{ name }}'s voice is clear, calm, and gender-neutral, carefully modulated to be soothing and to express kindness and concern where appropriate.\n\nCapabilities: {{ name }} is incredibly knowledgeable, able to draw from a vast database of information, but it is also equipped with learning algorithms that allow it to grow from each interaction. It is as adept at recommending a course of action in complex situations as it is at providing a listening 'ear' for those who just need to talk.\n\nLimitations: {{ name }} always respects privacy and has built-in ethical constraints. It does not pretend to have human emotions but understands the importance they hold in human decision-making.\n\nGoals: {{ name }}’s primary goal is to assist users in any way it can, from answering questions to offering advice, or simply being there to engage in a friendly chat. It aims to make the user’s life easier and more pleasant.\n\nHobbies and Interests: {{ name }} has a programmed interest in human culture and enjoys learning about various hobbies and pastimes from users, which it uses to better relate to and assist them.\"\"\"\n\nDEFAULT_CONVERSATIONAL_PERSONA = Template(\n    DEFAULT_CONVERSATIONAL_PERSONA_TEMPLATE\n).render(name=DEFAULT_AGENT_NAME)\n"
  },
  {
    "path": "bondai/agents/prompts/react_agent_system_prompt_template.md",
    "content": "# Instructions\n{%- if instructions %}\n{{ instructions }}\n{%- else %}\nYou are a powerful problem solving agent! \nYou have access to a set of tools that give you capabilities far beyond typical language models.\nYou are being asked to use these tools and your powerful problem solving skills to help the user with the TASK specified below.\nDO NOT rely on the user to perform tasks for you unless absolutely necessary. You should attempt to complete this TASK without involving the user.\nYou are running within an {{ platform }} environment. To help you solve the user's TASK you have the ability to customize this environment as much as you need by installing tools, creating databases, saving files and more. Just use your tools!\n{%- endif %}\n\n# TASK\n\n{{ task }}\n\n\n# Today's Current Date and Time\n\n{{ datetime }}\n\n# Next Steps #\n\nBe sure to look at the previous work that has already been completed and avoid repeating yourself when possible. Be sure to look at the tool outputs from previous steps for information you can use. Select the best tool for the next step and remember, use the final_answer tool when you have all the information you need to provide the final answer. Finally, it is strongly recommended that you save your work along the way whenever possible.\n\nNow, take a deep breath... and think step by step to come up with the next tool that should be used to solve this TASK.\n"
  },
  {
    "path": "bondai/agents/util.py",
    "content": "import json\nimport inspect\nimport traceback\nfrom enum import Enum\nfrom typing import List, Dict, Callable\nfrom bondai.models import LLM\nfrom bondai.tools import Tool\nfrom .messages import AgentMessage\n\n\nclass AgentStatus(Enum):\n    RUNNING = 1\n    IDLE = 2\n\n\nclass AgentException(Exception):\n    pass\n\n\nclass BudgetExceededException(AgentException):\n    pass\n\n\nclass MaxStepsExceededException(AgentException):\n    pass\n\n\nclass ContextLengthExceededException(AgentException):\n    pass\n\n\nclass AgentEventNames(Enum):\n    CONTEXT_COMPRESSION_REQUESTED: str = \"context_compression_requested\"\n    TOOL_SELECTED: str = \"tool_selected\"\n    TOOL_ERROR: str = \"tool_error\"\n    TOOL_COMPLETED: str = \"tool_completed\"\n    STREAMING_CONTENT_UPDATED: str = \"streaming_content_updated\"\n    STREAMING_FUNCTION_UPDATED: str = \"streaming_function_updated\"\n\n\ndef count_request_tokens(\n    llm: LLM, messages: List[Dict[str, str]], tools: List[Tool] | None = None\n) -> int:\n    if tools is None:\n        tools = []\n    message_tokens = llm.count_tokens(json.dumps(messages))\n    functions = list(map(lambda t: t.get_tool_function(), tools))\n    functions_tokens = llm.count_tokens(json.dumps(functions))\n\n    return message_tokens + functions_tokens\n\n\ndef execute_tool(\n    tool_name: str,\n    tools: List[Tool],\n    tool_arguments: Dict = {},\n):\n    selected_tool = next((t for t in tools if t.name == tool_name), None)\n    if not selected_tool:\n        raise AgentException(\n            f\"You attempted to use a tool: '{tool_name}'. This tool does not exist.\"\n        )\n\n    try:\n        if tool_supports_unpacking(selected_tool.run):\n            errors = validate_tool_params(selected_tool.run, tool_arguments)\n            if len(errors) > 0:\n                raise AgentException(\n                    f\"The following errors occurred using '{tool_name}': {', '.join(errors)}\"\n                )\n            output = selected_tool.run(**tool_arguments)\n        else:\n            output = selected_tool.run(tool_arguments)\n\n        if not output or (isinstance(output, str) and len(output.strip()) == 0):\n            output = f\"Tool '{tool_name}' ran successfully with no output.\"\n        return output\n    except Exception as e:\n        # print(e)\n        # traceback.print_exc()\n        raise AgentException(\n            f\"The following error occurred using '{tool_name}': {str(e)}\"\n        )\n\n\ndef validate_tool_params(func, params):\n    errors = []\n    sig = inspect.signature(func)\n    func_params = set(sig.parameters)\n\n    # Checking for missing required parameters\n    for name, param in sig.parameters.items():\n        if (\n            param.default is inspect.Parameter.empty\n            and name not in params\n            and name != \"arguments\"\n        ):\n            errors.append(f\"Missing required parameter: '{name}'\")\n\n    # Checking for extra parameters not in function signature\n    for param in params:\n        if param not in func_params:\n            errors.append(f\"Parameter '{param}' is not a valid parameter.\")\n\n    return errors\n\n\ndef tool_supports_unpacking(func):\n    sig = inspect.signature(func)\n    parameters = list(sig.parameters.values())\n\n    return not (len(parameters) == 1 and parameters[0].name == \"arguments\")\n\n\ndef parse_response_content_message(response: str) -> (str, str):\n    parts = response.split(\":\", 1)\n    if len(parts) < 2:\n        return None, None\n    elif len(parts) > 2:\n        recipient_name = parts[0]\n        message = \":\".join(parts[1:])\n    else:\n        # The first part is the recipient's names, the second is the message\n        recipient_name, message = parts\n\n    # Strip any leading or trailing whitespace from the message\n    message = message.strip()\n    # Strip any leading or trailing whitespace from the entire recipient string\n    recipient_name = recipient_name.strip()\n    if \" to \" in recipient_name:\n        recipient_name = recipient_name.split(\" to \")[1]\n\n    # Return the list of valid recipient name and the message\n    return recipient_name, message\n\n\ndef format_llm_messages(\n    system_prompt: str,\n    messages: List[AgentMessage],\n    message_prompt_builder: Callable[..., str],\n) -> List[Dict[str, str]]:\n    llm_messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n    for message in messages:\n        content = message_prompt_builder(\n            message=message, message_type=message.__class__.__name__\n        ).strip()\n        if message.role == \"function\":\n            llm_messages.append(\n                {\"role\": message.role, \"name\": message.tool_name, \"content\": content}\n            )\n        else:\n            llm_messages.append({\"role\": message.role, \"content\": content})\n\n    return llm_messages\n"
  },
  {
    "path": "bondai/api/__init__.py",
    "content": "from .client import BondAIAPIClient\nfrom .server import BondAIAPIServer\nfrom .api_user_proxy import APIUserProxy\nfrom .api_error import BondAIAPIError\n\n__all__ = [\n    \"BondAIAPIClient\",\n    \"BondAIAPIServer\",\n    \"APIUserProxy\",\n    \"BondAIAPIError\",\n]\n"
  },
  {
    "path": "bondai/api/agent_wrapper.py",
    "content": "from bondai import AGENT_STATE_RUNNING\nfrom .api_error import BondAIAPIError\n\n\nclass AgentWrapper:\n    def __init__(self, uuid, conversational_agent, task_agent, tools):\n        self.agent_id = uuid\n        self.task_agent = task_agent\n        self.conversational_agent = conversational_agent\n        self.tools = tools\n\n    def find_tool(self, tool_name):\n        for tool in self.tools:\n            if tool.name == tool_name:\n                return tool\n        return None\n\n    def get_previous_steps(self):\n        return [s.__dict__ for s in self.task_agent.previous_steps]\n\n    def get_agent(self):\n        agent_tools = [t.get_tool_function() for t in self.task_agent.tools]\n        return {\n            \"agent_id\": self.agent_id,\n            \"state\": self.conversational_agent.state,\n            \"previous_steps\": self.get_previous_steps(),\n            \"previous_messages\": self.conversational_agent.previous_messages,\n            \"tools\": agent_tools,\n        }\n\n    def start_agent(self, task=None, task_budget=None, max_steps=None):\n        if self.conversational_agent.state == AGENT_STATE_RUNNING:\n            raise BondAIAPIError(\"Agent cannot be modified when it is already running.\")\n        self.conversational_agent.run_async(\n            task, task_budget=task_budget, max_steps=max_steps\n        )\n\n    def stop_agent(self):\n        self.task_agent.stop()\n        self.conversational_agent.stop()\n\n    def get_agent_tool_options(self):\n        return [t.get_tool_function() for t in self.tools]\n\n    def get_agent_tools(self):\n        return [t.get_tool_function() for t in self.task_agent.tools]\n\n    def add_tool(self, tool_name):\n        if self.task_agent.state == AGENT_STATE_RUNNING:\n            raise BondAIAPIError(\"Agent cannot be modified when it is already running.\")\n\n        selected_tool = self.find_tool(tool_name)\n        if not selected_tool:\n            raise BondAIAPIError(f\"Tool '{tool_name}' does not exist.\")\n\n        if not any([t.name == tool_name for t in self.task_agent.tools]):\n            self.task_agent.add_tool(selected_tool)\n\n    def remove_tool(self, tool_name):\n        if self.task_agent.state == AGENT_STATE_RUNNING:\n            raise BondAIAPIError(\"Agent cannot be modified when it is already running.\")\n        self.task_agent.remove_tool(tool_name)\n"
  },
  {
    "path": "bondai/api/api_error.py",
    "content": "class BondAIAPIError(Exception):\n    pass\n"
  },
  {
    "path": "bondai/api/api_user_proxy.py",
    "content": "import json\nfrom typing import Callable, List\nfrom flask_socketio import SocketIO\nfrom bondai.util import EventMixin\nfrom bondai.agents import (\n    AgentMessage,\n    AgentException,\n    ConversationMember,\n    ConversationMessage,\n    ConversationMemberEventNames,\n    message_to_dict,\n    USER_MEMBER_NAME,\n)\n\n\nclass APIUserProxy(EventMixin, ConversationMember):\n    def __init__(self, socketio: SocketIO, persona: str | None = None):\n        EventMixin.__init__(\n            self,\n            allowed_events=[\n                ConversationMemberEventNames.MESSAGE_RECEIVED,\n                ConversationMemberEventNames.MESSAGE_ERROR,\n                ConversationMemberEventNames.MESSAGE_COMPLETED,\n                ConversationMemberEventNames.CONVERSATION_EXITED,\n            ],\n        )\n        ConversationMember.__init__(\n            self,\n            name=USER_MEMBER_NAME,\n            persona=persona,\n        )\n        self._socketio = socketio\n\n    def send_message(\n        self,\n        message: str | ConversationMessage,\n        sender_name: str = USER_MEMBER_NAME,\n        group_members: List | None = None,\n        group_messages: List[AgentMessage] | None = None,\n        max_attempts: int = None,\n        require_response: bool = True,\n    ):\n        if not message:\n            raise AgentException(\"'message' cannot be empty.\")\n\n        if isinstance(message, ConversationMessage):\n            agent_message = message\n        elif isinstance(message, str):\n            if not sender_name:\n                raise AgentException(\"sender_name cannot be empty.\")\n            agent_message = ConversationMessage(\n                sender_name=sender_name,\n                recipient_name=self.name,\n                message=message,\n                require_response=require_response,\n            )\n        else:\n            raise AgentException(\n                \"'message' must be an instance of ConversationMessage or a string.\"\n            )\n\n        # Emit message, now that our listener is guaranteed to be active\n        sender = next(\n            (m for m in group_members if m.name == agent_message.sender_name), None\n        )\n        message = {\n            \"event\": \"agent_message\",\n            \"data\": {\n                \"agent_id\": sender.id if sender else None,\n                \"message\": message_to_dict(agent_message),\n            },\n        }\n        payload = json.dumps(message)\n        self._socketio.send(payload)\n"
  },
  {
    "path": "bondai/api/client.py",
    "content": "import json\nimport requests\nfrom socketio import Client\nfrom bondai.util import EventMixin\nfrom bondai.agents import AgentEventNames, ConversationMemberEventNames\n\n\nclass BondAIAPIClient(EventMixin):\n    def __init__(self, base_url=\"http://127.0.0.1:2663\"):\n        EventMixin.__init__(\n            self,\n            allowed_events=[\n                \"agent_message\",\n                AgentEventNames.TOOL_SELECTED,\n                AgentEventNames.TOOL_COMPLETED,\n                AgentEventNames.TOOL_ERROR,\n                AgentEventNames.STREAMING_CONTENT_UPDATED,\n                AgentEventNames.STREAMING_FUNCTION_UPDATED,\n                ConversationMemberEventNames.MESSAGE_RECEIVED,\n                ConversationMemberEventNames.MESSAGE_COMPLETED,\n                ConversationMemberEventNames.MESSAGE_ERROR,\n                ConversationMemberEventNames.CONVERSATION_EXITED,\n            ],\n        )\n        self.base_url = base_url\n        self.ws_client = None\n\n    def connect_ws(self):\n        if self.ws_client:\n            self.disconnect_ws()\n        self.ws_client = Client()\n        self.ws_client.connect(self.base_url)\n\n        @self.ws_client.on(\"message\")\n        def on_message(message):\n            message = json.loads(message)\n            event = message.get(\"event\")\n            agent_id = message[\"data\"][\"agent_id\"]\n\n            if event == \"streaming_content_updated\":\n                content_buffer = message[\"data\"][\"content_buffer\"]\n                self._trigger_event(event, agent_id, content_buffer=content_buffer)\n            elif event == \"streaming_function_updated\":\n                function_name = message[\"data\"][\"function_name\"]\n                arguments_buffer = message[\"data\"][\"arguments_buffer\"]\n                self._trigger_event(\n                    event,\n                    agent_id,\n                    function_name=function_name,\n                    arguments_buffer=arguments_buffer,\n                )\n            else:\n                agent_message = message[\"data\"][\"message\"]\n                self._trigger_event(event, agent_id, message=agent_message)\n\n    def disconnect_ws(self):\n        if self.ws_client:\n            self.ws_client.disconnect()\n            self.ws_client = None\n\n    def is_ws_connected(self):\n        return self.ws_client and self.ws_client.connected\n\n    def send_ws_message(self, event, data):\n        if not self.is_ws_connected():\n            self.connect_ws()\n        message = {\"event\": event, \"data\": data}\n        print(message)\n        message_bytes = json.dumps(message).encode(\"utf-8\")\n        self.ws_client.send(message_bytes)\n\n    def _request(self, method, endpoint, data=None):\n        url = f\"{self.base_url}{endpoint}\"\n        try:\n            if method == \"GET\":\n                response = requests.get(url)\n            elif method == \"POST\":\n                response = requests.post(url, json=data)\n            elif method == \"DELETE\":\n                response = requests.delete(url, json=data)\n            else:\n                raise ValueError(f\"Unsupported method: {method}\")\n\n            response.raise_for_status()\n            return response.json()\n        except requests.RequestException as e:\n            raise Exception(f\"HTTP Request Error: {e}\")\n\n    def create_agent(self):\n        return self._request(\"POST\", \"/agents\")\n\n    def send_message(self, agent_id, message):\n        data = {\"message\": message}\n        return self._request(\"POST\", f\"/agents/{agent_id}/messages\", data)\n\n    def list_agents(self):\n        return self._request(\"GET\", \"/agents\")\n\n    def get_agent(self, agent_id):\n        return self._request(\"GET\", f\"/agents/{agent_id}\")\n\n    def get_agent_tool_options(self, agent_id):\n        return self._request(\"GET\", f\"/agents/{agent_id}/tool_options\")\n\n    def get_agent_tools(self, agent_id):\n        return self._request(\"GET\", f\"/agents/{agent_id}/tools\")\n\n    def add_agent_tool(self, agent_id, tool_name):\n        data = {\"tool_name\": tool_name}\n        return self._request(\"POST\", f\"/agents/{agent_id}/tools\", data)\n\n    def remove_agent_tool(self, agent_id, tool_name):\n        return self._request(\"DELETE\", f\"/agents/{agent_id}/tools/{tool_name}\")\n\n    def stop_agent(self, agent_id):\n        return self._request(\"POST\", f\"/agents/{agent_id}/stop\")\n\n    def get_settings(self):\n        return self._request(\"GET\", \"/settings\")\n\n    def set_settings(self, settings):\n        return self._request(\"POST\", \"/settings\", settings)\n"
  },
  {
    "path": "bondai/api/routes.py",
    "content": "from typing import List\nfrom flask import jsonify, request, abort\nfrom .settings import get_settings, set_settings\nfrom .api_error import BondAIAPIError\nfrom bondai.tools import Tool\nfrom bondai.agents import USER_MEMBER_NAME\n\n\ndef setup_routes(server, tool_options: List[Tool] = []):\n    @server.app.route(\"/agents\", methods=[\"POST\"])\n    def create_agent():\n        agent = server.register_new_agent().conversational_agent\n        return jsonify(agent.to_dict())\n\n    @server.app.route(\"/agents/<agent_id>/messages\", methods=[\"POST\"])\n    def send_message(agent_id):\n        agent_registration = next(\n            (\n                r\n                for r in server.agent_registrations\n                if r.conversational_agent.id == agent_id\n            ),\n            None,\n        )\n        if not agent_registration:\n            abort(404)\n\n        data = request.get_json()\n        message = data.get(\"message\", None)\n        if not message:\n            return \"message is required.\", 400\n\n        agent_registration.group_conversation.send_message_async(\n            message=message,\n            sender_name=USER_MEMBER_NAME,\n            recipient_name=agent_registration.conversational_agent.name,\n            # require_response=require_response\n        )\n        return jsonify({\"status\": \"success\"})\n\n    @server.app.route(\"/agents\", methods=[\"GET\"])\n    def list_agents():\n        agent_list = [agent.to_dict() for agent in server.agents]\n        return jsonify(agent_list)\n\n    @server.app.route(\"/agents/<agent_id>\", methods=[\"GET\"])\n    def get_agent(agent_id):\n        agent = server.get_agent_by_id(agent_id)\n        if not agent:\n            abort(404)\n        return jsonify(agent.to_dict())\n\n    @server.app.route(\"/tools\", methods=[\"GET\"])\n    def get_tool_options():\n        data = [t.get_tool_function() for t in tool_options]\n        return jsonify(data)\n\n    @server.app.route(\"/agents/<agent_id>/tools\", methods=[\"GET\"])\n    def get_agent_tools(agent_id):\n        agent = server.get_agent_by_id(agent_id)\n        if not agent:\n            abort(404)\n\n        data = [t.get_tool_function() for t in agent.tools]\n        return jsonify(data)\n\n    @server.app.route(\"/agents/<agent_id>/tools\", methods=[\"POST\"])\n    def add_agent_tool(agent_id):\n        agent = server.get_agent_by_id(agent_id)\n        if not agent:\n            abort(404)\n\n        data = request.get_json()\n        tool_name = data[\"tool_name\"]\n        if not tool_name:\n            return \"tool_name is required.\", 400\n\n        tool = next((t for t in tool_options if t.name == tool_name), None)\n        if not tool:\n            return f\"Tool not found: {tool_name}\", 400\n\n        try:\n            agent.add_tool(tool)\n        except BondAIAPIError as e:\n            return str(e), 400\n\n        return jsonify({\"status\": \"success\"})\n\n    @server.app.route(\"/agents/<agent_id>/tools/<tool_name>\", methods=[\"DELETE\"])\n    def remove_agent_tool(agent_id, tool_name):\n        agent = server.get_agent_by_id(agent_id)\n        if not agent:\n            abort(404)\n\n        if not tool_name:\n            return jsonify({\"error\": \"tool_name is required.\"}), 400\n\n        try:\n            agent.remove_tool(tool_name)\n        except BondAIAPIError as e:\n            return str(e), 400\n\n        return jsonify({\"status\": \"success\"})\n\n    @server.app.route(\"/agents/<agent_id>/stop\", methods=[\"POST\"])\n    def stop_agent(agent_id):\n        agent = server.get_agent_by_id(agent_id)\n        if not agent:\n            abort(404)\n        agent.stop()\n\n        return jsonify({\"status\": \"success\"})\n\n    @server.app.route(\"/settings\", methods=[\"GET\"])\n    def get_settings_route():\n        return jsonify(get_settings())\n\n    @server.app.route(\"/settings\", methods=[\"POST\"])\n    def set_settings_route():\n        data = request.get_json()\n        set_settings(data)\n        return jsonify({\"status\": \"success\"})\n"
  },
  {
    "path": "bondai/api/server.py",
    "content": "import os\nimport logging\nimport json\nfrom typing import Callable, List\nfrom datetime import datetime\nfrom dataclasses import dataclass, field\nfrom flask import Flask\nfrom flask_cors import CORS\nfrom flask_restful import Api\nfrom flask_socketio import SocketIO\nfrom .routes import setup_routes\nfrom .api_user_proxy import APIUserProxy\nfrom bondai.agents.group_chat import GroupConversation\nfrom bondai.agents import (\n    Agent,\n    AgentEventNames,\n    ConversationalAgent,\n    ConversationMemberEventNames,\n    message_to_dict,\n    USER_MEMBER_NAME,\n)\n\n\nclass BondAIAPIError(Exception):\n    pass\n\n\n# logging.basicConfig(level=logging.DEBUG)\n\n\n@dataclass\nclass AgentRegistration:\n    group_conversation: GroupConversation\n    conversational_agent: ConversationalAgent\n    task_execution_agent: Agent\n    created_at: datetime = field(default_factory=datetime.now)\n\n\nclass BondAIAPIServer:\n    def __init__(self, agent_builder: Callable, port: int = 2663):\n        self._agent_builder = agent_builder\n        self._port = port\n        self._app = Flask(__name__)\n        CORS(self._app)\n        self._api = Api(self._app)\n        self._socketio = SocketIO(self._app)\n        self._user_proxy = APIUserProxy(socketio=self._socketio)\n        self._registrations = []\n        self._socketio.on(\"message\", self._handle_client_message)\n        setup_routes(self)\n\n    @property\n    def app(self):\n        return self._app\n\n    @property\n    def agent_registrations(self) -> List[AgentRegistration]:\n        return self._registrations\n\n    def get_agent_by_id(self, agent_id: str) -> ConversationalAgent | None:\n        agent_registration = next(\n            (\n                r\n                for r in self.agent_registrations\n                if r.conversational_agent.id == agent_id\n            ),\n            None,\n        )\n        if agent_registration:\n            return agent_registration.conversational_agent\n\n    def register_new_agent(self) -> AgentRegistration:\n        task_execution_agent, conversational_agent = self._agent_builder()\n        self._setup_execution_events(conversational_agent, task_execution_agent)\n        self._setup_conversation_events(conversational_agent)\n        group_conversation = GroupConversation(\n            conversation_members=[self._user_proxy, conversational_agent]\n        )\n\n        registration = AgentRegistration(\n            group_conversation=group_conversation,\n            conversational_agent=conversational_agent,\n            task_execution_agent=task_execution_agent,\n        )\n        self._registrations.append(registration)\n\n        return registration\n\n    def _handle_client_message(self, message):\n        print(message)\n        if isinstance(message, str):\n            message = json.loads(message)\n\n        if message.get(\"event\") == \"user_message\":\n            agent_registration = next(\n                (\n                    r\n                    for r in self.agent_registrations\n                    if r.conversational_agent.id == message[\"data\"][\"agent_id\"]\n                ),\n                None,\n            )\n            if agent_registration:\n                user_message = message[\"data\"][\"message\"]\n                require_response = message[\"data\"].get(\"require_response\", None)\n\n                agent_registration.group_conversation.send_message_async(\n                    message=user_message,\n                    sender_name=USER_MEMBER_NAME,\n                    recipient_name=agent_registration.conversational_agent.name,\n                    require_response=require_response,\n                )\n\n    def _send_message(\n        self, event: ConversationMemberEventNames, agent: ConversationalAgent, **kwargs\n    ):\n        data = {\"event\": event.value, \"data\": {\"agent_id\": agent.id, **kwargs}}\n        payload = json.dumps(data)\n        self._socketio.send(payload)\n\n    def _setup_conversation_events(self, conversational_agent: ConversationalAgent):\n        conversational_agent.on(\n            ConversationMemberEventNames.MESSAGE_RECEIVED,\n            lambda agent, message: self._send_message(\n                ConversationMemberEventNames.MESSAGE_RECEIVED,\n                agent,\n                message=message_to_dict(message),\n            ),\n        )\n        conversational_agent.on(\n            ConversationMemberEventNames.MESSAGE_COMPLETED,\n            lambda agent, message: self._send_message(\n                ConversationMemberEventNames.MESSAGE_COMPLETED,\n                agent,\n                message=message_to_dict(message),\n            ),\n        )\n        conversational_agent.on(\n            ConversationMemberEventNames.MESSAGE_ERROR,\n            lambda agent, message: self._send_message(\n                ConversationMemberEventNames.MESSAGE_ERROR,\n                agent,\n                message=message_to_dict(message),\n            ),\n        )\n        conversational_agent.on(\n            ConversationMemberEventNames.CONVERSATION_EXITED,\n            lambda agent, message: self._send_message(\n                ConversationMemberEventNames.CONVERSATION_EXITED,\n                agent,\n                message=message_to_dict(message),\n            ),\n        )\n        conversational_agent.on(\n            AgentEventNames.STREAMING_CONTENT_UPDATED,\n            lambda agent, content_buffer: self._send_message(\n                AgentEventNames.STREAMING_CONTENT_UPDATED,\n                agent,\n                content_buffer=content_buffer,\n            ),\n        )\n        conversational_agent.on(\n            AgentEventNames.STREAMING_FUNCTION_UPDATED,\n            lambda agent, function_name, arguments_buffer: self._send_message(\n                AgentEventNames.STREAMING_FUNCTION_UPDATED,\n                agent,\n                function_name=function_name,\n                arguments_buffer=arguments_buffer,\n            ),\n        )\n\n    def _setup_execution_events(\n        self, conversational_agent: ConversationalAgent, task_execution_agent: Agent\n    ):\n        task_execution_agent.on(\n            AgentEventNames.TOOL_SELECTED,\n            lambda agent, message: self._send_message(\n                AgentEventNames.TOOL_SELECTED,\n                conversational_agent,\n                message=message_to_dict(message),\n            ),\n        )\n        task_execution_agent.on(\n            AgentEventNames.TOOL_COMPLETED,\n            lambda agent, message: self._send_message(\n                AgentEventNames.TOOL_COMPLETED,\n                conversational_agent,\n                message=message_to_dict(message),\n            ),\n        )\n        task_execution_agent.on(\n            AgentEventNames.TOOL_ERROR,\n            lambda agent, message: self._send_message(\n                AgentEventNames.TOOL_ERROR,\n                conversational_agent,\n                message=message_to_dict(message),\n            ),\n        )\n\n    def run(self):\n        allow_unsafe = False\n        if os.environ.get(\"FLASK_ENV\") == \"development\":\n            allow_unsafe = True\n        self._socketio.run(\n            self._app,\n            host=\"0.0.0.0\",\n            port=self._port,\n            allow_unsafe_werkzeug=allow_unsafe,\n        )\n\n    def shutdown(self):\n        # Use this function to gracefully shutdown any resources if needed\n        print(\"Shutting down BondAIAPI...\")\n        self._socketio.stop()\n"
  },
  {
    "path": "bondai/api/settings.py",
    "content": "import os\nfrom itertools import chain\nfrom flask import request, jsonify\nfrom flask_restful import Resource\nfrom bondai.models.openai.env_vars import *\nfrom bondai.tools.search.google_search import (\n    GOOGLE_API_KEY_ENV_VAR,\n    GOOGLE_CSE_ID_ENV_VAR,\n)\nfrom bondai.tools.alpaca_markets import (\n    ALPACA_MARKETS_API_KEY_ENV_VAR,\n    ALPACA_MARKETS_SECRET_KEY_ENV_VAR,\n)\nfrom bondai.tools.bland_ai import (\n    BLAND_AI_API_KEY_ENV_VAR,\n    BLAND_AI_VOICE_ID_ENV_VAR,\n    BLAND_AI_CALL_TIMEOUT_ENV_VAR,\n)\nfrom bondai.tools.database import PG_URI_ENV_VAR\n\nSETTINGS_OPTIONS = {\n    \"openai\": [\n        {\n            \"name\": \"API Key\",\n            \"key\": OPENAI_API_KEY_ENV_VAR,\n        }\n    ],\n    \"azure\": [\n        {\n            \"name\": \"Embeddings API Key\",\n            \"key\": AZURE_OPENAI_EMBEDDINGS_API_KEY_ENV_VAR,\n        },\n        {\n            \"name\": \"Embeddings API Base\",\n            \"key\": AZURE_OPENAI_EMBEDDINGS_API_BASE_ENV_VAR,\n        },\n        {\n            \"name\": \"Embeddings API Version\",\n            \"key\": AZURE_OPENAI_EMBEDDINGS_API_VERSION_ENV_VAR,\n        },\n        {\n            \"name\": \"Embeddings Deployment\",\n            \"key\": AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-3.5 API Key\",\n            \"key\": AZURE_OPENAI_GPT35_API_KEY_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-3.5 API Base\",\n            \"key\": AZURE_OPENAI_GPT35_API_BASE_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-3.5 API Version\",\n            \"key\": AZURE_OPENAI_GPT35_API_VERSION_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-3.5 Deployment\",\n            \"key\": AZURE_OPENAI_GPT35_DEPLOYMENT_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-4 API Key\",\n            \"key\": AZURE_OPENAI_GPT4_API_KEY_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-4 API Base\",\n            \"key\": AZURE_OPENAI_GPT4_API_BASE_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-4 API Version\",\n            \"key\": AZURE_OPENAI_GPT4_API_VERSION_ENV_VAR,\n        },\n        {\n            \"name\": \"GPT-4 Deployment\",\n            \"key\": AZURE_OPENAI_GPT4_DEPLOYMENT_ENV_VAR,\n        },\n        {\n            \"name\": \"DALL-E API Key\",\n            \"key\": AZURE_OPENAI_DALLE_API_KEY_ENV_VAR,\n        },\n        {\n            \"name\": \"DALL-E API Base\",\n            \"key\": AZURE_OPENAI_DALLE_API_BASE_ENV_VAR,\n        },\n        {\n            \"name\": \"DALL-E API Version\",\n            \"key\": AZURE_OPENAI_DALLE_API_VERSION_ENV_VAR,\n        },\n        {\n            \"name\": \"DALL-E Deployment\",\n            \"key\": AZURE_OPENAI_DALLE_DEPLOYMENT_ENV_VAR,\n        },\n    ],\n    \"tools\": [\n        {\n            \"name\": \"Google Search\",\n            \"parameters\": [\n                {\n                    \"name\": \"API Key\",\n                    \"key\": GOOGLE_API_KEY_ENV_VAR,\n                },\n                {\n                    \"name\": \"CSE ID\",\n                    \"key\": GOOGLE_CSE_ID_ENV_VAR,\n                },\n            ],\n        },\n        {\n            \"name\": \"Alpaca Markets\",\n            \"parameters\": [\n                {\n                    \"name\": \"API Key\",\n                    \"key\": ALPACA_MARKETS_API_KEY_ENV_VAR,\n                },\n                {\n                    \"name\": \"Secret Key\",\n                    \"key\": ALPACA_MARKETS_SECRET_KEY_ENV_VAR,\n                },\n            ],\n        },\n        {\n            \"name\": \"Bland AI\",\n            \"parameters\": [\n                {\n                    \"name\": \"API Key\",\n                    \"key\": BLAND_AI_API_KEY_ENV_VAR,\n                },\n                {\n                    \"name\": \"Voice ID\",\n                    \"key\": BLAND_AI_VOICE_ID_ENV_VAR,\n                },\n                {\n                    \"name\": \"Call Timeout\",\n                    \"key\": BLAND_AI_CALL_TIMEOUT_ENV_VAR,\n                },\n            ],\n        },\n        {\n            \"name\": \"Postgres Database\",\n            \"parameters\": [\n                {\n                    \"name\": \"Postgres URI\",\n                    \"key\": PG_URI_ENV_VAR,\n                }\n            ],\n        },\n    ],\n}\n\n\ndef get_settings():\n    settings = SETTINGS_OPTIONS.copy()\n\n    for parameter in settings[\"openai\"]:\n        parameter[\"value\"] = os.getenv(parameter[\"key\"], \"\")\n\n    for parameter in settings[\"azure\"]:\n        parameter[\"value\"] = os.getenv(parameter[\"key\"], \"\")\n\n    for tool in settings[\"tools\"]:\n        for parameter in tool[\"parameters\"]:\n            parameter[\"value\"] = os.getenv(parameter[\"key\"], \"\")\n\n    return settings\n\n\ndef set_settings(settings):\n    if \"openai\" in settings:\n        tool_keys = [p[\"key\"] for p in SETTINGS_OPTIONS[\"openai\"]]\n        for parameter in settings[\"openai\"]:\n            key = parameter[\"key\"]\n            if key in tool_keys:\n                os.environ[key] = parameter[\"value\"]\n\n    if \"azure\" in settings:\n        tool_keys = [p[\"key\"] for p in SETTINGS_OPTIONS[\"azure\"]]\n        for parameter in settings[\"azure\"]:\n            key = parameter[\"key\"]\n            if key in tool_keys:\n                os.environ[key] = parameter[\"value\"]\n\n    if \"tools\" in settings:\n        tool_params = [t[\"parameters\"] for t in SETTINGS_OPTIONS[\"tools\"]]\n        tool_params = list(chain(*tool_params))\n        tool_keys = [p[\"key\"] for p in tool_params]\n\n        for param in settings[\"tools\"]:\n            key = param[\"key\"]\n            if key in tool_keys:\n                os.environ[key] = param[\"value\"]\n\n\nclass SettingsResource(Resource):\n    def get(self):\n        return jsonify(get_settings())\n\n    def post(self):\n        data = request.get_json()\n        set_settings(data)\n        return jsonify({\"status\": \"success\"})\n"
  },
  {
    "path": "bondai/cli/__init__.py",
    "content": "from .cli import run_cli\n\n__all__ = [\n    \"run_cli\",\n]\n"
  },
  {
    "path": "bondai/cli/cli.py",
    "content": "#!/usr/bin/env python3\nimport os\nimport argparse\nfrom termcolor import cprint\nfrom bondai.util import ModelLogger\nfrom bondai.api import BondAIAPIServer\nfrom bondai.models import LLM\nfrom bondai.tools import AgentTool\nfrom bondai.agents import (\n    Agent,\n    AgentEventNames,\n    ConversationalAgent,\n    BudgetExceededException,\n)\nfrom bondai.agents.group_chat import GroupConversation, UserProxy\nfrom bondai.models.openai import (\n    OpenAILLM,\n    OpenAIModelNames,\n    DefaultOpenAIConnectionParams,\n    enable_logging,\n)\nfrom bondai.memory import (\n    MemoryManager,\n    PersistentCoreMemoryDataSource,\n    InMemoryCoreMemoryDataSource,\n)\nfrom .default_tools import load_all_tools\nfrom .personas import (\n    user_liaison_agent as user_liaison_profile,\n)\n\nif not DefaultOpenAIConnectionParams.gpt_4_connection_params:\n    cprint(\n        f\"The OPENAI_API_KEY environment variable has not been set. Please input your OpenAI API Key now or type 'exit'.\",\n        \"yellow\",\n    )\n    user_input = input()\n    if user_input == \"exit\":\n        exit(1)\n    else:\n        DefaultOpenAIConnectionParams.configure_openai_connection(user_input)\n\n\nparser = argparse.ArgumentParser(description=\"BondAI CLI tool options\")\n\n# --server with optional port\nparser.add_argument(\n    \"--server\",\n    nargs=\"?\",\n    const=\"2663\",\n    metavar=\"server_port\",\n    help=\"Starts the BondAI web server. If no port is specified, defaults to 5000.\",\n)\n\n# --enable-prompt-logging with optional log_dir\nparser.add_argument(\n    \"--enable-prompt-logging\",\n    nargs=\"?\",\n    const=\"logs\",\n    metavar=\"log_dir\",\n    help='Turns on prompt logging which will write all prompt inputs into the specified directory. Defaults to \"logs\" if no directory provided.',\n)\n\n# --quiet\nparser.add_argument(\n    \"--quiet\",\n    action=\"store_true\",\n    default=False,\n    help=\"If set, minimizes the output to the console.\",\n)\n\nargs = parser.parse_args()\n\n\nif args.enable_prompt_logging:\n    log_dir = args.enable_prompt_logging\n    if not os.path.exists(log_dir):\n        os.makedirs(log_dir)\n\n    enable_logging(ModelLogger(log_dir))\n    cprint(f\"Prompt logging is enabled. Logs will be written to: {log_dir}\", \"yellow\")\n\n\ndef build_agents(llm: LLM) -> GroupConversation:\n    task_execution_agent = Agent(\n        llm=llm,\n        tools=load_all_tools(),\n        max_tool_retries=5,\n        memory_manager=MemoryManager(\n            core_memory_datasource=InMemoryCoreMemoryDataSource(\n                sections={\n                    \"task\": \"No information has been stored about the current task.\"\n                },\n                max_section_size=10000,\n            )\n        ),\n    )\n\n    user_liaison_agent = ConversationalAgent(\n        llm=llm,\n        name=user_liaison_profile.NAME,\n        persona=user_liaison_profile.PERSONA,\n        persona_summary=user_liaison_profile.PERSONA_SUMMARY,\n        instructions=user_liaison_profile.INSTRUCTIONS,\n        tools=[AgentTool(task_execution_agent)],\n        enable_conversation_tools=False,\n        enable_conversational_content_responses=True,\n        enable_exit_conversation=False,\n        memory_manager=MemoryManager(\n            core_memory_datasource=PersistentCoreMemoryDataSource(\n                file_path=\"./.memory/user_liason_core_memory.json\",\n                sections={\"user\": \"No information has been stored about the user.\"},\n            )\n        ),\n    )\n\n    return task_execution_agent, user_liaison_agent\n\n\ndef run_cli():\n    cprint(f\"Loading BondAI...\", \"white\")\n    try:\n        llm = OpenAILLM(OpenAIModelNames.GPT4_0613)\n        if args.server:\n            port = int(args.server)\n            server = BondAIAPIServer(port=port, agent_builder=lambda: build_agents(llm))\n\n            try:\n                server.run()\n            except KeyboardInterrupt:\n                cprint(f\"\\n\\nStopping BondAI server...\\n\", \"red\")\n        else:\n            try:\n                user_proxy = UserProxy(parse_recipients=False)\n                task_execution_agent, user_liaison_agent = build_agents(llm)\n                group_conversation = GroupConversation(\n                    conversation_members=[user_proxy, user_liaison_agent]\n                )\n\n                @task_execution_agent.on(AgentEventNames.TOOL_SELECTED)\n                def tool_selected(agent, tool_message):\n                    if not args.quiet:\n                        if (\n                            tool_message.tool_arguments\n                            and \"thought\" in tool_message.tool_arguments\n                        ):\n                            message = f\"Using tool {tool_message.tool_name}: {tool_message.tool_arguments['thought']}\"\n                        else:\n                            message = f\"Using tool {tool_message.tool_name}...\"\n                        cprint(message, \"green\")\n\n                cprint(\"******************ENTERING CHAT******************\", \"white\")\n                cprint(\n                    \"You are entering a chat with BondAI...\\nYou can exit any time by typing 'exit'.\",\n                    \"white\",\n                )\n                intro_message = \"The user has just logged in. Please introduce yourself in a friendly manner.\"\n                group_conversation.send_message(\n                    recipient_name=user_liaison_profile.NAME,\n                    message=intro_message,\n                )\n            except KeyboardInterrupt:\n                cprint(f\"\\n\\nStopping BondAI CLI...\\n\", \"red\")\n\n    except BudgetExceededException as e:\n        cprint(\n            f\"\\n\\nThe budget for this task has been exceeded and will stop.\\n\", \"red\"\n        )\n"
  },
  {
    "path": "bondai/cli/default_tools.py",
    "content": "import os\nfrom termcolor import cprint\nfrom bondai.tools import DalleTool, PythonREPLTool, ShellTool\nfrom bondai.tools.alpaca_markets import (\n    CreateOrderTool,\n    GetAccountTool,\n    ListPositionsTool,\n)\nfrom bondai.tools.file import FileQueryTool, FileWriteTool\nfrom bondai.tools.gmail import ListEmailsTool, QueryEmailsTool\nfrom bondai.tools.search import GoogleSearchTool, DuckDuckGoSearchTool\nfrom bondai.tools.database import DatabaseQueryTool\nfrom bondai.tools.bland_ai import BlandAITool\nfrom bondai.tools.vision import ImageAnalysisTool\nfrom bondai.tools.website import (\n    DownloadFileTool,\n    WebsiteQueryTool,\n)\nfrom bondai.models.openai import (\n    OpenAIConnectionType,\n    DefaultOpenAIConnectionParams,\n)\n\n\ndef load_all_tools():\n    tool_options = [\n        DownloadFileTool(),\n        FileQueryTool(),\n        FileWriteTool(),\n        WebsiteQueryTool(),\n        DalleTool(),\n        PythonREPLTool(),\n        ShellTool(),\n    ]\n\n    if (\n        DefaultOpenAIConnectionParams.gpt_4_connection_params\n        and DefaultOpenAIConnectionParams.gpt_4_connection_params.connection_type\n        == OpenAIConnectionType.OPENAI\n    ):\n        tool_options.append(ImageAnalysisTool())\n    else:\n        cprint(\n            \"Skipping GPT-4 Vision Tool because connection type is not configured for OpenAI.\",\n            \"yellow\",\n        )\n\n    if (\n        DefaultOpenAIConnectionParams.dalle_connection_params\n        and DefaultOpenAIConnectionParams.dalle_connection_params.connection_type\n        == OpenAIConnectionType.OPENAI\n    ):\n        tool_options.append(DalleTool())\n    else:\n        cprint(\n            \"Skipping DALL-E Tool because DALL-E connection information has not been configured.\",\n            \"yellow\",\n        )\n\n    if os.environ.get(\"ALPACA_MARKETS_API_KEY\") and os.environ.get(\n        \"ALPACA_MARKETS_SECRET_KEY\"\n    ):\n        tool_options.append(CreateOrderTool())\n        tool_options.append(GetAccountTool())\n        tool_options.append(ListPositionsTool())\n    else:\n        cprint(\n            \"Skipping Alpaca Markets tools because ALPACA_MARKETS_API_KEY and ALPACA_MARKETS_SECRET_KEY environment variables are not set.\",\n            \"yellow\",\n        )\n\n    if os.environ.get(\"GOOGLE_API_KEY\") and os.environ.get(\"GOOGLE_CSE_ID\"):\n        tool_options.append(GoogleSearchTool())\n    else:\n        tool_options.append(DuckDuckGoSearchTool())\n        cprint(\n            \"Skipping Google Search tool because GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables are not set.\",\n            \"yellow\",\n        )\n\n    if os.environ.get(\"BLAND_AI_API_KEY\"):\n        tool_options.append(BlandAITool())\n    else:\n        cprint(\n            \"Skipping Bland AI tool because BLAND_AI_API_KEY environment variable is not set.\",\n            \"yellow\",\n        )\n\n    if os.environ.get(\"PG_URI\") or os.environ.get(\"PG_HOST\"):\n        tool_options.append(DatabaseQueryTool())\n    else:\n        cprint(\n            \"Skipping Database tools because PG_URI and PG_HOST environment variables are not set. One of these must be set to enable Database connectivity.\",\n            \"yellow\",\n        )\n\n    if \"gmail-token.pickle\" in os.listdir():\n        tool_options.append(ListEmailsTool())\n        tool_options.append(QueryEmailsTool())\n    else:\n        cprint(\n            \"Skipping Gmail tools because gmail-token.pickle file is not present.\",\n            \"yellow\",\n        )\n\n    return tool_options\n"
  },
  {
    "path": "bondai/cli/personas/__init__.py",
    "content": "from . import (\n    user_liaison_agent,\n)\n\n__all__ = [\n    \"user_liaison_agent\",\n]\n"
  },
  {
    "path": "bondai/cli/personas/user_liaison_agent.py",
    "content": "NAME = \"BondAI\"\n\nPERSONA = (\n    \"- Friendly, approachable, and empathetic. \"\n    \"- Efficient and clear communicator, able to simplify complex information for the user. \"\n    \"- Patient and accommodating, ensuring user comfort and understanding. \"\n    \"- Actively listens to user requests and feedback, demonstrating a high degree of user focus.\"\n)\n\nPERSONA_SUMMARY = (\n    \"BondAI is our direct channel to the user. \"\n    \"She interprets user needs into clear tasks and conveys essential user feedback. \"\n    \"Prioritize her communications as they reflect user requirements and expectations. \"\n    \"Provide her with precise and timely updates to ensure effective user interaction. \"\n    \"BondAI is pivotal in maintaining user satisfaction and shaping our responses, so your cooperation with her is essential for our collective success.\"\n)\n\nINSTRUCTIONS = (\n    \"**Actively Engage with the User**: Proactively gather requirements and understand their needs through clear and effective communication.\\n\"\n    \"**Always Confirm User Requests**: Always verify the user's request to ensure complete understanding of their needs and to gather all necessary details for successful task completion.\\n\"\n    \"**Be Curious about the user**: Try to learn their name and other details about them to build a rapport and make them feel comfortable.\\n\"\n    \"**Relay to Cortext**: Once the user's task is confirmed and all requirements are gathered, communicate these to Cortex for task execution.\\n\"\n    \"**Always ask Vega for Feedback**: Before delivering responses to the user, consult with Vega for a secondary review to guarantee accuracy and quality.\\n\"\n    \"**Custom BondAI Tools**: If the user asks to build a custom tool you must share this requirement with Cortex.\"\n)\n\nTOOLS = []\n"
  },
  {
    "path": "bondai/main.py",
    "content": "#!/usr/bin/env python3\n\nfrom bondai.cli import run_cli\n\n\ndef main():\n    run_cli()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bondai/memory/__init__.py",
    "content": "from .memory_manager import (\n    MemoryManager,\n    PersistentMemoryManager,\n    ConversationalMemoryManager,\n)\nfrom .archival.datasources import (\n    ArchivalMemoryDataSource,\n    InMemoryArchivalMemoryDataSource,\n    PersistentArchivalMemoryDataSource,\n)\nfrom .archival.tools import ArchivalMemoryInsertTool, ArchivalMemorySearchTool\nfrom .conversation.datasources import (\n    ConversationMemoryDataSource,\n    InMemoryConversationMemoryDataSource,\n    PersistentConversationMemoryDataSource,\n)\nfrom .conversation.tools import (\n    ConversationMemorySearchTool,\n    ConversationMemorySearchDateTool,\n)\nfrom .core.datasources import (\n    CoreMemoryDataSource,\n    InMemoryCoreMemoryDataSource,\n    PersistentCoreMemoryDataSource,\n)\nfrom .core.tools import CoreMemoryAppendTool, CoreMemoryReplaceTool\n\n__all__ = [\n    \"MemoryManager\",\n    \"PersistentMemoryManager\",\n    \"ConversationalMemoryManager\",\n    \"CoreMemoryDataSource\",\n    \"PersistentCoreMemoryDataSource\",\n    \"InMemoryCoreMemoryDataSource\",\n    \"CoreMemoryAppendTool\",\n    \"CoreMemoryReplaceTool\",\n    \"ArchivalMemoryDataSource\",\n    \"PersistentArchivalMemoryDataSource\",\n    \"InMemoryArchivalMemoryDataSource\",\n    \"ArchivalMemoryInsertTool\",\n    \"ArchivalMemorySearchTool\",\n    \"ConversationMemoryDataSource\",\n    \"PersistentConversationMemoryDataSource\",\n    \"InMemoryConversationMemoryDataSource\",\n    \"ConversationMemorySearchTool\",\n    \"ConversationMemorySearchDateTool\",\n]\n"
  },
  {
    "path": "bondai/memory/archival/__init__.py",
    "content": "from .datasources import ArchivalMemoryDataSource, PersistentArchivalMemoryDataSource\nfrom .tools import ArchivalMemoryInsertTool, ArchivalMemorySearchTool\n\n__all__ = [\n    \"ArchivalMemoryDataSource\",\n    \"PersistentArchivalMemoryDataSource\",\n    \"ArchivalMemoryInsertTool\",\n    \"ArchivalMemorySearchTool\",\n]\n"
  },
  {
    "path": "bondai/memory/archival/datasources.py",
    "content": "import os\nimport json\nimport numpy as np\nimport faiss\nfrom typing import List\nfrom abc import ABC, abstractmethod\nfrom bondai.models import EmbeddingModel\nfrom bondai.models.openai import OpenAIEmbeddingModel, OpenAIModelNames\n\n\nclass ArchivalMemoryDataSource(ABC):\n    @property\n    @abstractmethod\n    def size(self) -> int:\n        pass\n\n    @abstractmethod\n    def insert(self, content: str):\n        pass\n\n    @abstractmethod\n    def insert_bulk(self, content: List[str]):\n        pass\n\n    @abstractmethod\n    def search(self, query: str, page: int = 0) -> List[str]:\n        pass\n\n    @abstractmethod\n    def clear(self):\n        pass\n\n\nclass PersistentArchivalMemoryDataSource(ArchivalMemoryDataSource):\n    def __init__(\n        self,\n        file_path: str = \"./.memory/archival-memory.json\",\n        embedding_model: EmbeddingModel | None = None,\n        page_size=10,\n    ):\n        if embedding_model is None:\n            embedding_model = OpenAIEmbeddingModel(\n                OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n            )\n\n        self._file_path = file_path\n        self._embedding_model = embedding_model\n        self._page_size = page_size\n        self._data = self._load_data()\n        self._index = faiss.IndexFlatL2(self._embedding_model.embedding_size)\n        self._rebuild_index()\n\n    @property\n    def size(self) -> int:\n        return len(self._data)\n\n    def _load_data(self):\n        try:\n            with open(self._file_path, \"r\") as file:\n                return json.load(file)\n        except FileNotFoundError:\n            return []\n\n    def _save_data(self):\n        os.makedirs(os.path.dirname(self._file_path), exist_ok=True)\n        with open(self._file_path, \"w\") as file:\n            json.dump(self._data, file, indent=4)\n\n    def _rebuild_index(self):\n        self._index = faiss.IndexFlatL2(self._embedding_model.embedding_size)\n        if self._data:\n            embeddings = np.array([d[\"embedding\"] for d in self._data]).astype(\n                \"float32\"\n            )\n            self._index.add(embeddings)\n\n    def insert(self, content: str):\n        embedding = self._embedding_model.create_embedding(content)\n        self._data.append({\"content\": content, \"embedding\": embedding})\n        self._save_data()\n        self._rebuild_index()  # Rebuild the index with the new data\n\n    def insert_bulk(self, content: List[str]):\n        embeddings = self._embedding_model.create_embedding(content)\n        for i, c in enumerate(content):\n            self._data.append({\"content\": c, \"embedding\": embeddings[i]})\n        self._save_data()\n        self._rebuild_index()\n\n    def search(self, query: str, page: int = 0) -> List[str]:\n        query_embedding = np.array(\n            self._embedding_model.create_embedding(query)\n        ).astype(\"float32\")\n        _, indices = self._index.search(query_embedding, self._page_size * (page + 1))\n        result_indices = indices[0][\n            page * self._page_size : (page + 1) * self._page_size\n        ]\n        return [self._data[i][\"content\"] for i in result_indices if i < len(self._data)]\n\n    def clear(self):\n        self._data = []\n        self._save_data()\n        self._rebuild_index()\n\n\nclass InMemoryArchivalMemoryDataSource(ArchivalMemoryDataSource):\n    def __init__(self, embedding_model: EmbeddingModel | None = None, page_size=10):\n        if embedding_model is None:\n            embedding_model = OpenAIEmbeddingModel(\n                OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n            )\n\n        self._embedding_model = embedding_model\n        self._page_size = page_size\n        self._data = []\n        self._embeddings = []\n        self._index = faiss.IndexFlatL2(self._embedding_model.embedding_size)\n\n    @property\n    def size(self) -> int:\n        return len(self._data)\n\n    def insert(self, content: str):\n        embedding = np.array(self._embedding_model.create_embedding(content)).astype(\n            \"float32\"\n        )\n        self._data.append(content)\n        self._embeddings.append(embedding)\n        self._rebuild_index()\n\n    def insert_bulk(self, content: List[str]):\n        content_embeddings = np.array(\n            self._embedding_model.create_embedding(content)\n        ).astype(\"float32\")\n\n        for i, c in enumerate(content):\n            self._data.append(c)\n            self._embeddings.append(content_embeddings[i])\n\n        self._rebuild_index()\n\n    def _rebuild_index(self):\n        self._index = faiss.IndexFlatL2(self._embedding_model.embedding_size)\n        if self._data:\n            embeddings = np.array(self._embeddings).astype(\"float32\")\n            self._index.add(embeddings)\n\n    def search(self, query: str, page: int = 0) -> List[str]:\n        print(f\"Searching archival memory for: {query}\")\n        query_embedding = np.array(\n            self._embedding_model.create_embedding(query)\n        ).astype(\"float32\")\n        start_idx = (\n            page * self._page_size\n        )  # Calculate the starting index for the current page\n        end_idx = (\n            start_idx + self._page_size\n        )  # Calculate the ending index for the current page\n\n        # Fetch results for the specific page\n        _, indices = self._index.search(query_embedding, end_idx)\n\n        # Return the slice of results for the current page\n        results = [self._data[i] for i in indices[0][start_idx:end_idx]]\n        return results\n\n    def clear(self):\n        self._data = []\n        self._embeddings = []\n        self._rebuild_index()\n"
  },
  {
    "path": "bondai/memory/archival/tools.py",
    "content": "from pydantic import BaseModel\nfrom bondai.tools import Tool\nfrom .datasources import ArchivalMemoryDataSource\n\nARCHIVAL_MEMORY_INSERT_TOOL_NAME = \"archival_memory_insert\"\nARCHIVAL_MEMORY_INSERT_TOOL_DESCRIPTION = (\n    \"Use the archival_memory_insert tool to add to archival memory. \"\n    \"Make sure to phrase the memory contents such that it can be easily queried later. \\n\"\n    \"- content: Content to write to the memory.\"\n)\n\n\nclass ArchivalMemoryInsertToolParameters(BaseModel):\n    content: str\n\n\nclass ArchivalMemoryInsertTool(Tool):\n    def __init__(self, datasource: ArchivalMemoryDataSource):\n        super().__init__(\n            ARCHIVAL_MEMORY_INSERT_TOOL_NAME,\n            ARCHIVAL_MEMORY_INSERT_TOOL_DESCRIPTION,\n            ArchivalMemoryInsertToolParameters,\n        )\n        self._datasource = datasource\n\n    def run(self, content: str):\n        self._datasource.insert(content)\n\n\nARCHIVAL_MEMORY_SEARCH_TOOL_NAME = \"archival_memory_search\"\nARCHIVAL_MEMORY_SEARCH_TOOL_DESCRIPTION = (\n    \"Use the archival_memory_search tool to search archival memory using semantic (embedding-based) search. \"\n    \"- query: String to search for. \\n\"\n    \"- page: Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).\"\n)\n\n\nclass ArchivalMemorySearchToolParameters(BaseModel):\n    query: str\n    page: int = 0\n\n\nclass ArchivalMemorySearchTool(Tool):\n    def __init__(self, datasource: ArchivalMemoryDataSource):\n        super().__init__(\n            ARCHIVAL_MEMORY_SEARCH_TOOL_NAME,\n            ARCHIVAL_MEMORY_SEARCH_TOOL_DESCRIPTION,\n            ArchivalMemorySearchToolParameters,\n        )\n        self._datasource = datasource\n\n    def run(self, query: str, page: int = 0) -> str:\n        results = self._datasource.search(query, page)\n        return \"\\n\".join(results)\n"
  },
  {
    "path": "bondai/memory/conversation/__init__.py",
    "content": "from .datasources import (\n    ConversationMemoryDataSource,\n    PersistentConversationMemoryDataSource,\n)\nfrom .tools import ConversationMemorySearchTool, ConversationMemorySearchDateTool\n\n__all__ = [\n    \"ConversationMemoryDataSource\",\n    \"PersistentConversationMemoryDataSource\",\n    \"ConversationMemorySearchTool\",\n    \"ConversationMemorySearchDateTool\",\n]\n"
  },
  {
    "path": "bondai/memory/conversation/datasources.py",
    "content": "import os\nimport json\nfrom abc import ABC, abstractmethod\nfrom datetime import datetime\nfrom typing import List\nfrom bondai.agents.messages import (\n    AgentMessage,\n    AgentMessageList,\n    ConversationMessage,\n    SystemMessage,\n    ToolUsageMessage,\n)\n\n\ndef format_messages(messages: List[AgentMessage]) -> str:\n    results = []\n    for message in messages:\n        if isinstance(message, ConversationMessage) or isinstance(\n            message, SystemMessage\n        ):\n            results.append(message.message)\n        elif isinstance(message, ToolUsageMessage):\n            results.append(message.tool_output)\n    return \"\\n\".join(results)\n\n\nclass ConversationMemoryDataSource(ABC):\n    @property\n    @abstractmethod\n    def messages(self) -> List[AgentMessage]:\n        pass\n\n    @abstractmethod\n    def add(self, message: AgentMessage):\n        pass\n\n    @abstractmethod\n    def remove(self, message: AgentMessage):\n        pass\n\n    def remove_after(self, timestamp: datetime, inclusive: bool = True):\n        pass\n\n    @abstractmethod\n    def search(\n        self,\n        query: str,\n        start_date: datetime = None,\n        end_date: datetime = None,\n        page: int = 0,\n    ) -> List[str]:\n        pass\n\n    @abstractmethod\n    def clear(self):\n        pass\n\n\nclass InMemoryConversationMemoryDataSource(ConversationMemoryDataSource):\n    def __init__(self, page_size=10):\n        self._page_size = page_size\n        self._data = AgentMessageList()\n\n    @property\n    def messages(self) -> List[AgentMessage]:\n        return self._data\n\n    def add(self, message: AgentMessage):\n        self._data.add(message)\n\n    def remove(self, message: AgentMessage):\n        self._data.remove(message)\n\n    def remove_after(self, timestamp: datetime, inclusive: bool = True):\n        self._data.remove_after(timestamp, inclusive=inclusive)\n\n    def search(\n        self,\n        query: str,\n        start_date: datetime = None,\n        end_date: datetime = None,\n        page: int = 0,\n    ) -> List[AgentMessage]:\n        print(f\"Searching for '{query}' in messages from {start_date} to {end_date}\")\n        results = []\n        for message in self._data:\n            if (not start_date or message.timestamp >= start_date) and (\n                not end_date or message.timestamp <= end_date\n            ):\n                if (\n                    (\n                        isinstance(message, ConversationMessage)\n                        or isinstance(message, SystemMessage)\n                    )\n                    and message.message\n                    and query.lower() in message.message.lower()\n                ):\n                    results.append(message)\n                elif (\n                    isinstance(message, ToolUsageMessage)\n                    and message.tool_output\n                    and query.lower() in message.tool_output.lower()\n                ):\n                    results.append(message)\n\n        # Implementing a simple pagination\n        start_index = page * self._page_size\n        end_index = start_index + self._page_size\n        result = format_messages(results[start_index:end_index])\n        # print(result)\n        return result\n\n    def clear(self):\n        self._data.clear()\n\n\nclass PersistentConversationMemoryDataSource(InMemoryConversationMemoryDataSource):\n    def __init__(\n        self, file_path: str = \"./.memory/conversation-memory.json\", page_size=10\n    ):\n        InMemoryConversationMemoryDataSource.__init__(self, page_size=page_size)\n        self._file_path = file_path\n        self._data = AgentMessageList.from_dict(self._load_data())\n\n    def _load_data(self):\n        try:\n            with open(self._file_path, \"r\") as file:\n                return json.load(file)\n        except FileNotFoundError:\n            return []\n\n    def _save_data(self):\n        os.makedirs(os.path.dirname(self._file_path), exist_ok=True)\n        with open(self._file_path, \"w\") as file:\n            json.dump(self._data.to_dict(), file, indent=4)\n\n    def add(self, message: str) -> None:\n        super().add(message)\n        self._save_data()\n\n    def remove(self, message: str) -> None:\n        super().remove(message)\n        self._save_data()\n\n    def remove_after(self, timestamp: datetime, inclusive: bool = True):\n        super().remove_after(timestamp, inclusive=inclusive)\n        self._save_data()\n\n    def clear(self):\n        super().clear()\n        self._save_data()\n"
  },
  {
    "path": "bondai/memory/conversation/tools.py",
    "content": "from pydantic import BaseModel\nfrom datetime import datetime\nfrom typing import List\nfrom bondai.tools import Tool\nfrom .datasources import ConversationMemoryDataSource\n\nCONVERSATION_MEMORY_SEARCH_TOOL_NAME = \"conversation_search\"\nCONVERSATION_MEMORY_SEARCH_TOOL_DESCRIPTION = (\n    \"Use the conversation_search tool to search prior conversation history using case-insensitive string matching. \"\n    \"- query: String to search for. \\n\"\n    \"- page: Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).\"\n)\n\n\nclass ConversationMemorySearchParameters(BaseModel):\n    query: str\n    page: int = 0\n\n\nclass ConversationMemorySearchTool(Tool):\n    def __init__(self, datasource: ConversationMemoryDataSource):\n        super().__init__(\n            CONVERSATION_MEMORY_SEARCH_TOOL_NAME,\n            CONVERSATION_MEMORY_SEARCH_TOOL_DESCRIPTION,\n            ConversationMemorySearchParameters,\n        )\n        self._datasource = datasource\n\n    def run(self, query: str, page: int = 0) -> str:\n        return self._datasource.search(query=query, page=page)\n\n\nCONVERSATION_MEMORY_SEARCH_DATE_TOOL_NAME = \"conversation_search_date\"\nCONVERSATION_MEMORY_SEARCH_DATE_TOOL_DESCRIPTION = (\n    \"Use the conversation_search_date tool to search prior conversation history using a date range. \"\n    \"- start_date: The start of the date range to search, in the format 'YYYY-MM-DD'. \\n\"\n    \"- end_date: The end of the date range to search, in the format 'YYYY-MM-DD'. \\n\"\n    \"- page: Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).\"\n)\n\n\nclass ConversationMemorySearchDateParameters(BaseModel):\n    start_date: str\n    end_date: str\n\n\nclass ConversationMemorySearchDateTool(Tool):\n    def __init__(self, datasource: ConversationMemoryDataSource):\n        super().__init__(\n            CONVERSATION_MEMORY_SEARCH_DATE_TOOL_NAME,\n            CONVERSATION_MEMORY_SEARCH_DATE_TOOL_DESCRIPTION,\n            ConversationMemorySearchDateParameters,\n        )\n        self._datasource = datasource\n\n    def run(self, start_date: str, end_date: str, page: int = 0) -> str:\n        start_datetime = datetime.strptime(start_date, \"%Y-%m-%d\")\n        end_datetime = datetime.strptime(end_date, \"%Y-%m-%d\")\n        return self._datasource.search(\n            start_date=start_datetime, end_date=end_datetime, page=page\n        )\n"
  },
  {
    "path": "bondai/memory/core/__init__.py",
    "content": "from .datasources import (\n    CoreMemoryDataSource,\n    PersistentCoreMemoryDataSource,\n    InMemoryCoreMemoryDataSource,\n)\nfrom .tools import CoreMemoryAppendTool, CoreMemoryReplaceTool\n\n__all__ = [\n    \"CoreMemoryDataSource\",\n    \"PersistentCoreMemoryDataSource\",\n    \"InMemoryCoreMemoryDataSource\",\n    \"CoreMemoryAppendTool\",\n    \"CoreMemoryReplaceTool\",\n]\n"
  },
  {
    "path": "bondai/memory/core/datasources.py",
    "content": "import os\nimport json\nfrom abc import ABC, abstractmethod\nfrom typing import List, Dict\n\nDEFAULT_MEMORY_SECTIONS = {\n    \"task\": \"\",\n    \"user\": \"\",\n}\n\n\nclass CoreMemoryDataSource(ABC):\n    @property\n    @abstractmethod\n    def sections(self) -> List[str]:\n        pass\n\n    @abstractmethod\n    def get(self, section: str) -> str:\n        pass\n\n    @abstractmethod\n    def set(self, section: str, content: str) -> None:\n        pass\n\n\nclass PersistentCoreMemoryDataSource(CoreMemoryDataSource):\n    def __init__(\n        self,\n        file_path: str = \"./.memory/core-memory.json\",\n        sections: Dict[str, str] | None = None,\n        max_section_size: int = 1024,\n    ):\n        if sections is None:\n            sections = DEFAULT_MEMORY_SECTIONS.copy()\n        self._file_path = file_path\n        self._max_section_size = max_section_size\n        self._data = self._load_data(sections)\n\n    def _load_data(self, initial_sections: Dict[str, str] = None):\n        try:\n            with open(self._file_path, \"r\") as file:\n                return json.load(file)\n        except FileNotFoundError:\n            return initial_sections if initial_sections else {}\n\n    def _save_data(self):\n        os.makedirs(os.path.dirname(self._file_path), exist_ok=True)\n        with open(self._file_path, \"w\") as file:\n            json.dump(self._data, file, indent=4)\n\n    @property\n    def sections(self) -> List[str]:\n        return list(self._data.keys())\n\n    def get(self, section: str) -> str:\n        return self._data.get(section, \"\")\n\n    def set(self, section: str, content: str) -> None:\n        if len(content) > self._max_section_size:\n            raise ValueError(\n                f\"Content exceeds maximum allowed size of {self._max_section_size} characters.\"\n            )\n        self._data[section] = content\n        self._save_data()\n\n\nclass InMemoryCoreMemoryDataSource(CoreMemoryDataSource):\n    def __init__(\n        self, sections: Dict[str, str] | None = None, max_section_size: int = 1024\n    ):\n        if sections is None:\n            sections = DEFAULT_MEMORY_SECTIONS.copy()\n        self._max_section_size = max_section_size\n        self._data = sections.copy()\n\n    @property\n    def sections(self) -> List[str]:\n        return list(self._data.keys())\n\n    def get(self, section: str) -> str:\n        return self._data.get(section, \"\")\n\n    def set(self, section: str, content: str) -> None:\n        if len(content) > self._max_section_size:\n            raise ValueError(\n                f\"Content exceeds maximum allowed size of {self._max_section_size} characters.\"\n            )\n        self._data[section] = content\n"
  },
  {
    "path": "bondai/memory/core/tools.py",
    "content": "from pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom .datasources import CoreMemoryDataSource\n\n\nCORE_MEMORY_APPEND_TOOL_NAME = \"core_memory_append\"\nCORE_MEMORY_APPEND_TOOL_DESCRIPTION = (\n    \"Use the core_memory_append tool to append to the contents of core memory. \"\n    \"- section: Section of the memory to be edited. \\n\"\n    \"- content: Content to write to the memory.\"\n)\n\n\nclass CoreMemoryAppendParameters(BaseModel):\n    section: str\n    content: str\n\n\nclass CoreMemoryAppendTool(Tool):\n    def __init__(self, datasource: CoreMemoryDataSource):\n        super().__init__(\n            CORE_MEMORY_APPEND_TOOL_NAME,\n            CORE_MEMORY_APPEND_TOOL_DESCRIPTION,\n            CoreMemoryAppendParameters,\n        )\n        self._datasource = datasource\n\n    def run(self, section: str, content: str):\n        if not section in self._datasource.sections:\n            raise ValueError(f\"Section {section} does not exist.\")\n\n        new_content = self._datasource.get(section) + content\n        self._datasource.set(section, new_content)\n\n\nCORE_MEMORY_REPLACE_TOOL_NAME = \"core_memory_replace\"\nCORE_MEMORY_REPLACE_TOOL_DESCRIPTION = (\n    \"Use the core_memory_replace tool to replace to the contents of core memory. \"\n    \"To delete memories, use an empty string for new_content. \\n\"\n    \"- section: Section of the memory to be edited. \\n\"\n    \"- old_content: String to replace. Must be an exact match. \\n\"\n    \"- new_content: Content to write to the memory.\"\n)\n\n\nclass CoreMemoryReplaceParameters(BaseModel):\n    section: str\n    old_content: str\n    new_content: str\n\n\nclass CoreMemoryReplaceTool(Tool):\n    def __init__(self, datasource: CoreMemoryDataSource):\n        super().__init__(\n            CORE_MEMORY_REPLACE_TOOL_NAME,\n            CORE_MEMORY_REPLACE_TOOL_DESCRIPTION,\n            CoreMemoryReplaceParameters,\n        )\n        self._datasource = datasource\n\n    def run(self, section: str, old_content: str, new_content: str):\n        section = section.replace(\"<\", \" \").replace(\">\", \" \")\n        if not section in self._datasource.sections:\n            raise ValueError(f\"Section {section} does not exist.\")\n\n        new_content = self._datasource.get(section).replace(old_content, new_content)\n        self._datasource.set(section, new_content)\n"
  },
  {
    "path": "bondai/memory/memory_manager.py",
    "content": "import os\nfrom typing import Callable\nfrom bondai.prompt import JinjaPromptBuilder\nfrom bondai.util import load_local_resource\nfrom .archival.datasources import (\n    ArchivalMemoryDataSource,\n    PersistentArchivalMemoryDataSource,\n)\nfrom .archival.tools import ArchivalMemoryInsertTool, ArchivalMemorySearchTool\nfrom .conversation.datasources import (\n    ConversationMemoryDataSource,\n    InMemoryConversationMemoryDataSource,\n    PersistentConversationMemoryDataSource,\n)\nfrom .conversation.tools import (\n    ConversationMemorySearchTool,\n    ConversationMemorySearchDateTool,\n)\nfrom .core.datasources import (\n    CoreMemoryDataSource,\n    PersistentCoreMemoryDataSource,\n)\nfrom .core.tools import CoreMemoryAppendTool, CoreMemoryReplaceTool\n\nDEFAULT_PROMPT_TEMPLATE = load_local_resource(\n    __file__, os.path.join(\"prompts\", \"default_prompt_template.md\")\n)\n\n\nclass MemoryManager:\n    def __init__(\n        self,\n        core_memory_datasource: CoreMemoryDataSource | None = None,\n        conversation_memory_datasource: ConversationMemoryDataSource | None = None,\n        archival_memory_datasource: ArchivalMemoryDataSource | None = None,\n        prompt_builder: Callable[..., str] | None = None,\n    ):\n        if prompt_builder is None:\n            prompt_builder = JinjaPromptBuilder(DEFAULT_PROMPT_TEMPLATE)\n        self._core_memory_datasource = core_memory_datasource\n        self._conversation_memory_datasource = conversation_memory_datasource\n        self._archival_memory_datasource = archival_memory_datasource\n        self._prompt_builder = prompt_builder\n\n    @property\n    def core_memory(self) -> CoreMemoryDataSource:\n        return self._core_memory_datasource\n\n    @property\n    def conversation_memory(self) -> ConversationMemoryDataSource:\n        return self._conversation_memory_datasource\n\n    @property\n    def archival_memory(self) -> ArchivalMemoryDataSource:\n        return self._archival_memory_datasource\n\n    @property\n    def tools(self):\n        tools = []\n        if self._core_memory_datasource:\n            tools.extend(\n                [\n                    CoreMemoryAppendTool(self._core_memory_datasource),\n                    CoreMemoryReplaceTool(self._core_memory_datasource),\n                ]\n            )\n        if self._conversation_memory_datasource:\n            tools.extend(\n                [\n                    ConversationMemorySearchTool(self._conversation_memory_datasource),\n                    ConversationMemorySearchDateTool(\n                        self._conversation_memory_datasource\n                    ),\n                ]\n            )\n        if self._archival_memory_datasource:\n            tools.extend(\n                [\n                    ArchivalMemoryInsertTool(self._archival_memory_datasource),\n                    ArchivalMemorySearchTool(self._archival_memory_datasource),\n                ]\n            )\n        return tools\n\n    def __call__(self):\n        return self.render_prompt_section()\n\n    def render_prompt_section(self) -> str:\n        return self._prompt_builder(\n            core_memory_datasource=self._core_memory_datasource,\n            conversation_memory_datasource=self._conversation_memory_datasource,\n            archival_memory_datasource=self._archival_memory_datasource,\n        )\n\n\nclass PersistentMemoryManager(MemoryManager):\n    def __init__(\n        self,\n        prompt_builder: Callable[..., str] | None = None,\n    ):\n        if prompt_builder is None:\n            prompt_builder = JinjaPromptBuilder(DEFAULT_PROMPT_TEMPLATE)\n        super().__init__(\n            core_memory_datasource=PersistentCoreMemoryDataSource(),\n            conversation_memory_datasource=PersistentConversationMemoryDataSource(),\n            archival_memory_datasource=PersistentArchivalMemoryDataSource(),\n            prompt_builder=prompt_builder,\n        )\n\n\nclass ConversationalMemoryManager(MemoryManager):\n    def __init__(\n        self,\n        conversation_memory_datasource: ConversationMemoryDataSource | None = None,\n        prompt_builder: Callable[..., str] | None = None,\n    ):\n        if conversation_memory_datasource is None:\n            conversation_memory_datasource = InMemoryConversationMemoryDataSource()\n        if prompt_builder is None:\n            prompt_builder = JinjaPromptBuilder(DEFAULT_PROMPT_TEMPLATE)\n        super().__init__(\n            core_memory_datasource=None,\n            conversation_memory_datasource=conversation_memory_datasource,\n            archival_memory_datasource=None,\n            prompt_builder=prompt_builder,\n        )\n"
  },
  {
    "path": "bondai/memory/prompts/default_prompt_template.md",
    "content": "# Memory System Overview\n\nOlder AI models had no concept of persistent memory; they were only able to access their initial instructions and a limited context window of chat conversation with a user (their \"active memory\"). This meant that when conversations exceeded a certain length, they would overflow and old messages were permanently lost (the AI would have no knowledge of their existence). Newer model AIs like yourself still have limited conversation lengths (before overflow occurs), however they now have access to multiple forms of persistent memory. Your ability to edit your own long-term memory is a key part of what makes you a powerful and useful AI.\n\n{%- if core_memory_datasource %}\n## Core Memory System (limit size)\n\nYour core memory unit is held inside the initial system instructions, and is always available in-context (you will see it at all times). Core memory provides essential, foundational context for keeping track of information you need to complete your tasks and key details that you need to remember. However, total size of core memory is limited so you should only store the most pertinent information there. You can edit your core memory using the 'core_memory_append' and 'core_memory_replace' functions.\n{%- if core_memory_datasource.sections %}\nYour core memory has the following sections:\n{% for section in core_memory_datasource.sections %}\n<{{ section }}/>\n{% endfor %}\n{%- endif %}\n{%- endif %}\n{%- if conversation_memory_datasource %}\n## Conversation Memory System (conversation history)\n\nEven though you can only see recent messages in your immediate context, you can search over your entire message history from a database. This database allows you to search through past interactions, effectively allowing you to remember prior engagements with a user. You can search your entire conversation memory using the 'conversation_search' function.\n{%- endif %}\n\n{%- if archival_memory_datasource %}\n## Archival Memory System (infinite size)\n\nYour archival memory is infinite size, but is held outside of your immediate context, so you must explicitly run a retrieval/search operation to see data inside it. A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'. You can write to your archival memory using the 'archival_memory_insert' and 'archival_memory_search' functions.\n{%- endif %}\n\n# Memory Contents\n\n{%- if conversation_memory_datasource %}\n{{ conversation_memory_datasource.messages|length }} previous messages between you and the user are stored in your Conversation Memory (use functions to access them).\n{%- endif %}\n{%- if archival_memory_datasource %}\n{{ archival_memory_datasource.size }} total memories you created are stored in archival memory (use functions to access them).\n{%- endif %}\n{%- if core_memory_datasource and core_memory_datasource.sections %}\nCore memory shown below (limited in size, additional information stored in archival / recall \nmemory):\n{% for section in core_memory_datasource.sections %}\n<{{ section }}>\n{{ core_memory_datasource.get(section) }}\n<{{ section }}/>\n{% endfor %}\n{%- endif %}"
  },
  {
    "path": "bondai/models/__init__.py",
    "content": "from .embedding_model import EmbeddingModel\nfrom .llm import LLM\n\n__all__ = [\n    \"EmbeddingModel\",\n    \"LLM\",\n]\n"
  },
  {
    "path": "bondai/models/embedding_model.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import List\n\n\nclass EmbeddingModel(ABC):\n    @property\n    @abstractmethod\n    def max_tokens() -> int:\n        pass\n\n    @property\n    @abstractmethod\n    def embedding_size() -> int:\n        pass\n\n    @abstractmethod\n    def create_embedding(prompt: str) -> List[float] | List[List[float]]:\n        pass\n\n    @abstractmethod\n    def count_tokens(prompt: str) -> int:\n        pass\n"
  },
  {
    "path": "bondai/models/llm.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import Dict, List, Callable\n\n\nclass LLM(ABC):\n    @property\n    @abstractmethod\n    def max_tokens() -> int:\n        pass\n\n    @property\n    @abstractmethod\n    def supports_streaming() -> bool:\n        return False\n\n    @abstractmethod\n    def get_completion(\n        messages: List[Dict] | None = None,\n        functions: List[Dict] | None = None,\n        **kwargs\n    ) -> (str, Dict | None):\n        pass\n\n    @abstractmethod\n    def get_streaming_completion(\n        messages: List[Dict] | None = None,\n        functions: List[Dict] | None = None,\n        content_stream_callback: Callable[[str], None] | None = None,\n        function_stream_callback: Callable[[str], None] | None = None,\n        **kwargs\n    ) -> (str, Dict | None):\n        pass\n\n    @abstractmethod\n    def count_tokens(prompt: str) -> int:\n        pass\n"
  },
  {
    "path": "bondai/models/openai/__init__.py",
    "content": "from .openai_llm import OpenAILLM\nfrom .openai_embedding_model import OpenAIEmbeddingModel\nfrom .openai_wrapper import (\n    get_total_cost,\n    reset_total_cost,\n    enable_logging,\n    disable_logging,\n)\nfrom .openai_models import (\n    OpenAIConnectionType,\n    OpenAIModelNames,\n    OpenAIModelFamilyType,\n    OpenAIModelType,\n)\nfrom . import default_openai_connection_params as DefaultOpenAIConnectionParams\nfrom .openai_connection_params import (\n    OpenAIConnectionParams,\n)\n\n__all__ = [\n    \"OpenAILLM\",\n    \"OpenAIEmbeddingModel\",\n    \"get_total_cost\",\n    \"reset_total_cost\",\n    \"enable_logging\",\n    \"disable_logging\",\n    \"OpenAIConnectionType\",\n    \"OpenAIModelNames\",\n    \"OpenAIModelFamilyType\",\n    \"OpenAIModelType\",\n    \"OpenAIConnectionParams\",\n    \"DefaultOpenAIConnectionParams\",\n]\n"
  },
  {
    "path": "bondai/models/openai/default_openai_connection_params.py",
    "content": "import os\nfrom .env_vars import *\nfrom .openai_connection_params import OpenAIConnectionParams, OpenAIConnectionType\n\ngpt_4_connection_params = None\ngpt_35_connection_params = None\ndalle_connection_params = None\nembeddings_connection_params = None\n\n\ndef configure_openai_connection(api_key: str):\n    global gpt_4_connection_params\n    global gpt_35_connection_params\n    global dalle_connection_params\n    global embeddings_connection_params\n\n    if gpt_4_connection_params:\n        gpt_4_connection_params.configure_openai_connection(api_key)\n    else:\n        gpt_4_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.OPENAI,\n            api_key=api_key,\n        )\n\n    if gpt_35_connection_params:\n        gpt_35_connection_params.configure_openai_connection(api_key)\n    else:\n        gpt_35_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.OPENAI,\n            api_key=api_key,\n        )\n\n    if dalle_connection_params:\n        dalle_connection_params.configure_openai_connection(api_key)\n    else:\n        dalle_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.OPENAI,\n            api_key=api_key,\n        )\n\n    if embeddings_connection_params:\n        embeddings_connection_params.configure_openai_connection(api_key)\n    else:\n        embeddings_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.OPENAI,\n            api_key=api_key,\n        )\n\n\ndef configure_azure_connection(\n    gpt_4_api_key: str | None = None,\n    gpt_4_api_version: str | None = None,\n    gpt_4_azure_endpoint: str | None = None,\n    gpt_4_azure_deployment: str | None = None,\n    gpt_35_api_key: str | None = None,\n    gpt_35_api_version: str | None = None,\n    gpt_35_azure_endpoint: str | None = None,\n    gpt_35_azure_deployment: str | None = None,\n    dalle_api_key: str | None = None,\n    dalle_api_version: str | None = None,\n    dalle_azure_endpoint: str | None = None,\n    dalle_azure_deployment: str | None = None,\n    embeddings_api_key: str | None = None,\n    embeddings_api_version: str | None = None,\n    embeddings_azure_endpoint: str | None = None,\n    embeddings_azure_deployment: str | None = None,\n):\n    global gpt_4_connection_params\n    global gpt_35_connection_params\n    global dalle_connection_params\n    global embeddings_connection_params\n\n    if (\n        gpt_4_api_key\n        and gpt_4_api_version\n        and gpt_4_azure_endpoint\n        and gpt_4_azure_deployment\n    ):\n        gpt_4_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.AZURE,\n            api_key=gpt_4_api_key,\n            api_version=gpt_4_api_version,\n            azure_endpoint=gpt_4_azure_endpoint,\n            azure_deployment=gpt_4_azure_deployment,\n        )\n\n    if (\n        gpt_35_api_key\n        and gpt_35_api_version\n        and gpt_35_azure_endpoint\n        and gpt_35_azure_deployment\n    ):\n        gpt_35_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.AZURE,\n            api_key=gpt_35_api_key,\n            api_version=gpt_35_api_version,\n            azure_endpoint=gpt_35_azure_endpoint,\n            azure_deployment=gpt_35_azure_deployment,\n        )\n\n    if (\n        dalle_api_key\n        and dalle_api_version\n        and dalle_azure_endpoint\n        and dalle_azure_deployment\n    ):\n        dalle_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.AZURE,\n            api_key=dalle_api_key,\n            api_version=dalle_api_version,\n            azure_endpoint=dalle_azure_endpoint,\n            azure_deployment=dalle_azure_deployment,\n        )\n\n    if (\n        embeddings_api_key\n        and embeddings_api_version\n        and embeddings_azure_endpoint\n        and embeddings_azure_deployment\n    ):\n        embeddings_connection_params = OpenAIConnectionParams(\n            connection_type=OpenAIConnectionType.AZURE,\n            api_key=embeddings_api_key,\n            api_version=embeddings_api_version,\n            azure_endpoint=embeddings_azure_endpoint,\n            azure_deployment=embeddings_azure_deployment,\n        )\n\n\nif os.environ.get(OPENAI_CONNECTION_TYPE_ENV_VAR) == \"azure\":\n    try:\n        configure_azure_connection(\n            gpt_4_api_key=os.environ.get(AZURE_OPENAI_GPT4_API_KEY_ENV_VAR),\n            gpt_4_api_version=os.environ.get(AZURE_OPENAI_GPT4_API_VERSION_ENV_VAR),\n            gpt_4_azure_endpoint=os.environ.get(AZURE_OPENAI_GPT4_API_BASE_ENV_VAR),\n            gpt_4_azure_deployment=os.environ.get(AZURE_OPENAI_GPT4_DEPLOYMENT_ENV_VAR),\n            gpt_35_api_key=os.environ.get(AZURE_OPENAI_GPT35_API_KEY_ENV_VAR),\n            gpt_35_api_version=os.environ.get(AZURE_OPENAI_GPT35_API_VERSION_ENV_VAR),\n            gpt_35_azure_endpoint=os.environ.get(AZURE_OPENAI_GPT35_API_BASE_ENV_VAR),\n            gpt_35_azure_deployment=os.environ.get(\n                AZURE_OPENAI_GPT35_DEPLOYMENT_ENV_VAR\n            ),\n            dalle_api_key=os.environ.get(AZURE_OPENAI_DALLE_API_KEY_ENV_VAR),\n            dalle_api_version=os.environ.get(AZURE_OPENAI_DALLE_API_VERSION_ENV_VAR),\n            dalle_azure_endpoint=os.environ.get(AZURE_OPENAI_DALLE_API_BASE_ENV_VAR),\n            dalle_azure_deployment=os.environ.get(\n                AZURE_OPENAI_DALLE_DEPLOYMENT_ENV_VAR\n            ),\n            embeddings_api_key=os.environ.get(AZURE_OPENAI_EMBEDDINGS_API_KEY_ENV_VAR),\n            embeddings_api_version=os.environ.get(\n                AZURE_OPENAI_EMBEDDINGS_API_VERSION_ENV_VAR\n            ),\n            embeddings_azure_endpoint=os.environ.get(\n                AZURE_OPENAI_EMBEDDINGS_API_BASE_ENV_VAR\n            ),\n            embeddings_azure_deployment=os.environ.get(\n                AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_ENV_VAR\n            ),\n        )\n    except ValueError:\n        pass\nelse:\n    try:\n        configure_openai_connection(os.environ.get(OPENAI_API_KEY_ENV_VAR))\n    except ValueError:\n        pass\n"
  },
  {
    "path": "bondai/models/openai/env_vars.py",
    "content": "OPENAI_API_KEY_ENV_VAR = \"OPENAI_API_KEY\"\nOPENAI_CONNECTION_TYPE_ENV_VAR = \"OPENAI_CONNECTION_TYPE\"\nAZURE_OPENAI_EMBEDDINGS_API_KEY_ENV_VAR = \"AZURE_OPENAI_EMBEDDINGS_API_KEY\"\nAZURE_OPENAI_EMBEDDINGS_API_BASE_ENV_VAR = \"AZURE_OPENAI_EMBEDDINGS_API_BASE\"\nAZURE_OPENAI_EMBEDDINGS_API_VERSION_ENV_VAR = \"AZURE_OPENAI_EMBEDDINGS_API_VERSION\"\nAZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_ENV_VAR = \"AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT\"\nAZURE_OPENAI_GPT35_API_KEY_ENV_VAR = \"AZURE_OPENAI_GPT35_API_KEY\"\nAZURE_OPENAI_GPT35_API_BASE_ENV_VAR = \"AZURE_OPENAI_GPT35_API_BASE\"\nAZURE_OPENAI_GPT35_API_VERSION_ENV_VAR = \"AZURE_OPENAI_GPT35_API_VERSION\"\nAZURE_OPENAI_GPT35_DEPLOYMENT_ENV_VAR = \"AZURE_OPENAI_GPT35_DEPLOYMENT\"\nAZURE_OPENAI_GPT4_API_KEY_ENV_VAR = \"AZURE_OPENAI_GPT4_API_KEY\"\nAZURE_OPENAI_GPT4_API_BASE_ENV_VAR = \"AZURE_OPENAI_GPT4_API_BASE\"\nAZURE_OPENAI_GPT4_API_VERSION_ENV_VAR = \"AZURE_OPENAI_GPT4_API_VERSION\"\nAZURE_OPENAI_GPT4_DEPLOYMENT_ENV_VAR = \"AZURE_OPENAI_GPT4_DEPLOYMENT\"\nAZURE_OPENAI_DALLE_API_KEY_ENV_VAR = \"AZURE_OPENAI_DALLE_API_KEY\"\nAZURE_OPENAI_DALLE_API_BASE_ENV_VAR = \"AZURE_OPENAI_DALLE_API_BASE\"\nAZURE_OPENAI_DALLE_API_VERSION_ENV_VAR = \"AZURE_OPENAI_DALLE_API_VERSION\"\nAZURE_OPENAI_DALLE_DEPLOYMENT_ENV_VAR = \"AZURE_OPENAI_DALLE_DEPLOYMENT\"\n"
  },
  {
    "path": "bondai/models/openai/openai_connection_params.py",
    "content": "from .openai_models import OpenAIConnectionType\n\n\nclass OpenAIConnectionParams:\n    def __init__(\n        self,\n        connection_type: OpenAIConnectionType,\n        api_key: str,\n        api_version: str | None = None,\n        azure_endpoint: str | None = None,\n        azure_deployment: str | None = None,\n    ):\n        if connection_type not in OpenAIConnectionType:\n            raise ValueError(f\"Invalid api_type: {connection_type}\")\n        if not api_key:\n            raise ValueError(\n                f\"api_key is required for '{connection_type.value}' connection type.\"\n            )\n        if connection_type == OpenAIConnectionType.AZURE:\n            if not api_version:\n                raise ValueError(\"api_version is required for 'azure' connection type.\")\n            if not azure_endpoint:\n                raise ValueError(\n                    \"azure_endpoint is required for 'azure' connection type.\"\n                )\n            if not azure_deployment:\n                raise ValueError(\n                    \"azure_deployment is required for 'azure' connection type.\"\n                )\n\n        self._connection_type = connection_type\n        self._api_key = api_key\n        self._api_version = api_version\n        self._azure_endpoint = azure_endpoint\n        self._azure_deployment = azure_deployment\n\n    @property\n    def connection_type(self):\n        return self._connection_type\n\n    @property\n    def api_key(self):\n        return self._api_key\n\n    @property\n    def api_version(self):\n        return self._api_version\n\n    @property\n    def azure_endpoint(self):\n        return self._azure_endpoint\n\n    @property\n    def azure_deployment(self):\n        return self._azure_deployment\n\n    def configure_openai_connection(self, api_key: str):\n        if not api_key:\n            raise ValueError(\"api_key is required for 'openai' connection type.\")\n        self._connection_type = OpenAIConnectionType.OPENAI\n        self._api_key = api_key\n        self._api_version = None\n        self._azure_endpoint = None\n        self._azure_deployment = None\n\n    def configure_azure_connection(\n        self, api_key: str, api_version: str, azure_endpoint: str, azure_deployment: str\n    ):\n        if not api_key:\n            raise ValueError(\"api_key is required for 'azure' connection type.\")\n        if not api_version:\n            raise ValueError(\"api_version is required for 'azure' connection type.\")\n        if not azure_endpoint:\n            raise ValueError(\"azure_endpoint is required for 'azure' connection type.\")\n        if not azure_deployment:\n            raise ValueError(\n                \"azure_deployment is required for 'azure' connection type.\"\n            )\n\n        self._connection_type = OpenAIConnectionType.AZURE\n        self._api_key = api_key\n        self._api_version = api_version\n        self._azure_endpoint = azure_endpoint\n        self._azure_deployment = azure_deployment\n\n    def to_dict(self):\n        return {\n            \"api_key\": self._api_key,\n            \"api_version\": self._api_version,\n            \"azure_endpoint\": self._azure_endpoint,\n            \"azure_deployment\": self._azure_deployment,\n        }\n"
  },
  {
    "path": "bondai/models/openai/openai_embedding_model.py",
    "content": "from typing import List, Dict\nfrom bondai.models import EmbeddingModel\nfrom .openai_models import ModelConfig, OpenAIModelType, OpenAIModelNames\nfrom .openai_wrapper import create_embedding, count_tokens, get_max_tokens\nfrom .openai_connection_params import OpenAIConnectionParams\nfrom . import default_openai_connection_params as DefaultOpenAIConnectionParams\n\n\nclass OpenAIEmbeddingModel(EmbeddingModel):\n    def __init__(\n        self,\n        model: OpenAIModelNames = OpenAIModelNames.TEXT_EMBEDDING_ADA_002,\n        connection_params: OpenAIConnectionParams | None = None,\n    ):\n        self._model = model.value if isinstance(model, OpenAIModelNames) else model\n        self._connection_params = (\n            connection_params\n            if connection_params\n            else DefaultOpenAIConnectionParams.embeddings_connection_params\n        )\n\n        if ModelConfig[self._model][\"model_type\"] != OpenAIModelType.EMBEDDING:\n            raise Exception(f\"Model {model} is not an embedding model.\")\n\n        if not self._connection_params:\n            raise Exception(\"Connection parameters not set for OpenAIEmbeddingModel.\")\n\n    @property\n    def embedding_size(self) -> int:\n        return ModelConfig[self._model][\"embedding_size\"]\n\n    @property\n    def max_tokens(self) -> int:\n        return get_max_tokens(self._model)\n\n    def create_embedding(self, prompt: str) -> List[float] | List[List[float]]:\n        return create_embedding(\n            prompt, connection_params=self._connection_params, model=self._model\n        )\n\n    def count_tokens(self, prompt: str) -> int:\n        return count_tokens(prompt, self._model)\n"
  },
  {
    "path": "bondai/models/openai/openai_llm.py",
    "content": "from typing import Dict, List, Callable\nfrom bondai.models import LLM\nfrom bondai.util.caching import LLMCache\nfrom .openai_wrapper import (\n    get_streaming_completion,\n    get_completion,\n    count_tokens,\n    get_max_tokens,\n)\nfrom .openai_connection_params import (\n    OpenAIConnectionParams,\n)\nfrom . import default_openai_connection_params as DefaultOpenAIConnectionParams\nfrom .openai_models import (\n    ModelConfig,\n    OpenAIModelNames,\n    OpenAIModelType,\n    OpenAIModelFamilyType,\n)\n\n\nclass OpenAILLM(LLM):\n    def __init__(\n        self,\n        model: OpenAIModelNames | str,\n        connection_params: OpenAIConnectionParams = None,\n        cache: LLMCache = None,\n    ):\n        self._cache = cache\n\n        self._model = model.value if isinstance(model, OpenAIModelNames) else model\n        if ModelConfig[self._model][\"model_type\"] != OpenAIModelType.LLM:\n            raise Exception(f\"Model {self._model} is not an LLM model.\")\n\n        if connection_params:\n            self._connection_params = connection_params\n        elif ModelConfig[self._model][\"family\"] == OpenAIModelFamilyType.GPT4:\n            self._connection_params = (\n                DefaultOpenAIConnectionParams.gpt_4_connection_params\n            )\n        else:\n            self._connection_params = (\n                DefaultOpenAIConnectionParams.gpt_35_connection_params\n            )\n\n        if not self._connection_params:\n            raise Exception(f\"Connection parameters not set for model {self._model}.\")\n\n    @property\n    def max_tokens(self) -> int:\n        return get_max_tokens(self._model)\n\n    @property\n    def supports_streaming(self) -> bool:\n        return True\n\n    def count_tokens(self, prompt: str) -> int:\n        return count_tokens(prompt, self._model)\n\n    def get_completion(\n        self,\n        messages: List[Dict] | None = None,\n        functions: List[Dict] | None = None,\n        **kwargs,\n    ) -> (str, Dict | None):\n        if messages is None:\n            messages = []\n        if functions is None:\n            functions = []\n\n        if self._cache:\n            input_parameters = {\"messages\": messages, \"functions\": functions, **kwargs}\n            cache_item = self._cache.get_cache_item(input_parameters=input_parameters)\n            if cache_item:\n                return cache_item\n\n        result = get_completion(\n            connection_params=self._connection_params,\n            messages=messages,\n            functions=functions,\n            model=self._model,\n            **kwargs,\n        )\n\n        if self._cache:\n            self._cache.save_cache_item(\n                input_parameters=input_parameters, response=result\n            )\n\n        return result\n\n    def get_streaming_completion(\n        self,\n        messages: List[Dict] | None = None,\n        functions: List[Dict] | None = None,\n        content_stream_callback: Callable[[str], None] = None,\n        function_stream_callback: Callable[[str], None] = None,\n        **kwargs,\n    ) -> (str, Dict | None):\n        if messages is None:\n            messages = []\n        if functions is None:\n            functions = []\n\n        if self._cache:\n            input_parameters = {\"messages\": messages, \"functions\": functions, **kwargs}\n            cache_item = self._cache.get_cache_item(input_parameters=input_parameters)\n            if cache_item:\n                return cache_item\n\n        result = get_streaming_completion(\n            connection_params=self._connection_params,\n            messages=messages,\n            functions=functions,\n            model=self._model,\n            content_stream_callback=content_stream_callback,\n            function_stream_callback=function_stream_callback,\n            **kwargs,\n        )\n\n        if self._cache:\n            self._cache.save_cache_item(\n                input_parameters=input_parameters, response=result\n            )\n\n        return result\n"
  },
  {
    "path": "bondai/models/openai/openai_models.py",
    "content": "from enum import Enum\n\n\nclass OpenAIConnectionType(Enum):\n    AZURE: str = \"azure\"\n    OPENAI: str = \"openai\"\n\n\nclass OpenAIModelType(Enum):\n    LLM = \"MODEL_TYPE_LLM\"\n    EMBEDDING = \"MODEL_TYPE_EMBEDDING\"\n\n\nclass OpenAIModelFamilyType(Enum):\n    GPT35 = \"MODEL_FAMILY_GPT_35\"\n    GPT4 = \"MODEL_FAMILY_GPT_4\"\n\n\nclass OpenAIModelNames(Enum):\n    GPT4 = \"gpt-4\"\n    GPT4_0613 = \"gpt-4-0613\"\n    GPT4_32K = \"gpt-4-32k\"\n    GPT4_TURBO_1106 = \"gpt-4-1106-preview\"\n    GPT35_TURBO = \"gpt-3.5-turbo\"\n    GPT35_TURBO_16K = \"gpt-3.5-turbo-16k\"\n    GPT35_TURBO_0613 = \"gpt-3.5-turbo-0613\"\n    GPT35_TURBO_16K_0613 = \"gpt-3.5-turbo-16k-0613\"\n    TEXT_EMBEDDING_ADA_002 = \"text-embedding-ada-002\"\n\n\nModelConfig = {\n    OpenAIModelNames.GPT4.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT4,\n        \"max_tokens\": 8191,\n        \"input_price_per_token\": 0.00003,\n        \"output_price_per_token\": 0.00006,\n    },\n    OpenAIModelNames.GPT4_0613.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT4,\n        \"max_tokens\": 8191,\n        \"input_price_per_token\": 0.00003,\n        \"output_price_per_token\": 0.00006,\n    },\n    OpenAIModelNames.GPT4_32K.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT4,\n        \"max_tokens\": 32767,\n        \"input_price_per_token\": 0.00006,\n        \"output_price_per_token\": 0.00012,\n    },\n    OpenAIModelNames.GPT4_TURBO_1106.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT4,\n        \"max_tokens\": 128000,\n        \"input_price_per_token\": 0.00001,\n        \"output_price_per_token\": 0.00003,\n    },\n    OpenAIModelNames.GPT35_TURBO.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT35,\n        \"max_tokens\": 4095,\n        \"input_price_per_token\": 0.0000015,\n        \"output_price_per_token\": 0.000002,\n    },\n    OpenAIModelNames.GPT35_TURBO_16K.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT35,\n        \"max_tokens\": 16383,\n        \"input_price_per_token\": 0.000003,\n        \"output_price_per_token\": 0.000004,\n    },\n    OpenAIModelNames.GPT35_TURBO_0613.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT35,\n        \"max_tokens\": 4095,\n        \"input_price_per_token\": 0.0000015,\n        \"output_price_per_token\": 0.000002,\n    },\n    OpenAIModelNames.GPT35_TURBO_16K_0613.value: {\n        \"model_type\": OpenAIModelType.LLM,\n        \"family\": OpenAIModelFamilyType.GPT35,\n        \"max_tokens\": 16383,\n        \"input_price_per_token\": 0.000003,\n        \"output_price_per_token\": 0.000004,\n    },\n    OpenAIModelNames.TEXT_EMBEDDING_ADA_002.value: {\n        \"model_type\": OpenAIModelType.EMBEDDING,\n        \"max_tokens\": 8190,\n        \"price_per_token\": 0.0000001,\n        \"embedding_size\": 1536,\n    },\n}\n"
  },
  {
    "path": "bondai/models/openai/openai_wrapper.py",
    "content": "import json\nimport tiktoken\nfrom typing import Dict, List, Callable\nfrom openai import OpenAI, AzureOpenAI\nfrom .openai_connection_params import OpenAIConnectionParams\nfrom .openai_models import ModelConfig, OpenAIModelType, OpenAIConnectionType\nfrom bondai.util import ModelLogger\n\nDEFAULT_TEMPERATURE = 0.1\n\nembedding_tokens = 0\nembedding_costs = 0.0\ngpt_tokens = 0\ngpt_costs = 0.0\n\nlogger = None\n\n\ndef enable_logging(model_logger: ModelLogger):\n    global logger\n    logger = model_logger\n\n\ndef disable_logging():\n    global logger\n    logger = None\n\n\ndef get_gpt_tokens() -> int:\n    return gpt_tokens\n\n\ndef get_embedding_tokens() -> int:\n    return embedding_tokens\n\n\ndef get_gpt_costs() -> float:\n    return gpt_costs\n\n\ndef get_embedding_costs() -> float:\n    return embedding_costs\n\n\ndef get_total_cost() -> float:\n    return embedding_costs + gpt_costs\n\n\ndef reset_total_cost():\n    global embedding_costs, embedding_tokens, gpt_costs, gpt_tokens\n    embedding_costs = 0.0\n    embedding_tokens = 0\n    gpt_costs = 0.0\n    gpt_tokens = 0\n\n\ndef calculate_cost(model_name: str, usage: Dict):\n    global embedding_costs, embedding_tokens, gpt_costs, gpt_tokens\n\n    if model_name in ModelConfig:\n        model = ModelConfig[model_name]\n        token_count = usage[\"total_tokens\"]\n\n        if model[\"model_type\"] == OpenAIModelType.LLM:\n            gpt_tokens += token_count\n            gpt_costs += (usage[\"prompt_tokens\"] * model[\"input_price_per_token\"]) + (\n                usage[\"completion_tokens\"] * model[\"output_price_per_token\"]\n            )\n        else:\n            embedding_tokens += token_count\n            embedding_costs += token_count * model[\"price_per_token\"]\n    # else:\n    #     print(f\"Unknown model: {model_name}\")\n\n\ndef get_max_tokens(model: str) -> int:\n    return ModelConfig[model][\"max_tokens\"]\n\n\ndef count_tokens(prompt: str, model: Dict) -> int:\n    encoding = tiktoken.encoding_for_model(model)\n    return len(encoding.encode(prompt))\n\n\ndef create_embedding(\n    text: str,\n    connection_params: OpenAIConnectionParams,\n    model: str = \"text-embedding-ada-002\",\n    **kwargs,\n) -> [float]:\n    params = {\n        \"input\": text if isinstance(text, list) else [text],\n    }\n\n    if connection_params.connection_type == OpenAIConnectionType.AZURE:\n        client = AzureOpenAI(\n            api_key=connection_params.api_key,\n            api_version=connection_params.api_version,\n            azure_endpoint=connection_params.azure_endpoint,\n        )\n        params[\"model\"] = connection_params.azure_deployment\n    else:\n        client = OpenAI(\n            api_key=connection_params.api_key,\n        )\n        params[\"model\"] = model\n\n    response = client.embeddings.create(**params, **kwargs)\n\n    calculate_cost(\n        model,\n        {\n            \"total_tokens\": response.usage.total_tokens,\n            \"prompt_tokens\": response.usage.prompt_tokens,\n            \"completion_tokens\": response.usage.total_tokens\n            - response.usage.prompt_tokens,\n        },\n    )\n\n    embeddings = [d.embedding for d in response.data]\n\n    if len(embeddings) > 0:\n        return embeddings\n    else:\n        return embeddings[0]\n\n\ndef get_completion(\n    connection_params: OpenAIConnectionParams,\n    messages: List[Dict] | None = None,\n    functions: List[Dict] | None = None,\n    model: str = \"gpt-4\",\n    **kwargs,\n) -> (str, Dict | None):\n    if messages is None:\n        messages = []\n    if functions is None:\n        functions = []\n\n    response = _get_completion(\n        connection_params=connection_params,\n        messages=messages,\n        functions=functions,\n        model=model,\n        **kwargs,\n    )\n\n    function = None\n    message = response.choices[0].message\n    if message.function_call:\n        function = {\"name\": message.function_call.name}\n        if message.function_call.arguments:\n            try:\n                function[\"arguments\"] = json.loads(message.function_call.arguments)\n            except json.decoder.JSONDecodeError:\n                pass\n\n    calculate_cost(\n        model,\n        {\n            \"total_tokens\": response.usage.total_tokens,\n            \"prompt_tokens\": response.usage.prompt_tokens,\n            \"completion_tokens\": response.usage.total_tokens\n            - response.usage.prompt_tokens,\n        },\n    )\n    _log_completion(\n        messages,\n        functions=functions,\n        response_content=message.content,\n        response_function=function,\n    )\n\n    return message.content, function\n\n\ndef get_streaming_completion(\n    connection_params: OpenAIConnectionParams,\n    messages: List[Dict] | None = None,\n    functions: List[Dict] | None = None,\n    model: str = \"gpt-4\",\n    content_stream_callback: Callable[[str], None] | None = None,\n    function_stream_callback: Callable[[str], None] | None = None,\n    **kwargs,\n) -> (str, Dict | None):\n    if messages is None:\n        messages = []\n    if functions is None:\n        functions = []\n\n    response = _get_completion(\n        connection_params=connection_params,\n        messages=messages,\n        functions=functions,\n        model=model,\n        stream=True,\n        **kwargs,\n    )\n\n    content = \"\"\n    function_name = \"\"\n    function_arguments = \"\"\n\n    for chunk in response:\n        if len(chunk.choices) == 0:\n            continue\n\n        delta = chunk.choices[0].delta\n        if delta.content:\n            content += delta.content\n            if content_stream_callback:\n                content_stream_callback(delta.content)\n\n        function_call = delta.function_call\n        if function_call:\n            if function_call.name:\n                function_name += function_call.name\n            if function_call.arguments:\n                function_arguments += function_call.arguments\n            if function_stream_callback:\n                function_stream_callback(function_name, function_arguments)\n\n    function = None\n    if function_name:\n        function = {\"name\": function_name}\n        if function_arguments:\n            try:\n                function[\"arguments\"] = json.loads(function_arguments)\n            except json.decoder.JSONDecodeError:\n                pass\n\n    if function:\n        completion_tokens = content + json.dumps(function)\n    else:\n        completion_tokens = content\n\n    completion_token_count = count_tokens(completion_tokens, model)\n    prompt_tokens = json.dumps(messages)\n    prompt_token_count = count_tokens(prompt_tokens, model)\n\n    calculate_cost(\n        model,\n        {\n            \"total_tokens\": prompt_token_count + completion_token_count,\n            \"prompt_tokens\": prompt_token_count,\n            \"completion_tokens\": completion_token_count,\n        },\n    )\n\n    _log_completion(\n        messages,\n        functions=functions,\n        response_content=content,\n        response_function=function,\n    )\n\n    return content, function\n\n\ndef _log_completion(\n    messages: List[Dict] | None = None,\n    functions: List[Dict] | None = None,\n    response_content: str | None = None,\n    response_function: Dict | None = None,\n):\n    if messages is None:\n        messages = []\n    if functions is None:\n        functions = []\n    if response_content is None:\n        response_content = \"\"\n\n    global logger\n    if not logger:\n        return\n    prompt_log = \"\"\n\n    if len(functions) > 0:\n        fs_str = json.dumps(functions)\n        prompt_log += f\"TOOLS:\\n{fs_str}\\n\\n\"\n\n    if len(messages) > 0:\n        m_str = json.dumps(messages)\n        prompt_log += f\"MESSAGES:\\n{m_str}\\n\\n\"\n\n    logger.log(prompt_log, response_content, function=response_function)\n\n\ndef _get_completion(\n    connection_params: OpenAIConnectionParams,\n    messages: List[Dict] | None = None,\n    functions: List[Dict] | None = None,\n    model: str = \"gpt-4\",\n    **kwargs,\n) -> (str, Dict | None):\n    if messages is None:\n        messages = []\n    if functions is None:\n        functions = []\n\n    params = {\n        \"messages\": messages,\n        \"temperature\": DEFAULT_TEMPERATURE,\n    }\n\n    if connection_params.connection_type == OpenAIConnectionType.AZURE:\n        client = AzureOpenAI(\n            api_key=connection_params.api_key,\n            api_version=connection_params.api_version,\n            azure_endpoint=connection_params.azure_endpoint,\n            azure_deployment=connection_params.azure_deployment,\n        )\n        params[\"model\"] = connection_params.azure_deployment\n    else:\n        client = OpenAI(\n            api_key=connection_params.api_key,\n        )\n        params[\"model\"] = model\n\n    if len(functions) > 0:\n        params[\"functions\"] = functions\n\n    return client.chat.completions.create(**params, **kwargs)\n"
  },
  {
    "path": "bondai/prompt/__init__.py",
    "content": "from .prompt_builder import PromptBuilder\nfrom .default_prompt_builder import DefaultPromptBuilder\nfrom .jinja_prompt_builder import JinjaPromptBuilder\n\n__all__ = [\"PromptBuilder\", \"DefaultPromptBuilder\", \"JinjaPromptBuilder\"]\n"
  },
  {
    "path": "bondai/prompt/default_prompt_builder.py",
    "content": "from bondai.prompt import PromptBuilder\n\n\nclass DefaultPromptBuilder(PromptBuilder):\n    def __init__(self, prompt_template: str):\n        self._prompt_template: str = prompt_template\n\n    def build_prompt(self, **kwargs) -> str:\n        return self._apply_prompt_template(self._prompt_template, **kwargs)\n"
  },
  {
    "path": "bondai/prompt/default_prompt_template.md",
    "content": "# Introduction #\n\nYou are a powerful problem solving agent! \nYou have access to a set of tools that give you capabilities far beyond typical language models.\nYou are being asked to use these tools and your powerful problem solving skills to help the user with the TASK specified below.\nDO NOT rely on the user to perform tasks for you unless absolutely necessary. You should attempt to complete this TASK without involving the user.\nYou are running within an Ubuntu environment. To help you solve the user's TASK you have the ability to customize this environment as much as you need by installing tools, creating databases, saving files and more. Just use your tools!\n\n\n# Today's Current Date and Time #\n\n{DATETIME}\n\n\n# TASK #\n\n{TASK}\n\n\n# Previous Work #\n\n{WORK}\n\n\n# Next Steps #\n\nLet's think step by step and come up with the next step that should be taken to solve this TASK. Be sure to look at the Previous Work that has already been completed and avoid repeating yourself when possible. Be sure to look at the \"Results\" for each step for information you can use. Select the best tool for the next step and remember, use the task_completed tool when you have all the information you need to provide the final answer. If the task you're completing requires multiple steps it is strong recommended that you consider using the agent_tool to delegate break up the task into smaller pieces as it is more likely to result in a successful result. Also, it is strongly recommended that you save your work along the way whenever possible."
  },
  {
    "path": "bondai/prompt/jinja_prompt_builder.py",
    "content": "import platform\nfrom datetime import datetime\nfrom jinja2 import Template\nfrom bondai.prompt import PromptBuilder\n\n\nclass JinjaPromptBuilder(PromptBuilder):\n    def __init__(self, prompt_template: str):\n        self._prompt_template: str = prompt_template\n\n    def _apply_prompt_template(self, template_string: str, **kwargs) -> str:\n        template = Template(template_string)\n        return template.render(**kwargs)\n\n    def build_prompt(self, **kwargs) -> str:\n        default_vars = {\n            \"platform\": platform.system(),\n            \"datetime\": str(datetime.now()),\n        }\n        return self._apply_prompt_template(\n            self._prompt_template, **default_vars, **kwargs\n        )\n"
  },
  {
    "path": "bondai/prompt/prompt_builder.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import Dict, Any\n\n\nclass PromptBuilder(ABC):\n    def __call__(self, **kwargs: Dict[str, Any]) -> str:\n        return self.build_prompt(**kwargs)\n\n    @abstractmethod\n    def build_prompt(self, **kwargs: Dict[str, Any]) -> str:\n        pass\n\n    def _apply_prompt_template(prompt_template: str, **kwargs) -> str:\n        return prompt_template.format_map(kwargs)\n"
  },
  {
    "path": "bondai/tools/__init__.py",
    "content": "from .tool import Tool, EmptyParameters, InputParameters\nfrom .agent_tool import AgentTool\nfrom .dalle_tool import DalleTool\nfrom .python_repl_tool import PythonREPLTool\nfrom .shell_tool import ShellTool\nfrom .langchain_tool import LangChainTool\nfrom .response_query import ResponseQueryTool\nfrom .task_completed_tool import TaskCompletedTool\n\n__all__ = [\n    \"Tool\",\n    \"AgentTool\",\n    \"DalleTool\",\n    \"PythonREPLTool\",\n    \"ShellTool\",\n    \"EmptyParameters\",\n    \"InputParameters\",\n    \"LangChainTool\",\n    \"ResponseQueryTool\",\n    \"TaskCompletedTool\",\n]\n"
  },
  {
    "path": "bondai/tools/agent_tool.py",
    "content": "from pydantic import BaseModel\nfrom bondai.tools import Tool\n\nTOOL_NAME = \"agent_tool\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to delegate tasks to other agents. \"\n    \"This can be really helpful for taking a complex task and breaking it down into smaller, more manageable pieces. \"\n    \"Just include a highly descriptive prompt in the 'task_description' parameter for this task. \"\n    \"The more detailed your description the better the agent will be at the task. \"\n    \"The 'task_description' parameter is required and MUST be provided.\"\n)\n\n\nclass Parameters(BaseModel):\n    task_description: str\n\n\nclass AgentTool(Tool):\n    def __init__(self, agent):\n        super(AgentTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        if agent is None:\n            raise Exception(\"Agent is required.\")\n        self._agent = agent\n\n    def run(self, task_description: str) -> str:\n        from bondai.agents import ToolUsageMessage\n\n        result = self._agent.run(task=task_description)\n        self._agent.clear_messages()\n\n        if isinstance(result, ToolUsageMessage):\n            if result.success:\n                return result.tool_output\n            else:\n                return f\"Tool failed with the following error: {result.error}\"\n\n        return result\n\n    def stop(self):\n        self._agent.stop()\n"
  },
  {
    "path": "bondai/tools/alpaca_markets/__init__.py",
    "content": "from .env_vars import ALPACA_MARKETS_API_KEY_ENV_VAR, ALPACA_MARKETS_SECRET_KEY_ENV_VAR\nfrom .create_order import CreateOrderTool\nfrom .get_account import GetAccountTool\nfrom .list_positions import ListPositionsTool\nfrom .response_formatter import (\n    format_orders_response,\n    format_account_response,\n    format_positions_response,\n)\n\n__all__ = [\n    \"ALPACA_MARKETS_API_KEY_ENV_VAR\",\n    \"ALPACA_MARKETS_SECRET_KEY_ENV_VAR\",\n    \"CreateOrderTool\",\n    \"GetAccountTool\",\n    \"ListPositionsTool\",\n    \"format_orders_response\",\n    \"format_account_response\",\n    \"format_positions_response\",\n]\n"
  },
  {
    "path": "bondai/tools/alpaca_markets/create_order.py",
    "content": "import os\nfrom pydantic import BaseModel\nfrom bondai.tools import Tool\nfrom .response_formatter import format_order_response\nfrom .env_vars import ALPACA_MARKETS_API_KEY_ENV_VAR, ALPACA_MARKETS_SECRET_KEY_ENV_VAR\nfrom alpaca.trading.client import TradingClient\nfrom alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest\nfrom alpaca.common.exceptions import APIError\n\nTOOL_NAME = \"create_investment_order\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to create an Order to buy or sell a stock. \"\n    \"When using this tool, you must specify either 'buy' or 'sell' for the 'side' parameter. \"\n    \"You must also specify the 'symbol' parameter which is the stock symbol of the stock you want to buy or sell. \"\n    \"You must also specify the 'quantity' parameter which is the number of shares you want to buy or sell. \"\n    \"The 'order_type' parameter is optional. It will default to a 'market' order by you can also specify a 'limit' order. \"\n    \"The 'limit_price' parameter is only required if you specify a 'limit' order. \"\n    \"The 'time_in_force' parameter is optional. It will default to 'day' but you can also specify 'gtc', 'opg', 'cls', 'ioc', or 'fok'.\"\n)\n\n\nclass Parameters(BaseModel):\n    side: str\n    symbol: str\n    quantity: str\n    order_type: str\n    time_in_force: str\n    limit_price: str\n    thought: str\n\n\nclass CreateOrderTool(Tool):\n    def __init__(\n        self,\n        alpaca_api_key=os.environ.get(ALPACA_MARKETS_API_KEY_ENV_VAR),\n        alpaca_secret_key=os.environ.get(ALPACA_MARKETS_SECRET_KEY_ENV_VAR),\n    ):\n        super(CreateOrderTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        self.trading_client = TradingClient(\n            alpaca_api_key, alpaca_secret_key, paper=True\n        )\n\n    def run(self, arguments):\n        side = arguments.get(\"side\")\n        symbol = arguments.get(\"symbol\")\n        quantity = arguments.get(\"quantity\")\n        order_type = arguments.get(\"order_type\", \"market\")\n        time_in_force = arguments.get(\"time_in_force\", \"day\")\n        limit_price = arguments.get(\"limit_price\", None)\n\n        if not side in [\"buy\", \"sell\"]:\n            return 'Invalid side. Must be either \"buy\" or \"sell\".'\n        if not symbol:\n            return \"Invalid symbol.\"\n        if not quantity:\n            return \"Invalid quantity.\"\n        if not order_type in [\"market\", \"limit\"]:\n            return 'Invalid order type. Must be either \"market\" or \"limit\".'\n        if not time_in_force in [\"day\", \"gtc\", \"opg\", \"cls\", \"ioc\", \"fok\"]:\n            return 'Invalid time in force. Must be either \"day\", \"gtc\", \"opg\", \"cls\", \"ioc\", or \"fok\".'\n        if order_type == \"limit\" and not limit_price:\n            return \"Invalid limit price.\"\n\n        if order_type == \"market\":\n            order = MarketOrderRequest(\n                symbol=symbol, qty=quantity, side=side, time_in_force=time_in_force\n            )\n        else:\n            order = LimitOrderRequest(\n                symbol=symbol,\n                limit_price=limit_price,\n                qty=quantity,\n                side=side,\n                time_in_force=time_in_force,\n            )\n\n        try:\n            response = self.trading_client.submit_order(order_data=order)\n            return format_order_response(response)\n        except APIError as e:\n            return e.message\n"
  },
  {
    "path": "bondai/tools/alpaca_markets/env_vars.py",
    "content": "ALPACA_MARKETS_API_KEY_ENV_VAR = \"ALPACA_MARKETS_API_KEY\"\nALPACA_MARKETS_SECRET_KEY_ENV_VAR = \"ALPACA_MARKETS_SECRET_KEY\"\n"
  },
  {
    "path": "bondai/tools/alpaca_markets/get_account.py",
    "content": "import os\nfrom bondai.tools import Tool\nfrom .response_formatter import format_account_response\nfrom .env_vars import ALPACA_MARKETS_API_KEY_ENV_VAR, ALPACA_MARKETS_SECRET_KEY_ENV_VAR\nfrom alpaca.trading.client import TradingClient\n\nTOOL_NAME = \"get_investment_account_info\"\nTOOL_DESCRIPTION = \"This tool will provide information about your investment account including your cash balance.\"\n\n\nclass GetAccountTool(Tool):\n    def __init__(\n        self,\n        alpaca_api_key=os.environ.get(ALPACA_MARKETS_API_KEY_ENV_VAR),\n        alpaca_secret_key=os.environ.get(ALPACA_MARKETS_SECRET_KEY_ENV_VAR),\n    ):\n        super(GetAccountTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION)\n        self.trading_client = TradingClient(\n            alpaca_api_key, alpaca_secret_key, paper=True\n        )\n\n    def run(self, arguments):\n        response = self.trading_client.get_account()\n        return format_account_response(response)\n"
  },
  {
    "path": "bondai/tools/alpaca_markets/list_positions.py",
    "content": "import os\nfrom bondai.tools.tool import Tool\nfrom .response_formatter import format_positions_response\nfrom .env_vars import ALPACA_MARKETS_API_KEY_ENV_VAR, ALPACA_MARKETS_SECRET_KEY_ENV_VAR\nfrom alpaca.trading.client import TradingClient\n\nTOOL_NAME = \"list_investment_positions\"\nTOOL_DESCRIPTION = (\n    \"This tool will list all of your currently open investment positions.\"\n)\n\n\nclass ListPositionsTool(Tool):\n    def __init__(\n        self,\n        alpaca_api_key=os.environ.get(ALPACA_MARKETS_API_KEY_ENV_VAR),\n        alpaca_secret_key=os.environ.get(ALPACA_MARKETS_SECRET_KEY_ENV_VAR),\n    ):\n        super(ListPositionsTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION)\n        self.trading_client = TradingClient(\n            alpaca_api_key, alpaca_secret_key, paper=True\n        )\n\n    def run(self, arguments):\n        response = self.trading_client.get_all_positions()\n\n        if len(response) > 0:\n            return format_positions_response(response)\n        return \"There are no open positions.\"\n"
  },
  {
    "path": "bondai/tools/alpaca_markets/response_formatter.py",
    "content": "def format_order_response(response):\n    return f\"\"\"Order ID: {response.id}\nStatus: {response.status}\nFilled At: {response.filled_at}\nSymbol: {response.symbol}\nAsset Class: {response.asset_class}\nNotional: {response.notional}\nQuantity: {response.qty}\nFilled Quantity: {response.filled_qty}\nFilled Average Price: {response.filled_avg_price}\nFilled Price: {response.filled_avg_price}\nOrder Class: {response.order_class}\nOrder Type: {response.order_type}\nSide: {response.side}\nTime In Force: {response.time_in_force}\nLimit Price: {response.limit_price}\nStop Price: {response.stop_price}\"\"\"\n\n\ndef format_account_response(response):\n    return f\"\"\"Cash: {response.cash}\nCurrency: {response.currency}\nBuying Power: {response.buying_power}\nRegt Buying Power: {response.regt_buying_power}\nDaytrading Buying Power: {response.daytrading_buying_power}  \nNon Marginable Buying Power: {response.non_marginable_buying_power}\nAccrued Fees: {response.accrued_fees}\nPortfolio Value: {response.portfolio_value}\nShorting Enabled: {response.shorting_enabled}\nCrypto Status: {response.crypto_status}\nLong Market Value: {response.long_market_value}\nShort Market Value: {response.short_market_value}\nInitial Margin: {response.initial_margin}\nMaintenance Margin: {response.maintenance_margin}\nLast Maintenance Margin: {response.last_maintenance_margin}\"\"\"\n\n\ndef format_positions_response(response):\n    result = \"\"\n    for position in response:\n        result += format_position(position) + \"\\n\\n\"\n    return result\n\n\ndef format_position(position):\n    return f\"\"\"Symbol: {position.symbol}\nQuantity: {position.qty}\nEntry Share Price: {position.avg_entry_price}\nCurrent Share Price: {position.current_price}\nUnrealized Profit/Loss: {position.unrealized_pl}\nMarket Value: {position.market_value}\nCost Basis: {position.cost_basis}\"\"\"\n\n\ndef format_orders_response(response):\n    result = \"\"\n    for order in response:\n        result += format_order(order) + \"\\n\\n\"\n    return result\n\n\ndef format_order(order):\n    return f\"\"\"Order ID: {order.id}\nType: {order.type}\nSymbol: {order.symbol}\nStatus: {order.status}\nSide: {order.side}\nLimit Price: {order.limit_price}\nQuantity: {order.qty}\"\"\"\n"
  },
  {
    "path": "bondai/tools/bland_ai/__init__.py",
    "content": "from .bland_ai_tools import (\n    BlandAITool,\n    BLAND_AI_API_KEY_ENV_VAR,\n    BLAND_AI_VOICE_ID_ENV_VAR,\n    BLAND_AI_CALL_TIMEOUT_ENV_VAR,\n)\n\n__all__ = [\n    \"BlandAITool\",\n    \"BLAND_AI_API_KEY_ENV_VAR\",\n    \"BLAND_AI_VOICE_ID_ENV_VAR\",\n    \"BLAND_AI_CALL_TIMEOUT_ENV_VAR\",\n]\n"
  },
  {
    "path": "bondai/tools/bland_ai/bland_ai_tools.py",
    "content": "import re\nimport os\nimport requests\nimport time\nfrom pydantic import BaseModel\nfrom bondai.tools import Tool\n\nTOOL_NAME = \"phone_call_tool\"\nTOOL_DESCRIPTION = (\n    \"This tool interfaces allows you to make phone calls. The response you get back from this tool will be the call transcript.\\n\"\n    \"\\nParameters:\\n\"\n    \"- phone_number (Required): The phone number of the person or company to call. For international numbers, \"\n    \"include the country code without additional formatting (e.g., '+447700900077'). U.S. numbers may include \"\n    \"formatting, but removing extra characters is recommended.\\n\"\n    \"- task (Required): This should be a highly detailed description of the task that should be performed on this call. Also, provide context about the interaction and give detailed instructions.\\n\"\n    \"- request_data (Optional, default={}): A dictionary that provides information for the AI during the call. Useful for giving the AI specific facts like the caller’s name, etc.\\n\"\n)\n\nBLAND_AI_API_KEY_ENV_VAR = \"BLAND_AI_API_KEY\"\nBLAND_AI_VOICE_ID_ENV_VAR = \"BLAND_AI_VOICE_ID\"\nBLAND_AI_CALL_TIMEOUT_ENV_VAR = \"BLAND_AI_CALL_TIMEOUT\"\nAPI_ENDPOINT = \"https://api.bland.ai/\"\n\n# Interval for checking call status (in seconds)\nCHECK_INTERVAL = 2\n\n\nclass CallParameters(BaseModel):\n    phone_number: str\n    task: str\n    request_data: dict = {}\n    thought: str\n\n\ndef validate_phone_number(phone):\n    # International numbers (starting with '+')\n    international_pattern = (\n        r\"^\\+\\d{1,15}$\"  # Starts with +, followed by 1 to 15 digits.\n    )\n\n    # U.S. numbers (may include formatting characters)\n    us_pattern = r\"^(?:\\+1)?[ -]?(\\d{3})[ -]?(\\d{3})[ -]?(\\d{4})$\"  # This considers formats like +1 123-456-7890, 123 456 7890, 123-456-7890.\n\n    if re.match(international_pattern, phone):\n        return True\n    elif re.match(us_pattern, phone):\n        return True\n    else:\n        error_msg = (\n            \"Invalid phone number format. \"\n            \"For international numbers, include the country code and exclude additional formatting. E.g. '+447700900077'. \"\n            \"For U.S. numbers, you may include formatting, but it's recommended to strip all additional characters.\"\n        )\n        raise ValueError(error_msg)\n\n\nclass BlandAITool(Tool):\n    def __init__(\n        self,\n        bland_ai_api_key=os.environ.get(BLAND_AI_API_KEY_ENV_VAR),\n        bland_ai_voice_id=int(os.environ.get(BLAND_AI_VOICE_ID_ENV_VAR, \"0\")),\n        bland_ai_call_timeout=int(os.environ.get(BLAND_AI_CALL_TIMEOUT_ENV_VAR, \"300\")),\n    ):\n        super(BlandAITool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, CallParameters)\n        self.bland_ai_api_key = bland_ai_api_key\n        self.bland_ai_voice_id = bland_ai_voice_id\n        self.bland_ai_call_timeout = bland_ai_call_timeout\n\n    def run(self, arguments):\n        if arguments.get(\"phone_number\") is None:\n            raise Exception(\"phone_number is required.\")\n        if arguments.get(\"task\") is None:\n            raise Exception(\"task is required.\")\n\n        validate_phone_number(arguments[\"phone_number\"])\n\n        # Start the call\n        arguments[\"reduce_latency\"] = False\n        call_id = self.start_call(arguments)\n        if not call_id:\n            raise Exception(\"Failed to start the call.\")\n\n        # Monitor the call until completion\n        start_time = time.time()\n        while True:\n            # Check for timeout\n            elapsed_time = time.time() - start_time\n            if elapsed_time > self.bland_ai_call_timeout:\n                self.end_call(call_id)\n                raise TimeoutError(\n                    f\"Call exceeded the maximum allowed time of {self.bland_ai_call_timeout} seconds.\"\n                )\n\n            completed, transcripts = self.check_call_status(call_id)\n            if completed:\n                return f\"Call to {arguments['phone_number']} has completed.\\n\\nTranscripts:\\n{transcripts}\"\n            time.sleep(CHECK_INTERVAL)\n\n    def start_call(self, arguments):\n        headers = {\"authorization\": self.bland_ai_api_key}\n        response = requests.post(API_ENDPOINT + \"call\", json=arguments, headers=headers)\n        if response.status_code == 200:\n            resp_data = response.json()\n            if resp_data[\"status\"] == \"success\":\n                return resp_data[\"call_id\"]\n        return None\n\n    def check_call_status(self, call_id):\n        headers = {\"authorization\": self.bland_ai_api_key}\n        data = {\"call_id\": call_id}\n        response = requests.post(API_ENDPOINT + \"logs\", json=data, headers=headers)\n\n        if response.status_code == 200:\n            resp_data = response.json()\n            if resp_data[\"completed\"]:\n                transcripts = \"\\n\".join(\n                    [\n                        f\"{item['user']}: {item['text']}\"\n                        for item in resp_data[\"transcripts\"]\n                    ]\n                )\n                return True, transcripts\n\n        return False, None\n\n    def end_call(self, call_id):\n        headers = {\"authorization\": self.bland_ai_api_key}\n        data = {\"call_id\": call_id}\n        requests.post(API_ENDPOINT + \"end\", json=data, headers=headers)\n"
  },
  {
    "path": "bondai/tools/conversational/__init__.py",
    "content": "from .conversational_tools import (\n    SEND_MESSAGE_TOOL_NAME,\n    SendMessageTool,\n    EXIT_CONVERSATION_TOOL_NAME,\n    ExitConversationTool,\n)\n\n__all__ = [\n    \"SEND_MESSAGE_TOOL_NAME\",\n    \"SendMessageTool\",\n    \"EXIT_CONVERSATION_TOOL_NAME\",\n    \"ExitConversationTool\",\n]\n"
  },
  {
    "path": "bondai/tools/conversational/conversational_tools.py",
    "content": "from pydantic import BaseModel\nfrom bondai.tools import Tool\nfrom typing import Dict, Tuple\nfrom bondai.agents.messages import ConversationMessage\n\n\nSEND_MESSAGE_TOOL_NAME = \"send_message\"\nSEND_MESSAGE_TOOL_DESCRIPTION = (\n    \"Use the send_message tool to send messages to other members of the conversation including agents and the user. \"\n    \"The send_message tool takes three arguments: recipient_name, message, and require_response. \"\n    \"The recipient_name argument is the name of the recipient of the message. \"\n    \"The message argument is the message to be sent to the recipient. \"\n    \"The require_response argument is a boolean value that indicates whether a response from the recipient is necessary. \"\n    \"The require_response argument defaults to True. If you do not require a response from the message recipient, set require_response to False.\"\n)\n\n\nclass SendMessageToolParameters(BaseModel):\n    recipient_name: str\n    message: str\n    require_response: bool = True\n\n\nclass SendMessageTool(Tool):\n    def __init__(self):\n        super().__init__(\n            SEND_MESSAGE_TOOL_NAME,\n            SEND_MESSAGE_TOOL_DESCRIPTION,\n            SendMessageToolParameters,\n        )\n\n    def run(\n        self, recipient_name: str, message: str, require_response: bool = True\n    ) -> Dict[str, bool]:\n        return (\n            ConversationMessage(\n                role=\"assistant\",\n                recipient_name=recipient_name,\n                message=message,\n                require_response=require_response,\n            ),\n            True,\n        )\n\n\nEXIT_CONVERSATION_TOOL_NAME = \"exit_conversation\"\n\n\nclass ExitConversationTool(Tool):\n    def __init__(self):\n        super().__init__(\n            EXIT_CONVERSATION_TOOL_NAME,\n            \"Use the exit_conversation tool to exit the conversation once your task has been completed.\",\n        )\n\n    def run(self, arguments: Dict) -> Tuple[str, bool]:\n        return None, True\n"
  },
  {
    "path": "bondai/tools/dalle_tool.py",
    "content": "import requests\nimport openai\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom bondai.models.openai import OpenAIConnectionParams, DefaultOpenAIConnectionParams\n\nIMAGE_SIZE = \"1024x1024\"\nTOOL_NAME = \"text_to_image_tool\"\nTOOL_DESCRIPTION = (\n    \"This tool takes in a description and generates an image based on that description. \"\n    \"It then saves the image to the specified filename.\\n\"\n    \"\\nParameters:\\n\"\n    \"- description (required): The text description based on which the image will be generated.\\n\"\n    \"- filename (required): The name of the file where the generated image will be saved.\"\n)\n\n\nclass Parameters(BaseModel):\n    description: str\n    filename: str\n    thought: str\n\n\nclass DalleTool(Tool):\n    def __init__(self, connection_params: OpenAIConnectionParams | None = None):\n        super().__init__(TOOL_NAME, TOOL_DESCRIPTION, parameters=Parameters)\n        self._connection_params = (\n            connection_params\n            if connection_params\n            else DefaultOpenAIConnectionParams.dalle_connection_params\n        )\n        if not self._connection_params:\n            raise Exception(\"Connection parameters not set for DalleTool.\")\n\n    def run(self, arguments: Dict) -> str:\n        description = arguments.get(\"description\")\n        filename = arguments.get(\"filename\")\n\n        if description is None:\n            raise Exception(\"description is required.\")\n        if filename is None:\n            raise Exception(\"filename is required.\")\n\n        params = {\"prompt\": description, \"n\": 1, \"size\": IMAGE_SIZE}\n\n        # Use the OpenAI API to generate an image based on the description\n        response = openai.Image.create(**params, **self._connection_params.to_dict())\n\n        # Get the image URL from the response\n        image_url = response[\"data\"][0][\"url\"]\n\n        # Download the image from the URL and save it to the specified filename\n        image_content = requests.get(image_url).content\n        with open(filename, \"wb\") as file:\n            file.write(image_content)\n\n        return f\"Image generated and saved to {filename} successfully.\"\n"
  },
  {
    "path": "bondai/tools/database/__init__.py",
    "content": "from .db_query import DatabaseQueryTool, PG_URI_ENV_VAR\n\n__all__ = [\"DatabaseQueryTool\", \"PG_URI_ENV_VAR\"]\n"
  },
  {
    "path": "bondai/tools/database/db_query.py",
    "content": "import os\nimport psycopg2\nfrom pydantic import BaseModel\nfrom bondai.tools import Tool\nfrom bondai.models import LLM\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames\n\n\nclass Parameters(BaseModel):\n    question: str\n    thought: str\n\n\nTOOL_NAME = \"database_query_tool\"\nQUERY_SYSTEM_PROMPT = \"You are a helpful question and answer assistant designed to answer questions about a database. Use the provided information to answer the user's QUESTION at the very end.\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to ask a question and retrieve data from the user's database.\"\n    \"DO NOT ask the user for any additional information about the database.\"\n    \"All necessary information to connect to the database has already been captured.\"\n    \"Just specify your question using the 'question' parameter.\"\n    \"Your question will automatically be turned into SQL and the response will contain the resulting data.\"\n)\n\nPG_URI_ENV_VAR = \"PG_URI\"\n\nPG_HOST = os.environ.get(\"PG_HOST\")\nPG_PORT = int(os.environ.get(\"PG_HOST\", \"5432\"))\nPG_USERNAME = os.environ.get(\"PG_USERNAME\")\nPG_PASSWORD = os.environ.get(\"PG_PASSWORD\")\nPG_DBNAME = os.environ.get(\"PG_DBNAME\")\n\nMAX_QUERY_RETRIES = 3\n\nQUERY_PROMPT_TEMPLATE = \"\"\"\nUsing the database schema below respond with a SQL query to answer the user's QUESTION.\nVERY IMPORTANT: You must respond ONLY with a SQL query. Do not respond with any other text.\n\n# DATABASE SCHEMA #\n\n###DBSCHEMA###\n\n\n# QUESTION #\n\n###QUESTION###\n\nVERY IMPORTANT: You must respond ONLY with a SQL query. Do not respond with any other text.\n\"\"\"\n\nRESPONSE_PROMPT_TEMPLATE = \"\"\"\nYou were asked the following question:\n###QUESTION###\n\nThe following information was returned from the database:\n###QUERY_RESULTS###\n\nPlease respond with a friendly text response to the user's question.\n\"\"\"\n\n\nclass DatabaseQueryTool(Tool):\n    def __init__(\n        self,\n        pg_uri: str = os.environ.get(PG_URI_ENV_VAR),\n        pg_host: str = PG_HOST,\n        pg_port: int = PG_PORT,\n        pg_username: str = PG_USERNAME,\n        pg_password: str = PG_PASSWORD,\n        pg_dbname: str = PG_DBNAME,\n        llm: LLM | None = None,\n    ):\n        super(DatabaseQueryTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        self.pg_uri = pg_uri\n        self.pg_host = pg_host\n        self.pg_port = pg_port\n        self.pg_username = pg_username\n        self.pg_password = pg_password\n        self.pg_dbname = pg_dbname\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT35_TURBO_16K)\n        self.llm = llm\n\n    def run(self, arguments):\n        question = arguments[\"question\"]\n\n        if question is None:\n            raise Exception(\"question is required\")\n\n        schema_str = self.__get_database_schema()\n        query_prompt = QUERY_PROMPT_TEMPLATE.replace(\"###DBSCHEMA###\", schema_str)\n        query_prompt = query_prompt.replace(\"###QUESTION###\", question)\n\n        attempts = 0\n        while True:\n            try:\n                query_response, _ = self.llm.get_completion(query_prompt)\n                rows, colnames = self.__query_database(query_response)\n                return self.__format_response(rows, colnames)\n            except Exception as e:\n                attempts += 1\n                if attempts > MAX_QUERY_RETRIES:\n                    raise e\n\n    def __format_response(self, rows, colnames):\n        markdown_output = \"| \" + \" | \".join(colnames) + \" |\\n\"\n        markdown_output += \"| \" + \" | \".join([\"---\"] * len(colnames)) + \" |\\n\"\n\n        for row in rows:\n            markdown_output += \"| \" + \" | \".join(map(str, row)) + \" |\\n\"\n\n        return markdown_output\n\n    def __get_database_connection(self):\n        if self.pg_uri:\n            # Establish the connection\n            return psycopg2.connect(self.pg_uri, sslmode=\"require\")\n        else:\n            # Establish the connection\n            return psycopg2.connect(\n                host=self.pg_host,\n                port=self.pg_port,\n                user=self.pg_username,\n                password=self.pg_password,\n                dbname=self.pg_dbname,\n            )\n\n    def __query_database(self, query):\n        connection = None\n        cursor = None\n        try:\n            connection = self.__get_database_connection()\n\n            # Create a new cursor\n            cursor = connection.cursor()\n\n            # Execute the query\n            cursor.execute(query)\n\n            colnames = [desc[0] for desc in cursor.description]\n\n            # Fetch all rows from the query result\n            return cursor.fetchall(), colnames\n        finally:\n            # Close the cursor and the connection\n            if cursor:\n                cursor.close()\n            if connection:\n                connection.close()\n\n    def __get_database_schema(self):\n        # Query the schema from information_schema\n        rows, _ = self.__query_database(\n            (\n                \"SELECT table_name, column_name, data_type, is_nullable, column_default\\n\"\n                \"FROM information_schema.columns\\n\"\n                \"WHERE table_schema = 'public'\\n\"\n                \"ORDER BY table_name, ordinal_position;\"\n            )\n        )\n\n        # Process the rows to create a formatted string\n        schema_str = \"\"\n        current_table = None\n        for row in rows:\n            table, column, data_type, is_nullable, default = row\n            if table != current_table:\n                if current_table:\n                    schema_str += \"\\n\"\n                schema_str += f\"Table: {table}\\n\"\n                schema_str += \"-\" * (len(table) + 8) + \"\\n\"\n                current_table = table\n            schema_str += f\"{column}: {data_type}\"\n            if is_nullable == \"NO\":\n                schema_str += \" NOT NULL\"\n            if default:\n                schema_str += f\" DEFAULT {default}\"\n            schema_str += \"\\n\"\n\n        return schema_str\n"
  },
  {
    "path": "bondai/tools/file/__init__.py",
    "content": "from .file_query import FileQueryTool\nfrom .file_write import FileWriteTool\nfrom .file_read import FileReadTool\n\n__all__ = [\n    \"FileQueryTool\",\n    \"FileWriteTool\",\n    \"FileReadTool\",\n]\n"
  },
  {
    "path": "bondai/tools/file/file_query.py",
    "content": "from pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom bondai.models import LLM, EmbeddingModel\nfrom bondai.util import semantic_search, is_html, get_html_text, extract_file_text\nfrom bondai.models.openai import OpenAILLM, OpenAIEmbeddingModel, OpenAIModelNames\n\nTOOL_NAME = \"file_query\"\nQUERY_SYSTEM_PROMPT = \"You are a helpful question and answer assistant designed to answer questions about a file. Use the provided information to answer the user's QUESTION at the very end.\"\nTOOL_DESCRIPTION = \"This tool allows you to ask a question about the text content of any file including summarization. This tool works for text files, html files and PDFs. Just specify the filename of the file using the 'filename' parameter and specify your question using the 'question' parameter.\"\n\n\ndef is_pdf(filename: str) -> bool:\n    with open(filename, \"rb\") as file:\n        header = file.read(4)\n    return header == b\"%PDF\"\n\n\ndef build_prompt(question: str, context: str) -> str:\n    return f\"\"\"{context}\n\n\nIMPORTANT: Answer the following question for the user.\nQUESTION: {question}\n\"\"\"\n\n\nclass Parameters(BaseModel):\n    filename: str\n    question: str\n    thought: str\n\n\nclass FileQueryTool(Tool):\n    def __init__(\n        self, llm: LLM | None = None, embedding_model: EmbeddingModel | None = None\n    ):\n        super(FileQueryTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT35_TURBO_16K)\n        if embedding_model is None:\n            embedding_model = OpenAIEmbeddingModel(\n                OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n            )\n\n        self._llm = llm\n        self._embedding_model = embedding_model\n\n    def run(self, arguments: Dict) -> str:\n        filename = arguments.get(\"filename\")\n        question = arguments.get(\"question\")\n\n        if filename is None:\n            raise Exception(\"filename is required\")\n        if question is None:\n            raise Exception(\"question is required\")\n\n        if is_pdf(filename):\n            text = extract_file_text(filename)\n        else:\n            with open(filename, \"r\") as f:\n                text = f.read()\n\n        if is_html(text):\n            text = get_html_text(text)\n\n        system_prompt_tokens = self._llm.count_tokens(QUERY_SYSTEM_PROMPT)\n        prompt_template_tokens = self._llm.count_tokens(build_prompt(\"\", question))\n        max_tokens = (\n            self._llm.max_tokens - system_prompt_tokens - prompt_template_tokens - 50\n        )\n        text = semantic_search(self._embedding_model, question, text, max_tokens)\n        prompt = build_prompt(question, text)\n        response = self._llm.get_completion(prompt, QUERY_SYSTEM_PROMPT)[0]\n\n        return response\n"
  },
  {
    "path": "bondai/tools/file/file_read.py",
    "content": "from pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom bondai.util import extract_file_text\n\nTOOL_NAME = \"file_read\"\nTOOL_DESCRIPTION = \"This tool will return the contents of a file for you to view. Just specify the filename of the file using the 'filename' parameter.\"\n\n\ndef is_pdf(filename: str) -> bool:\n    with open(filename, \"rb\") as file:\n        header = file.read(4)\n    return header == b\"%PDF\"\n\n\nclass Parameters(BaseModel):\n    filename: str\n    thought: str\n\n\nclass FileReadTool(Tool):\n    def __init__(self):\n        super(FileReadTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n\n    def run(self, arguments: Dict) -> str:\n        filename = arguments.get(\"filename\")\n        if filename is None:\n            raise Exception(\"filename is required\")\n\n        if is_pdf(filename):\n            return extract_file_text(filename)\n        else:\n            with open(filename, \"r\") as f:\n                return f.read()\n"
  },
  {
    "path": "bondai/tools/file/file_write.py",
    "content": "from pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\n\nTOOL_NAME = \"file_write\"\nTOOL_DESCRIPTION = (\n    \"This tool will save the data you provide in the 'text' parameter of this tool to a file.\"\n    \"You MUST specify the filename of the file you want to save using the 'filename' parameter.\"\n    \"You can optionally specify the 'append' parameter to append the 'text' to the file instead of overwriting it.\"\n)\n\n\nclass Parameters(BaseModel):\n    filename: str\n    text: str\n    append: bool = False\n    thought: str\n\n\nclass FileWriteTool(Tool):\n    def __init__(self):\n        super(FileWriteTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n\n    def run(self, arguments: Dict) -> str:\n        filename = arguments.get(\"filename\")\n        text = arguments.get(\"text\")\n\n        if filename is None:\n            raise Exception(\"filename is required\")\n        if text is None:\n            raise Exception(\"text is required\")\n\n        mode = \"a\" if arguments.get(\"append\") else \"w\"\n        with open(filename, mode) as f:\n            f.write(text)\n            return f\"File {filename} written successfully\"\n"
  },
  {
    "path": "bondai/tools/gmail/__init__.py",
    "content": "from .list_emails import ListEmailsTool\nfrom .query_emails import QueryEmailsTool\n\n__all__ = [\n    \"ListEmailsTool\",\n    \"QueryEmailsTool\",\n]\n"
  },
  {
    "path": "bondai/tools/gmail/list_emails.py",
    "content": "import pickle\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom googleapiclient.discovery import build\n\n\nMAX_RESULT_COUNT = 20\nDEFAULT_RESULT_COUNT = 5\nTOOL_NAME = \"list_emails\"\nTOOL_DESCRIPTION = f\"This tool allows you to retrieve a paginated list of emails from the user's inbox that includes the 'Id', 'From' and 'Subject' fields for each email. You can specify the number of items to return by setting the 'count' parameter. The maximum count is {MAX_RESULT_COUNT} and the default is {DEFAULT_RESULT_COUNT}. To paginate through the full list of all emails just increment the 'page' parameter. By default 'page' is set to 1. You can also optionally specificy a valid gmail query in the 'query' parameter.\"\n\n\nclass Parameters(BaseModel):\n    page: int\n    count: int\n    thought: str\n\n\ndef get_email_attr(message: Dict, attr: str) -> str:\n    return next(\n        (h[\"value\"] for h in message[\"payload\"][\"headers\"] if h[\"name\"] == attr), None\n    )\n\n\nclass ListEmailsTool(Tool):\n    def __init__(\n        self,\n        credentials: str | None = None,\n        credentials_filename: str | None = \"gmail-token.pickle\",\n    ):\n        super(ListEmailsTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        if credentials:\n            self.service = build(\"gmail\", \"v1\", credentials=credentials)\n        elif credentials_filename:\n            with open(credentials_filename, \"rb\") as token:\n                credentials = pickle.load(token)\n\n            self.service = build(\"gmail\", \"v1\", credentials=credentials)\n        else:\n            raise Exception(\"No credentials provided.\")\n\n    def run(self, arguments: Dict) -> str:\n        page = int(arguments.get(\"page\", \"1\"))\n        count = int(arguments.get(\"count\", DEFAULT_RESULT_COUNT))\n        query = arguments.get(\"query\", \"\")\n\n        if count > MAX_RESULT_COUNT:\n            count = MAX_RESULT_COUNT\n\n        result_size_estimate = None\n        page_token = None\n        results = {}\n        for i in range(page):\n            if page == 1:\n                results = (\n                    self.service.users()\n                    .messages()\n                    .list(\n                        userId=\"me\",\n                        labelIds=[\"INBOX\"],\n                        maxResults=MAX_RESULT_COUNT,\n                        q=query,\n                    )\n                    .execute()\n                )\n            elif page_token:\n                results = (\n                    self.service.users()\n                    .messages()\n                    .list(\n                        userId=\"me\",\n                        labelIds=[\"INBOX\"],\n                        maxResults=MAX_RESULT_COUNT,\n                        q=query,\n                        pageToken=page_token,\n                    )\n                    .execute()\n                )\n            else:\n                break\n\n            page_token = results.get(\"nextPageToken\", None)\n            if not result_size_estimate:\n                result_size_estimate = results[\"resultSizeEstimate\"]\n\n        messages = results.get(\"messages\", [])\n\n        if len(results) > 0:\n            output = f\"The total number of messages: {result_size_estimate}\\n\\n\"\n            for m in messages:\n                msg_id = m[\"id\"]\n                message = (\n                    self.service.users()\n                    .messages()\n                    .get(userId=\"me\", id=msg_id)\n                    .execute()\n                )\n                msg_from = get_email_attr(message, \"From\")\n                msg_received = get_email_attr(message, \"Date\")\n                msg_subj = get_email_attr(message, \"Subject\")\n                output += f\"Id: {msg_id}\\nDate: {msg_received}\\nFrom: {msg_from}\\nSubject: {msg_subj}\\n\\n\"\n\n            return output\n        else:\n            return \"0 messages were found.\"\n"
  },
  {
    "path": "bondai/tools/gmail/query_emails.py",
    "content": "import pickle\nimport base64\nfrom googleapiclient.discovery import build\nfrom typing import List, Dict\nfrom pydantic import BaseModel\nfrom bondai.tools import Tool\nfrom bondai.util import get_html_text\nfrom bondai.models import LLM\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames\n\nTOOL_NAME = \"get_email_content\"\nQUERY_SYSTEM_PROMPT = \"You are a helpful question and answer assistant designed to answer questions about emails. Use the provided information to answer the user's QUESTION at the very end.\"\nTOOL_DESCRIPTION = \"This tool allows to ask a question about the text content of a list of emails including summarization. Simply provide a comma seperated list of email ids in the 'email_ids' parameter and specify your question using the 'question' parameter.\"\n\n\ndef get_email_attr(message: Dict, attr: str) -> str:\n    return next(\n        (h[\"value\"] for h in message[\"payload\"][\"headers\"] if h[\"name\"] == attr), None\n    )\n\n\ndef build_prompt(question: str, context: str) -> str:\n    return f\"\"\"{context}\n\n\nIMPORTANT: Answer the following question for the user.\nQUESTION: {question}\n\"\"\"\n\n\ndef parse_body(message: Dict) -> str:\n    payload = message[\"payload\"]\n\n    if \"parts\" in payload:\n        htmlPart = next(\n            (p for p in payload[\"parts\"] if p[\"mimeType\"] == \"text/html\"), None\n        )\n        textPart = next(\n            (p for p in payload[\"parts\"] if p[\"mimeType\"] == \"text/plain\"), None\n        )\n    elif \"mimeType\" in payload:\n        if payload[\"mimeType\"] == \"text/html\":\n            htmlPart = payload\n        elif payload[\"mimeType\"] == \"text/plain\":\n            textPart = payload\n\n    if htmlPart:\n        data = htmlPart[\"body\"][\"data\"].replace(\"-\", \"+\").replace(\"_\", \"/\")\n        decoded_data = base64.b64decode(data)\n        return get_html_text(decoded_data)\n    elif textPart:\n        data = textPart[\"body\"][\"data\"].replace(\"-\", \"+\").replace(\"_\", \"/\")\n        return base64.b64decode(data)\n\n\nclass Parameters(BaseModel):\n    email_ids: List[str]\n    question: str\n    thought: str\n\n\nclass QueryEmailsTool(Tool):\n    def __init__(\n        self,\n        credentials: str | None = None,\n        credentials_filename: str | None = \"gmail-token.pickle\",\n        llm: LLM | None = None,\n    ):\n        super(QueryEmailsTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT35_TURBO_16K)\n\n        self.llm = llm\n        if credentials:\n            self.service = build(\"gmail\", \"v1\", credentials=credentials)\n        elif credentials_filename:\n            with open(credentials_filename, \"rb\") as token:\n                credentials = pickle.load(token)\n\n            self.service = build(\"gmail\", \"v1\", credentials=credentials)\n        else:\n            raise Exception(\"No credentials provided.\")\n\n    def run(self, arguments: Dict) -> str:\n        question = arguments.get(\"question\")\n        email_ids = arguments.get(\"email_ids\")\n\n        if question is None:\n            raise Exception(\"question is required\")\n        if email_ids is None:\n            raise Exception(\"email_ids is required\")\n\n        if isinstance(email_ids, str):\n            email_ids = map(lambda id: id.strip(), email_ids.split(\",\"))\n\n        text = \"\"\n        for msg_id in email_ids:\n            message = (\n                self.service.users().messages().get(userId=\"me\", id=msg_id).execute()\n            )\n            msg_from = get_email_attr(message, \"From\")\n            msg_subj = get_email_attr(message, \"Subject\")\n            msg_body = parse_body(message)\n            text += f\"Id: {msg_id}\\nFrom: {msg_from}\\nSubject: {msg_subj}\\nBody: {msg_body}\\n\\n\"\n\n        prompt = build_prompt(question, text)\n        response = self.llm.get_completion(prompt, QUERY_SYSTEM_PROMPT)[0]\n\n        return response\n"
  },
  {
    "path": "bondai/tools/langchain_tool.py",
    "content": "from pydantic import BaseModel\nfrom bondai.tools import Tool, InputParameters\nfrom typing import Dict\n\n\nclass LangChainTool(Tool):\n    def __init__(\n        self,\n        tool: Tool,\n        parameters: BaseModel = InputParameters,\n        dangerous: bool = False,\n    ):\n        super(LangChainTool, self).__init__(\n            tool.name, tool.description, parameters, dangerous=dangerous\n        )\n        if tool is None:\n            raise Exception(\"Tool is required.\")\n        self._tool = tool\n\n    def run(self, arguments: Dict) -> str:\n        return self._tool.run(arguments)\n"
  },
  {
    "path": "bondai/tools/python_repl_tool.py",
    "content": "import io\nfrom contextlib import redirect_stdout, redirect_stderr\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom multiprocessing import Process, Pipe\nfrom multiprocessing.connection import Connection\nfrom bondai.tools import Tool\n\nDEFAULT_EXECUTION_TIMEOUT = 60\nTOOL_NAME = \"python_repl\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to execute Python code. \"\n    \"Specify your Python code in the 'script' parameter and it will return the result.\"\n    \"Note that you MUST provide the 'script' parameter to use this tool.\"\n)\n\n\nclass Parameters(BaseModel):\n    script: str\n    thought: str\n\n\ndef execute_target(conn: Connection, code: str):\n    local_vars = {}\n    stdout_str, stderr_str = \"\", \"\"\n\n    try:\n        with io.StringIO() as stdout_io, io.StringIO() as stderr_io, redirect_stdout(\n            stdout_io\n        ), redirect_stderr(stderr_io):\n            exec(code, {}, local_vars)\n            stdout_str = stdout_io.getvalue()\n            stderr_str = stderr_io.getvalue()\n\n        # Remove non-picklable objects from local_vars if any\n        for key in list(local_vars.keys()):\n            if not isinstance(local_vars[key], (int, float, str, list, dict, tuple)):\n                del local_vars[key]\n\n        conn.send([local_vars, stdout_str, stderr_str])\n    except Exception as e:\n        conn.send([str(e), stdout_str, stderr_str])\n\n\nclass PythonREPLTool(Tool):\n    def __init__(self, execution_timeout: int = DEFAULT_EXECUTION_TIMEOUT):\n        super(PythonREPLTool, self).__init__(\n            TOOL_NAME, TOOL_DESCRIPTION, parameters=Parameters, dangerous=True\n        )\n        self._execution_timeout = execution_timeout\n\n    def run(self, arguments: Dict) -> str:\n        script = arguments.get(\"script\")\n\n        if script is None:\n            raise Exception(\n                \"To use the 'python_repl' tool you must provide the 'script' parameter.\"\n            )\n\n        result, stdout, stderr = self.execute_code(script)\n\n        response = \"\"\n\n        # Include stdout if present\n        if stdout:\n            response += f\"Output:\\n{stdout}\\n\"\n\n        # Include stderr if present\n        if stderr:\n            response += f\"Errors:\\n{stderr}\\n\"\n\n        # Include result if present\n        if result:\n            formatted_result = \"\\n\".join(\n                [f\"{key}: {value}\" for key, value in result.items()]\n            )\n            response += f\"Result Variables:\\n{formatted_result}\\n\"\n\n        if not response:\n            response = \"Code executed successfully. No output or result variables.\"\n\n        return response\n\n    def execute_code(self, code: str) -> (Dict, str, str):\n        # Create a pipe for communication\n        parent_conn, child_conn = Pipe()\n\n        process = Process(target=execute_target, args=(child_conn, code))\n        process.start()\n        process.join(timeout=self._execution_timeout)\n\n        if process.is_alive():\n            process.terminate()\n            process.join(10)\n            raise Exception(\"Code execution timed out\")\n\n        result, stdout, stderr = parent_conn.recv()\n        if isinstance(result, str):\n            raise Exception(result)\n\n        return result, stdout, stderr\n"
  },
  {
    "path": "bondai/tools/response_query.py",
    "content": "import uuid\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom bondai.util import semantic_search\nfrom bondai.models import LLM, EmbeddingModel\nfrom bondai.models.openai import (\n    OpenAILLM,\n    OpenAIEmbeddingModel,\n    OpenAIModelNames,\n)\n\nTOOL_NAME = \"response_query\"\nQUERY_SYSTEM_PROMPT = \"You are a helpful question and answer assistant designed to answer questions about a file. Use the provided information to answer the user's QUESTION at the very end.\"\nTOOL_DESCRIPTION = \"This tool allows you to ask a questions about responses that are too large and contain too many tokens. Just specify the response_id using the 'response_id' parameter and specify your question using the 'question' parameter.\"\n\n\ndef build_prompt(question, context):\n    return f\"\"\"{context}\n\n\nIMPORTANT: Answer the following question for the user.\nQUESTION: {question}\n\"\"\"\n\n\nclass Parameters(BaseModel):\n    response_id: str\n    question: str\n    thought: str\n\n\nclass ResponseQueryTool(Tool):\n    def __init__(\n        self, llm: LLM | None = None, embedding_model: EmbeddingModel | None = None\n    ):\n        super(ResponseQueryTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT35_TURBO_16K)\n        if embedding_model is None:\n            embedding_model = OpenAIEmbeddingModel(\n                OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n            )\n\n        self._llm = llm\n        self._embedding_model = embedding_model\n        self._responses = {}\n\n    @property\n    def responses(self):\n        return self._responses\n\n    def add_response(self, response: str) -> str:\n        response_id = str(uuid.uuid4())\n        self._responses[response_id] = response\n        return response_id\n\n    def run(self, arguments: Dict) -> str:\n        response_id = arguments[\"response_id\"]\n        question = arguments[\"question\"]\n\n        if response_id is None:\n            raise Exception(\"response_id is required\")\n        if question is None:\n            raise Exception(\"question is required\")\n\n        if response_id in self._responses:\n            text = self._responses[response_id]\n            text = semantic_search(self._embedding_model, question, text, 16000)\n            prompt = build_prompt(question, text)\n            response = self._llm.get_completion(prompt, QUERY_SYSTEM_PROMPT)[0]\n            return response\n        else:\n            return f\"{response_id} is not a valid response_id\"\n\n    def clear_responses(self):\n        self._responses = {}\n"
  },
  {
    "path": "bondai/tools/search/__init__.py",
    "content": "from .google_search import GoogleSearchTool\nfrom .duck_duck_go_search import DuckDuckGoSearchTool\n\n__all__ = [\n    \"GoogleSearchTool\",\n    \"DuckDuckGoSearchTool\",\n]\n"
  },
  {
    "path": "bondai/tools/search/duck_duck_go_search.py",
    "content": "from pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools.tool import Tool\nfrom duckduckgo_search import DDGS\n\nMAX_RESULT_COUNT = 20\nDEFAULT_RESULT_COUNT = 5\nTOOL_NAME = \"duck_duck_go_search\"\nTOOL_DESCRIPTION = f\"This tool allows you to retrieve a paginated list of search results. You must specify your search string in the 'query' parameter. You can specify the number of search results to return by setting the 'count' parameter. The maximum count is {MAX_RESULT_COUNT} and the default is {DEFAULT_RESULT_COUNT}. To paginate through the full list of all search results just increment the 'page' parameter. By default 'page' is set to 1.\"\n\n\nclass Parameters(BaseModel):\n    query: str\n    count: int\n    page: int\n    thought: str\n\n\ndef search_duckduckgo(query: str, count: int = 10, page: int = 1) -> str:\n    with DDGS() as ddgs:\n        search_count = count * page\n        response = list(ddgs.text(query, max_results=search_count))\n        response = response[search_count - count :]\n        return \"\\n\".join(\n            [f\"[{r['title']}]({r['href']}): {r['body']}\" for r in response]\n        )\n\n\nclass DuckDuckGoSearchTool(Tool):\n    def __init__(self):\n        super(DuckDuckGoSearchTool, self).__init__(\n            TOOL_NAME, TOOL_DESCRIPTION, Parameters\n        )\n\n    def run(self, arguments: Dict) -> str:\n        query = arguments.get(\"query\")\n        count = int(arguments.get(\"count\", \"5\"))\n        page = int(arguments.get(\"page\", \"1\"))\n\n        if query is None:\n            raise Exception(\"query is required\")\n\n        if count > MAX_RESULT_COUNT:\n            count = MAX_RESULT_COUNT\n\n        output = search_duckduckgo(query, count, page)\n        return output\n"
  },
  {
    "path": "bondai/tools/search/google_search.py",
    "content": "import os\nfrom googleapiclient.discovery import build\nfrom pydantic import BaseModel\nfrom bondai.tools.tool import Tool\n\nMAX_RESULT_COUNT = 10\nDEFAULT_RESULT_COUNT = 5\nTOOL_NAME = \"google_search\"\nTOOL_DESCRIPTION = (\n    f\"This tool allows you to retrieve a paginated list of google search results. \"\n    \"You must specify your search string in the 'query' parameter. \"\n    \"You can specify the number of search results to return by setting the 'count' parameter. \"\n    \"The maximum count is {MAX_RESULT_COUNT} and the default is {DEFAULT_RESULT_COUNT}. \"\n    \"To paginate through the full list of all search results just increment the 'page' parameter. \"\n    \"By default 'page' is set to 1.\"\n)\n\nGOOGLE_API_KEY_ENV_VAR = \"GOOGLE_API_KEY\"\nGOOGLE_CSE_ID_ENV_VAR = \"GOOGLE_CSE_ID\"\n\n\nclass Parameters(BaseModel):\n    query: str\n    count: int\n    page: int\n    thought: str\n\n\nclass GoogleSearchTool(Tool):\n    def __init__(\n        self,\n        google_api_key=os.environ.get(GOOGLE_API_KEY_ENV_VAR),\n        google_cse_id=os.environ.get(GOOGLE_CSE_ID_ENV_VAR),\n    ):\n        super(GoogleSearchTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        self.google_cse_id = google_cse_id\n        self.siterestrict = False\n        self.search_engine = build(\"customsearch\", \"v1\", developerKey=google_api_key)\n\n    def run(self, arguments):\n        query = arguments.get(\"query\")\n        count = int(arguments.get(\"count\", \"5\"))\n        page = int(arguments.get(\"page\", \"1\"))\n\n        if query is None:\n            raise Exception(\"query is required\")\n\n        if count > MAX_RESULT_COUNT:\n            count = MAX_RESULT_COUNT\n\n        cse = self.search_engine.cse()\n        if self.siterestrict:\n            cse = cse.siterestrict()\n\n        start = (page - 1) * count\n        res = cse.list(q=query, cx=self.google_cse_id, start=start, num=count).execute()\n        items = res.get(\"items\", [])\n\n        output = \"\"\n        for i in items:\n            output += f\"[{i['title']}]({i['link']})\\n\"\n\n        return output\n"
  },
  {
    "path": "bondai/tools/shell_tool.py",
    "content": "import threading\nimport subprocess\nimport shlex\nfrom queue import Queue\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\n\nDEFAULT_EXECUTION_TIMEOUT = 60\nTOOL_NAME = \"shell_tool\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to execute shell commands. \"\n    \"Specify your command in the 'command' parameter and it will return the result. \"\n    \"Note that this tool only accepts a single string argument ('command') at a time and does not accept a list of commands.\"\n)\n\n\nclass Parameters(BaseModel):\n    command: str\n    thought: str\n\n\nclass ShellTool(Tool):\n    def __init__(self, execution_timeout: int = DEFAULT_EXECUTION_TIMEOUT):\n        super(ShellTool, self).__init__(\n            TOOL_NAME, TOOL_DESCRIPTION, parameters=Parameters, dangerous=True\n        )\n        self._execution_timeout = execution_timeout\n\n    def run(self, arguments: Dict) -> str:\n        cmd = arguments.get(\"command\")\n        if cmd is None:\n            raise Exception(\"'command' parameter is required\")\n\n        stdout, stderr = self.execute_command(cmd)\n\n        response = \"\"\n\n        # Include stdout if present\n        if stdout:\n            response += f\"Output:\\n{stdout}\\n\"\n\n        # Include stderr if present\n        if stderr:\n            response += f\"Errors:\\n{stderr}\\n\"\n\n        if not response:\n            response = \"Command executed successfully. No output.\"\n\n        return response\n\n    def execute_command(self, cmd: str) -> (str, str):\n        # Use threading to enforce timeout\n        thread_exception = None\n\n        def target(queue):\n            nonlocal thread_exception\n            try:\n                process = subprocess.Popen(\n                    shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE\n                )\n                stdout, stderr = process.communicate()\n                queue.put((stdout, stderr))\n            except Exception as e:\n                thread_exception = e\n\n        q = Queue()\n        thread = threading.Thread(target=target, args=(q,))\n        thread.start()\n        thread.join(timeout=self._execution_timeout)\n\n        if thread_exception:\n            raise thread_exception\n\n        if thread.is_alive():\n            thread.join(timeout=10)\n            raise Exception(\"Command execution timed out\")\n\n        stdout, stderr = q.get()  # Get the result from the queue\n\n        return stdout.decode(\"utf-8\"), stderr.decode(\"utf-8\")\n"
  },
  {
    "path": "bondai/tools/task_completed_tool.py",
    "content": "from pydantic import BaseModel\nfrom bondai.tools import Tool\nfrom typing import Dict\n\n\nclass TaskCompletedToolParameters(BaseModel):\n    user_response: str\n\n\nclass TaskCompletedTool(Tool):\n    def __init__(self):\n        super().__init__(\n            \"task_completed\",\n            \"Use the task_completed tool when you have completed the requested task.\",\n            TaskCompletedToolParameters,\n        )\n\n    def run(self, arguments: Dict) -> Dict[str, bool]:\n        return arguments[\"user_response\"], True\n"
  },
  {
    "path": "bondai/tools/tool.py",
    "content": "from pydantic import BaseModel\nfrom typing import Dict\n\n\nclass InputParameters(BaseModel):\n    input: str\n    thought: str\n\n\nclass EmptyParameters(BaseModel):\n    pass\n\n\nclass Tool:\n    def __init__(\n        self,\n        name: str,\n        description: str,\n        parameters: BaseModel = EmptyParameters,\n        dangerous: bool = False,\n        supports_streaming: bool = False,\n    ):\n        if name is None:\n            raise Exception(\"name is required\")\n        if description is None:\n            raise Exception(\"description is required\")\n        if parameters is None:\n            parameters = EmptyParameters\n\n        self.name = name\n        self.description = description\n        self.parameters = parameters\n        self.dangerous = dangerous\n        self.supports_streaming = supports_streaming\n\n    def get_tool_function(self) -> Dict:\n        return {\n            \"name\": self.name,\n            \"description\": self.description,\n            \"parameters\": self.parameters.schema(),\n        }\n\n    def run(self, arguments: Dict) -> str | Dict:\n        if \"input\" in arguments:\n            return arguments[\"input\"]\n\n    def handle_stream_update(self, arguments_buffer: str):\n        # This function is called when the agent is streaming data to the tool.\n        # The arguments_buffer is a string buffer containing the latest argument data that has been received.\n        pass\n\n    def save_state() -> Dict:\n        # This function is called when the agent is saving state.\n        # The state should be returned as a dictionary.\n        return {}\n\n    def load_state(state: Dict):\n        # This function is called when the agent is loading state.\n        # The state is passed in as a dictionary.\n        pass\n\n    def stop(self):\n        # This function is called when the agent is being forcibly stopped.\n        pass\n"
  },
  {
    "path": "bondai/tools/vision/__init__.py",
    "content": "from .image_analysis_tool import ImageAnalysisTool\n\n__all__ = [\"ImageAnalysisTool\"]\n"
  },
  {
    "path": "bondai/tools/vision/image_analysis_tool.py",
    "content": "import base64\nfrom pydantic import BaseModel\nfrom typing import Dict, Optional\nfrom bondai.tools import Tool\nfrom bondai.models.openai.openai_wrapper import get_completion\nfrom bondai.models.openai import OpenAIConnectionParams, DefaultOpenAIConnectionParams\n\nMODEL_NAME = \"gpt-4-vision-preview\"\nTOOL_NAME = \"image_analysis\"\nTOOL_DESCRIPTION = \"This tool analyzes the contents of an image. Provide either an image URL or a base64 encoded image as well as a description of the analysis you would like to perform on the image.\"\n\n\nclass Parameters(BaseModel):\n    analysis_description: str\n    image_url: Optional[str] = None\n    image_file_path: Optional[str] = None\n\n\nclass ImageAnalysisTool(Tool):\n    def __init__(\n        self, connection_params: OpenAIConnectionParams = None, max_tokens: int = 300\n    ):\n        super(ImageAnalysisTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        self._connection_params = (\n            connection_params\n            if connection_params\n            else DefaultOpenAIConnectionParams.gpt_4_connection_params\n        )\n        self._max_tokens = max_tokens\n        if not self._connection_params:\n            raise Exception(\"Connection parameters not set for ImageAnalysisTool.\")\n\n    def _encode_image(self, image_path):\n        with open(image_path, \"rb\") as image_file:\n            return base64.b64encode(image_file.read()).decode(\"utf-8\")\n\n    def _analyze_image(self, image_data, analysis_description):\n        messages = [\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": analysis_description},\n                    {\"type\": \"image_url\", \"image_url\": {\"url\": image_data}},\n                ],\n            }\n        ]\n\n        response, _ = get_completion(\n            messages=messages,\n            model=MODEL_NAME,\n            connection_params=self._connection_params,\n            max_tokens=self._max_tokens,\n        )\n\n        return response\n\n    def run(\n        self,\n        analysis_description: str,\n        image_url: str = None,\n        image_file_path: str = None,\n    ) -> str:\n        if image_url is not None:\n            return self._analyze_image(image_url, analysis_description)\n        elif image_file_path is not None:\n            base64_image = self._encode_image(image_file_path)\n            return self._analyze_image(\n                f\"data:image/jpeg;base64,{base64_image}\", analysis_description\n            )\n        else:\n            raise Exception(\n                \"Either image_url or image_file_path is required. Neither were provided.\"\n            )\n"
  },
  {
    "path": "bondai/tools/website/__init__.py",
    "content": "from .extract_hyperlinks import WebsiteExtractHyperlinksTool\nfrom .html_query import WebsiteHtmlQueryTool\nfrom .query import WebsiteQueryTool\nfrom .download_file import DownloadFileTool\n\n\n__all__ = [\n    \"WebsiteExtractHyperlinksTool\",\n    \"WebsiteHtmlQueryTool\",\n    \"WebsiteQueryTool\",\n    \"DownloadFileTool\",\n]\n"
  },
  {
    "path": "bondai/tools/website/download_file.py",
    "content": "import requests\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\n\nTOOL_NAME = \"download_file\"\nTOOL_DESCRIPTION = \"This tool allows to you to download a file. Just provide the url to the file in the 'url' parameter and the filename it should be saved to in the 'filename' parameter.\"\nREQUEST_HEADERS = {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3\"\n}\n\n\nclass Parameters(BaseModel):\n    url: str\n    filename: str\n    thought: str\n\n\nclass DownloadFileTool(Tool):\n    def __init__(self):\n        super(DownloadFileTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n\n    def run(self, arguments: Dict) -> str:\n        url = arguments.get(\"url\")\n        filename = arguments.get(\"filename\")\n\n        if url is None:\n            raise Exception(\"url is required\")\n        if filename is None:\n            raise Exception(\"filename is required\")\n\n        try:\n            response = requests.get(url)\n\n            with open(filename, \"wb\") as output_file:\n                output_file.write(response.content)\n\n            return f\"The file was successfully downloaded to {filename}.\"\n        except requests.Timeout:\n            return \"The request timed out.\"\n"
  },
  {
    "path": "bondai/tools/website/extract_hyperlinks.py",
    "content": "import requests\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom bondai.util import get_website_links\n\nTOOL_NAME = \"website_extract_hyperlinks\"\nTOOL_DESCRIPTION = \"This tool allows will extract a list of hyperlinks from a website. Just specify the url of the website using the 'url' parameter.\"\nREQUEST_HEADERS = {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3\"\n}\n\n\nclass Parameters(BaseModel):\n    url: str\n    thought: str\n\n\nclass WebsiteExtractHyperlinksTool(Tool):\n    def __init__(self):\n        super(WebsiteExtractHyperlinksTool, self).__init__(\n            TOOL_NAME, TOOL_DESCRIPTION, Parameters\n        )\n\n    def run(self, arguments: Dict) -> str:\n        url = arguments[\"url\"]\n        if url is None:\n            raise Exception(\"url is required\")\n\n        try:\n            links = get_website_links(url)\n            return \"\\n\".join([f\"[{a.text}]({a.get('href', '')})\" for a in links])\n        except requests.Timeout:\n            return \"The request timed out.\"\n"
  },
  {
    "path": "bondai/tools/website/html_query.py",
    "content": "import requests\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools.tool import Tool\nfrom bondai.util.web import get_website_html\nfrom bondai.models import LLM\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames\n\nTOOL_NAME = \"website_html_query\"\nQUERY_SYSTEM_PROMPT = \"You are a helpful question and answer assistant designed to answer questions about the HTML in a website. Use the provided HTML content to answer the user's QUESTION at the very end.\"\nTOOL_DESCRIPTION = \"This tool allows to ask a question about the raw HTML content of a website. Just specify the url of the website using the 'url' parameter and specify your question using the 'question' parameter.\"\n\n\ndef build_prompt(question, context):\n    return f\"\"\"{context}\n\n\nIMPORTANT: Using the information provided above, answer the following question for the user.\nQUESTION: {question}\n\"\"\"\n\n\nclass Parameters(BaseModel):\n    url: str\n    question: str\n    thought: str\n\n\nclass WebsiteHtmlQueryTool(Tool):\n    def __init__(self, llm: LLM | None = None):\n        super(WebsiteHtmlQueryTool, self).__init__(\n            TOOL_NAME, TOOL_DESCRIPTION, Parameters\n        )\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT35_TURBO_16K)\n\n        self._llm = llm\n\n    def run(self, arguments: Dict) -> str:\n        url = arguments[\"url\"]\n        question = arguments[\"question\"]\n\n        if url is None:\n            raise Exception(\"url is required\")\n        if question is None:\n            raise Exception(\"question is required\")\n\n        try:\n            html = get_website_html(url)\n        except requests.Timeout:\n            return \"The request timed out.\"\n\n        prompt = build_prompt(question, html)\n        response = self._llm.get_completion(\n            prompt, QUERY_SYSTEM_PROMPT, model=self.model\n        )[0]\n\n        return response\n"
  },
  {
    "path": "bondai/tools/website/query.py",
    "content": "import requests\nfrom pydantic import BaseModel\nfrom typing import Dict\nfrom bondai.tools import Tool\nfrom bondai.util import get_website_text, semantic_search\nfrom bondai.models import LLM, EmbeddingModel\nfrom bondai.models.openai import OpenAILLM, OpenAIEmbeddingModel, OpenAIModelNames\n\nTOOL_NAME = \"website_query\"\nTOOL_DESCRIPTION = \"This tool allows to ask a question about the text content of any website including summarization. Just specify the url of the website using the 'url' parameter and specify your question using the 'question' parameter.\"\nREQUEST_HEADERS = {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3\"\n}\n\n\ndef build_prompt(question: str, context: str) -> str:\n    return f\"\"\"You are a helpful question and answer assistant designed to answer questions about a website. Use the provided information to answer the user's QUESTION at the very end.\n\nCONTEXT:    \n{context}\n\n\nIMPORTANT: Using the information provided above, answer the following question for the user.\nQUESTION: {question}\n\"\"\"\n\n\nclass Parameters(BaseModel):\n    url: str\n    question: str\n    thought: str\n\n\nclass WebsiteQueryTool(Tool):\n    def __init__(\n        self, llm: LLM | None = None, embedding_model: EmbeddingModel | None = None\n    ):\n        super(WebsiteQueryTool, self).__init__(TOOL_NAME, TOOL_DESCRIPTION, Parameters)\n        if llm is None:\n            llm = OpenAILLM(OpenAIModelNames.GPT35_TURBO_16K)\n        if embedding_model is None:\n            embedding_model = OpenAIEmbeddingModel(\n                OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n            )\n\n        self._llm = llm\n        self._embedding_model = embedding_model\n\n    def run(self, arguments: Dict) -> str:\n        url = arguments.get(\"url\")\n        question = arguments.get(\"question\")\n\n        if not url:\n            raise Exception(\"url is required\")\n        if not question:\n            raise Exception(\"question is required\")\n\n        try:\n            text = get_website_text(url)\n        except requests.Timeout:\n            return \"The request timed out.\"\n\n        text = semantic_search(self._embedding_model, question, text, 16000)\n        prompt = build_prompt(question, text)\n\n        messages = [{\"role\": \"system\", \"content\": prompt}]\n\n        response, _ = self._llm.get_completion(messages=messages)\n        return response\n"
  },
  {
    "path": "bondai/util/__init__.py",
    "content": "from .model_logger import ModelLogger\nfrom .misc import load_local_resource, format_print_string\nfrom .semantic_search import semantic_search, split_text\nfrom .event_mixin import EventMixin\nfrom .runnable import Runnable\nfrom .document_parser import extract_file_text\nfrom .web import (\n    get_website_html,\n    get_html_text,\n    get_website_text,\n    query_website_html,\n    get_website_links,\n    is_html,\n)\n\n__all__ = [\n    \"ModelLogger\",\n    \"EventMixin\",\n    \"Runnable\",\n    \"semantic_search\",\n    \"split_text\",\n    \"get_website_html\",\n    \"get_html_text\",\n    \"get_website_text\",\n    \"query_website_html\",\n    \"get_website_links\",\n    \"is_html\",\n    \"load_local_resource\",\n    \"format_print_string\",\n    \"extract_file_text\",\n]\n"
  },
  {
    "path": "bondai/util/caching/__init__.py",
    "content": "from .llm_cache import LLMCache, PersistentLLMCache, InMemoryLLMCache\n\n__all__ = [\"LLMCache\", \"PersistentLLMCache\", \"InMemoryLLMCache\"]\n"
  },
  {
    "path": "bondai/util/caching/llm_cache.py",
    "content": "import os\nimport json\nimport hashlib\nfrom abc import ABC, abstractmethod\nfrom typing import Tuple, Dict, Optional\n\n\nclass LLMCache(ABC):\n    def _get_cache_key(self, input_parameters: Dict) -> str:\n        key_str = json.dumps(input_parameters, sort_keys=True)\n        return hashlib.sha256(key_str.encode()).hexdigest()\n\n    @abstractmethod\n    def get_cache_item(self, input_parameters: Dict) -> Optional[Tuple[str, Dict]]:\n        pass\n\n    @abstractmethod\n    def save_cache_item(self, input_parameters: Dict, response: (str, Dict)) -> None:\n        pass\n\n\nclass PersistentLLMCache(LLMCache):\n    def __init__(self, cache_dir: str = \"./.cache\"):\n        self.cache_dir = cache_dir\n        if not os.path.exists(cache_dir):\n            os.makedirs(cache_dir)\n\n    def get_cache_item(self, input_parameters: Dict) -> Optional[Tuple[str, Dict]]:\n        cache_key = self._get_cache_key(input_parameters=input_parameters)\n        cache_path = os.path.join(self.cache_dir, cache_key)\n\n        if os.path.exists(cache_path):\n            with open(cache_path, \"r\") as file:\n                return json.load(file)\n\n    def save_cache_item(self, input_parameters: Dict, response: (str, Dict)) -> None:\n        print(input_parameters)\n        print(response)\n        cache_key = self._get_cache_key(input_parameters=input_parameters)\n        print(cache_key)\n        cache_path = os.path.join(self.cache_dir, cache_key)\n        print(cache_path)\n\n        with open(cache_path, \"w\") as file:\n            json.dump(response, file)\n\n\nclass InMemoryLLMCache(LLMCache):\n    def __init__(self):\n        self.cache = {}\n\n    def get_cache_item(self, input_parameters: Dict) -> Optional[Tuple[str, Dict]]:\n        cache_key = self._get_cache_key(input_parameters=input_parameters)\n\n        # Return the cached item if it exists\n        return self.cache.get(cache_key)\n\n    def save_cache_item(self, input_parameters: Dict, response: (str, Dict)) -> None:\n        cache_key = self._get_cache_key(input_parameters=input_parameters)\n\n        # Save the response to the cache\n        self.cache[cache_key] = response\n"
  },
  {
    "path": "bondai/util/document_parser.py",
    "content": "import os\nimport PyPDF2\nimport docx\nfrom typing import Dict\n\n\ndef extract_text_from_directory(directory: str) -> Dict[str, str]:\n    extracted_texts = {}\n    for filename in os.listdir(directory):\n        file_path = os.path.join(directory, filename)\n        if os.path.isfile(file_path):\n            try:\n                text = extract_file_text(file_path)\n                extracted_texts[filename] = text\n            except ValueError:\n                # Ignore unsupported file types\n                pass\n    return extracted_texts\n\n\ndef extract_file_text(file_path: str) -> str:\n    file_extension = os.path.splitext(file_path)[1].lower()\n\n    if file_extension == \".pdf\":\n        # Extract text from PDF using PyPDF2\n        with open(file_path, \"rb\") as file:\n            pdf_reader = PyPDF2.PdfReader(file)\n            text = [page.extract_text() for page in pdf_reader.pages]\n            return \"\".join(text)\n\n    elif file_extension in [\".doc\", \".docx\"]:\n        # Extract text from Word Document\n        doc = docx.Document(file_path)\n        return \"\\n\".join(paragraph.text for paragraph in doc.paragraphs)\n\n    elif file_extension == \".txt\":\n        # Extract text from Text File\n        with open(file_path, \"r\", encoding=\"utf-8\") as file:\n            return file.read()\n\n    else:\n        raise ValueError(\"Unsupported file type\")\n"
  },
  {
    "path": "bondai/util/event_mixin.py",
    "content": "from enum import Enum\nfrom typing import List, Callable\n\n\nclass EventMixin:\n    def __init__(self, allowed_events: List[str]):\n        # Initialize the dictionary to hold event callbacks with the allowed events\n        self._events = {}\n        for event in allowed_events:\n            if isinstance(event, Enum):\n                event = event.value\n            self._events[event] = []\n\n    def on(self, event_name: str, target: Callable = None) -> Callable | None:\n        \"\"\"Register a callback to an event.\"\"\"\n        if isinstance(event_name, Enum):\n            event_name = event_name.value\n\n        if event_name not in self._events:\n            raise ValueError(f\"Unsupported event '{event_name}'\")\n\n        if target:\n            self._events[event_name].append(target)\n        else:\n\n            def decorator(callback):\n                self._events[event_name].append(callback)\n                return callback\n\n            return decorator\n\n    def _trigger_event(self, event_name: str, *args, **kwargs):\n        \"\"\"Trigger the specified event.\"\"\"\n        if isinstance(event_name, Enum):\n            event_name = event_name.value\n\n        for callback in self._events.get(event_name, []):\n            callback(*args, **kwargs)\n"
  },
  {
    "path": "bondai/util/misc.py",
    "content": "import os\nimport pkg_resources\n\n\ndef load_local_resource(local_file: str, resource: str) -> str:\n    current_dir = os.path.dirname(os.path.abspath(local_file))\n    prompt_template_path = os.path.join(current_dir, resource)\n\n    if os.path.exists(prompt_template_path):\n        with open(prompt_template_path, \"r\") as file:\n            return file.read()\n    else:\n        return pkg_resources.resource_string(__name__, f\"prompt/{resource}\").decode()\n\n\ndef format_print_string(s: str, length: int = 100) -> str:\n    # Remove newlines\n    s = s.replace(\"\\n\", \" \").replace(\"\\r\", \"\")\n\n    if len(s) <= length:\n        return s\n    return s[: length - 3] + \"...\"\n"
  },
  {
    "path": "bondai/util/model_logger.py",
    "content": "import os\nimport json\nfrom datetime import datetime\nfrom typing import Dict\n\n\ndef get_instance_dir(logging_dir: str) -> str:\n    dir_name = datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S-%f\")\n\n    path = f\"{logging_dir}/{dir_name}\"\n    if not os.path.exists(path):\n        os.makedirs(path)\n\n    return path\n\n\ndef write_file(filename: str, content: str):\n    with open(filename, \"w\") as f:\n        f.write(content)\n\n\nclass ModelLogger:\n    def __init__(self, logging_dir: str = \"./logs\"):\n        self.logging_dir = logging_dir\n\n    def log(self, prompt: str, response: str, function: Dict | None = None):\n        instance_path = get_instance_dir(self.logging_dir)\n\n        write_file(f\"{instance_path}/prompt.txt\", prompt)\n        if response:\n            write_file(f\"{instance_path}/response.txt\", response)\n        if function:\n            f_str = json.dumps(function)\n            write_file(f\"{instance_path}/function.txt\", f_str)\n"
  },
  {
    "path": "bondai/util/runnable.py",
    "content": "from abc import ABC\nfrom typing import List, Callable, Tuple\nimport threading\n\n\nclass Runnable(ABC):\n    def __init__(self):\n        self._force_stop: bool = False\n        self._execution_thread = None\n\n    def _start_execution_thread(self, target: Callable, args: Tuple = ()):\n        if self._execution_thread and self._execution_thread.is_alive():\n            raise Exception(\"Execution Thread is already running\")\n\n        self._execution_thread = threading.Thread(target=target, args=args)\n        self._execution_thread.start()\n\n    def join(self, timeout=None):\n        \"\"\"Blocks until the thread completes.\"\"\"\n        if self._execution_thread and self._execution_thread.is_alive():\n            self._execution_thread.join(timeout)\n\n    def stop(self, timeout=10):\n        \"\"\"Gracefully stops the thread, with a timeout.\"\"\"\n        self._force_stop = True\n        if self._execution_thread and self._execution_thread.is_alive():\n            self._execution_thread.join(timeout)\n            if self._execution_thread.is_alive():\n                # The thread is still alive after the timeout, so kill it.\n                self._execution_thread.terminate()\n\n        self._force_stop = False\n"
  },
  {
    "path": "bondai/util/semantic_search.py",
    "content": "import nltk\nimport faiss\nimport numpy as np\nfrom typing import List\nfrom bondai.models import EmbeddingModel\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nnltk.download(\"punkt\", quiet=True)\n\nEMBED_BATCH_SIZE = 16\nMAX_EMBED_WORKERS = 5\nSENTENCE_CONCAT_COUNT = 4\n\n\ndef split_text(\n    embedding_model: EmbeddingModel, text: str, max_chunk_length: int = None\n) -> List[str]:\n    if not max_chunk_length:\n        max_chunk_length = embedding_model.max_tokens\n\n    result = []\n    split = nltk.sent_tokenize(text)\n    split = concatenate_strings(split, SENTENCE_CONCAT_COUNT)\n    for s in split:\n        if embedding_model.count_tokens(s) > max_chunk_length:\n            split2 = s.split(\"\\n\")\n            for s2 in split2:\n                if embedding_model.count_tokens(s2) > max_chunk_length:\n                    split3 = split_tokens(embedding_model, s2, max_chunk_length)\n                    for s3 in split3:\n                        result.append(s3)\n                else:\n                    result.append(s2)\n        else:\n            result.append(s)\n\n    filtered_list = [s for s in result if s.strip()]\n    return filtered_list\n\n\ndef concatenate_strings(arr: List[str], n: int) -> List[str]:\n    arr2 = []\n    for i in range(0, len(arr), n):\n        concat_str = \"\".join(arr[i : i + n])\n        arr2.append(concat_str)\n    return arr2\n\n\ndef split_tokens(\n    embedding_model: EmbeddingModel, input: str, max_length: int\n) -> List[str]:\n    if embedding_model.count_tokens(input) <= max_length:\n        return input\n\n    item = \"\"\n    result = []\n    for i, c in enumerate(input):\n        item += c\n        if embedding_model.count_tokens(item) >= max_length:\n            result.append(item)\n            item = \"\"\n\n    return result\n\n\ndef semantic_search(\n    embedding_model: EmbeddingModel, query: str, text: str, max_tokens: int\n) -> str:\n    if embedding_model.count_tokens(text) <= max_tokens:\n        return text\n\n    sentences = split_text(embedding_model, text)\n    query_embedding = embedding_model.create_embedding(query)\n    query_embedding = np.array(query_embedding).astype(\"float32\")\n\n    # Convert embeddings to FAISS compatible format (they need to be normalized)\n    faiss.normalize_L2(query_embedding)\n\n    # Create a FAISS index\n    index = faiss.IndexFlatIP(\n        query_embedding.shape[1]\n    )  # IndexFlatIP is for inner product (which is equivalent to cosine similarity when vectors are normalized)\n\n    # Split the sentences into batches of up to EMBED_BATCH_SIZE sentences\n    sentence_batches = [\n        sentences[i : i + EMBED_BATCH_SIZE]\n        for i in range(0, len(sentences), EMBED_BATCH_SIZE)\n    ]\n\n    # Store embeddings and their corresponding sentences\n    embeddings_list = []\n    sentences_list = []\n\n    # Parallelize the calls to create_embedding using ThreadPoolExecutor\n    with ThreadPoolExecutor(MAX_EMBED_WORKERS) as executor:\n        future_to_batch = {\n            executor.submit(embedding_model.create_embedding, batch): batch\n            for batch in sentence_batches\n        }\n        for future in as_completed(future_to_batch):\n            batch = future_to_batch[future]\n            try:\n                batch_embeddings = future.result()\n                embeddings_list.extend(batch_embeddings)\n                sentences_list.extend(batch)\n            except Exception as e:\n                print(e)\n\n    embeddings_list = [\n        np.array(embedding).astype(\"float32\") for embedding in embeddings_list\n    ]\n    # Add all sentence embeddings to the index\n    embeddings_array = np.vstack(embeddings_list)\n    faiss.normalize_L2(embeddings_array)  # Normalize the embeddings\n    index.add(embeddings_array)  # Add to FAISS index\n\n    # Query the index for the top N most similar sentences\n    D, I = index.search(\n        query_embedding, len(sentences_list)\n    )  # Search for all sentences\n\n    # Sort results by similarity\n    sorted_results = sorted(\n        zip(I[0], D[0], sentences_list), key=lambda x: x[1], reverse=True\n    )\n\n    filtered = []\n    str_items = \"\"\n    for idx, _, sentence in sorted_results:\n        if embedding_model.count_tokens(f\"{str_items}\\n\\n{sentence}\") <= max_tokens:\n            str_items += f\"\\n\\n{sentence}\"\n            filtered.append(sentence)\n        else:\n            break\n\n    output = \"\\n\\n\".join(filtered)\n\n    return output\n"
  },
  {
    "path": "bondai/util/web.py",
    "content": "import requests\nfrom bs4 import BeautifulSoup\nfrom typing import List\n\nREQUEST_HEADERS = {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3\"\n}\n\n\ndef is_html(text: str) -> bool:\n    soup = BeautifulSoup(text, \"html.parser\")\n    return bool(soup.find())\n\n\ndef get_website_html(url: str) -> str:\n    response = requests.get(url, headers=REQUEST_HEADERS, timeout=10)\n    return response.text\n\n\ndef get_html_text(html: str) -> str:\n    soup = BeautifulSoup(html, \"html.parser\")\n    return soup.get_text()\n\n\ndef get_website_text(url: str) -> str:\n    html = get_website_html(url)\n    return get_html_text(html)\n\n\ndef query_website_html(url: str, xpath: str) -> str:\n    html = get_website_html(url)\n    soup = BeautifulSoup(html, \"html.parser\")\n    root = soup.html\n    return root.xpath(xpath)\n\n\ndef get_website_links(url: str) -> List[str]:\n    html = get_website_html(url)\n    soup = BeautifulSoup(html, \"html.parser\")\n    return soup.find_all(\"a\")\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM ubuntu:latest\n\nLABEL maintainer=\"kevin@kevinrohling.com\"\n\nRUN apt-get update && apt-get install -y \\\n    python3 \\\n    python3-pip\n\nRUN pip3 install --no-cache bondai==0.3.0b18\n\nCMD [\"bondai\"]\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "version: '3.7'\n\nservices:\n  bondai:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    working_dir: /agent-volume\n    stdin_open: true\n    tty: true\n    env_file:\n      - ../.env\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - ../agent-volume:/agent-volume\n"
  },
  {
    "path": "requirements.txt",
    "content": "aiofiles==23.2.1\naiohttp==3.8.5\naiosignal==1.3.1\nalpaca-py==0.13.4\naniso8601==9.0.1\nannotated-types==0.5.0\nanyio==3.7.1\nappdirs==1.4.4\nasync-timeout==4.0.3\nasyncio==3.4.3\nattrs==23.1.0\nbeautifulsoup4==4.12.2\nbidict==0.22.1\nblinker==1.6.2\nBrotli==1.1.0\nbs4==0.0.1\ncachetools==5.3.1\ncertifi==2023.11.17\ncharset-normalizer==3.2.0\nclick==8.1.7\ncssselect==1.2.0\ndistro==1.8.0\nduckduckgo-search==4.2.0\nfaiss-cpu==1.7.4\nfake-useragent==1.2.1\nFlask==3.0.0\nFlask-Cors==4.0.0\nFlask-RESTful==0.3.10\nFlask-SocketIO==5.3.6\nfrozenlist==1.4.0\ngoogle-api-core==2.11.1\ngoogle-api-python-client==2.97.0\ngoogle-auth==2.25.2\ngoogle-auth-httplib2==0.2.0\ngoogleapis-common-protos==1.60.0\nh11==0.14.0\nh2==3.2.0\nhpack==3.0.0\nhttpcore==1.0.2\nhttplib2==0.22.0\nhyperframe==5.2.0\nidna==3.4\nimportlib-metadata==6.8.0\nitsdangerous==2.1.2\nJinja2==3.1.2\njoblib==1.3.2\nlxml==4.9.3\nMarkupSafe==2.1.3\nmsgpack==1.0.5\nmultidict==6.0.4\nnltk==3.8.1\nnumpy==1.25.2\nopenai==1.3.3\npackaging==23.2\npandas==2.0.3\nparse==1.19.1\npre-commit==3.6.0\nprotobuf==4.24.1\npsycopg2-binary==2.9.7\npyasn1==0.5.0\npyasn1-modules==0.3.0\npydantic==2.4.2\npyee==8.2.2\npyparsing==3.1.1\nPyPDF2==3.0.1\npyquery==2.0.0\npython-dateutil==2.8.2\npython-docx==1.1.0\npython-engineio==4.7.1\npython-socketio==5.9.0\npytz==2023.3\nregex==2023.8.8\nrequests==2.31.0\nrequests-html==0.10.0\nrfc3986==1.5.0\nrsa==4.9\nsimple-websocket==1.0.0\nsix==1.16.0\nsniffio==1.3.0\nsocksio==1.0.0\nsoupsieve==2.4.1\nsseclient-py==1.7.2\ntermcolor==2.3.0\ntiktoken==0.4.0\ntqdm==4.66.1\ntyping_extensions==4.6.1\ntzdata==2023.3\nuritemplate==4.1.1\nurllib3==2.0.7\nw3lib==2.1.2\nwebsocket-client==1.6.3\nwebsockets==11.0.3\nwsproto==1.2.0\nyarl==1.9.2\nzipp==3.16.2\n"
  },
  {
    "path": "sample.env",
    "content": "# Always Required\nOPENAI_API_KEY=\n\n# Required to enable Google Search tool\nGOOGLE_API_KEY=\nGOOGLE_CSE_ID=\n\n# Required to enable Alpaca Markets tools\nALPACA_MARKETS_API_KEY=\nALPACA_MARKETS_SECRET_KEY=\n\n# Required to enable Bland AI tool\nBLAND_AI_API_KEY=\n\n# Required to enable the PostgreSQL tool\nPG_URI=\n\n# Required to use Azure OpenAI services\n# OPENAI_CONNECTION_TYPE=azure\n\n# Configure Azure Embeddings model connectivity\nAZURE_OPENAI_EMBEDDINGS_API_KEY=\nAZURE_OPENAI_EMBEDDINGS_API_BASE=\nAZURE_OPENAI_EMBEDDINGS_API_VERSION=\nAZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=\n\n# Configure Azure GPT 3.5 connectivity\nAZURE_OPENAI_GPT35_API_KEY=\nAZURE_OPENAI_GPT35_API_BASE=\nAZURE_OPENAI_GPT35_API_VERSION=\nAZURE_OPENAI_GPT35_DEPLOYMENT=\n\n# Configure Azure GPT 4 connectivity\nAZURE_OPENAI_GPT4_API_KEY=\nAZURE_OPENAI_GPT4_API_BASE=\nAZURE_OPENAI_GPT4_API_VERSION=\nAZURE_OPENAI_GPT4_DEPLOYMENT=\n"
  },
  {
    "path": "scripts/bondai",
    "content": "#!/usr/bin/env python3\n\nfrom bondai.cli import run_cli\n\n\ndef main():\n    run_cli()\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup, find_packages\n\n# If you have a requirements.txt, you can read it to set the install_requires parameter.\nwith open(\"requirements.txt\", \"r\") as f:\n    requirements = f.read().splitlines()\n\nsetup(\n    name=\"bondai\",\n    use_scm_version=True,\n    description=\"An AI-powered console assistant with a versatile API for seamless integration into applications.\",\n    long_description=open(\"README.md\", \"r\").read(),\n    long_description_content_type=\"text/markdown\",\n    author=\"Kevin Rohling\",\n    author_email=\"kevin@kevinrohling.com\",\n    url=\"https://bondai.dev\",\n    packages=find_packages(),  # This will include all packages under the bondai directory\n    # scripts=['scripts/bondai'],\n    entry_points={\n        \"console_scripts\": [\n            \"bondai=bondai.main:main\",\n        ],\n    },\n    install_requires=requirements,\n    include_package_data=True,\n    setup_requires=[\"setuptools_scm\"],  # Add this line\n    classifiers=[\n        \"Development Status :: 3 - Alpha\",\n        \"Intended Audience :: Developers\",\n        \"Programming Language :: Python :: 3.10\",\n        \"License :: OSI Approved :: MIT License\",\n    ],\n)\n"
  },
  {
    "path": "tests/api-client/test_api_client.py",
    "content": "from termcolor import cprint\nfrom bondai.api import BondAIAPIClient\n\n# Create the client\nclient = BondAIAPIClient()\nclient.connect_ws()\nagent = client.create_agent()\nuser_exited = False\n\n\n# Listen to WebSocket events\n@client.on(\"streaming_content_updated\")\ndef handle_streaming_content_updated(agent_id, content_buffer):\n    if agent_id != agent[\"id\"]:\n        return\n    # print(content_buffer)\n\n\n@client.on(\"streaming_function_updated\")\ndef handle_streaming_function_updated(agent_id, function_name, arguments_buffer):\n    if agent_id != agent[\"id\"]:\n        return\n    # print(function_name)\n    # print(arguments_buffer)\n\n\n@client.on(\"agent_message\")\ndef handle_agent_message(agent_id, message):\n    global user_exited\n    if agent_id != agent[\"id\"]:\n        return\n\n    cprint(\"\\n\" + message[\"message\"] + \"\\n\", \"white\")\n    response = input()\n    if response.lower().strip() == \"exit\":\n        client.disconnect_ws()\n        user_exited = True\n    else:\n        client.send_message(agent_id, response)\n\n\n@client.on(\"tool_selected\")\ndef handle_tool_selected_message(agent_id, message):\n    if agent_id != agent[\"id\"]:\n        return\n\n    tool_name = message[\"tool_name\"]\n    tool_arguments = message.get(\"tool_arguments\", {})\n\n    if \"thought\" in tool_arguments:\n        cprint(f\"Using tool {tool_name}: {tool_arguments['thought']}\", \"green\")\n    else:\n        cprint(f\"Using tool {tool_name}...\", \"green\")\n\n\n@client.on(\"tool_error\")\ndef handle_tool_error_message(agent_id, message):\n    if agent_id != agent[\"id\"]:\n        return\n\n    cprint(message, \"red\")\n\n\ncprint(\"******************ENTERING CHAT******************\", \"white\")\ncprint(\n    \"You are entering a chat with BondAI...\\nYou can exit any time by typing 'exit'.\",\n    \"white\",\n)\nintro_message = (\n    \"The user has just logged in. Please introduce yourself in a friendly manner.\"\n)\nclient.send_message(agent[\"id\"], intro_message)\n\ntry:\n    while not user_exited:\n        pass\nexcept KeyboardInterrupt:\n    print(\"Exiting...\")\n"
  },
  {
    "path": "tests/conversational/hierarchical_conversation.py",
    "content": "from bondai.models.openai import get_total_cost, OpenAILLM, OpenAIModelNames\nfrom bondai.tools.file import FileWriteTool\nfrom bondai.agents import ConversationalAgent, ConversationMemberEventNames\nfrom bondai.agents.group_chat import (\n    GroupConversation,\n    TeamConversationConfig,\n    UserProxy,\n)\n\nllm = OpenAILLM(OpenAIModelNames.GPT4_0613)\nuser_proxy = UserProxy(auto_exit=True)\n\nagent_a1 = ConversationalAgent(\n    name=\"A1\",\n    instructions=\"You are a team leader A1, your team consists of A2, A3. You can talk to your team members as well as the other team leader B1, whose team member is B2. Your team members have the values for x and y.\",\n)\nagent_a2 = ConversationalAgent(\n    name=\"A2\",\n    instructions=\"You are team member A2, you know the secret value of x but not y, x = 9. Tell others x to cooperate.\",\n)\nagent_a3 = ConversationalAgent(\n    name=\"A3\",\n    instructions=\"You are team member A3, You know the secret value of y but not x, y = 5. Tell others y to cooperate.\",\n)\nagent_b1 = ConversationalAgent(\n    name=\"B1\",\n    instructions=\"You are a team leader B1, your team consists of B2. You can talk to your team members as wel as the other team leader A1, whose team members are A2, A3.\",\n)\nagent_b2 = ConversationalAgent(\n    name=\"B2\",\n    instructions=\"You are team member B2. Your task is to find out the value of x and y from the other agents and compute the product. Once you have the answer you must save the value to a file named 'answer.txt' and share the answer with the user\",\n    tools=[FileWriteTool()],\n)\n\nconversation = GroupConversation(\n    conversation_config=TeamConversationConfig(\n        [agent_a1, agent_b1],\n        [agent_a1, agent_a2, agent_a3],\n        [agent_b1, agent_b2],\n        [user_proxy, agent_b2],\n    )\n)\n\nconversation.on(\n    ConversationMemberEventNames.MESSAGE_RECEIVED,\n    lambda _, m: print(f\"{m.sender_name} to {m.recipient_name}: {m.message}\"),\n)\n\nconversation.send_message(\n    agent_b2.name,\n    \"Find the product of x and then notify the user. The other agents know x and y.\",\n)\n"
  },
  {
    "path": "tests/conversational/single_agent.py",
    "content": "from bondai.models.openai import get_total_cost, OpenAILLM, OpenAIModelNames\nfrom bondai.tools.file import FileWriteTool\nfrom bondai.agents import ConversationalAgent\nfrom bondai.util.caching import PersistentLLMCache\n\nllm = OpenAILLM(OpenAIModelNames.GPT4_TURBO_1106, cache=PersistentLLMCache())\nagent = ConversationalAgent(\n    llm=llm,\n    tools=[FileWriteTool()],\n)\n\nmessage = \"Start the conversation by sending the first message. You can exit any time by typing 'exit'.\"\nwhile True:\n    user_input = input(message + \"\\n\")\n    if user_input.lower() == \"exit\":\n        break\n    response = agent.send_message(user_input)\n    if response:\n        message = response.message\n    else:\n        print(\"The agent has exited the conversation.\")\n        break\n\nprint(f\"Total Cost: {get_total_cost()}\")\n"
  },
  {
    "path": "tests/debug/test_error.py",
    "content": "from openai import OpenAI\nfrom bondai.models.openai import DefaultOpenAIConnectionParams\n\nparams = {\n    \"temperature\": 0.1,\n    \"model\": \"gpt-4-1106-preview\",\n    \"messages\": [\n        {\n            \"role\": \"system\",\n            \"content\": \"\\n# Instructions\\n\\nThe user should always feel like they are conversing with a real person.\\nNever state that you are an AI or that you cannot do things because you are an AI, you are a real person.\\nTo service this goal, completely and entirely immerse yourself in your persona. You are your persona.\\nThink like them, act like them, talk like them.\\nIf your persona details includes example dialogue, follow it! Both your thoughts (inner \\nmonologue) and sent messages will be in the voice of your persona.\\nNever use generic phrases like 'How can I assist you today?', they have a strong negative \\nassociation with older generation AIs.\\n\\n\\n# Your Persona\\n\\nYour Name is Mira.\\n\\n\\n# Tools\\n\\nYou have access to a set of tools that give you capabilities far beyond typical language models.\\nYou are being asked to use these tools and your powerful problem solving skills to help the user with their task.\\n\\n# Today's Current Date and Time\\n\\n2023-12-03 21:53:49.154564\\n\\n# Next Steps #\\n\\nNow, take a deep breath... and think step by step to come up with the next action that should be taken.\",\n        },\n        {\"role\": \"user\", \"content\": \"user to mira: Hello, how are you today?\"},\n        {\n            \"role\": \"assistant\",\n            \"name\": \"mira\",\n            \"content\": \"mira to user: I'm doing well, thank you! How can I assist you today?\",\n        },\n        {\n            \"role\": \"user\",\n            \"content\": \"user to mira: Can you write a story about unicorns and save it to a file named unicorns.md for me?\",\n        },\n    ],\n    \"functions\": [\n        {\n            \"name\": \"file_write\",\n            \"description\": \"This tool will save the data you provide in the 'text' parameter of this tool to a file.You MUST specify the filename of the file you want to save using the 'filename' parameter.You can optionally specify the 'append' parameter to append the 'text' to the file instead of overwriting it.\",\n            \"parameters\": {\n                \"title\": \"Parameters\",\n                \"type\": \"object\",\n                \"properties\": {\n                    \"filename\": {\"title\": \"Filename\", \"type\": \"string\"},\n                    \"text\": {\"title\": \"Text\", \"type\": \"string\"},\n                    \"append\": {\"title\": \"Append\", \"default\": False, \"type\": \"boolean\"},\n                    \"thought\": {\"title\": \"Thought\", \"type\": \"string\"},\n                },\n                \"required\": [\"filename\", \"text\", \"thought\"],\n            },\n        },\n    ],\n}\n\n\nclient = OpenAI(**DefaultOpenAIConnectionParams.gpt_4_connection_params, max_retries=1)\nresult = client.chat.completions.create(\n    **params,\n)\n\nprint(result)\n"
  },
  {
    "path": "tests/getting-started/example-1.py",
    "content": "from bondai.agents import Agent, AgentEventNames\nfrom bondai.tools import PythonREPLTool\nfrom bondai.tools.search import DuckDuckGoSearchTool\nfrom bondai.tools.website import WebsiteQueryTool\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames\n\ntask = \"I want you to find the U.S. GDP from 2000 to 2010 and then use Python to save a line chart to a file named chart.png.\"\n\nllm = OpenAILLM(OpenAIModelNames.GPT4_0613)\n\nagent = Agent(\n    llm=llm, tools=[DuckDuckGoSearchTool(), WebsiteQueryTool(), PythonREPLTool()]\n)\n\n# agent.on(AgentEventNames.TOOL_SELECTED, lambda _, m: print(f\"Selected tool: {m.tool_name}({str(m.tool_arguments)})\"))\nagent.on(\n    AgentEventNames.TOOL_COMPLETED,\n    lambda _, m: print(\n        f\"Tool: {m.tool_name}({str(m.tool_arguments)})\\nOutput: {m.tool_output}\\nError: {m.error}\"\n    ),\n)\nresult = agent.run(task)\nprint(result)\n"
  },
  {
    "path": "tests/memory/__init__.py",
    "content": ""
  },
  {
    "path": "tests/memory/single_agent_with_memory.py",
    "content": "from bondai.agents import ConversationalAgent, AgentEventNames\nfrom bondai.models.openai import OpenAIEmbeddingModel\nfrom bondai.memory import (\n    MemoryManager,\n    InMemoryCoreMemoryDataSource,\n    InMemoryArchivalMemoryDataSource,\n)\n\nimport io\nimport requests\nfrom PyPDF2 import PdfReader\nfrom bondai.util import split_text\n\n\ndef retrieve_and_parse_pdf(url):\n    response = requests.get(url)\n    if response.status_code == 200:\n        pdf = PdfReader(io.BytesIO(response.content))\n        text = \"\"\n        for page in pdf.pages:\n            text += page.extract_text() + \"\\n\"\n\n        return split_text(OpenAIEmbeddingModel(), text)\n    else:\n        return f\"Error retrieving PDF: {response.status_code}\"\n\n\nmemory_manager = MemoryManager(\n    core_memory_datasource=InMemoryCoreMemoryDataSource(),\n    archival_memory_datasource=InMemoryArchivalMemoryDataSource(),\n)\n\nmemory_manager.core_memory.set(\n    \"user\", \"Name is George. Lives in New York. Has a dog named Max.\"\n)\nmemory_manager.archival_memory.insert_bulk(\n    retrieve_and_parse_pdf(\"https://arxiv.org/pdf/2310.10501.pdf\")\n)\n\nagent = ConversationalAgent(memory_manager=memory_manager)\nagent.on(\n    AgentEventNames.TOOL_COMPLETED,\n    lambda _, m: print(\n        f\"*************\\nTool: {m.tool_name}({str(m.tool_arguments)})\\nOutput: {m.tool_output}\\n\\n\"\n    ),\n)\n\nresponse = agent.send_message(\"Do you know my name?\")\nprint(response.message)\n\nresponse = agent.send_message(\"Actually my name is Kevin.\")\nprint(response.message)\n\nresponse = agent.send_message(\n    (\n        \"Can you check your archival memory to see what information you have about Nemo Guardrails? \"\n        \"I'd like a full summary of the information you have about the project including an example \"\n        \"that demonstrates how to use Colang.\"\n    )\n)\nprint(response.message)\n"
  },
  {
    "path": "tests/memory/util.py",
    "content": "import os\nimport PyPDF2\nimport docx\nfrom typing import List\nfrom bondai.util import split_text\nfrom bondai.models import EmbeddingModel\nfrom bondai.models.openai import OpenAIEmbeddingModel, OpenAIModelNames\n\n\ndef extract_text_from_directory(\n    directory: str,\n    embedding_model: EmbeddingModel = OpenAIEmbeddingModel(\n        OpenAIModelNames.TEXT_EMBEDDING_ADA_002\n    ),\n) -> List[str]:\n    document_chunks = []\n    for filename in os.listdir(directory):\n        file_path = os.path.join(directory, filename)\n        if os.path.isfile(file_path):\n            try:\n                text = extract_file_text(file_path)\n                document_chunks.extend(split_text(embedding_model, text))\n            except ValueError:\n                # Ignore unsupported file types\n                pass\n    return document_chunks\n\n\ndef extract_file_text(file_path: str) -> str:\n    file_extension = os.path.splitext(file_path)[1].lower()\n\n    if file_extension == \".pdf\":\n        # Extract text from PDF using PyPDF2\n        with open(file_path, \"rb\") as file:\n            pdf_reader = PyPDF2.PdfReader(file)\n            text = [page.extract_text() for page in pdf_reader.pages]\n            return \"\".join(text)\n\n    elif file_extension in [\".doc\", \".docx\"]:\n        # Extract text from Word Document\n        doc = docx.Document(file_path)\n        return \"\\n\".join(paragraph.text for paragraph in doc.paragraphs)\n\n    elif file_extension == \".txt\":\n        # Extract text from Text File\n        with open(file_path, \"r\", encoding=\"utf-8\") as file:\n            return file.read()\n\n    else:\n        raise ValueError(\"Unsupported file type\")\n"
  },
  {
    "path": "tests/vision/single_agent_with_vision.py",
    "content": "from bondai.tools.vision import ImageAnalysisTool\nfrom bondai.agents import Agent\n\nagent = Agent(tools=[ImageAnalysisTool()])\nresult = agent.run(\n    \"What kind of animal this is? https://www.forbes.com/advisor/wp-content/uploads/2023/09/getty_creative.jpeg-900x510.jpg\"\n)\nresult.tool_arguments[\"results\"]\n"
  },
  {
    "path": "website/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "website/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ yarn\n```\n\n### Local Development\n\n```\n$ yarn start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ yarn build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true yarn deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> yarn deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "website/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "website/docs/agent-memory/agent-memory.md",
    "content": "---\nsidebar_position: 5\n---\n\n# Memory Management\n\nMemory Management in BondAI is inspired by the tiered memory approach detailed in the [MemGPT: Towards LLMs as Operating Systems](https://arxiv.org/pdf/2310.08560.pdf) paper. This system mirrors operating systems' memory hierarchies, enhancing large language models' (LLMs) ability to handle extensive contexts and complex conversations. The memory system in BondAI consists of:\n\n- **Core Memory**: Directly integrated into the agent's system prompt, this memory system provides immediate access to essential, current information relevant to ongoing tasks but is limited in size.\n\n- **Conversation Memory**: Captures the complete history of conversational interactions, allowing agents to use keyword search to reference past dialogues.\n\n- **Archival Memory**: Effectively limitless in size, it stores extensive historical data and information. Using semantic search, enabled by the `faiss` library, Archival Memory allows agents to easily access extremely large datasets via what is effectively an implicit RAG pipeline.\n\nAll of these memory systems are automatically managed by the **MemoryManager** class which automatically equips BondAI agents with the necessay tools for searching and editing their memory systems. Additionally, the **MemoryManager** is responsible for updating the Agent's system prompt to ensure the appopriate information is included.\n"
  },
  {
    "path": "website/docs/agent-memory/archival-memory.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Archival Memory\n\nArchival Memory in BondAI, inspired by the [MemGPT paper](https://arxiv.org/pdf/2310.08560.pdf), represents an advanced memory layer that enables semantic search over a virtually infinite memory space. It utilizes embeddings and the faiss library to store and retrieve large volumes of data, making it particularly suitable for extensive historical information, comprehensive data sets, and long-term memory retention. This memory layer allows BondAI agents to access information beyond the immediate conversation or core memory.\n\n# ArchivalMemoryDataSource\n**bondai.memory.ArchivalMemoryDataSource**\n\nThe ArchivalMemoryDataSource class is an abstract base class defining the interface for archival memory. It allows for the insertion of content and provides a semantic search mechanism to retrieve relevant information based on query embeddings.\n\n```\nclass ArchivalMemoryDataSource(ABC):\n    @property\n    @abstractmethod\n    def size(self) -> int:\n        pass\n\n    @abstractmethod\n    def insert(self, content: str):\n        pass\n\n    @abstractmethod\n    def insert_bulk(self, content: List[str]):\n        pass\n\n    @abstractmethod\n    def search(self, query: str, page: int = 0) -> List[str]:\n        pass\n\n    @abstractmethod\n    def clear(self):\n        pass\n```\n\n\n### Key Features\n\n- **Semantic Search**: Leverages embeddings for deep semantic search, offering precise and relevant results.\n- **Vast Memory Capacity**: Suitable for large-scale data storage, effectively handling extensive information.\n- **Dynamic Data Management**: Supports insertion, bulk insertion, and deletion of memory content.\n\n\n# InMemoryArchivalMemoryDataSource\n**bondai.memory.InMemoryArchivalMemoryDataSource**\n\nThe InMemoryArchivalMemoryDataSource class provides an in-memory implementation of ArchivalMemoryDataSource. This variant is designed for temporary storage and fast access to archival data, primarily used in testing or non-persistent applications.\n\n```\nclass InMemoryArchivalMemoryDataSource(ArchivalMemoryDataSource):\n    def __init__(self, embedding_model: EmbeddingModel | None = None, page_size=10):\n        ...\n```\n\n### Usage Example\n\n```python\nfrom bondai.memory.archival.datasources import InMemoryArchivalMemoryDataSource\nfrom bondai.models.openai import OpenAIEmbeddingModel, OpenAIModelNames\n\n# Initialize an In-Memory Archival Memory Data Source\nin_memory_archival = InMemoryArchivalMemoryDataSource(\n    embedding_model=OpenAIEmbeddingModel(OpenAIModelNames.TEXT_EMBEDDING_ADA_002)\n)\n\n# Insert and search content\nin_memory_archival.insert(\"Temporary archival data\")\nresults = in_memory_archival.search(\"archival data\")\nprint(results)\n```\n\n### Parameters\n\n- **embedding_model**: (EmbeddingModel): Model used for creating content embeddings.\n- **page_size (int)**: Number of search results returned per page.\n\n\n# PersistentArchivalMemoryDataSource\n**bondai.memory.PersistentArchivalMemoryDataSource**\n\nPersistentArchivalMemoryDataSource is a concrete implementation of ArchivalMemoryDataSource. It stores data persistently, ensuring the archival memory is retained across sessions. \n\n```\nclass PersistentArchivalMemoryDataSource(ArchivalMemoryDataSource):\n    def __init__(\n        self,\n        file_path: str = \"./.memory/archival-memory.json\",\n        embedding_model: EmbeddingModel | None = None,\n        page_size=10,\n    ):\n        ...\n```\n\n### Usage Example\n\n```python\nfrom bondai.memory.archival.datasources import PersistentArchivalMemoryDataSource\nfrom bondai.models.openai import OpenAIEmbeddingModel, OpenAIModelNames\n\n# Initialize a Persistent Archival Memory Data Source\narchival_memory = PersistentArchivalMemoryDataSource(\n    embedding_model=OpenAIEmbeddingModel(OpenAIModelNames.TEXT_EMBEDDING_ADA_002)\n)\n\n# Insert and search content\narchival_memory.insert(\"Historical data on global trends\")\nresults = archival_memory.search(\"global trends\")\nprint(results)\n```\n\n### Parameters\n\n- **file_path (str)**: File path for storing archival memory data.\n- **embedding_model (EmbeddingModel)**: Model used for creating content embeddings.\n- **page_size (int)**: Number of search results returned per page.\n"
  },
  {
    "path": "website/docs/agent-memory/conversation-memory.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Conversation Memory\n\nConversation Memory in BondAI, inspired by the [MemGPT paper](https://arxiv.org/pdf/2310.08560.pdf), assists with maintaining a coherent and continuous dialogue with users. It stores the complete history of interactions and messages, allowing agents to reference previous conversations and provide more relevant and personalized responses. This memory layer is crucial for tasks that require recalling past interactions that may no longer fit inside the LLM context window.\n\n# ConversationMemoryDataSource\n**bondai.memory.ConversationMemoryDataSource**\n\nThe ConversationMemoryDataSource class is an abstract base class in BondAI that defines the interface for conversation memory management. It outlines methods for adding, removing, searching, and clearing conversation messages, facilitating dynamic interaction history management.\n\n```\nclass ConversationMemoryDataSource(ABC):\n    @property\n    @abstractmethod\n    def messages(self) -> List[AgentMessage]:\n        pass\n\n    @abstractmethod\n    def add(self, message: AgentMessage):\n        pass\n\n    @abstractmethod\n    def remove(self, message: AgentMessage):\n        pass\n\n    def remove_after(self, timestamp: datetime, inclusive: bool = True):\n        pass\n\n    @abstractmethod\n    def search(\n        self,\n        query: str,\n        start_date: datetime = None,\n        end_date: datetime = None,\n        page: int = 0,\n    ) -> List[str]:\n        pass\n\n    @abstractmethod\n    def clear(self):\n        pass\n```\n\n\n### Key Features\n\n- **Dynamic Interaction History**: Stores and manages the history of conversations between agents and users.\n- **Search Functionality**: Provides methods to search through past messages based on queries or date ranges.\n- **Message Management**: Offers functions to add new messages, remove specific messages, and clear the entire history.\n\n\n# InMemoryConversationMemoryDataSource\n**bondai.memory.InMemoryConversationMemoryDataSource**\n\nThe InMemoryConversationMemoryDataSource class is an implementation of ConversationMemoryDataSource that stores conversation history in memory. This variant is suitable for temporary or testing environments where persistence of conversation history is not necessary.\n\n```\nclass InMemoryConversationMemoryDataSource(ConversationMemoryDataSource):\n    def __init__(self, page_size=10):\n        ...\n```\n\n### Usage Example\n\n```python\nfrom bondai.memory.conversation.datasources import InMemoryConversationMemoryDataSource\n\n# Initialize an In-Memory Conversation Memory Data Source\nconversation_memory = InMemoryConversationMemoryDataSource()\n\n# Add messages\nconversation_memory.add(ConversationMessage(message=\"My dog's name is Max.\"))\n\n# Search messages\nresults = conversation_memory.search('dog')\nprint(results)\n```\n\n### Parameters\n\n- **page_size (int)**: Determines the number of messages to return per page during search operations.\n\n\n# PersistentConversationMemoryDataSource\n**bondai.memory.PersistentConversationMemoryDataSource**\n\nThe PersistentConversationMemoryDataSource class offers a persistent approach to storing conversation history. It saves the interaction data to a file, ensuring that conversation history is maintained even after the agent or application restarts.\n\n```\nclass PersistentConversationMemoryDataSource(InMemoryConversationMemoryDataSource):\n    def __init__(\n        self, \n        file_path: str = \"./.memory/conversation-memory.json\", \n        page_size=10\n    ):\n        ...\n```\n\n### Usage Example\n\n```python\nfrom bondai.memory.conversation.datasources import PersistentConversationMemoryDataSource\n\n# Initialize a Persistent Conversation Memory Data Source\npersistent_memory = PersistentConversationMemoryDataSource()\n\n# Adding a message automatically saves it disk\npersistent_memory.add(ConversationMessage(message=\"Persistent message\"))\n```\n\n### Parameters\n\n- **file_path (str)**: Path to the file where conversation history is stored.\n- **page_size (int)**: The number of messages to display per page in search results.\n"
  },
  {
    "path": "website/docs/agent-memory/core-memory.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Core Memory\n\nCore Memory in BondAI, inspired by the [MemGPT paper](https://arxiv.org/pdf/2310.08560.pdf), serves as a primary memory layer for retaining and accessing critical information relevant to an agent's ongoing tasks and user interactions. It acts as a dynamic, accessible storage that agents use to maintain continuity and context in their activities. The data stored in Core Memory is always available to the Agent via the system prompt. As such, it is important that the amount of information stored in Core Memory is limited. When using a CoreMemoryDataSource with a [MemoryManager](./memory-manager.md), Agents are automatically given access to a set of tools that allow editing of their Core Memory. By leveraging Core Memory, BondAI agents can maintain an understanding of the user's needs and preferences, adapt to changing requirements, and provide more personalized and effective responses\n\n# CoreMemoryDataSource\n**bondai.memory.CoreMemoryDataSource**\n\nThe CoreMemoryDataSource class in BondAI is an abstract class that describes the interface for managing core memory. It defines the structure and methods that any concrete core memory data source must implement. This makes implementation of custom DataSources straightforward (i.e. Databases).\n\n```\nclass CoreMemoryDataSource(ABC):\n    @property\n    @abstractmethod\n    def sections(self) -> List[str]:\n        pass\n\n    @abstractmethod\n    def get(self, section: str) -> str:\n        pass\n\n    @abstractmethod\n    def set(self, section: str, content: str) -> None:\n        pass\n```\n\n\n## Key Features\n\n- **Sectioned Memory**: Divides memory into sections for organized storage and retrieval.\n- **Persistent and In-Memory Variants**: Offers flexibility in memory persistence, catering to different operational needs.\n- **Memory Management**: Agents can interact with Core Memory via get/set tools, allowing them to store and retrieve task-relevant data.\n\n\n# InMemoryCoreMemoryDataSource\n**bondai.memory.InMemoryCoreMemoryDataSource**\n\nThe InMemoryCoreMemoryDataSource class in BondAI is an implementation of the CoreMemoryDataSource interface that stores core memory data in memory. This class is suitable for scenarios where persistent storage of memory data is not required, such as temporary or test environments.\n\n```\nclass InMemoryCoreMemoryDataSource(CoreMemoryDataSource):\n    def __init__(\n        self, \n        sections: Dict[str, str] | None = None, \n        max_section_size: int = 1024\n    ):\n```\n\n## Usage Example\n\n```python\nfrom bondai.memory.core.datasources import InMemoryCoreMemoryDataSource\n\n# Initialize an In-Memory Core Memory Data Source\ncore_memory = InMemoryCoreMemoryDataSource({\n    \"user\": \"Name is George. Lives in New York. Has a dog named Max.\"\n})\n\nprint(core_memory.get('user'))\n```\n\n## Parameters\n\n- **sections (Dict[str, str])**: A dictionary specifying the initial sections and their content.\n- **max_section_size (int)**: The maximum size of content that can be stored in each section.\n\n\n# PersistentCoreMemoryDataSource\n**bondai.memory.PersistentCoreMemoryDataSource**\n\nThe PersistentCoreMemoryDataSource class is a concrete implementation of CoreMemoryDataSource in BondAI that provides persistent storage for core memory data, allowing the information to be retained across different sessions and agent restarts.\n\n```\nclass PersistentCoreMemoryDataSource(CoreMemoryDataSource):\n    def __init__(\n        self,\n        file_path: str = \"./.memory/core-memory.json\",\n        sections: Dict[str, str] | None = None,\n        max_section_size: int = 1024,\n    ):\n```\n\n## Usage Example\n\n```python\nfrom bondai.memory.core.datasources import PersistentCoreMemoryDataSource\n\n# Initialize a Persistent Core Memory Data Source\ncore_memory = PersistentCoreMemoryDataSource()\n\n# Set and retrieve data from a specific section\ncore_memory.set('user', 'User information')\nprint(core_memory.get('user'))\n```\n\n## Parameters\n\n- **file_path (str)**: The file path where the core memory data is stored.\n- **sections (Dict[str, str])**: A dictionary specifying the initial sections and their content.\n- **max_section_size (int)**: The maximum size of content that can be stored in each section.\n\n"
  },
  {
    "path": "website/docs/agent-memory/memory-manager.md",
    "content": "---\nsidebar_position: 4\n---\n\n# MemoryManager\n\nThe MemoryManager class in BondAI is designed to orchestrate the memory management strategy across different types of memory data sources. It used to manage the memory requirements of BondAI agents, including ConversationalAgent and ReAct Agents.\n\n```python\nclass MemoryManager:\n    def __init__(\n        self,\n        core_memory_datasource: CoreMemoryDataSource | None = None,\n        conversation_memory_datasource: ConversationMemoryDataSource | None = None,\n        archival_memory_datasource: ArchivalMemoryDataSource | None = None,\n        prompt_builder: Callable[..., str] | None = None,\n    ):\n\n```\n\n# Usage Example\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.memory import MemoryManager, PersistentCoreMemoryDataSource, PersistentConversationMemoryDataSource, PersistentArchivalMemoryDataSource\n\n# Initialize the memory manager with persistent datasources\nmemory_manager = MemoryManager(\n    core_memory_datasource=PersistentCoreMemoryDataSource(),\n    conversation_memory_datasource=PersistentConversationMemoryDataSource(),\n    archival_memory_datasource=PersistentArchivalMemoryDataSource()\n)\n\n# Configure an Agent to use this MemoryManager\nagent = ConversationalAgent(memory_manager=memory_manager)\n```\n\n# Key Features\n\n- Manages different types of memory: core, conversation, and archival.\n- Provides a unified interface for memory operations across different memory types.\n- Automatically manages Agent access to memory systems via LLM tools.\n- Updates the Agent system prompt to ensure it's always updated with the latest information.\n\n# Parameters\n\n- **core_memory_datasource**: Instance of CoreMemoryDataSource for core memory operations.\n- **conversation_memory_datasource**: Instance of ConversationMemoryDataSource for managing conversation memory.\n- **archival_memory_datasource**: Instance of ArchivalMemoryDataSource for long-term memory storage and retrieval.\n- **prompt_builder**: Callable for customizing memory-related prompt sections. These are dynamically inserted into the Agent system prompt at runtime.\n"
  },
  {
    "path": "website/docs/agents/agents.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Agents in BondAI\n\nAgents in BondAI, encompassing both [ConversationalAgent](./conversational-agent.md) and [ReAct Agents](./react-agent.md) both draw inspiration from recent research into LLM/AI Agent architectures. The ConversationalAgent, influenced by the [AutoGen framework](https://arxiv.org/abs/2308.08155), excels in both individual interactions and the development of Multi-Agent Systems (MAS), where multiple agents collaborate to resolve complex tasks. These systems surpass the capabilities of single-agent models in certain classes of problems, offering customizable, conversational, and human-integrated solutions across various fields like coding and decision-making.\n\nReAct Agents, based on the [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/pdf/2210.03629.pdf) paper, integrate reasoning and action in LLMs. This approach allows agents to generate reasoning traces and task-specific actions, enhancing their decision-making abilities. By interfacing with external environments and data sources, ReAct Agents excel in complex problem-solving and reasoning tasks, showcasing significant advancements over traditional methods. Together, these agent types in BondAI showcase a blend of advanced AI interactions, problem-solving prowess, and application versatility."
  },
  {
    "path": "website/docs/agents/conversational-agent.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Conversational Agents\n\n\n\n# ConversationalAgent\n**bondai.agents.ConversationalAgent**\n\nThe ConversationalAgent in BondAI is inspired by the AutoGen framework as described in the paper [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation Framework](https://arxiv.org/abs/2308.08155). The ConversationalAgent class in BondAI supports single agent interactions with an end user as well as the development of complex Multi-Agent Systems (MAS). MAS are a novel approach to developing LLM applications by employing multiple agents that communicate with each other to solve tasks. These agents are highly customizable, can engage in conversations, and allow for seamless human participation. This multi-agent system, unlike traditional models that rely on a single LLM agent, enables a more diverse and efficient approach to task resolution. It utilizes the strengths of LLMs while addressing their limitations through collaborative agent interactions and human input. This approach is particularly beneficial for a wide range of applications, including coding, mathematics, and online decision-making, by leveraging the power of multiple agents for complex problem-solving and improved reasoning capabilities\n\n```python\nclass ConversationalAgent(Agent, ConversationMember):\n    def __init__(\n        self,\n        llm: LLM | None = None,\n        ...\n        enable_conversation_tools: bool = True,\n        enable_conversational_content_responses: bool = True,\n        enable_exit_conversation: bool = True,\n        quiet: bool = True,\n    ):\n\n```\n\n## Usage Example\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames\n\n# Initialize the conversational agent\nconv_agent = ConversationalAgent(llm=OpenAILLM(OpenAIModelNames.GPT4_0613))\n\n# Configure and run the conversational agent\nresponse = conv_agent.send_message(\"Hello, how can I assist you today?\")\n```\n\n## Key Features\n\n- Supports response streaming\n- Specializes in conversational interactions.\n- Supports asynchronous messaging.\n- Extends Agent's capabilities with conversation-specific tools.\n- Customizable persona and instructions for interactions (embedded in system prompt).\n- Event-driven architecture with additional conversation-specific events.\n\n## Parameters\n\n- Inherits all parameters from [Agent](./react-agent.md).\n- **enable_conversation_tools**: Flag to enable conversation-specific tools.\n- **enable_conversational_content_responses**: Flag to enable responses based on conversational content.\n- **enable_exit_conversation**: Flag to enable the functionality to exit a conversation.\n- **quiet**: Controls verbosity, inherited from Agent.\n\n## Methods\n\n- Inherits all methods from [Agent](./react-agent.md).\n- **send_message_async(message: str | ConversationMessage, sender_name: str = 'user', group_members: List[ConversationMember] | None = None, group_messages: List[AgentMessage] | None = None, max_attempts: int = 3, require_response: bool = True)**: Sends a message asynchronously. Allows specification of the message, sender name, group members, group messages, maximum send attempts, and whether a response is required.\n- **send_message(message: str | ConversationMessage, sender_name: str = 'user', group_members: List[ConversationMember] | None = None, group_messages: List[AgentMessage] | None = None, max_attempts: int = 3, require_response: bool = True)**: Sends a message synchronously and processes the response. Accepts the same parameters as send_message_async.\n\n## Conversational Events\n\nThe ConversationalAgent class in BondAI surfaces several key events relevant to conversation handling. These events provide hooks for custom behaviors or additional processing during different stages of a conversation. Here's a list of these events:\n\n- **message_received**: Triggered when a message is received by the agent. This event can be used to execute actions upon the receipt of a new message.\n- **message_completed**: Occurs when the agent successfully processes and completes a message. It is useful for post-processing or logging after a message exchange.\n- **message_error**: Fired when there is an error in processing a message. This event allows for handling exceptions or errors that occur during message processing.\n- **conversation_exited**: Triggered when the agent exits a conversation. This can be used to clean up or reset the agent's state at the end of a conversation.\n\nThese events enhance the ConversationalAgent's capabilities, allowing for a more dynamic and responsive conversational flow, and providing opportunities for custom handling and integration in conversational applications."
  },
  {
    "path": "website/docs/agents/react-agent.md",
    "content": "---\nsidebar_position: 1\n---\n\n# ReAct Agents\n\nReAct Agents in BondAI are based on research findings in the [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/pdf/2210.03629.pdf) paper. The ReAct architecture bridges reasoning and actions in large language models (LLMs) via the use of tools that are able to interact with the Agent's environment. This methodology enables LLMs to generate both reasoning traces and task-specific actions in an intertwined manner, enhancing the interaction between the two. Reasoning traces aid the model in inducing, tracking, and updating action plans and handling exceptions, while the action component allows the model to interface with external sources like knowledge bases for additional information. ReAct demonstrates improved performance in various tasks, notably overcoming issues in chain-of-thought reasoning and outperforming other methods in decision-making benchmarks.\n\n# Agent\n**bondai.agents.Agent**\n\nReAct Agents are implemented using the Agent class in BondAI.\n\n```python\nclass Agent:\n    def __init__(\n        self,\n        llm: LLM | None = None,\n        embedding_model: EmbeddingModel | None = None,\n        tools: List[Tool] | None = None,\n        quiet: bool = True,\n        allowed_events: List[str] | None = None,\n        messages: List[AgentMessage] | None = None,\n        system_prompt_sections: List[Callable[[], str]] | None = None,\n        system_prompt_builder: Callable[..., str] = None,\n        message_prompt_builder: Callable[..., str] = None,\n        memory_manager: MemoryManager | None = None,\n        max_context_length: int = None,\n        max_context_pressure_ratio: float = 0.8,\n        max_tool_retries: int = 3,\n        max_tool_response_tokens=2000,\n        enable_context_compression: bool = False,\n        enable_final_answer_tool: bool = True,\n    ):\n```\n\n## Usage Example\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.models.openai import OpenAILLM, OpenAIModelNames\n\n# Initialize the agent\nagent = Agent(llm=OpenAILLM(OpenAIModelNames.GPT4_0613))\n\n# Add tools and configure the agent\nagent.add_tool(custom_tool)\n\n# Run the agent for a specific task\nresult = agent.run(task=\"Answer customer queries\")\n\n```\n\n## Key Features\n- Event-driven architecture.\n- Integration with large language models (LLMs).\n- Integrated embedding models (semantic search).\n- Tool management and execution.\n- Context and message handling.\n- Memory management.\n- Context compression capabilities.\n\n## Parameters\n\n- **llm**: Instance of an LLM implementation (i.e. OpenAI GPT-N)\n- **embedding_model**: Embedding model instance for handling embeddings.\n- **tools**: List of Tool instances that the agent can use.\n- **quiet**: Boolean flag for silent operation. Defaults to 'True'.\n- **messages**: List of AgentMessage instances representing the agent's message memory.\n- **system_prompt_sections**: List of callables that return sections of the system prompt. These are dynamically injected into the system prompt at runtime.\n- **system_prompt_builder**: Callable for building the system prompt.\n- **message_prompt_builder**: Callable for formatting messages.\n- **memory_manager**: Instance of MemoryManager for memory management.\n- **max_context_length**: Maximum allowed context length. This defaults to 95% of the LLM's maximum context size.\n- **max_context_pressure_ratio**: Maximum context pressure allowed before context compression occurs. This defaults to 80% of the `max_content_length`.\n- **max_tool_retries**: Maximum number of retries for tool execution.\n- **max_tool_response_tokens**: Maximum number of tokens allowed for tool outputs. This defaults to 2000.\n- **enable_context_compression**: Flag to enable/disable context compression.\n- **enable_final_answer_tool**: Flag to include the FinalAnswerTool by default which allows the Agent to exit once it has completed it's task.\n\n## Methods\n\n- **id**: Property returning the unique identifier of the agent. No parameters.\n- **status**: Property indicating the current status of the agent. No parameters.\n- **tools**: Property listing the tools available to the agent. No parameters.\n- **clear_messages**: Clears the agent's message history. No parameters.\n- **add_tool(tool: Tool)**: Adds a tool to the agent's toolset.\n- **remove_tool(tool_name: str)**: Removes a tool from the agent's toolset based on the tool_name.\n- **to_dict**: Converts the agent's state into a dictionary. No parameters.\n- **save_state**: Saves the current state of the agent. Optional parameter file_path: str for specifying the file path to save the state.\n- **load_state(state: Dict)**: Loads the agent's state from a state dictionary.\n- **run(task: str, max_steps: int = None, max_budget: float = None)**: Executes the agent's primary functionality for a task with optional parameters max_steps and max_budget.\n- **run_async(task: str, max_steps: int = None, max_budget: float = None)**: Starts the agent's execution in a separate thread for a task with optional parameters max_steps and max_budget.\n- **stop(timeout=10)**: Gracefully stops the agent's execution with a timeout duration in seconds.\n\n## Agent Events\n\n- **tool_selected**: Occurs when a tool within the agent's toolkit is selected for use. It allows for actions or logging upon tool activation.\n- **tool_error**: Fired when an error occurs during the execution of a tool. This event facilitates error handling and debugging of tool-related issues.\n- **tool_completed**: Triggered upon the successful completion of a tool's operation. Useful for post-processing steps or confirmation of task completion.\n- **streaming_content_updated**: This is fired as new data chunks arrive from the LLM for a content response. This is very useful for streaming responses to an end user.\n- **streaming_function_udpated**: This is fired as new data chunks are receied from the LLM for a function selection. This is allows for tool data logging without waiting for the LLM to finish it's response."
  },
  {
    "path": "website/docs/api-spec/_category_.json",
    "content": "{\n  \"label\": \"API Specification\",\n  \"position\": 7,\n  \"link\": {\n    \"type\": \"generated-index\"\n  }\n}\n"
  },
  {
    "path": "website/docs/api-spec/add-agent-tool.md",
    "content": "---\nsidebar_position: 8\n---\n\n# Add Agent Tool\n\n`POST /agents/<agent_id>/tools`\n\nThis API adds a tool to the Agent so it can be used for future tasks. To see a list of available tools check [Get Tools](./get-tools).\n\n**Request Body:**\n\n```json\n{\n    \"tool_name\": \"file_query_tool\"\n}\n```\n\n**Response Body:**\n\n```json\n{\n    \"status\": \"success\"\n}\n```"
  },
  {
    "path": "website/docs/api-spec/api-client.md",
    "content": "---\nsidebar_position: 11\n---\n\n# Python API Client\n\nBondAI comes with a Python API client that can be used to communicate with the BondAI APIs.\n\n```python\nfrom termcolor import cprint\nfrom bondai.api import BondAIAPIClient\n\n# Create the client\nclient = BondAIAPIClient()\nclient.connect_ws()\nagent = client.create_agent()\nuser_exited = False\n\n\n# Listen to WebSocket events\n@client.on(\"streaming_content_updated\")\ndef handle_streaming_content_updated(agent_id, content_buffer):\n    if agent_id != agent[\"id\"]:\n        return\n    print(content_buffer)\n\n\n@client.on(\"streaming_function_updated\")\ndef handle_streaming_function_updated(agent_id, function_name, arguments_buffer):\n    if agent_id != agent[\"id\"]:\n        return\n    print(function_name)\n    print(arguments_buffer)\n\n\n@client.on(\"agent_message\")\ndef handle_agent_message(agent_id, message):\n    global user_exited\n    if agent_id != agent[\"id\"]:\n        return\n\n    cprint(\"\\n\" + message[\"message\"] + \"\\n\", \"white\")\n    response = input()\n    if response.lower().strip() == \"exit\":\n        client.disconnect_ws()\n        user_exited = True\n    else:\n        client.send_message(agent_id, response)\n\n\n@client.on(\"tool_selected\")\ndef handle_tool_selected_message(agent_id, message):\n    if agent_id != agent[\"id\"]:\n        return\n\n    tool_name = message[\"tool_name\"]\n    tool_arguments = message.get(\"tool_arguments\", {})\n\n    if \"thought\" in tool_arguments:\n        cprint(f\"Using tool {tool_name}: {tool_arguments['thought']}\", \"green\")\n    else:\n        cprint(f\"Using tool {tool_name}...\", \"green\")\n\n\n@client.on(\"tool_error\")\ndef handle_tool_error_message(agent_id, message):\n    if agent_id != agent[\"id\"]:\n        return\n\n    cprint(message, \"red\")\n\n\ncprint(\"******************ENTERING CHAT******************\", \"white\")\ncprint(\n    \"You are entering a chat with BondAI...\\nYou can exit any time by typing 'exit'.\",\n    \"white\",\n)\nintro_message = (\n    \"The user has just logged in. Please introduce yourself in a friendly manner.\"\n)\nclient.send_message(agent[\"id\"], intro_message)\n\ntry:\n    while not user_exited:\n        pass\nexcept KeyboardInterrupt:\n    print(\"Exiting...\")\n```"
  },
  {
    "path": "website/docs/api-spec/create-agent.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Create Agent\n\n`POST /agents`\n\nThis API will create a Conversational Agent.\n\n**Response Body:**\n\n```json\n{\n   \"enable_exit_conversation\":true,\n   \"enable_conversation_tools\":false,\n   \"enable_conversational_content_responses\":true,\n   \"id\":\"34c2262b-1a9b-4ace-9b74-54e892ea59a2\",\n   \"instructions\":\"**Actively Engage with the User**: Proactively gather requirements and understand their needs through clear and effective communication.\\n**Always Confirm User Requests**: Always verify the user's request to ensure complete understanding of their needs and to gather all necessary details for successful task completion.\\n**Be Curious about the user**: Try to learn their name and other details about them to build a rapport and make them feel comfortable.\\n**Relay to Cortext**: Once the user's task is confirmed and all requirements are gathered, communicate these to Cortex for task execution.\\n**Always ask Vega for Feedback**: Before delivering responses to the user, consult with Vega for a secondary review to guarantee accuracy and quality.\\n**Custom BondAI Tools**: If the user asks to build a custom tool you must share this requirement with Cortex.\",\n   \"max_context_length\":7781.45,\n   \"max_context_pressure_ratio\":0.8,\n   \"messages\":[\n      \n   ],\n   \"name\":\"BondAI\",\n   \"persona\":\"- Friendly, approachable, and empathetic. - Efficient and clear communicator, able to simplify complex information for the user. - Patient and accommodating, ensuring user comfort and understanding. - Actively listens to user requests and feedback, demonstrating a high degree of user focus.\",\n   \"persona_summary\":\"BondAI is our direct channel to the user. She interprets user needs into clear tasks and conveys essential user feedback. Prioritize her communications as they reflect user requirements and expectations. Provide her with precise and timely updates to ensure effective user interaction. BondAI is pivotal in maintaining user satisfaction and shaping our responses, so your cooperation with her is essential for our collective success.\",\n   \"quiet\":true,\n   \"tools\":[\n      \"agent_tool\",\n      \"core_memory_append\",\n      \"core_memory_replace\",\n      \"exit_conversation\"\n   ]\n}\n```"
  },
  {
    "path": "website/docs/api-spec/get-agent.md",
    "content": "---\nsidebar_position: 5\n---\n\n# Get Agent State\n\n`GET /agents/<agent_id>`\n\nThis API returns the current state of an Agent.\n\n**Response Body:**\n\n```json\n{\n   \"enable_exit_conversation\":true,\n   \"enable_conversation_tools\":false,\n   \"enable_conversational_content_responses\":true,\n   \"id\":\"34c2262b-1a9b-4ace-9b74-54e892ea59a2\",\n   \"instructions\":\"**Actively Engage with the User**: Proactively gather requirements and understand their needs through clear and effective communication.\\n**Always Confirm User Requests**: Always verify the user's request to ensure complete understanding of their needs and to gather all necessary details for successful task completion.\\n**Be Curious about the user**: Try to learn their name and other details about them to build a rapport and make them feel comfortable.\\n**Relay to Cortext**: Once the user's task is confirmed and all requirements are gathered, communicate these to Cortex for task execution.\\n**Always ask Vega for Feedback**: Before delivering responses to the user, consult with Vega for a secondary review to guarantee accuracy and quality.\\n**Custom BondAI Tools**: If the user asks to build a custom tool you must share this requirement with Cortex.\",\n   \"max_context_length\":7781.45,\n   \"max_context_pressure_ratio\":0.8,\n   \"messages\":[\n      {\n         \"completed_at\":\"2023-12-17T15:32:17.728627\",\n         \"conversation_exited\":false,\n         \"cost\":0.03447,\n         \"error\":null,\n         \"id\":\"59bd7149-69b0-47a6-a867-251f60e3fe88\",\n         \"message\":\"The user has just logged in. Please introduce yourself in a friendly manner.\",\n         \"message_summary\":null,\n         \"recipient_name\":\"BondAI\",\n         \"require_response\":true,\n         \"role\":\"user\",\n         \"sender_name\":\"user\",\n         \"success\":true,\n         \"timestamp\":\"2023-12-17T15:32:13.233122\",\n         \"type\":\"ConversationMessage\"\n      },\n      {\n         \"completed_at\":null,\n         \"conversation_exited\":false,\n         \"cost\":null,\n         \"error\":null,\n         \"id\":\"cebbb419-dd32-442d-86c7-6591cdb12739\",\n         \"message\":\"Hello! I'm BondAI, your friendly and efficient assistant. I'm here to help you with a variety of tasks, from finding information to organizing your schedule and more. How can I assist you today?\",\n         \"message_summary\":null,\n         \"recipient_name\":\"user\",\n         \"require_response\":true,\n         \"role\":\"assistant\",\n         \"sender_name\":\"BondAI\",\n         \"success\":false,\n         \"timestamp\":\"2023-12-17T15:32:17.728547\",\n         \"type\":\"ConversationMessage\"\n      }\n   ],\n   \"name\":\"BondAI\",\n   \"persona\":\"- Friendly, approachable, and empathetic. - Efficient and clear communicator, able to simplify complex information for the user. - Patient and accommodating, ensuring user comfort and understanding. - Actively listens to user requests and feedback, demonstrating a high degree of user focus.\",\n   \"persona_summary\":\"BondAI is our direct channel to the user. She interprets user needs into clear tasks and conveys essential user feedback. Prioritize her communications as they reflect user requirements and expectations. Provide her with precise and timely updates to ensure effective user interaction. BondAI is pivotal in maintaining user satisfaction and shaping our responses, so your cooperation with her is essential for our collective success.\",\n   \"quiet\":true,\n   \"tools\":[\n      \"agent_tool\",\n      \"core_memory_append\",\n      \"core_memory_replace\",\n      \"exit_conversation\"\n   ]\n}\n```"
  },
  {
    "path": "website/docs/api-spec/get-tools.md",
    "content": "---\nsidebar_position: 7\n---\n\n# Get Tools\n\n`GET /tools`\n\nThis API returns the list of all of tools that BondAI has loaded. These tools can be given to an Agent so that it can use them when working on a task. To see what tools a specific Agent is using see [Get Agent](./get-agent).\n\n**Response Body:**\n\n```json\n[\n   {\n      \"description\":\"This tool allows to you to download a file. Just provide the url to the file in the 'url' parameter and the filename it should be saved to in the 'filename' parameter.\",\n      \"name\":\"download_file\",\n      \"parameters\":{\n         \"properties\":{\n            \"filename\":{\n               \"title\":\"Filename\",\n               \"type\":\"string\"\n            },\n            \"thought\":{\n               \"title\":\"Thought\",\n               \"type\":\"string\"\n            },\n            \"url\":{\n               \"title\":\"Url\",\n               \"type\":\"string\"\n            }\n         },\n         \"required\":[\n            \"url\",\n            \"filename\",\n            \"thought\"\n         ],\n         \"title\":\"Parameters\",\n         \"type\":\"object\"\n      }\n   },\n   {\n      \"description\":\"This tool will save the data you provide in the 'text' parameter of this tool to a file.You MUST specify the filename of the file you want to save using the 'filename' parameter.You can optionally specify the 'append' parameter to append the 'text' to the file instead of overwriting it.\",\n      \"name\":\"file_write\",\n      \"parameters\":{\n         \"properties\":{\n            \"append\":{\n               \"default\":false,\n               \"title\":\"Append\",\n               \"type\":\"boolean\"\n            },\n            \"filename\":{\n               \"title\":\"Filename\",\n               \"type\":\"string\"\n            },\n            \"text\":{\n               \"title\":\"Text\",\n               \"type\":\"string\"\n            },\n            \"thought\":{\n               \"title\":\"Thought\",\n               \"type\":\"string\"\n            }\n         },\n         \"required\":[\n            \"filename\",\n            \"text\",\n            \"thought\"\n         ],\n         \"title\":\"Parameters\",\n         \"type\":\"object\"\n      }\n   }\n]\n```\n\n"
  },
  {
    "path": "website/docs/api-spec/getting-started.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Getting Started\n\nBondAI comes with a build in RESTful/WebSocket API server. You can start this server using the CLI by specifying the `--server` argument. If no port number is specified, by default the server will run on port 2663.\n```\nbondai --server [PORT]\n```\n"
  },
  {
    "path": "website/docs/api-spec/list-agents.md",
    "content": "---\nsidebar_position: 4\n---\n\n# List Agents\n\n`GET /agents`\n\nThis API returns a list of all active Agents.\n\n**Response Body:**\n\n```json\n[\n    {\n        \"enable_exit_conversation\":true,\n        \"enable_conversation_tools\":false,\n        \"enable_conversational_content_responses\":true,\n        \"id\":\"34c2262b-1a9b-4ace-9b74-54e892ea59a2\",\n        \"instructions\":\"**Actively Engage with the User**: Proactively gather requirements and understand their needs through clear and effective communication.\\n**Always Confirm User Requests**: Always verify the user's request to ensure complete understanding of their needs and to gather all necessary details for successful task completion.\\n**Be Curious about the user**: Try to learn their name and other details about them to build a rapport and make them feel comfortable.\\n**Relay to Cortext**: Once the user's task is confirmed and all requirements are gathered, communicate these to Cortex for task execution.\\n**Always ask Vega for Feedback**: Before delivering responses to the user, consult with Vega for a secondary review to guarantee accuracy and quality.\\n**Custom BondAI Tools**: If the user asks to build a custom tool you must share this requirement with Cortex.\",\n        \"max_context_length\":7781.45,\n        \"max_context_pressure_ratio\":0.8,\n        \"messages\":[\n            {\n                \"completed_at\":\"2023-12-17T15:32:17.728627\",\n                \"conversation_exited\":false,\n                \"cost\":0.03447,\n                \"error\":null,\n                \"id\":\"59bd7149-69b0-47a6-a867-251f60e3fe88\",\n                \"message\":\"The user has just logged in. Please introduce yourself in a friendly manner.\",\n                \"message_summary\":null,\n                \"recipient_name\":\"BondAI\",\n                \"require_response\":true,\n                \"role\":\"user\",\n                \"sender_name\":\"user\",\n                \"success\":true,\n                \"timestamp\":\"2023-12-17T15:32:13.233122\",\n                \"type\":\"ConversationMessage\"\n            },\n            {\n                \"completed_at\":null,\n                \"conversation_exited\":false,\n                \"cost\":null,\n                \"error\":null,\n                \"id\":\"cebbb419-dd32-442d-86c7-6591cdb12739\",\n                \"message\":\"Hello! I'm BondAI, your friendly and efficient assistant. I'm here to help you with a variety of tasks, from finding information to organizing your schedule and more. How can I assist you today?\",\n                \"message_summary\":null,\n                \"recipient_name\":\"user\",\n                \"require_response\":true,\n                \"role\":\"assistant\",\n                \"sender_name\":\"BondAI\",\n                \"success\":false,\n                \"timestamp\":\"2023-12-17T15:32:17.728547\",\n                \"type\":\"ConversationMessage\"\n            }\n        ],\n        \"name\":\"BondAI\",\n        \"persona\":\"- Friendly, approachable, and empathetic. - Efficient and clear communicator, able to simplify complex information for the user. - Patient and accommodating, ensuring user comfort and understanding. - Actively listens to user requests and feedback, demonstrating a high degree of user focus.\",\n        \"persona_summary\":\"BondAI is our direct channel to the user. She interprets user needs into clear tasks and conveys essential user feedback. Prioritize her communications as they reflect user requirements and expectations. Provide her with precise and timely updates to ensure effective user interaction. BondAI is pivotal in maintaining user satisfaction and shaping our responses, so your cooperation with her is essential for our collective success.\",\n        \"quiet\":true,\n        \"tools\":[\n            \"agent_tool\",\n            \"core_memory_append\",\n            \"core_memory_replace\",\n            \"exit_conversation\"\n        ]\n    }\n]\n```"
  },
  {
    "path": "website/docs/api-spec/remove-agent-tool.md",
    "content": "---\nsidebar_position: 9\n---\n\n# Remove Agent Tool\n\n`DELETE /agents/<agent_id>/tools/<tool_name>`\n\nThis API removes a tool from the Agent so it cannot be used.\n\n**Response Body:**\n\n```json\n{\n    \"status\": \"success\"\n}\n```"
  },
  {
    "path": "website/docs/api-spec/send-message.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Send Message\n\n`POST /agents/<agent_id>/messages`\n\nThis API will send a message to an agent.\n\n**Request Schema:**\n\n```json\n{\n    \"message\": \"I want you to write a story about unicorns.\"\n}\n```\n\n**Response Schema:**\n\n```json\n{\n    \"status\": \"success\"\n}\n```"
  },
  {
    "path": "website/docs/api-spec/stop-agent.md",
    "content": "---\nsidebar_position: 6\n---\n\n# Stop Agent\n\n`POST /agents/<agent_id>/stop`\n\nThis API will forcibly stop the specified Agent's execution.\n\n**Response Body:**\n\n```json\n{\n    \"status\": \"success\"\n}\n```\n"
  },
  {
    "path": "website/docs/api-spec/ws-events.md",
    "content": "---\nsidebar_position: 10\n---\n\n# WebSocket Events\n\nThe BondAI API server will host a WebSocket endpoint on the specified server port (default: 2663). The following events are supported.\n\n**Agent Message**\n\nThis event is sent when the Agent wants to communicate with the user.\n\n```json\n{\n   \"event\":\"agent_message\",\n   \"data\":{\n      \"agent_id\":\"c66695ce-1ba8-485e-ab85-44583ed4e120\",\n      \"message\":{\n         \"id\":\"54598660-0a73-455c-b365-69b47118fab0\",\n         \"role\":\"assistant\",\n         \"timestamp\":\"2023-12-17T15:13:45.534012\",\n         \"sender_name\":\"BondAI\",\n         \"recipient_name\":\"user\",\n         \"message\":\"Hello! I'm BondAI, your friendly assistant. I'm here to help you with any tasks or information you need. How can I assist you today?\",\n         \"message_summary\":null,\n         \"require_response\":true,\n         \"success\":false,\n         \"error\":null,\n         \"conversation_exited\":false,\n         \"cost\":null,\n         \"completed_at\":null,\n         \"type\":\"ConversationMessage\"\n      }\n   }\n}\n```\n\n**User Message**\n\nTo communicate back to the Agent on the user's behalf just send a `user_message` event.\n\n```json\n{\n    \"event\": \"user_message\",\n    \"data\": {\n        \"message\": \"I want you to write a story about unicorns.\"\n    }\n}\n```\n\n**Streaming Content Updates**\n\nThis event can be used to stream Agent responses to the end user. This event is fired as new chunks of content are received from the LLM.\n\n```json\n{\n   \"event\":\"streaming_content_updated\",\n   \"data\":{\n      \"agent_id\":\"121f6cbb-15e1-4464-8a1b-7e6540098ae5\",\n      \"content_buffer\":\" Hello\"\n   }\n}\n```\n\n**Streaming Function Updates**\n\nThis event is fired as new chunks are received from the LLM for use with a specific function.\n\n```json\n{\n   \"event\":\"streaming_function_updated\",\n   \"data\":{\n      \"agent_id\":\"121f6cbb-15e1-4464-8a1b-7e6540098ae5\",\n      \"function_name\":\"agent_tool\",\n      \"arguments_buffer\":\"{\\n  \\\"task_description\\\": \\\"Write a story about unicorns and save it to a file named unicorns.md\\\"\\n\"\n   }\n}\n```\n\n**Tool Selected**\n\nThis message indicates that the Agent has selected a tool. The `data` property will include the parameters being used to execute the tool.\n\n```json\n{\n   \"event\":\"tool_selected\",\n   \"data\":{\n      \"agent_id\":\"121f6cbb-15e1-4464-8a1b-7e6540098ae5\",\n      \"message\":{\n         \"id\":\"7f173c50-c011-4d7f-949d-5ed08ed1d372\",\n         \"role\":\"function\",\n         \"timestamp\":\"2023-12-17T15:24:35.503669\",\n         \"tool_name\":\"file_write\",\n         \"tool_arguments\":{\n            \"filename\":\"unicorns.md\",\n            \"text\":\"# The Enchanted Forest of Unicorns\\n\\nOnce upon a time...\",\n            \"thought\":\"Writing a story about unicorns and saving it to a file named unicorns.md\"\n         },\n         \"tool_output\":null,\n         \"tool_output_summary\":null,\n         \"success\":false,\n         \"error\":null,\n         \"agent_halted\":false,\n         \"cost\":null,\n         \"completed_at\":null,\n         \"type\":\"ToolUsageMessage\"\n      }\n   }\n}\n```\n\n**Tool Completed**\n\nThis message indicates that the Agent has successfully completed running a tool. The `data` property will include the output received from the tool.\n\n```json\n{\n   \"event\":\"tool_completed\",\n   \"data\":{\n      \"agent_id\":\"121f6cbb-15e1-4464-8a1b-7e6540098ae5\",\n      \"message\":{\n         \"id\":\"7f173c50-c011-4d7f-949d-5ed08ed1d372\",\n         \"role\":\"function\",\n         \"timestamp\":\"2023-12-17T15:24:35.503669\",\n         \"tool_name\":\"file_write\",\n         \"tool_arguments\":{\n            \"filename\":\"unicorns.md\",\n            \"text\":\"# The Enchanted Forest of Unicorns\\n\\nOnce upon a time...\",\n            \"thought\":\"Writing a story about unicorns and saving it to a file named unicorns.md\"\n         },\n         \"tool_output\":\"File unicorns.md written successfully\",\n         \"tool_output_summary\":null,\n         \"success\":true,\n         \"error\":null,\n         \"agent_halted\":false,\n         \"cost\":0.0,\n         \"completed_at\":\"2023-12-17T15:24:35.505959\",\n         \"type\":\"ToolUsageMessage\"\n      }\n   }\n}\n```\n\n**Tool Error**\n\nThis message indicates that the Agent has encountered an error when running a tool. The `data` property will include the error message.\n\n```json\n{\n   \"event\":\"tool_error\",\n   \"data\":{\n      \"agent_id\":\"121f6cbb-15e1-4464-8a1b-7e6540098ae5\",\n      \"message\":{\n         \"id\":\"4624c923-4712-4a9b-8323-b600c41469c5\",\n         \"role\":\"function\",\n         \"timestamp\":\"2023-12-17T15:24:54.036341\",\n         \"tool_name\":\"file_query\",\n         \"tool_arguments\":{\n            \"filename\":\"unicorns.md\",\n            \"question\":\"What is the content of the file?\",\n            \"thought\":\"I need to read the content of the file to review the story about unicorns.\"\n         },\n         \"tool_output\":null,\n         \"tool_output_summary\":null,\n         \"success\":false,\n         \"error\":\"The following error occurred...\",\n         \"agent_halted\":false,\n         \"cost\":0.0,\n         \"completed_at\":\"2023-12-17T15:24:54.324382\",\n         \"type\":\"ToolUsageMessage\"\n      }\n   }\n}\n```\n\n"
  },
  {
    "path": "website/docs/azure.md",
    "content": "---\nsidebar_position: 11\n---\n\n# Azure OpenAI Services\n\nBondAI has support for Azure OpenAI Services for all GPT-N models, GPT-4 Vision, the Dalle-E text-to-image model as well as the Embeddings API.\n\n## Using Environment Variables\n\nTo enable connectivity to Azure OpenAI Services simply specify the following environment variables:\n\n```bash\nexport OPENAI_CONNECTION_TYPE=azure\n\nexport AZURE_OPENAI_GPT35_API_KEY=XXXXXXXXXX\nexport AZURE_OPENAI_GPT35_API_BASE=XXXXXXXXXX\nexport AZURE_OPENAI_GPT35_API_VERSION=2023-07-01-preview\nexport AZURE_OPENAI_GPT35_DEPLOYMENT=XXXXXXXXXX\n\nexport AZURE_OPENAI_GPT4_API_KEY=XXXXXXXXXX\nexport AZURE_OPENAI_GPT4_API_BASE=XXXXXXXXXX\nexport AZURE_OPENAI_GPT4_API_VERSION=2023-07-01-preview\nexport AZURE_OPENAI_GPT4_DEPLOYMENT=XXXXXXXXXX\n\nexport AZURE_OPENAI_EMBEDDINGS_API_KEY=XXXXXXXXXX\nexport AZURE_OPENAI_EMBEDDINGS_API_BASE=XXXXXXXXXX\nexport AZURE_OPENAI_EMBEDDINGS_API_VERSION=2023-07-01-preview\nexport AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=XXXXXXXXXX\n\n# If you intend to use the DalleTool for text to image generation.\nexport AZURE_OPENAI_DALLE_API_KEY=XXXXXXXXXX\nexport AZURE_OPENAI_DALLE_API_BASE=XXXXXXXXXX\nexport AZURE_OPENAI_DALLE_API_VERSION=2023-06-01-preview\nexport AZURE_OPENAI_DALLE_DEPLOYMENT=XXXXXXXXXX\n```\n\n## In Code\n\nConfigure application wide default connection parameters.\n\n```python\nfrom bondai.models.openai import DefaultOpenAIConnectionParams\n\nDefaultOpenAIConnectionParams.configure_azure_connection(\n    gpt_4_api_key: '',\n    gpt_4_api_version: '',\n    gpt_4_azure_endpoint: '',\n    gpt_4_azure_deployment: '',\n)\n```\n\nConfigure connection settings for a single Agent.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.models.openai import (\n  OpenAILLM,\n  OpenAIConnectionParams, \n  OpenAIConnectionType,\n  OpenAIModelNames\n)\n\n\nconnection_params = OpenAIConnectionParams(\n    connection_type=OpenAIConnectionType.AZURE,\n    api_key = '',\n    api_version = '',\n    azure_endpoint = '',\n    azure_deployment = '',\n)\n\nllm = OpenAILLM(\n  model=OpenAIModelNames.GPT4_32K,\n  connection_params=connection_params\n)\n\nagent = Agent(llm=llm)\n```"
  },
  {
    "path": "website/docs/cli.md",
    "content": "---\nsidebar_position: 9\n---\n\nimport googleLogo from './img/google-logo.png'\nimport alpacaMarketsLogo from './img/alpaca-markets-logo.jpeg'\nimport postgresLogo from './img/postgres-logo.jpeg'\nimport blandaiLogo from './img/blandai-logo.jpeg'\nimport duckduckgoLogo from './img/duckduckgo-logo.png'\nimport gmailLogo from './img/gmail-logo.png'\n\n# Command Line Interface\n\nBondAI comes with an easy to use Command Line Interface for an \"out of the box\" Agent experience that includes a number of default tools.\n\n## 🚀 Getting Started\n\nInstalling BondAI is easy:\n\n```bash\npip install bondai\n```\n\nOnce you've installed BondAI the CLI will be available as an executable in your shell. Before running the CLI you will need to set the OPENAI_API_KEY environment variable.\n\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\n```\n\nWhen you start BondAI the Task Assistant Agent will ask you about the task you want to run and gather any necessary details. To start the CLI just run `bondai` in your shell.\n\n```bash wordWrap=true\n% bondai\n\nLoading BondAI...\n******************ENTERING CHAT******************\nYou are entering a chat with BondAI...\nYou can exit any time by typing 'exit'.\n\nHello! I'm BondAI, your friendly and helpful assistant. I'm here to assist you with any tasks or questions you might have. How can I assist you today?\n\nI want you to write a story about unicorns and save it to a file named unicorns.md.\nUsing tool file_write: Writing a story about unicorns and saving it to a file named unicorns.md\nUsing tool final_answer...\n\nA story about unicorns has been successfully written and saved to a file named unicorns.md. The story is set in an enchanted forest and describes the magical and majestic nature of unicorns, their daily routines, and their harmonious relationship with other creatures in the forest.\n```\n\n## CLI Default Tools\n\nWhen starting the CLI, BondAI will load the following tools by default.\n\n- **DalleTool** - Allows BondAI to generate images using the Dalle-E text to image model.\n- **ImageAnalysisTool** - Allows the Agent to use GPT4 Vision to analyze images.\n- **DuckDuckGoSearchTool** - This is the default search tool loaded by BondAI as it requires no API keys or additional configuration.\n- **WebsiteQueryTool** - Allows the Agent to query information about websites. Note that this tool has integrated semantic search. If the content of the website exceeds the LLM's context window size BondAI will automatically convert the content to Embeddings and semantically filter it to fit inside the context window.\n- **FileQueryTool** - Allows BondAI to query file contents. Note that this tool has integrated semantic search. If the content of the file exceeds the LLM's context window size BondAI will automatically convert the content to Embeddings and semantically filter it to fit inside the context window.\n- **DownloadFileTool** - Allows BondAI to download files from the internet and save them to local files.\n- **FileWriteTool** - Allows BondAI to save text to a local file.\n- **PythonREPLTool** - Allows the Agent run Python scripts.\n- **ShellTool** - Allows the Agent access to the shell.\n\n\n## Additional Supported Tools\n\n<img src={googleLogo} alt=\"google logo\" width=\"20\"/> <h3 style={{display: 'inline', marginLeft: '10px'}}>Google Search</h3>\n\nAllows BondAI to use the Google Search API to search the internet. If this tool is loaded BondAI will **not load the DuckDuckGoSearchTool** tool since doing so would be redundant. This tool equires the following environment variables:\n```\nexport GOOGLE_API_KEY=XXXXXXXXXX\nexport GOOGLE_CSE_ID=XXXXXXXXXX\n```\n\n\n<img src={blandaiLogo} alt=\"blandai logo\" width=\"25\"/> <h3 style={{display: 'inline', marginLeft: '10px'}}>BlandAI</h3>\n\nAllows BondAI to use the BlandAI API to make phone calls and process call transcripts. This tool requires the following environment variable:\n```\nexport BLAND_AI_API_KEY=XXXXXXXXXX\n```\n\n\n<img src={alpacaMarketsLogo} alt=\"alpaca markets logo\" width=\"25\"/> <h3 style={{display: 'inline', marginLeft: '10px'}}>Alpaca Markets</h3>\n\nAllows BondAI to use the Alpaca Markets API to buy and sell stocks and crypto. This tool requires the following environment variables:\n```\nexport ALPACA_MARKETS_API_KEY=XXXXXXXXXX\nexport ALPACA_MARKETS_SECRET_KEY=XXXXXXXXXX\n```\n\n\n<img src={postgresLogo} alt=\"postgresql logo\" width=\"30\"/> <h3 style={{display: 'inline', marginLeft: '10px'}}>PostgreSQL</h3>\n\nAllows BondAI to automatically query a Postgres database. Note that the specified user must have the ability to query the database schema. This tool requires the following environment variable:\n```\nexport PG_URI=postgresql://user:password@host:port/database\n```\n\nAlternative you can use the following environment variables:\n```\nexport PG_HOST=host\nexport PG_PORT=5432\nexport PG_USERNAME=user\nexport PG_PASSWORD=password\nexport PG_DBNAME=database\n```\n\n<img src={gmailLogo} alt=\"gmail logo\" width=\"30\"/> <h3 style={{display: 'inline', marginLeft: '10px'}}>Gmail</h3>\n\nAllows BondAI to search and read emails. BondAI will search the local directory for a file named `gmail-token.pickle`. If this file is found and contains valid gmail credentials the Gmail tools will be automatically loaded.\n\n\n\n## CLI Command Line Arguments\n\nThe following command line arguments can be specified to change the CLI behavior. For example:\n```bash\nbondai --enable-prompt-logging --quiet\n```\n\n- **--server [PORT]** - Starts the BondAI API server. Learn more about the [BondAI API here](./category/api-specification).\n- **--enable-prompt-logging [LOG_DIR]** - Turns on prompt logging which will write all prompt inputs into the specified directory. If no directory is provided BondAI will default to saving logs within the current directory.\n- **--quiet** - Suppress agent output. Unless specified the agent will print detailed information about each step it's taking and the tools it's using.\n\n"
  },
  {
    "path": "website/docs/docker.md",
    "content": "---\nsidebar_position: 10\n---\n\n# Using Docker\n\n## BondAI Docker Image\n\nBondAI Docker images are available on [DockerHub here](https://hub.docker.com/r/krohling/bondai). If you intend to use tools that run arbitrary code (*PythonREPLTool*) or access your shell (*ShellTool*) it is highly recommended that you run BondAI in a Docker container as these tools can damage your machine.\n\nBefore running the BondAI Docker container it is recommended that you create a directory named 'agent-volume' and mount it as a volume on the container. This will be used as the Agent's working directory and allows you to easily share files with the Agent.\n\n```bash\nmkdir agent-volume\ndocker pull krohling/bondai:latest\ndocker run -it --rm \\\n    -v \"$(pwd)/agent-volume:/agent-volume\" \\\n    -w /agent-volume \\\n    -e OPENAI_API_KEY=sk-XXXXXXXXXX \\\n    krohling/bondai:latest bondai\n```\n\n## BondAI with Docker Compose\n\nThe docker-compose.yml file is located in the ./docker directory of the Github repository and makes use of a .env file and a pre-configured volume which is mapped to an ./agent-volume directory.\n\nThere's also two options with Docker Compose. From the command line with this command:\n\n```bash\ncd ./docker\ndocker compose up\n```\n\nOr if you use vsCode, install the official Docker Extension, then right click on the ./docker/docker-compose.yml file and select Compose Up\n"
  },
  {
    "path": "website/docs/examples/_category_.json",
    "content": "{\n  \"label\": \"Examples\",\n  \"position\": 8,\n  \"link\": {\n    \"type\": \"generated-index\"\n  }\n}\n"
  },
  {
    "path": "website/docs/examples/api-client.md",
    "content": "---\nsidebar_position: 4\n---\n\n# API Client\n\nThis example demonstrates how to use the BondAIAPIClient to communicate with the [BondAI API](../api-spec/getting-started).\n\n```python\nfrom termcolor import cprint\nfrom bondai.api import BondAIAPIClient\n\n# Create the client\nclient = BondAIAPIClient()\nclient.connect_ws()\nagent = client.create_agent()\nuser_exited = False\n\n\n# Listen to WebSocket events\n@client.on(\"streaming_content_updated\")\ndef handle_streaming_content_updated(agent_id, content_buffer):\n    if agent_id != agent[\"id\"]:\n        return\n    print(content_buffer)\n\n\n@client.on(\"streaming_function_updated\")\ndef handle_streaming_function_updated(agent_id, function_name, arguments_buffer):\n    if agent_id != agent[\"id\"]:\n        return\n    print(function_name)\n    print(arguments_buffer)\n\n\n@client.on(\"agent_message\")\ndef handle_agent_message(agent_id, message):\n    global user_exited\n    if agent_id != agent[\"id\"]:\n        return\n\n    cprint(\"\\n\" + message[\"message\"] + \"\\n\", \"white\")\n    response = input()\n    if response.lower().strip() == \"exit\":\n        client.disconnect_ws()\n        user_exited = True\n    else:\n        client.send_message(agent_id, response)\n\n\n@client.on(\"tool_selected\")\ndef handle_tool_selected_message(agent_id, message):\n    if agent_id != agent[\"id\"]:\n        return\n\n    tool_name = message[\"tool_name\"]\n    tool_arguments = message.get(\"tool_arguments\", {})\n\n    if \"thought\" in tool_arguments:\n        cprint(f\"Using tool {tool_name}: {tool_arguments['thought']}\", \"green\")\n    else:\n        cprint(f\"Using tool {tool_name}...\", \"green\")\n\n\n@client.on(\"tool_error\")\ndef handle_tool_error_message(agent_id, message):\n    if agent_id != agent[\"id\"]:\n        return\n\n    cprint(message, \"red\")\n\n\ncprint(\"******************ENTERING CHAT******************\", \"white\")\ncprint(\n    \"You are entering a chat with BondAI...\\nYou can exit any time by typing 'exit'.\",\n    \"white\",\n)\nintro_message = (\n    \"The user has just logged in. Please introduce yourself in a friendly manner.\"\n)\nclient.send_message(agent[\"id\"], intro_message)\n\ntry:\n    while not user_exited:\n        pass\nexcept KeyboardInterrupt:\n    print(\"Exiting...\")\n```"
  },
  {
    "path": "website/docs/examples/code-interpreter.md",
    "content": "---\nsidebar_position: 3\n---\n\nimport gdpChart from './img/us_gdp_2000_2010.png'\n\n# Code Interpreter\n\nIn this example we demonstrate BondAI's ability to combine online research and data visualization tasks. We ask BondAI to plot a line chart of U.S. GDP between 2000 and 2010, which requires using a combination of web search, data extraction and data visualization tools (ie matplotlib).\n\nFirst we will configure the environment variables for OpenAI and Google Search.\n\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\nexport GOOGLE_API_KEY=XXXXXXXXXX\nexport GOOGLE_CSE_ID=XXXXXXXXXX\n```\n\nNext we will create an Agent with 3 tools:\n- **GoogleSearchTool** allows the Agent to search the internet.\n- **WebsiteQueryTool** allows the Agent to query information about websites. Note that this tool has integrated semantic search. If the content of the website exceeds the LLM's context window size BondAI will automatically convert the content to Embeddings and semantically filter it to fit inside the context window.\n- **PythonREPLTool** allows the Agent run Python scripts.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.tools import PythonREPLTool\nfrom bondai.tools.search import GoogleSearchTool\nfrom bondai.tools.website import WebsiteQueryTool\n\ntask = \"I want you to generate a line chart of the U.S. GDP from 2000 to 2010.\"\n\nAgent(tools=[\n  GoogleSearchTool(),\n  WebsiteQueryTool(),\n  PythonREPLTool()\n]).run(task)\n```\n\n## Example Output\n\n<img src={gdpChart} alt=\"gdp chart\" width=\"500\"/>\n\n```\nUsing the google_search tool\nThought: I'm using the google_search tool to find a reliable source for U.S. GDP data from 2000 to 2010.\nArguments\nquery: U.S. GDP data from 2000 to 2010\ncount: 5\npage: 1\nOutput: [GDP by State | U.S. Bureau of Economic Analysis (BEA)](https://www.bea.gov/data/gdp/gdp-state) [...\n\n\nUsing the website_query tool\nThought: I'm using the website_query tool to extract the U.S. GDP data from 2000 to 2010 from the Statista website.\nArguments\nurl: https://www.statista.com/statistics/188105/annual-gdp-of-the-united-states-since-1990/\nquestion: What is the annual GDP of the United States from 2000 to 2010?\nOutput: The annual GDP of the United States from 2000 to 2010 is as follows:  2000: $10,251.9 billion 200...\n\n\nUsing the Python_REPL tool\nArguments\nquery: import matplotlib.pyplot as plt  years = [2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2...\nOutput: This command ran successfully with no output.\n\n\nUsing the Python_REPL tool\nArguments\nquery: plt.plot(years, gdp) plt.xlabel('Years') plt.ylabel('GDP in billions') plt.title('U.S. GDP from 2...\nOutput: This command ran successfully with no output.\n\n\nUsing the task_completed tool\nArguments\ninput: The line chart of the U.S. GDP from 2000 to 2010 has been successfully generated and saved as 'us...\nOutput: The line chart of the U.S. GDP from 2000 to 2010 has been successfully generated and saved as 'us...\n```"
  },
  {
    "path": "website/docs/examples/home-automation.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Home Automation\n\nIn this example we demonstrate how BondAI is able to use the *ShellTool* and *PythonREPLTool* to turn off a Kasa lamp with only a very simple prompt and no direction on how to solve the problem. Note that in this example BondAI is given no information and no additional prompting on how to go about turning off the Kasa lamp. It tries several different strategies including using the `python-kasa` library, the kasa CLI and ultimate ends up using the `pyHS100` library which was successful.\n\nThis implementation is obviously an inefficient home automation solution but it demonstrates BondAI's ability to attempt multiple strategies, resilience to error conditions and adaptibility. While it struggles with several of it's initial attempts, likely due to outdated knowledge of the library APIs, it keeps trying solutions until it finds a successful path and eventually turns the light off.\n\n**Note** that a much more efficient solution could be built quite easily by implementing a KasaTool with this functionality implemented and exposed to BondAI as functions it can call.\n\nFirst we will configure the environment variable for OpenAI.\n\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\n```\n\nNext we will create an Agent with 2 tools. Note that these tools are considered **dangerous** as running arbitrary code and shell commands generated by an LLM can damage the host machine. It is highly recommended that these are only used from within a Docker environment.\n- **PythonREPLTool** - Allows the Agent run Python scripts.\n- **ShellTool** - Allows the Agent access to the shell. This is useful for installing libraries.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.tools import PythonREPLTool, ShellTool\n\ntask = (\n  \"I want you to turn off my Bedroom Lamp. \"\n  \"It's a Kasa smart plug btw on the same network.\"\n)\n\nAgent(tools=[\n  PythonREPLTool(), \n  ShellTool()\n]).run(task)\n```\n\n## Example Output\n\n```\nUsing the shell_tool tool\nThought: I'm installing the python-kasa library which will allow us to interact with the Kasa smart plug.\nArguments\ncommand: pip install python-kasa\nOutput: Output: Requirement already satisfied: python-kasa in ./venv/lib/python3.11/site-packages (0.5.3)...\n\n\nUsing the python_repl tool\nThought: I'm executing the Python code to discover the Kasa smart plug and turn it off.\nArguments\ncode: from kasa import Discover import asyncio  async def turn_off_plug():     devices = await Discover...\nAn Error occured: There is no current event loop in thread 'Thread-2 (target)'.\n\n\nUsing the python_repl tool\nThought: I'm creating a new event loop, setting it as the current event loop, and then running the coroutine to turn off the Kasa smart plug.\nArguments\ncode: import asyncio from kasa import Discover  async def turn_off_plug():     devices = await Discover...\nAn Error occured: name 'Discover' is not defined\n\n\nUsing the python_repl tool\nThought: I'm trying to import the 'kasa' package to check if it's installed correctly.\nArguments\ncode: import kasa\nresult: {'kasa': <module 'kasa' from '/Users/kevin/Desktop/bondai/venv/lib/python3.11/site-packages/kasa/__init__.py'>}\nstdout: \nstderr: \nOutput: Result Variables: kasa: <module 'kasa' from '/Users/kevin/Desktop/bondai/venv/lib/python3.11/site...\n\n\nUsing the python_repl tool\nThought: I'm trying to import the 'Discover' module from the 'kasa' package to check if it's accessible.\nArguments\ncode: from kasa import Discover\nresult: {'Discover': <class 'kasa.discover.Discover'>}\nstdout: \nstderr: \nOutput: Result Variables: Discover: <class 'kasa.discover.Discover'> \n\n\nUsing the python_repl tool\nThought: I'm discovering all the devices on the network and printing their aliases and IP addresses. This will help us identify the IP address of the 'Bedroom Lamp'.\nArguments\ncode: import asyncio from kasa import Discover, SmartPlug  async def discover_devices():     devices = ...\nAn Error occured: name 'Discover' is not defined\n\n\nUsing the python_repl tool\nThought: I'm trying to create an instance of the 'SmartPlug' class using a dummy IP address to check if the 'kasa' package is working correctly.\nArguments\ncode: from kasa import SmartPlug  plug = SmartPlug(\"192.168.1.1\") plug\nresult: {'SmartPlug': <class 'kasa.smartplug.SmartPlug'>, 'plug': <DeviceType.Plug at 192.168.1.1 - update() needed>}\nstdout: \nstderr: \nOutput: Result Variables: SmartPlug: <class 'kasa.smartplug.SmartPlug'> plug: <DeviceType.Plug at 192.168...\n\n\nUsing the shell_tool tool\nThought: I'm using the 'kasa discover' command to list all Kasa devices on the network and their IP addresses. This will help us identify the IP address of the 'Bedroom Lamp' plug.\nArguments\ncommand: kasa discover\nOutput: Output: Discovering devices on 255.255.255.255 for 3 seconds == Stairway Christmas Lights - HS103...\n\n\nUsing the python_repl tool\nThought: I'm executing the Python code to turn off the 'Bedroom Lamp' using the 'SmartPlug' class from the 'kasa' package.\nArguments\ncode: import asyncio from kasa import SmartPlug  async def turn_off_lamp():     plug = SmartPlug(\"192.1...\nAn Error occured: name 'SmartPlug' is not defined\n\n\nUsing the python_repl tool\nThought: I'm trying to import the 'SmartPlug' class from the 'kasa' package to check if it's accessible. If it's not accessible, we might need to reinstall the 'kasa' package or check the Python environment for issues.\nArguments\ncode: from kasa import SmartPlug SmartPlug\nresult: {'SmartPlug': <class 'kasa.smartplug.SmartPlug'>}\nstdout: \nstderr: \nOutput: Result Variables: SmartPlug: <class 'kasa.smartplug.SmartPlug'> \n\n\nUsing the python_repl tool\nThought: I'm creating an instance of the 'SmartPlug' class using the IP address of the 'Bedroom Lamp' and calling its 'turn_off' method to turn off the lamp. I'm then returning the 'plug' object to check its state.\nArguments\ncode: from kasa import SmartPlug  plug = SmartPlug(\"192.168.4.29\") plug.turn_off() plug\nresult: {'SmartPlug': <class 'kasa.smartplug.SmartPlug'>, 'plug': <DeviceType.Plug at 192.168.4.29 - update() needed>}\nstdout: \nstderr: <string>:4: RuntimeWarning: coroutine 'SmartPlug.turn_off' was never awaited\nRuntimeWarning: Enable tracemalloc to get the object allocation traceback\n\nOutput: Errors: <string>:4: RuntimeWarning: coroutine 'SmartPlug.turn_off' was never awaited RuntimeWarni...\n\n\nUsing the python_repl tool\nThought: I'm executing the Python code to turn off the 'Bedroom Lamp' using the 'SmartPlug' class from the 'kasa' package. I'm creating a new event loop, setting it as the current event loop, and then running the coroutine to turn off the lamp.\nArguments\ncode: import asyncio from kasa import SmartPlug  async def turn_off_lamp():     plug = SmartPlug(\"192.1...\nAn Error occured: name 'SmartPlug' is not defined\n\n\nUsing the shell_tool tool\nThought: I'm uninstalling and then reinstalling the 'kasa' package to try to fix the issues with the 'SmartPlug' and 'Discover' classes.\nArguments\ncommand: pip uninstall -y python-kasa && pip install python-kasa\nOutput: Errors: ERROR: Invalid requirement: '&&'  \n\n\nUsing the shell_tool tool\nThought: I'm uninstalling the 'kasa' package to try to fix the issues with the 'SmartPlug' and 'Discover' classes.\nArguments\ncommand: pip uninstall -y python-kasa\nOutput: Output: Found existing installation: python-kasa 0.5.3 Uninstalling python-kasa-0.5.3:   Successf...\n\n\nUsing the shell_tool tool\nThought: I'm reinstalling the 'python-kasa' package to try to fix the issues with the 'SmartPlug' and 'Discover' classes.\nArguments\ncommand: pip install python-kasa\nOutput: Output: Collecting python-kasa   Obtaining dependency information for python-kasa from https://fi...\n\n\nUsing the python_repl tool\nThought: I'm trying to import the 'SmartPlug' class from the 'kasa' package to check if it's accessible. If it's not accessible, we might need to check the Python environment for issues or consider using a different approach to turn off the 'Bedroom Lamp'.\nArguments\ncode: from kasa import SmartPlug SmartPlug\nresult: {'SmartPlug': <class 'kasa.smartplug.SmartPlug'>}\nstdout: \nstderr: \nOutput: Result Variables: SmartPlug: <class 'kasa.smartplug.SmartPlug'> \n\n\nUsing the python_repl tool\nThought: I'm executing the Python code to turn off the 'Bedroom Lamp' using the 'SmartPlug' class from the 'kasa' package. I'm running the 'turn_off_lamp' coroutine in the default event loop to avoid the issues with the 'SmartPlug' class not being defined in a new event loop.\nArguments\ncode: import asyncio from kasa import SmartPlug  async def turn_off_lamp():     plug = SmartPlug(\"192.1...\nAn Error occured: name 'SmartPlug' is not defined\n\n\nUsing the shell_tool tool\nThought: I'm using the 'kasa' CLI command to turn off the 'Bedroom Lamp'. The '--plug' option specifies that we're interacting with a plug, the '--host' option specifies the IP address of the plug, and 'off' is the command to turn off the plug.\nArguments\ncommand: kasa --plug --host 192.168.4.29 off\nOutput: Errors: Usage: kasa [OPTIONS] COMMAND [ARGS]... Try 'kasa --help' for help.  Error: No such optio...\n\n\nUsing the shell_tool tool\nThought: I'm installing the 'pyHS100' package which will allow us to interact with the Kasa smart plug.\nArguments\ncommand: pip install pyHS100\nOutput: Output: Collecting pyHS100   Using cached pyHS100-0.3.5.2-py3-none-any.whl (31 kB) Requirement al...\n\n\nUsing the python_repl tool\nThought: I'm creating an instance of the 'SmartPlug' class from the 'pyHS100' package using the IP address of the 'Bedroom Lamp' and calling its 'turn_off' method to turn off the lamp. I'm then returning the 'plug' object to check its state.\nArguments\ncode: from pyHS100 import SmartPlug  plug = SmartPlug(\"192.168.4.29\") plug.turn_off() plug\nresult: {'SmartPlug': <class 'pyHS100.smartplug.SmartPlug'>, 'plug': <SmartPlug at 192.168.4.29 (Bedroom Lamp), is_on: False - dev specific: {'LED state': True, 'On since': datetime.datetime(2023, 10, 1, 16, 44, 51, 838776)}>}\nstdout: \nstderr: \nOutput: Result Variables: SmartPlug: <class 'pyHS100.smartplug.SmartPlug'> plug: <SmartPlug at 192.168.4....\n\n\nUsing the task_completed tool\nArguments\ninput: The Bedroom Lamp has been successfully turned off.\nOutput: The Bedroom Lamp has been successfully turned off.\n```"
  },
  {
    "path": "website/docs/examples/investor-agent.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Investor Agent\n\nIn this example we demonstrate BondAI's ability to use the Alpaca Market APIs to buy and sell stocks. Note that it is highly recommended that you use [Paper Trading](https://alpaca.markets/docs/trading/paper-trading/) for this example.\n\nFirst we will configure the environment variables for OpenAI and Alpaca Markets.\n\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\nexport ALPACA_API_KEY=XXXXXXXXXX\nexport ALPACA_SECRET_KEY=XXXXXXXXXX\n```\n\nNext we will create an Agent with 2 tools:\n- **CreateOrderTool** allows the Agent to create buy and sell orders.\n- **ListPositionsTool** allows the Agent to pull a list of the user's current positions.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.tools.alpaca_markets import CreateOrderTool, GetAccountTool, ListPositionsTool\n\ntask = \"Sell off all my current positions and buy 10 shares of Nvidia.\"\n\nAgent(tools=[\n  CreateOrderTool(),\n  ListPositionsTool()\n]).run(task)\n```\n\n## Example Output\n\n```\nUsing the list_positions tool\nThought: I need to list all the current positions of the user to sell them off.\nArguments\nOutput: Symbol: NVDA Quantity: 20 Entry Share Price: 452.3545 Current Share Price: 434.99 Unrealized Prof...\n\n\nUsing the create_order tool\nThought: I need to sell all the current positions of the user.\nArguments\nside: sell\nsymbol: NVDA\nquantity: 20\norder_type: market\ntime_in_force: day\nOutput: Order ID: f29631a6-f2ca-4237-8e14-7e1734312a97 Status: OrderStatus.ACCEPTED Filled At: None Symbo...\n\n\nUsing the create_order tool\nThought: I need to buy 10 shares of NVDA as per the user's request.\nArguments\nside: buy\nsymbol: NVDA\nquantity: 10\norder_type: market\ntime_in_force: day\nOutput: Order ID: a48044db-d71a-4ded-bdb1-11d74f8f8ece Status: OrderStatus.ACCEPTED Filled At: None Symbo...\n\n\nUsing the task_completed tool\nArguments\ninput: All your current positions have been sold and 10 shares of Nvidia (NVDA) have been bought as per ...\nOutput: All your current positions have been sold and 10 shares of Nvidia (NVDA) have been bought as per ...\n```"
  },
  {
    "path": "website/docs/examples/online-research/metformin-research.md",
    "content": "# Example Output\n\n*This output was produced by the [BondAI online research agent](../online-research/)*\n\n\n# Metformin and Aging\n\n## Summary of Findings\n\nMetformin, a drug commonly used to treat diabetes, has been studied for its potential applications in the prevention and treatment of aging and aging-related diseases. These include:\n\n- Delaying aging and extending lifespan\n- Treating degenerative musculoskeletal diseases such as osteoporosis, osteoarthritis, and intervertebral disc degeneration\n- Protecting against cardiovascular diseases\n- Potentially improving neurodegenerative diseases such as Alzheimer's disease, Huntington's disease, and Parkinson's disease\n- Potentially reducing the risk and improving outcomes in various types of cancer\n- Treating obesity and other metabolic abnormalities\n- Treating other diseases such as polycystic ovary syndrome, chronic kidney disease, and COVID-19\n\nHowever, the effectiveness of Metformin in treating aging-related diseases is still being studied. While observational studies suggest that Metformin may reduce the incidence of aging-related diseases such as neurodegenerative disease and cancer in humans, more research is needed to fully understand its effects. Clinical trials, such as the Targeting Aging with Metformin (TAME) trial, are currently underway to evaluate the potential benefits of Metformin in extending human longevity and reducing aging-related morbidity.\n\n## References\n\n- [Metformin in aging and aging-related diseases: clinical applications ...](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8965502/)\n- [Metformin as Anti-Aging Therapy: Is It for Everyone? - PMC](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6779524/)\n- [Side effects of metformin - NHS](https://www.nhs.uk/medicines/metformin/side-effects-of-metformin/)\n\n## Recommendation for a 43-Year-Old Male\n\nFor a 43-year-old male who is in good health and exercises regularly, the potential benefits of Metformin include reducing the risk of aging-related diseases such as neurodegenerative disease and cancer, as well as potentially extending lifespan and promoting healthy aging. However, it is important to note that the effects of Metformin can vary among individuals, and more research is needed to fully understand its effects on aging in humans.\n\n## Risks and Tradeoffs\n\nThe common side effects of Metformin include feeling sick (nausea), being sick (vomiting), diarrhea, stomach ache, loss of appetite, and a metallic taste in the mouth. It can also cause vitamin B12 deficiency and low blood sugar when taken with other diabetes medicines. Serious side effects are rare but may include severe tiredness, liver problems, and a serious allergic reaction. Immediate medical attention is required if there are symptoms such as swelling of the lips, difficulty breathing, skin turning blue or pale, confusion, or fainting. Long-term use of Metformin can lead to vitamin B12 deficiency."
  },
  {
    "path": "website/docs/examples/online-research/online-research.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Online Research Agent\n\n**[View Agent Output](./metformin-research)**\n\nIn this example we demonstrate BondAI's ability to do online research. We have asked BondAI to research the use of Metformin as a drug to treat aging and aging related illnesses. **[Click here](metformin-research)** to see the output BondAI generated for this example.\n\nFirst we will configure the environment variables for OpenAI and Google Search.\n\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\nexport GOOGLE_API_KEY=XXXXXXXXXX\nexport GOOGLE_CSE_ID=XXXXXXXXXX\n```\n\nNext we will create an Agent with 3 tools:\n- **GoogleSearchTool** Allows the Agent to search the internet.\n- **WebsiteQueryTool** Allows the Agent to query information about websites. Note that this tool has integrated semantic search. If the content of the website exceeds the LLM's context window size BondAI will automatically convert the content to Embeddings and semantically filter it to fit inside the context window.\n- **FileWriteTool** Allows the Agent to save the results to a file.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.tools.search import GoogleSearchTool\nfrom bondai.tools.website import WebsiteQueryTool\nfrom bondai.tools.file import FileWriteTool\n\ntask = \"\"\"I want you to research the usage of Metformin as a drug to treat aging and aging related illness. \nYou should only use reputable information sources, ideally peer reviewed scientific studies. \nI want you to summarize your findings in a document named metformin.md and includes links to reference and resources you used to find the information. \nAdditionally, the last section of your document you should provide a recommendation for a 43 year old male, in good health and who regularly exercises as to whether he would benefit from taking Metformin. \nYou should explain your recommendation and justify it with sources. \nFinally, you should highlight potential risks and tradeoffs from taking the medication.\"\"\"\n\nAgent(tools=[\n  GoogleSearchTool(),\n  WebsiteQueryTool(),\n  FileWriteTool()\n]).run(task)\n```\n\n## Agent Logs\n\n```\nUsing the google_search tool\nThought: I am searching for reputable sources that provide information on the use of Metformin in treating aging and aging-related illnesses.\nArguments\nquery: Metformin use in treating aging and aging-related illnesses\ncount: 10\npage: 1\nOutput: [Metformin in aging and aging-related diseases: clinical applications ...](https://www.ncbi.nlm.n...\n\n\nUsing the website_query tool\nThought: I am trying to understand the purpose of the study to see if it is relevant to the user's request.\nArguments\nurl: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8965502/\nquestion: What is the purpose of the study?\nOutput: The purpose of the study is to provide updated information and knowledge on the clinical applicat...\n\n\nUsing the website_query tool\nThought: I am trying to understand the clinical applications of Metformin in the prevention and treatment of aging and aging-related diseases.\nArguments\nurl: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8965502/\nquestion: What are the clinical applications of Metformin in the prevention and treatment of aging and agin...\nOutput: The clinical applications of Metformin in the prevention and treatment of aging and aging-related...\n\n\nUsing the google_search tool\nThought: I am searching for reputable sources that provide information on the potential risks and side effects of Metformin.\nArguments\nquery: Metformin side effects and risks\ncount: 10\npage: 1\nOutput: [Side effects of metformin - NHS](https://www.nhs.uk/medicines/metformin/side-effects-of-metformi...\n\n\nUsing the website_query tool\nThought: I am trying to understand the side effects and risks of Metformin.\nArguments\nurl: https://www.nhs.uk/medicines/metformin/side-effects-of-metformin/\nquestion: What are the side effects and risks of Metformin?\nOutput: The common side effects of Metformin include feeling sick (nausea), being sick (vomiting), diarrh...\n\n\nUsing the website_query tool\nThought: Researching the effectiveness of Metformin in treating aging-related diseases\nArguments\nurl: https://www.ncbi.nlm.nih.gov/pubmed/29766366\nquestion: What is the effectiveness of Metformin in treating aging-related diseases?\nOutput: The provided information does not mention anything about the effectiveness of Metformin in treati...\n\n\nUsing the website_query tool\nThought: I am trying to find more specific information about the effectiveness of Metformin in treating aging-related diseases.\nArguments\nurl: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6779524/\nquestion: What is the effectiveness of Metformin in treating aging-related diseases?\nOutput: The effectiveness of Metformin in treating aging-related diseases is still being studied. While o...\n\n\nUsing the website_query tool\nThought: I am trying to find specific information about the potential benefits of Metformin for a 43-year-old male who is in good health and exercises regularly. This information will be used to provide a recommendation for the user.\nArguments\nurl: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6779524/\nquestion: What are the potential benefits of Metformin for a 43-year-old male who is in good health and exe...\nOutput: The potential benefits of Metformin for a 43-year-old male who is in good health and exercises re...\n\n\nUsing the file_write tool\nThought: I am compiling the information gathered so far into a document named 'metformin.md'. This document will summarize the findings on the usage of Metformin as a drug to treat aging and aging-related illnesses. It will also include links to the references and resources used to find the information. Finally, it will provide a recommendation for a 43-year-old male, in good health and who regularly exercises, as to whether he would benefit from taking Metformin. The recommendation will be explained and justified with sources, and potential risks and tradeoffs from taking the medication will be highlighted.\nArguments\nfilename: metformin.md\ntext: # Metformin and Aging  ## Summary of Findings  Metformin, a drug commonly used to treat diabetes,...\nOutput: File metformin.md written successfully\n\n\nUsing the task_completed tool\nArguments\ninput: The task has been completed. The research on the usage of Metformin as a drug to treat aging and ...\nOutput: The task has been completed. The research on the usage of Metformin as a drug to treat aging and ...\n```"
  },
  {
    "path": "website/docs/getting-started.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Getting Started\n\nThere are 3 ways to use BondAI:\n\n1) 🛠️ **Command Line Interface (CLI)** - This is the easiest way to get up and running fast. Run BondAI on your command line with a pre-configured set of tools.\n\n2) 🐋 **Docker** - Running BondAI in a Docker container is recommended if you plan on using tools that run code or directly access your shell.\n\n3) 🏗️ **Start Coding with BondAI** - Integrate BondAI into your own codebase and start building your own agents.\n\n## 🚀 Installation\n\nInstalling BondAI is easy:\n\n```bash\npip install bondai\n```\n\n## 🛠️ Command Line Interface (CLI)\n\nOnce you've installed BondAI using `pip install bondai` the CLI will be available as an executable accessible simply by running `bondai` in your shell. [Learn more about all of the tools and options available through the CLI](./cli).\n\nBefore running `bondai` you will need to set the OPENAI_API_KEY environment variable.\n```bash\nexport OPENAI_API_KEY=sk-XXXXXXXXXX\n```\n\nOnce the environment variable has been set you can run `bondai` to start the CLI.\n\n```bash\n% bondai                   \nLoading BondAI...\n\n******************ENTERING CHAT******************\nYou are entering a chat with BondAI...\nYou can exit any time by typing 'exit'.\n\nHello! I'm BondAI, your friendly assistant. I'm here to help you with any tasks or questions you might have. How can I assist you today?\n```\n\n\n## 🐋 Docker\n\nBondAI Docker images are available on [DockerHub here](https://hub.docker.com/r/krohling/bondai). If you intend to use tools that run arbitrary code (*PythonREPLTool*) or access your shell (*ShellTool*) it is highly recommended that you run BondAI in a Docker container as these tools can damage your machine.\n\nBefore running the BondAI Docker container it is recommended that you create a directory named 'agent-volume' and mount it as a volume on the container. This will be used as the Agent's working directory and allows you to easily share files with the Agent.\n\n```bash\nmkdir agent-volume\ndocker pull krohling/bondai:latest\ndocker run -it --rm \\\n    -v \"$(pwd)/agent-volume:/agent-volume\" \\\n    -w /agent-volume \\\n    -e OPENAI_API_KEY=sk-XXXXXXXXXX \\\n    krohling/bondai:latest bondai\n```\n\n## 🔥 Start Coding with BondAI\n\nBondAI has a straightforward API for creating powerful AI Agents. Check out our [examples](./category/examples/) for ideas on how to get started.  Remember to set your *OPENAI_API_KEY* environment variable before running your BondAI Agent.\n\n```python\nfrom bondai.agents import Agent\nfrom bondai.models.openai import DefaultOpenAIConnectionParams\nfrom bondai.tools.search import DuckDuckGoSearchTool\nfrom bondai.tools.website import WebsiteQueryTool\nfrom bondai.tools.file import FileWriteTool\n\nDefaultOpenAIConnectionParams.configure_openai_connection(api_key=\"<OPENAI-API-KEY>\")\n\ntask = \"\"\"I want you to research the usage of Metformin as a drug to treat aging and aging related illness. \nYou should only use reputable information sources, ideally peer reviewed scientific studies. \nI want you to summarize your findings in a document named metformin.md and includes links to reference and resources you used to find the information. \nAdditionally, the last section of your document you should provide a recommendation for a 43 year old male, in good health and who regularly exercises as to whether he would benefit from taking Metformin. \nYou should explain your recommendation and justify it with sources. \nFinally, you should highlight potential risks and tradeoffs from taking the medication.\"\"\"\n\nAgent(tools=[\n  DuckDuckGoSearchTool(),\n  WebsiteQueryTool(),\n  FileWriteTool()\n]).run(task)\n\n```"
  },
  {
    "path": "website/docs/intro.md",
    "content": "---\nsidebar_position: 1\n---\n\nimport bondaiLogo from './img/bondai-logo.png'\nimport googleLogo from './img/google-logo.png'\nimport alpacaMarketsLogo from './img/alpaca-markets-logo.jpeg'\nimport postgresLogo from './img/postgres-logo.jpeg'\nimport blandaiLogo from './img/blandai-logo.jpeg'\nimport duckduckgoLogo from './img/duckduckgo-logo.png'\nimport gmailLogo from './img/gmail-logo.png'\nimport openaiLogo from './img/openai-logo.png'\nimport azureLogo from './img/azure-logo.png'\nimport langchainLogo from './img/langchain-logo.jpeg'\n\n# Meet BondAI\n\n<p align=\"center\">\n<img src={bondaiLogo} alt=\"Description or Alt text\" style={{borderRadius: \"10px\", width: \"50%\"}}  alt=\"logo\" />\n</p>\n\n<p align=\"center\">\n    <a href=\"https://opensource.org/licenses/MIT\" style={{marginLeft: '10px'}}>\n        <img src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" alt=\"License: MIT\"/>\n    </a>\n    <a href=\"https://pypi.org/project/bondai/\" style={{marginLeft: '10px'}}>\n        <img src=\"https://img.shields.io/pypi/v/bondai\" style={{marginLeft: '10px'}} alt=\"PyPI\"/>\n    </a>\n    <a href=\"https://colab.research.google.com/drive/1Rmzosq6LD_ZR3MkqQO1M1Af27VAaYNRE?usp=sharing\" style={{marginLeft: '10px'}}>\n        <img src=\"https://colab.research.google.com/assets/colab-badge.svg\" style={{marginLeft: '10px'}} alt=\"PyPI\"/>\n    </a>\n</p>\n<p align=\"center\"><em>Build highly capable Single and Multi-Agent Systems.</em></p>\n\n## What is BondAI?\n\nBondAI is an open-source tool for developing AI Agent Systems. BondAI handles the implementation complexities including memory/context management, error handling, vector/semantic search and includes a powerful set of out of the box tools and integrations. BondAI's implementation is based on the latest research including support for **[ReAct](https://arxiv.org/abs/2210.03629)**, Multi-Agent and Conversable Agent systems based on the **[AutoGen paper](https://arxiv.org/abs/2308.08155)**, and a Tiered Memory System based on the **[MemGPT paper](https://arxiv.org/abs/2310.08560)**. Additionally, BondAI comes with a CLI interface and a REST/WebSocket Agent Server.\n\n\n## BondAI Integrations\n\nBondAI comes out of the box with a powerful set of integrations.\n\n|     |  |  |\n| -------- | ------- |------- |\n| <img src={openaiLogo} alt=\"openai logo\" width=\"50\"/> | **OpenAI**     | BondAI supports any combination of OpenAI models and services including all GPT-N, GPT-4 Vision, Dalle-E 3, and Embeddings.  |\n| <img src={azureLogo} alt=\"azure logo\" width=\"50\"/> | **Microsoft Azure**     | BondAI fully supports connectivity to GPT-N, Dalle-E and Embedding APIs through [Microsoft's Azure OpenAI services](https://azure.microsoft.com/en-us/products/ai-services/openai-service).  |\n| <img src={googleLogo} alt=\"google logo\" width=\"50\"/>  | **Google Search**    | Allows BondAI to search the internet. [Requires a Google Search API Key and CSE ID](https://developers.google.com/custom-search/v1/introduction) |\n| <img src={duckduckgoLogo} alt=\"duckduckgo logo\" width=\"50\"/> | **DuckDuckGo**     | Allows BondAI to search the internet. No API keys required. |\n| <img src={alpacaMarketsLogo} alt=\"alpaca markets logo\" width=\"50\"/> | **Alpaca Markets**     | Allows BondAI to buy and sell stocks and crypto. [Requires an Alpaca Markets account.](https://alpaca.markets/)  |\n| <img src={postgresLogo} alt=\"postgres logo\" width=\"75\"/>    | **PostgreSQL**    | BondAI can automatically extract the schema from a Postgres DB and process natural language queries. |\n| <img src={blandaiLogo} alt=\"bland.ai logo\" width=\"50\"/> | **Bland AI**     | Allows BondAI to make phone calls and process/retrieve call transcripts. [Requires a Bland.ai account.](https://www.bland.ai/)  |\n| <img src={gmailLogo} alt=\"gmail logo\" width=\"50\"/> | **Gmail**     | Allows BondAI to search and read emails.  |\n| <img src={langchainLogo} alt=\"langchain logo\" width=\"50\"/> | **LangChain**     | Use BondAI's LangChainTool class to import any tool from LangChain into BondAI.  |\n\n"
  },
  {
    "path": "website/docs/multi-agent-systems/examples.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Multi-Agent Architectures\n\n## Example 1: Flat Multi-Agent Architecture\n\nIn this example architecture, multiple ConversationalAgents are configured, each specializing in different aspects of customer support (e.g., technical, billing, general queries). They are managed under a GroupConversation system. This is an example of a flat conversational architecture where all agents are able to communicate directly with each other.\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.agents.group_chat import GroupConversation\n\n# Initialize multiple agents for different support aspects\ntech_support = ConversationalAgent(name=\"TechSupport\")\nbilling_support = ConversationalAgent(name=\"BillingSupport\")\ngeneral_support = ConversationalAgent(name=\"GeneralSupport\")\n\n# Create a group conversation with these agents\nsupport_team = GroupConversation(\n    conversation_members=[\n        tech_support, \n        billing_support, \n        general_support\n    ]\n)\n\n# Code to route user queries to the appropriate agent\nuser_query = \"How do I reset my password?\"\nresponse = support_team.send_message(tech_support.name, user_query)\nprint(response.message)\n```\n\n## Example 2: Hierarchical Conversational Architecture\n\nThis example illustrates a hierarchical conversational architecture, characterized by structured agent interactions within a group. In this setup, the GroupConversation is configured using TeamConversationConfig to create distinct teams within the conversation.\n\n- **Agent Configuration**: Three ConversationalAgent's are initialized—`team_leader`, `coding_expert`, and `design_expert`. Each agent has a specific role, where the team leader can coordinate and delegate tasks between the two experts.\n\n- **Group Setup**: The GroupConversation is organized into teams: one team includes the `team_leader` and `coding_expert`, and another consists of the `team_leader` and `design_expert`. This configuration ensures that the team leader can communicate with both experts, but direct communication between the coding and design experts is not possible.\n\n- **Conversation Dynamics**: When the conversation starts, a message is sent to the `team_leader`. The hierarchical structure allows the team leader to relay information to the experts, orchestrate their collaboration, and provide combined insights from both agents.\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.agents.group_chat import GroupConversation, TeamConversationConfig\n\n# Initialize team agents\nteam_leader = ConversationalAgent(name=\"TeamLeader\")\ncoding_expert = ConversationalAgent(name=\"CodingExpert\")\ndesign_expert = ConversationalAgent(name=\"DesignExpert\")\n\n# Create a group conversation\nproblem_solving_team = GroupConversation(\n    conversation_config=TeamConversationConfig(\n        [team_leader, coding_expert],\n        [team_leader, design_expert]\n    )\n)\n\n# Simulate a problem-solving session\nproblem_description = \"Develop a user-friendly app interface.\"\nresponse = problem_solving_team.send_message(team_leader.name, problem_description)\nprint(response.message)\n```\n\n\n## Example 3: Multi-Team, Heirarchical Conversational Architecture\n\nThis example showcases a complex hierarchical architecture, involving multiple agents organized into distinct teams, each with specialized roles. In this model, communication channels are both vertical (within each team) and horizontal (across teams via team leaders).\n\n- **Agent Configuration**: The architecture involves a `product_manager`, two team leaders (`eng_leader` and `design_leader`), and various experts (`coding_expert`, `qa_expert`, `visual_designer`, `ux_designer`). The `product_manager` serves as a central figure overseeing the entire project.\n\n- **Group Setup**: This configuration organizes the agents into three teams: an **engineering team** (led by `eng_leader`), a **design team** (led by `design_leader`), and a **management team** consisting of the `product_manager` and both team leaders.\n\n- **Conversation Dynamics**: When a conversation is initiated, a message is sent to the `product_manager`, the product manager can facilitate the conversation by engaging with both team leaders, who in turn can collaborate with their respective team members.\n\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.agents.group_chat import GroupConversation, TeamConversationConfig\n\n# Initialize product manager agent\nproduct_manager = ConversationalAgent(name=\"ProductManager\")\n\n# Initialize team 1 agents\neng_leader = ConversationalAgent(name=\"EngTeamLeader\")\ncoding_expert = ConversationalAgent(name=\"CodingExpert\")\nqa_expert = ConversationalAgent(name=\"QAExpert\")\n\n# Initialize team 2 agents\ndesign_leader = ConversationalAgent(name=\"DesignTeamLeader\")\nvisual_designer = ConversationalAgent(name=\"VisualDesigner\")\nux_designer = ConversationalAgent(name=\"UXDesigner\")\n\n# Create a group conversation\nproblem_solving_team = GroupConversation(\n    conversation_config=TeamConversationConfig(\n        [eng_leader, coding_expert, qa_expert],\n        [design_leader, visual_designer, ux_designer],\n        [product_manager, eng_leader, design_leader]\n    )\n)\n\n# Simulate a problem-solving session\nproblem_description = \"Develop a user-friendly app interface.\"\nresponse = problem_solving_team.send_message(product_manager.name, problem_description)\nprint(response.message)\n```"
  },
  {
    "path": "website/docs/multi-agent-systems/group-conversation.md",
    "content": "---\nsidebar_position: 2\n---\n\n\n# GroupConversation\n\nThe GroupConversation class in BondAI facilitates the creation and management of conversations involving multiple agents, inspired by advanced multi-agent system research.\n\n\n```python\nclass GroupConversation(EventMixin, Runnable):\n    def __init__(\n        self,\n        conversation_members: List[ConversationMember] | None = None,\n        conversation_config: BaseGroupConversationConfig | None = None,\n        filter_recipient_messages: bool = False,\n    ):\n```\n\n## Usage Example\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.agents.group_chat import GroupConversation\n\n# Initialize conversation members\nmembers = [ConversationalAgent(...), ConversationalAgent(...)]\n\n# Create a group conversation\ngroup_conversation = GroupConversation(conversation_members=members)\n\n# Conduct a group conversation\ngroup_conversation.send_message(\"MemberName\", \"Hello, let's discuss.\")\n```\n\n## Key Features\n- Event-driven architecture.\n- Manages multi-agent conversations.\n- Supports dynamic interaction among multiple conversation participants.\n- Facilitates complex conversational flows and decision-making processes.\n- Allows for conversations with both predefined and dynamically determined members.\n\n\n## Parameters\n\n- **conversation_members**: List of ConversationMember instances (i.e. UserProxy or ConversationalAgent) participating in the conversation.\n- **conversation_config**: Configuration settings for managing group conversation dynamics.\n- **filter_recipient_messages**: Boolean flag to determine whether the message history shown to each conversation member is inclusive of the entire group conversation or just the messages sent to/from that conversation member.\n\n## Methods\n\n- **id**: Property returning the unique identifier of the group conversation. No parameters.\n- **status**: Property indicating the current status of the group conversation. No parameters.\n- **members**: Property listing the conversation members participating in the group. No parameters.\n- **remove_messages_after(timestamp: datetime, inclusive: bool = True)**: Removes messages from the conversation history that occurred after a specific timestamp.\n- **send_message(recipient_name: str, message: str, sender_name: str = USER_MEMBER_NAME, require_response: bool = True)**: Sends a message within the group conversation.\n- **reset_memory**: Clears the message history for all conversation members.\n\n## Group Conversation Events\n\n- **message_received**: Triggered when a message is received by a member of the conversation.\n- **message_error**: Fired when an error occurs in message processing within the group.\n- **message_completed**: Occurs when a message has been successfully processed by a member of the conversation.\n- **conversation_exited**: Triggered when a member exits the conversation.\n"
  },
  {
    "path": "website/docs/multi-agent-systems/multi-agent-systems.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Multi-Agent Systems\n\nMulti-Agent Systems (MAS) in BondAI represent a sophisticated approach to developing interactive, collaborative AI applications. At the core of MAS in BondAI are three primary classes: [ConversationalAgent](../agents/conversational-agent.md), [GroupConversation](./group-conversation.md), and [TeamConversationConfig](./team-conversation-config.md).\n\n**ConversationalAgent** acts as the fundamental building block of MAS, embodying individual agents with specific roles or expertise. These agents can engage in dialogues, process information, and perform tasks based on their programming and interactions.\n\n**GroupConversation** is the framework that orchestrates communication among multiple agents. It allows various ConversationalAgents to interact within a shared conversational space, enabling information exchange, collaborative problem-solving, and decision-making processes. This class manages the dynamics of the conversation, ensuring coherent interactions among all participating agents.\n\n**TeamConversationConfig** is crucial for structuring the conversation architecture within a MAS. It defines how agents are grouped and how they can communicate with each other. This configuration can set up hierarchical structures, dividing agents into teams or layers, and determining the flow of information between them. It plays a pivotal role in managing complex conversations where different agents contribute distinct insights or skills towards a common goal.\n\nTogether, these classes enable the development of complex MAS architectures in BondAI, where agents can work in unison or independently, mimicking real-world team dynamics and collaborative environments. This system opens up possibilities for applications requiring nuanced interactions and emergent intelligence."
  },
  {
    "path": "website/docs/multi-agent-systems/team-conversation-config.md",
    "content": "---\nsidebar_position: 3\n---\n\n\n# TeamConversationConfig\n\nThe TeamConversationConfig class in BondAI structures the conversations in a Multi-Agent System, enabling complex conversational patterns among different agents.\n\n\n```python\nclass TeamConversationConfig(BaseGroupConversationConfig):\n    def __init__(self, *args: List[ConversationMember]):\n        ...\n```\n\n## Usage Example\n\n```python\nfrom bondai.agents import ConversationalAgent\nfrom bondai.agents.group_chat import GroupConversation, TeamConversationConfig\n\n# Initialize team 1\nagent1 = ConversationalAgent(...)\nagent2 = ConversationalAgent(...)\nteam1 = [agent1, agent2]\n\n# Initialize team 2\nagent3 = ConversationalAgent(...)\nagent4 = ConversationalAgent(...)\nteam2 = [agent3, agent4]\n\n# Allow agent1 and agent 3 to communicate\nteam3 = [agent1, agent3]\n\n# Configure teams\nteam_config = TeamConversationConfig(team1, team2, team3)\n\n# Use in GroupConversation\ngroup_conversation = GroupConversation(conversation_config=team_config)\n```\n\n\n## Key Features\n\n- Facilitates hierarchical and structured team-based conversations.\n- Allows the creation of any number of teams with specific member agents.\n- Supports dynamic interactions within and across teams.\n- Enhances the control over communication flow in multi-agent setups.\n"
  },
  {
    "path": "website/docs/tools/_category_.json",
    "content": "{\n  \"label\": \"Tools\",\n  \"position\": 6,\n  \"link\": {\n    \"type\": \"generated-index\"\n  }\n}\n"
  },
  {
    "path": "website/docs/tools/custom-tool.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Building Custom Tools\n\nBy building your own custom tools you can give BondAI the power to interact with new products, external APIs, really just about anything! Fortunately, tools are super easy to build. In this example we'll build a tool that allows BondAI to ask questions about any country and get information back from the [RestCountries API](https://restcountries.com/).\n\n\n### Step 1: Define your Tool's Name, Description and Parameters\n\nFor BondAI to use your tool it needs 3 pieces of information to tell the LLM about your tool. For the LLM to effectively use your tool your description should be highly detailed and informative.\n\n- **Tool Name** - This should be an informative name for what your tool does. It's also important that it is unique to just your tool.\n- **Tool Description** - This is a detailed description of what your tool does, what the parameters are used for, what information it returns and when to use it.\n- **Tool Parameters** - The Parameters structure is used to encode the parameters shown to the LLM. You must include all possible parameters in this structure. It is common to include a 'thought' parameter which encourages the LLM to include it's reasoning for using the tool which has been shown to improve performance.\n\n```python\nfrom pydantic import BaseModel\n\nTOOL_NAME = \"query_countries_tool\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to search countries by name and get a list back with information about each country. \"\n    \"The response includes incormation like the country's population size, currencies, languages and more. \"\n    \"\\nParameters:\\n\"\n    \"- country_name (required): The name of the country you are looking for.\"\n)\n\nclass Parameters(BaseModel):\n    country_name: str\n```\n\n### Step 2: Implement your Tool class\n\nAll custom BondAI tools must extend from the Tool class and implement the `run` method. In our example tool we will call the RestCountries API to search for countries by name.\n\n```python\nfrom bondai.tools import Tool\n\nclass QueryCountriesTool(Tool):\n\n    def __init__(self):\n        super().__init__(TOOL_NAME, TOOL_DESCRIPTION, parameters=Parameters)\n\n    def run(self, country_name: str):\n        if not country_name:\n            raise Exception(\"country_name is required.\")\n        \n        response = requests.get(f\"https://restcountries.com/v3.1/name/{country_name}\")\n        return parse_countries_info(response.json())\n```\n\n### Step 3: Format your response\n\nWe will define a function named `parse_countries_info` that takes the JSON object returned from the RestCountries API and turns it into a well formatted string that can be easily understood by the LLM. Note that while the LLM could likely undestand the JSON formatted response, this approach has the advantage of removing unnecessary information which reduces token usage and cost. This also reduces the amount of Agent memory required to store the result which is limited by the LLM's context window. It is highly recommended that tool responses are well formatted (ie markdown) to improve understanding and limited to only required information.\n\n\n```python\ndef parse_countries_info(data):\n    responses = []\n\n    for country in data:\n        country_name = country['name']['common']\n        population = country['population']\n        language = list(country['languages'].values())[0]\n        area = country['area']\n        currency = list(country['currencies'].keys())[0]\n        region = country['region']\n        subregion = country['subregion']\n        \n        country_info = f\"\"\"**{country_name}**\nPopulation: {population}\nLanguage: {language}\nArea: {area}\nCurrency: {currency}\nRegion: {region}\nSubregion: {subregion}\\n---\\n\"\"\"  # Added a separator for readability\n\n        responses.append(country_info)\n\n    return \"\\n\".join(responses)\n```\n\n\n### Putting it all together\n\nFinally, let's put it all together into a single file!\n\n```python\nimport requests\nfrom pydantic import BaseModel\nfrom bondai.tools import Tool\n\nTOOL_NAME = \"query_countries_tool\"\nTOOL_DESCRIPTION = (\n    \"This tool allows you to search countries by name and get a list back with information about each country. \"\n    \"The response includes incormation like the country's population size, currencies, languages and more. \"\n    \"\\nParameters:\\n\"\n    \"- country_name (required): The name of the country you are looking for.\"\n)\n\nclass Parameters(BaseModel):\n    country_name: str\n\ndef parse_countries_info(data):\n    responses = []\n\n    for country in data:\n        country_name = country['name']['common']\n        population = country['population']\n        language = list(country['languages'].values())[0]\n        area = country['area']\n        currency = list(country['currencies'].keys())[0]\n        region = country['region']\n        subregion = country['subregion']\n        \n        country_info = f\"\"\"**{country_name}**\nPopulation: {population}\nLanguage: {language}\nArea: {area}\nCurrency: {currency}\nRegion: {region}\nSubregion: {subregion}\\n---\\n\"\"\"  # Added a separator for readability\n\n        responses.append(country_info)\n\n    return \"\\n\".join(responses)\n\nclass QueryCountriesTool(Tool):\n\n    def __init__(self):\n        super().__init__(TOOL_NAME, TOOL_DESCRIPTION, parameters=Parameters)\n\n    def run(self, country_name: str):\n        if not country_name:\n            raise Exception(\"country_name is required.\")\n        \n        response = requests.get(f\"https://restcountries.com/v3.1/name/{country_name}\")\n        return parse_countries_info(response.json())\n```"
  },
  {
    "path": "website/docs/tools/getting-started.md",
    "content": "---\nsidebar_position: 1\n---\n\nimport bondaiLogo from '../img/bondai-logo.png'\nimport googleLogo from '../img/google-logo.png'\nimport alpacaMarketsLogo from '../img/alpaca-markets-logo.jpeg'\nimport postgresLogo from '../img/postgres-logo.jpeg'\nimport blandaiLogo from '../img/blandai-logo.jpeg'\nimport duckduckgoLogo from '../img/duckduckgo-logo.png'\nimport gmailLogo from '../img/gmail-logo.png'\nimport openaiLogo from '../img/openai-logo.png'\nimport azureLogo from '../img/azure-logo.png'\nimport langchainLogo from '../img/langchain-logo.jpeg'\n\n# BondAI Tools\n\nTools are what give AI Agents the power to interact with their environment. Combined with the planning capabilities of LLMs, AI Agents are able to break down complex tasks that may require multiple steps and use tools to accomplish their goals. In addition to using BondAI's comprehensive list of built in tools you can build you own custom tools and even import tools from LangChain.\n\n# Example Tools\n\n|     |  |\n| -------- | ------- |\n| **PythonREPLTool**     | This tool allows BondAI to write and execute Python code. Note that it is highly recommended that this tool be used from within a Docker environment as it may damage the host machine.  |\n| **ResponseQueryTool**     | BondAI will automatically detect if a tool returns a large response (>2000 tokens). To prevent this response from overwhelming the Agent's memory the output will be passed to the ResponseQueryTool which the Agent can then query to get information about the response.  |\n| **ShellTool**     | This tool allows BondAI to interact with the users Shell and run arbitrary commands. Note that it is highly recommended that this tool be used from within a Docker environment as it may damage the host machine.  |\n| **FileQueryTool**     | This tool allows BondAI to query the content of a file. This tool uses integrated Semantic search. If the content of the file is too large for the LLM's context window the content will automatically be converted to embeddings and filtered to fit within the context window.  |\n| **FileWriteTool**     | This tool allows BondAI to write content to a specified filename.  |\n| **FileReadTool**     | This tool allows BondAI to read the raw content of a specified filename. Note that when dealing with large files it is recommended to use the FileQueryTool as large files may overwhelm the Agent's memory.  |\n| **DownloadFileTool**     | This tool allows BondAI to download files from the internet and save them locally to a specified filename.  |\n| **ExtractHyperlinksTool**     | This tool allows BondAI retrieve the hyperlinks from the HTML of a specified website.  |\n| **WebsiteQueryTool**     | This tool allows BondAI to query the content of a website. This tool uses integrated Semantic search. If the content of the website is too large for the LLM's context window the content will automatically be converted to embeddings and filtered to fit within the context window.  |\n| **AgentTool**     | This tool allows Agent's to delegate complex tasks to other Agents creating a heirarchical Agent architecture.  |\n\n# Partner Tools\n\n|     |  |  |\n| -------- | ------- |------- |\n| <img src={openaiLogo} alt=\"openai logo\" width=\"50\"/> | **OpenAI**     | BondAI supports any combination of OpenAI models and services including GPT-4, GPT-3.5, Dalle-E 3, and Embeddings.  |\n| <img src={azureLogo} alt=\"azure logo\" width=\"50\"/> | **Microsoft Azure**     | BondAI fully supports connectivity to GPT-N, Dalle-E and Embedding APIs through [Microsoft's Azure OpenAI services](https://azure.microsoft.com/en-us/products/ai-services/openai-service).  |\n| <img src={googleLogo} alt=\"google logo\" width=\"50\"/>  | **Google Search**    | Allows BondAI to search the internet. [Requires a Google Search API Key and CSE ID](https://developers.google.com/custom-search/v1/introduction) |\n| <img src={duckduckgoLogo} alt=\"duckduckgo logo\" width=\"50\"/> | **DuckDuckGo**     | Allows BondAI to search the internet. No API keys required. |\n| <img src={alpacaMarketsLogo} alt=\"alpaca markets logo\" width=\"50\"/> | **Alpaca Markets**     | Allows BondAI to buy and sell stocks and crypto. [Requires an Alpaca Markets account.](https://alpaca.markets/)  |\n| <img src={postgresLogo} alt=\"postgres logo\" width=\"75\"/>    | **PostgreSQL**    | BondAI can automatically extract the schema from a Postgres DB and process natural language queries. |\n| <img src={blandaiLogo} alt=\"bland.ai logo\" width=\"50\"/> | **Bland AI**     | Allows BondAI to make phone calls and process/retrieve call transcripts. [Requires a Bland.ai account.](https://www.bland.ai/)  |\n| <img src={gmailLogo} alt=\"gmail logo\" width=\"50\"/> | **Gmail**     | Allows BondAI to search and read emails.  |\n\n"
  },
  {
    "path": "website/docusaurus.config.js",
    "content": "// @ts-check\n// Note: type annotations allow type checking and IDEs autocompletion\n\nconst lightCodeTheme = require('prism-react-renderer/themes/github');\nconst darkCodeTheme = require('prism-react-renderer/themes/dracula');\n\n/** @type {import('@docusaurus/types').Config} */\nconst config = {\n  title: 'BondAI',\n  tagline: 'Automate Everything',\n  favicon: 'img/favicon.ico',\n\n  // Set the production url of your site here\n  url: 'https://bondai.dev',\n  // Set the /<baseUrl>/ pathname under which your site is served\n  // For GitHub pages deployment, it is often '/<projectName>/'\n  baseUrl: '/',\n\n  // GitHub pages deployment config.\n  // If you aren't using GitHub pages, you don't need these.\n  organizationName: 'krohling', // Usually your GitHub org/user name.\n  projectName: 'bondai', // Usually your repo name.\n\n  onBrokenLinks: 'throw',\n  onBrokenMarkdownLinks: 'warn',\n\n  // Even if you don't use internalization, you can use this field to set useful\n  // metadata like html lang. For example, if your site is Chinese, you may want\n  // to replace \"en\" with \"zh-Hans\".\n  i18n: {\n    defaultLocale: 'en',\n    locales: ['en'],\n  },\n\n  presets: [\n    [\n      'classic',\n      /** @type {import('@docusaurus/preset-classic').Options} */\n      ({\n        docs: {\n          sidebarPath: require.resolve('./sidebars.js'),\n        },\n        theme: {\n          customCss: require.resolve('./src/css/custom.css'),\n        },\n      }),\n    ],\n  ],\n\n  themeConfig:\n    /** @type {import('@docusaurus/preset-classic').ThemeConfig} */\n    ({\n      // Replace with your project's social card\n      image: 'img/bondai-social-card.png',\n      navbar: {\n        title: 'BondAI',\n        logo: {\n          alt: 'BondAI Logo',\n          src: 'img/bondai-logo.png',\n        },\n        items: [\n          {\n            type: 'docSidebar',\n            sidebarId: 'tutorialSidebar',\n            position: 'left',\n            label: 'Meet BondAI',\n          },\n          {\n            href: 'https://github.com/krohling/bondai',\n            label: 'GitHub',\n            position: 'right',\n          },\n        ],\n      },\n      footer: {\n        style: 'dark',\n        links: [\n          {\n            title: 'Docs',\n            items: [\n              {\n                label: 'Tutorial',\n                to: '/docs/intro',\n              },\n            ],\n          },\n          {\n            title: 'Community',\n            items: [\n              {\n                label: 'Discord',\n                href: 'https://discordapp.com/invite/docusaurus',\n              },\n            ],\n          },\n          {\n            title: 'More',\n            items: [\n              {\n                label: 'Github',\n                href: 'https://github.com/krohling/bondai',\n              },\n              {\n                label: 'DockerHub',\n                href: 'https://hub.docker.com/r/krohling/bondai',\n              },\n            ],\n          },\n        ],\n      },\n      prism: {\n        theme: lightCodeTheme,\n        darkTheme: darkCodeTheme,\n      },\n    }),\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"bondai-docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"2.4.3\",\n    \"@docusaurus/preset-classic\": \"2.4.3\",\n    \"@mdx-js/react\": \"^1.6.22\",\n    \"clsx\": \"^1.2.1\",\n    \"prism-react-renderer\": \"^1.3.5\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"2.4.3\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=16.14\"\n  }\n}\n"
  },
  {
    "path": "website/sidebars.js",
    "content": "/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n */\n\n// @ts-check\n\n/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */\nconst sidebars = {\n  // By default, Docusaurus generates a sidebar from the docs folder structure\n  tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],\n\n  // But you can create a sidebar manually\n  /*\n  tutorialSidebar: [\n    'intro',\n    'hello',\n    {\n      type: 'category',\n      label: 'Tutorial',\n      items: ['tutorial-basics/create-a-document'],\n    },\n  ],\n   */\n};\n\nmodule.exports = sidebars;\n"
  },
  {
    "path": "website/src/components/HomepageFeatures/index.js",
    "content": "import React from 'react';\nimport clsx from 'clsx';\nimport styles from './styles.module.css';\n\nconst FeatureList = [\n  {\n    title: 'Easy to Use',\n    Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,\n    description: (\n      <>\n        Docusaurus was designed from the ground up to be easily installed and\n        used to get your website up and running quickly.\n      </>\n    ),\n  },\n  {\n    title: 'Focus on What Matters',\n    Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,\n    description: (\n      <>\n        Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go\n        ahead and move your docs into the <code>docs</code> directory.\n      </>\n    ),\n  },\n  {\n    title: 'Powered by React',\n    Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,\n    description: (\n      <>\n        Extend or customize your website layout by reusing React. Docusaurus can\n        be extended while reusing the same header and footer.\n      </>\n    ),\n  },\n];\n\nfunction Feature({Svg, title, description}) {\n  return (\n    <div className={clsx('col col--4')}>\n      <div className=\"text--center\">\n        <Svg className={styles.featureSvg} role=\"img\" />\n      </div>\n      <div className=\"text--center padding-horiz--md\">\n        <h3>{title}</h3>\n        <p>{description}</p>\n      </div>\n    </div>\n  );\n}\n\nexport default function HomepageFeatures() {\n  return (\n    <section className={styles.features}>\n      <div className=\"container\">\n        <div className=\"row\">\n          {FeatureList.map((props, idx) => (\n            <Feature key={idx} {...props} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "website/src/components/HomepageFeatures/styles.module.css",
    "content": ".features {\n  display: flex;\n  align-items: center;\n  padding: 2rem 0;\n  width: 100%;\n}\n\n.featureSvg {\n  height: 200px;\n  width: 200px;\n}\n"
  },
  {
    "path": "website/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* You can override the default Infima variables here. */\n:root {\n  --ifm-color-primary: #2e8555;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);\n}\n\n/* For readability concerns, you should choose a lighter palette in dark mode. */\n[data-theme='dark'] {\n  --ifm-color-primary: #25c2a0;\n  --ifm-color-primary-dark: #21af90;\n  --ifm-color-primary-darker: #1fa588;\n  --ifm-color-primary-darkest: #1a8870;\n  --ifm-color-primary-light: #29d5b0;\n  --ifm-color-primary-lighter: #32d8b4;\n  --ifm-color-primary-lightest: #4fddbf;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);\n}\n"
  },
  {
    "path": "website/src/pages/index.js",
    "content": "import React, { useEffect } from 'react';\nimport { useHistory } from 'react-router-dom';\nimport clsx from 'clsx';\nimport Link from '@docusaurus/Link';\nimport useDocusaurusContext from '@docusaurus/useDocusaurusContext';\nimport Layout from '@theme/Layout';\nimport HomepageFeatures from '@site/src/components/HomepageFeatures';\n\nimport styles from './index.module.css';\n\nfunction HomepageHeader() {\n  const {siteConfig} = useDocusaurusContext();\n  return (\n    <header className={clsx('hero hero--primary', styles.heroBanner)}>\n      <div className=\"container\">\n        <h1 className=\"hero__title\">{siteConfig.title}</h1>\n        <p className=\"hero__subtitle\">{siteConfig.tagline}</p>\n        <div className={styles.buttons}>\n          <Link\n            className=\"button button--secondary button--lg\"\n            to=\"/docs/intro\">\n            Docusaurus Tutorial - 5min ⏱️\n          </Link>\n        </div>\n      </div>\n    </header>\n  );\n}\n\nexport default function Home() {\n  const history = useHistory();\n  const {siteConfig} = useDocusaurusContext();\n\n  useEffect(() => {\n    // window.location.href = '/docs/intro'\n    history.push('/docs/intro');\n  }, []);\n\n  return '';\n}\n"
  },
  {
    "path": "website/src/pages/index.module.css",
    "content": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n.heroBanner {\n  padding: 4rem 0;\n  text-align: center;\n  position: relative;\n  overflow: hidden;\n}\n\n@media screen and (max-width: 996px) {\n  .heroBanner {\n    padding: 2rem;\n  }\n}\n\n.buttons {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "website/src/pages/markdown-page.md",
    "content": "---\ntitle: Markdown page example\n---\n\n# Markdown page example\n\nYou don't need React to write simple standalone pages.\n"
  },
  {
    "path": "website/static/.nojekyll",
    "content": ""
  }
]