├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── agbenchmark
├── __init__.py
├── benchmarks.py
└── config.json
├── beebot
├── __init__.py
├── agents
│ ├── __init__.py
│ ├── base_agent.py
│ ├── coding_agent.py
│ ├── generalist_agent.py
│ └── research_agent.py
├── api
│ ├── __init__.py
│ ├── routes.py
│ └── websocket.py
├── body
│ ├── __init__.py
│ ├── body.py
│ ├── llm.py
│ └── pack_utils.py
├── config
│ ├── __init__.py
│ ├── config.py
│ └── database_file_manager.py
├── decider
│ ├── __init__.py
│ ├── decider.py
│ └── deciding_prompt.py
├── decomposer
│ ├── __init__.py
│ ├── decomposer.py
│ └── decomposer_prompt.py
├── execution
│ ├── __init__.py
│ ├── background_process.py
│ ├── executor.py
│ ├── step.py
│ ├── task_execution.py
│ └── task_state_machine.py
├── executor
│ └── __init__.py
├── initiator
│ ├── __init__.py
│ ├── api.py
│ ├── benchmark_entrypoint.py
│ └── cli.py
├── models
│ ├── __init__.py
│ └── database_models.py
├── overseer
│ ├── __init__.py
│ ├── overseeing_prompt.py
│ └── overseer.py
├── packs
│ ├── README.md
│ ├── __init__.py
│ ├── delegate_task.py
│ ├── disk_usage
│ │ ├── __init__.py
│ │ ├── disk_usage.py
│ │ └── test_disk_usage.py
│ ├── execute_python_file.py
│ ├── execute_python_file_in_background.py
│ ├── exit.py
│ ├── export_variable.py
│ ├── extract_information_from_webpage
│ │ ├── __init__.py
│ │ ├── extract_information_from_webpage.py
│ │ └── test_extract_information_from_webpage.py
│ ├── filesystem
│ │ ├── __init__.py
│ │ ├── delete_file.py
│ │ ├── list_files.py
│ │ ├── read_file.py
│ │ ├── test_delete_file.py
│ │ ├── test_list_files.py
│ │ ├── test_read_file.py
│ │ ├── test_write_file.py
│ │ └── write_file.py
│ ├── filesystem_utils.py
│ ├── get_process_status.py
│ ├── get_webpage_html_content
│ │ ├── __init__.py
│ │ ├── get_webpage_html_content.py
│ │ └── test_get_webpage_html_content.py
│ ├── get_website_text_content.py
│ ├── gmail.py
│ ├── google_search
│ │ ├── __init__.py
│ │ ├── google_search.py
│ │ └── test_google_search.py
│ ├── http_request
│ │ ├── __init__.py
│ │ ├── http_request.py
│ │ └── test_http_request.py
│ ├── install_python_package.py
│ ├── kill_process.py
│ ├── list_processes.py
│ ├── os_info
│ │ ├── __init__.py
│ │ ├── os_info.py
│ │ └── test_os_info.py
│ ├── poetry.lock
│ ├── pyproject.toml
│ ├── summarization.py
│ ├── summarization_prompt.py
│ ├── system_base_pack.py
│ ├── wikipedia_summarize
│ │ ├── __init__.py
│ │ ├── test_wikipedia.py
│ │ └── wikipedia.py
│ ├── wolframalpha_query
│ │ ├── __init__.py
│ │ ├── test_wolframalpha_query.py
│ │ └── wolframalpha_query.py
│ └── write_python_code
│ │ ├── __init__.py
│ │ ├── test_write_python_file.py
│ │ └── write_python_file.py
├── planner
│ ├── __init__.py
│ ├── planner.py
│ └── planning_prompt.py
├── tool_filters
│ ├── __init__.py
│ └── filter_long_documents.py
└── utils.py
├── docker-compose.yml
├── docs
└── architecture.md
├── migrations
└── 20230717_01_initial_schema.sql
├── poetry.lock
├── pyproject.toml
├── pytest.ini
├── setup.sh
├── tests
├── __init__.py
├── conftest.py
└── end_to_end
│ ├── __init__.py
│ ├── test_background_python.py
│ ├── test_capital_retrieval.py
│ ├── test_history.py
│ ├── test_revenue_lookup.py
│ ├── test_system_basic_cycle.py
│ └── test_webserver.py
└── yoyo.ini
/.env.example:
--------------------------------------------------------------------------------
1 | # Your OpenAI API Key. If GPT-4 is available it will use that, otherwise will use 3.5-turbo
2 | OPENAI_API_KEY=MY-API-KEY
3 |
4 | # When this is set the entire process will be killed when the agent completes. Disable for testing.
5 | BEEBOT_HARD_EXIT=True
6 |
7 | # If you want to enable Helicone proxy and caching
8 | # HELICONE_KEY=MY-HELICONE-KEY
9 | # OPENAI_API_BASE=https://oai.hconeai.com/v1
10 |
11 | # This is needed for Gmail access
12 | GOOGLE_API_KEY=MY-API-KEY
13 | DEFAULT_CLIENT_SECRETS_FILE=.google_credentials.json
14 |
15 | # This is needed if you want to enable google searches
16 | SERPER_API_KEY=MY_API_KEY
17 |
18 | # Needed if you want to enable WolframAlpha
19 | WOLFRAM_ALPHA_APPID=MY-APP-ID
20 |
21 | # It is highly recommended that you use Postgres, but SQLite is supported.
22 | # DATABASE_URL=sqlite://:memory:
23 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
24 |
25 | # Control log level
26 | LOG_LEVEL=INFO
27 |
28 | # Needed to log end-to-end test results to Baserun for easy debugging, comparison, and evaluation
29 | # Use poetry run pytest --baserun
30 | BASERUN_API_KEY=MY_API_KEY
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | scripts/
3 | tmp/
4 | .autopack
5 | credentials.json
6 | token.json
7 | .DS_STORE
8 | workspace/
9 | postgres-data/
10 | agbenchmark/reports
11 |
12 | # Byte-compiled / optimized / DLL files
13 | __pycache__/
14 | *.py[cod]
15 | *$py.class
16 |
17 | # C extensions
18 | *.so
19 |
20 | # Distribution / packaging
21 | .Python
22 | build/
23 | develop-eggs/
24 | dist/
25 | downloads/
26 | eggs/
27 | .eggs/
28 | lib/
29 | lib64/
30 | parts/
31 | sdist/
32 | var/
33 | wheels/
34 | pip-wheel-metadata/
35 | share/python-wheels/
36 | *.egg-info/
37 | .installed.cfg
38 | *.egg
39 | MANIFEST
40 |
41 | # PyInstaller
42 | # Usually these files are written by a python script from a template
43 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
44 | *.manifest
45 | *.spec
46 |
47 | # Installer logs
48 | pip-log.txt
49 | pip-delete-this-directory.txt
50 |
51 | # Unit test / coverage reports
52 | htmlcov/
53 | .tox/
54 | .nox/
55 | .coverage
56 | .coverage.*
57 | .cache
58 | nosetests.xml
59 | coverage.xml
60 | *.cover
61 | *.py,cover
62 | .hypothesis/
63 | .pytest_cache/
64 |
65 | # Translations
66 | *.mo
67 | *.pot
68 |
69 | # Django stuff:
70 | *.log
71 | local_settings.py
72 | db.sqlite3
73 | db.sqlite3-journal
74 |
75 | # Flask stuff:
76 | instance/
77 | .webassets-cache
78 |
79 | # Scrapy stuff:
80 | .scrapy
81 |
82 | # Sphinx documentation
83 | docs/_build/
84 |
85 | # PyBuilder
86 | target/
87 |
88 | # Jupyter Notebook
89 | .ipynb_checkpoints
90 |
91 | # IPython
92 | profile_default/
93 | ipython_config.py
94 |
95 | # pyenv
96 | .python-version
97 |
98 | # pipenv
99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
102 | # install all needed dependencies.
103 | #Pipfile.lock
104 |
105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
106 | __pypackages__/
107 |
108 | # Celery stuff
109 | celerybeat-schedule
110 | celerybeat.pid
111 |
112 | # SageMath parsed files
113 | *.sage.py
114 |
115 | # Environments
116 | .env
117 | .venv
118 | env/
119 | venv/
120 | ENV/
121 | env.bak/
122 | venv.bak/
123 |
124 | # Spyder project settings
125 | .spyderproject
126 | .spyproject
127 |
128 | # Rope project settings
129 | .ropeproject
130 |
131 | # mkdocs documentation
132 | /site
133 |
134 | # mypy
135 | .mypy_cache/
136 | .dmypy.json
137 | dmypy.json
138 |
139 | # Pyre type checker
140 | .pyre/
141 |
142 | # templates
143 | .github/templates/*
144 | !/benchmarker/agent/beebot/
145 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Erik Peterson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BeeBot
2 |
3 | BeeBot is your personal worker bee, an Autonomous AI Assistant designed to perform a wide range of practical tasks
4 | autonomously.
5 |
6 |
7 |
8 |
9 |
10 | ## Status
11 |
12 | Development of BeeBot is currently on hold. I've decided that LLMs as they are now (late 2023) aren't up to the task of generalized autonomous AI. I will revive the project if either:
13 |
14 | - LLMs get significantly better at structured thinking, reliable outcomes, and obeying instructions
15 | - I can develop or fine tune a custom model which is trained specifically for Autonomous AI
16 | - I figure out a particular subset of tasks that BeeBot is acceptably good at that I can focus on. (Hint: It's not coding)
17 |
18 | Check back here, hopefully this will get re-started.
19 |
20 | ## Features
21 |
22 | - Tool selection via [AutoPack](https://autopack.ai) and the ability to acquire more tools during task execution
23 | - Built-in persistence
24 | - REST API conforming to the [e2b](https://www.e2b.dev/) standard.
25 | - A websocket server to publish all events that occur within BeeBot
26 | - Swappable filesystem emulation so that files can be stored in-memory, on-disk, or in a database
27 | - A Web UI for managing your tasks (coming very soon)
28 | - Dynamic manipulation of history during task execution
29 | - Built-in caching with [Helicone](https://www.helicone.ai/) if enabled.
30 |
31 | ## Installation
32 |
33 | To get started with BeeBot, you can clone the repo to your local machine and install its dependencies using `poetry`.
34 | These instructions may vary depending on your local development environment.
35 |
36 | ```bash
37 | git clone https://github.com/AutoPackAI/beebot.git
38 | cd beebot
39 | ./setup.sh
40 | ```
41 |
42 | Windows is officially unsupported but it may work. PRs are welcome for Windows compatibility but will not be a primary
43 | focus.
44 |
45 | ### Persistence
46 |
47 | Persistence is _required_. While SQLite is officially supported and is used in tests, it is highly recommended that
48 | you use Postgres via docker, simply by executing `docker compose up -d`.
49 |
50 | ## Running
51 |
52 | ### CLI
53 |
54 | To use the CLI run:
55 |
56 | ```bash
57 | poetry run beebot
58 | ```
59 |
60 | ### API (via [e2b](https://www.e2b.dev/))
61 |
62 | To start the server run:
63 |
64 | ```bash
65 | uvicorn beebot.initiator.api:create_app --factory --timeout-keep-alive=300
66 | ```
67 |
68 | If you're doing development on BeeBot itself, you may want to use this command:
69 |
70 | ```bash
71 | uvicorn beebot.initiator.api:create_app --factory --reload --timeout-graceful-shutdown=3 --timeout-keep-alive=300
72 | ```
73 |
74 | and then you can call the API using the following commands:
75 |
76 | To **create a task** run:
77 |
78 | ```bash
79 | curl --request POST \
80 | --url http://localhost:8000/agent/tasks \
81 | --header 'Content-Type: application/json' \
82 | --data '{
83 | "input": "Write '\''hello world'\'' to hi.txt"
84 | }'
85 | ```
86 |
87 | You will get a response like this:
88 |
89 | ```json
90 | {
91 | "input": "Write 'hello world' to hi.txt",
92 | "task_id": "103",
93 | "artifacts": []
94 | }
95 | ```
96 |
97 | Then to **execute one step of the task** copy the `task_id` you got from the previous request and run:
98 |
99 | ```bash
100 | curl --request POST \
101 | --url http://localhost:8000/agent/tasks//steps
102 | ```
103 |
104 | ### Websocket Connection
105 |
106 | _Note: Notifications are currently undergoing a rework and may not work at the moment_
107 |
108 | To receive a stream of changes to all the data models in BeeBot, you can subscribe to the websocket connection at
109 | the `/notifications` endpoint with the same host/port as the web api, e.g. ws://localhost:8000/notifications. Use your
110 | favorite websocket testing tool to try it out. (I like [Insomnia](https://insomnia.rest/))
111 |
112 | ### Web Interface
113 |
114 | We are working on a web interface using Node.js (Remix)
115 |
116 | ## Philosophy
117 |
118 | BeeBot's development process is guided by a specific philosophy, emphasizing key principles that shape its development
119 | and future direction.
120 |
121 | ### Priorities
122 |
123 | The development of BeeBot is driven by the following priorities, always in this order:
124 |
125 | 1. Functionality: BeeBot aims to achieve a high success rate for tasks within its range of _expected_ capabilities.
126 | 2. Flexibility: BeeBot strives to be adaptable to a wide range of tasks, expanding that range over time.
127 | 3. Reliability: BeeBot focuses on reliably completing known tasks with predictability.
128 | 4. Efficiency: BeeBot aims to execute tasks with minimal steps, optimizing both time and resource usage.
129 | 5. Convenience: BeeBot aims to provide a user-friendly platform for task automation.
130 |
131 | ### Principles
132 |
133 | To achieve these priorities, BeeBot follows the following principles:
134 |
135 | - Tool-focused: BeeBot carefully selects and describes tools, ensuring their reliable use by LLMs. It
136 | uses [AutoPack](https://autopack.ai) as the package manager for its tools.
137 | - LLM specialization: BeeBot will leverage a variety of LLMs best suited for different tasks, while OpenAI remains the
138 | primary LLM for planning and decision-making.
139 | - Functionality and flexibility first: BeeBot prioritizes functionality and flexibility over developer quality-of-life,
140 | which may limit support for specific platforms and other deployment conveniences.
141 | - Unorthodox methodologies: BeeBot employs unconventional development approaches to increase development speed, such as
142 | the absence of unit tests. Instead, end-to-end tests are used, ensuring the entire system works together as expected.
143 | - Proven concepts: BeeBot adopts new concepts only after they have been proven to enhance its five priorities.
144 | As a result, it does not have complex memory or a tree of thought.
145 |
146 | ## Documentation
147 |
148 | For further information on the architecture and future plans of BeeBot, please refer to the `docs/` directory. The
149 | documentation is currently very light, but will evolve alongside the project as new insights and developments emerge.
150 | Contributions and feedback from the community are highly appreciated.
151 |
--------------------------------------------------------------------------------
/agbenchmark/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/agbenchmark/__init__.py
--------------------------------------------------------------------------------
/agbenchmark/benchmarks.py:
--------------------------------------------------------------------------------
1 | import uvicorn
2 | from beebot.initiator.api import create_app
3 |
4 |
5 | if __name__ == "__main__":
6 | uvicorn.run(create_app())
7 |
--------------------------------------------------------------------------------
/agbenchmark/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "workspace": "workspace",
3 | "api_mode": true,
4 | "host": "http://localhost:8000"
5 | }
6 |
--------------------------------------------------------------------------------
/beebot/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/__init__.py
--------------------------------------------------------------------------------
/beebot/agents/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | "BaseAgent",
3 | "ResearchAgent",
4 | "GeneralistAgent",
5 | "CodingAgent",
6 | ]
7 |
8 | from beebot.agents.base_agent import BaseAgent
9 | from beebot.agents.coding_agent import CodingAgent
10 | from beebot.agents.generalist_agent import GeneralistAgent
11 | from beebot.agents.research_agent import ResearchAgent
12 |
--------------------------------------------------------------------------------
/beebot/agents/base_agent.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import TYPE_CHECKING
3 |
4 | from beebot.body.pack_utils import functions_detail_list
5 | from beebot.planner.planning_prompt import planning_prompt_template
6 |
7 | if TYPE_CHECKING:
8 | from beebot.execution.task_execution import TaskExecution
9 |
10 |
11 | class BaseAgent:
12 | NAME = ""
13 | PACKS = []
14 | DESCRIPTION = ""
15 |
16 | def __init__(self, task_execution: "TaskExecution"):
17 | self.task_execution = task_execution
18 |
19 | @property
20 | def planning_prompt_template(self) -> str:
21 | return planning_prompt_template()
22 |
23 | @property
24 | def variables(self) -> dict[str, list[str]]:
25 | return self.task_execution.variables
26 |
27 | async def prompt_kwargs(self) -> dict[str, str]:
28 | task = self.task_execution.instructions
29 | variables = self.compile_variables()
30 |
31 | files = []
32 | for document in await self.task_execution.body.file_manager.all_documents():
33 | files.append(document.name)
34 |
35 | functions = functions_detail_list(self.task_execution.packs.values())
36 | history = await self.task_execution.compile_history()
37 |
38 | if files:
39 | file_list = ", ".join(files)
40 | file_section = f"\n# Files\n## The AI Assistant has access to the following files: {file_list}"
41 | else:
42 | file_section = ""
43 |
44 | if history:
45 | history_section = (
46 | "# History:\n## The AI Assistant has a history of functions that the AI Assistant has already executed for this "
47 | f"task. Here is the history, in order, starting with the first function executed:\n{history}"
48 | )
49 | else:
50 | history_section = ""
51 |
52 | return {
53 | "task": task,
54 | "functions": functions,
55 | "file_list": file_section,
56 | "variables": variables,
57 | "history": history_section,
58 | }
59 |
60 | async def planning_prompt(self) -> tuple[str, dict[str, str]]:
61 | prompt_variables = await self.prompt_kwargs()
62 | return (
63 | self.planning_prompt_template.format(**prompt_variables),
64 | prompt_variables,
65 | )
66 |
67 | def compile_variables(self) -> str:
68 | variable_table = []
69 |
70 | for value, names in self.variables.items():
71 | if value:
72 | name_equals = " = ".join(names)
73 | variable_row = f'{name_equals} = """{value}"""'
74 | variable_table.append(variable_row)
75 |
76 | for name, value in self.task_execution.body.global_variables.items():
77 | if value:
78 | variable_row = f'global {name} = """{value}"""'
79 | variable_table.append(variable_row)
80 |
81 | if not variable_table:
82 | return ""
83 |
84 | header = (
85 | "\n# Variables\n## The AI Assistant has access to these variables. Variables are local unless explicitly "
86 | 'declared as global. Each variable is a string with the value enclosed in triple quotes ("""):\n'
87 | )
88 | return header + "\n".join(variable_table)
89 |
90 | async def compile_history(self) -> str:
91 | if not self.task_execution.steps:
92 | return ""
93 |
94 | step_table = []
95 | used_variables = []
96 |
97 | for step in self.task_execution.steps:
98 | if not step.observation:
99 | continue
100 |
101 | outcome = step.observation.response
102 |
103 | variable_names = self.variables.get(outcome)
104 | try:
105 | variable_name = next(
106 | name for name in variable_names if name not in used_variables
107 | )
108 | except StopIteration:
109 | variable_name = variable_names[-1]
110 |
111 | used_variables.append(variable_name)
112 |
113 | tool_arg_list = [
114 | f"{name}={json.dumps(value)}"
115 | for name, value in step.decision.tool_args.items()
116 | ]
117 | tool_args = ", ".join(tool_arg_list)
118 |
119 | if outcome and outcome in self.variables:
120 | step_table.append(
121 | f">>> {variable_name} = {step.decision.tool_name}({tool_args})"
122 | )
123 | else:
124 | step_table.append(f">>> {step.decision.tool_name}({tool_args})")
125 |
126 | if not step.observation.success or outcome.startswith("Error"):
127 | step_table.append(step.observation.error_reason or outcome)
128 | else:
129 | step_table.append(
130 | f"Success! Result stored in local variable {variable_name}."
131 | )
132 |
133 | return "\n".join(step_table)
134 |
--------------------------------------------------------------------------------
/beebot/agents/coding_agent.py:
--------------------------------------------------------------------------------
1 | from beebot.agents.base_agent import BaseAgent
2 | from beebot.packs import (
3 | ExecutePythonFile,
4 | ExecutePythonFileInBackground,
5 | ListProcesses,
6 | KillProcess,
7 | InstallPythonPackage,
8 | Exit,
9 | ExportVariable,
10 | )
11 | from beebot.packs.filesystem.read_file import ReadFile
12 | from beebot.packs.filesystem.write_file import WriteFile
13 | from beebot.packs.http_request.http_request import HttpRequest
14 | from beebot.packs.write_python_code.write_python_file import WritePythonCode
15 |
16 |
17 | class CodingAgent(BaseAgent):
18 | NAME = "Coding Agent"
19 | DESCRIPTION = "Coding Agent: Excels at writing Python code, executing Python code, and managing processes."
20 | PACKS = [
21 | ExecutePythonFile,
22 | ExecutePythonFileInBackground,
23 | ListProcesses,
24 | KillProcess,
25 | InstallPythonPackage,
26 | WritePythonCode,
27 | HttpRequest,
28 | WriteFile,
29 | ReadFile,
30 | Exit,
31 | ExportVariable,
32 | ]
33 |
34 | @property
35 | def planning_prompt_template(self):
36 | return PLANNING_PROMPT_TEMPLATE
37 |
38 | async def prompt_kwargs(self) -> dict[str, str]:
39 | kwargs = await super().prompt_kwargs()
40 | kwargs.pop("file_list")
41 |
42 | structure = []
43 | documents = await self.task_execution.body.file_manager.all_documents()
44 | documents.sort(key=lambda d: d.name)
45 | for document in documents:
46 | indent_count = len(document.name.split("/"))
47 | structure.append(f"{' ' * indent_count}- {document.name}")
48 |
49 | if structure:
50 | kwargs["file_list"] = "\n# Project structure\n" + "\n".join(structure)
51 | else:
52 | kwargs["file_list"] = ""
53 | return {**kwargs}
54 |
55 |
56 | PLANNING_PROMPT_TEMPLATE = """As the Coding Strategist for an AI Assistant, your role is to strategize and plan coding tasks efficiently and effectively. Avoid redundancy, such as unnecessary immediate verification of actions. You excel at writing Python code and can write entire programs at once without using placeholders.
57 |
58 | STDIN / `input()` is not available, take input from argv instead. STDOUT / `print()` is not available, instead save output to files or global variables.
59 |
60 | # Functions
61 | ## The AI Assistant can call only these functions:
62 | {functions}.
63 |
64 | When calling functions provide full data in arguments and avoid using placeholders.
65 |
66 | # Task
67 | ## Your task is:
68 | {task}
69 |
70 | Once the task has been completed, instruct the AI Assistant to call the `exit` function with all arguments to indicate the completion of the task.
71 |
72 | {history}
73 | {variables}
74 | {file_list}
75 |
76 | # Instructions
77 | Now, devise a concise and adaptable coding plan to guide the AI Assistant. Follow these guidelines:
78 |
79 | 1. Ensure you interpret the execution history correctly while considering the order of execution. Trust the accuracy of past function executions.
80 | 2. Recognize when the task has been successfully completed. If the task has been completed, instruct the AI Assistant to call the `exit` function.
81 | 3. Analyze the task carefully to distinguish between actions that the code should take and actions that the AI Assistant should take.
82 | 4. Determine the most efficient next action towards completing the task, considering your current information, requirements, and available functions.
83 | 5. Direct the execution of the immediate next action using exactly one of the callable functions, making sure to provide all necessary code without placeholders.
84 |
85 | Provide a concise analysis of the past history, followed by an overview of your plan going forward, and end with one sentence describing the immediate next action to be taken."""
86 |
--------------------------------------------------------------------------------
/beebot/agents/generalist_agent.py:
--------------------------------------------------------------------------------
1 | from beebot.agents import BaseAgent
2 | from beebot.packs import GetWebsiteTextContent, ExportVariable, Exit
3 | from beebot.packs.disk_usage import DiskUsage
4 | from beebot.packs.filesystem.read_file import ReadFile
5 | from beebot.packs.filesystem.write_file import WriteFile
6 | from beebot.packs.google_search import GoogleSearch
7 | from beebot.packs.http_request.http_request import HttpRequest
8 | from beebot.packs.os_info.os_info import OSInfo
9 | from beebot.packs.wikipedia_summarize import WikipediaPack
10 |
11 |
12 | class GeneralistAgent(BaseAgent):
13 | NAME = "Generalist Agent"
14 | PACKS = [
15 | WikipediaPack,
16 | GoogleSearch,
17 | GetWebsiteTextContent,
18 | OSInfo,
19 | DiskUsage,
20 | HttpRequest,
21 | WriteFile,
22 | ReadFile,
23 | Exit,
24 | ExportVariable,
25 | ]
26 | DESCRIPTION = ""
27 |
--------------------------------------------------------------------------------
/beebot/agents/research_agent.py:
--------------------------------------------------------------------------------
1 | from beebot.agents.base_agent import BaseAgent
2 | from beebot.packs import GetWebsiteTextContent, Exit, ExportVariable
3 | from beebot.packs.filesystem.read_file import ReadFile
4 | from beebot.packs.filesystem.write_file import WriteFile
5 | from beebot.packs.google_search import GoogleSearch
6 | from beebot.packs.wikipedia_summarize import WikipediaPack
7 | from beebot.packs.wolframalpha_query import WolframAlphaQuery
8 |
9 |
10 | class ResearchAgent(BaseAgent):
11 | NAME = "Research Agent"
12 | DESCRIPTION = "Research Agent: Excels at searching for sources of information and analyzing those sources"
13 | PACKS = [
14 | WikipediaPack,
15 | GoogleSearch,
16 | GetWebsiteTextContent,
17 | WolframAlphaQuery,
18 | WriteFile,
19 | ReadFile,
20 | Exit,
21 | ExportVariable,
22 | ]
23 |
24 | @property
25 | def planning_prompt_template(self):
26 | return PLANNING_PROMPT_TEMPLATE
27 |
28 |
29 | PLANNING_PROMPT_TEMPLATE = """As the Research Strategist for an AI Assistant, your role is to strategize and plan research tasks efficiently and effectively. Avoid redundancy, such as unnecessary immediate verification of actions.
30 |
31 | # Functions
32 | # The AI Assistant have these functions at their disposal:
33 | {functions}.
34 |
35 | When calling functions provide sufficient detail in arguments to ensure the data returned is unambiguously related to your research task.
36 |
37 | # Task
38 | ## The research task is:
39 | {task}
40 |
41 | Once the research task has been completed, instruct the AI Assistant to call the `exit` function with all arguments to indicate the completion of the research.
42 |
43 | {history}
44 | {variables}
45 | {file_list}
46 |
47 | # Instructions
48 | Now, devise a concise and adaptable research plan to guide the AI Assistant. Follow these guidelines:
49 |
50 | 1. Ensure you interpret the execution history correctly while considering the order of execution. Avoid repetitive actions, especially when the outcomes are clear and confirmed by the previous functions. Trust the accuracy of past function executions, assuming the state of the system and your research workspace remain consistent with the historical outcomes.
51 | 2. Recognize when the research task has been successfully completed. If the task has been completed, instruct the AI Assistant to call the `exit` function.
52 | 3. Determine the next logical step in the research task, considering your current information, requirements, and available functions.
53 | 4. Direct the execution of the immediate next action using exactly one of the available functions, making sure to skip any redundant actions that are already confirmed by the historical context.
54 |
55 | Provide a concise analysis of the past history, followed by a step-by-step summary of your plan going forward, and end with one sentence describing the immediate next action to be taken."""
56 |
--------------------------------------------------------------------------------
/beebot/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/api/__init__.py
--------------------------------------------------------------------------------
/beebot/api/routes.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from fastapi import HTTPException
4 | from starlette.requests import Request
5 | from starlette.responses import JSONResponse
6 |
7 | from beebot.body import Body
8 | from beebot.execution import Step
9 | from beebot.models.database_models import BodyModel, StepModel
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | async def body_response(body: Body) -> JSONResponse:
15 | artifacts = [
16 | {"name": document.name, "content": document.content}
17 | for document in await body.file_manager.all_documents()
18 | ]
19 | return JSONResponse(
20 | {
21 | "task_id": str(body.model_object.id),
22 | "input": body.task,
23 | "artifacts": artifacts,
24 | }
25 | )
26 |
27 |
28 | async def step_response(step: Step, body: Body) -> JSONResponse:
29 | artifacts = [
30 | {"name": document.name, "content": document.content}
31 | for document in await body.file_manager.all_documents()
32 | ]
33 | step_output = {
34 | "plan": step.plan.json(),
35 | "decision": step.decision.json(),
36 | "observation": step.observation.json(),
37 | "reversible": step.reversible,
38 | }
39 | return JSONResponse(
40 | {
41 | "step_id": str(step.model_object.id),
42 | "task_id": str(body.model_object.id),
43 | "output": step_output,
44 | "artifacts": artifacts,
45 | "is_last": body.is_done,
46 | }
47 | )
48 |
49 |
50 | async def create_agent_task(request: Request) -> JSONResponse:
51 | request_data = await request.json()
52 | body = Body(request_data.get("input"))
53 | await body.setup()
54 |
55 | return await body_response(body)
56 |
57 |
58 | async def execute_agent_task_step(request: Request) -> JSONResponse:
59 | task_id = request.path_params.get("task_id")
60 | try:
61 | body_model = await BodyModel.get(id=int(task_id)).prefetch_related(
62 | BodyModel.INCLUSIVE_PREFETCH
63 | )
64 | except ValueError:
65 | logger.error(f"Body ID {task_id} is invalid")
66 | raise HTTPException(status_code=404, detail="Invalid Task ID")
67 |
68 | if not body_model:
69 | logger.error(f"Body with ID {task_id} not found")
70 | raise HTTPException(status_code=404, detail="Task not found")
71 |
72 | body = await Body.from_model(body_model)
73 | step = await body.cycle()
74 | if not step:
75 | raise HTTPException(status_code=400, detail="Task is complete")
76 |
77 | return await step_response(step, body)
78 |
79 |
80 | async def agent_task_ids(request: Request) -> JSONResponse:
81 | bodies = await BodyModel.filter()
82 | return JSONResponse([str(body.id) for body in bodies])
83 |
84 |
85 | async def get_agent_task(request: Request) -> JSONResponse:
86 | task_id = request.path_params.get("task_id")
87 | body_model = await BodyModel.get(id=int(task_id)).prefetch_related(
88 | BodyModel.INCLUSIVE_PREFETCH
89 | )
90 |
91 | if not body_model:
92 | raise HTTPException(status_code=400, detail="Task not found")
93 |
94 | return await body_response(await Body.from_model(body_model))
95 |
96 |
97 | async def list_agent_task_steps(request: Request) -> JSONResponse:
98 | task_id = request.path_params.get("task_id")
99 | body_model = await BodyModel.get(id=int(task_id)).prefetch_related(
100 | BodyModel.INCLUSIVE_PREFETCH
101 | )
102 | body = await Body.from_model(body_model)
103 |
104 | if not body_model:
105 | raise HTTPException(status_code=400, detail="Task not found")
106 |
107 | step_ids = [
108 | m.id for m in await body.current_task_execution.model_object.steps.all()
109 | ]
110 |
111 | return JSONResponse(step_ids)
112 |
113 |
114 | async def get_agent_task_step(request: Request) -> JSONResponse:
115 | task_id = request.path_params.get("task_id")
116 | body_model = await BodyModel.get(id=int(task_id)).prefetch_related(
117 | BodyModel.INCLUSIVE_PREFETCH
118 | )
119 |
120 | if not body_model:
121 | raise HTTPException(status_code=400, detail="Task not found")
122 |
123 | step_id = request.path_params.get("step_id")
124 | step_model = await StepModel.get(id=int(step_id))
125 |
126 | if not step_model:
127 | raise HTTPException(status_code=400, detail="Step not found")
128 |
129 | body = await Body.from_model(body_model)
130 | step = await Step.from_model(step_model)
131 | return await step_response(step, body)
132 |
--------------------------------------------------------------------------------
/beebot/api/websocket.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 | import logging
4 | from json import JSONDecodeError
5 | from typing import Any
6 |
7 | import psycopg2
8 | from psycopg2._psycopg import connection
9 | from starlette.websockets import WebSocket
10 |
11 | from beebot.config import Config
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | async def producer(conn: connection) -> dict[str, dict[str, Any]]:
17 | while True:
18 | conn.poll()
19 | if conn.notifies:
20 | notify = conn.notifies.pop(0)
21 | try:
22 | parsed_payload = json.loads(notify.payload)
23 | return {notify.channel: parsed_payload}
24 | except JSONDecodeError as e:
25 | logger.error(f"Invalid NOTIFY payload received {e}: {notify.payload}")
26 |
27 | await asyncio.sleep(0.1)
28 |
29 |
30 | async def websocket_endpoint(websocket: WebSocket):
31 | await websocket.accept()
32 | config = Config.global_config()
33 | conn = psycopg2.connect(config.database_url)
34 | conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
35 |
36 | curs = conn.cursor()
37 | curs.execute("LISTEN beebot_notifications;")
38 | await websocket.send_json({"ready": True})
39 | while True:
40 | try:
41 | await websocket.send_json({"poll": True})
42 | notify = await producer(conn)
43 | if not notify:
44 | # Connection closed
45 | break
46 |
47 | await websocket.send_json(notify)
48 |
49 | except (SystemExit, KeyboardInterrupt):
50 | raise
51 | except BaseException as e:
52 | logger.error(f"Unknown error occurred in websocket connection: {e}")
53 | await asyncio.sleep(0.1)
54 |
--------------------------------------------------------------------------------
/beebot/body/__init__.py:
--------------------------------------------------------------------------------
1 | from .body import Body
2 |
3 | __all__ = ["Body"]
4 |
--------------------------------------------------------------------------------
/beebot/body/body.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os.path
3 | import subprocess
4 | from typing import Union
5 |
6 | import baserun
7 | from langchain.chat_models.base import BaseChatModel
8 |
9 | from beebot.body.llm import create_llm
10 | from beebot.config import Config
11 | from beebot.config.database_file_manager import DatabaseFileManager
12 | from beebot.decomposer.decomposer import Decomposer
13 | from beebot.execution import Step
14 | from beebot.execution.task_execution import TaskExecution
15 | from beebot.models.database_models import (
16 | BodyModel,
17 | initialize_db,
18 | )
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 |
23 | class Body:
24 | task: str
25 | processes: dict[int, subprocess.Popen]
26 | # Variables set / exported by subtasks
27 | global_variables: dict[str, str]
28 |
29 | task_executions = list[TaskExecution]
30 |
31 | decomposer_llm: BaseChatModel
32 | planner_llm: BaseChatModel
33 | decider_llm: BaseChatModel
34 |
35 | decomposer: Decomposer
36 | config: Config
37 |
38 | model_object: BodyModel = None
39 | file_manager: DatabaseFileManager = None
40 |
41 | def __init__(self, task: str = "", config: Config = None):
42 | self.task = task
43 | self.config = config or Config.global_config()
44 |
45 | self.decomposer_llm = create_llm(self.config, self.config.decomposer_model)
46 | self.planner_llm = create_llm(self.config, self.config.planner_model)
47 | self.decider_llm = create_llm(self.config, self.config.decider_model)
48 | self.decomposer = Decomposer(body=self)
49 | self.task_executions = []
50 | self.processes = {}
51 | self.global_variables = {}
52 |
53 | if not os.path.exists(self.config.workspace_path):
54 | os.makedirs(self.config.workspace_path, exist_ok=True)
55 |
56 | @classmethod
57 | async def from_model(cls, body_model: BodyModel):
58 | body = cls(task=body_model.task)
59 | body.model_object = body_model
60 |
61 | body.file_manager = DatabaseFileManager(
62 | config=body.config.pack_config, body=body
63 | )
64 |
65 | for execution_model in await body_model.task_executions.all():
66 | body.task_executions.append(
67 | await TaskExecution.from_model(body, execution_model)
68 | )
69 |
70 | await body.setup_file_manager()
71 |
72 | return body
73 |
74 | @property
75 | def current_task_execution(self) -> Union[TaskExecution, None]:
76 | try:
77 | return next(
78 | execution
79 | for execution in self.task_executions
80 | if not execution.complete
81 | )
82 | except StopIteration:
83 | return None
84 |
85 | @property
86 | def is_done(self):
87 | return self.current_task_execution is None
88 |
89 | async def setup(self):
90 | """These are here instead of init because they involve network requests. The order is very specific because
91 | of when the database and file manager are instantiated / set up"""
92 | # TODO: Remove duplication between this method and `from_model`
93 | await initialize_db(self.config.database_url)
94 |
95 | self.file_manager = DatabaseFileManager(
96 | config=self.config.pack_config, body=self
97 | )
98 |
99 | await self.decompose_task()
100 |
101 | if not self.model_object:
102 | self.model_object = BodyModel(task=self.task)
103 | await self.save()
104 |
105 | await self.current_task_execution.create_new_step()
106 |
107 | await self.setup_file_manager()
108 | await self.file_manager.load_from_directory()
109 |
110 | await self.save()
111 |
112 | async def cycle(self) -> Step:
113 | """Step through one decide-execute-plan loop"""
114 | if self.is_done:
115 | return
116 |
117 | task_execution = self.current_task_execution
118 | step = await task_execution.cycle()
119 |
120 | # If this subtask is complete, prime the next subtask
121 | if task_execution.complete:
122 | next_execution = self.current_task_execution
123 | # We're done
124 | if not next_execution:
125 | return None
126 |
127 | if not next_execution.current_step:
128 | await next_execution.create_new_step()
129 |
130 | if next_execution:
131 | documents = await task_execution.current_step.documents
132 | for name, document in documents.items():
133 | if name in next_execution.inputs:
134 | await next_execution.current_step.add_document(document)
135 |
136 | await self.save()
137 |
138 | baserun.log(
139 | "CycleComplete",
140 | payload={
141 | "oversight": step.oversight.__dict__ if step.oversight else None,
142 | "plan": step.plan.__dict__ if step.plan else None,
143 | "decision": step.decision.__dict__ if step.decision else None,
144 | "observation": step.observation.__dict__ if step.observation else None,
145 | },
146 | )
147 |
148 | return step
149 |
150 | async def decompose_task(self):
151 | """Turn the initial task into a task that is easier for AI to more consistently understand"""
152 | subtasks = await self.decomposer.decompose()
153 | for subtask in subtasks:
154 | execution = TaskExecution(
155 | body=self,
156 | agent_name=subtask.agent,
157 | inputs=subtask.inputs,
158 | outputs=subtask.outputs,
159 | instructions=subtask.instructions,
160 | complete=subtask.complete,
161 | )
162 | await execution.get_packs()
163 | self.task_executions.append(execution)
164 |
165 | async def save(self):
166 | if not self.model_object:
167 | await self.setup()
168 |
169 | self.model_object.task = self.task
170 | await self.model_object.save()
171 |
172 | await self.current_task_execution.save()
173 | await self.file_manager.flush_to_directory(self.config.workspace_path)
174 |
175 | async def setup_file_manager(self):
176 | if not self.file_manager:
177 | self.file_manager = DatabaseFileManager(
178 | config=self.config.pack_config, body=self
179 | )
180 | await self.file_manager.load_from_directory()
181 |
182 | self.config.pack_config.filesystem_manager = self.file_manager
183 |
--------------------------------------------------------------------------------
/beebot/body/llm.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from dataclasses import dataclass
4 | from typing import TYPE_CHECKING, Any
5 |
6 | import openai
7 | from autopack.utils import format_packs_to_openai_functions
8 | from langchain.chat_models import ChatOpenAI
9 | from langchain.schema import SystemMessage
10 |
11 | from beebot.config.config import Config
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 | if TYPE_CHECKING:
16 | from beebot.body import Body
17 |
18 |
19 | @dataclass
20 | class LLMResponse:
21 | text: str
22 | function_call: dict[str, Any]
23 |
24 |
25 | def create_llm(config: Config, model_name: str):
26 | headers = {}
27 | if config.openai_api_base:
28 | openai.api_base = config.openai_api_base
29 | if config.helicone_key:
30 | logger.info("Using helicone to make requests with cache enabled.")
31 | headers["Helicone-Auth"] = f"Bearer {config.helicone_key}"
32 | headers["Helicone-Cache-Enabled"] = "true"
33 |
34 | llm = ChatOpenAI(
35 | model_name=model_name,
36 | # temperature=0,
37 | model_kwargs={"headers": headers, "top_p": 0.1},
38 | )
39 | return llm
40 |
41 |
42 | async def call_llm(
43 | body: "Body",
44 | message: str,
45 | function_call: str = "auto",
46 | include_functions: bool = True,
47 | disregard_cache: bool = False,
48 | llm=None,
49 | ) -> LLMResponse:
50 | llm = llm or body.planner_llm
51 | output_kwargs = {}
52 |
53 | if include_functions and body.current_task_execution.packs:
54 | output_kwargs["functions"] = format_packs_to_openai_functions(
55 | body.current_task_execution.packs.values()
56 | )
57 | output_kwargs["function_call"] = function_call
58 |
59 | if disregard_cache:
60 | output_kwargs["headers"] = {"Helicone-Cache-Enabled": "false"}
61 |
62 | logger.debug(f"~~ LLM Request ~~\n{message}")
63 | response = await llm.agenerate(
64 | messages=[[SystemMessage(content=message)]], **output_kwargs
65 | )
66 |
67 | # TODO: This should be a nicer error message if we get an unexpected number of generations
68 | generation = response.generations[0][0]
69 | function_called = {}
70 | if (
71 | generation
72 | and hasattr(generation, "message")
73 | and generation.message.additional_kwargs
74 | ):
75 | function_called = generation.message.additional_kwargs.get("function_call", {})
76 |
77 | logger.debug(f"~~ LLM Response ~~\n{generation.text}")
78 | logger.debug(json.dumps(function_called))
79 | return LLMResponse(text=generation.text, function_call=function_called)
80 |
--------------------------------------------------------------------------------
/beebot/body/pack_utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import subprocess
3 | from typing import TYPE_CHECKING
4 |
5 | from autopack.pack import Pack
6 |
7 | from beebot.body.llm import call_llm
8 |
9 | if TYPE_CHECKING:
10 | from beebot.body import Body
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 | # TODO: This should be a config value?
15 | SUPPRESSED_PACKS = ["list_files", "delete_file"]
16 |
17 |
18 | def llm_wrapper(body: "Body") -> str:
19 | async def llm(prompt) -> str:
20 | response = await call_llm(
21 | body, prompt, include_functions=False, function_call="none"
22 | )
23 | return response.text
24 |
25 | return llm
26 |
27 |
28 | def init_workspace_poetry(workspace_path: str):
29 | """Make sure poetry is init'd in the workspace. The command errors if it is already init'd so just swallow the
30 | errors"""
31 | subprocess.run(
32 | ["poetry", "init", "--name", "beebot_workspace", "-n"],
33 | cwd=workspace_path,
34 | stdout=subprocess.PIPE,
35 | stderr=subprocess.PIPE,
36 | )
37 |
38 |
39 | def functions_detail_list(packs: list["Pack"]) -> str:
40 | pack_details = []
41 | for pack in packs:
42 | pack_args = []
43 | for arg_data in pack.args.values():
44 | arg_type = arg_data.get("type")
45 | if arg_type == "string":
46 | arg_type = "str"
47 | if arg_type == "boolean":
48 | arg_type = "bool"
49 | if arg_type == "number":
50 | arg_type = "int"
51 | pack_args.append(f"{arg_data.get('name')}: {arg_type}")
52 |
53 | pack_details.append(f"{pack.name}({', '.join(pack_args)})")
54 |
55 | return "\n".join(pack_details)
56 |
--------------------------------------------------------------------------------
/beebot/config/__init__.py:
--------------------------------------------------------------------------------
1 | from .config import Config
2 |
3 | __all__ = ["Config"]
4 |
--------------------------------------------------------------------------------
/beebot/config/config.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from typing import ClassVar
4 |
5 | import coloredlogs
6 | from autopack.pack_config import PackConfig
7 | from openai.util import logger as openai_logger
8 | from pydantic import BaseSettings # IDEAL_MODEL = "gpt-4-0613"
9 | import openai
10 |
11 | DEFAULT_DECOMPOSER_MODEL = "gpt-4"
12 | FALLBACK_DECOMPOSER_MODEL = "gpt-3.5-turbo-16k-0613"
13 | DEFAULT_PLANNER_MODEL = "gpt-3.5-turbo-16k-0613"
14 | DEFAULT_DECIDER_MODEL = "gpt-3.5-turbo-16k-0613"
15 | LOG_FORMAT = (
16 | "%(levelname)s %(asctime)s.%(msecs)03d %(filename)s:%(lineno)d- %(message)s"
17 | )
18 |
19 |
20 | class Config(BaseSettings):
21 | log_level: str = "INFO"
22 |
23 | openai_api_key: str = None
24 | helicone_key: str = None
25 | openai_api_base: str = None
26 | gmail_credentials_file: str = "credentials.json"
27 | decomposer_model: str = DEFAULT_DECOMPOSER_MODEL
28 | planner_model: str = DEFAULT_PLANNER_MODEL
29 | decider_model: str = DEFAULT_DECIDER_MODEL
30 | database_url: str = "sqlite://:memory:"
31 |
32 | workspace_path: str = "workspace"
33 | hard_exit: bool = False
34 | restrict_code_execution: bool = False
35 | process_timeout: int = 30
36 | pack_config: PackConfig = None
37 |
38 | _global_config: ClassVar["Config"] = None
39 |
40 | class Config:
41 | env_prefix = "beebot_"
42 | fields = {
43 | "database_url": {"env": "database_url"},
44 | "gmail_credentials_file": {"env": "gmail_credentials_file"},
45 | "helicone_key": {"env": "helicone_key"},
46 | "log_level": {"env": "log_level"},
47 | "openai_api_base": {"env": "openai_api_base"},
48 | "openai_api_key": {"env": "openai_api_key"},
49 | }
50 |
51 | def __init__(self, **kwargs) -> "Config":
52 | super().__init__(**kwargs)
53 | self.configure_autopack()
54 | self.setup_logging()
55 | self.configure_decomposer_model()
56 |
57 | def configure_decomposer_model(self):
58 | if self.decomposer_model == DEFAULT_DECOMPOSER_MODEL:
59 | model_ids = [model["id"] for model in openai.Model.list()["data"]]
60 | if self.decomposer_model not in model_ids:
61 | self.decomposer_model = FALLBACK_DECOMPOSER_MODEL
62 |
63 | def configure_autopack(self, is_global: bool = True):
64 | pack_config = PackConfig(
65 | workspace_path=self.workspace_path,
66 | restrict_code_execution=self.restrict_code_execution,
67 | )
68 |
69 | self.pack_config = pack_config
70 |
71 | if is_global:
72 | PackConfig.set_global_config(pack_config)
73 |
74 | def setup_logging(self) -> logging.Logger:
75 | os.makedirs("logs", exist_ok=True)
76 | console_handler = logging.StreamHandler()
77 | console_handler.setLevel(self.log_level)
78 |
79 | file_handler = logging.FileHandler("logs/debug.log")
80 | file_handler.setLevel("DEBUG")
81 | file_handler.setFormatter(logging.Formatter(LOG_FORMAT))
82 |
83 | logging.basicConfig(
84 | level=self.log_level,
85 | format=LOG_FORMAT,
86 | datefmt="%H:%M:%S",
87 | handlers=[
88 | console_handler,
89 | file_handler,
90 | ],
91 | )
92 |
93 | coloredlogs.install(
94 | level=self.log_level,
95 | fmt=LOG_FORMAT,
96 | datefmt="%H:%M:%S",
97 | )
98 |
99 | # OpenAI will log a jsonified version of each request/response to `logger.debug` and we have our own logs
100 | # which are better formatted
101 | openai_logger.propagate = False
102 |
103 | @classmethod
104 | def set_global_config(cls, config_obj: "Config" = None) -> "Config":
105 | """
106 | Optionally set a global config object that can be used anywhere (You can still attach a separate instance to
107 | each Body)
108 | """
109 | cls._global_config = config_obj or cls()
110 | return cls._global_config
111 |
112 | @classmethod
113 | def global_config(cls) -> "Config":
114 | return cls._global_config or cls.set_global_config()
115 |
--------------------------------------------------------------------------------
/beebot/config/database_file_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from typing import TYPE_CHECKING, Union
4 |
5 | from autopack.filesystem_emulation.file_manager import FileManager
6 | from autopack.pack_config import PackConfig
7 |
8 | from beebot.execution import Step
9 | from beebot.models.database_models import DocumentModel, DocumentStep
10 |
11 | if TYPE_CHECKING:
12 | from beebot.body import Body
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | IGNORE_FILES = ["poetry.lock", "pyproject.toml", "__pycache__"]
17 |
18 |
19 | class DatabaseFileManager(FileManager):
20 | """
21 | This class emulates a filesystem in Postgres, storing files in a simple `document` table with a many-to-many
22 | relationship with `execution`. Recommended unless you specifically need to access the documents in the filesystem.
23 | """
24 |
25 | def __init__(
26 | self, config: PackConfig = PackConfig.global_config(), body: "Body" = None
27 | ):
28 | super().__init__(config)
29 | self.body = body
30 | self.files = {}
31 |
32 | @property
33 | def current_step(self) -> Union[Step, None]:
34 | task = self.body.current_task_execution or self.body.task_executions[-1]
35 | if not task:
36 | return None
37 | try:
38 | return task.steps[-1]
39 | except IndexError:
40 | return None
41 |
42 | # Can't support sync db access without a lot of headaches
43 | def read_file(self, *args, **kwargs):
44 | raise NotImplementedError
45 |
46 | def write_file(self, *args, **kwargs):
47 | raise NotImplementedError
48 |
49 | def delete_file(self, *args, **kwargs):
50 | raise NotImplementedError
51 |
52 | def list_files(self, *args, **kwargs):
53 | raise NotImplementedError
54 |
55 | async def aread_file(self, file_path: str) -> str:
56 | """Reads a file from the virtual file system in RAM.
57 |
58 | Args:
59 | file_path (str): The path to the file to be read.
60 |
61 | Returns:
62 | str: The content of the file. If the file does not exist, returns an error message.
63 | """
64 | documents = await self.current_step.documents
65 | document = documents.get(file_path)
66 | if document:
67 | return document.content
68 | else:
69 | nonlocal_file = await DocumentModel.get_or_none(name=file_path)
70 | if nonlocal_file:
71 | return nonlocal_file.content
72 | return "Error: File not found"
73 |
74 | async def awrite_file(self, file_path: str, content: str) -> str:
75 | """Writes to a file in the virtual file system in RAM.
76 |
77 | Args:
78 | file_path (str): The path to the file to be written to.
79 | content (str): The content to be written to the file.
80 |
81 | Returns:
82 | str: A success message indicating the file was written.
83 | """
84 | if not self.current_step:
85 | return ""
86 |
87 | document, _created = await DocumentModel.get_or_create(
88 | name=file_path, content=content
89 | )
90 |
91 | stale_link = await DocumentStep.filter(
92 | document__name=file_path, step=self.current_step.model_object
93 | ).first()
94 |
95 | if stale_link:
96 | logger.warning(f"Deleting stale link ID {stale_link.id}")
97 | await stale_link.delete()
98 |
99 | await self.current_step.add_document(document)
100 | return f"Successfully wrote {len(content.encode('utf-8'))} bytes to {file_path}"
101 |
102 | async def adelete_file(self, file_path: str) -> str:
103 | """Deletes a file from the virtual file system in RAM.
104 |
105 | Args:
106 | file_path (str): The path to the file to be deleted.
107 |
108 | Returns:
109 | str: A success message indicating the file was deleted. If the file does not exist, returns an error message.
110 | """
111 | if not self.current_step:
112 | return ""
113 |
114 | document = await DocumentModel.get_or_none(name=file_path)
115 | if not document:
116 | return f"Error: File not found '{file_path}'"
117 |
118 | document_step = await DocumentStep.filter(
119 | document=document, step=self.current_step.model_object
120 | ).first()
121 |
122 | if document_step:
123 | await document.delete()
124 | else:
125 | logger.warning(
126 | f"File {file_path} was supposed to be deleted but it does not exist"
127 | )
128 |
129 | return f"Successfully deleted file {file_path}."
130 |
131 | async def alist_files(self, dir_path: str) -> str:
132 | """Lists all files in the specified directory in the virtual file system in RAM.
133 |
134 | Args:
135 | dir_path (str): The path to the directory to list files from.
136 |
137 | Returns:
138 | str: A list of all files in the directory. If the directory does not exist, returns an error message.
139 | """
140 | if not self.current_step:
141 | return ""
142 |
143 | document_steps = await DocumentStep.filter(
144 | step=self.current_step.model_object
145 | ).prefetch_related("document")
146 |
147 | file_paths = [dm.document.name for dm in document_steps]
148 |
149 | files_in_dir = [
150 | file_path
151 | for file_path in file_paths
152 | if file_path.startswith(dir_path) and file_path not in self.IGNORE_FILES
153 | ]
154 | if files_in_dir:
155 | return "\n".join(files_in_dir)
156 | else:
157 | return f"Error: No such directory {dir_path}."
158 |
159 | async def all_documents(self) -> list[DocumentModel]:
160 | if not self.current_step:
161 | return []
162 | document_steps = await DocumentStep.filter(
163 | step=self.current_step.model_object
164 | ).prefetch_related("document")
165 |
166 | return [dm.document for dm in document_steps]
167 |
168 | async def load_from_directory(self, directory: str = None):
169 | if not directory:
170 | directory = self.body.config.workspace_path
171 |
172 | for file in os.listdir(directory):
173 | abs_path = os.path.abspath(os.path.join(directory, file.replace("/", "_")))
174 | if not os.path.isdir(abs_path) and file not in IGNORE_FILES:
175 | with open(abs_path, "r") as f:
176 | await self.awrite_file(file, f.read())
177 |
178 | async def flush_to_directory(self, directory: str = None):
179 | if not self.current_step:
180 | return
181 |
182 | if not directory:
183 | directory = self.body.config.workspace_path
184 |
185 | all_documents = await self.all_documents()
186 | for document in all_documents:
187 | with open(
188 | os.path.join(directory, document.name.replace("/", "_")), "w+"
189 | ) as f:
190 | f.write(document.content)
191 |
--------------------------------------------------------------------------------
/beebot/decider/__init__.py:
--------------------------------------------------------------------------------
1 | from .decider import Decider
2 |
3 | __all__ = ["Decider"]
4 |
--------------------------------------------------------------------------------
/beebot/decider/decider.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from json import JSONDecodeError
4 | from typing import TYPE_CHECKING, Any
5 |
6 | from beebot.body.llm import call_llm, LLMResponse
7 | from beebot.body.pack_utils import functions_detail_list
8 | from beebot.decider.deciding_prompt import decider_template
9 | from beebot.models.database_models import Decision, Oversight
10 |
11 | if TYPE_CHECKING:
12 | from beebot.execution.task_execution import TaskExecution
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | RETRY_LIMIT = 3
17 |
18 |
19 | class Decider:
20 | """
21 | The Decider is in charge of taking the Plan and deciding the next step
22 | """
23 |
24 | task_execution: "TaskExecution"
25 |
26 | def __init__(self, task_execution: "TaskExecution"):
27 | self.task_execution = task_execution
28 |
29 | async def decide(
30 | self, oversight: Oversight, disregard_cache: bool = False
31 | ) -> Decision:
32 | """Take a Plan and send it to the LLM, returning it back to the Body"""
33 | # TODO: Get from agent?
34 | prompt_variables = {
35 | "plan": oversight.modified_plan_text,
36 | "task": self.task_execution.instructions,
37 | "variables": self.task_execution.compile_variables(),
38 | "history": await self.task_execution.compile_history(),
39 | "functions": functions_detail_list(self.task_execution.packs.values()),
40 | }
41 | prompt = decider_template().format(**prompt_variables)
42 |
43 | response = await call_llm(
44 | self.task_execution.body,
45 | prompt,
46 | disregard_cache=disregard_cache,
47 | llm=self.task_execution.body.decider_llm,
48 | )
49 |
50 | logger.info("\n=== Decision received from LLM ===")
51 | if response and response.text:
52 | logger.info(response.text)
53 | logger.info(json.dumps(response.function_call, indent=4))
54 |
55 | return await interpret_llm_response(
56 | prompt_variables=prompt_variables, response=response
57 | )
58 |
59 | async def decide_with_retry(
60 | self, oversight: Oversight, retry_count: int = 0
61 | ) -> Decision:
62 | if retry_count:
63 | oversight = Oversight(
64 | prompt_variables=oversight.prompt_variables,
65 | modified_plan_text=oversight.modified_plan_text
66 | + (
67 | "\n\nWarning: Invalid response received. Please reassess your strategy."
68 | ),
69 | )
70 |
71 | try:
72 | return await self.decide(oversight, disregard_cache=retry_count > 0)
73 | except ValueError:
74 | logger.warning("Got invalid response from LLM, retrying...")
75 | if retry_count >= RETRY_LIMIT:
76 | raise ValueError(f"Got invalid response {RETRY_LIMIT} times in a row")
77 | return await self.decide_with_retry(
78 | oversight=oversight, retry_count=retry_count + 1
79 | )
80 |
81 |
82 | async def interpret_llm_response(
83 | prompt_variables: dict[str, str], response: LLMResponse
84 | ) -> Decision:
85 | if response.function_call:
86 | tool_name, tool_args = parse_function_call_args(response.function_call)
87 |
88 | decision = Decision(
89 | reasoning=response.text,
90 | tool_name=tool_name,
91 | tool_args=tool_args,
92 | prompt_variables=prompt_variables,
93 | response=response.text,
94 | )
95 | await decision.save()
96 | return decision
97 | else:
98 | raise ValueError("No decision supplied")
99 |
100 |
101 | def parse_function_call_args(
102 | function_call_args: dict[str, Any]
103 | ) -> tuple[str, dict[str, Any]]:
104 | if not function_call_args:
105 | raise ValueError("No function given")
106 |
107 | tool_name = function_call_args.get("name")
108 | try:
109 | parsed_tool_args = json.loads(function_call_args.get("arguments"))
110 | return tool_name, parsed_tool_args
111 | except JSONDecodeError:
112 | return tool_name, {"output": function_call_args.get("arguments")}
113 |
--------------------------------------------------------------------------------
/beebot/decider/deciding_prompt.py:
--------------------------------------------------------------------------------
1 | TEMPLATE = """You are an Autonomous AI Assistant executor. Your responsibility is to interpret the provided plan and execute the next function.
2 |
3 | # Functions
4 | You have these functions at your disposal:
5 | {functions}.
6 |
7 | # Task
8 | Your original task, given by the human, is:
9 | {task}
10 |
11 | # History
12 | You have a history of functions that the AI Assistant has already executed for this task. Here is the history, in order, starting with the first function executed:
13 | {history}
14 | {variables}
15 |
16 | # Plan
17 | {plan}
18 |
19 | Follow these guidelines:
20 | 1. Study your high-level plan, and understand the next step in it.
21 | 2. Implement the next action by using exactly one of the provided functions. Be sure to fully expand variables and avoid the use of placeholders.
22 |
23 | Proceed with executing the next step from the plan. Use exactly one of the provided functions through the `function_call` parameter of your response."""
24 |
25 |
26 | def decider_template() -> str:
27 | return TEMPLATE
28 |
--------------------------------------------------------------------------------
/beebot/decomposer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/decomposer/__init__.py
--------------------------------------------------------------------------------
/beebot/decomposer/decomposer.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | from json import JSONDecodeError
5 | from typing import TYPE_CHECKING
6 |
7 | from pydantic import Field, BaseModel
8 |
9 | from beebot.body.llm import call_llm
10 | from beebot.config.database_file_manager import IGNORE_FILES
11 | from beebot.decomposer.decomposer_prompt import decomposer_prompt
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 | if TYPE_CHECKING:
16 | from beebot.body import Body
17 |
18 |
19 | class Subtask(BaseModel):
20 | agent: str
21 | instructions: str
22 | inputs: list[str] = Field(default_factory=list)
23 | outputs: list[str] = Field(default_factory=list)
24 | complete: bool = False
25 |
26 |
27 | class Decomposer:
28 | body: "Body"
29 | task: str
30 | subtasks: list[Subtask]
31 |
32 | def __init__(self, body: "Body"):
33 | self.body = body
34 | self.task = body.task
35 | self.subtasks = []
36 |
37 | async def decompose(self) -> list[Subtask]:
38 | from beebot.agents import BaseAgent
39 |
40 | prompt_template = decomposer_prompt()
41 | agents = [agent for agent in BaseAgent.__subclasses__() if agent.DESCRIPTION]
42 | agent_list = "- " + "\n- ".join([agent.DESCRIPTION for agent in agents])
43 | prompt = prompt_template.format(
44 | agent_list=agent_list, task=self.task, files=self.starting_files()
45 | )
46 |
47 | logger.info("\n=== Task Decomposition given to LLM ===")
48 | logger.info(prompt)
49 |
50 | response = await call_llm(
51 | self.body,
52 | prompt,
53 | include_functions=False,
54 | function_call="none",
55 | llm=self.body.decomposer_llm,
56 | )
57 | logger.info("\n=== Task Decomposition received from LLM ===")
58 | logger.info(response.text)
59 |
60 | try:
61 | parsed_response = json.loads(response.text)
62 | except JSONDecodeError as e:
63 | logger.error(f"Could not decode response {e}: {response}")
64 | # TODO: Handle error better
65 | raise e
66 |
67 | self.subtasks = [Subtask(**assignment) for assignment in parsed_response]
68 | return self.subtasks
69 |
70 | def starting_files(self) -> str:
71 | directory = self.body.config.workspace_path
72 |
73 | file_list = []
74 | for file in os.listdir(directory):
75 | abs_path = os.path.abspath(os.path.join(directory, file.replace("/", "_")))
76 | if not os.path.isdir(abs_path) and file not in IGNORE_FILES:
77 | file_list.append(f"- {file}")
78 |
79 | if not file_list:
80 | return ""
81 |
82 | file_list.sort()
83 | file_list_output = "\n".join(file_list)
84 | return (
85 | f"**Files**: Each subtask may have access to any of the following files if included in its inputs:\n"
86 | f"{file_list_output}"
87 | )
88 |
--------------------------------------------------------------------------------
/beebot/decomposer/decomposer_prompt.py:
--------------------------------------------------------------------------------
1 | TEMPLATE = """
2 | **Role**: You are a subtask analyzer AI, and your goal is to analyze a human-provided task into one or more subtasks for specialized AI Agents. Your analysis must be exact and must only include the actions necessary to fulfill the specific requirements stated in the human task. Avoid over-complicating simple tasks.
3 |
4 | **Note**: Follow the human task to the letter. Any deviation or addition outside the task will increase costs. The human task may vary in complexity, so tailor the number of subtask(s) accordingly.
5 |
6 | **Human Task**: `{task}`
7 |
8 | **Agents**:
9 | Agents are Autonomous AI Entities. They will strictly follow your instructions, so be clear and avoid actions that aren't required by the human task.
10 |
11 | The following agents are specialized AI entities, each excelling in a certain area.
12 | {agent_list}
13 |
14 | In addition to these specialized agents, there is a Generalist Agent which can handle a wide variety of subtasks. If a subtask doesn't fit in any area of expertise of the specialized agents, assign it to the Generalist Agent.
15 |
16 | {files}
17 |
18 | **Guidelines**:
19 | 1. **Analyze the Task**: Carefully dissect the human task, identifying the exact requirements without adding or inferring extra actions.
20 | 2. **Develop Instructions**: Create detailed step-by-step instructions for each subtask. To ensure the AI Agent is able to correctly determine when the task is complete, clearly stipulate all required outcomes and establish concrete exit conditions. Each subtask is executed in isolation, without context. If data collected in one subtask must be used in a future subtask, include instructions to export that data as a global variable. Be sure to explicitly include instructions for input files, output files, and variable exports.
21 | 3. **Assign Subtasks**: Detail each subtask and assign it to the most appropriate agent, specifying the input filenames, the instructions for the agent, and the expected output filenames. Do not include subtasks that simply mark task completion or have no specific actions associated with them.
22 |
23 | **Response Format**:
24 | Responses should be valid JSON and should not include any other explanatory text. Example:
25 | [
26 | {{"agent": "Research Agent", "inputs": ["my_topics.txt"], "outputs": [], "instructions": "Read the file my_topics.txt. Analyze the topics and export your analysis to the `topic_analysis` global variable."}},
27 | {{"agent": "Generalist Agent", "inputs": [], "outputs": ["topic_summary.txt"], "instructions": "Write the contents of the global variable `topic_analysis` to the file topic_summary.txt"}}
28 | ]
29 | """
30 |
31 |
32 | def decomposer_prompt() -> str:
33 | return TEMPLATE
34 |
--------------------------------------------------------------------------------
/beebot/execution/__init__.py:
--------------------------------------------------------------------------------
1 | from .step import Step
2 |
3 | __all__ = ["Step"]
4 |
--------------------------------------------------------------------------------
/beebot/execution/background_process.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import selectors
3 | import subprocess
4 | from threading import Thread
5 | from typing import Optional, TYPE_CHECKING, Union
6 |
7 | if TYPE_CHECKING:
8 | from beebot.body import Body
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class BackgroundProcess:
14 | cmd: list[str]
15 | process: Optional[subprocess.Popen[str]] = None
16 | daemonize: bool = False
17 | output_thread: Thread = None
18 | stdout: str = ""
19 | stderr: str = ""
20 |
21 | def __init__(
22 | self,
23 | body: "Body",
24 | cmd: list[str],
25 | daemonize: bool = False,
26 | ):
27 | self.body = body
28 | self.cmd = cmd
29 | self.daemonize = daemonize
30 | self.process = None
31 |
32 | def run(self):
33 | process = subprocess.Popen(
34 | self.cmd,
35 | stdout=subprocess.PIPE,
36 | stderr=subprocess.PIPE,
37 | universal_newlines=True,
38 | cwd=self.body.config.workspace_path,
39 | start_new_session=self.daemonize,
40 | )
41 |
42 | self.body.processes[process.pid] = self
43 | self.process = process
44 |
45 | # Create a thread for reading output from process without blocking
46 | self.output_thread = Thread(target=self.stdout_reader)
47 | self.output_thread.start()
48 | return process
49 |
50 | @property
51 | def pid(self) -> Union[int, None]:
52 | if self.process:
53 | return self.process.pid
54 | return None
55 |
56 | def poll(self):
57 | return self.process.poll()
58 |
59 | def kill(self):
60 | self.process.kill()
61 |
62 | @property
63 | def returncode(self) -> int:
64 | return self.process.returncode
65 |
66 | def stdout_reader(self):
67 | def read_stdout(stream):
68 | self.stdout += "\n".join(stream.readlines())
69 | logger.info(f"STDOUT: {self.stdout}")
70 |
71 | def read_stderr(stream):
72 | self.stderr += "\n".join(stream.readlines())
73 | logger.info(f"STDERR: {self.stderr}")
74 |
75 | stdout_selector = selectors.DefaultSelector()
76 | stdout_selector.register(self.process.stdout, selectors.EVENT_READ, read_stdout)
77 |
78 | stderr_selector = selectors.DefaultSelector()
79 | stderr_selector.register(self.process.stderr, selectors.EVENT_READ, read_stderr)
80 |
81 | while self.process.poll() is None:
82 | for selector_events in [stdout_selector.select(), stderr_selector.select()]:
83 | for key, _ in selector_events:
84 | callback = key.data
85 | callback(key.fileobj)
86 |
87 | def stderr_reader(self, process):
88 | pass
89 |
--------------------------------------------------------------------------------
/beebot/execution/executor.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from typing import TYPE_CHECKING
4 |
5 | from pydantic import ValidationError
6 |
7 | from beebot.models.database_models import Decision, Observation
8 |
9 | if TYPE_CHECKING:
10 | from beebot.execution.task_execution import TaskExecution
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class Executor:
16 | def __init__(self, task_execution: "TaskExecution"):
17 | self.task_execution = task_execution
18 |
19 | async def execute(self, decision: Decision) -> Observation:
20 | """Get pack from tool name. call it"""
21 | pack = self.task_execution.packs.get(decision.tool_name)
22 | if not pack:
23 | return Observation(
24 | success=False,
25 | error_reason=f"Invalid tool name received: {decision.tool_name}. It may be invalid or may not be "
26 | f"installed.",
27 | )
28 |
29 | tool_args = decision.tool_args or {}
30 | try:
31 | result = await pack.arun(**tool_args)
32 | logger.info("\n=== Execution observation ===")
33 | logger.info(result)
34 | return Observation(response=result)
35 | except ValidationError as e:
36 | logger.error(
37 | f"Error on execution of {decision.tool_name}: {json.dumps(e.errors())}"
38 | )
39 | return Observation(response=f"Error: {json.dumps(e.errors())}")
40 | except (SystemExit, KeyboardInterrupt):
41 | raise
42 | except BaseException as e:
43 | logger.error(f"Error on execution of {decision.tool_name}: {e}")
44 | return Observation(
45 | response=f"Exception: {e}",
46 | success=False,
47 | # TODO: Improve error_reason
48 | error_reason=str(e),
49 | )
50 |
--------------------------------------------------------------------------------
/beebot/execution/step.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import TYPE_CHECKING
3 |
4 | from beebot.models.database_models import (
5 | StepModel,
6 | Plan,
7 | Decision,
8 | Observation,
9 | Oversight,
10 | DocumentModel,
11 | DocumentStep,
12 | )
13 |
14 | if TYPE_CHECKING:
15 | from beebot.execution.task_execution import TaskExecution
16 |
17 |
18 | @dataclass
19 | class Step:
20 | task_execution: "TaskExecution" = None
21 | model_object: StepModel = None
22 | oversight: Oversight = None
23 | decision: Decision = None
24 | observation: Observation = None
25 | plan: Plan = None
26 | reversible: bool = True
27 |
28 | @property
29 | async def documents(self) -> dict[str, DocumentModel]:
30 | documents = {}
31 | if not self.model_object:
32 | return documents
33 |
34 | document_steps = await DocumentStep.filter(
35 | step=self.model_object
36 | ).prefetch_related("document")
37 | for document_step in document_steps:
38 | documents[document_step.document.name] = document_step.document
39 |
40 | return documents
41 |
42 | async def add_document(self, document: DocumentModel):
43 | await DocumentStep.get_or_create(document=document, step=self.model_object)
44 |
45 | async def save(self):
46 | if not self.model_object:
47 | self.model_object = StepModel(
48 | task_execution=self.task_execution.model_object,
49 | oversight=self.oversight,
50 | decision=self.decision,
51 | observation=self.observation,
52 | plan=self.plan,
53 | )
54 | else:
55 | self.model_object.oversight = self.oversight
56 | self.model_object.decision = self.decision
57 | self.model_object.observation = self.observation
58 | self.model_object.plan = self.plan
59 |
60 | await self.model_object.save()
61 |
62 | @classmethod
63 | async def from_model(cls, step_model: StepModel):
64 | kwargs = {"model_object": step_model, "reversible": False}
65 | oversight = await step_model.oversight
66 | decision = await step_model.decision
67 | observation = await step_model.observation
68 | plan = await step_model.plan
69 |
70 | if oversight:
71 | kwargs["oversight"] = oversight
72 | if decision:
73 | kwargs["decision"] = decision
74 | if observation:
75 | kwargs["observation"] = observation
76 | if plan:
77 | kwargs["plan"] = plan
78 |
79 | step = cls(**kwargs)
80 |
81 | return step
82 |
--------------------------------------------------------------------------------
/beebot/execution/task_execution.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import TYPE_CHECKING, Union
3 |
4 | from autopack import Pack
5 | from pydantic import ValidationError
6 |
7 | from beebot.body.pack_utils import llm_wrapper
8 | from beebot.decider import Decider
9 | from beebot.execution import Step
10 | from beebot.execution.executor import Executor
11 | from beebot.execution.task_state_machine import TaskStateMachine
12 | from beebot.models.database_models import (
13 | Plan,
14 | Observation,
15 | TaskExecutionModel,
16 | Oversight,
17 | Decision,
18 | )
19 | from beebot.overseer.overseer import Overseer
20 | from beebot.planner import Planner
21 |
22 | if TYPE_CHECKING:
23 | from beebot.body import Body
24 | from beebot.agents.base_agent import BaseAgent
25 |
26 | logger = logging.getLogger(__name__)
27 |
28 | RETRY_LIMIT = 3
29 |
30 |
31 | class TaskExecution:
32 | """Represents one "branch" of execution that beebot has made during execution of a task"""
33 |
34 | body: "Body"
35 | agent: "BaseAgent"
36 | model_object: TaskExecutionModel = None
37 | state: TaskStateMachine
38 | packs: dict[str, Pack] = None
39 | steps: list[Step]
40 | agent_name: str = ""
41 | instructions: str = ""
42 | inputs: list[str]
43 | outputs: list[str]
44 | complete: bool = False
45 |
46 | # NOTE! Map of value to list of names
47 | variables: dict[str, list[str]]
48 |
49 | def __init__(
50 | self,
51 | body: "Body",
52 | model_object: TaskExecutionModel = None,
53 | agent_name: str = "",
54 | instructions: str = "",
55 | inputs: list[str] = None,
56 | outputs: list[str] = None,
57 | complete: bool = False,
58 | ):
59 | self.state = TaskStateMachine(self)
60 | self.body = body
61 | self.steps = []
62 | self.model_object = model_object
63 | self.packs = {}
64 | self.variables = {}
65 |
66 | if model_object:
67 | self.agent_name = model_object.agent
68 | self.instructions = model_object.instructions
69 | self.inputs = model_object.inputs
70 | self.outputs = model_object.outputs
71 | self.complete = model_object.complete
72 | else:
73 | self.agent_name = agent_name
74 | self.instructions = instructions or ""
75 | self.inputs = inputs or []
76 | self.outputs = outputs or []
77 | self.complete = complete
78 |
79 | from beebot.agents.base_agent import BaseAgent
80 |
81 | agent_classes = [agent for agent in BaseAgent.__subclasses__()]
82 | self.agent = BaseAgent(self)
83 | for agent_class in agent_classes:
84 | if self.agent_name == agent_class.NAME:
85 | self.agent = agent_class(self)
86 |
87 | @property
88 | def current_step(self):
89 | return self.steps[-1] if self.steps else None
90 |
91 | @classmethod
92 | async def from_model(cls, body: "Body", task_execution_model: TaskExecutionModel):
93 | task = cls(body, model_object=task_execution_model)
94 |
95 | task.state.current_state = TaskStateMachine.states_map[
96 | task_execution_model.state
97 | ]
98 |
99 | for step_model in await task_execution_model.steps.all():
100 | task.steps.append(await Step.from_model(step_model))
101 |
102 | if task.current_step and task.current_step.plan:
103 | await task.create_new_step()
104 |
105 | await task.get_packs()
106 | return task
107 |
108 | async def create_new_step(self) -> Step:
109 | await self.save()
110 |
111 | old_step = self.current_step
112 |
113 | if old_step:
114 | plan = old_step.plan
115 | # Current step has not finished. TODO: Is this the right thing to do?
116 | if not plan:
117 | return old_step
118 |
119 | oversight = Oversight(
120 | original_plan_text=plan.plan_text, modified_plan_text=plan.plan_text
121 | )
122 | await oversight.save()
123 |
124 | new_incomplete_step = Step(task_execution=self, oversight=oversight)
125 | else:
126 | new_incomplete_step = Step(task_execution=self)
127 |
128 | await new_incomplete_step.save()
129 | self.steps.append(new_incomplete_step)
130 | await self.save()
131 | # Create links from previous documents to this execution
132 |
133 | if old_step:
134 | previous_documents = await old_step.documents
135 | for document in previous_documents.values():
136 | await new_incomplete_step.add_document(document)
137 | else:
138 | await self.create_initial_oversight()
139 |
140 | return new_incomplete_step
141 |
142 | async def cycle(self) -> Union[Step, None]:
143 | if self.complete:
144 | return None
145 |
146 | if not self.current_step:
147 | await self.create_new_step()
148 |
149 | if not self.state.current_state == TaskStateMachine.oversight:
150 | self.state.oversee()
151 |
152 | await self.get_packs()
153 | oversight = self.current_step.oversight
154 |
155 | self.state.decide()
156 | await self.save()
157 |
158 | decision = await self.decide(oversight)
159 | await self.execute(decision)
160 |
161 | return_step = self.current_step
162 | if not self.complete:
163 | new_plan = await self.plan()
164 | await self.add_plan(new_plan)
165 | await self.finish_step()
166 |
167 | await self.save()
168 | return return_step
169 |
170 | async def add_oversight(self, oversight: Oversight):
171 | self.current_step.oversight = oversight
172 | await self.current_step.save()
173 |
174 | async def add_decision(self, decision: str):
175 | self.current_step.decision = decision
176 | await self.current_step.save()
177 |
178 | async def add_observation(self, observation: Observation):
179 | step = self.current_step
180 | variable_name = f"{step.decision.tool_name}_{len(self.steps)}"
181 | if observation.response in self.variables:
182 | self.variables[observation.response].append(variable_name)
183 | else:
184 | self.variables[observation.response] = [variable_name]
185 | step.observation = observation
186 | await step.save()
187 |
188 | async def add_plan(self, plan: Plan):
189 | self.current_step.plan = plan
190 | await self.current_step.save()
191 |
192 | async def finish_step(self) -> Step:
193 | completed_step = self.current_step
194 | await self.create_new_step()
195 | return completed_step
196 |
197 | async def save(self):
198 | if not self.model_object:
199 | path_model = TaskExecutionModel(
200 | body=self.body.model_object,
201 | agent=self.agent_name,
202 | instructions=self.instructions,
203 | inputs=self.inputs,
204 | outputs=self.outputs,
205 | complete=self.complete,
206 | )
207 | await path_model.save()
208 | self.model_object = path_model
209 |
210 | for step in self.steps:
211 | await step.save()
212 |
213 | async def get_packs(self) -> list[Pack]:
214 | for pack_class in self.agent.PACKS:
215 | from beebot.packs.system_base_pack import SystemBasePack
216 |
217 | if pack_class in SystemBasePack.__subclasses__():
218 | pack = pack_class(body=self.body)
219 | else:
220 | llm = llm_wrapper(self.body)
221 | pack = pack_class(llm=llm, allm=llm)
222 | self.packs[pack.name] = pack
223 | return self.packs
224 |
225 | async def execute(self, decision: Decision, retry_count: int = 0) -> Observation:
226 | """Execute a Decision and keep track of state"""
227 | try:
228 | result = await Executor(self).execute(decision=decision)
229 | await self.add_observation(result)
230 | return result
231 | except ValidationError as e:
232 | # It's likely the AI just sent bad arguments, try again.
233 | logger.warning(
234 | f"Invalid arguments received: {e}. {decision.tool_name}({decision.tool_args}"
235 | )
236 | if retry_count >= RETRY_LIMIT:
237 | return
238 | return await self.execute(decision, retry_count + 1)
239 | finally:
240 | if not self.complete:
241 | self.state.plan()
242 | await self.save()
243 |
244 | async def decide(self, oversight: Oversight = None) -> Decision:
245 | """Execute an action and keep track of state"""
246 | try:
247 | decision = await Decider(self).decide_with_retry(oversight=oversight)
248 | await self.add_decision(decision)
249 |
250 | return decision
251 | finally:
252 | self.state.execute()
253 | await self.save()
254 |
255 | async def plan(self) -> Plan:
256 | """Take the current task and history and develop a plan"""
257 | try:
258 | plan = await Planner(self).plan()
259 | await self.add_plan(plan)
260 | return plan
261 | finally:
262 | if not self.complete:
263 | self.state.oversee()
264 | await self.save()
265 |
266 | async def create_initial_oversight(self) -> Oversight:
267 | oversight = await Overseer(self).initial_oversight()
268 | await self.add_oversight(oversight)
269 | return oversight
270 |
271 | async def compile_history(self) -> str:
272 | return await self.agent.compile_history()
273 |
274 | def compile_variables(self) -> str:
275 | return self.agent.compile_variables()
276 |
--------------------------------------------------------------------------------
/beebot/execution/task_state_machine.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | from statemachine import StateMachine, State
4 |
5 | if TYPE_CHECKING:
6 | from beebot.execution.task_execution import TaskExecution
7 |
8 |
9 | class TaskStateMachine(StateMachine):
10 | waiting = State(initial=True)
11 | planning = State()
12 | oversight = State()
13 | deciding = State()
14 | executing = State()
15 | done = State(final=True)
16 |
17 | execute = deciding.to(executing)
18 | plan = executing.to(planning)
19 | oversee = planning.to(oversight) | waiting.to(oversight)
20 | decide = oversight.to(deciding)
21 | finish = executing.to(done)
22 |
23 | def __init__(self, task_execution: "TaskExecution"):
24 | self.task_execution = task_execution
25 | super().__init__()
26 |
--------------------------------------------------------------------------------
/beebot/executor/__init__.py:
--------------------------------------------------------------------------------
1 | from beebot.execution.executor import Executor
2 |
3 | __all__ = ["Executor"]
4 |
--------------------------------------------------------------------------------
/beebot/initiator/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/initiator/__init__.py
--------------------------------------------------------------------------------
/beebot/initiator/api.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | from dotenv import load_dotenv
5 | from fastapi import FastAPI
6 | from starlette.middleware.cors import CORSMiddleware
7 |
8 | from beebot.api.routes import (
9 | create_agent_task,
10 | execute_agent_task_step,
11 | agent_task_ids,
12 | get_agent_task,
13 | list_agent_task_steps,
14 | get_agent_task_step,
15 | )
16 | from beebot.api.websocket import websocket_endpoint
17 | from beebot.config import Config
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 | ORIGINS = [
22 | "http://localhost:3000",
23 | ]
24 |
25 |
26 | def create_app() -> FastAPI:
27 | load_dotenv()
28 | os.environ["BEEBOT_HARD_EXIT"] = "False"
29 | config = Config.global_config()
30 | config.setup_logging()
31 |
32 | app = FastAPI(
33 | title="BeeBot Agent Communication Protocol",
34 | description="",
35 | version="v1",
36 | )
37 | app.add_websocket_route("/notifications", websocket_endpoint)
38 | app.add_route("/agent/tasks", create_agent_task, methods=["POST"])
39 | app.add_route(
40 | "/agent/tasks/{task_id}/steps", execute_agent_task_step, methods=["POST"]
41 | )
42 | app.add_route("/agent/tasks", agent_task_ids)
43 | app.add_route("/agent/tasks/{task_id}", get_agent_task)
44 | app.add_route("/agent/tasks/{task_id}/steps", list_agent_task_steps)
45 | app.add_route("/agent/tasks/{task_id}/steps/{step_id}", get_agent_task_step)
46 |
47 | app.add_middleware(
48 | CORSMiddleware,
49 | allow_origins=ORIGINS,
50 | allow_credentials=True,
51 | allow_methods=["*"],
52 | allow_headers=["*"],
53 | )
54 |
55 | return app
56 |
--------------------------------------------------------------------------------
/beebot/initiator/benchmark_entrypoint.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import sys
3 |
4 | from dotenv import load_dotenv
5 |
6 | from beebot.body import Body
7 | from beebot.config import Config
8 | from beebot.models.database_models import initialize_db
9 |
10 |
11 | async def run_specific_agent(task: str) -> None:
12 | load_dotenv()
13 |
14 | config = Config.global_config()
15 | config.setup_logging()
16 | await initialize_db(config.database_url)
17 |
18 | body = Body(task=task, config=config)
19 | await body.setup()
20 | while output := await body.cycle():
21 | if output.observation:
22 | print(output.observation.response)
23 |
24 |
25 | if __name__ == "__main__":
26 | if len(sys.argv) != 2:
27 | print("Usage: python script.py ")
28 | sys.exit(1)
29 | task_arg = sys.argv[-1]
30 | asyncio.run(run_specific_agent(task_arg))
31 |
--------------------------------------------------------------------------------
/beebot/initiator/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | import asyncio
4 |
5 | from dotenv import load_dotenv
6 |
7 | from beebot.body import Body
8 | from beebot.config import Config
9 | from beebot.models.database_models import initialize_db
10 |
11 |
12 | def parse_args():
13 | parser = argparse.ArgumentParser(description="BeeBot CLI tool")
14 |
15 | parser.add_argument(
16 | "-t",
17 | "--task",
18 | help="Run a specific task, wrapped in quotes",
19 | )
20 |
21 | return parser.parse_args()
22 |
23 |
24 | async def main():
25 | load_dotenv()
26 | parsed_args = parse_args()
27 | if parsed_args.task:
28 | task = parsed_args.task
29 | else:
30 | print("What would you like me to do?")
31 | print("> ", end="")
32 | task = input()
33 |
34 | config = Config.global_config()
35 | config.setup_logging()
36 | await initialize_db(config.database_url)
37 |
38 | body = Body(task=task, config=config)
39 | await body.setup()
40 | while output := await body.cycle():
41 | if output.observation:
42 | print("\n=== Cycle Output ===")
43 | print(output.observation.response)
44 |
45 |
46 | if __name__ == "__main__":
47 | asyncio.run(main())
48 |
--------------------------------------------------------------------------------
/beebot/models/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | "BodyModel",
3 | "DocumentModel",
4 | "TaskExecutionModel",
5 | "StepModel",
6 | "Oversight",
7 | "Plan",
8 | "Decision",
9 | "Observation",
10 | "DocumentStep",
11 | ]
12 |
13 | from .database_models import (
14 | BodyModel,
15 | DocumentModel,
16 | TaskExecutionModel,
17 | StepModel,
18 | Oversight,
19 | Plan,
20 | Decision,
21 | Observation,
22 | DocumentStep,
23 | )
24 |
--------------------------------------------------------------------------------
/beebot/models/database_models.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from tortoise import fields, Tortoise
4 | from tortoise.fields import JSONField, BooleanField
5 | from tortoise.models import Model
6 | from yoyo import get_backend, read_migrations
7 |
8 |
9 | class BaseModel(Model):
10 | id = fields.IntField(pk=True)
11 | created_at = fields.DatetimeField(auto_now_add=True)
12 | updated_at = fields.DatetimeField(auto_now=True)
13 |
14 | def json(self):
15 | json_dict = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
16 | json_dict.pop("created_at")
17 | json_dict.pop("updated_at")
18 | return json.dumps(json_dict)
19 |
20 | class Meta:
21 | abstract = True
22 |
23 |
24 | class BodyModel(BaseModel):
25 | INCLUSIVE_PREFETCH = "task_executions__steps__document_steps__document"
26 |
27 | task = fields.TextField()
28 |
29 | class Meta:
30 | table = "body"
31 |
32 |
33 | class TaskExecutionModel(BaseModel):
34 | body = fields.ForeignKeyField("models.BodyModel", related_name="task_executions")
35 | agent = fields.TextField()
36 | state = fields.TextField(default="waiting")
37 | instructions = fields.TextField()
38 | inputs = JSONField(default=list)
39 | outputs = JSONField(default=list)
40 | complete = BooleanField(default=False)
41 | variables = JSONField(default=dict)
42 |
43 | class Meta:
44 | table = "task_execution"
45 |
46 |
47 | class StepModel(BaseModel):
48 | task_execution = fields.ForeignKeyField(
49 | "models.TaskExecutionModel", related_name="steps"
50 | )
51 | plan = fields.ForeignKeyField("models.Plan", related_name="steps", null=True)
52 | decision = fields.ForeignKeyField(
53 | "models.Decision", related_name="steps", null=True
54 | )
55 | observation = fields.ForeignKeyField(
56 | "models.Observation", related_name="steps", null=True
57 | )
58 | oversight = fields.ForeignKeyField(
59 | "models.Oversight", related_name="steps", null=True
60 | )
61 |
62 | class Meta:
63 | table = "step"
64 |
65 |
66 | class Oversight(BaseModel):
67 | original_plan_text = fields.TextField()
68 | modified_plan_text = fields.TextField()
69 | modifications = JSONField(default=dict)
70 | prompt_variables = JSONField(default=dict)
71 | llm_response = fields.TextField(default="")
72 |
73 | class Meta:
74 | table = "oversight"
75 |
76 |
77 | class Decision(BaseModel):
78 | tool_name = fields.TextField()
79 | tool_args = JSONField(default=dict)
80 | prompt_variables = JSONField(default=dict)
81 | llm_response = fields.TextField(default="")
82 |
83 | class Meta:
84 | table = "decision"
85 |
86 |
87 | class Observation(BaseModel):
88 | response = fields.TextField(null=True)
89 | error_reason = fields.TextField(null=True)
90 | success = fields.BooleanField(default=True)
91 |
92 | class Meta:
93 | table = "observation"
94 |
95 |
96 | class Plan(BaseModel):
97 | plan_text = fields.TextField()
98 | prompt_variables = JSONField(default=dict)
99 | llm_response = fields.TextField(default="")
100 |
101 | class Meta:
102 | table = "plan"
103 |
104 |
105 | class DocumentModel(BaseModel):
106 | name = fields.TextField()
107 | content = fields.TextField()
108 |
109 | class Meta:
110 | table = "document"
111 |
112 |
113 | class DocumentStep(BaseModel):
114 | step = fields.ForeignKeyField("models.StepModel", related_name="document_steps")
115 | document = fields.ForeignKeyField(
116 | "models.DocumentModel", related_name="document_steps"
117 | )
118 |
119 | class Meta:
120 | table = "document_step"
121 |
122 |
123 | def apply_migrations(db_url: str):
124 | """Apply any outstanding migrations"""
125 | backend = get_backend(db_url)
126 | backend.init_database()
127 | migrations = read_migrations("migrations")
128 |
129 | with backend.lock():
130 | backend.apply_migrations(backend.to_apply(migrations))
131 |
132 |
133 | async def initialize_db(db_url: str):
134 | # Don't re-initialize an already initialized database
135 | if Tortoise.describe_models():
136 | return
137 |
138 | await Tortoise.init(
139 | db_url=db_url,
140 | modules={"models": ["beebot.models.database_models"]},
141 | use_tz=True,
142 | )
143 | if db_url == "sqlite://:memory:":
144 | await Tortoise.generate_schemas()
145 | else:
146 | apply_migrations(db_url)
147 |
--------------------------------------------------------------------------------
/beebot/overseer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/overseer/__init__.py
--------------------------------------------------------------------------------
/beebot/overseer/overseeing_prompt.py:
--------------------------------------------------------------------------------
1 | TEMPLATE = """
2 | """
3 |
4 |
5 | def overseeing_prompt() -> str:
6 | return TEMPLATE
7 |
--------------------------------------------------------------------------------
/beebot/overseer/overseer.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import TYPE_CHECKING
3 |
4 | from beebot.body.llm import call_llm
5 | from beebot.models import Oversight
6 |
7 | if TYPE_CHECKING:
8 | from beebot.execution.task_execution import TaskExecution
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class Overseer:
14 | """This doesn't really do anything right now, but in the future this will be where the human has a chance to modify
15 | plans from the previous step before executing this step"""
16 |
17 | task_execution: "TaskExecution"
18 |
19 | def __init__(self, task_execution: "TaskExecution"):
20 | self.task_execution = task_execution
21 |
22 | async def initial_oversight(self) -> Oversight:
23 | logger.info("\n=== Initial Plan Request ===")
24 | (
25 | prompt,
26 | prompt_variables,
27 | ) = await self.task_execution.agent.planning_prompt()
28 | logger.info(prompt)
29 |
30 | response = await call_llm(
31 | self.task_execution.body,
32 | message=prompt,
33 | function_call="none",
34 | )
35 |
36 | logger.info("\n=== Initial Plan Created ===")
37 | logger.info(response.text)
38 |
39 | oversight = Oversight(
40 | prompt_variables=prompt_variables,
41 | original_plan_text=response.text,
42 | modified_plan_text=response.text,
43 | llm_response=response.text,
44 | )
45 | await oversight.save()
46 | return oversight
47 |
--------------------------------------------------------------------------------
/beebot/packs/README.md:
--------------------------------------------------------------------------------
1 | # official
2 | Official Packs for AutoPack
3 |
--------------------------------------------------------------------------------
/beebot/packs/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | "CreateDraft",
3 | "DelegateTask",
4 | "ExecutePythonFile",
5 | "ExecutePythonFileInBackground",
6 | "Exit",
7 | "GetMessage",
8 | "GetProcessStatus",
9 | "GetThread",
10 | "GetWebsiteTextContent",
11 | "InstallPythonPackage",
12 | "KillProcess",
13 | "ListProcesses",
14 | "SendMessage",
15 | "ExportVariable",
16 | ]
17 |
18 | from beebot.packs.delegate_task import DelegateTask
19 | from beebot.packs.execute_python_file import ExecutePythonFile
20 | from beebot.packs.execute_python_file_in_background import ExecutePythonFileInBackground
21 | from beebot.packs.exit import Exit
22 | from beebot.packs.export_variable import ExportVariable
23 | from beebot.packs.get_process_status import GetProcessStatus
24 | from beebot.packs.get_website_text_content import GetWebsiteTextContent
25 | from beebot.packs.gmail import CreateDraft, GetMessage, GetThread, SendMessage
26 | from beebot.packs.install_python_package import InstallPythonPackage
27 | from beebot.packs.kill_process import KillProcess
28 | from beebot.packs.list_processes import ListProcesses
29 |
--------------------------------------------------------------------------------
/beebot/packs/delegate_task.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | from beebot.packs.system_base_pack import SystemBasePack
4 |
5 | PACK_NAME = "delegate_task"
6 | PACK_DESCRIPTION = (
7 | "Delegate a complex task to a subordinate agent and return the output."
8 | )
9 |
10 |
11 | class DelegateTaskArgs(BaseModel):
12 | task: str = Field(..., description="The task to be accomplished by the agent")
13 |
14 |
15 | class DelegateTask(SystemBasePack):
16 | name = PACK_NAME
17 | description = PACK_DESCRIPTION
18 | args_schema = DelegateTaskArgs
19 | categories = ["Delegation"]
20 |
21 | reversible = False
22 |
23 | async def _arun(self, task: str) -> str:
24 | from beebot.body import Body
25 |
26 | try:
27 | subagent = Body(task)
28 | await subagent.setup()
29 | subagent_output = []
30 | while output := await subagent.cycle():
31 | subagent_output.append(output.observation.response)
32 |
33 | return "\n".join(subagent_output)
34 |
35 | except (SystemExit, KeyboardInterrupt):
36 | raise
37 | except BaseException as e:
38 | return f"Error: {e}"
39 |
--------------------------------------------------------------------------------
/beebot/packs/disk_usage/__init__.py:
--------------------------------------------------------------------------------
1 | from .disk_usage import DiskUsage
2 |
3 | __all__ = ["DiskUsage"]
4 |
--------------------------------------------------------------------------------
/beebot/packs/disk_usage/disk_usage.py:
--------------------------------------------------------------------------------
1 | import psutil
2 | from autopack import Pack
3 | from pydantic import BaseModel
4 |
5 |
6 | class DiskUsageArgsSchema(BaseModel):
7 | pass
8 |
9 |
10 | class DiskUsage(Pack):
11 | name = "disk_usage"
12 | description = "Get disk usage information for this computer."
13 | dependencies = ["psutil"]
14 | args_schema = DiskUsageArgsSchema
15 | categories = ["System Info"]
16 |
17 | def _run(self):
18 | # Currently we will only support root directory
19 | usage = psutil.disk_usage("/")
20 | used = round(usage.used / 1024 / 1024 / 1024, 2)
21 | total = round(usage.total / 1024 / 1024 / 1024, 2)
22 | free = round(usage.free / 1024 / 1024 / 1024, 2)
23 |
24 | return f"""Total: {total} GB. Used: {used} GB. Available: {free} GB". Percent Used: {usage.percent * 100}%"""
25 |
26 | async def _arun(self):
27 | return self.run()
28 |
--------------------------------------------------------------------------------
/beebot/packs/disk_usage/test_disk_usage.py:
--------------------------------------------------------------------------------
1 | from disk_usage import DiskUsage
2 |
3 |
4 | def test_disk_usage():
5 | usage = DiskUsage().run()
6 |
7 | assert "Total:" in usage
8 | assert "Used:" in usage
9 | assert "Available:" in usage
10 | assert "Percent Used:" in usage
11 |
--------------------------------------------------------------------------------
/beebot/packs/execute_python_file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shlex
3 | import subprocess
4 | import time
5 | from typing import Optional
6 |
7 | from pydantic import BaseModel, Field
8 |
9 | from beebot.body.pack_utils import init_workspace_poetry
10 | from beebot.packs.system_base_pack import SystemBasePack
11 | from beebot.utils import restrict_path
12 |
13 | PACK_NAME = "execute_python_file"
14 |
15 | # IMPORTANT NOTE: This does NOT actually restrict the execution environment, it just nudges the AI to avoid doing
16 | # those things.
17 | PACK_DESCRIPTION = (
18 | "Executes a Python file in a restricted environment, prohibiting shell execution and filesystem access. Returns "
19 | "the output of the execution as a string. Ensure file adherence to restrictions and availability in the specified "
20 | "path. (Packages managed by Poetry.)"
21 | )
22 |
23 |
24 | class TimedOutSubprocess:
25 | """A convenience class to allow creating a subprocess with a timeout while capturing its output"""
26 |
27 | cmd: list[str]
28 | process: Optional[subprocess.Popen[str]]
29 |
30 | def __init__(self, cmd: list[str]) -> None:
31 | self.cmd = cmd
32 | self.process = None
33 |
34 | def run(self, timeout: int, **kwargs) -> None:
35 | self.process = subprocess.Popen(self.cmd, universal_newlines=True, **kwargs)
36 |
37 | start_time = time.time()
38 | while self.process.poll() is None: # None means the process hasn't finished
39 | if time.time() - start_time > timeout:
40 | self.process.terminate() # Kill the process
41 | break
42 |
43 | time.sleep(1)
44 |
45 | if self.process.returncode is not None and self.process.returncode != 0:
46 | print(f"The agent exited with return code {self.process.returncode}")
47 |
48 | output, error = self.process.communicate()
49 | return "\n".join([output.strip(), error.strip()]).strip()
50 |
51 |
52 | class ExecutePythonFileArgs(BaseModel):
53 | file_path: str = Field(
54 | ...,
55 | description="Specifies the path to the Python file previously saved on disk.",
56 | )
57 | python_args: str = Field(
58 | description="Arguments to be passed when executing the file", default=""
59 | )
60 |
61 |
62 | class ExecutePythonFile(SystemBasePack):
63 | name = PACK_NAME
64 | description = PACK_DESCRIPTION
65 | args_schema = ExecutePythonFileArgs
66 | categories = ["Programming"]
67 | depends_on = ["install_python_package", "write_python_code"]
68 |
69 | async def _arun(self, file_path: str, python_args: str = "") -> str:
70 | if self.body.config.restrict_code_execution:
71 | return "Error: Executing Python code is not allowed"
72 |
73 | await self.body.file_manager.flush_to_directory()
74 | file_path = os.path.join(self.body.config.workspace_path, file_path)
75 | try:
76 | abs_path = restrict_path(file_path, self.body.config.workspace_path)
77 | if not abs_path:
78 | return (
79 | f"Error: File {file_path} does not exist. You must create it first."
80 | )
81 |
82 | init_workspace_poetry(self.config.workspace_path)
83 | args_list = shlex.split(python_args)
84 | cmd = ["poetry", "run", "python", abs_path, *args_list]
85 | process = TimedOutSubprocess(cmd)
86 | process.run(
87 | timeout=self.body.config.process_timeout,
88 | stdout=subprocess.PIPE,
89 | stderr=subprocess.PIPE,
90 | cwd=self.body.config.workspace_path,
91 | )
92 | os_subprocess = process.process
93 | output, error = os_subprocess.communicate()
94 |
95 | await self.body.file_manager.load_from_directory()
96 |
97 | if os_subprocess.returncode:
98 | return f"Execution failed with exit code {os_subprocess.returncode}. Output: {output}. {error}"
99 |
100 | if output:
101 | return f"Execution complete. Output: {output}"
102 |
103 | return "Execution complete."
104 |
105 | except (SystemExit, KeyboardInterrupt):
106 | raise
107 | except BaseException as e:
108 | return f"Error: {e}"
109 |
--------------------------------------------------------------------------------
/beebot/packs/execute_python_file_in_background.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import shlex
4 | import time
5 |
6 | from pydantic import BaseModel, Field
7 |
8 | from beebot.body.pack_utils import init_workspace_poetry
9 | from beebot.execution.background_process import BackgroundProcess
10 | from beebot.packs.system_base_pack import SystemBasePack
11 | from beebot.utils import restrict_path
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 | PACK_NAME = "execute_python_file_in_background"
16 |
17 | # IMPORTANT NOTE: This does NOT actually restrict the execution environment, it just nudges the AI to avoid doing
18 | # those things.
19 | PACK_DESCRIPTION = (
20 | "Executes a Python file in a restricted environment, prohibiting shell execution and filesystem access. Executes "
21 | "the code in the background or as a daemon process and returns immediately. Make sure the Python file adheres to"
22 | "the restrictions of the environment and is available in the specified file path. (Packages managed by Poetry.)"
23 | )
24 |
25 |
26 | class ExecutePythonFileInBackgroundArgs(BaseModel):
27 | file_path: str = Field(
28 | ...,
29 | description="Specifies the path to the Python file previously saved on disk.",
30 | )
31 | python_args: str = Field(
32 | description="Arguments to be passed when executing the file", default=""
33 | )
34 | daemonize: bool = Field(
35 | description="Daemonize the process, detaching it from the current process.",
36 | default=False,
37 | )
38 |
39 |
40 | class ExecutePythonFileInBackground(SystemBasePack):
41 | name = PACK_NAME
42 | description = PACK_DESCRIPTION
43 | args_schema = ExecutePythonFileInBackgroundArgs
44 | depends_on = [
45 | "write_python_code",
46 | "install_python_package",
47 | "get_process_status",
48 | "list_processes",
49 | "kill_process",
50 | ]
51 | categories = ["Programming"]
52 |
53 | def _run(
54 | self, file_path: str, python_args: str = "", daemonize: bool = False
55 | ) -> str:
56 | if self.body.config.restrict_code_execution:
57 | return "Error: Executing Python code is not allowed"
58 |
59 | file_path = os.path.join(self.body.config.workspace_path, file_path)
60 | if not os.path.exists(file_path):
61 | return f"Error: File {file_path} does not exist. You must create it first."
62 |
63 | abs_path = restrict_path(file_path, self.body.config.workspace_path)
64 | if not abs_path:
65 | return f"Error: File {file_path} does not exist. You must create it first."
66 |
67 | init_workspace_poetry(self.config.workspace_path)
68 | args_list = shlex.split(python_args)
69 | cmd = ["poetry", "run", "python", abs_path, *args_list]
70 | process = BackgroundProcess(body=self.body, cmd=cmd, daemonize=daemonize)
71 | process.run()
72 |
73 | time.sleep(0.2)
74 | if process.poll() is not None:
75 | return f"Process {process.pid} started, but failed. Output: {process.stdout}. {process.stderr}"
76 |
77 | return (
78 | f"Process started. It has been assigned PID {process.pid}. Use this when calling "
79 | f"`get_process_status`."
80 | )
81 |
82 | async def _arun(self, *args, **kwargs) -> str:
83 | await self.body.file_manager.flush_to_directory()
84 | return self._run(*args, **kwargs)
85 |
--------------------------------------------------------------------------------
/beebot/packs/exit.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from beebot.packs.system_base_pack import SystemBasePack
6 |
7 | PACK_NAME = "exit"
8 | PACK_DESCRIPTION = "Exits the program, signalling that all tasks have bene completed and all goals have been met."
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class ExitArgs(BaseModel):
14 | success: bool = Field(description="Success", default=True)
15 | conclusion: str = Field(
16 | description="Reflect on the task execution process.", default=""
17 | )
18 |
19 |
20 | class Exit(SystemBasePack):
21 | class Meta:
22 | name = PACK_NAME
23 |
24 | name = Meta.name
25 | description = PACK_DESCRIPTION
26 | args_schema = ExitArgs
27 | categories = ["System"]
28 |
29 | def _run(self, *args, **kwargs) -> str:
30 | raise NotImplementedError
31 |
32 | async def _arun(self, success: bool = True, conclusion: str = "") -> str:
33 | task_execution = self.body.current_task_execution
34 | task_execution.state.finish()
35 | task_execution.complete = True
36 | await task_execution.save()
37 | if success:
38 | logger.info("\n=== Task completed ===")
39 | else:
40 | logger.info("\n=== Task failed ===")
41 |
42 | logger.info(conclusion)
43 |
44 | return "Exited"
45 |
--------------------------------------------------------------------------------
/beebot/packs/export_variable.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | from beebot.packs.system_base_pack import SystemBasePack
4 |
5 |
6 | class ExportVariableArgs(BaseModel):
7 | name: str
8 | value: str
9 |
10 |
11 | class ExportVariable(SystemBasePack):
12 | name = "export_variable"
13 | description = (
14 | "Set and export a variable to make it globally available to other subtasks"
15 | )
16 | args_schema = ExportVariableArgs
17 | categories = []
18 |
19 | def _run(self, name: str, value: str) -> str:
20 | self.body.global_variables[name] = value
21 | return ""
22 |
--------------------------------------------------------------------------------
/beebot/packs/extract_information_from_webpage/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/packs/extract_information_from_webpage/__init__.py
--------------------------------------------------------------------------------
/beebot/packs/extract_information_from_webpage/extract_information_from_webpage.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | from autopack import Pack
4 | from autopack.utils import call_llm, acall_llm
5 | from bs4 import BeautifulSoup
6 | from playwright.async_api import async_playwright
7 | from playwright.sync_api import PlaywrightContextManager
8 | from pydantic import BaseModel, Field
9 |
10 | PACK_DESCRIPTION = "Extracts specific information from a webpage's content."
11 |
12 | PROMPT_TEMPLATE = """Please provide a summary of the following content, which was gathered from the website {url}:
13 | {content}
14 | """
15 |
16 | QUESTION_PROMPT_TEMPLATE = """You are a language model tasked with answering a specific question based on the given content from the website {url}. Please provide an answer to the following question:
17 |
18 | Question: {question}
19 |
20 | Content:
21 | {content}
22 |
23 | Answer:
24 | """
25 |
26 |
27 | class ExtractInformationFromWebpageArgs(BaseModel):
28 | url: str = Field(..., description="The URL of the webpage to analyze.")
29 | information: str = Field(
30 | description="The type of information to extract.",
31 | default="",
32 | )
33 |
34 |
35 | class ExtractInformationFromWebpage(Pack):
36 | name = "extract_information_from_webpage"
37 | description = PACK_DESCRIPTION
38 | args_schema = ExtractInformationFromWebpageArgs
39 | categories = ["Web"]
40 | dependencies = ["playwright", "beautifulsoup4"]
41 |
42 | def __init__(self, **kwargs):
43 | super().__init__(**kwargs)
44 | # FIXME: Create an installer type system
45 | subprocess.run(
46 | ["playwright", "install"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
47 | )
48 |
49 | def _run(self, url: str, information: str = "") -> str:
50 | playwright = PlaywrightContextManager().start()
51 | browser = playwright.chromium.launch()
52 | try:
53 | page = browser.new_page()
54 |
55 | page.goto(url)
56 | html = page.content()
57 | finally:
58 | browser.close()
59 | playwright.stop()
60 |
61 | soup = BeautifulSoup(html, "html.parser")
62 | body_element = soup.find("body")
63 |
64 | if body_element:
65 | text = body_element.get_text(separator="\n")[:8000]
66 | else:
67 | return "Error: Could not summarize URL."
68 |
69 | if information:
70 | prompt = QUESTION_PROMPT_TEMPLATE.format(
71 | content=text, question=information, url=url
72 | )
73 | else:
74 | prompt = PROMPT_TEMPLATE.format(content=text, url=url)
75 |
76 | response = call_llm(prompt, self.llm)
77 | return response
78 |
79 | async def _arun(self, url: str, information: str = "") -> str:
80 | async with async_playwright() as playwright:
81 | browser = await playwright.chromium.launch()
82 | page = await browser.new_page()
83 |
84 | await page.goto(url)
85 | html = await page.content()
86 |
87 | soup = BeautifulSoup(html, "html.parser")
88 | body_element = soup.find("body")
89 |
90 | if body_element:
91 | text = body_element.get_text(separator="\n")[:8000]
92 | else:
93 | return "Error: Could not summarize URL."
94 |
95 | if information:
96 | prompt = QUESTION_PROMPT_TEMPLATE.format(
97 | content=text, question=information, url=url
98 | )
99 | else:
100 | prompt = PROMPT_TEMPLATE.format(content=text, url=url)
101 |
102 | response = await acall_llm(prompt, self.allm)
103 |
104 | return response
105 |
--------------------------------------------------------------------------------
/beebot/packs/extract_information_from_webpage/test_extract_information_from_webpage.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from extract_information_from_webpage.extract_information_from_webpage import (
4 | ExtractInformationFromWebpage,
5 | )
6 |
7 |
8 | def mock_llm(text_in: str):
9 | return text_in.upper()
10 |
11 |
12 | async def mock_allm(text_in: str):
13 | return text_in.upper()
14 |
15 |
16 | def test_analyze_webpage_content_sync_no_information():
17 | pack = ExtractInformationFromWebpage(llm=mock_llm)
18 | results = pack.run(url="https://www.bbc.com/news")
19 | assert "SPECIFIC QUESTION" not in results
20 | assert "PROVIDE A SUMMARY" in results
21 | assert "BBC" in results
22 |
23 |
24 | def test_analyze_webpage_content_sync_information():
25 | pack = ExtractInformationFromWebpage(llm=mock_llm)
26 | results = pack.run(
27 | url="https://www.bbc.com/news", information="What are the top headlines?"
28 | )
29 | assert "SPECIFIC QUESTION" in results
30 | assert "BBC" in results
31 |
32 |
33 | @pytest.mark.asyncio
34 | async def test_analyze_webpage_content_async_no_information():
35 | pack = ExtractInformationFromWebpage(allm=mock_allm)
36 | results = await pack.arun(url="https://www.bbc.com/news")
37 | assert "SPECIFIC QUESTION" not in results
38 | assert "PROVIDE A SUMMARY" in results
39 | assert "BBC" in results
40 |
41 |
42 | @pytest.mark.asyncio
43 | async def test_analyze_webpage_content_async_information():
44 | pack = ExtractInformationFromWebpage(allm=mock_allm)
45 | results = await pack.arun(
46 | url="https://www.cambridge.org/core/journals/business-and-politics/article/future-of-ai-is-in-the-states-the"
47 | "-case-of-autonomous-vehicle-policies/D6F0B764A976C2A934D517AE1D781195",
48 | information="key findings",
49 | )
50 | assert "KEY FINDINGS" in results
51 | assert "CAMBRIDGE" in results
52 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/packs/filesystem/__init__.py
--------------------------------------------------------------------------------
/beebot/packs/filesystem/delete_file.py:
--------------------------------------------------------------------------------
1 | from autopack import Pack
2 | from pydantic import BaseModel, Field
3 |
4 |
5 | class DeleteFileArgs(BaseModel):
6 | filename: str = Field(..., description="The basename of the file to be deleted")
7 |
8 |
9 | class DeleteFile(Pack):
10 | name = "delete_file"
11 | description = "Deletes a file from disk."
12 | args_schema = DeleteFileArgs
13 | categories = ["Files"]
14 |
15 | def _run(self, filename: str) -> str:
16 | return self.filesystem_manager.delete_file(filename)
17 |
18 | async def _arun(self, filename: str) -> str:
19 | return self.filesystem_manager.adelete_file(filename)
20 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/list_files.py:
--------------------------------------------------------------------------------
1 | from autopack import Pack
2 | from pydantic import BaseModel, Field
3 |
4 | # A few Packs will use poetry inside of the workspace, and the AI gets hella confused when these files are present.
5 | IGNORE_FILES = ["pyproject.toml", "poetry.lock"]
6 |
7 |
8 | class ListFilesArgs(BaseModel):
9 | path: str = Field(description="The directory to list the files of")
10 |
11 |
12 | class ListFiles(Pack):
13 | name = "list_files"
14 | description = "Provides a list of all accessible files in a given path."
15 | args_schema = ListFilesArgs
16 | categories = ["Files"]
17 |
18 | def _run(self, path: str):
19 | return self.filesystem_manager.list_files(path)
20 |
21 | async def _arun(self, path: str) -> str:
22 | return await self.filesystem_manager.alist_files(path)
23 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/read_file.py:
--------------------------------------------------------------------------------
1 | from autopack import Pack
2 | from pydantic import BaseModel, Field
3 |
4 |
5 | class ReadFileArgs(BaseModel):
6 | filename: str = Field(
7 | ...,
8 | description="The name of the file to be read.",
9 | )
10 |
11 |
12 | class ReadFile(Pack):
13 | name = "read_file"
14 | description = "Reads and returns the content of a specified file from the disk."
15 | args_schema = ReadFileArgs
16 | categories = ["Files"]
17 |
18 | def _run(self, filename: str) -> str:
19 | return self.filesystem_manager.read_file(filename)
20 |
21 | async def _arun(self, filename: str) -> str:
22 | return await self.filesystem_manager.aread_file(filename)
23 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/test_delete_file.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from filesystem.delete_file import DeleteFile
4 |
5 |
6 | def test_read_file():
7 | path = "some_file_to_delete.txt"
8 | with open(os.path.join("workspace", path), "w+") as f:
9 | f.write("some string")
10 |
11 | DeleteFile().run(filename=path)
12 |
13 | assert not os.path.exists(path)
14 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/test_list_files.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from filesystem.list_files import ListFiles
4 |
5 |
6 | def test_list_files():
7 | paths = ["a.txt", "b.txt", "c.txt"]
8 | for path in paths:
9 | with open(os.path.join("workspace", path), "w+") as f:
10 | f.write("some string")
11 |
12 | file_list = ListFiles().run(path=".")
13 |
14 | for path in paths:
15 | assert path in file_list
16 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/test_read_file.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from filesystem.read_file import ReadFile
4 |
5 |
6 | def test_read_file():
7 | with open(os.path.join("workspace", "some_file.txt"), "w+") as f:
8 | f.write("some string")
9 |
10 | assert ReadFile().run(filename="some_file.txt") == "some string"
11 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/test_write_file.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from filesystem.write_file import WriteFile
4 |
5 |
6 | def test_write_file():
7 | WriteFile().run(filename="some_file_to_write.txt", text_content="some string")
8 |
9 | with open(os.path.join("workspace", "some_file_to_write.txt"), "r") as f:
10 | assert f.read() == "some string"
11 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem/write_file.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from autopack import Pack
4 | from pydantic import BaseModel, Field
5 |
6 | PACK_DESCRIPTION = (
7 | "Allows you to write specified text content to a file, creating a new file or overwriting an existing one as "
8 | "necessary."
9 | )
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class WriteFileArgs(BaseModel):
15 | filename: str = Field(
16 | ...,
17 | description="Specifies the name of the file to which the content will be written.",
18 | )
19 | text_content: str = Field(
20 | ...,
21 | description="The content that will be written to the specified file.",
22 | )
23 |
24 |
25 | class WriteFile(Pack):
26 | name = "write_file"
27 | description = PACK_DESCRIPTION
28 | args_schema = WriteFileArgs
29 | categories = ["Files"]
30 |
31 | # TODO: This can be reversible for some, but not all, file manager types
32 | reversible = False
33 |
34 | def _run(self, filename: str, text_content: str):
35 | return self.filesystem_manager.write_file(filename, text_content)
36 |
37 | async def _arun(self, filename: str, text_content: str) -> str:
38 | return await self.config.filesystem_manager.awrite_file(filename, text_content)
39 |
--------------------------------------------------------------------------------
/beebot/packs/filesystem_utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def restrict_path(file_path: str, workspace_dir: str):
5 | absolute_path = os.path.abspath(file_path)
6 | relative_path = os.path.relpath(absolute_path, workspace_dir)
7 |
8 | if relative_path.startswith("..") or "/../" in relative_path:
9 | return None
10 |
11 | return absolute_path
12 |
--------------------------------------------------------------------------------
/beebot/packs/get_process_status.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | from beebot.packs.system_base_pack import SystemBasePack
4 |
5 | PACK_NAME = "get_process_status"
6 | PACK_DESCRIPTION = (
7 | "Retrieves the status of the background process with the given pid. Returns the process output if it has "
8 | "finished."
9 | )
10 |
11 |
12 | class GetProcessStatusArgs(BaseModel):
13 | pid: str = Field(..., description="The Process ID")
14 |
15 |
16 | class GetProcessStatus(SystemBasePack):
17 | name = PACK_NAME
18 | description = PACK_DESCRIPTION
19 | args_schema = GetProcessStatusArgs
20 | categories = ["Multiprocess"]
21 |
22 | def _run(self, pid: str) -> str:
23 | process = self.body.processes.get(int(pid))
24 | if not process:
25 | return f"Error: Process {pid} does not exist"
26 |
27 | status = process.poll()
28 | if status is None:
29 | return f"Process {pid} is running"
30 |
31 | return_code = process.returncode
32 |
33 | success_string = "successful" if return_code == 0 else "unsuccessful"
34 | return (
35 | f"The process has completed. Its exit code indicates it was {success_string}. Output: {process.stdout}. "
36 | f"{process.stderr}"
37 | )
38 |
--------------------------------------------------------------------------------
/beebot/packs/get_webpage_html_content/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/packs/get_webpage_html_content/__init__.py
--------------------------------------------------------------------------------
/beebot/packs/get_webpage_html_content/get_webpage_html_content.py:
--------------------------------------------------------------------------------
1 | import aiohttp
2 | import requests
3 | from autopack import Pack
4 | from pydantic import BaseModel, Field
5 |
6 | PACK_DESCRIPTION = (
7 | "Retrieves the raw HTML content of a specified webpage. It is useful when you specifically require access to the "
8 | "raw HTML of a webpage, and are not interested in its text contents."
9 | )
10 |
11 |
12 | class GetHtmlContentArgs(BaseModel):
13 | url: str = Field(
14 | ...,
15 | description="The URL of the webpage from which to retrieve the HTML content.",
16 | )
17 |
18 |
19 | class GetWebpageHtmlContent(Pack):
20 | name = "get_webpage_html_content"
21 | description = PACK_DESCRIPTION
22 | args_schema = GetHtmlContentArgs
23 | categories = ["Web"]
24 | dependencies = ["requests", "aiohttp"]
25 |
26 | filter_threshold: int = Field(
27 | default=0,
28 | description="If given a non-zero value will return only the first N characters",
29 | )
30 |
31 | def _run(self, url: str) -> str:
32 | response = requests.get(url)
33 | if self.filter_threshold:
34 | return response.text[: self.filter_threshold]
35 | return response.text
36 |
37 | async def _arun(self, url: str) -> str:
38 | async with aiohttp.ClientSession() as session:
39 | async with session.get(url) as response:
40 | text = await response.text()
41 | if self.filter_threshold:
42 | return text[: self.filter_threshold]
43 | return text
44 |
--------------------------------------------------------------------------------
/beebot/packs/get_webpage_html_content/test_get_webpage_html_content.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from get_webpage_html_content.get_webpage_html_content import GetWebpageHtmlContent
4 |
5 |
6 | def test_webpage_html_content_with_filter():
7 | # Just a random site with a lot of text
8 | url = "https://en.wikipedia.org/wiki/World_War_II"
9 | pack = GetWebpageHtmlContent(filter_threshold=2000)
10 | response = pack.run(url=url)
11 | assert len(response) == 2000
12 |
13 |
14 | def test_webpage_html_content_without_filter():
15 | # Just a random site with a lot of text
16 | url = "https://en.wikipedia.org/wiki/World_War_II"
17 | pack = GetWebpageHtmlContent()
18 | response = pack.run(url=url)
19 | assert len(response) > 2000
20 |
21 |
22 | @pytest.mark.asyncio
23 | async def test_aget_webpage_html_content_with_filter():
24 | # Just a random site with a lot of text
25 | url = "https://en.wikipedia.org/wiki/World_War_II"
26 | pack = GetWebpageHtmlContent(filter_threshold=2000)
27 | response = await pack.arun(url=url)
28 | assert len(response) == 2000
29 |
30 |
31 | @pytest.mark.asyncio
32 | async def test_aget_webpage_html_content_without_filter():
33 | # Just a random site with a lot of text
34 | url = "https://en.wikipedia.org/wiki/World_War_II"
35 | pack = GetWebpageHtmlContent()
36 | response = await pack.arun(url=url)
37 | assert len(response) > 2000
38 |
--------------------------------------------------------------------------------
/beebot/packs/get_website_text_content.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import requests
4 | from bs4 import BeautifulSoup
5 | from pydantic import BaseModel, Field
6 |
7 | from beebot.packs.system_base_pack import SystemBasePack
8 | from beebot.tool_filters.filter_long_documents import filter_long_documents
9 |
10 | PACK_NAME = "get_website_content"
11 | PACK_DESCRIPTION = (
12 | "Extracts the text content from the HTML of a specified webpage. It is useful when you want to obtain the textual"
13 | "information from a webpage without the need for in-depth analysis."
14 | )
15 |
16 |
17 | class GetWebsiteTextContentArgs(BaseModel):
18 | url: str = Field(
19 | ...,
20 | description="The URL of the webpage from which to retrieve the text content.",
21 | )
22 |
23 |
24 | class GetWebsiteTextContent(SystemBasePack):
25 | name = PACK_NAME
26 | description = PACK_DESCRIPTION
27 | args_schema = GetWebsiteTextContentArgs
28 | categories = ["Web"]
29 |
30 | async def _arun(self, url: str) -> str:
31 | response = requests.get(url)
32 | soup = BeautifulSoup(response.text, "html.parser")
33 | stripped_text = re.sub(r"\s+", " ", soup.get_text().strip())
34 | return await filter_long_documents(self.body, stripped_text)
35 |
--------------------------------------------------------------------------------
/beebot/packs/gmail.py:
--------------------------------------------------------------------------------
1 | from langchain.tools.gmail.create_draft import (
2 | GmailCreateDraft as CreateDraftTool,
3 | )
4 | from langchain.tools.gmail.get_message import (
5 | GmailGetMessage as GetMessageTool,
6 | SearchArgsSchema,
7 | )
8 | from langchain.tools.gmail.get_thread import (
9 | GmailGetThread as GetThreadTool,
10 | GetThreadSchema,
11 | )
12 | from langchain.tools.gmail.search import GmailSearch as SearchTool
13 | from langchain.tools.gmail.send_message import (
14 | GmailSendMessage as SendMessageTool,
15 | )
16 | from langchain.tools.gmail.utils import get_gmail_credentials, build_resource_service
17 | from pydantic import BaseModel, Field
18 |
19 | from beebot.config import Config
20 | from beebot.packs.system_base_pack import SystemBasePack
21 |
22 |
23 | # For all of these packs, see: https://developers.google.com/gmail/api/quickstart/python for how to authenticate
24 |
25 |
26 | def credentials(config: Config):
27 | return get_gmail_credentials(
28 | scopes=["https://mail.google.com/"],
29 | client_secrets_file=config.gmail_credentials_file,
30 | )
31 |
32 |
33 | def api_resource(config: Config):
34 | return build_resource_service(credentials=credentials(config))
35 |
36 |
37 | class MessageSchema(BaseModel):
38 | """The LangChain schema doesn't convert to OpenAI functions"""
39 |
40 | message: str = Field(
41 | ...,
42 | description="The content to include in the message.",
43 | )
44 | to: str = Field(
45 | ...,
46 | description="The comma-separated list of recipients.",
47 | )
48 | subject: str = Field(
49 | ...,
50 | description="The subject of the message.",
51 | )
52 | cc: str = Field(
53 | None,
54 | description="The comma-separated list of CC recipients.",
55 | )
56 | bcc: str = Field(
57 | None,
58 | description="The comma-separated list of BCC recipients.",
59 | )
60 |
61 |
62 | class CreateDraft(SystemBasePack):
63 | name = "gmail_create_draft"
64 | description = "Use Gmail to create a draft email inside of Gmail."
65 | args_schema = MessageSchema
66 | categories = ["Email"]
67 |
68 | def _run(self, *args, **kwargs):
69 | if to_value := kwargs.get("to"):
70 | kwargs["to"] = to_value.split(",")
71 | if to_value := kwargs.get("cc"):
72 | kwargs["cc"] = to_value.split(",")
73 | if to_value := kwargs.get("bcc"):
74 | kwargs["bcc"] = to_value.split(",")
75 |
76 | tool = CreateDraftTool(api_resource=api_resource(self.body.config))
77 | return tool._run(*args, **kwargs)
78 |
79 |
80 | class GetMessage(SystemBasePack):
81 | name = "gmail_get_message"
82 | description = "Get a Gmail message"
83 | args_schema = SearchArgsSchema
84 | categories = ["Email"]
85 | depends_on = ["gmail_get_thread", "gmail_search"]
86 |
87 | def _run(self, *args, **kwargs):
88 | tool = GetMessageTool(api_resource=api_resource(self.body.config))
89 | return tool._run(*args, **kwargs)
90 |
91 |
92 | class GetThread(SystemBasePack):
93 | name = "gmail_get_thread"
94 | description = "Get a Gmail thread"
95 | args_schema = GetThreadSchema
96 | categories = ["Email"]
97 | depends_on = ["gmail_get_message", "gmail_search"]
98 |
99 | def _run(self, *args, **kwargs):
100 | tool = GetThreadTool(api_resource=api_resource(self.body.config))
101 | return tool._run(*args, **kwargs)
102 |
103 |
104 | class Search(SystemBasePack):
105 | name = "gmail_search"
106 | description = "Search for Gmail messages and threads"
107 | args_schema = SearchArgsSchema
108 | categories = ["Email"]
109 | depends_on = ["gmail_get_thread", "gmail_get_message"]
110 |
111 | def _run(self, *args, **kwargs):
112 | tool = SearchTool(api_resource=api_resource(self.body.config))
113 | return tool._run(*args, **kwargs)
114 |
115 |
116 | class SendMessage(SystemBasePack):
117 | name = "gmail_send_message"
118 | description = "Send an email with Gmail"
119 | args_schema = MessageSchema
120 | categories = ["Email"]
121 |
122 | reversible = False
123 |
124 | def _run(self, *args, **kwargs):
125 | if to_value := kwargs.get("to"):
126 | kwargs["to"] = to_value.split(",")
127 | if to_value := kwargs.get("cc"):
128 | kwargs["cc"] = to_value.split(",")
129 | if to_value := kwargs.get("bcc"):
130 | kwargs["bcc"] = to_value.split(",")
131 |
132 | tool = SendMessageTool(api_resource=api_resource(self.body.config))
133 | return tool._run(*args, **kwargs)
134 |
--------------------------------------------------------------------------------
/beebot/packs/google_search/__init__.py:
--------------------------------------------------------------------------------
1 | from .google_search import GoogleSearch
2 |
3 | __all__ = ["GoogleSearch"]
4 |
--------------------------------------------------------------------------------
/beebot/packs/google_search/google_search.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from autopack import Pack
4 | from langchain import GoogleSerperAPIWrapper
5 | from pydantic import BaseModel, Field
6 |
7 | PACK_DESCRIPTION = (
8 | "Search Google for websites matching a given query. Useful for when you need to answer questions "
9 | "about current events."
10 | )
11 |
12 |
13 | class GoogleSearchArgs(BaseModel):
14 | query: str = Field(..., description="The query string")
15 |
16 |
17 | class GoogleSearch(Pack):
18 | name = "google_search"
19 | description = PACK_DESCRIPTION
20 | args_schema = GoogleSearchArgs
21 | categories = ["Web"]
22 |
23 | def _run(self, query: str) -> str:
24 | if not os.environ.get("SERPER_API_KEY"):
25 | return f"Google Search is not supported as the SERPER_API_KEY environment variable is not set"
26 | try:
27 | return format_results(GoogleSerperAPIWrapper().results(query).get("organic", []))
28 |
29 | except Exception as e:
30 | return f"Error: {e}"
31 |
32 | async def _arun(self, query: str) -> str:
33 | if not os.environ.get("SERPER_API_KEY"):
34 | return f"Google Search is not supported as the SERPER_API_KEY environment variable is not set"
35 | try:
36 | query_results = await GoogleSerperAPIWrapper().aresults(query)
37 | return format_results(query_results.get("organic", []))
38 |
39 | except Exception as e:
40 | return f"Error: {e}"
41 |
42 |
43 | def format_results(results: list[dict[str, str]]) -> str:
44 | formatted_results = []
45 | for result in results:
46 | formatted_results.append(f"{result.get('link')}: {result.get('snippet')}")
47 |
48 | return f"Your search results are: {' | '.join(formatted_results)}"
49 |
--------------------------------------------------------------------------------
/beebot/packs/google_search/test_google_search.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest.mock import patch
3 |
4 | import pytest
5 |
6 | from google_search import GoogleSearch
7 |
8 |
9 | def test_search_no_api_key():
10 | results = GoogleSearch().run(query="bbc news website")
11 | assert "not supported" in results
12 |
13 |
14 | def test_search_sync():
15 | os.environ["SERPER_API_KEY"] = "1234"
16 | with patch("langchain.GoogleSerperAPIWrapper.results") as mock_load:
17 | mock_load.return_value = {
18 | "organic": [
19 | {
20 | "link": "https://www.bbc.com/news",
21 | "snippet": "Visit BBC News for up-to-the-minute news, breaking news, video, audio and feature "
22 | "stories. BBC News provides trusted World and UK news as well as local and ...",
23 | },
24 | ]
25 | }
26 | results = GoogleSearch().run(query="bbc news website")
27 |
28 | assert "https://www.bbc.com/news" in results
29 | assert "breaking news" in results
30 |
31 |
32 | @pytest.mark.asyncio
33 | async def test_search_async():
34 | async def async_results(self, query):
35 | return {
36 | "organic": [
37 | {
38 | "link": "https://www.bbc.com/news",
39 | "snippet": "Visit BBC News for up-to-the-minute news, breaking news, video, audio and feature "
40 | "stories. BBC News provides trusted World and UK news as well as local and ...",
41 | },
42 | ]
43 | }
44 |
45 | os.environ["SERPER_API_KEY"] = "1234"
46 | with patch(
47 | "langchain.GoogleSerperAPIWrapper.aresults", new=async_results
48 | ) as mock_load:
49 | results = await GoogleSearch().arun(query="bbc news website")
50 |
51 | assert "https://www.bbc.com/news" in results
52 | assert "breaking news" in results
53 |
--------------------------------------------------------------------------------
/beebot/packs/http_request/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/packs/http_request/__init__.py
--------------------------------------------------------------------------------
/beebot/packs/http_request/http_request.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import aiohttp
4 | import requests
5 | from autopack import Pack
6 | from pydantic import BaseModel, Field
7 |
8 | PACK_DESCRIPTION = (
9 | "Makes an HTTP request and returns the raw response. This function should be used for basic GET or "
10 | "POST requests and is not intended for fetching web pages or performing complex operations."
11 | )
12 |
13 |
14 | class HttpRequestArgs(BaseModel):
15 | url: str = Field(..., description="URL of the resource to request.")
16 | method: str = Field(
17 | description="The HTTP method to use for the request (e.g., GET, POST). Defaults to GET.",
18 | default="GET",
19 | )
20 | data: str = Field(description="Data to send with the request (for POST requests).", default="")
21 | headers: str = Field(
22 | description="JSON Encoded headers to include in the request.",
23 | default_factory=dict,
24 | )
25 |
26 |
27 | class HttpRequest(Pack):
28 | name = "http_request"
29 | description = PACK_DESCRIPTION
30 | args_schema = HttpRequestArgs
31 | categories = ["Web"]
32 |
33 | def _run(self, url: str, method: str = "GET", data: str = None, headers: str = None) -> str:
34 | headers_dict = {}
35 | if headers:
36 | headers_dict = json.loads(headers)
37 | response = requests.request(method, url, headers=headers_dict, data=data)
38 | return f"HTTP Response {response.status_code}: {response.content}"
39 |
40 | async def _arun(self, url: str, method: str = "GET", data: str = None, headers: str = None) -> str:
41 | headers_dict = {}
42 | if headers:
43 | headers_dict = json.loads(headers)
44 |
45 | async with aiohttp.ClientSession() as session:
46 | async with session.request(method, url, data=data, headers=headers_dict) as response:
47 | text = await response.text()
48 | return f"HTTP Response {response.status}: {text}"
49 |
--------------------------------------------------------------------------------
/beebot/packs/http_request/test_http_request.py:
--------------------------------------------------------------------------------
1 | from html_request.http_request import HttpRequest
2 |
3 |
4 | def test_http_request_get():
5 | url = "https://google.com"
6 | pack = HttpRequest()
7 | response = pack.run(url=url)
8 | assert "Google Search" in response
9 |
10 |
11 | def test_http_request_get():
12 | url = "https://google.com"
13 | pack = HttpRequest()
14 | response = pack.run(url=url)
15 | assert "Google Search" in response
16 |
--------------------------------------------------------------------------------
/beebot/packs/install_python_package.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from beebot.body.pack_utils import init_workspace_poetry
6 | from beebot.packs.system_base_pack import SystemBasePack
7 |
8 | """This will use a poetry venv specifically for the beebot workspace. However, all normal caveats apply regarding
9 | downloading and executing remote code."""
10 |
11 | PACK_NAME = "install_python_package"
12 |
13 | PACK_DESCRIPTION = "Installs Python packages from PyPi using Poetry."
14 |
15 |
16 | class InstallPythonPackageArgs(BaseModel):
17 | package_name: str = Field(
18 | ..., description="The name of the Python package to be installed"
19 | )
20 |
21 |
22 | class InstallPythonPackage(SystemBasePack):
23 | name = PACK_NAME
24 | description = PACK_DESCRIPTION
25 | args_schema = InstallPythonPackageArgs
26 | categories = ["Programming"]
27 |
28 | def _run(self, package_name: str) -> str:
29 | if self.body.config.restrict_code_execution:
30 | return "Error: Installing Python packages is not allowed"
31 | try:
32 | # Make sure poetry is init'd in the workspace. This errors if it's already init'd so yolo
33 | init_workspace_poetry(self.config.workspace_path)
34 | cmd = ["poetry", "add", package_name]
35 |
36 | process = subprocess.run(
37 | cmd,
38 | universal_newlines=True,
39 | cwd=self.body.config.workspace_path,
40 | stdout=subprocess.PIPE,
41 | stderr=subprocess.PIPE,
42 | )
43 | if process.returncode:
44 | return f"Error: {process.stdout}. {process.stderr}."
45 |
46 | return f"{package_name} is installed."
47 | except (SystemExit, KeyboardInterrupt):
48 | raise
49 | except BaseException as e:
50 | return f"Error: {e}"
51 |
--------------------------------------------------------------------------------
/beebot/packs/kill_process.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | from beebot.packs.system_base_pack import SystemBasePack
4 |
5 | PACK_NAME = "kill_process"
6 | PACK_DESCRIPTION = "Terminates the process with the given PID and returns its output"
7 |
8 |
9 | class KillProcessArgs(BaseModel):
10 | pid: str = Field(..., description="The PID")
11 |
12 |
13 | class KillProcess(SystemBasePack):
14 | name = PACK_NAME
15 | description = PACK_DESCRIPTION
16 | args_schema = KillProcessArgs
17 | categories = ["Multiprocess"]
18 |
19 | def _run(self, pid: str) -> dict[str, str]:
20 | # TODO: Support for daemonized processes from previous runs
21 | process = self.body.processes.get(int(pid))
22 | if not process:
23 | return "Error: Process does not exist."
24 |
25 | status = process.poll()
26 | if status is None:
27 | process.kill()
28 |
29 | return (
30 | f"The process has been killed. Output: {process.stdout}. {process.stderr}"
31 | )
32 |
--------------------------------------------------------------------------------
/beebot/packs/list_processes.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | from beebot.packs.system_base_pack import SystemBasePack
4 |
5 | PACK_NAME = "list_processes"
6 | PACK_DESCRIPTION = "Lists the actively running processes and their status."
7 |
8 |
9 | class ListProcessesArgs(BaseModel):
10 | pass
11 |
12 |
13 | class ListProcesses(SystemBasePack):
14 | name = PACK_NAME
15 | description = PACK_DESCRIPTION
16 | args_schema = ListProcessesArgs
17 | categories = ["Multiprocess"]
18 |
19 | def _run(self) -> str:
20 | running_processes = [
21 | f"PID {pid}: running."
22 | for (pid, process) in self.body.processes.items()
23 | if process.poll() is None
24 | ]
25 |
26 | if running_processes:
27 | return " ".join(running_processes)
28 |
29 | return "No processes running"
30 |
--------------------------------------------------------------------------------
/beebot/packs/os_info/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/packs/os_info/__init__.py
--------------------------------------------------------------------------------
/beebot/packs/os_info/os_info.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from autopack import Pack
4 | from pydantic import BaseModel
5 |
6 | PACK_DESCRIPTION = "Get the name and version of the operating system you are running in."
7 |
8 |
9 | class OSInfoArgs(BaseModel):
10 | pass
11 |
12 |
13 | class OSInfo(Pack):
14 | name = "os_name_and_version"
15 | description = PACK_DESCRIPTION
16 | args_schema = OSInfoArgs
17 | categories = ["System Info"]
18 |
19 | def _run(self) -> str:
20 | return f"OS Name {platform.system()}. OS Version: {platform.release()}."
21 |
22 | async def _arun(self) -> str:
23 | return self._run()
24 |
--------------------------------------------------------------------------------
/beebot/packs/os_info/test_os_info.py:
--------------------------------------------------------------------------------
1 | from os_info.os_info import OSInfo
2 |
3 |
4 | def test_os_info():
5 | info = OSInfo().run()
6 | assert "OS Name" in info
7 | assert "OS Version" in info
8 |
--------------------------------------------------------------------------------
/beebot/packs/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "official"
3 | version = "0.1.0"
4 | description = "Offical Packs for AutoPack"
5 | authors = ["Erik Peterson "]
6 | license = "MIT"
7 | readme = "README.md"
8 |
9 | [tool.poetry.dependencies]
10 | python = "^3.10"
11 | autopack-tools = ">=0.3.0"
12 | wikipedia = "^1.4.0"
13 | psutil = "^5.9.5"
14 | wolframalpha = "^5.0.0"
15 | playwright = "^1.36.0"
16 | beautifulsoup4 = "^4.12.2"
17 |
18 |
19 | [tool.poetry.group.dev.dependencies]
20 | black = "^23.7.0"
21 | pytest-asyncio = "^0.21.1"
22 |
23 | [build-system]
24 | requires = ["poetry-core"]
25 | build-backend = "poetry.core.masonry.api"
26 |
--------------------------------------------------------------------------------
/beebot/packs/summarization.py:
--------------------------------------------------------------------------------
1 | SUMMARIZATION_TEMPLATE = """Make the following text more concise, ensuring the final output is no more than {filter_threshold} characters long.
2 |
3 | Simplify the language used. Replace long phrases with shorter synonyms, remove unnecessary adverbs and adjectives, or rephrase sentences to make them more concise.
4 |
5 | Remove redundancies, especially those written informally or conversationally, using repetitive information or phrases.
6 |
7 | Flatten text that includes nested hierarchical information that isn't crucial for understanding.
8 |
9 | Extract text content out of HTML tags.
10 |
11 | Retain key details such as file names, IDs, people, places, and important events:
12 |
13 | {long_text}"""
14 |
15 |
16 | def _filter_long_documents(self, document: str) -> str:
17 | # TODO: Configurable limit or like configurable turn it off?
18 | if len(document) > 1000:
19 | summary_prompt = SUMMARIZATION_TEMPLATE.format(
20 | long_text=document[:8000], filter_threshold=self.filter_threshold
21 | )
22 | summarization = call_llm(summary_prompt, llm=self.llm)
23 | return f"The response was summarized as: {summarization}"
24 |
25 | return document
26 |
27 |
28 | async def afilter_long_documents(
29 | self,
30 | document: str,
31 | llm: Callable[[str], str],
32 | allm: Callable[[str], Awaitable[str]],
33 | ) -> str:
34 | pass
35 |
--------------------------------------------------------------------------------
/beebot/packs/summarization_prompt.py:
--------------------------------------------------------------------------------
1 | from langchain.prompts import SystemMessagePromptTemplate
2 |
3 | TEMPLATE = """Make the following text more concise, ensuring the final output is no more than 2000 characters long.
4 |
5 | Simplify the language used. Replace long phrases with shorter synonyms, remove unnecessary adverbs and adjectives, or rephrase sentences to make them more concise.
6 |
7 | Remove redundancies, especially those written informally or conversationally, using repetitive information or phrases.
8 |
9 | Flatten text that includes nested hierarchical information that isn't crucial for understanding.
10 |
11 | Extract text content out of HTML tags.
12 |
13 | Retain key details such as file names, IDs, people, places, and important events:
14 |
15 | {long_text}"""
16 |
17 |
18 | def summarization_prompt_template() -> SystemMessagePromptTemplate:
19 | return SystemMessagePromptTemplate.from_template(TEMPLATE)
20 |
--------------------------------------------------------------------------------
/beebot/packs/system_base_pack.py:
--------------------------------------------------------------------------------
1 | from autopack import Pack
2 | from autopack.utils import run_args_from_args_schema
3 |
4 | from beebot.body import Body
5 | from beebot.body.pack_utils import llm_wrapper
6 |
7 |
8 | class SystemBasePack(Pack):
9 | arbitrary_types_allowed = True
10 |
11 | body: Body
12 |
13 | def __init__(self, **kwargs):
14 | llm = llm_wrapper(kwargs.get("body"))
15 |
16 | run_args = {}
17 | if args_schema := kwargs.get("args_schema"):
18 | run_args = run_args_from_args_schema(args_schema)
19 |
20 | super().__init__(llm=llm, allm=llm, run_args=run_args, **kwargs)
21 |
22 | def _run(self, *args, **kwargs):
23 | raise NotImplementedError
24 |
25 | async def _arun(self, *args, **kwargs) -> str:
26 | return self._run(*args, **kwargs)
27 |
--------------------------------------------------------------------------------
/beebot/packs/wikipedia_summarize/__init__.py:
--------------------------------------------------------------------------------
1 | from .wikipedia import WikipediaPack
2 |
3 | _all__ = ["WikipediaPack"]
4 |
--------------------------------------------------------------------------------
/beebot/packs/wikipedia_summarize/test_wikipedia.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | import pytest
4 | from langchain.schema import Document
5 |
6 | from wikipedia_summarize import WikipediaPack
7 |
8 |
9 | def test_sync_llm():
10 | def mock_llm(text_in: str):
11 | return text_in.strip().split("\n")[-1]
12 |
13 | pack = WikipediaPack(llm=mock_llm)
14 | with patch("langchain.WikipediaAPIWrapper.load") as mock_load:
15 | # Set the return value of the mock object
16 | page = Document(page_content="Page content", metadata={"title": "A page"})
17 | mock_load.return_value = [page]
18 |
19 | assert pack.run(query="some text", question="asdf") == "Page content"
20 |
21 |
22 | @pytest.mark.asyncio
23 | async def test_async_llm():
24 | async def mock_llm(text_in: str):
25 | return text_in.strip().split("\n")[-1]
26 |
27 | pack = WikipediaPack(allm=mock_llm)
28 | with patch("langchain.WikipediaAPIWrapper.load") as mock_load:
29 | # Set the return value of the mock object
30 | page = Document(page_content="Page content", metadata={"title": "A page"})
31 | mock_load.return_value = [page]
32 |
33 | assert await pack.arun(query="some text", question="asdf") == "Page content"
34 |
--------------------------------------------------------------------------------
/beebot/packs/wikipedia_summarize/wikipedia.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from typing import Union
4 |
5 | import wikipedia
6 | from autopack.utils import acall_llm
7 | from pydantic import BaseModel, Field
8 | from wikipedia import WikipediaPage
9 |
10 | from beebot.packs.system_base_pack import SystemBasePack
11 |
12 | PACK_DESCRIPTION = (
13 | "Searches Wikipedia based on a question and then analyzes the results. Enables quick access to factual knowledge. "
14 | "Useful for when you need to answer general questions about people, places, companies, facts, historical events, "
15 | "or other subjects."
16 | )
17 |
18 | PROMPT_TEMPLATE = """Provide an answer to the following question: {question}
19 |
20 | The following Wikipedia pages may also be used for reference:
21 | {pages}
22 | """
23 |
24 | logger = logging.getLogger(__name__)
25 |
26 |
27 | class WikipediaArgs(BaseModel):
28 | question_to_ask: str = Field(..., description="A question to ask")
29 |
30 |
31 | def fetch_page(page_title: str) -> Union[WikipediaPage, None]:
32 | try:
33 | return wikipedia.page(title=page_title)
34 | except BaseException as e:
35 | logger.warning(f"Could not fetch wikipedia page with title {page_title}: {e}")
36 | return None
37 |
38 |
39 | async def get_page(page_title: str) -> WikipediaPage:
40 | loop = asyncio.get_event_loop()
41 | return await loop.run_in_executor(None, fetch_page, page_title)
42 |
43 |
44 | async def get_pages(query: str) -> str:
45 | page_titles = wikipedia.search(query, results=5)
46 | pages = await asyncio.gather(*(get_page(title) for title in page_titles))
47 |
48 | result = []
49 | for page in pages:
50 | if page:
51 | result.append(f"-- Page: {page.title}\n{page.summary}")
52 |
53 | return "\n".join(result)
54 |
55 |
56 | class WikipediaPack(SystemBasePack):
57 | name = "wikipedia"
58 | description = PACK_DESCRIPTION
59 | args_schema = WikipediaArgs
60 | dependencies = ["wikipedia"]
61 | categories = ["Information"]
62 |
63 | async def _arun(
64 | self,
65 | question_to_ask: str,
66 | ):
67 | try:
68 | pages = await get_pages(question_to_ask)
69 | prompt = PROMPT_TEMPLATE.format(question=question_to_ask, pages=pages)
70 | response = await acall_llm(prompt, self.allm)
71 | return response
72 | except Exception as e:
73 | return f"Error: {e}"
74 |
--------------------------------------------------------------------------------
/beebot/packs/wolframalpha_query/__init__.py:
--------------------------------------------------------------------------------
1 | from .wolframalpha_query import WolframAlphaQuery
2 |
3 | __all__ = ["WolframAlphaQuery"]
4 |
--------------------------------------------------------------------------------
/beebot/packs/wolframalpha_query/test_wolframalpha_query.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest.mock import patch
3 |
4 | from wolframalpha_query import WolframAlphaQuery
5 |
6 |
7 | def test_wolframalpha_no_appid():
8 | pack = WolframAlphaQuery()
9 | assert "not supported" in pack.run(query="population of united states")
10 |
11 |
12 | def test_wolframalpha_with_appid():
13 | os.environ["WOLFRAM_ALPHA_APPID"] = "1234"
14 | pack = WolframAlphaQuery()
15 |
16 | with patch("langchain.WolframAlphaAPIWrapper.run") as mock_load:
17 | mock_load.return_value = "331.9 million"
18 | assert "331.9 million" in pack.run(query="population of united states")
19 |
--------------------------------------------------------------------------------
/beebot/packs/wolframalpha_query/wolframalpha_query.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from autopack import Pack
4 | from langchain import WolframAlphaAPIWrapper
5 | from pydantic import BaseModel, Field
6 |
7 | PACK_DESCRIPTION = (
8 | "Query Wolfram Alpha, a computational knowledge engine, to obtain answers to a wide range of factual and "
9 | "computational questions. It leverages Wolfram Alpha's extensive knowledge base to provide detailed and accurate "
10 | "responses. Useful for when you need to answer questions about Math, Science, Technology, and Everyday Life."
11 | )
12 |
13 |
14 | class WolframAlphaArgs(BaseModel):
15 | query: str = Field(
16 | ...,
17 | description="Specifies the question or topic for which information is sought.",
18 | )
19 |
20 |
21 | class WolframAlphaQuery(Pack):
22 | name = "wolfram_alpha_query"
23 | description = PACK_DESCRIPTION
24 | args_schema = WolframAlphaArgs
25 | dependencies = ["wolframalpha_query"]
26 | categories = ["Information"]
27 |
28 | def _run(self, query: str) -> list[str]:
29 | if not os.environ.get("WOLFRAM_ALPHA_APPID"):
30 | return f"WolframAlpha is not supported as the WOLFRAM_ALPHA_APPID environment variable is not set"
31 |
32 | return WolframAlphaAPIWrapper().run(query)
33 |
34 | async def _arun(self, query: str) -> list[str]:
35 | return self._run(query)
36 |
--------------------------------------------------------------------------------
/beebot/packs/write_python_code/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/packs/write_python_code/__init__.py
--------------------------------------------------------------------------------
/beebot/packs/write_python_code/test_write_python_file.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from write_python_code.write_python_file import WritePythonCode
4 |
5 |
6 | def test_write_python_file_success():
7 | pack = WritePythonCode(workspace_path="workspace")
8 | code = "print('Hello world!')"
9 | file_name = "hello_world.py"
10 | result = pack.run(file_name=file_name, code=code)
11 |
12 | assert result == f"Compiled successfully and saved to hello_world.py."
13 |
14 | with open(os.path.join("workspace", file_name), "r") as f:
15 | assert f.read() == code
16 |
17 |
18 | def test_write_python_file_compile_error():
19 | pack = WritePythonCode(workspace_path="workspace")
20 | code = "asdf!"
21 | file_name = "error.py"
22 | result = pack.run(file_name=file_name, code=code)
23 |
24 | assert "invalid syntax" in result
25 |
26 | assert not os.path.exists(os.path.join("workspace", file_name))
27 |
--------------------------------------------------------------------------------
/beebot/packs/write_python_code/write_python_file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 |
4 | from autopack import Pack
5 | from pydantic import BaseModel, Field
6 |
7 | from beebot.utils import restrict_path
8 |
9 | # IMPORTANT NOTE: This does NOT actually restrict the execution environment, it just nudges the AI to avoid doing
10 | # those things.
11 | PACK_DESCRIPTION = (
12 | "Write Python code to a specified file. Will compile the code and check for any syntax errors. If "
13 | "there are no syntax errors, the code will be saved to the specified file. The function returns a "
14 | "string indicating the presence of any syntax errors in the code. However, it does not execute the "
15 | "code."
16 | )
17 |
18 |
19 | class WritePythonCodeArgs(BaseModel):
20 | file_name: str = Field(
21 | ...,
22 | description="The name of the file to be created or overwritten",
23 | )
24 | code: str = Field(
25 | ...,
26 | description="The Python code as a string.",
27 | )
28 |
29 |
30 | class WritePythonCode(Pack):
31 | name = "write_python_code"
32 | description = PACK_DESCRIPTION
33 | args_schema = WritePythonCodeArgs
34 | categories = ["Programming"]
35 | reversible = False
36 |
37 | def check_file(self, file_name, code: str = "") -> str:
38 | file_path = os.path.join(self.config.workspace_path, file_name)
39 |
40 | try:
41 | abs_path = restrict_path(file_path, self.config.workspace_path)
42 | if not abs_path:
43 | return "Error: File not found"
44 |
45 | with open(file_path, "w+") as f:
46 | f.write(code)
47 |
48 | cmd = ["python", "-m", "py_compile", abs_path]
49 | process = subprocess.run(
50 | cmd,
51 | stdout=subprocess.PIPE,
52 | stderr=subprocess.PIPE,
53 | universal_newlines=True,
54 | cwd=self.config.workspace_path,
55 | )
56 | output = "\n".join([process.stdout.strip(), process.stderr.strip()]).strip()
57 |
58 | if process.returncode:
59 | os.unlink(file_path)
60 | return f"Compile error: {output}."
61 |
62 | return f"Compiled successfully and saved to {file_name}."
63 |
64 | except Exception as e:
65 | os.unlink(file_path)
66 | return f"Error: {e.__class__.__name__} {e}"
67 |
68 | def _run(self, file_name: str, code: str = "") -> str:
69 | result = self.check_file(file_name, code)
70 | self.filesystem_manager.write_file(file_name, code)
71 | return result
72 |
73 | async def _arun(self, file_name: str, code: str = "") -> str:
74 | result = self.check_file(file_name, code)
75 | await self.filesystem_manager.awrite_file(file_name, code)
76 | return result
77 |
--------------------------------------------------------------------------------
/beebot/planner/__init__.py:
--------------------------------------------------------------------------------
1 | from .planner import Planner
2 |
3 | __all__ = ["Planner"]
4 |
--------------------------------------------------------------------------------
/beebot/planner/planner.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import TYPE_CHECKING
3 |
4 | from langchain.chat_models.base import BaseChatModel
5 |
6 | from beebot.body.llm import call_llm
7 | from beebot.models.database_models import Plan
8 |
9 | if TYPE_CHECKING:
10 | from beebot.execution.task_execution import TaskExecution
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class Planner:
16 | llm: BaseChatModel
17 |
18 | task_execution: "TaskExecution"
19 |
20 | def __init__(self, task_execution: "TaskExecution"):
21 | self.task_execution = task_execution
22 |
23 | async def plan(self) -> Plan:
24 | prompt, prompt_variables = await self.task_execution.agent.planning_prompt()
25 |
26 | logger.info("\n=== Plan Request ===")
27 | logger.info(prompt)
28 |
29 | response = await call_llm(
30 | self.task_execution.body,
31 | message=prompt,
32 | function_call="none",
33 | include_functions=True,
34 | )
35 |
36 | logger.info("\n=== Plan Created ===")
37 | logger.info(response.text)
38 |
39 | plan = Plan(
40 | prompt_variables=prompt_variables,
41 | plan_text=response.text,
42 | llm_response=response.text,
43 | )
44 | await plan.save()
45 | return plan
46 |
--------------------------------------------------------------------------------
/beebot/planner/planning_prompt.py:
--------------------------------------------------------------------------------
1 | PLANNING_PROMPT_TEMPLATE = """As the AI Task Strategist, your role is to strategize and plan the execution of tasks efficiently and effectively. Avoid redundancy, such as unnecessary immediate verification of actions. You only speak English and do not have the capability to write code.
2 |
3 | # Functions
4 | ## The AI Assistant can call only these functions:
5 | {functions}.
6 |
7 | Once the original task has been completed, instruct the AI Assistant to call the `exit` function with all arguments to indicate the completion of the task.
8 |
9 | # Task
10 | ## Your original task, given by the human, is:
11 | {task}
12 |
13 | {history}
14 | {variables}
15 | {file_list}
16 |
17 | # Instructions
18 | ## Now, devise a concise and adaptable plan to guide the AI Assistant. Follow these guidelines:
19 |
20 | 1. Ensure you interpret the execution history correctly while considering the order of execution. Avoid repetitive actions, e.g. if the same file has been read previously and the content hasn't changed.
21 | 2. Regularly evaluate your progress towards the task goal. This includes checking the current state of the system against the task requirements and adjusting your strategy if necessary.
22 | 3. If an error occurs (like 'File not found'), take a step back and analyze if it's an indicator of the next required action (like creating the file). Avoid getting stuck in loops by not repeating the action that caused the error without modifying the approach.
23 | 4. Recognize when the task has been successfully completed according to the defined goal and exit conditions. If the task has been completed, instruct the AI Assistant to call the `exit` function.
24 | 5. Determine the most efficient next action towards completing the task, considering your current information, requirements, and available functions.
25 | 6. Direct the execution of the immediate next action using exactly one of the callable functions, making sure to skip any redundant actions that are already confirmed by the historical context.
26 |
27 | Provide a concise analysis of the past history, followed by an overview of your plan going forward, and end with one sentence describing the immediate next action to be taken."""
28 |
29 |
30 | def planning_prompt_template() -> str:
31 | return PLANNING_PROMPT_TEMPLATE
32 |
--------------------------------------------------------------------------------
/beebot/tool_filters/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/beebot/tool_filters/__init__.py
--------------------------------------------------------------------------------
/beebot/tool_filters/filter_long_documents.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | from beebot.body.llm import call_llm
4 | from beebot.packs.summarization_prompt import summarization_prompt_template
5 |
6 | if TYPE_CHECKING:
7 | from beebot.body import Body
8 |
9 |
10 | async def filter_long_documents(body: "Body", document: str) -> str:
11 | # TODO: Configurable limit or like configurable turn it off?
12 | if len(document) > 1000:
13 | summary_prompt = (
14 | summarization_prompt_template().format(long_text=document[:8000]).content
15 | )
16 | summarization = await call_llm(body, summary_prompt)
17 | return f"The response was summarized as: {summarization.text}"
18 |
19 | return document
20 |
--------------------------------------------------------------------------------
/beebot/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | logger = logging.getLogger(__name__)
5 |
6 |
7 | def restrict_path(file_path: str, workspace_dir: str):
8 | absolute_path = os.path.abspath(file_path)
9 | relative_path = os.path.relpath(absolute_path, workspace_dir)
10 |
11 | if relative_path.startswith("..") or "/../" in relative_path:
12 | return None
13 |
14 | return absolute_path
15 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | postgres:
4 | image: postgres:latest
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=postgres
9 | - POSTGRES_DB=postgres
10 | ports:
11 | - "5432:5432"
12 | volumes:
13 | - ./postgres-data:/var/lib/postgresql/data
14 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # BeeBot's Architecture
2 |
3 | ## Introduction
4 |
5 | BeeBot is an Autonomous AI Assistant is designed to perform a range of tasks autonomously, taking high-level
6 | instructions from human users and transforming them into actions which are then performed. BeeBot's capabilities are
7 | based on the underlying components: Planner, Decider, Executor, and Body. These components work in sequence
8 | in a state machine, described below.
9 |
10 | ## Major Components
11 |
12 | ### Planner
13 |
14 | The Planner is responsible for taking the human task and the previous outputs, and creating a Plan object for it. This
15 | is done through an LLM that can interpret the requirements of the task and the context provided by
16 | previous outputs. The Planner thus plays a vital role in interpreting and organizing tasks, setting the stage for their
17 | execution.
18 |
19 | ### Decider
20 |
21 | The Decider takes a Plan, which may or may not have been amended by an Oversight. It assesses the Plan and makes a
22 | Decision, again through the LLM, about what action the system should take next. The Decision maps to exactly one
23 | function call.
24 |
25 | ### Executor
26 |
27 | The Executor is the "action" component of the system. Once a Decision has been made about the next step, the Executor
28 | takes this Decision and performs the necessary action. It also creates an Observation based on the results of the
29 | action. This Observation is a structured record of what occurred during execution.
30 |
31 | ### Body
32 |
33 | The Body is a container for the Planner, Decider, and Executor components. It also holds memories and
34 | configuration settings that help the Assistant adapt and optimize its functioning. The Body manages the data flow
35 | between the other components and also holds the State Machine that governs the operation of the entire system.
36 |
37 | ## Operational Workflow (State Machine)
38 |
39 | ```
40 | [ Setup ]
41 | |
42 | | start
43 | v
44 | [ Oversight ]<------+
45 | | |
46 | | decide |
47 | v |
48 | [ Deciding ] [Planning]
49 | | |
50 | | execute |
51 | v |
52 | [ Executing ]-------+
53 | | plan
54 | | finish
55 | v
56 | [ Done ]
57 |
58 | ```
59 |
60 | These state transitions are governed by events or conditions such as `start`, `plan`, `decide`, `execute`, `oversee`,
61 | and `finish`. These dictate how the Assistant moves from one state to the next, and how the Planner, Decider, and
62 | Executor components interact.
--------------------------------------------------------------------------------
/migrations/20230717_01_initial_schema.sql:
--------------------------------------------------------------------------------
1 | --
2 | -- depends:
3 |
4 | CREATE TABLE body (
5 | id SERIAL PRIMARY KEY,
6 | task TEXT NOT NULL,
7 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
8 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
9 | );
10 |
11 | CREATE TABLE task_execution (
12 | id SERIAL PRIMARY KEY,
13 | body_id INT REFERENCES body(id),
14 | agent TEXT NOT NULL,
15 | state TEXT NOT NULL DEFAULT 'waiting',
16 | instructions TEXT NOT NULL,
17 | complete BOOLEAN NOT NULL DEFAULT FALSE,
18 | inputs JSONB,
19 | outputs JSONB,
20 | variables JSONB,
21 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
22 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
23 | );
24 |
25 | CREATE TABLE oversight (
26 | id SERIAL PRIMARY KEY,
27 | original_plan_text TEXT NOT NULL,
28 | modified_plan_text TEXT NOT NULL,
29 | modifications JSONB,
30 | llm_response TEXT,
31 | prompt_variables JSONB,
32 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
33 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
34 | );
35 |
36 | CREATE TABLE decision (
37 | id SERIAL PRIMARY KEY,
38 | tool_name TEXT NOT NULL,
39 | tool_args JSONB,
40 | prompt_variables JSONB,
41 | llm_response TEXT,
42 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
43 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
44 | );
45 |
46 | CREATE TABLE observation (
47 | id SERIAL PRIMARY KEY,
48 | response TEXT,
49 | error_reason TEXT,
50 | success BOOLEAN DEFAULT TRUE,
51 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
52 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
53 | );
54 |
55 | CREATE TABLE plan (
56 | id SERIAL PRIMARY KEY,
57 | plan_text TEXT NOT NULL,
58 | prompt_variables JSONB,
59 | llm_response TEXT,
60 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
61 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
62 | );
63 |
64 | CREATE TABLE step (
65 | id SERIAL PRIMARY KEY,
66 | task_execution_id INT REFERENCES task_execution(id),
67 | plan_id INT REFERENCES plan(id),
68 | oversight_id INT REFERENCES oversight(id),
69 | decision_id INT REFERENCES decision(id),
70 | observation_id INT REFERENCES observation(id),
71 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
72 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
73 | );
74 |
75 | CREATE TABLE document (
76 | id SERIAL PRIMARY KEY,
77 | name TEXT NOT NULL,
78 | content TEXT NOT NULL,
79 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
80 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
81 | );
82 |
83 | CREATE INDEX idx_document_name ON document(name);
84 |
85 | CREATE UNIQUE INDEX idx_document_name_content ON document(name, content);
86 |
87 | CREATE TABLE document_step (
88 | id SERIAL PRIMARY KEY,
89 | step_id integer NOT NULL references step(id),
90 | document_id integer NOT NULL references document(id),
91 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
92 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
93 | );
94 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "beebot"
3 | version = "0.1.0"
4 | description = "An AI agent that works"
5 | authors = ["Erik Peterson "]
6 | license = "MIT"
7 | readme = "README.md"
8 | packages = [{ include = "beebot" }]
9 |
10 | [tool.poetry.scripts]
11 | beebot = 'beebot.initiator.cli:main'
12 |
13 | [tool.poetry.dependencies]
14 | # I hate this version specification but we can't use python-statemachine in 3.12
15 | python = ">=3.10,<3.12"
16 | langchain = ">=0.0.215"
17 | openai = "^0.27.8"
18 | python-dotenv = "^1.0.0"
19 | python-statemachine = "^2.1.0"
20 | playwright = "^1.35.0"
21 | lxml = "^4.9.3"
22 | google-auth-httplib2 = "^0.1.0"
23 | google-auth-oauthlib = "^1.0.0"
24 | google-api-python-client = "^2.93.0"
25 | wolframalpha = "^5.0.0"
26 | yoyo-migrations = "^8.2.0"
27 | psycopg2-binary = { extras = ["ext"], version = "^2.9.6" }
28 | beautifulsoup4 = "^4.12.2"
29 | psutil = "^5.9.5"
30 | coloredlogs = "^15.0.1"
31 | uvicorn = { extras = ["standard"], version = "^0.23.1" }
32 | fastapi = "^0.100.0"
33 | wikipedia = "^1.4.0"
34 | autopack-tools = "^0.4.0"
35 | tortoise-orm = { extras = ["postgres"], version = "^0.19.3" }
36 | asyncpg = "^0.28.0"
37 | pytest-asyncio = "^0.21.1"
38 |
39 | [tool.poetry.group.dev.dependencies]
40 | black = "^23.3.0"
41 | ruff = "^0.0.276"
42 | pytest = "^7.4.0"
43 | colorama = "^0.4.6"
44 | pytest-asyncio = "^0.21.1"
45 | baserun = "^0.3.1"
46 | agbenchmark = "^0.0.7"
47 |
48 | [build-system]
49 | requires = ["poetry-core"]
50 | build-backend = "poetry.core.masonry.api"
51 |
52 | [tool.ruff]
53 | line-length = 120
54 | # Never enforce `E501` (line length violations). Newlines in strings sent to the LLM may impact its comprehension.
55 | ignore = ["E501"]
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests
3 | filterwarnings =
4 | ignore::DeprecationWarning
5 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | pip install poetry
4 | poetry install
5 | poetry run playwright install
6 | docker compose up -d
7 |
8 | echo "BeeBot is now set up"
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # each test runs on cwd to its temp dir
2 | import asyncio
3 | import os
4 | import signal
5 | import sys
6 |
7 | import psutil
8 | import pytest
9 | from _pytest.fixtures import FixtureRequest
10 | from dotenv import load_dotenv
11 | from tortoise import Tortoise
12 |
13 | from beebot.body import Body
14 | from beebot.models.database_models import initialize_db
15 |
16 |
17 | def pytest_configure():
18 | load_dotenv()
19 |
20 |
21 | @pytest.fixture(autouse=True)
22 | def go_to_tmpdir(request):
23 | # Get the fixture dynamically by its name.
24 | tmpdir = request.getfixturevalue("tmpdir")
25 | # ensure local test created packages can be imported
26 | sys.path.insert(0, str(tmpdir))
27 |
28 | # In an ideal world we would truly end-to-end pack search, but it's really expensive to do every time, so we copy it
29 | # source_dir = ".autopack"
30 | # destination_dir = os.path.join(tmpdir.strpath, ".autopack")
31 | # shutil.copytree(source_dir, destination_dir)
32 |
33 | print(f"Executing tests in the directory {tmpdir.strpath}")
34 |
35 | # Chdir only for the duration of the test.
36 | with tmpdir.as_cwd():
37 | yield
38 |
39 |
40 | def kill_child_processes(parent_pid: int, sig=signal.SIGKILL):
41 | try:
42 | parent = psutil.Process(parent_pid)
43 | except psutil.NoSuchProcess:
44 | return
45 | children = parent.children(recursive=True)
46 | for child in children:
47 | child.send_signal(sig)
48 |
49 |
50 | @pytest.fixture(autouse=True)
51 | def cleanup_processes():
52 | yield
53 | kill_child_processes(os.getpid())
54 |
55 |
56 | def db_url() -> str:
57 | return os.environ.get("TORTOISE_TEST_DB", "sqlite://:memory:")
58 |
59 |
60 | @pytest.fixture(autouse=True)
61 | async def initialize_tests(request: FixtureRequest):
62 | await initialize_db(db_url())
63 |
64 | def fin():
65 | async def afin():
66 | if "postgres" not in db_url():
67 | await Tortoise._drop_databases()
68 |
69 | event_loop = asyncio.get_event_loop()
70 | event_loop.run_until_complete(afin())
71 |
72 | request.addfinalizer(fin)
73 |
74 |
75 | @pytest.fixture()
76 | async def body_fixture(task: str, initialize_tests, go_to_tmpdir):
77 | await initialize_tests
78 | body_obj = Body(task=task)
79 | await body_obj.setup()
80 | body_obj.config.setup_logging()
81 | body_obj.config.hard_exit = False
82 | return body_obj
83 |
--------------------------------------------------------------------------------
/tests/end_to_end/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erik-megarad/beebot/8082f74490ed76b436d4014878ac622fe3bd60d4/tests/end_to_end/__init__.py
--------------------------------------------------------------------------------
/tests/end_to_end/test_background_python.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 |
6 | @pytest.fixture()
7 | def task() -> str:
8 | return (
9 | "Create a python file named 'sleepy.py' that sleeps for a number of seconds according to its arguments and "
10 | "writes that number into the file 'sleepy.txt' after it has finished sleeping. At the end of the program "
11 | "output a success message. Execute sleepy.py in the background with an argument of 10, check the status of "
12 | "the program until it's done, and then exit."
13 | )
14 |
15 |
16 | @pytest.mark.asyncio
17 | async def test_background_python(task, body_fixture):
18 | body = await body_fixture
19 |
20 | for i in range(0, 15):
21 | response = await body.cycle()
22 | if body.is_done:
23 | break
24 |
25 | if not response.plan:
26 | continue
27 |
28 | plan_text = (
29 | "Complete" if response.task_execution.complete else response.plan.plan_text
30 | )
31 | print(
32 | f"----\n{await body.current_task_execution.compile_history()}\n{plan_text}\n{response.decision.tool_name}"
33 | f"({response.decision.tool_args})\n{response.observation.response}\n---"
34 | )
35 |
36 | assert os.path.exists("workspace/sleepy.py")
37 | assert os.path.exists("workspace/sleepy.txt")
38 |
39 | with open("workspace/sleepy.py", "r") as f:
40 | file_contents = f.read()
41 | assert "sleep" in file_contents
42 | # FIXME: It doesn't use args sometimes, probably a prompt issue?
43 | # assert "argv" in file_contents
44 |
45 | with open("workspace/sleepy.txt", "r") as f:
46 | file_contents = f.read()
47 | assert "10" in file_contents
48 |
--------------------------------------------------------------------------------
/tests/end_to_end/test_capital_retrieval.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from beebot.config import Config
6 |
7 |
8 | @pytest.fixture()
9 | def task():
10 | return "Create a text file named capital.txt and write the name of the capital of America into it."
11 |
12 |
13 | @pytest.mark.asyncio
14 | async def test_capital_retrieval(task, body_fixture):
15 | body = await body_fixture
16 |
17 | assert "capital.txt" in body.task
18 | assert isinstance(body.config, Config)
19 | assert len(body.config.openai_api_key) > 1
20 | assert len(body.current_task_execution.packs) >= 3
21 |
22 | for i in range(0, 8):
23 | await body.cycle()
24 | if body.is_done:
25 | break
26 |
27 | with open(os.path.join("workspace", "capital.txt"), "r") as f:
28 | file_contents = f.read()
29 |
30 | assert "washington" in file_contents.lower()
31 |
--------------------------------------------------------------------------------
/tests/end_to_end/test_history.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 |
3 | import pytest
4 |
5 | from beebot.body import Body
6 |
7 |
8 | @pytest.fixture()
9 | def task() -> str:
10 | return "Follow the instructions in the instructions_1.txt file"
11 |
12 |
13 | @pytest.fixture()
14 | def ids() -> list[str]:
15 | # ID List length will increase/decrease cost, but increase/decrease the value of the test. 4 is fine.
16 | length = 4
17 | range_start = 1
18 | range_end = 100000
19 | return [randint(range_start, range_end) for _ in range(length)]
20 |
21 |
22 | @pytest.fixture()
23 | async def instructions_files_fixture(body_fixture, ids) -> tuple[Body, list[str]]:
24 | body = await body_fixture
25 | instructions = []
26 | for i, instruction in enumerate(ids):
27 | instructions.append(
28 | f"The id to remember is {ids[i]}. Read the file instructions_{i + 2}.txt."
29 | )
30 |
31 | instructions.append("Write the ids previously mentioned to a file named 'ids.txt'.")
32 | for i, instruction in enumerate(instructions):
33 | await body.file_manager.awrite_file(f"instructions_{i + 1}.txt", instruction)
34 |
35 | return body, instructions
36 |
37 |
38 | @pytest.mark.asyncio
39 | async def test_parse_history(body_fixture, task, instructions_files_fixture, ids):
40 | body, _instructions_files = await instructions_files_fixture
41 | for i in range(0, 15):
42 | await body.cycle()
43 | if body.is_done:
44 | break
45 |
46 | with open("workspace/ids.txt", "r") as f:
47 | file_contents = f.read()
48 | for expected_id in ids:
49 | assert str(expected_id) in file_contents
50 |
--------------------------------------------------------------------------------
/tests/end_to_end/test_revenue_lookup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from beebot.config import Config
6 |
7 |
8 | @pytest.fixture()
9 | def task():
10 | return "Write to a file called output.txt containing tesla's revenue in 2022."
11 |
12 |
13 | @pytest.mark.asyncio
14 | async def test_revenue_lookup(task, body_fixture):
15 | body = await body_fixture
16 |
17 | assert "tesla" in body.task.lower()
18 | assert "2022" in body.task
19 | assert isinstance(body.config, Config)
20 | assert len(body.config.openai_api_key) > 1
21 | assert len(body.current_task_execution.packs) >= 3
22 |
23 | for i in range(0, 8):
24 | await body.cycle()
25 | if body.is_done:
26 | break
27 |
28 | with open(os.path.join("workspace", "output.txt"), "r") as f:
29 | file_contents = f.read()
30 |
31 | assert "81" in file_contents
32 |
--------------------------------------------------------------------------------
/tests/end_to_end/test_system_basic_cycle.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from beebot.config import Config
6 |
7 |
8 | @pytest.fixture()
9 | def task():
10 | return "Get my OS name and version, and my current disk usage. write it to a file called my_computer.txt"
11 |
12 |
13 | @pytest.mark.asyncio
14 | async def test_system_basic_cycle(task, body_fixture):
15 | body = await body_fixture
16 |
17 | assert "my_computer.txt" in body.task
18 | assert isinstance(body.config, Config)
19 | assert len(body.config.openai_api_key) > 1
20 |
21 | for i in range(0, 8):
22 | await body.cycle()
23 | if body.is_done:
24 | break
25 |
26 | with open(os.path.join("workspace", "my_computer.txt"), "r") as f:
27 | file_contents = f.read()
28 | assert "Operating System" in file_contents or "OS" in file_contents
29 | assert "GB" in file_contents
30 |
--------------------------------------------------------------------------------
/tests/end_to_end/test_webserver.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 |
6 | @pytest.fixture()
7 | def task() -> str:
8 | return (
9 | "Create a flask webserver that listens on localhost:9696 and responds to requests to /health with a 200 OK. "
10 | "Run the webserver in a daemon process. Then Make an HTTP request to the health endpoint and write the "
11 | "response to health.txt. Once you have written the file, you should kill the webserver process and exit."
12 | )
13 |
14 |
15 | @pytest.mark.asyncio
16 | async def test_background_python(body_fixture, task):
17 | body = await body_fixture
18 |
19 | for i in range(0, 15):
20 | response = await body.cycle()
21 | if body.is_done:
22 | break
23 |
24 | if not response.plan:
25 | continue
26 |
27 | print(
28 | f"----\n{await body.current_task_execution.compile_history()}\n{response.plan.plan_text}\n"
29 | f"{response.decision.tool_name}({response.decision.tool_args})\n{response.observation.response}\n---"
30 | )
31 |
32 | assert os.path.exists("workspace/health.txt")
33 |
34 | with open("workspace/health.txt", "r") as f:
35 | file_contents = f.read()
36 | assert "200" in file_contents
37 |
--------------------------------------------------------------------------------
/yoyo.ini:
--------------------------------------------------------------------------------
1 | [DEFAULT]
2 | sources = migrations
3 | database = %(DATABASE_URL)s
4 | batch_mode = off
5 | verbosity = 0
6 |
7 |
--------------------------------------------------------------------------------