Compare commits

...

202 Commits

Author SHA1 Message Date
Merwane Hamadi
1fead303a0 Retry 503 OpenAI errors
Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com>
2023-06-19 13:12:33 -07:00
Luke K (pr-0f3t)
abb397e442 Release v0.4.1 (#4686)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Nicholas Tindle <nicktindle@outlook.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by: Merwane Hamadi <merwanehamadi@gmail.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
Co-authored-by: Luke K <2609441+lc0rp@users.noreply.github.com>
Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com>
Co-authored-by: Erik Peterson <e@eriklp.com>
Co-authored-by: Auto-GPT-Bot <github-bot@agpt.co>
Co-authored-by: Benny van der Lans <49377421+bfalans@users.noreply.github.com>
Co-authored-by: Jan <jan-github@phobia.de>
Co-authored-by: Robin Richtsfeld <robin.richtsfeld@gmail.com>
Co-authored-by: Marc Bornträger <marc.borntraeger@gmail.com>
Co-authored-by: Stefan Ayala <stefanayala3266@gmail.com>
Co-authored-by: javableu <45064273+javableu@users.noreply.github.com>
Co-authored-by: DGdev91 <DGdev91@users.noreply.github.com>
Co-authored-by: Kinance <kinance@gmail.com>
Co-authored-by: digger yu <digger-yu@outlook.com>
Co-authored-by: David <scenaristeur@gmail.com>
Co-authored-by: gravelBridge <john.tian31@gmail.com>
Fix Python CI "update cassettes" step (#4591)
fix CI (#4596)
Fix inverted logic for deny_command (#4563)
fix current_score.json generation (#4601)
Fix duckduckgo rate limiting (#4592)
Fix debug code challenge (#4632)
Fix issues with information retrieval challenge a (#4622)
fix issues with env configuration and .env.template (#4630)
Fix prompt issue causing 'No Command' issues and challenge to fail (#4623)
Fix benchmark logs (#4653)
Fix typo in docs/setup.md (#4613)
Fix run.sh shebang (#4561)
Fix autogpt docker image not working because missing prompt_settings (#4680)
Fix execute_command coming from plugins (#4730)
2023-06-19 12:41:40 -04:00
Reinier van der Leer
25a7957bb8 Release v0.4.0 (#4539) 2023-06-05 16:05:49 +02:00
Reinier van der Leer
3dbc377308 Update version numbers for v0.4.0 release 2023-06-04 20:05:57 +02:00
Reinier van der Leer
120fe762df Update bulletin with highlights for v0.4.0 release (#4576) 2023-06-04 19:31:58 +02:00
merwanehamadi
af28510aba Fix test_web_selenium (#4554) 2023-06-04 16:38:32 +02:00
Reinier van der Leer
84e58051fa Clean up CI git logic 2023-06-04 16:01:40 +02:00
Merwane Hamadi
02846fcf91 remove information retrieval challenge b from beaten challenges 2023-06-03 21:24:36 -07:00
Merwane Hamadi
0a20fa4fdf Fix CI git authentication and cassettes 2023-06-03 21:19:58 -07:00
Reinier van der Leer
c4e2d8fbdd debug 2023-06-04 03:28:26 +02:00
Reinier van der Leer
3e5868f223 Fix CI git diff 2023-06-04 02:40:51 +02:00
Reinier van der Leer
4c11b21dff Fix CI git authorization 2023-06-04 02:01:35 +02:00
Reinier van der Leer
ce44012866 Fix pushing cassettes in CI 2023-06-02 23:27:15 +02:00
merwanehamadi
4cc6e27f02 Remove news about config (#4553) 2023-06-02 23:09:16 +02:00
Reinier van der Leer
9156db41a1 Merge remote-tracking branch 'origin/stable' into release-0.4.0 2023-06-02 22:58:50 +02:00
Reinier van der Leer
9247f9480c Fix CI for internal PRs with CI changes (#4552)
* Port fixed CI workflow from master

* Trigger CI

* Improve CI concurrency check
2023-06-02 13:49:05 -07:00
Richard Beales
fc0688673e Update BULLETIN.md 2023-06-02 12:08:33 +01:00
Reinier van der Leer
15c157343d Remove Redis memory configuration from docker-compose configs 2023-06-01 23:23:57 +02:00
Reinier van der Leer
3fffe65a3b Update bulletin for v0.4.0 2023-06-01 17:01:30 +02:00
kaneda2004
63b79a88c6 Adding support for openai_organization env variable (#289) 2023-05-30 19:02:16 -07:00
merwanehamadi
9e9b128b72 ignore cassettes and current score for all push events in github action (#4485) 2023-05-30 19:16:49 -05:00
Auto-GPT-Bot
64973bfe12 Update submodule reference 2023-05-30 23:33:40 +00:00
Auto-GPT-Bot
41df0204f3 Update current score 2023-05-30 23:33:38 +00:00
Douglas Schonholtz
f6ee61d607 create debug challenge (#4286)
Co-authored-by: Merwane Hamadi <merwanehamadi@gmail.com>
Co-authored-by: symphony <john.tian31@gmail.com>
2023-05-30 16:28:32 -07:00
merwanehamadi
87776b2886 Make the information retrieval challenge a harder while still passing (#4468) 2023-05-30 15:56:58 -07:00
merwanehamadi
86b6231f70 ignore push if current score changed (#4482) 2023-05-30 12:23:49 -07:00
Auto-GPT-Bot
387f65c16c Update submodule reference 2023-05-30 19:15:33 +00:00
Auto-GPT-Bot
4c25fabec9 Update current score 2023-05-30 19:15:30 +00:00
merwanehamadi
6806b66509 Information retrieval challenge (#4456)
* test: add information retrieval challenge b

* test: get information retrieval challenge be working.

* chore: clean up comments and imports.

* chore: fix incorrect import

* chore: clean up imports.

* fix: add web_selenium cmd. resolve missing loop cycle

* chore: remove commented code and unused imports.

* fix (4261): use 2 cycles instead of 3

* chore: fix mypy formatting

* chore: try 2 for mypy formatting

* chore: resolve flake8 issues

* chore: add docs

* chore: resolve linting flake8

* chore: correct formatting to black

* Update challenge_b.md

* refactored challenge

---------

Co-authored-by: PortlandKyGuy <kyleaaron1@gmail.com>
2023-05-30 12:10:49 -07:00
Erik Peterson
b56352e218 Fix #4461: Don't record error requests in challenges (#4469)
Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
2023-05-30 07:31:45 -07:00
Auto-GPT-Bot
d3a1770dc0 Update submodule reference 2023-05-30 13:03:41 +00:00
merwanehamadi
22429c6e98 add vcr to dalle (#4474) 2023-05-30 06:00:37 -07:00
Auto-GPT-Bot
56650b60f4 Update current score 2023-05-30 02:33:34 +00:00
merwanehamadi
ba8046753e Update current score when PR merged (#4464) 2023-05-29 19:30:41 -07:00
merwanehamadi
d34b8a2b61 Replace 'prompt change' with 'behavior change' (#4473)
* Replace 'prompt change' with 'behavior change'

* replaced behavior by behaviour

* fixed typo on behaviour
2023-05-30 03:01:46 +01:00
Luke K (pr-0f3t)
1446ffddb0 Fix #942: Respect --gpt4only, --gpt3only cli args (#3144)
Co-authored-by: Luke K <KayLuke@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-29 20:10:51 +03:00
Kinance
1ddf2324ff Improve the steps for running docker compose in the setup documentation (#4462)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-29 19:24:38 +03:00
Auto-GPT-Bot
b5d78a48a7 Update submodule reference 2023-05-29 15:29:31 +00:00
merwanehamadi
1127d9c5f5 Master doesn't beat memory challenge b level 1 in a consistent manner (#4460) 2023-05-29 18:25:45 +03:00
merwanehamadi
31cd836530 "Beat Challenges" Mode (#4447)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-29 08:47:06 +01:00
Toran Bruce Richards
daafda320b Update README.md
Remove outdated reference to En_GPT
2023-05-29 15:28:57 +12:00
merwanehamadi
ee9f10a8d8 remove unused imports automatically (#4449)
* remove unused imports automatically

* add linters to pr template

* remove useless try statement
2023-05-28 05:50:50 -07:00
Auto-GPT-Bot
78774526f4 Update submodule reference 2023-05-28 01:30:02 +00:00
merwanehamadi
44f6d946f5 fix information retrieval challenge (#4448) 2023-05-27 18:26:30 -07:00
Ryan Johns
03036c1bd6 Added three more tests to check for edge cases in URL validation (#4441)
Co-authored-by: Ryan Johns <rkjohns@verisk.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-27 18:58:38 +03:00
Kinance
4b7fa7f49d Fix typo and links in documentation (#4440) 2023-05-27 10:52:38 +01:00
Toran Bruce Richards
e05c000615 Update README.md
Update Sponsors
2023-05-27 09:05:58 +12:00
k-boikov
064e95b46b Allow spinning to be disabled (#4329) 2023-05-27 00:01:34 +03:00
merwanehamadi
e7c0d3330c Create cassette submodule (#4420)
* gfeat: specify directory of cassettes and automatically load them depending on module

fix: formatting for linter

test: commit newly generated cassettes to their respective folder

tests: update latest fixtures with master

fix: update .gitattributes with updated path to cassettes

fix: use cassettes from master instead of generating them myself

fix: update path in .gitattributes

fix: make sure to match default functionality by using test name for cassette directory

fix: actually add git submodule

ci: checkout git submodules in CI

ci: update git submodules separately to ensure it gets called

feat: add a hooks directory so we can update git submodules on post-checkout

feat: make sure we push the tests/cassettes submodule on merge into master

ci: remove unused code now that we are using git submodules to keep cassettes in sync

fix: simplify how we load the submodule and fix updating cassettes on merge to master

chore: remove echo of checkout hook, it's unneeded

ci: remove unneccesary step

* cassettes submodule

* cassettes submodule

* cassettes submodule

* cassettes submodule

* cassettes submodule

---------

Co-authored-by: Stefan Ayala <stefanayala3266@gmail.com>
2023-05-26 12:33:49 -07:00
merwanehamadi
6c45fcd067 remove cassettes auto commit (#4432) 2023-05-26 11:02:40 -07:00
Nicholas Tindle
acfd966aa4 Pass Configs to Commands and remove CFG = Config() in the commands/ folder (#4328)
* feat: pass config to call_ai_functions in coimmands

* feat: config for read_audio_from_file

* feat: file operations cfg

NOTE: we replaced the CFG in the command enable with TRUE b/c not sure how to handle this yet

* feat: git command conversion

* feat: google search

* feat: image generation

* feat: extract cfg from browser commands

* feat: remove cfg from execute code commands

* fix: file operation related tests

* fix: linting

* fix: tests for read_audio

* fix: test error

* feat: update cassettes

* fix: linting

* fix: test typechecking

* fix: google_search errors if unexpected kw arg is passed

* fix: pass config param to google search test

* fix: agent commands were broken + cassettes

* fix: agent test

* feat: cassettes

* feat: enable/disable logic for commands

* fix: some commands threw errors

* feat: fix tests

* Add new cassettes

* Add new cassettes

* ci: trigger ci

* Update autogpt/commands/execute_code.py

Co-authored-by: Reinier van der Leer <github@pwuts.nl>

* fix prompt

* fix prompt + rebase

* add config remove useless imports

* put back CFG just for download file

* lint

* The signature should be mandatory in the decorator

* black isort

* fix: remove the CFG

* fix: non typed arg

* lint: type some args

* lint: add types for libraries

* Add new cassettes

* fix: windows compatibility

* fix: add config access to decorator

* fix: remove twitter mention

* DDGS search works at 3.0.2 version

* ci: linting

---------

Co-authored-by: Auto-GPT-Bot <github-bot@agpt.co>
Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: kinance <kinance@gmail.com>
2023-05-26 08:39:25 -07:00
k-boikov
f07fcdf0a7 Pull auto-gpt-plugin-template from GitHub instead of PyPI (#4402)
* switch from pypi to github for auto-gpt-plugin-template

* adapt tests to new plugin interface
2023-05-26 14:48:36 +02:00
k-boikov
d3fc8c4286 add 30 min timeout for tests (#4416) 2023-05-25 16:15:27 -07:00
k-boikov
ddf59273e6 Remove obsolete full_message_history kwarg from tests (#4411) 2023-05-25 22:00:52 +03:00
Reinier van der Leer
bfbe613960 Vector memory revamp (part 1: refactoring) (#4208)
Additional changes:

* Improve typing

* Modularize message history memory & fix/refactor lots of things

* Fix summarization

* Move memory relevance calculation to MemoryItem & improve test

* Fix import warnings in web_selenium.py

* Remove `memory_add` ghost command

* Implement overlap in `split_text`

* Move memory tests into subdirectory

* Remove deprecated `get_ada_embedding()` and helpers

* Fix used token calculation in `chat_with_ai`

* Replace Message TypedDict by dataclass

* Fix AgentManager singleton issues in tests

---------

Co-authored-by: Auto-GPT-Bot <github-bot@agpt.co>
2023-05-25 20:31:11 +02:00
Kinance
10489e0df2 Fix duckduckgo-search to 3.0.2 stable version (#4405) 2023-05-25 19:10:48 +03:00
Pi
6e14782f82 Re-point wiki link in PULL_REQUEST_TEMPLATE.md 2023-05-25 16:00:29 +01:00
Pi
2a6f01aae2 Re-point wiki link in 2.feature.yml 2023-05-25 15:59:50 +01:00
Pi
1ab3a40f03 Repoint to wiki page 1.bug.yml 2023-05-25 15:59:04 +01:00
Pi
e3e9a8121c Re-point link to wiki in CONTRIBUTING.md
Also update text
2023-05-25 15:57:06 +01:00
Nicholas Tindle
43565b280f Remove Twitter Command (#4381) 2023-05-24 22:40:30 -05:00
Kinance
5168cb5219 Migrate google search to use DDGS.text function (#4383) 2023-05-24 19:36:23 +03:00
merwanehamadi
438d3e489b empty commit to see cassette status (#4382)
* empty commit

* Add new cassettes

* empty commit

---------

Co-authored-by: Auto-GPT-Bot <github-bot@agpt.co>
2023-05-24 09:13:22 -07:00
Kinance
d4d50fda7c Fix the version of duckduckgo-search to avoid empty result issue (#4368) 2023-05-23 18:00:25 +03:00
Sicong Zhao
a8af3313c1 Update get_hyperlinks description (#4122)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Auto-GPT-Bot <github-bot@agpt.co>
2023-05-23 16:47:58 +03:00
merwanehamadi
9c7f5671e6 CI: set base repo before gh pr checkout (#4363) 2023-05-23 14:23:03 +02:00
merwanehamadi
3a2800bcdd Cassettes should be added in the same pull request, without extra pull request (#4355)
* fix ci

* fix ci

* add only what we need

* Updated ci.yml to inform about challenge score

* Updated ci.yml to inform about challenge score

---------

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-22 20:03:23 -07:00
Nicholas Tindle
cc709bbbaa Improve Azure setup wording in docs (#4325)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-23 00:51:52 +03:00
Robin Richtsfeld
77ee9f8119 Pass command line args as list (#1486)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-22 22:53:31 +03:00
Reinier van der Leer
3e24d312d3 Create data folder in Docker release build (#4347) 2023-05-22 13:00:31 +02:00
k-boikov
360d5cd577 Prevent docker compose to break config by creating folders (#4125) 2023-05-22 13:36:20 +03:00
Benny van der Lans
dcb1cbe5d6 Update 1.bug.yml to improve consistency of data (#4293)
* Update 1.bug.yml to improve consistency of data

in this version of the bug report several dropdown fileds have been added and text fields have been removed to have better control over the data being sent in bug reports

* Update 1.bug.yml fixing indent error

fixing of an indent error

* Update 1.bug.yml

changed the file uploads to input fields for the log contents since files are not supported on github

* Update 1.bug.yml

made the issue field required

* Update 1.bug.yml

Added changes according to luke's comments

---------

Co-authored-by: Pi <sunfish7@gmail.com>
2023-05-22 00:15:10 +01:00
sherif-med
31525dfef7 Text file loaders (#3031)
* adding requiered packages for loading pdf, docx, md, tex files (preferably pure python packages)

* adding text file utils providing function to load file based on extension && adding read_text_file command

* adding test cases for text file loading (pdf file creation is hardcoded due to external package requierment for creation (a sample file can be added))

* formatting

* changing command name from 'read_text_file' to 'parse_text_document'

* fallback to txtParser if file extension is not known to read script and code files

* adding extension respective parsers

* adding binary file check function

* adding file existance check && raising valueError for unsupported binary file formats

* adding check file type (binary) in test_parsers for specific extensions && fixing mock pdf generation to include null bytes

* adding .yml extension parser

* removal of .doc parser

* updating file loading commands names

* updating test (removing .doc mock function)

* fix: import sort

* new cassette for mem A

* feat: update Cassettes

* feat: consolidate commands

* feat: linting

* feat: updates to cassettes

---------

Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-21 14:48:40 -05:00
Nameless Dude
e1c6778c3a Update README.md (#4333) 2023-05-21 21:26:45 +03:00
k-boikov
bcc32cc441 Fix split_file when overlap = 0, add test (#3599)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-20 20:20:03 -05:00
Konrad
57ea7b5216 Fixed #4229 (#4278)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-21 02:45:27 +03:00
Nicholas Tindle
c30f5b7d5e fix: completion was being called with wrong data types (#4324) 2023-05-20 18:04:57 -05:00
Luke K (pr-0f3t)
023a50d26b Adding devcontainer extensions (#4181)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-20 15:51:01 -05:00
GoCodeo
7e21f38185 Added unittest cases for commands/audio_text.py (#4307)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-19 20:18:32 +01:00
Reinier van der Leer
c3f7916dfd Clean up BULLETIN.md (#4305)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Pi <sunfish7@gmail.com>
2023-05-19 19:51:36 +01:00
Omri Grossman
a08fc850f0 Used a regex expression for simple URL validation and added tests (#3763)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-19 19:47:04 +01:00
WladBlank
bf33f4a7b0 add command shell blacklist and whitelist (#3950)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-19 18:50:43 +01:00
John Cole
a6d4deaf20 Update setup.md (#3690)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-19 18:39:12 +01:00
Trajan Wiley-Jimenez
ccc03c1a8d Enabling/disabling none/all of plugins from .env file (#4036)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-19 20:33:14 +03:00
Luke K (pr-0f3t)
ee98641210 Imagegen delay retry huggingface (#4194)
Co-authored-by: Kory Becker <kbecker@primaryobjects.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Nicholas Tindle <nicktindle@outlook.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-19 20:19:39 +03:00
Reinier van der Leer
812be60d2a Delete sponsors_readme.yml workflow (#4304) 2023-05-19 18:07:48 +03:00
GoCodeo
f13aba0958 Added unittest cases for commands/analyze_code.py file (#4212)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-19 12:33:02 +03:00
Luke K (pr-0f3t)
5b4bcf1a04 Prevent test collection errors in devcontainer (#4180)
Co-authored-by: desojo <ethan.johnson1997@outlook.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-19 00:22:16 +03:00
Alex Kissi Jr
e2851dc73f Incremented Version BULLETIN.md (#4236) 2023-05-18 01:31:43 -05:00
Media
c4b32e067c switching from unittest to pytest in test_json_parser (#3481) 2023-05-18 00:58:08 -05:00
Toran Bruce Richards
dc41db9522 Update README.md 2023-05-18 15:45:58 +12:00
Toran Bruce Richards
0b39b92bde Update README.md 2023-05-18 15:41:17 +12:00
Zorinik
19767ca4a4 Logs output to console if we're not on speak mode (#3715)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-17 22:17:33 +03:00
DGdev91
42a5a0ce13 Make prompt parameters configurable (#3375)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-17 20:12:10 +03:00
merwanehamadi
1c399e67f4 Add $ to timestamp in ci.yml and improve diff (#4266) 2023-05-17 17:52:52 +01:00
Andres Caicedo
7508e9941f Implement Logging of Self-Feedback in logs/Debug Folder (#3868)
* Adds SELF_FEEDBACK_FILE_NAME

* Add self-feedback logging to logs/Debug folder

* Reformatting

* Uses JSON file

* Update agent.py

Changes position

* Update agent.py

* Adds PROMPT_FEEDBACK_FILE_NAME

* Update agent.py

* Update agent.py

* Reformatting

* Update agent.py

* Update agent.py

* Changes file names

* Update agent.py

* Reformatting

* Update agent.py

* Changes conts names

* Update agent_manager.py

* Update agent_manager.py

* HARD reset

* Update test_get_self_feedback.py

* Update test_get_self_feedback.py

---------

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
2023-05-17 04:38:42 -07:00
Richard Beales
feae20d8fa Update PULL_REQUEST_TEMPLATE.md to include linting (#3625)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-16 23:24:57 -05:00
Boostrix
7c71b43d42 mention docker rebuild is necessary if changing requirements.txt (#4136)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-16 22:30:38 -05:00
k-boikov
2cd835e5a4 Added custom_search_engine_id as mandatory for official google search (#4228)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-16 22:20:02 -05:00
Boostrix
1c96a5acf7 encourage people to use a separate user account w/o VM/docker (#3961)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Nicholas Tindle <nicktindle@outlook.com>
2023-05-16 22:13:14 -05:00
merwanehamadi
55af3e19b4 CI Pipeline: create cassettes in fork (#4257)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-16 21:49:12 -05:00
Richard Beales
52874cc442 Update README.md - make stable branch less shouty (#3702)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-16 21:38:36 -05:00
amokduke
627aeb20a2 Adds check for Python 3.10 and print error message if required version not detected. (#3598)
* Updated run script to check for Python 3.10 and above, and provide informative error messages for unsupported versions.

* Updated run script to check for Python 3.10 and above, and provide informative error messages for unsupported versions.

---------

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
2023-05-16 08:14:24 -07:00
Kory Becker
f424fac1d8 Huggingface retry generate_image with delay (#2745)
Co-authored-by: Media <12145726+rihp@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Nicholas Tindle <nicktindle@outlook.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by; lc0rp
2023-05-16 10:02:55 -05:00
Stefan Ayala
c1cd54d1ea Allow absolute paths if contained in workspace (#3932)
Maybe I'm missing something but I don't see the harm of accessing an absolute path if it's contained in the workspace

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-16 17:10:33 +03:00
Cenny
0839a16d27 Clarify .env.template image-provider options (#3720)
* Clarify .env.template image-provider options

* feat: consolidate duplicate options

* Simplify

---------

Co-authored-by: Nicholas Tindle <nicktindle@outlook.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-16 12:07:32 +03:00
Nicholas Tindle
cb2b13dff6 Legal warning on continuous run (#4239)
* feat: legal warning on continuous

* fix: formatting

* ci: fix formatting
2023-05-16 11:46:20 +03:00
Malahieude Timothé
85fe6f36c2 Fix ai_name not passed to Agent (#3948)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-15 23:20:14 -05:00
merwanehamadi
517c080544 fix ci cassettes (#4234)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-15 22:43:21 -05:00
gravelBridge
c04f2210cc Added feature to interrupt y -N continuous commands. (#4230)
* Added feature to interrupt y -N continuous commands.

* Fixed formatting.

---------

Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-15 20:02:33 -07:00
merwanehamadi
6c4426d8e6 Refactor challenges to use cycle count instead of time (#4222)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-15 21:44:36 -05:00
Boostrix
17c45ee53a Show workspace during startup / fix for #2793 (#4082)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-16 02:34:48 +03:00
Boostrix
3c80e05e00 Improve error message by providing the name of the env file (#3964)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-16 02:26:13 +03:00
k-boikov
cf35112291 Fixed error in safe_google_results, added tests (#3606) 2023-05-15 20:54:40 +01:00
k-boikov
d06d8a609f Fix commands with same name overwriting (#4226) 2023-05-15 19:41:11 +01:00
Richard Beales
16b7e7a91e Update prompt.py to clarify how to call commands (#4027)
* Update prompt.py to clarify how to call commands

* new cassettes

* try again with a new cassette

* one last try with the cassette

* black dot

---------

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-15 15:55:33 +03:00
Reinier van der Leer
1a2f7684bc Patch CI proxy 2023-05-15 14:35:35 +02:00
Pi
19e642259c Sync stable into master after v0.3.1 release (#4203) 2023-05-15 13:06:31 +01:00
Pi
3d494f1032 Merge branch 'master' into stable 2023-05-15 12:58:10 +01:00
Richard Beales
dda8d0f6bf Release 0.3.1 (#4201)
* Feature/tighten up ci pipeline (#3700)

* Fix docker volume mounts (#3710)

Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* Feature/enable intuitive logs for community challenge step 1 (#3695)

* Feature/enable intuitive logs summarization (#3697)

* Move task_complete command out of prompt (#3663)

* feat: move task_complete command out of prompt

* fix: formatting fixes

* Add the shutdown command to the test agents

* tests: update test vcrs

---------

Co-authored-by: James Collins <collijk@uw.edu>

* Allow users to Disable Commands via the .env (#3667)

* Document Disabling command categories (#3669)

* feat: move task_complete command out of prompt

* fix: formatting fixes

* feat: add command disabling

* docs: document how to disable command categories

* Enable denylist handling for plugins (#3688)

Co-authored-by: Luke Kyohere <lkyohere@mfsafrica.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* Fix call to `plugin.post_planning` (#3414)

Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* create information retrieval challenge a (#3770)

Co-authored-by: Richard Beales <rich@richbeales.net>

* fix typos (#3798)

* Update run.bat (#3783)

Co-authored-by: Richard Beales <rich@richbeales.net>

* Update run.sh (#3752)

Co-authored-by: Richard Beales <rich@richbeales.net>

* ADD: Bash block in the contributing markdown (#3701)

Co-authored-by: Richard Beales <rich@richbeales.net>

* BUGFIX: Selenium Driver object reference was included in the browsing results for some reason (#3642)

* * there is really no need to return the  reference to the Selenium driver along with the text summary and list of links.

* * removing unused second return value from browse_website()

* * updated cassette

* * updated YAML cassette for test_browse_website

* * after requirements reinstall, another update YAML cassette for test_browse_website

* * another update YAML cassette for test_browse_website, only as a placholder commit to trigger re-testing due to some docker TCP timeout issue

* * another update YAML cassette for test_browse_website

---------

Co-authored-by: batyu <batyu@localhost>

* Update CONTRIBUTING.md

* Self feedback Improvement (#3680)

* Improved `Self-Feedback`

* minor tweak

* Test: Updated `test_get_self_feedback.py`

* community challenges in the wiki (#3764)

* Update README.md

* Update PULL_REQUEST_TEMPLATE.md

Added link to wiki Contributing page

* Add link to wiki Contributing page

* fix

* Add link to wiki page  on Contributing

* Implement Logging of User Input in logs/Debug Folder (#3867)

* Adds USER_INPUT_FILE_NAME

* Update agent.py

* Update agent.py

Log only if console_input is not the authorise_key

* Reformatting

* add information retrieval challenge to the wiki (#3876)

* add code owners policy (#3981)

* add code owners

* added @ to codeowners

* switched to team ownership

* Memory Challenge C (#3908)

* Memory Challenge C

* Working cassettes

* Doc fixes

* Linting and doc fix

* Updated cassette

* One more cassette try

---------

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>

* memory challenge c inconsistent (#3985)

* Improve & fix memory challenge docs. (#3989)

Co-authored-by: Kaan Osmanagaoglu <kaano@questps.com.au>

* Feature/centralize prompt (#3990)

Co-authored-by: xiao.hu <454292663@qq.com>

* Use correct reference to prompt_generator in autogpt/llm/chat.py (#4011)

* fix typos (#3998)

Co-authored-by: Minfeng Lu <minfenglu@Minfengs-MacBook-Pro.local>
Co-authored-by: Richard Beales <rich@richbeales.net>

* fix typo in the getting started docs (#3997)

Co-authored-by: Richard Beales <rich@richbeales.net>

* Fix path to workspace directory in setup guide (#3927)

Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* document that docker-compose 1.29.0 is minimally required (#3963)

Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* Integrate pytest-xdist Plugin for Parallel and Concurrent Testing (#3870)

* Adds pytest-parallel dependencies

* Implement pytest-parallel for faster tests

* Uses pytest-xdist

* Auto number of workers processes

* Update ci.yml

---------

Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* explain temperature setting in env file (#4140)

Co-authored-by: Richard Beales <rich@richbeales.net>

* Catch JSON error in summary_memory.py (#3996)

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Update duckduckgo dependency - min should be 2.9.5 (#4142)

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Update Dockerfile - add missing scripts and plugins directories. (#3706)

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Updated memory setup links (#3829)

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Parse package versions so upgrades can be forced (#4149)

* parse package versions so upgrades can be forced

* better version from @collijk

* fix typo in autopgt/agent/agent.py (#3747)

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Fix `milvus_memory_test.py` mock `Config` (#3424)

Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Implemented showing the number of preauthorised commands left. #1035 (#3322)

Co-authored-by: mayubi <marwand@ayubi-it.de>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Challenge: Kubernetes and documentation (#4121)

* challenge_kubes_and_readme

* docs

* testing

* black and isort

* revision

* lint

* comments

* blackisort

* docs

* docs

* deleting_cassette

* suggestions

* misspelling_errors

---------

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>

* Make sdwebui tests pass (when SD is running) (#3721)

Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* Add Edge browser support using EdgeChromiumDriverManager (#3058)

Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>

* Added --install-plugin-deps to Docker (#4151)

Co-authored-by: Nicholas Tindle <nick@ntindle.com>

* Feature/basic proxy (#4164)

* basic proxy (#54)

* basic proxy (#55)

* basic proxy

* basic proxy

* basic proxy

* basic proxy

* add back double quotes

* add more specific files

* write file

* basic proxy

* Put back double quotes

* test new CI (#4168)

* test new CI

* test new CI

* remove double quotes

* Feature/test new ci pipeline 2 (#4169)

* test new CI

* remove double quotes

* make it a variable

* make it a variable

* Test New CI Pipeline (#4170)

* introduce dummy prompt change

* introduce dummy prompt change

* empty commit

* empty commit

* empty commit

* push to origin repo

* add s to quote

* Feature/fix rate limiting issue Step 1 (#4173)


* temporarilly remove 3.11

* add back 3.11 (#4185)

* Revert "Put back 3.11 until it's removed as a requirement" (#4191)

---------

Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by: Peter Petermann <ppetermann80@googlemail.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: James Collins <collijk@uw.edu>
Co-authored-by: Luke K <2609441+pr-0f3t@users.noreply.github.com>
Co-authored-by: Luke Kyohere <lkyohere@mfsafrica.com>
Co-authored-by: Robin Richtsfeld <robin.richtsfeld@gmail.com>
Co-authored-by: RainRat <rainrat78@yahoo.ca>
Co-authored-by: itsmarble <130370814+itsmarble@users.noreply.github.com>
Co-authored-by: Ambuj Pawar <pawar.ambuj@gmail.com>
Co-authored-by: bszollosinagy <4211175+bszollosinagy@users.noreply.github.com>
Co-authored-by: batyu <batyu@localhost>
Co-authored-by: Pi <sunfish7@gmail.com>
Co-authored-by: AbTrax <45964236+AbTrax@users.noreply.github.com>
Co-authored-by: Andres Caicedo <73312784+AndresCdo@users.noreply.github.com>
Co-authored-by: Douglas Schonholtz <15002691+dschonholtz@users.noreply.github.com>
Co-authored-by: Kaan <kaanixir@gmail.com>
Co-authored-by: Kaan Osmanagaoglu <kaano@questps.com.au>
Co-authored-by: xiao.hu <454292663@qq.com>
Co-authored-by: Tomasz Kasperczyk <tomaszikasperczyk@gmail.com>
Co-authored-by: minfeng-ai <42948406+minfenglu@users.noreply.github.com>
Co-authored-by: Minfeng Lu <minfenglu@Minfengs-MacBook-Pro.local>
Co-authored-by: Shlomi <81581678+jit-shlomi@users.noreply.github.com>
Co-authored-by: Itai Steinherz <itaisteinherz@gmail.com>
Co-authored-by: Boostrix <119627414+Boostrix@users.noreply.github.com>
Co-authored-by: Kristian Jackson <kristian.jackson@gmail.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
Co-authored-by: Eduardo Salinas <edus@microsoft.com>
Co-authored-by: prom3theu5 <dave@simcube.co.uk>
Co-authored-by: dominic-ks <contact@bedevious.co.uk>
Co-authored-by: andrey13771 <51243350+andrey13771@users.noreply.github.com>
Co-authored-by: Marwand Ayubi <98717667+xhypeDE@users.noreply.github.com>
Co-authored-by: mayubi <marwand@ayubi-it.de>
Co-authored-by: Media <12145726+rihp@users.noreply.github.com>
Co-authored-by: Cenny <cwenner@gmail.com>
Co-authored-by: Abdelkarim Habouch <37211852+karimhabush@users.noreply.github.com>
2023-05-14 21:36:55 +01:00
k-boikov
f778483ac3 Revert "Put back 3.11 until it's removed as a requirement" (#4191) 2023-05-14 16:30:10 +03:00
merwanehamadi
15ebe23bc2 add back 3.11 (#4185) 2023-05-14 14:20:14 +01:00
merwanehamadi
bc6f8a27ff Feature/fix rate limiting issue Step 1 (#4173)
* temporarilly remove 3.11
2023-05-13 22:31:24 -04:00
merwanehamadi
2d9b9294d0 Test New CI Pipeline (#4170)
* introduce dummy prompt change

* introduce dummy prompt change

* empty commit

* empty commit

* empty commit

* push to origin repo

* add s to quote
2023-05-14 13:18:31 +12:00
merwanehamadi
de6b8ee9f2 Feature/test new ci pipeline 2 (#4169)
* test new CI

* remove double quotes

* make it a variable

* make it a variable
2023-05-14 11:40:31 +12:00
merwanehamadi
a110ff94a5 test new CI (#4168)
* test new CI

* test new CI

* remove double quotes
2023-05-14 11:25:56 +12:00
merwanehamadi
4143d212a5 Feature/basic proxy (#4164)
* basic proxy (#54)

* basic proxy (#55)

* basic proxy

* basic proxy

* basic proxy

* basic proxy

* add back double quotes

* add more specific files

* write file

* basic proxy

* Put back double quotes
2023-05-14 11:07:37 +12:00
k-boikov
e6f8e51504 Added --install-plugin-deps to Docker (#4151)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-14 01:59:36 +03:00
Abdelkarim Habouch
b958386689 Add Edge browser support using EdgeChromiumDriverManager (#3058)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-14 00:28:30 +03:00
Cenny
2f7beebc61 Make sdwebui tests pass (when SD is running) (#3721)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-13 15:06:50 -05:00
Media
900de5fe63 Challenge: Kubernetes and documentation (#4121)
* challenge_kubes_and_readme

* docs

* testing

* black and isort

* revision

* lint

* comments

* blackisort

* docs

* docs

* deleting_cassette

* suggestions

* misspelling_errors

---------

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
2023-05-13 12:21:21 -07:00
Marwand Ayubi
233f900fa6 Implemented showing the number of preauthorised commands left. #1035 (#3322)
Co-authored-by: mayubi <marwand@ayubi-it.de>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-13 18:21:16 +03:00
Robin Richtsfeld
7a34d49264 Fix milvus_memory_test.py mock Config (#3424)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-13 17:19:18 +03:00
andrey13771
6c78d80d37 fix typo in autopgt/agent/agent.py (#3747)
Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-13 04:00:08 +03:00
k-boikov
12e806d754 Parse package versions so upgrades can be forced (#4149)
* parse package versions so upgrades can be forced

* better version from @collijk
2023-05-12 16:23:54 -07:00
dominic-ks
21a202b655 Updated memory setup links (#3829)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-13 02:01:52 +03:00
prom3theu5
79fba4ab7b Update Dockerfile - add missing scripts and plugins directories. (#3706)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-13 01:47:02 +03:00
Eduardo Salinas
c771e1fd50 Update duckduckgo dependency - min should be 2.9.5 (#4142)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-13 00:57:30 +03:00
Kristian Jackson
a60512cdae Catch JSON error in summary_memory.py (#3996)
Co-authored-by: k-boikov <64261260+k-boikov@users.noreply.github.com>
2023-05-12 22:18:15 +03:00
Boostrix
b06ea616d9 explain temperature setting in env file (#4140)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-12 19:22:26 +01:00
Andres Caicedo
2513178980 Integrate pytest-xdist Plugin for Parallel and Concurrent Testing (#3870)
* Adds pytest-parallel dependencies

* Implement pytest-parallel for faster tests

* Uses pytest-xdist

* Auto number of workers processes

* Update ci.yml

---------

Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-11 17:19:52 -07:00
Boostrix
8f3119621c document that docker-compose 1.29.0 is minimally required (#3963)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-10 01:01:45 -05:00
Itai Steinherz
5989c14577 Fix path to workspace directory in setup guide (#3927)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-10 00:45:09 -05:00
Shlomi
980bbe2bc3 fix typo in the getting started docs (#3997)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-08 14:16:48 +01:00
minfeng-ai
23e1e1ed53 fix typos (#3998)
Co-authored-by: Minfeng Lu <minfenglu@Minfengs-MacBook-Pro.local>
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-08 14:10:58 +01:00
Tomasz Kasperczyk
0166eacb2b Use correct reference to prompt_generator in autogpt/llm/chat.py (#4011) 2023-05-08 13:59:12 +01:00
merwanehamadi
33a3e6f998 Feature/centralize prompt (#3990)
Co-authored-by: xiao.hu <454292663@qq.com>
2023-05-08 07:24:53 +01:00
Kaan
d1327fd1c2 Improve & fix memory challenge docs. (#3989)
Co-authored-by: Kaan Osmanagaoglu <kaano@questps.com.au>
2023-05-07 19:03:58 -07:00
merwanehamadi
baa7873ec1 memory challenge c inconsistent (#3985) 2023-05-07 20:16:26 -05:00
Douglas Schonholtz
dc959596fc Memory Challenge C (#3908)
* Memory Challenge C

* Working cassettes

* Doc fixes

* Linting and doc fix

* Updated cassette

* One more cassette try

---------

Co-authored-by: merwanehamadi <merwanehamadi@gmail.com>
2023-05-07 16:28:43 -07:00
merwanehamadi
08bc8ff3f7 add code owners policy (#3981)
* add code owners

* added @ to codeowners

* switched to team ownership
2023-05-08 11:16:59 +12:00
merwanehamadi
ca5abff93f add information retrieval challenge to the wiki (#3876) 2023-05-06 17:48:08 +01:00
Andres Caicedo
d184d0d235 Implement Logging of User Input in logs/Debug Folder (#3867)
* Adds USER_INPUT_FILE_NAME

* Update agent.py

* Update agent.py

Log only if console_input is not the authorise_key

* Reformatting
2023-05-05 17:14:08 -07:00
Pi
06317dfb2b Add link to wiki page on Contributing 2023-05-05 23:10:56 +01:00
Pi
d57af05f66 fix 2023-05-05 23:07:55 +01:00
Pi
7eddfacd10 Add link to wiki Contributing page 2023-05-05 23:06:48 +01:00
Pi
3df88be6cb Update PULL_REQUEST_TEMPLATE.md
Added link to wiki Contributing page
2023-05-05 23:03:09 +01:00
Pi
b496cdcfb2 Update README.md 2023-05-05 22:59:21 +01:00
merwanehamadi
6d4bea3bb6 community challenges in the wiki (#3764) 2023-05-05 17:35:12 +01:00
AbTrax
e12438de41 Self feedback Improvement (#3680)
* Improved `Self-Feedback`

* minor tweak

* Test: Updated `test_get_self_feedback.py`
2023-05-05 09:09:21 -07:00
Pi
f2bef76368 Update CONTRIBUTING.md 2023-05-05 02:38:40 +01:00
bszollosinagy
ea08050049 BUGFIX: Selenium Driver object reference was included in the browsing results for some reason (#3642)
* * there is really no need to return the  reference to the Selenium driver along with the text summary and list of links.

* * removing unused second return value from browse_website()

* * updated cassette

* * updated YAML cassette for test_browse_website

* * after requirements reinstall, another update YAML cassette for test_browse_website

* * another update YAML cassette for test_browse_website, only as a placholder commit to trigger re-testing due to some docker TCP timeout issue

* * another update YAML cassette for test_browse_website

---------

Co-authored-by: batyu <batyu@localhost>
2023-05-04 23:11:21 +01:00
Ambuj Pawar
7d234522b7 ADD: Bash block in the contributing markdown (#3701)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-04 19:08:54 +01:00
itsmarble
d9170cab22 Update run.sh (#3752)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-04 19:01:53 +01:00
itsmarble
ad8b8cb9eb Update run.bat (#3783)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-04 18:55:58 +01:00
RainRat
fad24b3525 fix typos (#3798) 2023-05-04 18:46:37 +01:00
merwanehamadi
b0163230a9 create information retrieval challenge a (#3770)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-04 17:44:10 +01:00
Robin Richtsfeld
cb97f5c101 Fix call to plugin.post_planning (#3414)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-03 21:45:00 -05:00
Luke K
a48f26c150 Enable denylist handling for plugins (#3688)
Co-authored-by: Luke Kyohere <lkyohere@mfsafrica.com>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-03 21:31:23 -05:00
Nicholas Tindle
911cea781f Document Disabling command categories (#3669)
* feat: move task_complete command out of prompt

* fix: formatting fixes

* feat: add command disabling

* docs: document how to disable command categories
2023-05-03 19:14:07 -05:00
Nicholas Tindle
d2a9e54dfb Allow users to Disable Commands via the .env (#3667) 2023-05-03 19:05:35 -05:00
Nicholas Tindle
d74428057e Move task_complete command out of prompt (#3663)
* feat: move task_complete command out of prompt

* fix: formatting fixes

* Add the shutdown command to the test agents

* tests: update test vcrs

---------

Co-authored-by: James Collins <collijk@uw.edu>
2023-05-03 16:40:49 -07:00
merwanehamadi
e21917cc93 Feature/enable intuitive logs summarization (#3697) 2023-05-03 17:32:03 +01:00
merwanehamadi
26c6cfeefd Feature/enable intuitive logs for community challenge step 1 (#3695) 2023-05-03 08:27:54 +01:00
Peter Petermann
479c7468b4 Fix docker volume mounts (#3710)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-02 18:08:15 -05:00
merwanehamadi
b0c6ed999c Feature/tighten up ci pipeline (#3700) 2023-05-02 19:07:50 +01:00
Reinier van der Leer
b5f95fd672 Patch master with fixes from release-v0.3 (#3694) 2023-05-02 17:01:04 +02:00
Reinier van der Leer
d8f5cdbb50 Release v0.3.0 (#3683) 2023-05-02 16:53:43 +02:00
Reinier van der Leer
6e5ddeb015 v0.3.0 2023-05-02 16:32:19 +02:00
Reinier van der Leer
725abbb662 Fix bulletin 2023-05-02 16:30:37 +02:00
Reinier van der Leer
e4129e1a3a Fix CI for stable 2023-05-02 13:35:23 +02:00
Reinier van der Leer
dbd68df40c Merge branch 'stable' into release-v0.3 2023-05-02 13:27:40 +02:00
Reinier van der Leer
3a80e2f399 Revert "Revert "Merge branch 'master' into stable""
This reverts commit 999990b614.
2023-05-02 13:26:30 +02:00
Reinier van der Leer
0e1c0c55f8 Synchronize stable -> master (#3677)
* Revert "Merge branch 'master' into stable"

This reverts commit c4008971f7, reversing
changes made to fe855fef13.

* Fix `validate_json` file error when cwd != project root (#2665)

Co-authored-by: qianchengliang <qianchengliang1@huawei.com>

* Revert "Revert "Merge branch 'master' into stable""

This reverts commit 999990b614.

---------

Co-authored-by: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com>
Co-authored-by: Mick <30898949+mickjagger19@users.noreply.github.com>
Co-authored-by: qianchengliang <qianchengliang1@huawei.com>
2023-05-02 12:17:09 +01:00
gravelBridge
2e9c80a486 Fix MACOS Zip Import Error when compressing plugin (#3629)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-05-01 22:49:44 -05:00
Reinier van der Leer
1d26f6b697 Add warning for LLM to avoid context overflow (#3646) 2023-05-01 19:48:27 -05:00
kinance
4767fe63d3 Fix the maximum context length issue by chunking (#3222)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
2023-05-01 20:13:24 +02:00
k-boikov
0ef6f06462 Fix validate_json scheme path (#3631)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
2023-05-01 20:06:22 +02:00
sidewaysthought
a5f856328d Fix multi-byte character handling in read_file (#3173)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
2023-05-01 19:50:50 +02:00
non-adjective
7fc6f2abfc update web_selenium.py to use try-with for headers (#2988)
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-01 16:45:52 +01:00
Bob
94ec4a4ea5 Fix file operations logger (#3489)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
2023-05-01 17:37:30 +02:00
Ashutosh Kataria
9c56b1beef Message about Pinecone initializing (#1194)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-05-01 15:31:28 +01:00
AbTrax
34261a1583 Fix side effects on message history (#3619)
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
2023-05-01 15:16:26 +02:00
Reinier van der Leer
d8968ae899 Update documentation URLs to docs.agpt.co (#3621) 2023-05-01 14:01:13 +02:00
Valay Dave
6ae90a3ea2 [bug] list_files api signature change in data_ingestion.py and lo… (#3601) 2023-05-01 06:57:16 +01:00
zyt600
c317cf0e75 fix bug #3455 (#3591)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
2023-04-30 16:24:07 -05:00
Richard Beales
c1329c92fd rename search_files to list_files (#3595) 2023-04-30 16:14:53 -05:00
Toran Bruce Richards
abd6115aea Add website to README.md 2023-05-01 08:35:42 +12:00
WladBlank
6d2c0c4242 add report method to typewriter_log & load report plugins into logger (#3582)
* add report method to typewriter_log & load report plugins into logger

* more clear log and comment

* isort and black
2023-04-30 09:43:01 -07:00
k-boikov
aab79fdf6d added tests for clone_repository (#3558)
Co-authored-by: Nicholas Tindle <nick@ntindle.com>
Co-authored-by: Richard Beales <rich@richbeales.net>
2023-04-30 10:41:45 +01:00
Mick
91537b0496 Fix validate_json file error when cwd != project root (#2665)
Co-authored-by: qianchengliang <qianchengliang1@huawei.com>
2023-04-21 03:26:28 +02:00
BillSchumacher
999990b614 Revert "Merge branch 'master' into stable"
This reverts commit c4008971f7, reversing
changes made to fe855fef13.
2023-04-20 01:15:46 -05:00
233 changed files with 9442 additions and 9666 deletions

View File

@@ -7,11 +7,12 @@
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "true",
"username": "vscode",
"userUid": "6942",
"userGid": "6942",
"userUid": "1000",
"userGid": "1000",
"upgradePackages": "true"
},
"ghcr.io/devcontainers/features/desktop-lite:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/python:1": "none",
"ghcr.io/devcontainers/features/node:1": "none",
"ghcr.io/devcontainers/features/git:1": {
@@ -25,8 +26,20 @@
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python"
}
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false
},
"extensions": [
"ms-python.python",
"VisualStudioExptTeam.vscodeintellicode",
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"ms-python.isort",
"GitHub.vscode-pull-request-github",
"GitHub.copilot",
"github.vscode-github-actions"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
@@ -36,5 +49,8 @@
// "postCreateCommand": "pip3 install --user -r requirements.txt",
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
"remoteUser": "vscode",
// Add the freshly containerized repo to the list of safe repositories
"postCreateCommand": "git config --global --add safe.directory /workspace/Auto-GPT && pip3 install --user -r requirements.txt"
}

View File

@@ -4,16 +4,9 @@ version: '3.9'
services:
auto-gpt:
depends_on:
- redis
build:
dockerfile: .devcontainer/Dockerfile
context: ../
tty: true
environment:
MEMORY_BACKEND: ${MEMORY_BACKEND:-redis}
REDIS_HOST: ${REDIS_HOST:-redis}
volumes:
- ../:/workspace/Auto-GPT
redis:
image: 'redis/redis-stack-server:latest'

View File

@@ -2,6 +2,7 @@
*.template
*.yaml
*.yml
!prompt_settings.yaml
*.md
*.png

View File

@@ -1,10 +1,16 @@
# For further descriptions of these settings see docs/configuration/options.md or go to docs.agpt.co
################################################################################
### AUTO-GPT - GENERAL SETTINGS
################################################################################
## OPENAI_API_KEY - OpenAI API Key (Example: my-openai-api-key)
OPENAI_API_KEY=your-openai-api-key
## EXECUTE_LOCAL_COMMANDS - Allow local command execution (Default: False)
## RESTRICT_TO_WORKSPACE - Restrict file operations to workspace ./auto_gpt_workspace (Default: True)
# EXECUTE_LOCAL_COMMANDS=False
## RESTRICT_TO_WORKSPACE - Restrict file operations to workspace ./auto_gpt_workspace (Default: True)
# RESTRICT_TO_WORKSPACE=True
## USER_AGENT - Define the user-agent used by the requests library to browse website (string)
@@ -13,202 +19,186 @@
## AI_SETTINGS_FILE - Specifies which AI Settings file to use (defaults to ai_settings.yaml)
# AI_SETTINGS_FILE=ai_settings.yaml
## PLUGINS_CONFIG_FILE - The path to the plugins_config.yaml file (Default plugins_config.yaml)
# PLUGINS_CONFIG_FILE=plugins_config.yaml
## PROMPT_SETTINGS_FILE - Specifies which Prompt Settings file to use (defaults to prompt_settings.yaml)
# PROMPT_SETTINGS_FILE=prompt_settings.yaml
## OPENAI_API_BASE_URL - Custom url for the OpenAI API, useful for connecting to custom backends. No effect if USE_AZURE is true, leave blank to keep the default url
# the following is an example:
# OPENAI_API_BASE_URL=http://localhost:443/v1
## AUTHORISE COMMAND KEY - Key to authorise commands
# AUTHORISE_COMMAND_KEY=y
## EXIT_KEY - Key to exit AUTO-GPT
# EXIT_KEY=n
## PLAIN_OUTPUT - Plain output, which disables the spinner (Default: False)
# PLAIN_OUTPUT=False
## DISABLED_COMMAND_CATEGORIES - The list of categories of commands that are disabled (Default: None)
# DISABLED_COMMAND_CATEGORIES=
################################################################################
### LLM PROVIDER
################################################################################
### OPENAI
## OPENAI_API_KEY - OpenAI API Key (Example: my-openai-api-key)
## TEMPERATURE - Sets temperature in OpenAI (Default: 0)
## USE_AZURE - Use Azure OpenAI or not (Default: False)
OPENAI_API_KEY=your-openai-api-key
# TEMPERATURE=0
# USE_AZURE=False
### AZURE
# moved to `azure.yaml.template`
## OPENAI_ORGANIZATION - Your OpenAI Organization key (Default: None)
# OPENAI_ORGANIZATION=
## USE_AZURE - Use Azure OpenAI or not (Default: False)
# USE_AZURE=False
################################################################################
### LLM MODELS
################################################################################
## SMART_LLM_MODEL - Smart language model (Default: gpt-4)
## SMART_LLM_MODEL - Smart language model (Default: gpt-3.5-turbo)
# SMART_LLM_MODEL=gpt-3.5-turbo
## FAST_LLM_MODEL - Fast language model (Default: gpt-3.5-turbo)
# SMART_LLM_MODEL=gpt-4
# FAST_LLM_MODEL=gpt-3.5-turbo
### LLM MODEL SETTINGS
## FAST_TOKEN_LIMIT - Fast token limit for OpenAI (Default: 4000)
## SMART_TOKEN_LIMIT - Smart token limit for OpenAI (Default: 8000)
## When using --gpt3only this needs to be set to 4000.
# FAST_TOKEN_LIMIT=4000
# SMART_TOKEN_LIMIT=8000
## EMBEDDING_MODEL - Model to use for creating embeddings
# EMBEDDING_MODEL=text-embedding-ada-002
################################################################################
### SHELL EXECUTION
################################################################################
## SHELL_COMMAND_CONTROL - Whether to use "allowlist" or "denylist" to determine what shell commands can be executed (Default: denylist)
# SHELL_COMMAND_CONTROL=denylist
## ONLY if SHELL_COMMAND_CONTROL is set to denylist:
## SHELL_DENYLIST - List of shell commands that ARE NOT allowed to be executed by Auto-GPT (Default: sudo,su)
# SHELL_DENYLIST=sudo,su
## ONLY if SHELL_COMMAND_CONTROL is set to allowlist:
## SHELL_ALLOWLIST - List of shell commands that ARE allowed to be executed by Auto-GPT (Default: None)
# SHELL_ALLOWLIST=
################################################################################
### MEMORY
################################################################################
### MEMORY_BACKEND - Memory backend type
## local - Default
## pinecone - Pinecone (if configured)
## redis - Redis (if configured)
## milvus - Milvus (if configured - also works with Zilliz)
## MEMORY_INDEX - Name of index created in Memory backend (Default: auto-gpt)
# MEMORY_BACKEND=local
### General
## MEMORY_BACKEND - Memory backend type
# MEMORY_BACKEND=json_file
## MEMORY_INDEX - Value used in the Memory backend for scoping, naming, or indexing (Default: auto-gpt)
# MEMORY_INDEX=auto-gpt
### PINECONE
## PINECONE_API_KEY - Pinecone API Key (Example: my-pinecone-api-key)
## PINECONE_ENV - Pinecone environment (region) (Example: us-west-2)
# PINECONE_API_KEY=your-pinecone-api-key
# PINECONE_ENV=your-pinecone-region
### Redis
### REDIS
## REDIS_HOST - Redis host (Default: localhost, use "redis" for docker-compose)
## REDIS_PORT - Redis port (Default: 6379)
## REDIS_PASSWORD - Redis password (Default: "")
## WIPE_REDIS_ON_START - Wipes data / index on start (Default: True)
# REDIS_HOST=localhost
## REDIS_PORT - Redis port (Default: 6379)
# REDIS_PORT=6379
## REDIS_PASSWORD - Redis password (Default: "")
# REDIS_PASSWORD=
## WIPE_REDIS_ON_START - Wipes data / index on start (Default: True)
# WIPE_REDIS_ON_START=True
### WEAVIATE
## MEMORY_BACKEND - Use 'weaviate' to use Weaviate vector storage
## WEAVIATE_HOST - Weaviate host IP
## WEAVIATE_PORT - Weaviate host port
## WEAVIATE_PROTOCOL - Weaviate host protocol (e.g. 'http')
## USE_WEAVIATE_EMBEDDED - Whether to use Embedded Weaviate
## WEAVIATE_EMBEDDED_PATH - File system path were to persist data when running Embedded Weaviate
## WEAVIATE_USERNAME - Weaviate username
## WEAVIATE_PASSWORD - Weaviate password
## WEAVIATE_API_KEY - Weaviate API key if using API-key-based authentication
# WEAVIATE_HOST="127.0.0.1"
# WEAVIATE_PORT=8080
# WEAVIATE_PROTOCOL="http"
# USE_WEAVIATE_EMBEDDED=False
# WEAVIATE_EMBEDDED_PATH="/home/me/.local/share/weaviate"
# WEAVIATE_USERNAME=
# WEAVIATE_PASSWORD=
# WEAVIATE_API_KEY=
### MILVUS
## MILVUS_ADDR - Milvus remote address (e.g. localhost:19530, https://xxx-xxxx.xxxx.xxxx.zillizcloud.com:443)
## MILVUS_USERNAME - username for your Milvus database
## MILVUS_PASSWORD - password for your Milvus database
## MILVUS_SECURE - True to enable TLS. (Default: False)
## Setting MILVUS_ADDR to a `https://` URL will override this setting.
## MILVUS_COLLECTION - Milvus collection, change it if you want to start a new memory and retain the old memory.
# MILVUS_ADDR=localhost:19530
# MILVUS_USERNAME=
# MILVUS_PASSWORD=
# MILVUS_SECURE=
# MILVUS_COLLECTION=autogpt
################################################################################
### IMAGE GENERATION PROVIDER
################################################################################
### OPEN AI
## IMAGE_PROVIDER - Image provider (Example: dalle)
## IMAGE_SIZE - Image size (Example: 256)
## DALLE: 256, 512, 1024
### Common
## IMAGE_PROVIDER - Image provider (Default: dalle)
# IMAGE_PROVIDER=dalle
## IMAGE_SIZE - Image size (Default: 256)
# IMAGE_SIZE=256
### HUGGINGFACE
## HUGGINGFACE_IMAGE_MODEL - Text-to-image model from Huggingface (Default: CompVis/stable-diffusion-v1-4)
## HUGGINGFACE_API_TOKEN - HuggingFace API token (Example: my-huggingface-api-token)
# HUGGINGFACE_IMAGE_MODEL=CompVis/stable-diffusion-v1-4
# HUGGINGFACE_API_TOKEN=your-huggingface-api-token
### Huggingface (IMAGE_PROVIDER=huggingface)
### STABLE DIFFUSION WEBUI
## SD_WEBUI_AUTH - Stable diffusion webui username:password pair (Example: username:password)
## SD_WEBUI_URL - Stable diffusion webui API URL (Example: http://127.0.0.1:7860)
## HUGGINGFACE_IMAGE_MODEL - Text-to-image model from Huggingface (Default: CompVis/stable-diffusion-v1-4)
# HUGGINGFACE_IMAGE_MODEL=CompVis/stable-diffusion-v1-4
## HUGGINGFACE_API_TOKEN - HuggingFace API token (Default: None)
# HUGGINGFACE_API_TOKEN=
### Stable Diffusion (IMAGE_PROVIDER=sdwebui)
## SD_WEBUI_AUTH - Stable Diffusion Web UI username:password pair (Default: None)
# SD_WEBUI_AUTH=
# SD_WEBUI_URL=http://127.0.0.1:7860
## SD_WEBUI_URL - Stable Diffusion Web UI API URL (Default: http://localhost:7860)
# SD_WEBUI_URL=http://localhost:7860
################################################################################
### AUDIO TO TEXT PROVIDER
################################################################################
### HUGGINGFACE
# HUGGINGFACE_AUDIO_TO_TEXT_MODEL=facebook/wav2vec2-base-960h
## AUDIO_TO_TEXT_PROVIDER - Audio-to-text provider (Default: huggingface)
# AUDIO_TO_TEXT_PROVIDER=huggingface
## HUGGINGFACE_AUDIO_TO_TEXT_MODEL - The model for HuggingFace to use (Default: CompVis/stable-diffusion-v1-4)
# HUGGINGFACE_AUDIO_TO_TEXT_MODEL=CompVis/stable-diffusion-v1-4
################################################################################
### GIT Provider for repository actions
################################################################################
### GITHUB
## GITHUB_API_KEY - Github API key / PAT (Example: github_pat_123)
## GITHUB_USERNAME - Github username
# GITHUB_API_KEY=github_pat_123
# GITHUB_USERNAME=your-github-username
################################################################################
## GITHUB_API_KEY - Github API key / PAT (Default: None)
# GITHUB_API_KEY=
## GITHUB_USERNAME - Github username (Default: None)
# GITHUB_USERNAME=
################################################################################
### WEB BROWSING
################################################################################
### BROWSER
## HEADLESS_BROWSER - Whether to run the browser in headless mode (default: True)
## USE_WEB_BROWSER - Sets the web-browser driver to use with selenium (default: chrome).
## Note: set this to either 'chrome', 'firefox', or 'safari' depending on your current browser
# HEADLESS_BROWSER=True
## USE_WEB_BROWSER - Sets the web-browser driver to use with selenium (default: chrome)
# USE_WEB_BROWSER=chrome
## BROWSE_CHUNK_MAX_LENGTH - When browsing website, define the length of chunks to summarize (in number of tokens, excluding the response. 75 % of FAST_TOKEN_LIMIT is usually wise )
## BROWSE_CHUNK_MAX_LENGTH - When browsing website, define the length of chunks to summarize (Default: 3000)
# BROWSE_CHUNK_MAX_LENGTH=3000
## BROWSE_SPACY_LANGUAGE_MODEL is used to split sentences. Install additional languages via pip, and set the model name here. Example Chinese: python -m spacy download zh_core_web_sm
## BROWSE_SPACY_LANGUAGE_MODEL - spaCy language model](https://spacy.io/usage/models) to use when creating chunks. (Default: en_core_web_sm)
# BROWSE_SPACY_LANGUAGE_MODEL=en_core_web_sm
### GOOGLE
## GOOGLE_API_KEY - Google API key (Example: my-google-api-key)
## CUSTOM_SEARCH_ENGINE_ID - Custom search engine ID (Example: my-custom-search-engine-id)
# GOOGLE_API_KEY=your-google-api-key
# CUSTOM_SEARCH_ENGINE_ID=your-custom-search-engine-id
## GOOGLE_API_KEY - Google API key (Default: None)
# GOOGLE_API_KEY=
## GOOGLE_CUSTOM_SEARCH_ENGINE_ID - Google custom search engine ID (Default: None)
# GOOGLE_CUSTOM_SEARCH_ENGINE_ID=
################################################################################
### TTS PROVIDER
### TEXT TO SPEECH PROVIDER
################################################################################
### MAC OS
## USE_MAC_OS_TTS - Use Mac OS TTS or not (Default: False)
# USE_MAC_OS_TTS=False
## TEXT_TO_SPEECH_PROVIDER - Which Text to Speech provider to use (Default: gtts)
# TEXT_TO_SPEECH_PROVIDER=gtts
### STREAMELEMENTS
## USE_BRIAN_TTS - Use Brian TTS or not (Default: False)
# USE_BRIAN_TTS=False
### Only if TEXT_TO_SPEECH_PROVIDER=streamelements
## STREAMELEMENTS_VOICE - Voice to use for StreamElements (Default: Brian)
# STREAMELEMENTS_VOICE=Brian
### ELEVENLABS
## ELEVENLABS_API_KEY - Eleven Labs API key (Example: my-elevenlabs-api-key)
## ELEVENLABS_VOICE_1_ID - Eleven Labs voice 1 ID (Example: my-voice-id-1)
## ELEVENLABS_VOICE_2_ID - Eleven Labs voice 2 ID (Example: my-voice-id-2)
# ELEVENLABS_API_KEY=your-elevenlabs-api-key
# ELEVENLABS_VOICE_1_ID=your-voice-id-1
# ELEVENLABS_VOICE_2_ID=your-voice-id-2
### Only if TEXT_TO_SPEECH_PROVIDER=elevenlabs
## ELEVENLABS_API_KEY - Eleven Labs API key (Default: None)
# ELEVENLABS_API_KEY=
## ELEVENLABS_VOICE_ID - Eleven Labs voice ID (Example: None)
# ELEVENLABS_VOICE_ID=
################################################################################
### TWITTER API
### CHAT MESSAGES
################################################################################
# TW_CONSUMER_KEY=
# TW_CONSUMER_SECRET=
# TW_ACCESS_TOKEN=
# TW_ACCESS_TOKEN_SECRET=
################################################################################
### ALLOWLISTED PLUGINS
################################################################################
#ALLOWLISTED_PLUGINS - Sets the listed plugins that are allowed (Example: plugin1,plugin2,plugin3)
ALLOWLISTED_PLUGINS=
################################################################################
### CHAT PLUGIN SETTINGS
################################################################################
# CHAT_MESSAGES_ENABLED - Enable chat messages (Default: False)
## CHAT_MESSAGES_ENABLED - Enable chat messages (Default: False)
# CHAT_MESSAGES_ENABLED=False

2
.gitattributes vendored
View File

@@ -1,5 +1,5 @@
# Exclude VCR cassettes from stats
tests/**/cassettes/**.y*ml linguist-generated
tests/Auto-GPT-test-cassettes/**/**.y*ml linguist-generated
# Mark documentation as such
docs/**.md linguist-documentation

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
.github/workflows/ @Significant-Gravitas/Auto-GPT-Source

View File

@@ -8,14 +8,16 @@ body:
### ⚠️ Before you continue
* Check out our [backlog], [roadmap] and join our [discord] to discuss what's going on
* If you need help, you can ask in the [discussions] section or in [#tech-support]
* **Throughly search the [existing issues] before creating a new one**
* **Thoroughly search the [existing issues] before creating a new one**
* Read our [wiki page on Contributing]
[backlog]: https://github.com/orgs/Significant-Gravitas/projects/1
[roadmap]: https://github.com/orgs/Significant-Gravitas/projects/2
[discord]: https://discord.gg/autogpt
[discussions]: https://github.com/Significant-Gravitas/Auto-GPT/discussions
[#tech-support]: https://discord.com/channels/1092243196446249134/1092275629602394184
[existing issues]: https://github.com/Significant-Gravitas/Auto-GPT/issues?q=is%3Aissue
[wiki page on Contributing]: https://github.com/Significant-Gravitas/Nexus/wiki/Contributing
- type: checkboxes
attributes:
label: ⚠️ Search for existing issues first ⚠️
@@ -25,23 +27,29 @@ body:
options:
- label: I have searched the existing issues, and there is no existing issue for my problem
required: true
- type: markdown
attributes:
value: |
Please provide a searchable summary of the issue in the title above ⬆️.
⚠️ SUPER-busy repo, please help the volunteer maintainers.
The less time we spend here, the more time we spend building AutoGPT.
Please confirm that the issue you have is described well and precise in the title above ⬆️.
A good rule of thumb: What would you type if you were searching for the issue?
Please help us help you:
- Does it work on `stable` branch (https://github.com/Torantulino/Auto-GPT/tree/stable)?
- Does it work on current `master` (https://github.com/Torantulino/Auto-GPT/tree/master)?
- Search for existing issues, "add comment" is tidier than "new issue"
- Ask on our Discord (https://discord.gg/autogpt)
For example:
BAD - my auto-gpt keeps looping
GOOD - After performing execute_python_file, auto-gpt goes into a loop where it keeps trying to execute the file.
⚠️ SUPER-busy repo, please help the volunteer maintainers.
The less time we spend here, the more time we can spend building AutoGPT.
Please help us help you by following these steps:
- Search for existing issues, adding a comment when you have the same or similar issue is tidier than "new issue" and
newer issues will not be reviewed earlier, this is dependent on the current priorities set by our wonderful team
- Ask on our Discord if your issue is known when you are unsure (https://discord.gg/autogpt)
- Provide relevant info:
- Provide commit-hash (`git rev-parse HEAD` gets it)
- If it's a pip/packages issue, provide pip version, python version
- If it's a crash, provide traceback.
- Provide commit-hash (`git rev-parse HEAD` gets it) if possible
- If it's a pip/packages issue, mention this in the title and provide pip version, python version
- If it's a crash, provide traceback and describe the error you got as precise as possible in the title.
- type: dropdown
attributes:
label: Which Operating System are you using?
@@ -54,9 +62,15 @@ body:
- Docker
- Devcontainer / Codespace
- Windows Subsystem for Linux (WSL)
- Other (Please specify in your problem)
- Other
validations:
required: true
nested_fields:
- type: text
attributes:
label: Specify the system
description: Please specify the system you are working on.
- type: dropdown
attributes:
label: Which version of Auto-GPT are you using?
@@ -71,61 +85,80 @@ body:
- Master (branch)
validations:
required: true
- type: dropdown
attributes:
label: GPT-3 or GPT-4?
label: Do you use OpenAI GPT-3 or GPT-4?
description: >
If you are using Auto-GPT with `--gpt3only`, your problems may be caused by
the [limitations](https://github.com/Significant-Gravitas/Auto-GPT/issues?q=is%3Aissue+label%3A%22AI+model+limitation%22) of GPT-3.5.
options:
- GPT-3.5
- GPT-4
- GPT-4(32k)
validations:
required: true
- type: textarea
- type: dropdown
attributes:
label: Steps to reproduce 🕹
description: |
**⚠️ Issues that we can't reproduce will be closed.**
- type: textarea
attributes:
label: Current behavior 😯
description: Describe what happens instead of the expected behavior.
- type: textarea
attributes:
label: Expected behavior 🤔
description: Describe what should happen.
- type: textarea
attributes:
label: Your prompt 📝
label: Which area covers your issue best?
description: >
If applicable please provide the prompt you are using. Your prompt is stored in your `ai_settings.yaml` file.
value: |
```yaml
# Paste your prompt here
```
Select the area related to the issue you are reporting.
options:
- Installation and setup
- Memory
- Performance
- Prompt
- Commands
- Plugins
- AI Model Limitations
- Challenges
- Documentation
- Logging
- Agents
- Other
validations:
required: true
autolabels: true
nested_fields:
- type: text
attributes:
label: Specify the area
description: Please specify the area you think is best related to the issue.
- type: textarea
attributes:
label: Your Logs 📒
description: |
Please include the log showing your error and the command that caused it, if applicable.
You can copy it from your terminal or from `logs/activity.log`.
This will help us understand your issue better!
<details>
<summary><i>Example</i></summary>
```log
INFO NEXT ACTION: COMMAND = execute_shell ARGUMENTS = {'command_line': 'some_command'}
INFO -=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=
Traceback (most recent call last):
File "/home/anaconda3/lib/python3.9/site-packages/openai/api_requestor.py", line 619, in _interpret_response
self._interpret_response_line(
File "/home/anaconda3/lib/python3.9/site-packages/openai/api_requestor.py", line 682, in _interpret_response_line
raise self.handle_error_response(
openai.error.InvalidRequestError: This model's maximum context length is 8191 tokens, however you requested 10982 tokens (10982 in your prompt; 0 for the completion). Please reduce your prompt; or completion length.
```
</details>
label: Describe your issue.
description: Describe the problem you are experiencing. Try to describe only the issue and phrase it short but clear. ⚠️ Provide NO other data in this field
validations:
required: true
#Following are optional file content uploads
- type: markdown
attributes:
value: |
```log
<insert your logs here>
```
The following is OPTIONAL, please keep in mind that the log files may contain personal information such as credentials.⚠️
"The log files are located in the folder 'logs' inside the main auto-gpt folder."
- type: input
attributes:
label: Upload Activity Log Content
description: |
Upload the activity log content, this can help us understand the issue better.
To do this, go to the folder logs in your main auto-gpt folder, open activity.log and copy/paste the contents to this field.
⚠️ The activity log may contain personal data given to auto-gpt by you in prompt or input as well as
any personal information that auto-gpt collected out of files during last run. Do not add the activity log if you are not comfortable with sharing it. ⚠️
validations:
required: false
- type: input
attributes:
label: Upload Error Log Content
description: |
Upload the error log content, this will help us understand the issue better.
To do this, go to the folder logs in your main auto-gpt folder, open error.log and copy/paste the contents to this field.
⚠️ The error log may contain personal data given to auto-gpt by you in prompt or input as well as
any personal information that auto-gpt collected out of files during last run. Do not add the activity log if you are not comfortable with sharing it. ⚠️
validations:
required: false

View File

@@ -1,13 +1,12 @@
name: Feature request 🚀
description: Suggest a new idea for Auto-GPT.
description: Suggest a new idea for Auto-GPT!
labels: ['status: needs triage']
body:
- type: markdown
attributes:
value: |
First, check out our [wiki page on Contributing](https://github.com/Significant-Gravitas/Nexus/wiki/Contributing)
Please provide a searchable summary of the issue in the title above ⬆️.
Thanks for contributing by creating an issue! ❤️
- type: checkboxes
attributes:
label: Duplicates
@@ -26,4 +25,4 @@ body:
- type: textarea
attributes:
label: Motivation 🔦
description: What are you trying to accomplish? How has the lack of this feature affected you? Providing context helps us come up with a solution that is more useful in the real world.
description: What are you trying to accomplish? How has the lack of this feature affected you? Providing context helps us come up with a solution that is more useful in the real world.

View File

@@ -14,6 +14,8 @@ Provide clear documentation and explanations of the changes made.
Ensure diffs are limited to the intended lines — no applying preferred formatting styles or line endings (unless that's what the PR is about).
For guidance on committing only the specific lines you have changed, refer to this helpful video: https://youtu.be/8-hSNHHbiZg
Check out our [wiki page on Contributing](https://github.com/Significant-Gravitas/Nexus/wiki/Contributing)
By following these guidelines, your PRs are more likely to be merged quickly after testing, as long as they align with the project's overall direction. -->
### Background
@@ -33,7 +35,14 @@ By following these guidelines, your PRs are more likely to be merged quickly aft
- [ ] I have thoroughly tested my changes with multiple different prompts.
- [ ] I have considered potential risks and mitigations for my changes.
- [ ] I have documented my changes clearly and comprehensively.
- [ ] I have not snuck in any "extra" small tweaks changes <!-- Submit these as separate Pull Requests, they are the easiest to merge! -->
- [ ] I have not snuck in any "extra" small tweaks changes. <!-- Submit these as separate Pull Requests, they are the easiest to merge! -->
- [ ] I have run the following commands against my code to ensure it passes our linters:
```shell
black .
isort .
mypy
autoflake --remove-all-unused-imports --recursive --ignore-init-module-imports --ignore-pass-after-docstring autogpt tests --in-place
```
<!-- If you haven't added tests, please explain why. If you have, check the appropriate box. If you've ensured your PR is atomic and well-documented, check the corresponding boxes. -->

View File

@@ -1,31 +1,73 @@
name: Run Benchmarks
name: Benchmarks
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
build:
Benchmark:
name: ${{ matrix.config.task-name }}
runs-on: ubuntu-latest
env:
python-version: '3.10'
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
config:
- python-version: "3.10"
task: "tests/challenges"
task-name: "Mandatory Tasks"
- python-version: "3.10"
task: "--beat-challenges -ra tests/challenges"
task-name: "Challenging Tasks"
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: master
- name: Set up Python ${{ env.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ env.python-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.config.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- id: get_date
name: Get date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: benchmark
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python benchmark/benchmark_entrepreneur_gpt_with_undecisive_user.py
- name: Set up Python dependency cache
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ steps.get_date.outputs.date }}
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run pytest with coverage
run: |
rm -rf tests/Auto-GPT-test-cassettes
pytest -n auto --record-mode=all ${{ matrix.config.task }}
env:
CI: true
PROXY: ${{ secrets.PROXY }}
AGENT_MODE: ${{ secrets.AGENT_MODE }}
AGENT_TYPE: ${{ secrets.AGENT_TYPE }}
PLAIN_OUTPUT: True
- name: Upload logs as artifact
if: always()
uses: actions/upload-artifact@v3
with:
name: test-logs-${{ matrix.config.task-name }}
path: logs/
- name: Upload cassettes as artifact
if: always()
uses: actions/upload-artifact@v3
with:
name: cassettes-${{ matrix.config.task-name }}
path: tests/Auto-GPT-test-cassettes/

View File

@@ -2,16 +2,24 @@ name: Python CI
on:
push:
branches: [ master ]
branches: [ master, ci-test* ]
paths-ignore:
- 'tests/Auto-GPT-test-cassettes'
- 'tests/challenges/current_score.json'
pull_request:
branches: [ master ]
branches: [ stable, master, release-* ]
pull_request_target:
branches: [ master, release-*, ci-test* ]
concurrency:
group: ${{ format('ci-{0}', github.head_ref && format('pr-{0}', github.event.pull_request.number) || github.sha) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ${{ format('ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
cancel-in-progress: ${{ startsWith(github.event_name, 'pull_request') }}
jobs:
lint:
# eliminate duplicate runs
if: github.event_name == 'push' || (github.event.pull_request.head.repo.fork == (github.event_name == 'pull_request_target'))
runs-on: ubuntu-latest
env:
min-python-version: "3.10"
@@ -19,12 +27,26 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Set up Python ${{ env.min-python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ env.min-python-version }}
- id: get_date
name: Get date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Set up Python dependency cache
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ steps.get_date.outputs.date }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@@ -41,7 +63,19 @@ jobs:
run: isort . --check
if: success() || failure()
- name: Check mypy formatting
run: mypy
if: success() || failure()
- name: Check for unused imports and pass statements
run: |
cmd="autoflake --remove-all-unused-imports --recursive --ignore-init-module-imports --ignore-pass-after-docstring autogpt tests"
$cmd --check || (echo "You have unused imports or pass statements, please run '${cmd} --in-place'" && exit 1)
test:
# eliminate duplicate runs
if: github.event_name == 'push' || (github.event.pull_request.head.repo.fork == (github.event_name == 'pull_request_target'))
permissions:
# Gives the action the necessary permissions for publishing new
# comments in pull requests.
@@ -51,27 +85,170 @@ jobs:
# comments (to avoid publishing multiple comments in the same PR)
contents: write
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.10"]
steps:
- name: Check out repository
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
submodules: true
- name: Configure git user Auto-GPT-Bot
run: |
git config --global user.name "Auto-GPT-Bot"
git config --global user.email "github-bot@agpt.co"
- name: Checkout cassettes
if: ${{ startsWith(github.event_name, 'pull_request') }}
run: |
cassette_branch="${{ github.event.pull_request.user.login }}-${{ github.event.pull_request.head.ref }}"
cd tests/Auto-GPT-test-cassettes
if git ls-remote --exit-code --heads origin $cassette_branch ; then
git fetch origin $cassette_branch
git fetch origin ${{ github.event.pull_request.base.ref }}
git checkout $cassette_branch
# Pick non-conflicting cassette updates from the base branch
git merge --no-commit --strategy-option=ours origin/${{ github.event.pull_request.base.ref }}
echo "Using cassettes from mirror branch '$cassette_branch'," \
"synced to upstream branch '${{ github.event.pull_request.base.ref }}'."
else
git checkout -b $cassette_branch
echo "Branch '$cassette_branch' does not exist in cassette submodule." \
"Using cassettes from '${{ github.event.pull_request.base.ref }}'."
fi
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
- id: get_date
name: Get date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Set up Python dependency cache
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ steps.get_date.outputs.date }}
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run unittest tests with coverage
- name: Run pytest with coverage
run: |
pytest --cov=autogpt --cov-report term-missing --cov-branch --cov-report xml --cov-report term
pytest -n auto --cov=autogpt --cov-branch --cov-report term-missing --cov-report xml \
tests/unit tests/integration tests/challenges
python tests/challenges/utils/build_current_score.py
env:
CI: true
PROXY: ${{ secrets.PROXY }}
AGENT_MODE: ${{ secrets.AGENT_MODE }}
AGENT_TYPE: ${{ secrets.AGENT_TYPE }}
PLAIN_OUTPUT: True
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- id: setup_git_auth
name: Set up git token authentication
# Cassettes may be pushed even when tests fail
if: success() || failure()
run: |
config_key="http.${{ github.server_url }}/.extraheader"
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64 -w0)
git config "$config_key" \
"Authorization: Basic $base64_pat"
cd tests/Auto-GPT-test-cassettes
git config "$config_key" \
"Authorization: Basic $base64_pat"
echo "config_key=$config_key" >> $GITHUB_OUTPUT
- name: Push updated challenge scores
if: github.event_name == 'push'
run: |
score_file="tests/challenges/current_score.json"
if ! git diff --quiet $score_file; then
git add $score_file
git commit -m "Update challenge scores"
git push origin HEAD:${{ github.ref_name }}
else
echo "The challenge scores didn't change."
fi
- id: push_cassettes
name: Push updated cassettes
# For pull requests, push updated cassettes even when tests fail
if: github.event_name == 'push' || success() || failure()
run: |
if [ "${{ startsWith(github.event_name, 'pull_request') }}" = "true" ]; then
is_pull_request=true
cassette_branch="${{ github.event.pull_request.user.login }}-${{ github.event.pull_request.head.ref }}"
else
cassette_branch="${{ github.ref_name }}"
fi
cd tests/Auto-GPT-test-cassettes
# Commit & push changes to cassettes if any
if ! git diff --quiet; then
git add .
git commit -m "Auto-update cassettes"
git push origin HEAD:$cassette_branch
if [ ! $is_pull_request ]; then
cd ../..
git add tests/Auto-GPT-test-cassettes
git commit -m "Update cassette submodule"
git push origin HEAD:$cassette_branch
fi
echo "updated=true" >> $GITHUB_OUTPUT
else
echo "updated=false" >> $GITHUB_OUTPUT
echo "No cassette changes to commit"
fi
- name: Post Set up git token auth
if: steps.setup_git_auth.outcome == 'success'
run: |
git config --unset-all '${{ steps.setup_git_auth.outputs.config_key }}'
git submodule foreach git config --unset-all '${{ steps.setup_git_auth.outputs.config_key }}'
- name: Apply "behaviour change" label and comment on PR
if: ${{ startsWith(github.event_name, 'pull_request') }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
TOKEN=${{ secrets.PAT_REVIEW }}
REPO=${{ github.repository }}
if [[ "${{ steps.push_cassettes.outputs.updated }}" == "true" ]]; then
echo "Adding label and comment..."
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/$REPO/issues/$PR_NUMBER/labels \
-d '{"labels":["behaviour change"]}'
echo $TOKEN | gh auth login --with-token
gh api repos/$REPO/issues/$PR_NUMBER/comments -X POST -F body="You changed AutoGPT's behaviour. The cassettes have been updated and will be merged to the submodule when this Pull Request gets merged."
fi
- name: Upload logs as artifact
if: always()
uses: actions/upload-artifact@v3
with:
name: test-logs
path: logs/

View File

@@ -3,8 +3,11 @@ name: Docker CI
on:
push:
branches: [ master ]
paths-ignore:
- 'tests/Auto-GPT-test-cassettes'
- 'tests/challenges/current_score.json'
pull_request:
branches: [ master ]
branches: [ master, release-*, stable ]
concurrency:
group: ${{ format('docker-ci-{0}', github.head_ref && format('pr-{0}', github.event.pull_request.number) || github.sha) }}
@@ -73,43 +76,51 @@ jobs:
# Docker setup needs fixing before this is going to work: #1843
test:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Check out repository
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- id: build
name: Build image
uses: docker/build-push-action@v3
with:
build-args: BUILD_TYPE=dev # include pytest
tags: ${{ env.IMAGE_NAME }}
load: true # save to docker images
# cache layers in GitHub Actions cache to speed up builds
cache-from: type=gha,scope=docker-dev
cache-to: type=gha,scope=docker-dev,mode=max
- id: build
name: Build image
uses: docker/build-push-action@v3
with:
build-args: BUILD_TYPE=dev # include pytest
tags: ${{ env.IMAGE_NAME }}
load: true # save to docker images
# cache layers in GitHub Actions cache to speed up builds
cache-from: type=gha,scope=docker-dev
cache-to: type=gha,scope=docker-dev,mode=max
- id: test
name: Run tests
env:
CI: true
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set +e
test_output=$(
docker run --env CI --env OPENAI_API_KEY --entrypoint python ${{ env.IMAGE_NAME }} -m \
pytest --cov=autogpt --cov-report term-missing --cov-branch --cov-report xml --cov-report term 2>&1
)
test_failure=$?
- id: test
name: Run tests
env:
PLAIN_OUTPUT: True
CI: true
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set +e
test_output=$(
docker run --env CI --env OPENAI_API_KEY --entrypoint python ${{ env.IMAGE_NAME }} -m \
pytest -n auto --cov=autogpt --cov-branch --cov-report term-missing \
tests/unit tests/integration 2>&1
)
test_failure=$?
echo "$test_output"
echo "$test_output"
cat << $EOF >> $GITHUB_STEP_SUMMARY
# Tests $([ $test_failure = 0 ] && echo '✅' || echo '❌')
\`\`\`
$test_output
\`\`\`
$EOF
cat << $EOF >> $GITHUB_STEP_SUMMARY
# Tests $([ $test_failure = 0 ] && echo '✅' || echo '❌')
\`\`\`
$test_output
\`\`\`
$EOF
exit $test_failure

View File

@@ -3,7 +3,10 @@ name: "Pull Request auto-label"
on:
# So that PRs touching the same files as the push are updated
push:
branches: [ master ]
branches: [ master, release-* ]
paths-ignore:
- 'tests/Auto-GPT-test-cassettes'
- 'tests/challenges/current_score.json'
# So that the `dirtyLabel` is removed if conflicts are resolve
# We recommend `pull_request_target` so that github secrets are available.
# In `pull_request` we wouldn't be able to change labels of fork PRs
@@ -45,11 +48,10 @@ jobs:
s_label: 'size/s'
s_max_size: 10
m_label: 'size/m'
m_max_size: 50
m_max_size: 100
l_label: 'size/l'
l_max_size: 200
l_max_size: 500
xl_label: 'size/xl'
message_if_xl: >
This PR exceeds the recommended size of 200 lines.
This PR exceeds the recommended size of 500 lines.
Please make sure you are NOT addressing multiple issues with one PR.
Note this PR might be rejected due to its size

View File

@@ -1,28 +0,0 @@
name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: '0 */12 * * *'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.README_UPDATER_PAT }}
file: 'README.md'
minimum: 2500
maximum: 99999
- name: Deploy to GitHub Pages 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: master
folder: '.'
token: ${{ secrets.README_UPDATER_PAT }}

13
.gitignore vendored
View File

@@ -1,12 +1,7 @@
## Original ignores
autogpt/keys.py
autogpt/*json
autogpt/node_modules/
autogpt/__pycache__/keys.cpython-310.pyc
autogpt/auto_gpt_workspace
package-lock.json
*.pyc
auto_gpt_workspace/*
autogpt/*.json
**/auto_gpt_workspace/*
*.mpeg
.env
azure.yaml
@@ -21,6 +16,7 @@ logs
*.log
*.mp3
mem.sqlite3
venvAutoGPT
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -36,6 +32,7 @@ build/
develop-eggs/
dist/
plugins/
plugins_config.yaml
downloads/
eggs/
.eggs/
@@ -162,4 +159,4 @@ vicuna-*
openai/
# news
CURRENT_BULLETIN.md
CURRENT_BULLETIN.md

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "tests/Auto-GPT-test-cassettes"]
path = tests/Auto-GPT-test-cassettes
url = https://github.com/Significant-Gravitas/Auto-GPT-test-cassettes
branch = master

View File

@@ -22,8 +22,18 @@ repos:
- id: black
language_version: python3.10
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.3.0'
hooks:
- id: mypy
- repo: local
hooks:
- id: autoflake
name: autoflake
entry: autoflake --in-place --remove-all-unused-imports --recursive --ignore-init-module-imports --ignore-pass-after-docstring autogpt tests
language: python
types: [ python ]
- id: pytest-check
name: pytest-check
entry: pytest --cov=autogpt --without-integration --without-slow-integration

View File

@@ -1,9 +1,27 @@
Welcome to Auto-GPT! We'll keep you informed of the latest news and features by printing messages here.
If you don't wish to see this message, you can run Auto-GPT with the --skip-news flag
# Website and Documentation Site 📰📖
Check out *https://agpt.co*, the official news & updates site for Auto-GPT!
The documentation also has a place here, at *https://docs.agpt.co*
# INCLUDED COMMAND 'send_tweet' IS DEPRICATED, AND WILL BE REMOVED IN THE NEXT STABLE RELEASE
Base Twitter functionality (and more) is now covered by plugins: https://github.com/Significant-Gravitas/Auto-GPT-Plugins
# For contributors 👷🏼
Since releasing v0.3.0, whave been working on re-architecting the Auto-GPT core to make it more extensible and make room for structural performance-oriented R&D.
## Changes to Docker configuration
The workdir has been changed from /home/appuser to /app. Be sure to update any volume mounts accordingly.
Check out the contribution guide on our wiki:
https://github.com/Significant-Gravitas/Auto-GPT/wiki/Contributing
# 🚀 v0.4.1 Release 🚀
Two weeks and 50+ pull requests have passed since v0.4.0, and we are happy to announce the release of v0.4.1!
Highlights and notable changes since v0.4.0:
- The .env.template is more readable and better explains the purpose of each environment variable.
- More dependable search
- The CUSTOM_SEARCH_ENGINE_ID variable has been replaced to GOOGLE_CUSTOM_SEARCH_ENGINE_ID, make sure you update it.
- Better read_file
- More reliable python code execution
- Lots of JSON error fixes
- Directory-based plugins
## Further fixes and changes 🛠️
Under the hood, we've done a bunch of work improving architectures and streamlining code. Most of that won't be user-visible
## Take a look at the Release Notes on Github for the full changelog!
https://github.com/Significant-Gravitas/Auto-GPT/releases

View File

@@ -1,148 +1,14 @@
# Contributing to Auto-GPT
We maintain a knowledgebase at this [wiki](https://github.com/Significant-Gravitas/Nexus/wiki)
First of all, thank you for considering contributing to our project! We appreciate your time and effort, and we value any contribution, whether it's reporting a bug, suggesting a new feature, or submitting a pull request.
We would like to say "We value all contributions". After all, we are an open-source project, so we should say something fluffy like this, right?
This document provides guidelines and best practices to help you contribute effectively.
However the reality is that some contributions are SUPER-valuable, while others create more trouble than they are worth and actually _create_ work for the core team.
## Code of Conduct
If you wish to contribute, please look through the wiki [contributing](https://github.com/Significant-Gravitas/Nexus/wiki/Contributing) page.
By participating in this project, you agree to abide by our [Code of Conduct]. Please read it to understand the expectations we have for everyone who contributes to this project.
If you wish to involve with the project (beyond just contributing PRs), please read the wiki [catalyzing](https://github.com/Significant-Gravitas/Nexus/wiki/Catalyzing) page.
[Code of Conduct]: https://significant-gravitas.github.io/Auto-GPT/code-of-conduct.md
In fact, why not just look through the whole wiki (it's only a few pages) and hop on our discord (you'll find it in the wiki).
## 📢 A Quick Word
Right now we will not be accepting any Contributions that add non-essential commands to Auto-GPT.
However, you absolutely can still add these commands to Auto-GPT in the form of plugins.
Please check out this [template](https://github.com/Significant-Gravitas/Auto-GPT-Plugin-Template).
## Getting Started
1. Fork the repository and clone your fork.
2. Create a new branch for your changes (use a descriptive name, such as `fix-bug-123` or `add-new-feature`).
3. Make your changes in the new branch.
4. Test your changes thoroughly.
5. Commit and push your changes to your fork.
6. Create a pull request following the guidelines in the [Submitting Pull Requests](#submitting-pull-requests) section.
## How to Contribute
### Reporting Bugs
If you find a bug in the project, please create an issue on GitHub with the following information:
- A clear, descriptive title for the issue.
- A description of the problem, including steps to reproduce the issue.
- Any relevant logs, screenshots, or other supporting information.
### Suggesting Enhancements
If you have an idea for a new feature or improvement, please create an issue on GitHub with the following information:
- A clear, descriptive title for the issue.
- A detailed description of the proposed enhancement, including any benefits and potential drawbacks.
- Any relevant examples, mockups, or supporting information.
### Submitting Pull Requests
When submitting a pull request, please ensure that your changes meet the following criteria:
- Your pull request should be atomic and focus on a single change.
- Your pull request should include tests for your change. We automatically enforce this with [CodeCov](https://docs.codecov.com/docs/commit-status)
- You should have thoroughly tested your changes with multiple different prompts.
- You should have considered potential risks and mitigations for your changes.
- You should have documented your changes clearly and comprehensively.
- You should not include any unrelated or "extra" small tweaks or changes.
## Style Guidelines
### Code Formatting
We use the `black` and `isort` code formatters to maintain a consistent coding style across the project. Please ensure that your code is formatted properly before submitting a pull request.
To format your code, run the following commands in the project's root directory:
```bash
python -m black .
python -m isort .
```
Or if you have these tools installed globally:
```bash
black .
isort .
```
### Pre-Commit Hooks
We use pre-commit hooks to ensure that code formatting and other checks are performed automatically before each commit. To set up pre-commit hooks for this project, follow these steps:
Install the pre-commit package using pip:
```bash
pip install pre-commit
```
Run the following command in the project's root directory to install the pre-commit hooks:
```bash
pre-commit install
```
Now, the pre-commit hooks will run automatically before each commit, checking your code formatting and other requirements.
If you encounter any issues or have questions, feel free to reach out to the maintainers or open a new issue on GitHub. We're here to help and appreciate your efforts to contribute to the project.
Happy coding, and once again, thank you for your contributions!
Maintainers will look at PR that have no merge conflicts when deciding what to add to the project. Make sure your PR shows up here:
https://github.com/Significant-Gravitas/Auto-GPT/pulls?q=is%3Apr+is%3Aopen+-label%3Aconflicts
## Testing your changes
If you add or change code, make sure the updated code is covered by tests.
To increase coverage if necessary, [write tests using pytest].
For more info on running tests, please refer to ["Running tests"](https://significant-gravitas.github.io/Auto-GPT/testing/).
[write tests using pytest]: https://realpython.com/pytest-python-testing/
### API-dependent tests
To run tests that involve making calls to the OpenAI API, we use VCRpy. It caches known
requests and matching responses in so-called *cassettes*, allowing us to run the tests
in CI without needing actual API access.
When changes cause a test prompt to be generated differently, it will likely miss the
cache and make a request to the API, updating the cassette with the new request+response.
*Be sure to include the updated cassette in your PR!*
When you run Pytest locally:
- If no prompt change: you will not consume API tokens because there are no new OpenAI calls required.
- If the prompt changes in a way that the cassettes are not reusable:
- If no API key, the test fails. It requires a new cassette. So, add an API key to .env.
- If the API key is present, the tests will make a real call to OpenAI.
- If the test ends up being successful, your prompt changes didn't introduce regressions. This is good. Commit your cassettes to your PR.
- If the test is unsuccessful:
- Either: Your change made Auto-GPT less capable, in that case, you have to change your code.
- Or: The test might be poorly written. In that case, you can make suggestions to change the test.
In our CI pipeline, Pytest will use the cassettes and not call paid API providers, so we need your help to record the replays that you break.
### Community Challenges
Challenges are goals we need Auto-GPT to achieve.
To pick the challenge you like, go to the tests/integration/challenges folder and select the areas you would like to work on.
- a challenge is new if level_currently_beaten is None
- a challenge is in progress if level_currently_beaten is greater or equal to 1
- a challenge is beaten if level_currently_beaten = max_level
Here is an example of how to run the memory challenge A and attempt to beat level 3.
pytest -s tests/integration/challenges/memory/test_memory_challenge_a.py --level=3
To beat a challenge, you're not allowed to change anything in the tests folder, you have to add code in the autogpt folder
Challenges use cassettes. Cassettes allow us to replay your runs in our CI pipeline.
Don't hesitate to delete the cassettes associated to the challenge you're working on if you need to. Otherwise it will keep replaying the last run.
Once you've beaten a new level of a challenge, please create a pull request and we will analyze how you changed Auto-GPT to beat the challenge.
❤️ & 🔆
The team @ Auto-GPT

View File

@@ -6,11 +6,13 @@ FROM python:3.10-slim AS autogpt-base
# Install browsers
RUN apt-get update && apt-get install -y \
chromium-driver firefox-esr \
ca-certificates
chromium-driver firefox-esr ca-certificates \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install utilities
RUN apt-get install -y curl jq wget git
RUN apt-get update && apt-get install -y \
curl jq wget git \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Set environment variables
ENV PIP_NO_CACHE_DIR=yes \
@@ -22,7 +24,7 @@ ENV PATH="$PATH:/root/.local/bin"
COPY requirements.txt .
# Set the entrypoint
ENTRYPOINT ["python", "-m", "autogpt"]
ENTRYPOINT ["python", "-m", "autogpt", "--install-plugin-deps"]
# dev build -> include everything
FROM autogpt-base as autogpt-dev
@@ -36,5 +38,9 @@ RUN sed -i '/Items below this point will not be included in the Docker Image/,$d
pip install --no-cache-dir -r requirements.txt
WORKDIR /app
ONBUILD COPY autogpt/ ./autogpt
ONBUILD COPY scripts/ ./scripts
ONBUILD COPY plugins/ ./plugins
ONBUILD COPY prompt_settings.yaml ./prompt_settings.yaml
ONBUILD RUN mkdir ./data
FROM autogpt-${BUILD_TYPE} AS auto-gpt

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,29 @@
import json
import signal
import sys
from datetime import datetime
from colorama import Fore, Style
from autogpt.app import execute_command, get_command
from autogpt.commands.command import CommandRegistry
from autogpt.config import Config
from autogpt.json_utils.json_fix_llm import fix_json_using_multiple_techniques
from autogpt.json_utils.utilities import LLM_DEFAULT_RESPONSE_FORMAT, validate_json
from autogpt.llm import chat_with_ai, create_chat_completion, create_chat_message
from autogpt.config.ai_config import AIConfig
from autogpt.json_utils.utilities import extract_json_from_response, validate_json
from autogpt.llm.base import ChatSequence
from autogpt.llm.chat import chat_with_ai, create_chat_completion
from autogpt.llm.providers.openai import OPEN_AI_CHAT_MODELS
from autogpt.llm.utils import count_string_tokens
from autogpt.log_cycle.log_cycle import (
FULL_MESSAGE_HISTORY_FILE_NAME,
NEXT_ACTION_FILE_NAME,
PROMPT_SUPERVISOR_FEEDBACK_FILE_NAME,
SUPERVISOR_FEEDBACK_FILE_NAME,
USER_INPUT_FILE_NAME,
LogCycleHandler,
)
from autogpt.logs import logger, print_assistant_thoughts
from autogpt.memory.message_history import MessageHistory
from autogpt.memory.vector import VectorMemory
from autogpt.speech import say_text
from autogpt.spinner import Spinner
from autogpt.utils import clean_input
@@ -18,7 +36,6 @@ class Agent:
Attributes:
ai_name: The name of the agent.
memory: The memory object to use.
full_message_history: The full message history.
next_action_count: The number of actions to execute.
system_prompt: The system prompt is the initial prompt that defines everything
the AI needs to know to achieve its task successfully.
@@ -27,7 +44,7 @@ class Agent:
triggering_prompt: The last sentence the AI will see before answering.
For Auto-GPT, this prompt is:
Determine which next command to use, and respond using the format specified
Determine exactly one command to use, and respond using the format specified
above:
The triggering prompt is not part of the system prompt because between the
system prompt and the triggering
@@ -43,110 +60,152 @@ class Agent:
def __init__(
self,
ai_name,
memory,
full_message_history,
next_action_count,
command_registry,
config,
system_prompt,
triggering_prompt,
workspace_directory,
ai_name: str,
memory: VectorMemory,
next_action_count: int,
command_registry: CommandRegistry,
ai_config: AIConfig,
system_prompt: str,
triggering_prompt: str,
workspace_directory: str,
config: Config,
):
cfg = Config()
self.ai_name = ai_name
self.memory = memory
self.summary_memory = (
"I was created." # Initial memory necessary to avoid hilucination
)
self.last_memory_index = 0
self.full_message_history = full_message_history
self.history = MessageHistory(self)
self.next_action_count = next_action_count
self.command_registry = command_registry
self.config = config
self.ai_config = ai_config
self.system_prompt = system_prompt
self.triggering_prompt = triggering_prompt
self.workspace = Workspace(workspace_directory, cfg.restrict_to_workspace)
self.workspace = Workspace(workspace_directory, config.restrict_to_workspace)
self.created_at = datetime.now().strftime("%Y%m%d_%H%M%S")
self.cycle_count = 0
self.log_cycle_handler = LogCycleHandler()
self.fast_token_limit = OPEN_AI_CHAT_MODELS.get(
config.fast_llm_model
).max_tokens
def start_interaction_loop(self):
# Avoid circular imports
from autogpt.app import execute_command, get_command
# Interaction Loop
cfg = Config()
loop_count = 0
self.cycle_count = 0
command_name = None
arguments = None
user_input = ""
# Signal handler for interrupting y -N
def signal_handler(signum, frame):
if self.next_action_count == 0:
sys.exit()
else:
print(
Fore.RED
+ "Interrupt signal received. Stopping continuous command execution."
+ Style.RESET_ALL
)
self.next_action_count = 0
signal.signal(signal.SIGINT, signal_handler)
while True:
# Discontinue if continuous limit is reached
loop_count += 1
self.cycle_count += 1
self.log_cycle_handler.log_count_within_cycle = 0
self.log_cycle_handler.log_cycle(
self.ai_config.ai_name,
self.created_at,
self.cycle_count,
[m.raw() for m in self.history],
FULL_MESSAGE_HISTORY_FILE_NAME,
)
if (
cfg.continuous_mode
and cfg.continuous_limit > 0
and loop_count > cfg.continuous_limit
self.config.continuous_mode
and self.config.continuous_limit > 0
and self.cycle_count > self.config.continuous_limit
):
logger.typewriter_log(
"Continuous Limit Reached: ", Fore.YELLOW, f"{cfg.continuous_limit}"
"Continuous Limit Reached: ",
Fore.YELLOW,
f"{self.config.continuous_limit}",
)
break
# Send message to AI, get response
with Spinner("Thinking... "):
with Spinner("Thinking... ", plain_output=self.config.plain_output):
assistant_reply = chat_with_ai(
self.config,
self,
self.system_prompt,
self.triggering_prompt,
self.full_message_history,
self.memory,
cfg.fast_token_limit,
) # TODO: This hardcodes the model to use GPT3.5. Make this an argument
self.fast_token_limit,
self.config.fast_llm_model,
)
assistant_reply_json = fix_json_using_multiple_techniques(assistant_reply)
for plugin in cfg.plugins:
try:
assistant_reply_json = extract_json_from_response(assistant_reply)
validate_json(assistant_reply_json)
except json.JSONDecodeError as e:
logger.error(f"Exception while validating assistant reply JSON: {e}")
assistant_reply_json = {}
for plugin in self.config.plugins:
if not plugin.can_handle_post_planning():
continue
assistant_reply_json = plugin.post_planning(self, assistant_reply_json)
assistant_reply_json = plugin.post_planning(assistant_reply_json)
# Print Assistant thoughts
if assistant_reply_json != {}:
validate_json(assistant_reply_json, LLM_DEFAULT_RESPONSE_FORMAT)
# Get command name and arguments
try:
print_assistant_thoughts(
self.ai_name, assistant_reply_json, cfg.speak_mode
self.ai_name, assistant_reply_json, self.config.speak_mode
)
command_name, arguments = get_command(assistant_reply_json)
if cfg.speak_mode:
if self.config.speak_mode:
say_text(f"I want to execute {command_name}")
arguments = self._resolve_pathlike_command_args(arguments)
except Exception as e:
logger.error("Error: \n", str(e))
self.log_cycle_handler.log_cycle(
self.ai_config.ai_name,
self.created_at,
self.cycle_count,
assistant_reply_json,
NEXT_ACTION_FILE_NAME,
)
if not cfg.continuous_mode and self.next_action_count == 0:
# First log new-line so user can differentiate sections better in console
logger.typewriter_log("\n")
logger.typewriter_log(
"NEXT ACTION: ",
Fore.CYAN,
f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} "
f"ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}",
)
if not self.config.continuous_mode and self.next_action_count == 0:
# ### GET USER AUTHORIZATION TO EXECUTE COMMAND ###
# Get key press: Prompt the user to press enter to continue or escape
# to exit
self.user_input = ""
logger.typewriter_log(
"NEXT ACTION: ",
Fore.CYAN,
f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} "
f"ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}",
)
logger.info(
"Enter 'y' to authorise command, 'y -N' to run N continuous commands, 's' to run self-feedback commands"
"Enter 'y' to authorise command, 'y -N' to run N continuous commands, 's' to run self-feedback commands, "
"'n' to exit program, or enter feedback for "
f"{self.ai_name}..."
)
while True:
if cfg.chat_messages_enabled:
if self.config.chat_messages_enabled:
console_input = clean_input("Waiting for your response...")
else:
console_input = clean_input(
Fore.MAGENTA + "Input:" + Style.RESET_ALL
)
if console_input.lower().strip() == cfg.authorise_key:
if console_input.lower().strip() == self.config.authorise_key:
user_input = "GENERATE NEXT COMMAND JSON"
break
elif console_input.lower().strip() == "s":
@@ -157,22 +216,22 @@ class Agent:
)
thoughts = assistant_reply_json.get("thoughts", {})
self_feedback_resp = self.get_self_feedback(
thoughts, cfg.fast_llm_model
thoughts, self.config.fast_llm_model
)
logger.typewriter_log(
f"SELF FEEDBACK: {self_feedback_resp}",
Fore.YELLOW,
"",
)
if self_feedback_resp[0].lower().strip() == cfg.authorise_key:
user_input = "GENERATE NEXT COMMAND JSON"
else:
user_input = self_feedback_resp
user_input = self_feedback_resp
command_name = "self_feedback"
break
elif console_input.lower().strip() == "":
logger.warn("Invalid input format.")
continue
elif console_input.lower().startswith(f"{cfg.authorise_key} -"):
elif console_input.lower().startswith(
f"{self.config.authorise_key} -"
):
try:
self.next_action_count = abs(
int(console_input.split(" ")[1])
@@ -185,12 +244,19 @@ class Agent:
)
continue
break
elif console_input.lower() == cfg.exit_key:
elif console_input.lower() == self.config.exit_key:
user_input = "EXIT"
break
else:
user_input = console_input
command_name = "human_feedback"
self.log_cycle_handler.log_cycle(
self.ai_config.ai_name,
self.created_at,
self.cycle_count,
user_input,
USER_INPUT_FILE_NAME,
)
break
if user_input == "GENERATE NEXT COMMAND JSON":
@@ -203,37 +269,45 @@ class Agent:
logger.info("Exiting...")
break
else:
# Print command
# First log new-line so user can differentiate sections better in console
logger.typewriter_log("\n")
# Print authorized commands left value
logger.typewriter_log(
"NEXT ACTION: ",
Fore.CYAN,
f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL}"
f" ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}",
f"{Fore.CYAN}AUTHORISED COMMANDS LEFT: {Style.RESET_ALL}{self.next_action_count}"
)
# Execute command
if command_name is not None and command_name.lower().startswith("error"):
result = (
f"Command {command_name} threw the following error: {arguments}"
)
result = f"Could not execute command: {arguments}"
elif command_name == "human_feedback":
result = f"Human feedback: {user_input}"
elif command_name == "self_feedback":
result = f"Self feedback: {user_input}"
else:
for plugin in cfg.plugins:
for plugin in self.config.plugins:
if not plugin.can_handle_pre_command():
continue
command_name, arguments = plugin.pre_command(
command_name, arguments
)
command_result = execute_command(
self.command_registry,
command_name,
arguments,
self.config.prompt_generator,
command_name=command_name,
arguments=arguments,
agent=self,
)
result = f"Command {command_name} returned: " f"{command_result}"
for plugin in cfg.plugins:
result_tlength = count_string_tokens(
str(command_result), self.config.fast_llm_model
)
memory_tlength = count_string_tokens(
str(self.history.summary_message()), self.config.fast_llm_model
)
if result_tlength + memory_tlength + 600 > self.fast_token_limit:
result = f"Failure: command {command_name} returned too much output. \
Do not execute this command again with the same arguments."
for plugin in self.config.plugins:
if not plugin.can_handle_post_command():
continue
result = plugin.post_command(command_name, result)
@@ -243,12 +317,10 @@ class Agent:
# Check if there's a result from the command append it to the message
# history
if result is not None:
self.full_message_history.append(create_chat_message("system", result))
self.history.add("system", result, "action_result")
logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result)
else:
self.full_message_history.append(
create_chat_message("system", "Unable to execute command")
)
self.history.add("system", "Unable to execute command", "action_result")
logger.typewriter_log(
"SYSTEM: ", Fore.YELLOW, "Unable to execute command"
)
@@ -276,15 +348,32 @@ class Agent:
Returns:
str: A feedback response generated using the provided thoughts dictionary.
"""
ai_role = self.config.ai_role
ai_role = self.ai_config.ai_role
feedback_prompt = f"Below is a message from an AI agent with the role of {ai_role}. Please review the provided Thought, Reasoning, Plan, and Criticism. If these elements accurately contribute to the successful execution of the assumed role, respond with the letter 'Y' followed by a space, and then explain why it is effective. If the provided information is not suitable for achieving the role's objectives, please provide one or more sentences addressing the issue and suggesting a resolution."
feedback_prompt = f"Below is a message from me, an AI Agent, assuming the role of {ai_role}. whilst keeping knowledge of my slight limitations as an AI Agent Please evaluate my thought process, reasoning, and plan, and provide a concise paragraph outlining potential improvements. Consider adding or removing ideas that do not align with my role and explaining why, prioritizing thoughts based on their significance, or simply refining my overall thought process."
reasoning = thoughts.get("reasoning", "")
plan = thoughts.get("plan", "")
thought = thoughts.get("thoughts", "")
criticism = thoughts.get("criticism", "")
feedback_thoughts = thought + reasoning + plan + criticism
return create_chat_completion(
[{"role": "user", "content": feedback_prompt + feedback_thoughts}],
llm_model,
feedback_thoughts = thought + reasoning + plan
prompt = ChatSequence.for_model(llm_model)
prompt.add("user", feedback_prompt + feedback_thoughts)
self.log_cycle_handler.log_cycle(
self.ai_config.ai_name,
self.created_at,
self.cycle_count,
prompt.raw(),
PROMPT_SUPERVISOR_FEEDBACK_FILE_NAME,
)
feedback = create_chat_completion(prompt)
self.log_cycle_handler.log_cycle(
self.ai_config.ai_name,
self.created_at,
self.cycle_count,
feedback,
SUPERVISOR_FEEDBACK_FILE_NAME,
)
return feedback

View File

@@ -1,10 +1,9 @@
"""Agent manager for managing GPT agents"""
from __future__ import annotations
from typing import List
from autogpt.config.config import Config
from autogpt.llm import Message, create_chat_completion
from autogpt.config import Config
from autogpt.llm.base import ChatSequence
from autogpt.llm.chat import Message, create_chat_completion
from autogpt.singleton import Singleton
@@ -13,55 +12,55 @@ class AgentManager(metaclass=Singleton):
def __init__(self):
self.next_key = 0
self.agents = {} # key, (task, full_message_history, model)
self.agents: dict[
int, tuple[str, list[Message], str]
] = {} # key, (task, full_message_history, model)
self.cfg = Config()
# Create new GPT agent
# TODO: Centralise use of create_chat_completion() to globally enforce token limit
def create_agent(self, task: str, prompt: str, model: str) -> tuple[int, str]:
def create_agent(
self, task: str, creation_prompt: str, model: str
) -> tuple[int, str]:
"""Create a new agent and return its key
Args:
task: The task to perform
prompt: The prompt to use
model: The model to use
creation_prompt: Prompt passed to the LLM at creation
model: The model to use to run this agent
Returns:
The key of the new agent
"""
messages: List[Message] = [
{"role": "user", "content": prompt},
]
messages = ChatSequence.for_model(model, [Message("user", creation_prompt)])
for plugin in self.cfg.plugins:
if not plugin.can_handle_pre_instruction():
continue
if plugin_messages := plugin.pre_instruction(messages):
messages.extend(iter(plugin_messages))
if plugin_messages := plugin.pre_instruction(messages.raw()):
messages.extend([Message(**raw_msg) for raw_msg in plugin_messages])
# Start GPT instance
agent_reply = create_chat_completion(
model=model,
messages=messages,
)
agent_reply = create_chat_completion(prompt=messages)
messages.append({"role": "assistant", "content": agent_reply})
messages.add("assistant", agent_reply)
plugins_reply = ""
for i, plugin in enumerate(self.cfg.plugins):
if not plugin.can_handle_on_instruction():
continue
if plugin_result := plugin.on_instruction(messages):
if plugin_result := plugin.on_instruction([m.raw() for m in messages]):
sep = "\n" if i else ""
plugins_reply = f"{plugins_reply}{sep}{plugin_result}"
if plugins_reply and plugins_reply != "":
messages.append({"role": "assistant", "content": plugins_reply})
messages.add("assistant", plugins_reply)
key = self.next_key
# This is done instead of len(agents) to make keys unique even if agents
# are deleted
self.next_key += 1
self.agents[key] = (task, messages, model)
self.agents[key] = (task, list(messages), model)
for plugin in self.cfg.plugins:
if not plugin.can_handle_post_instruction():
@@ -83,33 +82,30 @@ class AgentManager(metaclass=Singleton):
task, messages, model = self.agents[int(key)]
# Add user message to message history before sending to agent
messages.append({"role": "user", "content": message})
messages = ChatSequence.for_model(model, messages)
messages.add("user", message)
for plugin in self.cfg.plugins:
if not plugin.can_handle_pre_instruction():
continue
if plugin_messages := plugin.pre_instruction(messages):
for plugin_message in plugin_messages:
messages.append(plugin_message)
if plugin_messages := plugin.pre_instruction([m.raw() for m in messages]):
messages.extend([Message(**raw_msg) for raw_msg in plugin_messages])
# Start GPT instance
agent_reply = create_chat_completion(
model=model,
messages=messages,
)
agent_reply = create_chat_completion(prompt=messages)
messages.append({"role": "assistant", "content": agent_reply})
messages.add("assistant", agent_reply)
plugins_reply = agent_reply
for i, plugin in enumerate(self.cfg.plugins):
if not plugin.can_handle_on_instruction():
continue
if plugin_result := plugin.on_instruction(messages):
if plugin_result := plugin.on_instruction([m.raw() for m in messages]):
sep = "\n" if i else ""
plugins_reply = f"{plugins_reply}{sep}{plugin_result}"
# Update full message history
if plugins_reply and plugins_reply != "":
messages.append({"role": "assistant", "content": plugins_reply})
messages.add("assistant", plugins_reply)
for plugin in self.cfg.plugins:
if not plugin.can_handle_post_instruction():

View File

@@ -1,21 +1,15 @@
""" Command and Control """
import json
from typing import Dict, List, NoReturn, Union
from typing import Dict, List, Union
from autogpt.agent.agent import Agent
from autogpt.agent.agent_manager import AgentManager
from autogpt.commands.command import CommandRegistry, command
from autogpt.commands.command import command
from autogpt.commands.web_requests import scrape_links, scrape_text
from autogpt.config import Config
from autogpt.logs import logger
from autogpt.memory import get_memory
from autogpt.processing.text import summarize_text
from autogpt.prompts.generator import PromptGenerator
from autogpt.speech import say_text
from autogpt.url_utils.validators import validate_url
CFG = Config()
AGENT_MANAGER = AgentManager()
def is_valid_int(value: str) -> bool:
"""Check if the value is a valid integer
@@ -90,50 +84,44 @@ def map_command_synonyms(command_name: str):
def execute_command(
command_registry: CommandRegistry,
command_name: str,
arguments,
prompt: PromptGenerator,
arguments: dict[str, str],
agent: Agent,
):
"""Execute the command and return the result
Args:
command_name (str): The name of the command to execute
arguments (dict): The arguments for the command
agent (Agent): The agent that is executing the command
Returns:
str: The result of the command
"""
try:
cmd = command_registry.commands.get(command_name)
cmd = agent.command_registry.commands.get(command_name)
# If the command is found, call it with the provided arguments
if cmd:
return cmd(**arguments)
return cmd(**arguments, agent=agent)
# TODO: Remove commands below after they are moved to the command registry.
command_name = map_command_synonyms(command_name.lower())
if command_name == "memory_add":
return get_memory(CFG).add(arguments["string"])
# TODO: Change these to take in a file rather than pasted code, if
# non-file is given, return instructions "Input should be a python
# filepath, write your code to file and try again
elif command_name == "task_complete":
shutdown()
else:
for command in prompt.commands:
if (
command_name == command["label"].lower()
or command_name == command["name"].lower()
):
return command["function"](**arguments)
return (
f"Unknown command '{command_name}'. Please refer to the 'COMMANDS'"
" list for available commands and only respond in the specified JSON"
" format."
)
for command in agent.ai_config.prompt_generator.commands:
if (
command_name == command["label"].lower()
or command_name == command["name"].lower()
):
return command["function"](**arguments)
return (
f"Unknown command '{command_name}'. Please refer to the 'COMMANDS'"
" list for available commands and only respond in the specified JSON"
" format."
)
except Exception as e:
return f"Error: {str(e)}"
@@ -142,8 +130,8 @@ def execute_command(
"get_text_summary", "Get text summary", '"url": "<url>", "question": "<question>"'
)
@validate_url
def get_text_summary(url: str, question: str) -> str:
"""Return the results of a Google search
def get_text_summary(url: str, question: str, agent: Agent) -> str:
"""Get the text summary of a webpage
Args:
url (str): The url to scrape
@@ -152,15 +140,16 @@ def get_text_summary(url: str, question: str) -> str:
Returns:
str: The summary of the text
"""
text = scrape_text(url)
summary = summarize_text(url, text, question)
text = scrape_text(url, agent)
summary, _ = summarize_text(text, question=question)
return f""" "Result" : {summary}"""
@command("get_hyperlinks", "Get text summary", '"url": "<url>"')
@command("get_hyperlinks", "Get hyperlinks", '"url": "<url>"')
@validate_url
def get_hyperlinks(url: str) -> Union[str, List[str]]:
"""Return the results of a Google search
def get_hyperlinks(url: str, agent: Agent) -> Union[str, List[str]]:
"""Get all hyperlinks on a webpage
Args:
url (str): The url to scrape
@@ -168,13 +157,7 @@ def get_hyperlinks(url: str) -> Union[str, List[str]]:
Returns:
str or list: The hyperlinks on the page
"""
return scrape_links(url)
def shutdown() -> NoReturn:
"""Shut down the program"""
logger.info("Shutting down...")
quit()
return scrape_links(url, agent)
@command(
@@ -182,7 +165,7 @@ def shutdown() -> NoReturn:
"Start GPT Agent",
'"name": "<name>", "task": "<short_task_desc>", "prompt": "<prompt>"',
)
def start_agent(name: str, task: str, prompt: str, model=CFG.fast_llm_model) -> str:
def start_agent(name: str, task: str, prompt: str, agent: Agent, model=None) -> str:
"""Start an agent with a given name, task, and prompt
Args:
@@ -194,6 +177,8 @@ def start_agent(name: str, task: str, prompt: str, model=CFG.fast_llm_model) ->
Returns:
str: The response of the agent
"""
agent_manager = AgentManager()
# Remove underscores from name
voice_name = name.replace("_", " ")
@@ -201,48 +186,48 @@ def start_agent(name: str, task: str, prompt: str, model=CFG.fast_llm_model) ->
agent_intro = f"{voice_name} here, Reporting for duty!"
# Create agent
if CFG.speak_mode:
if agent.config.speak_mode:
say_text(agent_intro, 1)
key, ack = AGENT_MANAGER.create_agent(task, first_message, model)
key, ack = agent_manager.create_agent(task, first_message, model)
if CFG.speak_mode:
if agent.config.speak_mode:
say_text(f"Hello {voice_name}. Your task is as follows. {task}.")
# Assign task (prompt), get response
agent_response = AGENT_MANAGER.message_agent(key, prompt)
agent_response = agent_manager.message_agent(key, prompt)
return f"Agent {name} created with key {key}. First response: {agent_response}"
@command("message_agent", "Message GPT Agent", '"key": "<key>", "message": "<message>"')
def message_agent(key: str, message: str) -> str:
def message_agent(key: str, message: str, agent: Agent) -> str:
"""Message an agent with a given key and message"""
# Check if the key is a valid integer
if is_valid_int(key):
agent_response = AGENT_MANAGER.message_agent(int(key), message)
agent_response = AgentManager().message_agent(int(key), message)
else:
return "Invalid key, must be an integer."
# Speak response
if CFG.speak_mode:
if agent.config.speak_mode:
say_text(agent_response, 1)
return agent_response
@command("list_agents", "List GPT Agents", "")
def list_agents() -> str:
@command("list_agents", "List GPT Agents", "() -> str")
def list_agents(agent: Agent) -> str:
"""List all agents
Returns:
str: A list of all agents
"""
return "List of agents:\n" + "\n".join(
[str(x[0]) + ": " + x[1] for x in AGENT_MANAGER.list_agents()]
[str(x[0]) + ": " + x[1] for x in AgentManager().list_agents()]
)
@command("delete_agent", "Delete GPT Agent", '"key": "<key>"')
def delete_agent(key: str) -> str:
def delete_agent(key: str, agent: Agent) -> str:
"""Delete an agent with a given key
Args:
@@ -251,5 +236,5 @@ def delete_agent(key: str) -> str:
Returns:
str: A message indicating whether the agent was deleted or not
"""
result = AGENT_MANAGER.delete_agent(key)
result = AgentManager().delete_agent(key)
return f"Agent {key} deleted." if result else f"Agent {key} does not exist."

View File

@@ -15,6 +15,11 @@ import click
"-C",
help="Specifies which ai_settings.yaml file to use, will also automatically skip the re-prompt.",
)
@click.option(
"--prompt-settings",
"-P",
help="Specifies which prompt_settings.yaml file to use.",
)
@click.option(
"-l",
"--continuous-limit",
@@ -66,6 +71,7 @@ def main(
continuous: bool,
continuous_limit: int,
ai_settings: str,
prompt_settings: str,
skip_reprompt: bool,
speak: bool,
debug: bool,
@@ -91,6 +97,7 @@ def main(
continuous,
continuous_limit,
ai_settings,
prompt_settings,
skip_reprompt,
speak,
debug,

View File

@@ -1,8 +1,9 @@
"""Code evaluation module."""
from __future__ import annotations
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.llm import call_ai_function
from autogpt.llm.utils import call_ai_function
@command(
@@ -10,7 +11,7 @@ from autogpt.llm import call_ai_function
"Analyze Code",
'"code": "<full_code_string>"',
)
def analyze_code(code: str) -> list[str]:
def analyze_code(code: str, agent: Agent) -> list[str]:
"""
A function that takes in a string and returns a response from create chat
completion api call.
@@ -28,4 +29,6 @@ def analyze_code(code: str) -> list[str]:
"Analyzes the given code and returns a list of suggestions for improvements."
)
return call_ai_function(function_string, args, description_string)
return call_ai_function(
function_string, args, description_string, config=agent.config
)

View File

@@ -3,20 +3,19 @@ import json
import requests
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.config import Config
CFG = Config()
@command(
"read_audio_from_file",
"Convert Audio to text",
'"filename": "<filename>"',
CFG.huggingface_audio_to_text_model,
"Configure huggingface_audio_to_text_model.",
lambda config: config.huggingface_audio_to_text_model
and config.huggingface_api_token,
"Configure huggingface_audio_to_text_model and Hugging Face api token.",
)
def read_audio_from_file(filename: str) -> str:
def read_audio_from_file(filename: str, agent: Agent) -> str:
"""
Convert audio to text.
@@ -28,10 +27,10 @@ def read_audio_from_file(filename: str) -> str:
"""
with open(filename, "rb") as audio_file:
audio = audio_file.read()
return read_audio(audio)
return read_audio(audio, agent.config)
def read_audio(audio: bytes) -> str:
def read_audio(audio: bytes, agent: Agent) -> str:
"""
Convert audio to text.
@@ -41,9 +40,20 @@ def read_audio(audio: bytes) -> str:
Returns:
str: The text from the audio
"""
model = CFG.huggingface_audio_to_text_model
if agent.config.audio_to_text_provider == "huggingface":
text = read_huggingface_audio(audio, agent.config)
if text:
return f"The audio says: {text}"
else:
return f"Error, couldn't convert audio to text"
return "Error: No audio to text provider given"
def read_huggingface_audio(audio: bytes, agent: Agent) -> str:
model = agent.config.huggingface_audio_to_text_model
api_url = f"https://api-inference.huggingface.co/models/{model}"
api_token = CFG.huggingface_api_token
api_token = agent.config.huggingface_api_token
headers = {"Authorization": f"Bearer {api_token}"}
if api_token is None:
@@ -57,5 +67,5 @@ def read_audio(audio: bytes) -> str:
data=audio,
)
text = json.loads(response.content.decode("utf-8"))["text"]
return f"The audio says: {text}"
response_json = json.loads(response.content.decode("utf-8"))
return response_json.get("text")

View File

@@ -1,8 +1,12 @@
import functools
import importlib
import inspect
from inspect import Parameter
from typing import Any, Callable, Optional
from autogpt.config import Config
from autogpt.logs import logger
# Unique identifier for auto-gpt commands
AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command"
@@ -22,19 +26,23 @@ class Command:
description: str,
method: Callable[..., Any],
signature: str = "",
enabled: bool = True,
enabled: bool | Callable[[Config], bool] = True,
disabled_reason: Optional[str] = None,
):
self.name = name
self.description = description
self.method = method
self.signature = signature if signature else str(inspect.signature(self.method))
self.signature = signature
self.enabled = enabled
self.disabled_reason = disabled_reason
def __call__(self, *args, **kwargs) -> Any:
if hasattr(kwargs, "config") and callable(self.enabled):
self.enabled = self.enabled(kwargs["config"])
if not self.enabled:
return f"Command '{self.name}' is disabled: {self.disabled_reason}"
if self.disabled_reason:
return f"Command '{self.name}' is disabled: {self.disabled_reason}"
return f"Command '{self.name}' is disabled"
return self.method(*args, **kwargs)
def __str__(self) -> str:
@@ -59,6 +67,10 @@ class CommandRegistry:
return importlib.reload(module)
def register(self, cmd: Command) -> None:
if cmd.name in self.commands:
logger.warn(
f"Command '{cmd.name}' already registered and will be overwritten!"
)
self.commands[cmd.name] = cmd
def unregister(self, command_name: str):
@@ -127,12 +139,22 @@ class CommandRegistry:
def command(
name: str,
description: str,
signature: str = "",
enabled: bool = True,
signature: str,
enabled: bool | Callable[[Config], bool] = True,
disabled_reason: Optional[str] = None,
) -> Callable[..., Any]:
"""The command decorator is used to create Command objects from ordinary functions."""
# TODO: Remove this in favor of better command management
CFG = Config()
if callable(enabled):
enabled = enabled(CFG)
if not enabled:
if disabled_reason is not None:
logger.debug(f"Command '{name}' is disabled: {disabled_reason}")
return lambda func: func
def decorator(func: Callable[..., Any]) -> Command:
cmd = Command(
name=name,
@@ -154,3 +176,32 @@ def command(
return wrapper
return decorator
def ignore_unexpected_kwargs(func: Callable[..., Any]) -> Callable[..., Any]:
def filter_kwargs(kwargs: dict) -> dict:
sig = inspect.signature(func)
# Parameter.VAR_KEYWORD - a dict of keyword arguments that aren't bound to any other
if any(map(lambda p: p.kind == Parameter.VAR_KEYWORD, sig.parameters.values())):
# if **kwargs exist, return directly
return kwargs
_params = list(
filter(
lambda p: p.kind
in {Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD},
sig.parameters.values(),
)
)
res_kwargs = {
param.name: kwargs[param.name] for param in _params if param.name in kwargs
}
return res_kwargs
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
kwargs = filter_kwargs(kwargs)
return func(*args, **kwargs)
return wrapper

View File

@@ -6,15 +6,53 @@ from pathlib import Path
import docker
from docker.errors import ImageNotFound
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.config import Config
from autogpt.logs import logger
from autogpt.setup import CFG
from autogpt.workspace.workspace import Workspace
CFG = Config()
ALLOWLIST_CONTROL = "allowlist"
DENYLIST_CONTROL = "denylist"
@command(
"execute_python_code",
"Create a Python file and execute it",
'"code": "<code>", "basename": "<basename>"',
)
def execute_python_code(code: str, basename: str, agent: Agent) -> str:
"""Create and execute a Python file in a Docker container and return the STDOUT of the
executed code. If there is any data that needs to be captured use a print statement
Args:
code (str): The Python code to run
basename (str): A name to be given to the Python file
Returns:
str: The STDOUT captured from the code when it ran
"""
ai_name = agent.ai_name
directory = os.path.join(agent.config.workspace_path, ai_name, "executed_code")
os.makedirs(directory, exist_ok=True)
if not basename.endswith(".py"):
basename = basename + ".py"
path = os.path.join(directory, basename)
try:
with open(path, "w+", encoding="utf-8") as f:
f.write(code)
return execute_python_file(f.name, agent)
except Exception as e:
return f"Error: {str(e)}"
@command("execute_python_file", "Execute Python File", '"filename": "<filename>"')
def execute_python_file(filename: str) -> str:
def execute_python_file(filename: str, agent: Agent) -> str:
"""Execute a Python file in a Docker container and return the output
Args:
@@ -23,17 +61,30 @@ def execute_python_file(filename: str) -> str:
Returns:
str: The output of the file
"""
logger.info(f"Executing file '{filename}'")
logger.info(
f"Executing python file '{filename}' in working directory '{CFG.workspace_path}'"
)
if not filename.endswith(".py"):
return "Error: Invalid file type. Only .py files are allowed."
if not os.path.isfile(filename):
return f"Error: File '{filename}' does not exist."
workspace = Workspace(
agent.config.workspace_path, agent.config.restrict_to_workspace
)
path = workspace.get_path(filename)
if not path.is_file():
# Mimic the response that you get from the command line so that it's easier to identify
return (
f"python: can't open file '{filename}': [Errno 2] No such file or directory"
)
if we_are_running_in_a_docker_container():
result = subprocess.run(
f"python {filename}", capture_output=True, encoding="utf8", shell=True
["python", str(path)],
capture_output=True,
encoding="utf8",
cwd=CFG.workspace_path,
)
if result.returncode == 0:
return result.stdout
@@ -65,9 +116,9 @@ def execute_python_file(filename: str) -> str:
logger.info(status)
container = client.containers.run(
image_name,
f"python {Path(filename).relative_to(CFG.workspace_path)}",
["python", str(path.relative_to(workspace.root))],
volumes={
CFG.workspace_path: {
agent.config.workspace_path: {
"bind": "/workspace",
"mode": "ro",
}
@@ -97,16 +148,36 @@ def execute_python_file(filename: str) -> str:
return f"Error: {str(e)}"
def validate_command(command: str, config: Config) -> bool:
"""Validate a command to ensure it is allowed
Args:
command (str): The command to validate
Returns:
bool: True if the command is allowed, False otherwise
"""
if not command:
return False
command_name = command.split()[0]
if config.shell_command_control == ALLOWLIST_CONTROL:
return command_name in config.shell_allowlist
else:
return command_name not in config.shell_denylist
@command(
"execute_shell",
"Execute Shell Command, non-interactive commands only",
'"command_line": "<command_line>"',
CFG.execute_local_commands,
lambda cfg: cfg.execute_local_commands,
"You are not allowed to run local shell commands. To execute"
" shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' "
"in your config. Do not attempt to bypass the restriction.",
"in your config file: .env - do not attempt to bypass the restriction.",
)
def execute_shell(command_line: str) -> str:
def execute_shell(command_line: str, agent: Agent) -> str:
"""Execute a shell command and return the output
Args:
@@ -115,11 +186,14 @@ def execute_shell(command_line: str) -> str:
Returns:
str: The output of the command
"""
if not validate_command(command_line, agent.config):
logger.info(f"Command '{command_line}' not allowed")
return "Error: This Shell Command is not allowed."
current_dir = Path.cwd()
# Change dir into workspace if necessary
if not current_dir.is_relative_to(CFG.workspace_path):
os.chdir(CFG.workspace_path)
if not current_dir.is_relative_to(agent.config.workspace_path):
os.chdir(agent.config.workspace_path)
logger.info(
f"Executing command '{command_line}' in working directory '{os.getcwd()}'"
@@ -138,12 +212,12 @@ def execute_shell(command_line: str) -> str:
"execute_shell_popen",
"Execute Shell Command, non-interactive commands only",
'"command_line": "<command_line>"',
CFG.execute_local_commands,
lambda config: config.execute_local_commands,
"You are not allowed to run local shell commands. To execute"
" shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' "
"in your config. Do not attempt to bypass the restriction.",
)
def execute_shell_popen(command_line) -> str:
def execute_shell_popen(command_line, agent: Agent) -> str:
"""Execute a shell command with Popen and returns an english description
of the event and the process id
@@ -153,11 +227,14 @@ def execute_shell_popen(command_line) -> str:
Returns:
str: Description of the fact that the process started and its id
"""
if not validate_command(command_line, agent.config):
logger.info(f"Command '{command_line}' not allowed")
return "Error: This Shell Command is not allowed."
current_dir = os.getcwd()
# Change dir into workspace if necessary
if CFG.workspace_path not in current_dir:
os.chdir(CFG.workspace_path)
if agent.config.workspace_path not in current_dir:
os.chdir(agent.config.workspace_path)
logger.info(
f"Executing command '{command_line}' in working directory '{os.getcwd()}'"

View File

@@ -1,83 +1,127 @@
"""File operations for AutoGPT"""
from __future__ import annotations
import hashlib
import os
import os.path
from typing import Generator
import re
from typing import Generator, Literal
import requests
from colorama import Back, Fore
from confection import Config
from requests.adapters import HTTPAdapter, Retry
from autogpt.commands.command import command
from autogpt.agent.agent import Agent
from autogpt.commands.command import command, ignore_unexpected_kwargs
from autogpt.commands.file_operations_utils import read_textual_file
from autogpt.config import Config
from autogpt.logs import logger
from autogpt.memory.vector import MemoryItem, VectorMemory
from autogpt.spinner import Spinner
from autogpt.utils import readable_file_size
CFG = Config()
Operation = Literal["write", "append", "delete"]
def check_duplicate_operation(operation: str, filename: str) -> bool:
"""Check if the operation has already been performed on the given file
def text_checksum(text: str) -> str:
"""Get the hex checksum for the given text."""
return hashlib.md5(text.encode("utf-8")).hexdigest()
Args:
operation (str): The operation to check for
filename (str): The name of the file to check for
def operations_from_log(
log_path: str,
) -> Generator[tuple[Operation, str, str | None], None, None]:
"""Parse the file operations log and return a tuple containing the log entries"""
try:
log = open(log_path, "r", encoding="utf-8")
except FileNotFoundError:
return
for line in log:
line = line.replace("File Operation Logger", "").strip()
if not line:
continue
operation, tail = line.split(": ", maxsplit=1)
operation = operation.strip()
if operation in ("write", "append"):
try:
path, checksum = (x.strip() for x in tail.rsplit(" #", maxsplit=1))
except ValueError:
logger.warn(f"File log entry lacks checksum: '{line}'")
path, checksum = tail.strip(), None
yield (operation, path, checksum)
elif operation == "delete":
yield (operation, tail.strip(), None)
log.close()
def file_operations_state(log_path: str) -> dict[str, str]:
"""Iterates over the operations log and returns the expected state.
Parses a log file at config.file_logger_path to construct a dictionary that maps
each file path written or appended to its checksum. Deleted files are removed
from the dictionary.
Returns:
bool: True if the operation has already been performed on the file
A dictionary mapping file paths to their checksums.
Raises:
FileNotFoundError: If config.file_logger_path is not found.
ValueError: If the log file content is not in the expected format.
"""
log_content = read_file(CFG.file_logger_path)
log_entry = f"{operation}: {filename}\n"
return log_entry in log_content
state = {}
for operation, path, checksum in operations_from_log(log_path):
if operation in ("write", "append"):
state[path] = checksum
elif operation == "delete":
del state[path]
return state
def log_operation(operation: str, filename: str) -> None:
def is_duplicate_operation(
operation: Operation, filename: str, config: Config, checksum: str | None = None
) -> bool:
"""Check if the operation has already been performed
Args:
operation: The operation to check for
filename: The name of the file to check for
checksum: The checksum of the contents to be written
Returns:
True if the operation has already been performed on the file
"""
state = file_operations_state(config.file_logger_path)
if operation == "delete" and filename not in state:
return True
if operation == "write" and state.get(filename) == checksum:
return True
return False
def log_operation(
operation: str, filename: str, agent: Agent, checksum: str | None = None
) -> None:
"""Log the file operation to the file_logger.txt
Args:
operation (str): The operation to log
filename (str): The name of the file the operation was performed on
operation: The operation to log
filename: The name of the file the operation was performed on
checksum: The checksum of the contents to be written
"""
log_entry = f"{operation}: {filename}\n"
append_to_file(CFG.file_logger_path, log_entry, should_log=False)
log_entry = f"{operation}: {filename}"
if checksum is not None:
log_entry += f" #{checksum}"
logger.debug(f"Logging file operation: {log_entry}")
append_to_file(
agent.config.file_logger_path, f"{log_entry}\n", agent, should_log=False
)
def split_file(
content: str, max_length: int = 4000, overlap: int = 0
) -> Generator[str, None, None]:
"""
Split text into chunks of a specified maximum length with a specified overlap
between chunks.
:param content: The input text to be split into chunks
:param max_length: The maximum length of each chunk,
default is 4000 (about 1k token)
:param overlap: The number of overlapping characters between chunks,
default is no overlap
:return: A generator yielding chunks of text
"""
start = 0
content_length = len(content)
while start < content_length:
end = start + max_length
if end + overlap < content_length:
chunk = content[start : end + overlap - 1]
else:
chunk = content[start:content_length]
# Account for the case where the last chunk is shorter than the overlap, so it has already been consumed
if len(chunk) <= overlap:
break
yield chunk
start += max_length - overlap
@command("read_file", "Read file", '"filename": "<filename>"')
def read_file(filename: str) -> str:
@command("read_file", "Read a file", '"filename": "<filename>"')
def read_file(filename: str, agent: Agent) -> str:
"""Read a file and return the contents
Args:
@@ -87,49 +131,46 @@ def read_file(filename: str) -> str:
str: The contents of the file
"""
try:
with open(filename, "r", encoding="utf-8") as f:
content = f.read()
content = read_textual_file(filename, logger)
# TODO: invalidate/update memory when file is edited
file_memory = MemoryItem.from_text_file(content, filename)
if len(file_memory.chunks) > 1:
return file_memory.summary
return content
except Exception as e:
return f"Error: {str(e)}"
def ingest_file(
filename: str, memory, max_length: int = 4000, overlap: int = 200
filename: str,
memory: VectorMemory,
) -> None:
"""
Ingest a file by reading its content, splitting it into chunks with a specified
maximum length and overlap, and adding the chunks to the memory storage.
:param filename: The name of the file to ingest
:param memory: An object with an add() method to store the chunks in memory
:param max_length: The maximum length of each chunk, default is 4000
:param overlap: The number of overlapping characters between chunks, default is 200
Args:
filename: The name of the file to ingest
memory: An object with an add() method to store the chunks in memory
"""
try:
logger.info(f"Working with file {filename}")
logger.info(f"Ingesting file {filename}")
content = read_file(filename)
content_length = len(content)
logger.info(f"File length: {content_length} characters")
chunks = list(split_file(content, max_length=max_length, overlap=overlap))
# TODO: differentiate between different types of files
file_memory = MemoryItem.from_text_file(content, filename)
logger.debug(f"Created memory: {file_memory.dump()}")
memory.add(file_memory)
num_chunks = len(chunks)
for i, chunk in enumerate(chunks):
logger.info(f"Ingesting chunk {i + 1} / {num_chunks} into memory")
memory_to_add = (
f"Filename: {filename}\n" f"Content part#{i + 1}/{num_chunks}: {chunk}"
)
memory.add(memory_to_add)
logger.info(f"Done ingesting {num_chunks} chunks from {filename}.")
except Exception as e:
logger.info(f"Error while ingesting file '{filename}': {str(e)}")
logger.info(f"Ingested {len(file_memory.e_chunks)} chunks from {filename}")
except Exception as err:
logger.warn(f"Error while ingesting file '{filename}': {err}")
@command("write_to_file", "Write to file", '"filename": "<filename>", "text": "<text>"')
def write_to_file(filename: str, text: str) -> str:
def write_to_file(filename: str, text: str, agent: Agent) -> str:
"""Write text to a file
Args:
@@ -139,23 +180,88 @@ def write_to_file(filename: str, text: str) -> str:
Returns:
str: A message indicating success or failure
"""
if check_duplicate_operation("write", filename):
checksum = text_checksum(text)
if is_duplicate_operation("write", filename, agent.config, checksum):
return "Error: File has already been updated."
try:
directory = os.path.dirname(filename)
os.makedirs(directory, exist_ok=True)
with open(filename, "w", encoding="utf-8") as f:
f.write(text)
log_operation("write", filename)
log_operation("write", filename, agent, checksum)
return "File written to successfully."
except Exception as err:
return f"Error: {err}"
@command(
"replace_in_file",
"Replace text or code in a file",
'"filename": "<filename>", '
'"old_text": "<old_text>", "new_text": "<new_text>", '
'"occurrence_index": "<occurrence_index>"',
)
def replace_in_file(
filename: str, old_text: str, new_text: str, agent: Agent, occurrence_index=None
):
"""Update a file by replacing one or all occurrences of old_text with new_text using Python's built-in string
manipulation and regular expression modules for cross-platform file editing similar to sed and awk.
Args:
filename (str): The name of the file
old_text (str): String to be replaced. \n will be stripped from the end.
new_text (str): New string. \n will be stripped from the end.
occurrence_index (int): Optional index of the occurrence to replace. If None, all occurrences will be replaced.
Returns:
str: A message indicating whether the file was updated successfully or if there were no matches found for old_text
in the file.
Raises:
Exception: If there was an error updating the file.
"""
try:
with open(filename, "r", encoding="utf-8") as f:
content = f.read()
old_text = old_text.rstrip("\n")
new_text = new_text.rstrip("\n")
if occurrence_index is None:
new_content = content.replace(old_text, new_text)
else:
matches = list(re.finditer(re.escape(old_text), content))
if not matches:
return f"No matches found for {old_text} in {filename}"
if int(occurrence_index) >= len(matches):
return f"Occurrence index {occurrence_index} is out of range for {old_text} in {filename}"
match = matches[int(occurrence_index)]
start, end = match.start(), match.end()
new_content = content[:start] + new_text + content[end:]
if content == new_content:
return f"No matches found for {old_text} in {filename}"
with open(filename, "w", encoding="utf-8") as f:
f.write(new_content)
with open(filename, "r", encoding="utf-8") as f:
checksum = text_checksum(f.read())
log_operation("update", filename, agent, checksum=checksum)
return f"File {filename} updated successfully."
except Exception as e:
return f"Error: {str(e)}"
return "Error: " + str(e)
@command(
"append_to_file", "Append to file", '"filename": "<filename>", "text": "<text>"'
)
def append_to_file(filename: str, text: str, should_log: bool = True) -> str:
def append_to_file(
filename: str, text: str, agent: Agent, should_log: bool = True
) -> str:
"""Append text to a file
Args:
@@ -169,19 +275,21 @@ def append_to_file(filename: str, text: str, should_log: bool = True) -> str:
try:
directory = os.path.dirname(filename)
os.makedirs(directory, exist_ok=True)
with open(filename, "a") as f:
with open(filename, "a", encoding="utf-8") as f:
f.write(text)
if should_log:
log_operation("append", filename)
with open(filename, "r", encoding="utf-8") as f:
checksum = text_checksum(f.read())
log_operation("append", filename, agent, checksum=checksum)
return "Text appended successfully."
except Exception as e:
return f"Error: {str(e)}"
except Exception as err:
return f"Error: {err}"
@command("delete_file", "Delete file", '"filename": "<filename>"')
def delete_file(filename: str) -> str:
def delete_file(filename: str, agent: Agent) -> str:
"""Delete a file
Args:
@@ -190,19 +298,20 @@ def delete_file(filename: str) -> str:
Returns:
str: A message indicating success or failure
"""
if check_duplicate_operation("delete", filename):
if is_duplicate_operation("delete", filename, agent.config):
return "Error: File has already been deleted."
try:
os.remove(filename)
log_operation("delete", filename)
log_operation("delete", filename, agent)
return "File deleted successfully."
except Exception as e:
return f"Error: {str(e)}"
except Exception as err:
return f"Error: {err}"
@command("search_files", "Search Files", '"directory": "<directory>"')
def search_files(directory: str) -> list[str]:
"""Search for files in a directory
@command("list_files", "List Files in Directory", '"directory": "<directory>"')
@ignore_unexpected_kwargs
def list_files(directory: str, agent: Agent) -> list[str]:
"""lists files in a directory recursively
Args:
directory (str): The directory to search in
@@ -217,7 +326,7 @@ def search_files(directory: str) -> list[str]:
if file.startswith("."):
continue
relative_path = os.path.relpath(
os.path.join(root, file), CFG.workspace_path
os.path.join(root, file), agent.config.workspace_path
)
found_files.append(relative_path)
@@ -228,10 +337,10 @@ def search_files(directory: str) -> list[str]:
"download_file",
"Download File",
'"url": "<url>", "filename": "<filename>"',
CFG.allow_downloads,
lambda config: config.allow_downloads,
"Error: You do not have user authorization to download files locally.",
)
def download_file(url, filename):
def download_file(url, filename, agent: Agent):
"""Downloads a file
Args:
url (str): URL of the file to download
@@ -241,7 +350,7 @@ def download_file(url, filename):
directory = os.path.dirname(filename)
os.makedirs(directory, exist_ok=True)
message = f"{Fore.YELLOW}Downloading file from {Back.LIGHTBLUE_EX}{url}{Back.RESET}{Fore.RESET}"
with Spinner(message) as spinner:
with Spinner(message, plain_output=agent.config.plain_output) as spinner:
session = requests.Session()
retry = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(max_retries=retry)
@@ -266,7 +375,7 @@ def download_file(url, filename):
spinner.update_message(f"{message} {progress}")
return f'Successfully downloaded and locally stored file: "{filename}"! (Size: {readable_file_size(downloaded_size)})'
except requests.HTTPError as e:
return f"Got an HTTP Error whilst trying to download file: {e}"
except Exception as e:
return "Error: " + str(e)
except requests.HTTPError as err:
return f"Got an HTTP Error whilst trying to download file: {err}"
except Exception as err:
return f"Error: {err}"

View File

@@ -0,0 +1,161 @@
import json
import os
import charset_normalizer
import docx
import markdown
import PyPDF2
import yaml
from bs4 import BeautifulSoup
from pylatexenc.latex2text import LatexNodes2Text
from autogpt import logs
from autogpt.logs import logger
class ParserStrategy:
def read(self, file_path: str) -> str:
raise NotImplementedError
# Basic text file reading
class TXTParser(ParserStrategy):
def read(self, file_path: str) -> str:
charset_match = charset_normalizer.from_path(file_path).best()
logger.debug(f"Reading '{file_path}' with encoding '{charset_match.encoding}'")
return str(charset_match)
# Reading text from binary file using pdf parser
class PDFParser(ParserStrategy):
def read(self, file_path: str) -> str:
parser = PyPDF2.PdfReader(file_path)
text = ""
for page_idx in range(len(parser.pages)):
text += parser.pages[page_idx].extract_text()
return text
# Reading text from binary file using docs parser
class DOCXParser(ParserStrategy):
def read(self, file_path: str) -> str:
doc_file = docx.Document(file_path)
text = ""
for para in doc_file.paragraphs:
text += para.text
return text
# Reading as dictionary and returning string format
class JSONParser(ParserStrategy):
def read(self, file_path: str) -> str:
with open(file_path, "r") as f:
data = json.load(f)
text = str(data)
return text
class XMLParser(ParserStrategy):
def read(self, file_path: str) -> str:
with open(file_path, "r") as f:
soup = BeautifulSoup(f, "xml")
text = soup.get_text()
return text
# Reading as dictionary and returning string format
class YAMLParser(ParserStrategy):
def read(self, file_path: str) -> str:
with open(file_path, "r") as f:
data = yaml.load(f, Loader=yaml.FullLoader)
text = str(data)
return text
class HTMLParser(ParserStrategy):
def read(self, file_path: str) -> str:
with open(file_path, "r") as f:
soup = BeautifulSoup(f, "html.parser")
text = soup.get_text()
return text
class MarkdownParser(ParserStrategy):
def read(self, file_path: str) -> str:
with open(file_path, "r") as f:
html = markdown.markdown(f.read())
text = "".join(BeautifulSoup(html, "html.parser").findAll(string=True))
return text
class LaTeXParser(ParserStrategy):
def read(self, file_path: str) -> str:
with open(file_path, "r") as f:
latex = f.read()
text = LatexNodes2Text().latex_to_text(latex)
return text
class FileContext:
def __init__(self, parser: ParserStrategy, logger: logs.Logger):
self.parser = parser
self.logger = logger
def set_parser(self, parser: ParserStrategy) -> None:
self.logger.debug(f"Setting Context Parser to {parser}")
self.parser = parser
def read_file(self, file_path) -> str:
self.logger.debug(f"Reading file {file_path} with parser {self.parser}")
return self.parser.read(file_path)
extension_to_parser = {
".txt": TXTParser(),
".csv": TXTParser(),
".pdf": PDFParser(),
".docx": DOCXParser(),
".json": JSONParser(),
".xml": XMLParser(),
".yaml": YAMLParser(),
".yml": YAMLParser(),
".html": HTMLParser(),
".htm": HTMLParser(),
".xhtml": HTMLParser(),
".md": MarkdownParser(),
".markdown": MarkdownParser(),
".tex": LaTeXParser(),
}
def is_file_binary_fn(file_path: str):
"""Given a file path load all its content and checks if the null bytes is present
Args:
file_path (_type_): _description_
Returns:
bool: is_binary
"""
with open(file_path, "rb") as f:
file_data = f.read()
if b"\x00" in file_data:
return True
return False
def read_textual_file(file_path: str, logger: logs.Logger) -> str:
if not os.path.isfile(file_path):
raise FileNotFoundError(
f"read_file {file_path} failed: no such file or directory"
)
is_binary = is_file_binary_fn(file_path)
file_extension = os.path.splitext(file_path)[1].lower()
parser = extension_to_parser.get(file_extension)
if not parser:
if is_binary:
raise ValueError(f"Unsupported binary file format: {file_extension}")
# fallback to txt file parser (to support script and code files loading)
parser = TXTParser()
file_context = FileContext(parser, logger)
return file_context.read_file(file_path)

View File

@@ -1,22 +1,21 @@
"""Git operations for autogpt"""
from git.repo import Repo
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.config import Config
from autogpt.url_utils.validators import validate_url
CFG = Config()
@command(
"clone_repository",
"Clone Repository",
'"url": "<repository_url>", "clone_path": "<clone_path>"',
CFG.github_username and CFG.github_api_key,
lambda config: config.github_username and config.github_api_key,
"Configure github_username and github_api_key.",
)
@validate_url
def clone_repository(url: str, clone_path: str) -> str:
def clone_repository(url: str, clone_path: str, agent: Agent) -> str:
"""Clone a GitHub repository locally.
Args:
@@ -27,7 +26,11 @@ def clone_repository(url: str, clone_path: str) -> str:
str: The result of the clone operation.
"""
split_url = url.split("//")
auth_repo_url = f"//{CFG.github_username}:{CFG.github_api_key}@".join(split_url)
auth_repo_url = (
f"//{agent.config.github_username}:{agent.config.github_api_key}@".join(
split_url
)
)
try:
Repo.clone_from(url=auth_repo_url, to_path=clone_path)
return f"""Cloned {url} to {clone_path}"""

View File

@@ -2,17 +2,24 @@
from __future__ import annotations
import json
import time
from itertools import islice
from duckduckgo_search import ddg
from duckduckgo_search import DDGS
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.config import Config
CFG = Config()
DUCKDUCKGO_MAX_ATTEMPTS = 3
@command("google", "Google Search", '"query": "<query>"', not CFG.google_api_key)
def google_search(query: str, num_results: int = 8) -> str:
@command(
"google",
"Google Search",
'"query": "<query>"',
lambda config: not config.google_api_key,
)
def google_search(query: str, agent: Agent, num_results: int = 8) -> str:
"""Return the results of a Google search
Args:
@@ -23,15 +30,20 @@ def google_search(query: str, num_results: int = 8) -> str:
str: The results of the search.
"""
search_results = []
if not query:
return json.dumps(search_results)
attempts = 0
results = ddg(query, max_results=num_results)
if not results:
return json.dumps(search_results)
while attempts < DUCKDUCKGO_MAX_ATTEMPTS:
if not query:
return json.dumps(search_results)
for j in results:
search_results.append(j)
results = DDGS().text(query)
search_results = list(islice(results, num_results))
if search_results:
break
time.sleep(1)
attempts += 1
results = json.dumps(search_results, ensure_ascii=False, indent=4)
return safe_google_results(results)
@@ -41,10 +53,13 @@ def google_search(query: str, num_results: int = 8) -> str:
"google",
"Google Search",
'"query": "<query>"',
bool(CFG.google_api_key),
"Configure google_api_key.",
lambda config: bool(config.google_api_key)
and bool(config.google_custom_search_engine_id),
"Configure google_api_key and custom_search_engine_id.",
)
def google_official_search(query: str, num_results: int = 8) -> str | list[str]:
def google_official_search(
query: str, agent: Agent, num_results: int = 8
) -> str | list[str]:
"""Return the results of a Google search using the official Google API
Args:
@@ -60,8 +75,8 @@ def google_official_search(query: str, num_results: int = 8) -> str | list[str]:
try:
# Get the Google API key and Custom Search Engine ID from the config file
api_key = CFG.google_api_key
custom_search_engine_id = CFG.custom_search_engine_id
api_key = agent.config.google_api_key
custom_search_engine_id = agent.config.google_custom_search_engine_id
# Initialize the Custom Search API service
service = build("customsearch", "v1", developerKey=api_key)
@@ -110,7 +125,7 @@ def safe_google_results(results: str | list) -> str:
"""
if isinstance(results, list):
safe_message = json.dumps(
[result.encode("utf-8", "ignore") for result in results]
[result.encode("utf-8", "ignore").decode("utf-8") for result in results]
)
else:
safe_message = results.encode("utf-8", "ignore").decode("utf-8")

View File

@@ -1,5 +1,7 @@
""" Image Generation Module for AutoGPT."""
import io
import json
import time
import uuid
from base64 import b64decode
@@ -7,15 +9,19 @@ import openai
import requests
from PIL import Image
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.config import Config
from autogpt.logs import logger
CFG = Config()
@command("generate_image", "Generate Image", '"prompt": "<prompt>"', CFG.image_provider)
def generate_image(prompt: str, size: int = 256) -> str:
@command(
"generate_image",
"Generate Image",
'"prompt": "<prompt>"',
lambda config: config.image_provider,
"Requires a image provider to be set.",
)
def generate_image(prompt: str, agent: Agent, size: int = 256) -> str:
"""Generate an image from a prompt.
Args:
@@ -25,21 +31,21 @@ def generate_image(prompt: str, size: int = 256) -> str:
Returns:
str: The filename of the image
"""
filename = f"{CFG.workspace_path}/{str(uuid.uuid4())}.jpg"
filename = f"{agent.config.workspace_path}/{str(uuid.uuid4())}.jpg"
# DALL-E
if CFG.image_provider == "dalle":
return generate_image_with_dalle(prompt, filename, size)
if agent.config.image_provider == "dalle":
return generate_image_with_dalle(prompt, filename, size, agent)
# HuggingFace
elif CFG.image_provider == "huggingface":
return generate_image_with_hf(prompt, filename)
elif agent.config.image_provider == "huggingface":
return generate_image_with_hf(prompt, filename, agent)
# SD WebUI
elif CFG.image_provider == "sdwebui":
return generate_image_with_sd_webui(prompt, filename, size)
elif agent.config.image_provider == "sdwebui":
return generate_image_with_sd_webui(prompt, filename, agent, size)
return "No Image Provider Set"
def generate_image_with_hf(prompt: str, filename: str) -> str:
def generate_image_with_hf(prompt: str, filename: str, agent: Agent) -> str:
"""Generate an image with HuggingFace's API.
Args:
@@ -49,35 +55,57 @@ def generate_image_with_hf(prompt: str, filename: str) -> str:
Returns:
str: The filename of the image
"""
API_URL = (
f"https://api-inference.huggingface.co/models/{CFG.huggingface_image_model}"
)
if CFG.huggingface_api_token is None:
API_URL = f"https://api-inference.huggingface.co/models/{agent.config.huggingface_image_model}"
if agent.config.huggingface_api_token is None:
raise ValueError(
"You need to set your Hugging Face API token in the config file."
)
headers = {
"Authorization": f"Bearer {CFG.huggingface_api_token}",
"Authorization": f"Bearer {agent.config.huggingface_api_token}",
"X-Use-Cache": "false",
}
response = requests.post(
API_URL,
headers=headers,
json={
"inputs": prompt,
},
)
retry_count = 0
while retry_count < 10:
response = requests.post(
API_URL,
headers=headers,
json={
"inputs": prompt,
},
)
image = Image.open(io.BytesIO(response.content))
logger.info(f"Image Generated for prompt:{prompt}")
if response.ok:
try:
image = Image.open(io.BytesIO(response.content))
logger.info(f"Image Generated for prompt:{prompt}")
image.save(filename)
return f"Saved to disk:{filename}"
except Exception as e:
logger.error(e)
break
else:
try:
error = json.loads(response.text)
if "estimated_time" in error:
delay = error["estimated_time"]
logger.debug(response.text)
logger.info("Retrying in", delay)
time.sleep(delay)
else:
break
except Exception as e:
logger.error(e)
break
image.save(filename)
retry_count += 1
return f"Saved to disk:{filename}"
return f"Error creating image."
def generate_image_with_dalle(prompt: str, filename: str, size: int) -> str:
def generate_image_with_dalle(
prompt: str, filename: str, size: int, agent: Agent
) -> str:
"""Generate an image with DALL-E.
Args:
@@ -102,7 +130,7 @@ def generate_image_with_dalle(prompt: str, filename: str, size: int) -> str:
n=1,
size=f"{size}x{size}",
response_format="b64_json",
api_key=CFG.openai_api_key,
api_key=agent.config.openai_api_key,
)
logger.info(f"Image Generated for prompt:{prompt}")
@@ -118,6 +146,7 @@ def generate_image_with_dalle(prompt: str, filename: str, size: int) -> str:
def generate_image_with_sd_webui(
prompt: str,
filename: str,
agent: Agent,
size: int = 512,
negative_prompt: str = "",
extra: dict = {},
@@ -134,13 +163,13 @@ def generate_image_with_sd_webui(
"""
# Create a session and set the basic auth if needed
s = requests.Session()
if CFG.sd_webui_auth:
username, password = CFG.sd_webui_auth.split(":")
if agent.config.sd_webui_auth:
username, password = agent.config.sd_webui_auth.split(":")
s.auth = (username, password or "")
# Generate the images
response = requests.post(
f"{CFG.sd_webui_url}/sdapi/v1/txt2img",
f"{agent.config.sd_webui_url}/sdapi/v1/txt2img",
json={
"prompt": prompt,
"negative_prompt": negative_prompt,

View File

@@ -2,8 +2,9 @@ from __future__ import annotations
import json
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.llm import call_ai_function
from autogpt.llm.utils import call_ai_function
@command(
@@ -11,7 +12,7 @@ from autogpt.llm import call_ai_function
"Get Improved Code",
'"suggestions": "<list_of_suggestions>", "code": "<full_code_string>"',
)
def improve_code(suggestions: list[str], code: str) -> str:
def improve_code(suggestions: list[str], code: str, agent: Agent) -> str:
"""
A function that takes in code and suggestions and returns a response from create
chat completion api call.
@@ -32,4 +33,6 @@ def improve_code(suggestions: list[str], code: str) -> str:
" provided, making no other changes."
)
return call_ai_function(function_string, args, description_string)
return call_ai_function(
function_string, args, description_string, config=agent.config
)

View File

@@ -0,0 +1,27 @@
"""Task Statuses module."""
from __future__ import annotations
from typing import NoReturn
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.logs import logger
@command(
"task_complete",
"Task Complete (Shutdown)",
'"reason": "<reason>"',
)
def task_complete(reason: str, agent: Agent) -> NoReturn:
"""
A function that takes in a string and exits the program
Parameters:
reason (str): The reason for shutting down.
Returns:
A result string from create chat completion. A list of suggestions to
improve the code.
"""
logger.info(title="Shutting down...\n", message=reason)
quit()

View File

@@ -1,41 +0,0 @@
"""A module that contains a command to send a tweet."""
import os
import tweepy
from autogpt.commands.command import command
@command(
"send_tweet",
"Send Tweet",
'"tweet_text": "<tweet_text>"',
)
def send_tweet(tweet_text: str) -> str:
"""
A function that takes in a string and returns a response from create chat
completion api call.
Args:
tweet_text (str): Text to be tweeted.
Returns:
A result from sending the tweet.
"""
consumer_key = os.environ.get("TW_CONSUMER_KEY")
consumer_secret = os.environ.get("TW_CONSUMER_SECRET")
access_token = os.environ.get("TW_ACCESS_TOKEN")
access_token_secret = os.environ.get("TW_ACCESS_TOKEN_SECRET")
# Authenticate to Twitter
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
# Create API object
api = tweepy.API(auth)
# Send tweet
try:
api.update_status(tweet_text)
return "Tweet sent successfully!"
except tweepy.TweepyException as e:
return f"Error sending tweet: {e.reason}"

View File

@@ -1,23 +1,24 @@
"""Browse a webpage and summarize it using the LLM model"""
from __future__ import annotations
from typing import TYPE_CHECKING
import requests
from bs4 import BeautifulSoup
from requests import Response
from autogpt.config import Config
from autogpt.processing.html import extract_hyperlinks, format_hyperlinks
from autogpt.url_utils.validators import validate_url
CFG = Config()
session = requests.Session()
session.headers.update({"User-Agent": CFG.user_agent})
if TYPE_CHECKING:
from autogpt.agent.agent import Agent
@validate_url
def get_response(
url: str, timeout: int = 10
url: str, agent: Agent, timeout: int = 10
) -> tuple[None, str] | tuple[Response, None]:
"""Get the response from a URL
@@ -33,6 +34,7 @@ def get_response(
requests.exceptions.RequestException: If the HTTP request fails
"""
try:
session.headers.update({"User-Agent": agent.config.user_agent})
response = session.get(url, timeout=timeout)
# Check if the response contains an HTTP error
@@ -50,7 +52,7 @@ def get_response(
return None, f"Error: {str(re)}"
def scrape_text(url: str) -> str:
def scrape_text(url: str, agent: Agent) -> str:
"""Scrape text from a webpage
Args:
@@ -59,7 +61,7 @@ def scrape_text(url: str) -> str:
Returns:
str: The scraped text
"""
response, error_message = get_response(url)
response, error_message = get_response(url, agent)
if error_message:
return error_message
if not response:
@@ -78,7 +80,7 @@ def scrape_text(url: str) -> str:
return text
def scrape_links(url: str) -> str | list[str]:
def scrape_links(url: str, agent: Agent) -> str | list[str]:
"""Scrape links from a webpage
Args:
@@ -87,7 +89,7 @@ def scrape_links(url: str) -> str | list[str]:
Returns:
str | list[str]: The scraped links
"""
response, error_message = get_response(url)
response, error_message = get_response(url, agent)
if error_message:
return error_message
if not response:
@@ -100,13 +102,3 @@ def scrape_links(url: str) -> str | list[str]:
hyperlinks = extract_hyperlinks(soup, url)
return format_hyperlinks(hyperlinks)
def create_message(chunk, question):
"""Create a message for the user to summarize a chunk of text"""
return {
"role": "user",
"content": f'"""{chunk}""" Using the above text, answer the following'
f' question: "{question}" -- if the question cannot be answered using the'
" text, summarize the text.",
}

View File

@@ -4,28 +4,39 @@ from __future__ import annotations
import logging
from pathlib import Path
from sys import platform
from typing import Optional, Type
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.chrome.service import Service as ChromeDriverService
from selenium.webdriver.chrome.webdriver import WebDriver as ChromeDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.edge.options import Options as EdgeOptions
from selenium.webdriver.edge.service import Service as EdgeDriverService
from selenium.webdriver.edge.webdriver import WebDriver as EdgeDriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.firefox.service import Service as GeckoDriverService
from selenium.webdriver.firefox.webdriver import WebDriver as FirefoxDriver
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.safari.options import Options as SafariOptions
from selenium.webdriver.safari.webdriver import WebDriver as SafariDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from webdriver_manager.microsoft import EdgeChromiumDriverManager as EdgeDriverManager
import autogpt.processing.text as summary
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.config import Config
from autogpt.logs import logger
from autogpt.memory.vector import MemoryItem, get_memory
from autogpt.processing.html import extract_hyperlinks, format_hyperlinks
from autogpt.url_utils.validators import validate_url
BrowserOptions = ChromeOptions | EdgeOptions | FirefoxOptions | SafariOptions
FILE_DIR = Path(__file__).parent.parent
CFG = Config()
@command(
@@ -34,7 +45,7 @@ CFG = Config()
'"url": "<url>", "question": "<what_you_want_to_find_on_website>"',
)
@validate_url
def browse_website(url: str, question: str) -> tuple[str, WebDriver]:
def browse_website(url: str, question: str, agent: Agent) -> str:
"""Browse a website and return the answer and links to the user
Args:
@@ -45,25 +56,25 @@ def browse_website(url: str, question: str) -> tuple[str, WebDriver]:
Tuple[str, WebDriver]: The answer and links to the user and the webdriver
"""
try:
driver, text = scrape_text_with_selenium(url)
driver, text = scrape_text_with_selenium(url, agent)
except WebDriverException as e:
# These errors are often quite long and include lots of context.
# Just grab the first line.
msg = e.msg.split("\n")[0]
return f"Error: {msg}", None
return f"Error: {msg}"
add_header(driver)
summary_text = summary.summarize_text(url, text, question, driver)
summary = summarize_memorize_webpage(url, text, question, agent, driver)
links = scrape_links_with_selenium(driver, url)
# Limit links to 5
if len(links) > 5:
links = links[:5]
close_browser(driver)
return f"Answer gathered from website: {summary_text} \n \n Links: {links}", driver
return f"Answer gathered from website: {summary}\n\nLinks: {links}"
def scrape_text_with_selenium(url: str) -> tuple[WebDriver, str]:
def scrape_text_with_selenium(url: str, agent: Agent) -> tuple[WebDriver, str]:
"""Scrape text from a website using selenium
Args:
@@ -74,44 +85,49 @@ def scrape_text_with_selenium(url: str) -> tuple[WebDriver, str]:
"""
logging.getLogger("selenium").setLevel(logging.CRITICAL)
options_available = {
options_available: dict[str, Type[BrowserOptions]] = {
"chrome": ChromeOptions,
"safari": SafariOptions,
"edge": EdgeOptions,
"firefox": FirefoxOptions,
"safari": SafariOptions,
}
options = options_available[CFG.selenium_web_browser]()
options: BrowserOptions = options_available[agent.config.selenium_web_browser]()
options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.49 Safari/537.36"
)
if CFG.selenium_web_browser == "firefox":
if CFG.selenium_headless:
if agent.config.selenium_web_browser == "firefox":
if agent.config.selenium_headless:
options.headless = True
options.add_argument("--disable-gpu")
driver = webdriver.Firefox(
executable_path=GeckoDriverManager().install(), options=options
driver = FirefoxDriver(
service=GeckoDriverService(GeckoDriverManager().install()), options=options
)
elif CFG.selenium_web_browser == "safari":
elif agent.config.selenium_web_browser == "edge":
driver = EdgeDriver(
service=EdgeDriverService(EdgeDriverManager().install()), options=options
)
elif agent.config.selenium_web_browser == "safari":
# Requires a bit more setup on the users end
# See https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari
driver = webdriver.Safari(options=options)
driver = SafariDriver(options=options)
else:
if platform == "linux" or platform == "linux2":
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--remote-debugging-port=9222")
options.add_argument("--no-sandbox")
if CFG.selenium_headless:
if agent.config.selenium_headless:
options.add_argument("--headless=new")
options.add_argument("--disable-gpu")
chromium_driver_path = Path("/usr/bin/chromedriver")
driver = webdriver.Chrome(
executable_path=chromium_driver_path
driver = ChromeDriver(
service=ChromeDriverService(str(chromium_driver_path))
if chromium_driver_path.exists()
else ChromeDriverManager().install(),
else ChromeDriverService(ChromeDriverManager().install()),
options=options,
)
driver.get(url)
@@ -175,4 +191,40 @@ def add_header(driver: WebDriver) -> None:
Returns:
None
"""
driver.execute_script(open(f"{FILE_DIR}/js/overlay.js", "r").read())
try:
with open(f"{FILE_DIR}/js/overlay.js", "r") as overlay_file:
overlay_script = overlay_file.read()
driver.execute_script(overlay_script)
except Exception as e:
print(f"Error executing overlay.js: {e}")
def summarize_memorize_webpage(
url: str,
text: str,
question: str,
agent: Agent,
driver: Optional[WebDriver] = None,
) -> str:
"""Summarize text using the OpenAI API
Args:
url (str): The url of the text
text (str): The text to summarize
question (str): The question to ask the model
driver (WebDriver): The webdriver to use to scroll the page
Returns:
str: The summary of the text
"""
if not text:
return "Error: No text to summarize"
text_length = len(text)
logger.info(f"Text length: {text_length} characters")
memory = get_memory(agent.config)
new_memory = MemoryItem.from_webpage(text, url, question=question)
memory.add(new_memory)
return new_memory.summary

View File

@@ -3,8 +3,9 @@ from __future__ import annotations
import json
from autogpt.agent.agent import Agent
from autogpt.commands.command import command
from autogpt.llm import call_ai_function
from autogpt.llm.utils import call_ai_function
@command(
@@ -12,7 +13,7 @@ from autogpt.llm import call_ai_function
"Write Tests",
'"code": "<full_code_string>", "focus": "<list_of_focus_areas>"',
)
def write_tests(code: str, focus: list[str]) -> str:
def write_tests(code: str, focus: list[str], agent: Agent) -> str:
"""
A function that takes in code and focus topics and returns a response from create
chat completion api call.
@@ -34,4 +35,6 @@ def write_tests(code: str, focus: list[str]) -> str:
" specific areas if required."
)
return call_ai_function(function_string, args, description_string)
return call_ai_function(
function_string, args, description_string, config=agent.config
)

View File

@@ -7,12 +7,14 @@ from __future__ import annotations
import os
import platform
from pathlib import Path
from typing import Any, Optional, Type
from typing import TYPE_CHECKING, Optional
import distro
import yaml
from autogpt.prompts.generator import PromptGenerator
if TYPE_CHECKING:
from autogpt.commands.command import CommandRegistry
from autogpt.prompts.generator import PromptGenerator
# Soon this will go in a folder where it remembers more stuff about the run(s)
SAVE_FILE = str(Path(os.getcwd()) / "ai_settings.yaml")
@@ -53,8 +55,8 @@ class AIConfig:
self.ai_role = ai_role
self.ai_goals = ai_goals
self.api_budget = api_budget
self.prompt_generator = None
self.command_registry = None
self.prompt_generator: PromptGenerator | None = None
self.command_registry: CommandRegistry | None = None
@staticmethod
def load(config_file: str = SAVE_FILE) -> "AIConfig":
@@ -73,7 +75,7 @@ class AIConfig:
try:
with open(config_file, encoding="utf-8") as file:
config_params = yaml.load(file, Loader=yaml.FullLoader)
config_params = yaml.load(file, Loader=yaml.FullLoader) or {}
except FileNotFoundError:
config_params = {}

View File

@@ -7,6 +7,7 @@ import yaml
from auto_gpt_plugin_template import AutoGPTPluginTemplate
from colorama import Fore
import autogpt
from autogpt.singleton import Singleton
@@ -17,8 +18,8 @@ class Config(metaclass=Singleton):
def __init__(self) -> None:
"""Initialize the Config class"""
self.workspace_path = None
self.file_logger_path = None
self.workspace_path: str = None
self.file_logger_path: str = None
self.debug_mode = False
self.continuous_mode = False
@@ -30,17 +31,44 @@ class Config(metaclass=Singleton):
self.authorise_key = os.getenv("AUTHORISE_COMMAND_KEY", "y")
self.exit_key = os.getenv("EXIT_KEY", "n")
self.plain_output = os.getenv("PLAIN_OUTPUT", "False") == "True"
disabled_command_categories = os.getenv("DISABLED_COMMAND_CATEGORIES")
if disabled_command_categories:
self.disabled_command_categories = disabled_command_categories.split(",")
else:
self.disabled_command_categories = []
self.shell_command_control = os.getenv("SHELL_COMMAND_CONTROL", "denylist")
# DENY_COMMANDS is deprecated and included for backwards-compatibility
shell_denylist = os.getenv("SHELL_DENYLIST", os.getenv("DENY_COMMANDS"))
if shell_denylist:
self.shell_denylist = shell_denylist.split(",")
else:
self.shell_denylist = ["sudo", "su"]
# ALLOW_COMMANDS is deprecated and included for backwards-compatibility
shell_allowlist = os.getenv("SHELL_ALLOWLIST", os.getenv("ALLOW_COMMANDS"))
if shell_allowlist:
self.shell_allowlist = shell_allowlist.split(",")
else:
self.shell_allowlist = []
self.ai_settings_file = os.getenv("AI_SETTINGS_FILE", "ai_settings.yaml")
self.prompt_settings_file = os.getenv(
"PROMPT_SETTINGS_FILE", "prompt_settings.yaml"
)
self.fast_llm_model = os.getenv("FAST_LLM_MODEL", "gpt-3.5-turbo")
self.smart_llm_model = os.getenv("SMART_LLM_MODEL", "gpt-4")
self.fast_token_limit = int(os.getenv("FAST_TOKEN_LIMIT", 4000))
self.smart_token_limit = int(os.getenv("SMART_TOKEN_LIMIT", 8000))
self.browse_chunk_max_length = int(os.getenv("BROWSE_CHUNK_MAX_LENGTH", 3000))
self.smart_llm_model = os.getenv("SMART_LLM_MODEL", "gpt-3.5-turbo")
self.embedding_model = os.getenv("EMBEDDING_MODEL", "text-embedding-ada-002")
self.browse_spacy_language_model = os.getenv(
"BROWSE_SPACY_LANGUAGE_MODEL", "en_core_web_sm"
)
self.openai_api_key = os.getenv("OPENAI_API_KEY")
self.openai_organization = os.getenv("OPENAI_ORGANIZATION")
self.temperature = float(os.getenv("TEMPERATURE", "0"))
self.use_azure = os.getenv("USE_AZURE") == "True"
self.execute_local_commands = (
@@ -55,53 +83,49 @@ class Config(metaclass=Singleton):
openai.api_type = self.openai_api_type
openai.api_base = self.openai_api_base
openai.api_version = self.openai_api_version
elif os.getenv("OPENAI_API_BASE_URL", None):
openai.api_base = os.getenv("OPENAI_API_BASE_URL")
if self.openai_organization is not None:
openai.organization = self.openai_organization
self.elevenlabs_api_key = os.getenv("ELEVENLABS_API_KEY")
self.elevenlabs_voice_1_id = os.getenv("ELEVENLABS_VOICE_1_ID")
self.elevenlabs_voice_2_id = os.getenv("ELEVENLABS_VOICE_2_ID")
# ELEVENLABS_VOICE_1_ID is deprecated and included for backwards-compatibility
self.elevenlabs_voice_id = os.getenv(
"ELEVENLABS_VOICE_ID", os.getenv("ELEVENLABS_VOICE_1_ID")
)
self.streamelements_voice = os.getenv("STREAMELEMENTS_VOICE", "Brian")
self.use_mac_os_tts = False
self.use_mac_os_tts = os.getenv("USE_MAC_OS_TTS")
# Backwards-compatibility shim for deprecated env variables
if os.getenv("USE_MAC_OS_TTS"):
default_tts_provider = "macos"
elif self.elevenlabs_api_key:
default_tts_provider = "elevenlabs"
elif os.getenv("USE_BRIAN_TTS"):
default_tts_provider = "streamelements"
else:
default_tts_provider = "gtts"
self.chat_messages_enabled = os.getenv("CHAT_MESSAGES_ENABLED") == "True"
self.use_brian_tts = False
self.use_brian_tts = os.getenv("USE_BRIAN_TTS")
self.text_to_speech_provider = os.getenv(
"TEXT_TO_SPEECH_PROVIDER", default_tts_provider
)
self.github_api_key = os.getenv("GITHUB_API_KEY")
self.github_username = os.getenv("GITHUB_USERNAME")
self.google_api_key = os.getenv("GOOGLE_API_KEY")
self.custom_search_engine_id = os.getenv("CUSTOM_SEARCH_ENGINE_ID")
self.pinecone_api_key = os.getenv("PINECONE_API_KEY")
self.pinecone_region = os.getenv("PINECONE_ENV")
self.weaviate_host = os.getenv("WEAVIATE_HOST")
self.weaviate_port = os.getenv("WEAVIATE_PORT")
self.weaviate_protocol = os.getenv("WEAVIATE_PROTOCOL", "http")
self.weaviate_username = os.getenv("WEAVIATE_USERNAME", None)
self.weaviate_password = os.getenv("WEAVIATE_PASSWORD", None)
self.weaviate_scopes = os.getenv("WEAVIATE_SCOPES", None)
self.weaviate_embedded_path = os.getenv("WEAVIATE_EMBEDDED_PATH")
self.weaviate_api_key = os.getenv("WEAVIATE_API_KEY", None)
self.use_weaviate_embedded = (
os.getenv("USE_WEAVIATE_EMBEDDED", "False") == "True"
# CUSTOM_SEARCH_ENGINE_ID is deprecated and included for backwards-compatibility
self.google_custom_search_engine_id = os.getenv(
"GOOGLE_CUSTOM_SEARCH_ENGINE_ID", os.getenv("CUSTOM_SEARCH_ENGINE_ID")
)
# milvus or zilliz cloud configuration.
self.milvus_addr = os.getenv("MILVUS_ADDR", "localhost:19530")
self.milvus_username = os.getenv("MILVUS_USERNAME")
self.milvus_password = os.getenv("MILVUS_PASSWORD")
self.milvus_collection = os.getenv("MILVUS_COLLECTION", "autogpt")
self.milvus_secure = os.getenv("MILVUS_SECURE") == "True"
self.image_provider = os.getenv("IMAGE_PROVIDER")
self.image_size = int(os.getenv("IMAGE_SIZE", 256))
self.huggingface_api_token = os.getenv("HUGGINGFACE_API_TOKEN")
self.huggingface_image_model = os.getenv(
"HUGGINGFACE_IMAGE_MODEL", "CompVis/stable-diffusion-v1-4"
)
self.audio_to_text_provider = os.getenv("AUDIO_TO_TEXT_PROVIDER", "huggingface")
self.huggingface_audio_to_text_model = os.getenv(
"HUGGINGFACE_AUDIO_TO_TEXT_MODEL"
)
@@ -121,25 +145,48 @@ class Config(metaclass=Singleton):
" (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
)
self.memory_backend = os.getenv("MEMORY_BACKEND", "json_file")
self.memory_index = os.getenv("MEMORY_INDEX", "auto-gpt-memory")
self.redis_host = os.getenv("REDIS_HOST", "localhost")
self.redis_port = os.getenv("REDIS_PORT", "6379")
self.redis_port = int(os.getenv("REDIS_PORT", "6379"))
self.redis_password = os.getenv("REDIS_PASSWORD", "")
self.wipe_redis_on_start = os.getenv("WIPE_REDIS_ON_START", "True") == "True"
self.memory_index = os.getenv("MEMORY_INDEX", "auto-gpt")
# Note that indexes must be created on db 0 in redis, this is not configurable.
self.memory_backend = os.getenv("MEMORY_BACKEND", "local")
self.plugins_dir = os.getenv("PLUGINS_DIR", "plugins")
self.plugins: List[AutoGPTPluginTemplate] = []
self.plugins_openai = []
# Deprecated. Kept for backwards-compatibility. Will remove in a future version.
plugins_allowlist = os.getenv("ALLOWLISTED_PLUGINS")
if plugins_allowlist:
self.plugins_allowlist = plugins_allowlist.split(",")
else:
self.plugins_allowlist = []
self.plugins_denylist = []
# Deprecated. Kept for backwards-compatibility. Will remove in a future version.
plugins_denylist = os.getenv("DENYLISTED_PLUGINS")
if plugins_denylist:
self.plugins_denylist = plugins_denylist.split(",")
else:
self.plugins_denylist = []
# Avoid circular imports
from autogpt.plugins import DEFAULT_PLUGINS_CONFIG_FILE
self.plugins_config_file = os.getenv(
"PLUGINS_CONFIG_FILE", DEFAULT_PLUGINS_CONFIG_FILE
)
self.load_plugins_config()
self.chat_messages_enabled = os.getenv("CHAT_MESSAGES_ENABLED") == "True"
def load_plugins_config(self) -> "autogpt.plugins.PluginsConfig":
# Avoid circular import
from autogpt.plugins.plugins_config import PluginsConfig
self.plugins_config = PluginsConfig.load_config(global_config=self)
return self.plugins_config
def get_azure_deployment_id_for_model(self, model: str) -> str:
"""
@@ -180,7 +227,7 @@ class Config(metaclass=Singleton):
None
"""
with open(config_file) as file:
config_params = yaml.load(file, Loader=yaml.FullLoader)
config_params = yaml.load(file, Loader=yaml.FullLoader) or {}
self.openai_api_type = config_params.get("azure_api_type") or "azure"
self.openai_api_base = config_params.get("azure_api_base") or ""
self.openai_api_version = (
@@ -208,17 +255,9 @@ class Config(metaclass=Singleton):
"""Set the smart LLM model value."""
self.smart_llm_model = value
def set_fast_token_limit(self, value: int) -> None:
"""Set the fast token limit value."""
self.fast_token_limit = value
def set_smart_token_limit(self, value: int) -> None:
"""Set the smart token limit value."""
self.smart_token_limit = value
def set_browse_chunk_max_length(self, value: int) -> None:
"""Set the browse_website command chunk max length value."""
self.browse_chunk_max_length = value
def set_embedding_model(self, value: str) -> None:
"""Set the model to use for creating embeddings."""
self.embedding_model = value
def set_openai_api_key(self, value: str) -> None:
"""Set the OpenAI API key value."""
@@ -230,7 +269,7 @@ class Config(metaclass=Singleton):
def set_elevenlabs_voice_1_id(self, value: str) -> None:
"""Set the ElevenLabs Voice 1 ID value."""
self.elevenlabs_voice_1_id = value
self.elevenlabs_voice_id = value
def set_elevenlabs_voice_2_id(self, value: str) -> None:
"""Set the ElevenLabs Voice 2 ID value."""
@@ -242,15 +281,7 @@ class Config(metaclass=Singleton):
def set_custom_search_engine_id(self, value: str) -> None:
"""Set the custom search engine id value."""
self.custom_search_engine_id = value
def set_pinecone_api_key(self, value: str) -> None:
"""Set the Pinecone API key value."""
self.pinecone_api_key = value
def set_pinecone_region(self, value: str) -> None:
"""Set the Pinecone region value."""
self.pinecone_region = value
self.google_custom_search_engine_id = value
def set_debug_mode(self, value: bool) -> None:
"""Set the debug mode value."""

View File

@@ -0,0 +1,53 @@
# sourcery skip: do-not-use-staticmethod
"""
A module that contains the PromptConfig class object that contains the configuration
"""
import yaml
from colorama import Fore
from autogpt import utils
from autogpt.config.config import Config
from autogpt.logs import logger
CFG = Config()
class PromptConfig:
"""
A class object that contains the configuration information for the prompt, which will be used by the prompt generator
Attributes:
constraints (list): Constraints list for the prompt generator.
resources (list): Resources list for the prompt generator.
performance_evaluations (list): Performance evaluation list for the prompt generator.
"""
def __init__(
self,
config_file: str = CFG.prompt_settings_file,
) -> None:
"""
Initialize a class instance with parameters (constraints, resources, performance_evaluations) loaded from
yaml file if yaml file exists,
else raises error.
Parameters:
constraints (list): Constraints list for the prompt generator.
resources (list): Resources list for the prompt generator.
performance_evaluations (list): Performance evaluation list for the prompt generator.
Returns:
None
"""
# Validate file
(validated, message) = utils.validate_yaml_file(config_file)
if not validated:
logger.typewriter_log("FAILED FILE VALIDATION", Fore.RED, message)
logger.double_check()
exit(1)
with open(config_file, encoding="utf-8") as file:
config_params = yaml.load(file, Loader=yaml.FullLoader)
self.constraints = config_params.get("constraints", [])
self.resources = config_params.get("resources", [])
self.performance_evaluations = config_params.get("performance_evaluations", [])

View File

@@ -1,19 +1,29 @@
"""Configurator module."""
from __future__ import annotations
from typing import TYPE_CHECKING
import click
from colorama import Back, Fore, Style
from autogpt import utils
from autogpt.config import Config
from autogpt.llm.utils import check_model
from autogpt.logs import logger
from autogpt.memory import get_supported_memory_backends
from autogpt.memory.vector import get_supported_memory_backends
CFG = Config()
if TYPE_CHECKING:
from autogpt.config import Config
GPT_4_MODEL = "gpt-4"
GPT_3_MODEL = "gpt-3.5-turbo"
def create_config(
config: Config,
continuous: bool,
continuous_limit: int,
ai_settings_file: str,
prompt_settings_file: str,
skip_reprompt: bool,
speak: bool,
debug: bool,
@@ -30,6 +40,7 @@ def create_config(
continuous (bool): Whether to run in continuous mode
continuous_limit (int): The number of times to run in continuous mode
ai_settings_file (str): The path to the ai_settings.yaml file
prompt_settings_file (str): The path to the prompt_settings.yaml file
skip_reprompt (bool): Whether to skip the re-prompting messages at the beginning of the script
speak (bool): Whether to enable speak mode
debug (bool): Whether to enable debug mode
@@ -40,13 +51,13 @@ def create_config(
allow_downloads (bool): Whether to allow Auto-GPT to download files natively
skips_news (bool): Whether to suppress the output of latest news on startup
"""
CFG.set_debug_mode(False)
CFG.set_continuous_mode(False)
CFG.set_speak_mode(False)
config.set_debug_mode(False)
config.set_continuous_mode(False)
config.set_speak_mode(False)
if debug:
logger.typewriter_log("Debug Mode: ", Fore.GREEN, "ENABLED")
CFG.set_debug_mode(True)
config.set_debug_mode(True)
if continuous:
logger.typewriter_log("Continuous Mode: ", Fore.RED, "ENABLED")
@@ -57,13 +68,13 @@ def create_config(
" cause your AI to run forever or carry out actions you would not usually"
" authorise. Use at your own risk.",
)
CFG.set_continuous_mode(True)
config.set_continuous_mode(True)
if continuous_limit:
logger.typewriter_log(
"Continuous Limit: ", Fore.GREEN, f"{continuous_limit}"
)
CFG.set_continuous_limit(continuous_limit)
config.set_continuous_limit(continuous_limit)
# Check if continuous limit is used without continuous mode
if continuous_limit and not continuous:
@@ -71,15 +82,28 @@ def create_config(
if speak:
logger.typewriter_log("Speak Mode: ", Fore.GREEN, "ENABLED")
CFG.set_speak_mode(True)
config.set_speak_mode(True)
# Set the default LLM models
if gpt3only:
logger.typewriter_log("GPT3.5 Only Mode: ", Fore.GREEN, "ENABLED")
CFG.set_smart_llm_model(CFG.fast_llm_model)
# --gpt3only should always use gpt-3.5-turbo, despite user's FAST_LLM_MODEL config
config.set_fast_llm_model(GPT_3_MODEL)
config.set_smart_llm_model(GPT_3_MODEL)
if gpt4only:
elif (
gpt4only
and check_model(GPT_4_MODEL, model_type="smart_llm_model") == GPT_4_MODEL
):
logger.typewriter_log("GPT4 Only Mode: ", Fore.GREEN, "ENABLED")
CFG.set_fast_llm_model(CFG.smart_llm_model)
# --gpt4only should always use gpt-4, despite user's SMART_LLM_MODEL config
config.set_fast_llm_model(GPT_4_MODEL)
config.set_smart_llm_model(GPT_4_MODEL)
else:
config.set_fast_llm_model(check_model(config.fast_llm_model, "fast_llm_model"))
config.set_smart_llm_model(
check_model(config.smart_llm_model, "smart_llm_model")
)
if memory_type:
supported_memory = get_supported_memory_backends()
@@ -90,13 +114,13 @@ def create_config(
Fore.RED,
f"{supported_memory}",
)
logger.typewriter_log("Defaulting to: ", Fore.YELLOW, CFG.memory_backend)
logger.typewriter_log("Defaulting to: ", Fore.YELLOW, config.memory_backend)
else:
CFG.memory_backend = chosen
config.memory_backend = chosen
if skip_reprompt:
logger.typewriter_log("Skip Re-prompt: ", Fore.GREEN, "ENABLED")
CFG.skip_reprompt = True
config.skip_reprompt = True
if ai_settings_file:
file = ai_settings_file
@@ -109,11 +133,24 @@ def create_config(
exit(1)
logger.typewriter_log("Using AI Settings File:", Fore.GREEN, file)
CFG.ai_settings_file = file
CFG.skip_reprompt = True
config.ai_settings_file = file
config.skip_reprompt = True
if prompt_settings_file:
file = prompt_settings_file
# Validate file
(validated, message) = utils.validate_yaml_file(file)
if not validated:
logger.typewriter_log("FAILED FILE VALIDATION", Fore.RED, message)
logger.double_check()
exit(1)
logger.typewriter_log("Using Prompt Settings File:", Fore.GREEN, file)
config.prompt_settings_file = file
if browser_name:
CFG.selenium_web_browser = browser_name
config.selenium_web_browser = browser_name
if allow_downloads:
logger.typewriter_log("Native Downloading:", Fore.GREEN, "ENABLED")
@@ -128,7 +165,7 @@ def create_config(
Fore.YELLOW,
f"{Back.RED + Style.BRIGHT}ALWAYS REMEMBER TO NEVER OPEN FILES YOU AREN'T SURE OF!{Style.RESET_ALL}",
)
CFG.allow_downloads = True
config.allow_downloads = True
if skip_news:
CFG.skip_news = True
config.skip_news = True

View File

@@ -1,121 +0,0 @@
"""This module contains functions to fix JSON strings using general programmatic approaches, suitable for addressing
common JSON formatting issues."""
from __future__ import annotations
import contextlib
import json
import re
from typing import Optional
from autogpt.config import Config
from autogpt.json_utils.utilities import extract_char_position
from autogpt.logs import logger
CFG = Config()
def fix_invalid_escape(json_to_load: str, error_message: str) -> str:
"""Fix invalid escape sequences in JSON strings.
Args:
json_to_load (str): The JSON string.
error_message (str): The error message from the JSONDecodeError
exception.
Returns:
str: The JSON string with invalid escape sequences fixed.
"""
while error_message.startswith("Invalid \\escape"):
bad_escape_location = extract_char_position(error_message)
json_to_load = (
json_to_load[:bad_escape_location] + json_to_load[bad_escape_location + 1 :]
)
try:
json.loads(json_to_load)
return json_to_load
except json.JSONDecodeError as e:
logger.debug("json loads error - fix invalid escape", e)
error_message = str(e)
return json_to_load
def balance_braces(json_string: str) -> Optional[str]:
"""
Balance the braces in a JSON string.
Args:
json_string (str): The JSON string.
Returns:
str: The JSON string with braces balanced.
"""
open_braces_count = json_string.count("{")
close_braces_count = json_string.count("}")
while open_braces_count > close_braces_count:
json_string += "}"
close_braces_count += 1
while close_braces_count > open_braces_count:
json_string = json_string.rstrip("}")
close_braces_count -= 1
with contextlib.suppress(json.JSONDecodeError):
json.loads(json_string)
return json_string
def add_quotes_to_property_names(json_string: str) -> str:
"""
Add quotes to property names in a JSON string.
Args:
json_string (str): The JSON string.
Returns:
str: The JSON string with quotes added to property names.
"""
def replace_func(match: re.Match) -> str:
return f'"{match[1]}":'
property_name_pattern = re.compile(r"(\w+):")
corrected_json_string = property_name_pattern.sub(replace_func, json_string)
try:
json.loads(corrected_json_string)
return corrected_json_string
except json.JSONDecodeError as e:
raise e
def correct_json(json_to_load: str) -> str:
"""
Correct common JSON errors.
Args:
json_to_load (str): The JSON string.
"""
try:
logger.debug("json", json_to_load)
json.loads(json_to_load)
return json_to_load
except json.JSONDecodeError as e:
logger.debug("json loads error", e)
error_message = str(e)
if error_message.startswith("Invalid \\escape"):
json_to_load = fix_invalid_escape(json_to_load, error_message)
if error_message.startswith(
"Expecting property name enclosed in double quotes"
):
json_to_load = add_quotes_to_property_names(json_to_load)
try:
json.loads(json_to_load)
return json_to_load
except json.JSONDecodeError as e:
logger.debug("json loads error - add quotes", e)
error_message = str(e)
if balanced_str := balance_braces(json_to_load):
return balanced_str
return json_to_load

View File

@@ -1,239 +0,0 @@
"""This module contains functions to fix JSON strings generated by LLM models, such as ChatGPT, using the assistance
of the ChatGPT API or LLM models."""
from __future__ import annotations
import contextlib
import json
from typing import Any, Dict
from colorama import Fore
from regex import regex
from autogpt.config import Config
from autogpt.json_utils.json_fix_general import correct_json
from autogpt.llm import call_ai_function
from autogpt.logs import logger
from autogpt.speech import say_text
JSON_SCHEMA = """
{
"command": {
"name": "command name",
"args": {
"arg name": "value"
}
},
"thoughts":
{
"text": "thought",
"reasoning": "reasoning",
"plan": "- short bulleted\n- list that conveys\n- long-term plan",
"criticism": "constructive self-criticism",
"speak": "thoughts summary to say to user"
}
}
"""
CFG = Config()
def auto_fix_json(json_string: str, schema: str) -> str:
"""Fix the given JSON string to make it parseable and fully compliant with
the provided schema using GPT-3.
Args:
json_string (str): The JSON string to fix.
schema (str): The schema to use to fix the JSON.
Returns:
str: The fixed JSON string.
"""
# Try to fix the JSON using GPT:
function_string = "def fix_json(json_string: str, schema:str=None) -> str:"
args = [f"'''{json_string}'''", f"'''{schema}'''"]
description_string = (
"This function takes a JSON string and ensures that it"
" is parseable and fully compliant with the provided schema. If an object"
" or field specified in the schema isn't contained within the correct JSON,"
" it is omitted. The function also escapes any double quotes within JSON"
" string values to ensure that they are valid. If the JSON string contains"
" any None or NaN values, they are replaced with null before being parsed."
)
# If it doesn't already start with a "`", add one:
if not json_string.startswith("`"):
json_string = "```json\n" + json_string + "\n```"
result_string = call_ai_function(
function_string, args, description_string, model=CFG.fast_llm_model
)
logger.debug("------------ JSON FIX ATTEMPT ---------------")
logger.debug(f"Original JSON: {json_string}")
logger.debug("-----------")
logger.debug(f"Fixed JSON: {result_string}")
logger.debug("----------- END OF FIX ATTEMPT ----------------")
try:
json.loads(result_string) # just check the validity
return result_string
except json.JSONDecodeError: # noqa: E722
# Get the call stack:
# import traceback
# call_stack = traceback.format_exc()
# print(f"Failed to fix JSON: '{json_string}' "+call_stack)
return "failed"
def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]:
"""Fix the given JSON string to make it parseable and fully compliant with two techniques.
Args:
json_string (str): The JSON string to fix.
Returns:
str: The fixed JSON string.
"""
assistant_reply = assistant_reply.strip()
if assistant_reply.startswith("```json"):
assistant_reply = assistant_reply[7:]
if assistant_reply.endswith("```"):
assistant_reply = assistant_reply[:-3]
try:
return json.loads(assistant_reply) # just check the validity
except json.JSONDecodeError: # noqa: E722
pass
if assistant_reply.startswith("json "):
assistant_reply = assistant_reply[5:]
assistant_reply = assistant_reply.strip()
try:
return json.loads(assistant_reply) # just check the validity
except json.JSONDecodeError: # noqa: E722
pass
# Parse and print Assistant response
assistant_reply_json = fix_and_parse_json(assistant_reply)
logger.debug("Assistant reply JSON: %s", str(assistant_reply_json))
if assistant_reply_json == {}:
assistant_reply_json = attempt_to_fix_json_by_finding_outermost_brackets(
assistant_reply
)
logger.debug("Assistant reply JSON 2: %s", str(assistant_reply_json))
if assistant_reply_json != {}:
return assistant_reply_json
logger.error(
"Error: The following AI output couldn't be converted to a JSON:\n",
assistant_reply,
)
if CFG.speak_mode:
say_text("I have received an invalid JSON response from the OpenAI API.")
return {}
def fix_and_parse_json(
json_to_load: str, try_to_fix_with_gpt: bool = True
) -> Dict[Any, Any]:
"""Fix and parse JSON string
Args:
json_to_load (str): The JSON string.
try_to_fix_with_gpt (bool, optional): Try to fix the JSON with GPT.
Defaults to True.
Returns:
str or dict[Any, Any]: The parsed JSON.
"""
with contextlib.suppress(json.JSONDecodeError):
json_to_load = json_to_load.replace("\t", "")
return json.loads(json_to_load)
with contextlib.suppress(json.JSONDecodeError):
json_to_load = correct_json(json_to_load)
return json.loads(json_to_load)
# Let's do something manually:
# sometimes GPT responds with something BEFORE the braces:
# "I'm sorry, I don't understand. Please try again."
# {"text": "I'm sorry, I don't understand. Please try again.",
# "confidence": 0.0}
# So let's try to find the first brace and then parse the rest
# of the string
try:
brace_index = json_to_load.index("{")
maybe_fixed_json = json_to_load[brace_index:]
last_brace_index = maybe_fixed_json.rindex("}")
maybe_fixed_json = maybe_fixed_json[: last_brace_index + 1]
return json.loads(maybe_fixed_json)
except (json.JSONDecodeError, ValueError) as e:
return try_ai_fix(try_to_fix_with_gpt, e, json_to_load)
def try_ai_fix(
try_to_fix_with_gpt: bool, exception: Exception, json_to_load: str
) -> Dict[Any, Any]:
"""Try to fix the JSON with the AI
Args:
try_to_fix_with_gpt (bool): Whether to try to fix the JSON with the AI.
exception (Exception): The exception that was raised.
json_to_load (str): The JSON string to load.
Raises:
exception: If try_to_fix_with_gpt is False.
Returns:
str or dict[Any, Any]: The JSON string or dictionary.
"""
if not try_to_fix_with_gpt:
raise exception
if CFG.debug_mode:
logger.warn(
"Warning: Failed to parse AI output, attempting to fix."
"\n If you see this warning frequently, it's likely that"
" your prompt is confusing the AI. Try changing it up"
" slightly."
)
# Now try to fix this up using the ai_functions
ai_fixed_json = auto_fix_json(json_to_load, JSON_SCHEMA)
if ai_fixed_json != "failed":
return json.loads(ai_fixed_json)
# This allows the AI to react to the error message,
# which usually results in it correcting its ways.
# logger.error("Failed to fix AI output, telling the AI.")
return {}
def attempt_to_fix_json_by_finding_outermost_brackets(json_string: str):
if CFG.speak_mode and CFG.debug_mode:
say_text(
"I have received an invalid JSON response from the OpenAI API. "
"Trying to fix it now."
)
logger.error("Attempting to fix JSON by finding outermost brackets\n")
try:
json_pattern = regex.compile(r"\{(?:[^{}]|(?R))*\}")
json_match = json_pattern.search(json_string)
if json_match:
# Extract the valid JSON object from the string
json_string = json_match.group(0)
logger.typewriter_log(
title="Apparently json was fixed.", title_color=Fore.GREEN
)
if CFG.speak_mode and CFG.debug_mode:
say_text("Apparently json was fixed.")
else:
return {}
except (json.JSONDecodeError, ValueError):
if CFG.debug_mode:
logger.error(f"Error: Invalid JSON: {json_string}\n")
if CFG.speak_mode:
say_text("Didn't work. I will have to ignore this response then.")
logger.error("Error: Invalid JSON, setting it to empty JSON now.\n")
json_string = {}
return fix_and_parse_json(json_string)

View File

@@ -5,11 +5,25 @@
"thoughts": {
"type": "object",
"properties": {
"text": {"type": "string"},
"reasoning": {"type": "string"},
"plan": {"type": "string"},
"criticism": {"type": "string"},
"speak": {"type": "string"}
"text": {
"type": "string",
"description": "thoughts"
},
"reasoning": {
"type": "string"
},
"plan": {
"type": "string",
"description": "- short bulleted\n- list that conveys\n- long-term plan"
},
"criticism": {
"type": "string",
"description": "constructive self-criticism"
},
"speak": {
"type": "string",
"description": "thoughts summary to say to user"
}
},
"required": ["text", "reasoning", "plan", "criticism", "speak"],
"additionalProperties": false

View File

@@ -1,6 +1,8 @@
"""Utilities for the json_fixes package."""
import ast
import json
import re
import os.path
from typing import Any
from jsonschema import Draft7Validator
@@ -11,36 +13,47 @@ CFG = Config()
LLM_DEFAULT_RESPONSE_FORMAT = "llm_response_format_1"
def extract_char_position(error_message: str) -> int:
"""Extract the character position from the JSONDecodeError message.
def extract_json_from_response(response_content: str) -> dict:
# Sometimes the response includes the JSON in a code block with ```
if response_content.startswith("```") and response_content.endswith("```"):
# Discard the first and last ```, then re-join in case the response naturally included ```
response_content = "```".join(response_content.split("```")[1:-1])
Args:
error_message (str): The error message from the JSONDecodeError
exception.
Returns:
int: The character position.
"""
char_pattern = re.compile(r"\(char (\d+)\)")
if match := char_pattern.search(error_message):
return int(match[1])
else:
raise ValueError("Character position not found in the error message.")
# response content comes from OpenAI as a Python `str(content_dict)`, literal_eval reverses this
try:
return ast.literal_eval(response_content)
except BaseException as e:
logger.error(f"Error parsing JSON response with literal_eval {e}")
# TODO: How to raise an error here without causing the program to exit?
return {}
def validate_json(json_object: object, schema_name: str) -> dict | None:
def llm_response_schema(
schema_name: str = LLM_DEFAULT_RESPONSE_FORMAT,
) -> dict[str, Any]:
filename = os.path.join(os.path.dirname(__file__), f"{schema_name}.json")
with open(filename, "r") as f:
return json.load(f)
def validate_json(
json_object: object, schema_name: str = LLM_DEFAULT_RESPONSE_FORMAT
) -> bool:
"""
:type schema_name: object
:param schema_name: str
:type json_object: object
Returns:
bool: Whether the json_object is valid or not
"""
with open(f"autogpt/json_utils/{schema_name}.json", "r") as f:
schema = json.load(f)
schema = llm_response_schema(schema_name)
validator = Draft7Validator(schema)
if errors := sorted(validator.iter_errors(json_object), key=lambda e: e.path):
logger.error("The JSON object is invalid.")
for error in errors:
logger.error(f"JSON Validation Error: {error}")
if CFG.debug_mode:
logger.error(
json.dumps(json_object, indent=4)
@@ -49,10 +62,11 @@ def validate_json(json_object: object, schema_name: str) -> dict | None:
for error in errors:
logger.error(f"Error: {error.message}")
else:
logger.debug("The JSON object is valid.")
return False
return json_object
logger.debug("The JSON object is valid.")
return True
def validate_json_string(json_string: str, schema_name: str) -> dict | None:
@@ -64,7 +78,9 @@ def validate_json_string(json_string: str, schema_name: str) -> dict | None:
try:
json_loaded = json.loads(json_string)
return validate_json(json_loaded, schema_name)
if not validate_json(json_loaded, schema_name):
return None
return json_loaded
except:
return None

View File

@@ -1,4 +1,3 @@
from autogpt.llm.api_manager import ApiManager
from autogpt.llm.base import (
ChatModelInfo,
ChatModelResponse,
@@ -8,17 +7,8 @@ from autogpt.llm.base import (
Message,
ModelInfo,
)
from autogpt.llm.chat import chat_with_ai, create_chat_message, generate_context
from autogpt.llm.llm_utils import (
call_ai_function,
create_chat_completion,
get_ada_embedding,
)
from autogpt.llm.modelsinfo import COSTS
from autogpt.llm.token_counter import count_message_tokens, count_string_tokens
__all__ = [
"ApiManager",
"Message",
"ModelInfo",
"ChatModelInfo",
@@ -26,13 +16,4 @@ __all__ = [
"LLMResponse",
"ChatModelResponse",
"EmbeddingModelResponse",
"create_chat_message",
"generate_context",
"chat_with_ai",
"call_ai_function",
"create_chat_completion",
"get_ada_embedding",
"COSTS",
"count_message_tokens",
"count_string_tokens",
]

View File

@@ -1,9 +1,13 @@
from __future__ import annotations
from typing import List, Optional
import openai
from openai import Model
from autogpt.config import Config
from autogpt.llm.modelsinfo import COSTS
from autogpt.llm.base import CompletionModelInfo, MessageDict
from autogpt.llm.providers.openai import OPEN_AI_MODELS
from autogpt.logs import logger
from autogpt.singleton import Singleton
@@ -14,21 +18,23 @@ class ApiManager(metaclass=Singleton):
self.total_completion_tokens = 0
self.total_cost = 0
self.total_budget = 0
self.models: Optional[list[Model]] = None
def reset(self):
self.total_prompt_tokens = 0
self.total_completion_tokens = 0
self.total_cost = 0
self.total_budget = 0.0
self.models = None
def create_chat_completion(
self,
messages: list, # type: ignore
messages: list[MessageDict],
model: str | None = None,
temperature: float = None,
max_tokens: int | None = None,
deployment_id=None,
) -> str:
):
"""
Create a chat completion and update the cost.
Args:
@@ -59,13 +65,14 @@ class ApiManager(metaclass=Singleton):
max_tokens=max_tokens,
api_key=cfg.openai_api_key,
)
logger.debug(f"Response: {response}")
prompt_tokens = response.usage.prompt_tokens
completion_tokens = response.usage.completion_tokens
self.update_cost(prompt_tokens, completion_tokens, model)
if not hasattr(response, "error"):
logger.debug(f"Response: {response}")
prompt_tokens = response.usage.prompt_tokens
completion_tokens = response.usage.completion_tokens
self.update_cost(prompt_tokens, completion_tokens, model)
return response
def update_cost(self, prompt_tokens, completion_tokens, model):
def update_cost(self, prompt_tokens, completion_tokens, model: str):
"""
Update the total cost, prompt tokens, and completion tokens.
@@ -74,12 +81,18 @@ class ApiManager(metaclass=Singleton):
completion_tokens (int): The number of tokens used in the completion.
model (str): The model used for the API call.
"""
# the .model property in API responses can contain version suffixes like -v2
model = model[:-3] if model.endswith("-v2") else model
model_info = OPEN_AI_MODELS[model]
self.total_prompt_tokens += prompt_tokens
self.total_completion_tokens += completion_tokens
self.total_cost += (
prompt_tokens * COSTS[model]["prompt"]
+ completion_tokens * COSTS[model]["completion"]
) / 1000
self.total_cost += prompt_tokens * model_info.prompt_token_cost / 1000
if issubclass(type(model_info), CompletionModelInfo):
self.total_cost += (
completion_tokens * model_info.completion_token_cost / 1000
)
logger.debug(f"Total running cost: ${self.total_cost:.3f}")
def set_total_budget(self, total_budget):
@@ -126,3 +139,17 @@ class ApiManager(metaclass=Singleton):
float: The total budget for API calls.
"""
return self.total_budget
def get_models(self) -> List[Model]:
"""
Get list of available GPT models.
Returns:
list: List of available GPT models.
"""
if self.models is None:
all_models = openai.Model.list()["data"]
self.models = [model for model in all_models if "gpt" in model["id"]]
return self.models

View File

@@ -1,12 +1,28 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, TypedDict
from math import ceil, floor
from typing import List, Literal, TypedDict
MessageRole = Literal["system", "user", "assistant"]
MessageType = Literal["ai_response", "action_result"]
class Message(TypedDict):
class MessageDict(TypedDict):
role: MessageRole
content: str
@dataclass
class Message:
"""OpenAI Message object containing a role and the message content"""
role: str
role: MessageRole
content: str
type: MessageType | None = None
def raw(self) -> MessageDict:
return {"role": self.role, "content": self.content}
@dataclass
@@ -15,20 +31,28 @@ class ModelInfo:
Would be lovely to eventually get this directly from APIs, but needs to be scraped from
websites for now.
"""
name: str
prompt_token_cost: float
completion_token_cost: float
max_tokens: int
prompt_token_cost: float
@dataclass
class ChatModelInfo(ModelInfo):
class CompletionModelInfo(ModelInfo):
"""Struct for generic completion model information."""
completion_token_cost: float
@dataclass
class ChatModelInfo(CompletionModelInfo):
"""Struct for chat model information."""
pass
@dataclass
class TextModelInfo(CompletionModelInfo):
"""Struct for text completion model information."""
@dataclass
@@ -38,6 +62,73 @@ class EmbeddingModelInfo(ModelInfo):
embedding_dimensions: int
@dataclass
class ChatSequence:
"""Utility container for a chat sequence"""
model: ChatModelInfo
messages: list[Message] = field(default_factory=list)
def __getitem__(self, i: int):
return self.messages[i]
def __iter__(self):
return iter(self.messages)
def __len__(self):
return len(self.messages)
def append(self, message: Message):
return self.messages.append(message)
def extend(self, messages: list[Message] | ChatSequence):
return self.messages.extend(messages)
def insert(self, index: int, *messages: Message):
for message in reversed(messages):
self.messages.insert(index, message)
@classmethod
def for_model(cls, model_name: str, messages: list[Message] | ChatSequence = []):
from autogpt.llm.providers.openai import OPEN_AI_CHAT_MODELS
if not model_name in OPEN_AI_CHAT_MODELS:
raise ValueError(f"Unknown chat model '{model_name}'")
return ChatSequence(
model=OPEN_AI_CHAT_MODELS[model_name], messages=list(messages)
)
def add(self, message_role: MessageRole, content: str):
self.messages.append(Message(message_role, content))
@property
def token_length(self):
from autogpt.llm.utils import count_message_tokens
return count_message_tokens(self.messages, self.model.name)
def raw(self) -> list[MessageDict]:
return [m.raw() for m in self.messages]
def dump(self) -> str:
SEPARATOR_LENGTH = 42
def separator(text: str):
half_sep_len = (SEPARATOR_LENGTH - 2 - len(text)) / 2
return f"{floor(half_sep_len)*'-'} {text.upper()} {ceil(half_sep_len)*'-'}"
formatted_messages = "\n".join(
[f"{separator(m.role)}\n{m.content}" for m in self.messages]
)
return f"""
============== ChatSequence ==============
Length: {self.token_length} tokens; {len(self.messages)} messages
{formatted_messages}
==========================================
"""
@dataclass
class LLMResponse:
"""Standard response struct for a response from an LLM model."""

View File

@@ -1,253 +1,202 @@
import time
from random import shuffle
from __future__ import annotations
from openai.error import RateLimitError
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from autogpt.agent.agent import Agent
from autogpt.config import Config
from autogpt.llm.api_manager import ApiManager
from autogpt.llm.base import Message
from autogpt.llm.llm_utils import create_chat_completion
from autogpt.llm.token_counter import count_message_tokens
from autogpt.llm.base import ChatSequence, Message
from autogpt.llm.utils import count_message_tokens, create_chat_completion
from autogpt.log_cycle.log_cycle import CURRENT_CONTEXT_FILE_NAME
from autogpt.logs import logger
from autogpt.memory_management.store_memory import (
save_memory_trimmed_from_context_window,
)
from autogpt.memory_management.summary_memory import (
get_newly_trimmed_messages,
update_running_summary,
)
cfg = Config()
def create_chat_message(role, content) -> Message:
"""
Create a chat message with the given role and content.
Args:
role (str): The role of the message sender, e.g., "system", "user", or "assistant".
content (str): The content of the message.
Returns:
dict: A dictionary containing the role and content of the message.
"""
return {"role": role, "content": content}
def generate_context(prompt, relevant_memory, full_message_history, model):
current_context = [
create_chat_message("system", prompt),
create_chat_message(
"system", f"The current time and date is {time.strftime('%c')}"
),
# create_chat_message(
# "system",
# f"This reminds you of these events from your past:\n{relevant_memory}\n\n",
# ),
]
# Add messages from the full message history until we reach the token limit
next_message_to_add_index = len(full_message_history) - 1
insertion_index = len(current_context)
# Count the currently used tokens
current_tokens_used = count_message_tokens(current_context, model)
return (
next_message_to_add_index,
current_tokens_used,
insertion_index,
current_context,
)
# TODO: Change debug from hardcode to argument
def chat_with_ai(
agent, prompt, user_input, full_message_history, permanent_memory, token_limit
config: Config,
agent: Agent,
system_prompt: str,
user_input: str,
token_limit: int,
model: str | None = None,
):
"""Interact with the OpenAI API, sending the prompt, user input, message history,
and permanent memory."""
while True:
try:
"""
Interact with the OpenAI API, sending the prompt, user input,
message history, and permanent memory.
"""
Interact with the OpenAI API, sending the prompt, user input,
message history, and permanent memory.
Args:
prompt (str): The prompt explaining the rules to the AI.
user_input (str): The input from the user.
full_message_history (list): The list of all messages sent between the
user and the AI.
permanent_memory (Obj): The memory object containing the permanent
memory.
token_limit (int): The maximum number of tokens allowed in the API call.
Args:
config (Config): The config to use.
agent (Agent): The agent to use.
system_prompt (str): The prompt explaining the rules to the AI.
user_input (str): The input from the user.
token_limit (int): The maximum number of tokens allowed in the API call.
model (str, optional): The model to use. If None, the config.fast_llm_model will be used. Defaults to None.
Returns:
str: The AI's response.
"""
model = cfg.fast_llm_model # TODO: Change model from hardcode to argument
# Reserve 1000 tokens for the response
logger.debug(f"Token limit: {token_limit}")
send_token_limit = token_limit - 1000
Returns:
str: The AI's response.
"""
if model is None:
model = config.fast_llm_model
# if len(full_message_history) == 0:
# relevant_memory = ""
# else:
# recent_history = full_message_history[-5:]
# shuffle(recent_history)
# relevant_memories = permanent_memory.get_relevant(
# str(recent_history), 5
# )
# if relevant_memories:
# shuffle(relevant_memories)
# relevant_memory = str(relevant_memories)
relevant_memory = ""
logger.debug(f"Memory Stats: {permanent_memory.get_stats()}")
# Reserve 1000 tokens for the response
logger.debug(f"Token limit: {token_limit}")
send_token_limit = token_limit - 1000
(
next_message_to_add_index,
current_tokens_used,
insertion_index,
current_context,
) = generate_context(prompt, relevant_memory, full_message_history, model)
# if len(agent.history) == 0:
# relevant_memory = ""
# else:
# recent_history = agent.history[-5:]
# shuffle(recent_history)
# relevant_memories = agent.memory.get_relevant(
# str(recent_history), 5
# )
# if relevant_memories:
# shuffle(relevant_memories)
# relevant_memory = str(relevant_memories)
# logger.debug(f"Memory Stats: {agent.memory.get_stats()}")
relevant_memory = []
# while current_tokens_used > 2500:
# # remove memories until we are under 2500 tokens
# relevant_memory = relevant_memory[:-1]
# (
# next_message_to_add_index,
# current_tokens_used,
# insertion_index,
# current_context,
# ) = generate_context(
# prompt, relevant_memory, full_message_history, model
# )
message_sequence = ChatSequence.for_model(
model,
[
Message("system", system_prompt),
Message("system", f"The current time and date is {time.strftime('%c')}"),
# Message(
# "system",
# f"This reminds you of these events from your past:\n{relevant_memory}\n\n",
# ),
],
)
current_tokens_used += count_message_tokens(
[create_chat_message("user", user_input)], model
) # Account for user input (appended later)
# Add messages from the full message history until we reach the token limit
next_message_to_add_index = len(agent.history) - 1
insertion_index = len(message_sequence)
# Count the currently used tokens
current_tokens_used = message_sequence.token_length
current_tokens_used += 500 # Account for memory (appended later) TODO: The final memory may be less than 500 tokens
# while current_tokens_used > 2500:
# # remove memories until we are under 2500 tokens
# relevant_memory = relevant_memory[:-1]
# (
# next_message_to_add_index,
# current_tokens_used,
# insertion_index,
# current_context,
# ) = generate_context(
# prompt, relevant_memory, agent.history, model
# )
# Add Messages until the token limit is reached or there are no more messages to add.
while next_message_to_add_index >= 0:
# print (f"CURRENT TOKENS USED: {current_tokens_used}")
message_to_add = full_message_history[next_message_to_add_index]
# Account for user input (appended later)
user_input_msg = Message("user", user_input)
current_tokens_used += count_message_tokens([user_input_msg], model)
tokens_to_add = count_message_tokens([message_to_add], model)
if current_tokens_used + tokens_to_add > send_token_limit:
# save_memory_trimmed_from_context_window(
# full_message_history,
# next_message_to_add_index,
# permanent_memory,
# )
break
current_tokens_used += 500 # Reserve space for new_summary_message
# Add the most recent message to the start of the current context,
# after the two system prompts.
current_context.insert(
insertion_index, full_message_history[next_message_to_add_index]
)
# Add Messages until the token limit is reached or there are no more messages to add.
for cycle in reversed(list(agent.history.per_cycle())):
messages_to_add = [msg for msg in cycle if msg is not None]
tokens_to_add = count_message_tokens(messages_to_add, model)
if current_tokens_used + tokens_to_add > send_token_limit:
break
# Count the currently used tokens
current_tokens_used += tokens_to_add
# Add the most recent message to the start of the chain,
# after the system prompts.
message_sequence.insert(insertion_index, *messages_to_add)
current_tokens_used += tokens_to_add
# Move to the next most recent message in the full message history
next_message_to_add_index -= 1
# Update & add summary of trimmed messages
if len(agent.history) > 0:
new_summary_message, trimmed_messages = agent.history.trim_messages(
current_message_chain=list(message_sequence),
)
tokens_to_add = count_message_tokens([new_summary_message], model)
message_sequence.insert(insertion_index, new_summary_message)
current_tokens_used += tokens_to_add - 500
# Insert Memories
if len(full_message_history) > 0:
(
newly_trimmed_messages,
agent.last_memory_index,
) = get_newly_trimmed_messages(
full_message_history=full_message_history,
current_context=current_context,
last_memory_index=agent.last_memory_index,
)
agent.summary_memory = update_running_summary(
current_memory=agent.summary_memory,
new_events=newly_trimmed_messages,
)
current_context.insert(insertion_index, agent.summary_memory)
# FIXME: uncomment when memory is back in use
# memory_store = get_memory(cfg)
# for _, ai_msg, result_msg in agent.history.per_cycle(trimmed_messages):
# memory_to_add = MemoryItem.from_ai_action(ai_msg, result_msg)
# logger.debug(f"Storing the following memory:\n{memory_to_add.dump()}")
# memory_store.add(memory_to_add)
api_manager = ApiManager()
# inform the AI about its remaining budget (if it has one)
if api_manager.get_total_budget() > 0.0:
remaining_budget = (
api_manager.get_total_budget() - api_manager.get_total_cost()
)
if remaining_budget < 0:
remaining_budget = 0
system_message = (
f"Your remaining API budget is ${remaining_budget:.3f}"
+ (
" BUDGET EXCEEDED! SHUT DOWN!\n\n"
if remaining_budget == 0
else " Budget very nearly exceeded! Shut down gracefully!\n\n"
if remaining_budget < 0.005
else " Budget nearly exceeded. Finish up.\n\n"
if remaining_budget < 0.01
else "\n\n"
)
)
logger.debug(system_message)
current_context.append(create_chat_message("system", system_message))
api_manager = ApiManager()
# inform the AI about its remaining budget (if it has one)
if api_manager.get_total_budget() > 0.0:
remaining_budget = api_manager.get_total_budget() - api_manager.get_total_cost()
if remaining_budget < 0:
remaining_budget = 0
budget_message = f"Your remaining API budget is ${remaining_budget:.3f}" + (
" BUDGET EXCEEDED! SHUT DOWN!\n\n"
if remaining_budget == 0
else " Budget very nearly exceeded! Shut down gracefully!\n\n"
if remaining_budget < 0.005
else " Budget nearly exceeded. Finish up.\n\n"
if remaining_budget < 0.01
else "\n\n"
)
logger.debug(budget_message)
message_sequence.add("system", budget_message)
current_tokens_used += count_message_tokens([message_sequence[-1]], model)
# Append user input, the length of this is accounted for above
current_context.extend([create_chat_message("user", user_input)])
# Append user input, the length of this is accounted for above
message_sequence.append(user_input_msg)
plugin_count = len(cfg.plugins)
for i, plugin in enumerate(cfg.plugins):
if not plugin.can_handle_on_planning():
continue
plugin_response = plugin.on_planning(
agent.prompt_generator, current_context
)
if not plugin_response or plugin_response == "":
continue
tokens_to_add = count_message_tokens(
[create_chat_message("system", plugin_response)], model
)
if current_tokens_used + tokens_to_add > send_token_limit:
logger.debug("Plugin response too long, skipping:", plugin_response)
logger.debug("Plugins remaining at stop:", plugin_count - i)
break
current_context.append(create_chat_message("system", plugin_response))
plugin_count = len(config.plugins)
for i, plugin in enumerate(config.plugins):
if not plugin.can_handle_on_planning():
continue
plugin_response = plugin.on_planning(
agent.ai_config.prompt_generator, message_sequence.raw()
)
if not plugin_response or plugin_response == "":
continue
tokens_to_add = count_message_tokens(
[Message("system", plugin_response)], model
)
if current_tokens_used + tokens_to_add > send_token_limit:
logger.debug(f"Plugin response too long, skipping: {plugin_response}")
logger.debug(f"Plugins remaining at stop: {plugin_count - i}")
break
message_sequence.add("system", plugin_response)
# Calculate remaining tokens
tokens_remaining = token_limit - current_tokens_used
# assert tokens_remaining >= 0, "Tokens remaining is negative.
# This should never happen, please submit a bug report at
# https://www.github.com/Torantulino/Auto-GPT"
# Calculate remaining tokens
tokens_remaining = token_limit - current_tokens_used
# assert tokens_remaining >= 0, "Tokens remaining is negative.
# This should never happen, please submit a bug report at
# https://www.github.com/Torantulino/Auto-GPT"
# Debug print the current context
logger.debug(f"Token limit: {token_limit}")
logger.debug(f"Send Token Count: {current_tokens_used}")
logger.debug(f"Tokens remaining for response: {tokens_remaining}")
logger.debug("------------ CONTEXT SENT TO AI ---------------")
for message in message_sequence:
# Skip printing the prompt
if message.role == "system" and message.content == system_prompt:
continue
logger.debug(f"{message.role.capitalize()}: {message.content}")
logger.debug("")
logger.debug("----------- END OF CONTEXT ----------------")
agent.log_cycle_handler.log_cycle(
agent.ai_name,
agent.created_at,
agent.cycle_count,
message_sequence.raw(),
CURRENT_CONTEXT_FILE_NAME,
)
# Debug print the current context
logger.debug(f"Token limit: {token_limit}")
logger.debug(f"Send Token Count: {current_tokens_used}")
logger.debug(f"Tokens remaining for response: {tokens_remaining}")
logger.debug("------------ CONTEXT SENT TO AI ---------------")
for message in current_context:
# Skip printing the prompt
if message["role"] == "system" and message["content"] == prompt:
continue
logger.debug(f"{message['role'].capitalize()}: {message['content']}")
logger.debug("")
logger.debug("----------- END OF CONTEXT ----------------")
# TODO: use a model defined elsewhere, so that model can contain
# temperature and other settings we care about
assistant_reply = create_chat_completion(
prompt=message_sequence,
max_tokens=tokens_remaining,
)
# TODO: use a model defined elsewhere, so that model can contain
# temperature and other settings we care about
assistant_reply = create_chat_completion(
model=model,
messages=current_context,
max_tokens=tokens_remaining,
)
# Update full message history
agent.history.append(user_input_msg)
agent.history.add("assistant", assistant_reply, "ai_response")
# Update full message history
full_message_history.append(create_chat_message("user", user_input))
full_message_history.append(
create_chat_message("assistant", assistant_reply)
)
return assistant_reply
except RateLimitError:
# TODO: When we switch to langchain, this is built in
logger.warn("Error: ", "API Rate Limit Reached. Waiting 10 seconds...")
time.sleep(10)
return assistant_reply

View File

@@ -1,7 +0,0 @@
COSTS = {
"gpt-3.5-turbo": {"prompt": 0.002, "completion": 0.002},
"gpt-3.5-turbo-0301": {"prompt": 0.002, "completion": 0.002},
"gpt-4-0314": {"prompt": 0.03, "completion": 0.06},
"gpt-4": {"prompt": 0.03, "completion": 0.06},
"text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0},
}

View File

@@ -1,37 +1,90 @@
from autogpt.llm.base import ChatModelInfo, EmbeddingModelInfo
from autogpt.llm.base import ChatModelInfo, EmbeddingModelInfo, TextModelInfo
OPEN_AI_CHAT_MODELS = {
"gpt-3.5-turbo": ChatModelInfo(
name="gpt-3.5-turbo",
prompt_token_cost=0.002,
completion_token_cost=0.002,
max_tokens=4096,
),
"gpt-4": ChatModelInfo(
name="gpt-4",
prompt_token_cost=0.03,
completion_token_cost=0.06,
max_tokens=8192,
),
"gpt-4-32k": ChatModelInfo(
name="gpt-4-32k",
prompt_token_cost=0.06,
completion_token_cost=0.12,
max_tokens=32768,
),
info.name: info
for info in [
ChatModelInfo(
name="gpt-3.5-turbo-0301",
prompt_token_cost=0.0015,
completion_token_cost=0.002,
max_tokens=4096,
),
ChatModelInfo(
name="gpt-3.5-turbo-0613",
prompt_token_cost=0.0015,
completion_token_cost=0.002,
max_tokens=4096,
),
ChatModelInfo(
name="gpt-3.5-turbo-16k-0613",
prompt_token_cost=0.003,
completion_token_cost=0.004,
max_tokens=16384,
),
ChatModelInfo(
name="gpt-4-0314",
prompt_token_cost=0.03,
completion_token_cost=0.06,
max_tokens=8192,
),
ChatModelInfo(
name="gpt-4-0613",
prompt_token_cost=0.03,
completion_token_cost=0.06,
max_tokens=8192,
),
ChatModelInfo(
name="gpt-4-32k-0314",
prompt_token_cost=0.06,
completion_token_cost=0.12,
max_tokens=32768,
),
ChatModelInfo(
name="gpt-4-32k-0613",
prompt_token_cost=0.06,
completion_token_cost=0.12,
max_tokens=32768,
),
]
}
# Set aliases for rolling model IDs
chat_model_mapping = {
"gpt-3.5-turbo": "gpt-3.5-turbo-0301",
"gpt-3.5-turbo-16k": "gpt-3.5-turbo-16k-0613",
"gpt-4": "gpt-4-0314",
"gpt-4-32k": "gpt-4-32k-0314",
}
for alias, target in chat_model_mapping.items():
alias_info = ChatModelInfo(**OPEN_AI_CHAT_MODELS[target].__dict__)
alias_info.name = alias
OPEN_AI_CHAT_MODELS[alias] = alias_info
OPEN_AI_TEXT_MODELS = {
info.name: info
for info in [
TextModelInfo(
name="text-davinci-003",
prompt_token_cost=0.02,
completion_token_cost=0.02,
max_tokens=4097,
),
]
}
OPEN_AI_EMBEDDING_MODELS = {
"text-embedding-ada-002": EmbeddingModelInfo(
name="text-embedding-ada-002",
prompt_token_cost=0.0004,
completion_token_cost=0.0,
max_tokens=8191,
embedding_dimensions=1536,
),
info.name: info
for info in [
EmbeddingModelInfo(
name="text-embedding-ada-002",
prompt_token_cost=0.0001,
max_tokens=8191,
embedding_dimensions=1536,
),
]
}
OPEN_AI_MODELS = {
OPEN_AI_MODELS: dict[str, ChatModelInfo | EmbeddingModelInfo | TextModelInfo] = {
**OPEN_AI_CHAT_MODELS,
**OPEN_AI_TEXT_MODELS,
**OPEN_AI_EMBEDDING_MODELS,
}

View File

@@ -2,17 +2,59 @@ from __future__ import annotations
import functools
import time
from typing import List, Optional
from typing import List, Literal, Optional
from unittest.mock import patch
import openai
import openai.api_resources.abstract.engine_api_resource as engine_api_resource
import openai.util
from colorama import Fore, Style
from openai.error import APIError, RateLimitError, Timeout
from openai.error import APIError, RateLimitError
from openai.openai_object import OpenAIObject
from autogpt.config import Config
from autogpt.llm.api_manager import ApiManager
from autogpt.llm.base import Message
from autogpt.logs import logger
from ..api_manager import ApiManager
from ..base import ChatSequence, Message
from ..providers.openai import OPEN_AI_CHAT_MODELS
from .token_counter import *
def metered(func):
"""Adds ApiManager metering to functions which make OpenAI API calls"""
api_manager = ApiManager()
openai_obj_processor = openai.util.convert_to_openai_object
def update_usage_with_response(response: OpenAIObject):
try:
usage = response.usage
logger.debug(f"Reported usage from call to model {response.model}: {usage}")
api_manager.update_cost(
response.usage.prompt_tokens,
response.usage.completion_tokens if "completion_tokens" in usage else 0,
response.model,
)
except Exception as err:
logger.warn(f"Failed to update API costs: {err.__class__.__name__}: {err}")
def metering_wrapper(*args, **kwargs):
openai_obj = openai_obj_processor(*args, **kwargs)
if isinstance(openai_obj, OpenAIObject) and "usage" in openai_obj:
update_usage_with_response(openai_obj)
return openai_obj
def metered_func(*args, **kwargs):
with patch.object(
engine_api_resource.util,
"convert_to_openai_object",
side_effect=metering_wrapper,
):
return func(*args, **kwargs)
return metered_func
def retry_openai_api(
num_retries: int = 10,
@@ -30,7 +72,7 @@ def retry_openai_api(
api_key_error_msg = (
f"Please double check that you have setup a "
f"{Fore.CYAN + Style.BRIGHT}PAID{Style.RESET_ALL} OpenAI API Account. You can "
f"read more here: {Fore.CYAN}https://significant-gravitas.github.io/Auto-GPT/setup/#getting-an-api-key{Fore.RESET}"
f"read more here: {Fore.CYAN}https://docs.agpt.co/setup/#getting-an-api-key{Fore.RESET}"
)
backoff_msg = (
f"{Fore.RED}Error: API Bad gateway. Waiting {{backoff}} seconds...{Fore.RESET}"
@@ -55,7 +97,9 @@ def retry_openai_api(
user_warned = True
except APIError as e:
if (e.http_status != 502) or (attempt == num_attempts):
if (e.http_status not in [429, 502, 503]) or (
attempt == num_attempts
):
raise
backoff = backoff_base ** (attempt + 2)
@@ -68,7 +112,11 @@ def retry_openai_api(
def call_ai_function(
function: str, args: list, description: str, model: str | None = None
function: str,
args: list,
description: str,
model: str | None = None,
config: Config = None,
) -> str:
"""Call an AI function
@@ -84,29 +132,62 @@ def call_ai_function(
Returns:
str: The response from the function
"""
cfg = Config()
if model is None:
model = cfg.smart_llm_model
model = config.smart_llm_model
# For each arg, if any are None, convert to "None":
args = [str(arg) if arg is not None else "None" for arg in args]
# parse args to comma separated string
args: str = ", ".join(args)
messages: List[Message] = [
{
"role": "system",
"content": f"You are now the following python function: ```# {description}"
f"\n{function}```\n\nOnly respond with your `return` value.",
},
{"role": "user", "content": args},
]
arg_str: str = ", ".join(args)
return create_chat_completion(model=model, messages=messages, temperature=0)
prompt = ChatSequence.for_model(
model,
[
Message(
"system",
f"You are now the following python function: ```# {description}"
f"\n{function}```\n\nOnly respond with your `return` value.",
),
Message("user", arg_str),
],
)
return create_chat_completion(prompt=prompt, temperature=0)
@metered
@retry_openai_api()
def create_text_completion(
prompt: str,
model: Optional[str],
temperature: Optional[float],
max_output_tokens: Optional[int],
) -> str:
cfg = Config()
if model is None:
model = cfg.fast_llm_model
if temperature is None:
temperature = cfg.temperature
if cfg.use_azure:
kwargs = {"deployment_id": cfg.get_azure_deployment_id_for_model(model)}
else:
kwargs = {"model": model}
response = openai.Completion.create(
**kwargs,
prompt=prompt,
temperature=temperature,
max_tokens=max_output_tokens,
api_key=cfg.openai_api_key,
)
return response.choices[0].text
# Overly simple abstraction until we create something better
# simple retry mechanism when getting a rate error or a bad gateway
@metered
@retry_openai_api()
def create_chat_completion(
messages: List[Message], # type: ignore
prompt: ChatSequence,
model: Optional[str] = None,
temperature: float = None,
max_tokens: Optional[int] = None,
@@ -123,23 +204,25 @@ def create_chat_completion(
str: The response from the chat completion
"""
cfg = Config()
if model is None:
model = prompt.model.name
if temperature is None:
temperature = cfg.temperature
if max_tokens is None:
max_tokens = OPEN_AI_CHAT_MODELS[model].max_tokens - prompt.token_length
num_retries = 10
warned_user = False
logger.debug(
f"{Fore.GREEN}Creating chat completion with model {model}, temperature {temperature}, max_tokens {max_tokens}{Fore.RESET}"
)
for plugin in cfg.plugins:
if plugin.can_handle_chat_completion(
messages=messages,
messages=prompt.raw(),
model=model,
temperature=temperature,
max_tokens=max_tokens,
):
message = plugin.handle_chat_completion(
messages=messages,
messages=prompt.raw(),
model=model,
temperature=temperature,
max_tokens=max_tokens,
@@ -148,58 +231,20 @@ def create_chat_completion(
return message
api_manager = ApiManager()
response = None
for attempt in range(num_retries):
backoff = 2 ** (attempt + 2)
try:
if cfg.use_azure:
response = api_manager.create_chat_completion(
deployment_id=cfg.get_azure_deployment_id_for_model(model),
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
else:
response = api_manager.create_chat_completion(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
break
except RateLimitError:
logger.debug(
f"{Fore.RED}Error: ", f"Reached rate limit, passing...{Fore.RESET}"
)
if not warned_user:
logger.double_check(
f"Please double check that you have setup a {Fore.CYAN + Style.BRIGHT}PAID{Style.RESET_ALL} OpenAI API Account. "
+ f"You can read more here: {Fore.CYAN}https://significant-gravitas.github.io/Auto-GPT/setup/#getting-an-api-key{Fore.RESET}"
)
warned_user = True
except (APIError, Timeout) as e:
if e.http_status != 502:
raise
if attempt == num_retries - 1:
raise
logger.debug(
f"{Fore.RED}Error: ",
f"API Bad gateway. Waiting {backoff} seconds...{Fore.RESET}",
)
time.sleep(backoff)
if response is None:
logger.typewriter_log(
"FAILED TO GET RESPONSE FROM OPENAI",
Fore.RED,
"Auto-GPT has failed to get a response from OpenAI's services. "
+ f"Try running Auto-GPT again, and if the problem the persists try running it with `{Fore.CYAN}--debug{Fore.RESET}`.",
)
logger.double_check()
if cfg.debug_mode:
raise RuntimeError(f"Failed to get response after {num_retries} retries")
else:
quit(1)
resp = response.choices[0].message["content"]
if cfg.use_azure:
kwargs = {"deployment_id": cfg.get_azure_deployment_id_for_model(model)}
else:
kwargs = {"model": model}
response = api_manager.create_chat_completion(
**kwargs,
messages=prompt.raw(),
temperature=temperature,
max_tokens=max_tokens,
)
resp = response.choices[0].message.content
for plugin in cfg.plugins:
if not plugin.can_handle_on_response():
continue
@@ -207,52 +252,20 @@ def create_chat_completion(
return resp
def get_ada_embedding(text: str) -> List[float]:
"""Get an embedding from the ada model.
Args:
text (str): The text to embed.
Returns:
List[float]: The embedding.
"""
cfg = Config()
model = "text-embedding-ada-002"
text = text.replace("\n", " ")
if cfg.use_azure:
kwargs = {"engine": cfg.get_azure_deployment_id_for_model(model)}
else:
kwargs = {"model": model}
embedding = create_embedding(text, **kwargs)
def check_model(
model_name: str, model_type: Literal["smart_llm_model", "fast_llm_model"]
) -> str:
"""Check if model is available for use. If not, return gpt-3.5-turbo."""
api_manager = ApiManager()
api_manager.update_cost(
prompt_tokens=embedding.usage.prompt_tokens,
completion_tokens=0,
model=model,
)
return embedding["data"][0]["embedding"]
@retry_openai_api()
def create_embedding(
text: str,
*_,
**kwargs,
) -> openai.Embedding:
"""Create an embedding using the OpenAI API
Args:
text (str): The text to embed.
kwargs: Other arguments to pass to the OpenAI API embedding creation call.
Returns:
openai.Embedding: The embedding object.
"""
cfg = Config()
return openai.Embedding.create(
input=[text],
api_key=cfg.openai_api_key,
**kwargs,
models = api_manager.get_models()
if any(model_name in m["id"] for m in models):
return model_name
logger.typewriter_log(
"WARNING: ",
Fore.YELLOW,
f"You do not have access to {model_name}. Setting {model_type} to "
f"gpt-3.5-turbo.",
)
return "gpt-3.5-turbo"

View File

@@ -24,36 +24,32 @@ def count_message_tokens(
Returns:
int: The number of tokens used by the list of messages.
"""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
logger.warn("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
if model == "gpt-3.5-turbo":
# !Note: gpt-3.5-turbo may change over time.
# Returning num tokens assuming gpt-3.5-turbo-0301.")
return count_message_tokens(messages, model="gpt-3.5-turbo-0301")
elif model == "gpt-4":
# !Note: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.")
return count_message_tokens(messages, model="gpt-4-0314")
elif model == "gpt-3.5-turbo-0301":
if model.startswith("gpt-3.5-turbo"):
tokens_per_message = (
4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
)
tokens_per_name = -1 # if there's a name, the role is omitted
elif model == "gpt-4-0314":
encoding_model = "gpt-3.5-turbo"
elif model.startswith("gpt-4"):
tokens_per_message = 3
tokens_per_name = 1
encoding_model = "gpt-4"
else:
raise NotImplementedError(
f"num_tokens_from_messages() is not implemented for model {model}.\n"
f"count_message_tokens() is not implemented for model {model}.\n"
" See https://github.com/openai/openai-python/blob/main/chatml.md for"
" information on how messages are converted to tokens."
)
try:
encoding = tiktoken.encoding_for_model(encoding_model)
except KeyError:
logger.warn("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
for key, value in message.raw().items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name

View File

@@ -0,0 +1,20 @@
import json
import logging
class JsonFileHandler(logging.FileHandler):
def __init__(self, filename, mode="a", encoding=None, delay=False):
super().__init__(filename, mode, encoding, delay)
def emit(self, record):
json_data = json.loads(self.format(record))
with open(self.baseFilename, "w", encoding="utf-8") as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
import logging
class JsonFormatter(logging.Formatter):
def format(self, record):
return record.msg

View File

@@ -0,0 +1,88 @@
import json
import os
from typing import Any, Dict, Union
from autogpt.logs import logger
DEFAULT_PREFIX = "agent"
FULL_MESSAGE_HISTORY_FILE_NAME = "full_message_history.json"
CURRENT_CONTEXT_FILE_NAME = "current_context.json"
NEXT_ACTION_FILE_NAME = "next_action.json"
PROMPT_SUMMARY_FILE_NAME = "prompt_summary.json"
SUMMARY_FILE_NAME = "summary.txt"
SUPERVISOR_FEEDBACK_FILE_NAME = "supervisor_feedback.txt"
PROMPT_SUPERVISOR_FEEDBACK_FILE_NAME = "prompt_supervisor_feedback.json"
USER_INPUT_FILE_NAME = "user_input.txt"
class LogCycleHandler:
"""
A class for logging cycle data.
"""
def __init__(self):
self.log_count_within_cycle = 0
@staticmethod
def create_directory_if_not_exists(directory_path: str) -> None:
if not os.path.exists(directory_path):
os.makedirs(directory_path, exist_ok=True)
def create_outer_directory(self, ai_name: str, created_at: str) -> str:
log_directory = logger.get_log_directory()
if os.environ.get("OVERWRITE_DEBUG") == "1":
outer_folder_name = "auto_gpt"
else:
ai_name_short = self.get_agent_short_name(ai_name)
outer_folder_name = f"{created_at}_{ai_name_short}"
outer_folder_path = os.path.join(log_directory, "DEBUG", outer_folder_name)
self.create_directory_if_not_exists(outer_folder_path)
return outer_folder_path
def get_agent_short_name(self, ai_name):
return ai_name[:15].rstrip() if ai_name else DEFAULT_PREFIX
def create_inner_directory(self, outer_folder_path: str, cycle_count: int) -> str:
nested_folder_name = str(cycle_count).zfill(3)
nested_folder_path = os.path.join(outer_folder_path, nested_folder_name)
self.create_directory_if_not_exists(nested_folder_path)
return nested_folder_path
def create_nested_directory(
self, ai_name: str, created_at: str, cycle_count: int
) -> str:
outer_folder_path = self.create_outer_directory(ai_name, created_at)
nested_folder_path = self.create_inner_directory(outer_folder_path, cycle_count)
return nested_folder_path
def log_cycle(
self,
ai_name: str,
created_at: str,
cycle_count: int,
data: Union[Dict[str, Any], Any],
file_name: str,
) -> None:
"""
Log cycle data to a JSON file.
Args:
data (Any): The data to be logged.
file_name (str): The name of the file to save the logged data.
"""
nested_folder_path = self.create_nested_directory(
ai_name, created_at, cycle_count
)
json_data = json.dumps(data, ensure_ascii=False, indent=4)
log_file_path = os.path.join(
nested_folder_path, f"{self.log_count_within_cycle}_{file_name}"
)
logger.log_json(json_data, log_file_path)
self.log_count_within_cycle += 1

View File

@@ -5,9 +5,11 @@ import random
import re
import time
from logging import LogRecord
from typing import Any
from colorama import Fore, Style
from autogpt.log_cycle.json_handler import JsonFileHandler, JsonFormatter
from autogpt.singleton import Singleton
from autogpt.speech import say_text
@@ -74,7 +76,13 @@ class Logger(metaclass=Singleton):
self.logger.addHandler(error_handler)
self.logger.setLevel(logging.DEBUG)
self.json_logger = logging.getLogger("JSON_LOGGER")
self.json_logger.addHandler(self.file_handler)
self.json_logger.addHandler(error_handler)
self.json_logger.setLevel(logging.DEBUG)
self.speak_mode = False
self.chat_plugins = []
def typewriter_log(
self, title="", title_color="", content="", speak_text=False, level=logging.INFO
@@ -82,6 +90,9 @@ class Logger(metaclass=Singleton):
if speak_text and self.speak_mode:
say_text(f"{title}. {content}")
for plugin in self.chat_plugins:
plugin.report(f"{title}. {content}")
if content:
if isinstance(content, list):
content = " ".join(content)
@@ -148,6 +159,26 @@ class Logger(metaclass=Singleton):
self.typewriter_log("DOUBLE CHECK CONFIGURATION", Fore.YELLOW, additionalText)
def log_json(self, data: Any, file_name: str) -> None:
# Define log directory
this_files_dir_path = os.path.dirname(__file__)
log_dir = os.path.join(this_files_dir_path, "../logs")
# Create a handler for JSON files
json_file_path = os.path.join(log_dir, file_name)
json_data_handler = JsonFileHandler(json_file_path)
json_data_handler.setFormatter(JsonFormatter())
# Log the JSON data using the custom file handler
self.json_logger.addHandler(json_data_handler)
self.json_logger.debug(data)
self.json_logger.removeHandler(json_data_handler)
def get_log_directory(self):
this_files_dir_path = os.path.dirname(__file__)
log_dir = os.path.join(this_files_dir_path, "../logs")
return os.path.abspath(log_dir)
"""
Output stream to console using simulated typing
@@ -195,12 +226,16 @@ class AutoGptFormatter(logging.Formatter):
if hasattr(record, "color"):
record.title_color = (
getattr(record, "color")
+ getattr(record, "title")
+ getattr(record, "title", "")
+ " "
+ Style.RESET_ALL
)
else:
record.title_color = getattr(record, "title")
record.title_color = getattr(record, "title", "")
# Add this line to set 'title' to an empty string if it doesn't exist
record.title = getattr(record, "title", "")
if hasattr(record, "msg"):
record.message_no_color = remove_color_codes(getattr(record, "msg"))
else:
@@ -252,5 +287,8 @@ def print_assistant_thoughts(
logger.typewriter_log("- ", Fore.GREEN, line.strip())
logger.typewriter_log("CRITICISM:", Fore.YELLOW, f"{assistant_thoughts_criticism}")
# Speak the assistant's thoughts
if speak_mode and assistant_thoughts_speak:
say_text(assistant_thoughts_speak)
if assistant_thoughts_speak:
if speak_mode:
say_text(assistant_thoughts_speak)
else:
logger.typewriter_log("SPEAK:", Fore.YELLOW, f"{assistant_thoughts_speak}")

View File

@@ -3,25 +3,46 @@ import logging
import sys
from pathlib import Path
from colorama import Fore
from colorama import Fore, Style
from autogpt.agent.agent import Agent
from autogpt.agent import Agent
from autogpt.commands.command import CommandRegistry
from autogpt.config import Config, check_openai_api_key
from autogpt.configurator import create_config
from autogpt.logs import logger
from autogpt.memory import get_memory
from autogpt.memory.vector import get_memory
from autogpt.plugins import scan_plugins
from autogpt.prompts.prompt import DEFAULT_TRIGGERING_PROMPT, construct_main_ai_config
from autogpt.utils import get_current_git_branch, get_latest_bulletin
from autogpt.utils import (
get_current_git_branch,
get_latest_bulletin,
get_legal_warning,
markdown_to_ansi_style,
)
from autogpt.workspace import Workspace
from scripts.install_plugin_deps import install_plugin_dependencies
COMMAND_CATEGORIES = [
"autogpt.commands.analyze_code",
"autogpt.commands.audio_text",
"autogpt.commands.execute_code",
"autogpt.commands.file_operations",
"autogpt.commands.git_operations",
"autogpt.commands.google_search",
"autogpt.commands.image_gen",
"autogpt.commands.improve_code",
"autogpt.commands.web_selenium",
"autogpt.commands.write_tests",
"autogpt.app",
"autogpt.commands.task_statuses",
]
def run_auto_gpt(
continuous: bool,
continuous_limit: int,
ai_settings: str,
prompt_settings: str,
skip_reprompt: bool,
speak: bool,
debug: bool,
@@ -41,10 +62,13 @@ def run_auto_gpt(
cfg = Config()
# TODO: fill in llm values here
check_openai_api_key()
create_config(
cfg,
continuous,
continuous_limit,
ai_settings,
prompt_settings,
skip_reprompt,
speak,
debug,
@@ -56,10 +80,24 @@ def run_auto_gpt(
skip_news,
)
if cfg.continuous_mode:
for line in get_legal_warning().split("\n"):
logger.warn(markdown_to_ansi_style(line), "LEGAL:", Fore.RED)
if not cfg.skip_news:
motd = get_latest_bulletin()
motd, is_new_motd = get_latest_bulletin()
if motd:
logger.typewriter_log("NEWS: ", Fore.GREEN, motd)
motd = markdown_to_ansi_style(motd)
for motd_line in motd.split("\n"):
logger.info(motd_line, "NEWS:", Fore.GREEN)
if is_new_motd and not cfg.chat_messages_enabled:
input(
Fore.MAGENTA
+ Style.BRIGHT
+ "NEWS: Bulletin was updated! Press Enter to continue..."
+ Style.RESET_ALL
)
git_branch = get_current_git_branch()
if git_branch and git_branch != "stable":
logger.typewriter_log(
@@ -104,27 +142,37 @@ def run_auto_gpt(
cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode))
# Create a CommandRegistry instance and scan default folder
command_registry = CommandRegistry()
command_registry.import_commands("autogpt.commands.analyze_code")
command_registry.import_commands("autogpt.commands.audio_text")
command_registry.import_commands("autogpt.commands.execute_code")
command_registry.import_commands("autogpt.commands.file_operations")
command_registry.import_commands("autogpt.commands.git_operations")
command_registry.import_commands("autogpt.commands.google_search")
command_registry.import_commands("autogpt.commands.image_gen")
command_registry.import_commands("autogpt.commands.improve_code")
command_registry.import_commands("autogpt.commands.twitter")
command_registry.import_commands("autogpt.commands.web_selenium")
command_registry.import_commands("autogpt.commands.write_tests")
command_registry.import_commands("autogpt.app")
logger.debug(
f"The following command categories are disabled: {cfg.disabled_command_categories}"
)
enabled_command_categories = [
x for x in COMMAND_CATEGORIES if x not in cfg.disabled_command_categories
]
logger.debug(
f"The following command categories are enabled: {enabled_command_categories}"
)
for command_category in enabled_command_categories:
command_registry.import_commands(command_category)
ai_name = ""
ai_config = construct_main_ai_config()
ai_config.command_registry = command_registry
if ai_config.ai_name:
ai_name = ai_config.ai_name
# print(prompt)
# Initialize variables
full_message_history = []
next_action_count = 0
# add chat plugins capable of report to logger
if cfg.chat_messages_enabled:
for plugin in cfg.plugins:
if hasattr(plugin, "can_handle_report") and plugin.can_handle_report():
logger.info(f"Loaded plugin into logger: {plugin.__class__.__name__}")
logger.chat_plugins.append(plugin)
# Initialize memory and make sure it is empty.
# this is particularly important for indexing and referencing pinecone memory
memory = get_memory(cfg, init=True)
@@ -139,12 +187,12 @@ def run_auto_gpt(
agent = Agent(
ai_name=ai_name,
memory=memory,
full_message_history=full_message_history,
next_action_count=next_action_count,
command_registry=command_registry,
config=ai_config,
system_prompt=system_prompt,
triggering_prompt=DEFAULT_TRIGGERING_PROMPT,
workspace_directory=workspace_directory,
ai_config=ai_config,
config=cfg,
)
agent.start_interaction_loop()

View File

@@ -1,96 +0,0 @@
from autogpt.logs import logger
from autogpt.memory.local import LocalCache
from autogpt.memory.no_memory import NoMemory
# List of supported memory backends
# Add a backend to this list if the import attempt is successful
supported_memory = ["local", "no_memory"]
try:
from autogpt.memory.redismem import RedisMemory
supported_memory.append("redis")
except ImportError:
RedisMemory = None
try:
from autogpt.memory.pinecone import PineconeMemory
supported_memory.append("pinecone")
except ImportError:
PineconeMemory = None
try:
from autogpt.memory.weaviate import WeaviateMemory
supported_memory.append("weaviate")
except ImportError:
WeaviateMemory = None
try:
from autogpt.memory.milvus import MilvusMemory
supported_memory.append("milvus")
except ImportError:
MilvusMemory = None
def get_memory(cfg, init=False):
memory = None
if cfg.memory_backend == "pinecone":
if not PineconeMemory:
logger.warn(
"Error: Pinecone is not installed. Please install pinecone"
" to use Pinecone as a memory backend."
)
else:
memory = PineconeMemory(cfg)
if init:
memory.clear()
elif cfg.memory_backend == "redis":
if not RedisMemory:
logger.warn(
"Error: Redis is not installed. Please install redis-py to"
" use Redis as a memory backend."
)
else:
memory = RedisMemory(cfg)
elif cfg.memory_backend == "weaviate":
if not WeaviateMemory:
logger.warn(
"Error: Weaviate is not installed. Please install weaviate-client to"
" use Weaviate as a memory backend."
)
else:
memory = WeaviateMemory(cfg)
elif cfg.memory_backend == "milvus":
if not MilvusMemory:
logger.warn(
"Error: pymilvus sdk is not installed."
"Please install pymilvus to use Milvus or Zilliz Cloud as memory backend."
)
else:
memory = MilvusMemory(cfg)
elif cfg.memory_backend == "no_memory":
memory = NoMemory(cfg)
if memory is None:
memory = LocalCache(cfg)
if init:
memory.clear()
return memory
def get_supported_memory_backends():
return supported_memory
__all__ = [
"get_memory",
"LocalCache",
"RedisMemory",
"PineconeMemory",
"NoMemory",
"MilvusMemory",
"WeaviateMemory",
]

View File

@@ -1,31 +0,0 @@
"""Base class for memory providers."""
import abc
from autogpt.singleton import AbstractSingleton
class MemoryProviderSingleton(AbstractSingleton):
@abc.abstractmethod
def add(self, data):
"""Adds to memory"""
pass
@abc.abstractmethod
def get(self, data):
"""Gets from memory"""
pass
@abc.abstractmethod
def clear(self):
"""Clears memory"""
pass
@abc.abstractmethod
def get_relevant(self, data, num_relevant=5):
"""Gets relevant memory for"""
pass
@abc.abstractmethod
def get_stats(self):
"""Get stats from memory"""
pass

View File

@@ -1,126 +0,0 @@
from __future__ import annotations
import dataclasses
from pathlib import Path
from typing import Any, List
import numpy as np
import orjson
from autogpt.llm import get_ada_embedding
from autogpt.memory.base import MemoryProviderSingleton
EMBED_DIM = 1536
SAVE_OPTIONS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_SERIALIZE_DATACLASS
def create_default_embeddings():
return np.zeros((0, EMBED_DIM)).astype(np.float32)
@dataclasses.dataclass
class CacheContent:
texts: List[str] = dataclasses.field(default_factory=list)
embeddings: np.ndarray = dataclasses.field(
default_factory=create_default_embeddings
)
class LocalCache(MemoryProviderSingleton):
"""A class that stores the memory in a local file"""
def __init__(self, cfg) -> None:
"""Initialize a class instance
Args:
cfg: Config object
Returns:
None
"""
workspace_path = Path(cfg.workspace_path)
self.filename = workspace_path / f"{cfg.memory_index}.json"
self.filename.touch(exist_ok=True)
file_content = b"{}"
with self.filename.open("w+b") as f:
f.write(file_content)
self.data = CacheContent()
def add(self, text: str):
"""
Add text to our list of texts, add embedding as row to our
embeddings-matrix
Args:
text: str
Returns: None
"""
if "Command Error:" in text:
return ""
self.data.texts.append(text)
embedding = get_ada_embedding(text)
vector = np.array(embedding).astype(np.float32)
vector = vector[np.newaxis, :]
self.data.embeddings = np.concatenate(
[
self.data.embeddings,
vector,
],
axis=0,
)
with open(self.filename, "wb") as f:
out = orjson.dumps(self.data, option=SAVE_OPTIONS)
f.write(out)
return text
def clear(self) -> str:
"""
Clears the data in memory.
Returns: A message indicating that the memory has been cleared.
"""
self.data = CacheContent()
return "Obliviated"
def get(self, data: str) -> list[Any] | None:
"""
Gets the data from the memory that is most relevant to the given data.
Args:
data: The data to compare to.
Returns: The most relevant data.
"""
return self.get_relevant(data, 1)
def get_relevant(self, text: str, k: int) -> list[Any]:
""" "
matrix-vector mult to find score-for-each-row-of-matrix
get indices for top-k winning scores
return texts for those indices
Args:
text: str
k: int
Returns: List[str]
"""
embedding = get_ada_embedding(text)
scores = np.dot(self.data.embeddings, embedding)
top_k_indices = np.argsort(scores)[-k:][::-1]
return [self.data.texts[i] for i in top_k_indices]
def get_stats(self) -> tuple[int, tuple[int, ...]]:
"""
Returns: The stats of the local cache.
"""
return len(self.data.texts), self.data.embeddings.shape

View File

@@ -0,0 +1,241 @@
from __future__ import annotations
import copy
import json
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from autogpt.agent import Agent
from autogpt.config import Config
from autogpt.json_utils.utilities import (
LLM_DEFAULT_RESPONSE_FORMAT,
extract_json_from_response,
is_string_valid_json,
)
from autogpt.llm.base import ChatSequence, Message, MessageRole, MessageType
from autogpt.llm.providers.openai import OPEN_AI_CHAT_MODELS
from autogpt.llm.utils import count_string_tokens, create_chat_completion
from autogpt.log_cycle.log_cycle import PROMPT_SUMMARY_FILE_NAME, SUMMARY_FILE_NAME
from autogpt.logs import logger
@dataclass
class MessageHistory:
agent: Agent
messages: list[Message] = field(default_factory=list)
summary: str = "I was created"
last_trimmed_index: int = 0
def __getitem__(self, i: int):
return self.messages[i]
def __iter__(self):
return iter(self.messages)
def __len__(self):
return len(self.messages)
def add(
self,
role: MessageRole,
content: str,
type: MessageType | None = None,
):
return self.append(Message(role, content, type))
def append(self, message: Message):
return self.messages.append(message)
def trim_messages(
self,
current_message_chain: list[Message],
) -> tuple[Message, list[Message]]:
"""
Returns a list of trimmed messages: messages which are in the message history
but not in current_message_chain.
Args:
current_message_chain (list[Message]): The messages currently in the context.
Returns:
Message: A message with the new running summary after adding the trimmed messages.
list[Message]: A list of messages that are in full_message_history with an index higher than last_trimmed_index and absent from current_message_chain.
"""
# Select messages in full_message_history with an index higher than last_trimmed_index
new_messages = [
msg for i, msg in enumerate(self) if i > self.last_trimmed_index
]
# Remove messages that are already present in current_message_chain
new_messages_not_in_chain = [
msg for msg in new_messages if msg not in current_message_chain
]
if not new_messages_not_in_chain:
return self.summary_message(), []
new_summary_message = self.update_running_summary(
new_events=new_messages_not_in_chain
)
# Find the index of the last message processed
last_message = new_messages_not_in_chain[-1]
self.last_trimmed_index = self.messages.index(last_message)
return new_summary_message, new_messages_not_in_chain
def per_cycle(self, messages: list[Message] | None = None):
"""
Yields:
Message: a message containing user input
Message: a message from the AI containing a proposed action
Message: the message containing the result of the AI's proposed action
"""
messages = messages or self.messages
for i in range(0, len(messages) - 1):
ai_message = messages[i]
if ai_message.type != "ai_response":
continue
user_message = (
messages[i - 1] if i > 0 and messages[i - 1].role == "user" else None
)
result_message = messages[i + 1]
try:
assert is_string_valid_json(
ai_message.content, LLM_DEFAULT_RESPONSE_FORMAT
), "AI response is not a valid JSON object"
assert result_message.type == "action_result"
yield user_message, ai_message, result_message
except AssertionError as err:
logger.debug(
f"Invalid item in message history: {err}; Messages: {messages[i-1:i+2]}"
)
def summary_message(self) -> Message:
return Message(
"system",
f"This reminds you of these events from your past: \n{self.summary}",
)
def update_running_summary(self, new_events: list[Message]) -> Message:
"""
This function takes a list of dictionaries representing new events and combines them with the current summary,
focusing on key and potentially important information to remember. The updated summary is returned in a message
formatted in the 1st person past tense.
Args:
new_events (List[Dict]): A list of dictionaries containing the latest events to be added to the summary.
Returns:
str: A message containing the updated summary of actions, formatted in the 1st person past tense.
Example:
new_events = [{"event": "entered the kitchen."}, {"event": "found a scrawled note with the number 7"}]
update_running_summary(new_events)
# Returns: "This reminds you of these events from your past: \nI entered the kitchen and found a scrawled note saying 7."
"""
cfg = Config()
if not new_events:
return self.summary_message()
# Create a copy of the new_events list to prevent modifying the original list
new_events = copy.deepcopy(new_events)
# Replace "assistant" with "you". This produces much better first person past tense results.
for event in new_events:
if event.role.lower() == "assistant":
event.role = "you"
# Remove "thoughts" dictionary from "content"
try:
content_dict = extract_json_from_response(event.content)
if "thoughts" in content_dict:
del content_dict["thoughts"]
event.content = json.dumps(content_dict)
except json.JSONDecodeError as e:
logger.error(f"Error: Invalid JSON: {e}")
if cfg.debug_mode:
logger.error(f"{event.content}")
elif event.role.lower() == "system":
event.role = "your computer"
# Delete all user messages
elif event.role == "user":
new_events.remove(event)
# Summarize events and current summary in batch to a new running summary
# Assume an upper bound length for the summary prompt template, i.e. Your task is to create a concise running summary...., in summarize_batch func
# TODO make this default dynamic
prompt_template_length = 100
max_tokens = OPEN_AI_CHAT_MODELS.get(cfg.fast_llm_model).max_tokens
summary_tlength = count_string_tokens(str(self.summary), cfg.fast_llm_model)
batch = []
batch_tlength = 0
# TODO Can put a cap on length of total new events and drop some previous events to save API cost, but need to think thru more how to do it without losing the context
for event in new_events:
event_tlength = count_string_tokens(str(event), cfg.fast_llm_model)
if (
batch_tlength + event_tlength
> max_tokens - prompt_template_length - summary_tlength
):
# The batch is full. Summarize it and start a new one.
self.summarize_batch(batch, cfg)
summary_tlength = count_string_tokens(
str(self.summary), cfg.fast_llm_model
)
batch = [event]
batch_tlength = event_tlength
else:
batch.append(event)
batch_tlength += event_tlength
if batch:
# There's an unprocessed batch. Summarize it.
self.summarize_batch(batch, cfg)
return self.summary_message()
def summarize_batch(self, new_events_batch, cfg):
prompt = f'''Your task is to create a concise running summary of actions and information results in the provided text, focusing on key and potentially important information to remember.
You will receive the current summary and your latest actions. Combine them, adding relevant key information from the latest development in 1st person past tense and keeping the summary concise.
Summary So Far:
"""
{self.summary}
"""
Latest Development:
"""
{new_events_batch or "Nothing new happened."}
"""
'''
prompt = ChatSequence.for_model(cfg.fast_llm_model, [Message("user", prompt)])
self.agent.log_cycle_handler.log_cycle(
self.agent.ai_name,
self.agent.created_at,
self.agent.cycle_count,
prompt.raw(),
PROMPT_SUMMARY_FILE_NAME,
)
self.summary = create_chat_completion(prompt)
self.agent.log_cycle_handler.log_cycle(
self.agent.ai_name,
self.agent.created_at,
self.agent.cycle_count,
self.summary,
SUMMARY_FILE_NAME,
)

View File

@@ -1,162 +0,0 @@
""" Milvus memory storage provider."""
import re
from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections
from autogpt.config import Config
from autogpt.llm import get_ada_embedding
from autogpt.memory.base import MemoryProviderSingleton
class MilvusMemory(MemoryProviderSingleton):
"""Milvus memory storage provider."""
def __init__(self, cfg: Config) -> None:
"""Construct a milvus memory storage connection.
Args:
cfg (Config): Auto-GPT global config.
"""
self.configure(cfg)
connect_kwargs = {}
if self.username:
connect_kwargs["user"] = self.username
connect_kwargs["password"] = self.password
connections.connect(
**connect_kwargs,
uri=self.uri or "",
address=self.address or "",
secure=self.secure,
)
self.init_collection()
def configure(self, cfg: Config) -> None:
# init with configuration.
self.uri = None
self.address = cfg.milvus_addr
self.secure = cfg.milvus_secure
self.username = cfg.milvus_username
self.password = cfg.milvus_password
self.collection_name = cfg.milvus_collection
# use HNSW by default.
self.index_params = {
"metric_type": "IP",
"index_type": "HNSW",
"params": {"M": 8, "efConstruction": 64},
}
if (self.username is None) != (self.password is None):
raise ValueError(
"Both username and password must be set to use authentication for Milvus"
)
# configured address may be a full URL.
if re.match(r"^(https?|tcp)://", self.address) is not None:
self.uri = self.address
self.address = None
if self.uri.startswith("https"):
self.secure = True
# Zilliz Cloud requires AutoIndex.
if re.match(r"^https://(.*)\.zillizcloud\.(com|cn)", self.uri) is not None:
self.index_params = {
"metric_type": "IP",
"index_type": "AUTOINDEX",
"params": {},
}
def init_collection(self) -> None:
"""Initialize collection in vector database."""
fields = [
FieldSchema(name="pk", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embeddings", dtype=DataType.FLOAT_VECTOR, dim=1536),
FieldSchema(name="raw_text", dtype=DataType.VARCHAR, max_length=65535),
]
# create collection if not exist and load it.
self.schema = CollectionSchema(fields, "auto-gpt memory storage")
self.collection = Collection(self.collection_name, self.schema)
# create index if not exist.
if not self.collection.has_index():
self.collection.release()
self.collection.create_index(
"embeddings",
self.index_params,
index_name="embeddings",
)
self.collection.load()
def add(self, data) -> str:
"""Add an embedding of data into memory.
Args:
data (str): The raw text to construct embedding index.
Returns:
str: log.
"""
embedding = get_ada_embedding(data)
result = self.collection.insert([[embedding], [data]])
_text = (
"Inserting data into memory at primary key: "
f"{result.primary_keys[0]}:\n data: {data}"
)
return _text
def get(self, data):
"""Return the most relevant data in memory.
Args:
data: The data to compare to.
"""
return self.get_relevant(data, 1)
def clear(self) -> str:
"""Drop the index in memory.
Returns:
str: log.
"""
self.collection.drop()
self.collection = Collection(self.collection_name, self.schema)
self.collection.create_index(
"embeddings",
self.index_params,
index_name="embeddings",
)
self.collection.load()
return "Obliviated"
def get_relevant(self, data: str, num_relevant: int = 5):
"""Return the top-k relevant data in memory.
Args:
data: The data to compare to.
num_relevant (int, optional): The max number of relevant data.
Defaults to 5.
Returns:
list: The top-k relevant data.
"""
# search the embedding and return the most relevant text.
embedding = get_ada_embedding(data)
search_params = {
"metrics_type": "IP",
"params": {"nprobe": 8},
}
result = self.collection.search(
[embedding],
"embeddings",
search_params,
num_relevant,
output_fields=["raw_text"],
)
return [item.entity.value_of_field("raw_text") for item in result[0]]
def get_stats(self) -> str:
"""
Returns: The stats of the milvus cache.
"""
return f"Entities num: {self.collection.num_entities}"

View File

@@ -1,73 +0,0 @@
"""A class that does not store any data. This is the default memory provider."""
from __future__ import annotations
from typing import Any
from autogpt.memory.base import MemoryProviderSingleton
class NoMemory(MemoryProviderSingleton):
"""
A class that does not store any data. This is the default memory provider.
"""
def __init__(self, cfg):
"""
Initializes the NoMemory provider.
Args:
cfg: The config object.
Returns: None
"""
pass
def add(self, data: str) -> str:
"""
Adds a data point to the memory. No action is taken in NoMemory.
Args:
data: The data to add.
Returns: An empty string.
"""
return ""
def get(self, data: str) -> list[Any] | None:
"""
Gets the data from the memory that is most relevant to the given data.
NoMemory always returns None.
Args:
data: The data to compare to.
Returns: None
"""
return None
def clear(self) -> str:
"""
Clears the memory. No action is taken in NoMemory.
Returns: An empty string.
"""
return ""
def get_relevant(self, data: str, num_relevant: int = 5) -> list[Any] | None:
"""
Returns all the data in the memory that is relevant to the given data.
NoMemory always returns None.
Args:
data: The data to compare to.
num_relevant: The number of relevant data to return.
Returns: None
"""
return None
def get_stats(self):
"""
Returns: An empty dictionary as there are no stats in NoMemory.
"""
return {}

View File

@@ -1,75 +0,0 @@
import pinecone
from colorama import Fore, Style
from autogpt.llm import get_ada_embedding
from autogpt.logs import logger
from autogpt.memory.base import MemoryProviderSingleton
class PineconeMemory(MemoryProviderSingleton):
def __init__(self, cfg):
pinecone_api_key = cfg.pinecone_api_key
pinecone_region = cfg.pinecone_region
pinecone.init(api_key=pinecone_api_key, environment=pinecone_region)
dimension = 1536
metric = "cosine"
pod_type = "p1"
table_name = "auto-gpt"
# this assumes we don't start with memory.
# for now this works.
# we'll need a more complicated and robust system if we want to start with
# memory.
self.vec_num = 0
try:
pinecone.whoami()
except Exception as e:
logger.typewriter_log(
"FAILED TO CONNECT TO PINECONE",
Fore.RED,
Style.BRIGHT + str(e) + Style.RESET_ALL,
)
logger.double_check(
"Please ensure you have setup and configured Pinecone properly for use."
+ f"You can check out {Fore.CYAN + Style.BRIGHT}"
"https://github.com/Torantulino/Auto-GPT#-pinecone-api-key-setup"
f"{Style.RESET_ALL} to ensure you've set up everything correctly."
)
exit(1)
if table_name not in pinecone.list_indexes():
pinecone.create_index(
table_name, dimension=dimension, metric=metric, pod_type=pod_type
)
self.index = pinecone.Index(table_name)
def add(self, data):
vector = get_ada_embedding(data)
# no metadata here. We may wish to change that long term.
self.index.upsert([(str(self.vec_num), vector, {"raw_text": data})])
_text = f"Inserting data into memory at index: {self.vec_num}:\n data: {data}"
self.vec_num += 1
return _text
def get(self, data):
return self.get_relevant(data, 1)
def clear(self):
self.index.delete(deleteAll=True)
return "Obliviated"
def get_relevant(self, data, num_relevant=5):
"""
Returns all the data in the memory that is relevant to the given data.
:param data: The data to compare to.
:param num_relevant: The number of relevant data to return. Defaults to 5
"""
query_embedding = get_ada_embedding(data)
results = self.index.query(
query_embedding, top_k=num_relevant, include_metadata=True
)
sorted_results = sorted(results.matches, key=lambda x: x.score)
return [str(item["metadata"]["raw_text"]) for item in sorted_results]
def get_stats(self):
return self.index.describe_index_stats()

View File

@@ -1,156 +0,0 @@
"""Redis memory provider."""
from __future__ import annotations
from typing import Any
import numpy as np
import redis
from colorama import Fore, Style
from redis.commands.search.field import TextField, VectorField
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
from redis.commands.search.query import Query
from autogpt.llm import get_ada_embedding
from autogpt.logs import logger
from autogpt.memory.base import MemoryProviderSingleton
SCHEMA = [
TextField("data"),
VectorField(
"embedding",
"HNSW",
{"TYPE": "FLOAT32", "DIM": 1536, "DISTANCE_METRIC": "COSINE"},
),
]
class RedisMemory(MemoryProviderSingleton):
def __init__(self, cfg):
"""
Initializes the Redis memory provider.
Args:
cfg: The config object.
Returns: None
"""
redis_host = cfg.redis_host
redis_port = cfg.redis_port
redis_password = cfg.redis_password
self.dimension = 1536
self.redis = redis.Redis(
host=redis_host,
port=redis_port,
password=redis_password,
db=0, # Cannot be changed
)
self.cfg = cfg
# Check redis connection
try:
self.redis.ping()
except redis.ConnectionError as e:
logger.typewriter_log(
"FAILED TO CONNECT TO REDIS",
Fore.RED,
Style.BRIGHT + str(e) + Style.RESET_ALL,
)
logger.double_check(
"Please ensure you have setup and configured Redis properly for use. "
+ f"You can check out {Fore.CYAN + Style.BRIGHT}"
f"https://github.com/Torantulino/Auto-GPT#redis-setup{Style.RESET_ALL}"
" to ensure you've set up everything correctly."
)
exit(1)
if cfg.wipe_redis_on_start:
self.redis.flushall()
try:
self.redis.ft(f"{cfg.memory_index}").create_index(
fields=SCHEMA,
definition=IndexDefinition(
prefix=[f"{cfg.memory_index}:"], index_type=IndexType.HASH
),
)
except Exception as e:
logger.warn("Error creating Redis search index: ", e)
existing_vec_num = self.redis.get(f"{cfg.memory_index}-vec_num")
self.vec_num = int(existing_vec_num.decode("utf-8")) if existing_vec_num else 0
def add(self, data: str) -> str:
"""
Adds a data point to the memory.
Args:
data: The data to add.
Returns: Message indicating that the data has been added.
"""
if "Command Error:" in data:
return ""
vector = get_ada_embedding(data)
vector = np.array(vector).astype(np.float32).tobytes()
data_dict = {b"data": data, "embedding": vector}
pipe = self.redis.pipeline()
pipe.hset(f"{self.cfg.memory_index}:{self.vec_num}", mapping=data_dict)
_text = (
f"Inserting data into memory at index: {self.vec_num}:\n" f"data: {data}"
)
self.vec_num += 1
pipe.set(f"{self.cfg.memory_index}-vec_num", self.vec_num)
pipe.execute()
return _text
def get(self, data: str) -> list[Any] | None:
"""
Gets the data from the memory that is most relevant to the given data.
Args:
data: The data to compare to.
Returns: The most relevant data.
"""
return self.get_relevant(data, 1)
def clear(self) -> str:
"""
Clears the redis server.
Returns: A message indicating that the memory has been cleared.
"""
self.redis.flushall()
return "Obliviated"
def get_relevant(self, data: str, num_relevant: int = 5) -> list[Any] | None:
"""
Returns all the data in the memory that is relevant to the given data.
Args:
data: The data to compare to.
num_relevant: The number of relevant data to return.
Returns: A list of the most relevant data.
"""
query_embedding = get_ada_embedding(data)
base_query = f"*=>[KNN {num_relevant} @embedding $vector AS vector_score]"
query = (
Query(base_query)
.return_fields("data", "vector_score")
.sort_by("vector_score")
.dialect(2)
)
query_vector = np.array(query_embedding).astype(np.float32).tobytes()
try:
results = self.redis.ft(f"{self.cfg.memory_index}").search(
query, query_params={"vector": query_vector}
)
except Exception as e:
logger.warn("Error calling Redis search: ", e)
return None
return [result.data for result in results.docs]
def get_stats(self):
"""
Returns: The stats of the memory index.
"""
return self.redis.ft(f"{self.cfg.memory_index}").info()

View File

@@ -0,0 +1,138 @@
from autogpt.config import Config
from autogpt.logs import logger
from .memory_item import MemoryItem, MemoryItemRelevance
from .providers.base import VectorMemoryProvider as VectorMemory
from .providers.json_file import JSONFileMemory
from .providers.no_memory import NoMemory
# List of supported memory backends
# Add a backend to this list if the import attempt is successful
supported_memory = ["json_file", "no_memory"]
# try:
# from .providers.redis import RedisMemory
# supported_memory.append("redis")
# except ImportError:
# RedisMemory = None
# try:
# from .providers.pinecone import PineconeMemory
# supported_memory.append("pinecone")
# except ImportError:
# PineconeMemory = None
# try:
# from .providers.weaviate import WeaviateMemory
# supported_memory.append("weaviate")
# except ImportError:
# WeaviateMemory = None
# try:
# from .providers.milvus import MilvusMemory
# supported_memory.append("milvus")
# except ImportError:
# MilvusMemory = None
def get_memory(cfg: Config, init=False) -> VectorMemory:
memory = None
match cfg.memory_backend:
case "json_file":
memory = JSONFileMemory(cfg)
case "pinecone":
raise NotImplementedError(
"The Pinecone memory backend has been rendered incompatible by work on "
"the memory system, and was removed. Whether support will be added back "
"in the future is subject to discussion, feel free to pitch in: "
"https://github.com/Significant-Gravitas/Auto-GPT/discussions/4280"
)
# if not PineconeMemory:
# logger.warn(
# "Error: Pinecone is not installed. Please install pinecone"
# " to use Pinecone as a memory backend."
# )
# else:
# memory = PineconeMemory(cfg)
# if init:
# memory.clear()
case "redis":
raise NotImplementedError(
"The Redis memory backend has been rendered incompatible by work on "
"the memory system, and has been removed temporarily."
)
# if not RedisMemory:
# logger.warn(
# "Error: Redis is not installed. Please install redis-py to"
# " use Redis as a memory backend."
# )
# else:
# memory = RedisMemory(cfg)
case "weaviate":
raise NotImplementedError(
"The Weaviate memory backend has been rendered incompatible by work on "
"the memory system, and was removed. Whether support will be added back "
"in the future is subject to discussion, feel free to pitch in: "
"https://github.com/Significant-Gravitas/Auto-GPT/discussions/4280"
)
# if not WeaviateMemory:
# logger.warn(
# "Error: Weaviate is not installed. Please install weaviate-client to"
# " use Weaviate as a memory backend."
# )
# else:
# memory = WeaviateMemory(cfg)
case "milvus":
raise NotImplementedError(
"The Milvus memory backend has been rendered incompatible by work on "
"the memory system, and was removed. Whether support will be added back "
"in the future is subject to discussion, feel free to pitch in: "
"https://github.com/Significant-Gravitas/Auto-GPT/discussions/4280"
)
# if not MilvusMemory:
# logger.warn(
# "Error: pymilvus sdk is not installed."
# "Please install pymilvus to use Milvus or Zilliz Cloud as memory backend."
# )
# else:
# memory = MilvusMemory(cfg)
case "no_memory":
memory = NoMemory()
case _:
raise ValueError(
f"Unknown memory backend '{cfg.memory_backend}'. Please check your config."
)
if memory is None:
memory = JSONFileMemory(cfg)
return memory
def get_supported_memory_backends():
return supported_memory
__all__ = [
"get_memory",
"MemoryItem",
"MemoryItemRelevance",
"JSONFileMemory",
"NoMemory",
"VectorMemory",
# "RedisMemory",
# "PineconeMemory",
# "MilvusMemory",
# "WeaviateMemory",
]

View File

@@ -0,0 +1,223 @@
from __future__ import annotations
import dataclasses
import json
from typing import Literal
import numpy as np
from autogpt.config import Config
from autogpt.llm import Message
from autogpt.llm.utils import count_string_tokens
from autogpt.logs import logger
from autogpt.processing.text import chunk_content, split_text, summarize_text
from .utils import Embedding, get_embedding
MemoryDocType = Literal["webpage", "text_file", "code_file", "agent_history"]
@dataclasses.dataclass
class MemoryItem:
"""Memory object containing raw content as well as embeddings"""
raw_content: str
summary: str
chunks: list[str]
chunk_summaries: list[str]
e_summary: Embedding
e_chunks: list[Embedding]
metadata: dict
def relevance_for(self, query: str, e_query: Embedding | None = None):
return MemoryItemRelevance.of(self, query, e_query)
@staticmethod
def from_text(
text: str,
source_type: MemoryDocType,
metadata: dict = {},
how_to_summarize: str | None = None,
question_for_summary: str | None = None,
):
cfg = Config()
logger.debug(f"Memorizing text:\n{'-'*32}\n{text}\n{'-'*32}\n")
chunks = [
chunk
for chunk, _ in (
split_text(text, cfg.embedding_model)
if source_type != "code_file"
else chunk_content(text, cfg.embedding_model)
)
]
logger.debug("Chunks: " + str(chunks))
chunk_summaries = [
summary
for summary, _ in [
summarize_text(
text_chunk,
instruction=how_to_summarize,
question=question_for_summary,
)
for text_chunk in chunks
]
]
logger.debug("Chunk summaries: " + str(chunk_summaries))
e_chunks = get_embedding(chunks)
summary = (
chunk_summaries[0]
if len(chunks) == 1
else summarize_text(
"\n\n".join(chunk_summaries),
instruction=how_to_summarize,
question=question_for_summary,
)[0]
)
logger.debug("Total summary: " + summary)
# TODO: investigate search performance of weighted average vs summary
# e_average = np.average(e_chunks, axis=0, weights=[len(c) for c in chunks])
e_summary = get_embedding(summary)
metadata["source_type"] = source_type
return MemoryItem(
text,
summary,
chunks,
chunk_summaries,
e_summary,
e_chunks,
metadata=metadata,
)
@staticmethod
def from_text_file(content: str, path: str):
return MemoryItem.from_text(content, "text_file", {"location": path})
@staticmethod
def from_code_file(content: str, path: str):
# TODO: implement tailored code memories
return MemoryItem.from_text(content, "code_file", {"location": path})
@staticmethod
def from_ai_action(ai_message: Message, result_message: Message):
# The result_message contains either user feedback
# or the result of the command specified in ai_message
if ai_message["role"] != "assistant":
raise ValueError(f"Invalid role on 'ai_message': {ai_message['role']}")
result = (
result_message["content"]
if result_message["content"].startswith("Command")
else "None"
)
user_input = (
result_message["content"]
if result_message["content"].startswith("Human feedback")
else "None"
)
memory_content = (
f"Assistant Reply: {ai_message['content']}"
"\n\n"
f"Result: {result}"
"\n\n"
f"Human Feedback: {user_input}"
)
return MemoryItem.from_text(
text=memory_content,
source_type="agent_history",
how_to_summarize="if possible, also make clear the link between the command in the assistant's response and the command result. Do not mention the human feedback if there is none",
)
@staticmethod
def from_webpage(content: str, url: str, question: str | None = None):
return MemoryItem.from_text(
text=content,
source_type="webpage",
metadata={"location": url},
question_for_summary=question,
)
def dump(self) -> str:
token_length = count_string_tokens(self.raw_content, Config().embedding_model)
return f"""
=============== MemoryItem ===============
Length: {token_length} tokens in {len(self.e_chunks)} chunks
Metadata: {json.dumps(self.metadata, indent=2)}
---------------- SUMMARY -----------------
{self.summary}
------------------ RAW -------------------
{self.raw_content}
==========================================
"""
@dataclasses.dataclass
class MemoryItemRelevance:
"""
Class that encapsulates memory relevance search functionality and data.
Instances contain a MemoryItem and its relevance scores for a given query.
"""
memory_item: MemoryItem
for_query: str
summary_relevance_score: float
chunk_relevance_scores: list[float]
@staticmethod
def of(
memory_item: MemoryItem, for_query: str, e_query: Embedding | None = None
) -> MemoryItemRelevance:
e_query = e_query or get_embedding(for_query)
_, srs, crs = MemoryItemRelevance.calculate_scores(memory_item, e_query)
return MemoryItemRelevance(
for_query=for_query,
memory_item=memory_item,
summary_relevance_score=srs,
chunk_relevance_scores=crs,
)
@staticmethod
def calculate_scores(
memory: MemoryItem, compare_to: Embedding
) -> tuple[float, float, list[float]]:
"""
Calculates similarity between given embedding and all embeddings of the memory
Returns:
float: the aggregate (max) relevance score of the memory
float: the relevance score of the memory summary
list: the relevance scores of the memory chunks
"""
summary_relevance_score = np.dot(memory.e_summary, compare_to)
chunk_relevance_scores = np.dot(memory.e_chunks, compare_to)
logger.debug(f"Relevance of summary: {summary_relevance_score}")
logger.debug(f"Relevance of chunks: {chunk_relevance_scores}")
relevance_scores = [summary_relevance_score, *chunk_relevance_scores]
logger.debug(f"Relevance scores: {relevance_scores}")
return max(relevance_scores), summary_relevance_score, chunk_relevance_scores
@property
def score(self) -> float:
"""The aggregate relevance score of the memory item for the given query"""
return max([self.summary_relevance_score, *self.chunk_relevance_scores])
@property
def most_relevant_chunk(self) -> tuple[str, float]:
"""The most relevant chunk of the memory item + its score for the given query"""
i_relmax = np.argmax(self.chunk_relevance_scores)
return self.memory_item.chunks[i_relmax], self.chunk_relevance_scores[i_relmax]
def __str__(self):
return (
f"{self.memory_item.summary} ({self.summary_relevance_score}) "
f"{self.chunk_relevance_scores}"
)

View File

@@ -0,0 +1,7 @@
from .json_file import JSONFileMemory
from .no_memory import NoMemory
__all__ = [
"JSONFileMemory",
"NoMemory",
]

View File

@@ -0,0 +1,74 @@
import abc
import functools
from typing import MutableSet, Sequence
import numpy as np
from autogpt.config.config import Config
from autogpt.logs import logger
from autogpt.singleton import AbstractSingleton
from .. import MemoryItem, MemoryItemRelevance
from ..utils import Embedding, get_embedding
class VectorMemoryProvider(MutableSet[MemoryItem], AbstractSingleton):
@abc.abstractmethod
def __init__(self, config: Config):
pass
def get(self, query: str) -> MemoryItemRelevance | None:
"""
Gets the data from the memory that is most relevant to the given query.
Args:
data: The data to compare to.
Returns: The most relevant Memory
"""
result = self.get_relevant(query, 1)
return result[0] if result else None
def get_relevant(self, query: str, k: int) -> Sequence[MemoryItemRelevance]:
"""
Returns the top-k most relevant memories for the given query
Args:
query: the query to compare stored memories to
k: the number of relevant memories to fetch
Returns:
list[MemoryItemRelevance] containing the top [k] relevant memories
"""
if len(self) < 1:
return []
logger.debug(
f"Searching for {k} relevant memories for query '{query}'; "
f"{len(self)} memories in index"
)
relevances = self.score_memories_for_relevance(query)
logger.debug(f"Memory relevance scores: {[str(r) for r in relevances]}")
# take last k items and reverse
top_k_indices = np.argsort([r.score for r in relevances])[-k:][::-1]
return [relevances[i] for i in top_k_indices]
def score_memories_for_relevance(
self, for_query: str
) -> Sequence[MemoryItemRelevance]:
"""
Returns MemoryItemRelevance for every memory in the index.
Implementations may override this function for performance purposes.
"""
e_query: Embedding = get_embedding(for_query)
return [m.relevance_for(for_query, e_query) for m in self]
def get_stats(self) -> tuple[int, int]:
"""
Returns:
tuple (n_memories: int, n_chunks: int): the stats of the memory index
"""
return len(self), functools.reduce(lambda t, m: t + len(m.e_chunks), self, 0)

View File

@@ -0,0 +1,68 @@
from __future__ import annotations
from pathlib import Path
from typing import Iterator
import orjson
from autogpt.config import Config
from autogpt.logs import logger
from ..memory_item import MemoryItem
from .base import VectorMemoryProvider
class JSONFileMemory(VectorMemoryProvider):
"""Memory backend that stores memories in a JSON file"""
SAVE_OPTIONS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_SERIALIZE_DATACLASS
file_path: Path
memories: list[MemoryItem]
def __init__(self, cfg: Config) -> None:
"""Initialize a class instance
Args:
cfg: Config object
Returns:
None
"""
workspace_path = Path(cfg.workspace_path)
self.file_path = workspace_path / f"{cfg.memory_index}.json"
self.file_path.touch()
logger.debug(f"Initialized {__name__} with index path {self.file_path}")
self.memories = []
self.save_index()
def __iter__(self) -> Iterator[MemoryItem]:
return iter(self.memories)
def __contains__(self, x: MemoryItem) -> bool:
return x in self.memories
def __len__(self) -> int:
return len(self.memories)
def add(self, item: MemoryItem):
self.memories.append(item)
self.save_index()
return len(self.memories)
def discard(self, item: MemoryItem):
try:
self.remove(item)
except:
pass
def clear(self):
"""Clears the data in memory."""
self.memories.clear()
self.save_index()
def save_index(self):
logger.debug(f"Saving memory index to file {self.file_path}")
with self.file_path.open("wb") as f:
return f.write(orjson.dumps(self.memories, option=self.SAVE_OPTIONS))

View File

@@ -0,0 +1,36 @@
"""A class that does not store any data. This is the default memory provider."""
from __future__ import annotations
from typing import Iterator, Optional
from autogpt.config.config import Config
from .. import MemoryItem
from .base import VectorMemoryProvider
class NoMemory(VectorMemoryProvider):
"""
A class that does not store any data. This is the default memory provider.
"""
def __init__(self, config: Optional[Config] = None):
pass
def __iter__(self) -> Iterator[MemoryItem]:
return iter([])
def __contains__(self, x: MemoryItem) -> bool:
return False
def __len__(self) -> int:
return 0
def add(self, item: MemoryItem):
pass
def discard(self, item: MemoryItem):
pass
def clear(self):
pass

View File

@@ -0,0 +1,70 @@
from typing import Any, overload
import numpy as np
import openai
from autogpt.config import Config
from autogpt.llm.utils import metered, retry_openai_api
from autogpt.logs import logger
Embedding = list[np.float32] | np.ndarray[Any, np.dtype[np.float32]]
"""Embedding vector"""
TText = list[int]
"""Token array representing text"""
@overload
def get_embedding(input: str | TText) -> Embedding:
...
@overload
def get_embedding(input: list[str] | list[TText]) -> list[Embedding]:
...
@metered
@retry_openai_api()
def get_embedding(
input: str | TText | list[str] | list[TText],
) -> Embedding | list[Embedding]:
"""Get an embedding from the ada model.
Args:
input: Input text to get embeddings for, encoded as a string or array of tokens.
Multiple inputs may be given as a list of strings or token arrays.
Returns:
List[float]: The embedding.
"""
cfg = Config()
multiple = isinstance(input, list) and all(not isinstance(i, int) for i in input)
if isinstance(input, str):
input = input.replace("\n", " ")
elif multiple and isinstance(input[0], str):
input = [text.replace("\n", " ") for text in input]
model = cfg.embedding_model
if cfg.use_azure:
kwargs = {"engine": cfg.get_azure_deployment_id_for_model(model)}
else:
kwargs = {"model": model}
logger.debug(
f"Getting embedding{f's for {len(input)} inputs' if multiple else ''}"
f" with model '{model}'"
+ (f" via Azure deployment '{kwargs['engine']}'" if cfg.use_azure else "")
)
embeddings = openai.Embedding.create(
input=input,
api_key=cfg.openai_api_key,
**kwargs,
).data
if not multiple:
return embeddings[0]["embedding"]
embeddings = sorted(embeddings, key=lambda x: x["index"])
return [d["embedding"] for d in embeddings]

View File

@@ -1,127 +0,0 @@
import weaviate
from weaviate import Client
from weaviate.embedded import EmbeddedOptions
from weaviate.util import generate_uuid5
from autogpt.llm import get_ada_embedding
from autogpt.logs import logger
from autogpt.memory.base import MemoryProviderSingleton
def default_schema(weaviate_index):
return {
"class": weaviate_index,
"properties": [
{
"name": "raw_text",
"dataType": ["text"],
"description": "original text for the embedding",
}
],
}
class WeaviateMemory(MemoryProviderSingleton):
def __init__(self, cfg):
auth_credentials = self._build_auth_credentials(cfg)
url = f"{cfg.weaviate_protocol}://{cfg.weaviate_host}:{cfg.weaviate_port}"
if cfg.use_weaviate_embedded:
self.client = Client(
embedded_options=EmbeddedOptions(
hostname=cfg.weaviate_host,
port=int(cfg.weaviate_port),
persistence_data_path=cfg.weaviate_embedded_path,
)
)
logger.info(
f"Weaviate Embedded running on: {url} with persistence path: {cfg.weaviate_embedded_path}"
)
else:
self.client = Client(url, auth_client_secret=auth_credentials)
self.index = WeaviateMemory.format_classname(cfg.memory_index)
self._create_schema()
@staticmethod
def format_classname(index):
# weaviate uses capitalised index names
# The python client uses the following code to format
# index names before the corresponding class is created
index = index.replace("-", "_")
if len(index) == 1:
return index.capitalize()
return index[0].capitalize() + index[1:]
def _create_schema(self):
schema = default_schema(self.index)
if not self.client.schema.contains(schema):
self.client.schema.create_class(schema)
def _build_auth_credentials(self, cfg):
if cfg.weaviate_username and cfg.weaviate_password:
return weaviate.AuthClientPassword(
cfg.weaviate_username, cfg.weaviate_password
)
if cfg.weaviate_api_key:
return weaviate.AuthApiKey(api_key=cfg.weaviate_api_key)
else:
return None
def add(self, data):
vector = get_ada_embedding(data)
doc_uuid = generate_uuid5(data, self.index)
data_object = {"raw_text": data}
with self.client.batch as batch:
batch.add_data_object(
uuid=doc_uuid,
data_object=data_object,
class_name=self.index,
vector=vector,
)
return f"Inserting data into memory at uuid: {doc_uuid}:\n data: {data}"
def get(self, data):
return self.get_relevant(data, 1)
def clear(self):
self.client.schema.delete_all()
# weaviate does not yet have a neat way to just remove the items in an index
# without removing the entire schema, therefore we need to re-create it
# after a call to delete_all
self._create_schema()
return "Obliterated"
def get_relevant(self, data, num_relevant=5):
query_embedding = get_ada_embedding(data)
try:
results = (
self.client.query.get(self.index, ["raw_text"])
.with_near_vector({"vector": query_embedding, "certainty": 0.7})
.with_limit(num_relevant)
.do()
)
if len(results["data"]["Get"][self.index]) > 0:
return [
str(item["raw_text"]) for item in results["data"]["Get"][self.index]
]
else:
return []
except Exception as err:
logger.warn(f"Unexpected error {err=}, {type(err)=}")
return []
def get_stats(self):
result = self.client.query.aggregate(self.index).with_meta_count().do()
class_data = result["data"]["Aggregate"][self.index]
return class_data[0]["meta"] if class_data else {}

View File

@@ -1,33 +0,0 @@
from autogpt.json_utils.utilities import (
LLM_DEFAULT_RESPONSE_FORMAT,
is_string_valid_json,
)
from autogpt.logs import logger
def format_memory(assistant_reply, next_message_content):
# the next_message_content is a variable to stores either the user_input or the command following the assistant_reply
result = (
"None" if next_message_content.startswith("Command") else next_message_content
)
user_input = (
"None"
if next_message_content.startswith("Human feedback")
else next_message_content
)
return f"Assistant Reply: {assistant_reply}\nResult: {result}\nHuman Feedback:{user_input}"
def save_memory_trimmed_from_context_window(
full_message_history, next_message_to_add_index, permanent_memory
):
while next_message_to_add_index >= 0:
message_content = full_message_history[next_message_to_add_index]["content"]
if is_string_valid_json(message_content, LLM_DEFAULT_RESPONSE_FORMAT):
next_message = full_message_history[next_message_to_add_index + 1]
memory_to_add = format_memory(message_content, next_message["content"])
logger.debug(f"Storing the following memory: {memory_to_add}")
permanent_memory.add(memory_to_add)
next_message_to_add_index -= 1

View File

@@ -1,112 +0,0 @@
import json
from typing import Dict, List, Tuple
from autogpt.config import Config
from autogpt.llm.llm_utils import create_chat_completion
cfg = Config()
def get_newly_trimmed_messages(
full_message_history: List[Dict[str, str]],
current_context: List[Dict[str, str]],
last_memory_index: int,
) -> Tuple[List[Dict[str, str]], int]:
"""
This function returns a list of dictionaries contained in full_message_history
with an index higher than prev_index that are absent from current_context.
Args:
full_message_history (list): A list of dictionaries representing the full message history.
current_context (list): A list of dictionaries representing the current context.
last_memory_index (int): An integer representing the previous index.
Returns:
list: A list of dictionaries that are in full_message_history with an index higher than last_memory_index and absent from current_context.
int: The new index value for use in the next loop.
"""
# Select messages in full_message_history with an index higher than last_memory_index
new_messages = [
msg for i, msg in enumerate(full_message_history) if i > last_memory_index
]
# Remove messages that are already present in current_context
new_messages_not_in_context = [
msg for msg in new_messages if msg not in current_context
]
# Find the index of the last message processed
new_index = last_memory_index
if new_messages_not_in_context:
last_message = new_messages_not_in_context[-1]
new_index = full_message_history.index(last_message)
return new_messages_not_in_context, new_index
def update_running_summary(current_memory: str, new_events: List[Dict]) -> str:
"""
This function takes a list of dictionaries representing new events and combines them with the current summary,
focusing on key and potentially important information to remember. The updated summary is returned in a message
formatted in the 1st person past tense.
Args:
new_events (List[Dict]): A list of dictionaries containing the latest events to be added to the summary.
Returns:
str: A message containing the updated summary of actions, formatted in the 1st person past tense.
Example:
new_events = [{"event": "entered the kitchen."}, {"event": "found a scrawled note with the number 7"}]
update_running_summary(new_events)
# Returns: "This reminds you of these events from your past: \nI entered the kitchen and found a scrawled note saying 7."
"""
# Replace "assistant" with "you". This produces much better first person past tense results.
for event in new_events:
if event["role"].lower() == "assistant":
event["role"] = "you"
# Remove "thoughts" dictionary from "content"
content_dict = json.loads(event["content"])
if "thoughts" in content_dict:
del content_dict["thoughts"]
event["content"] = json.dumps(content_dict)
elif event["role"].lower() == "system":
event["role"] = "your computer"
# Delete all user messages
elif event["role"] == "user":
new_events.remove(event)
# This can happen at any point during execturion, not just the beginning
if len(new_events) == 0:
new_events = "Nothing new happened."
prompt = f'''Your task is to create a concise running summary of actions and information results in the provided text, focusing on key and potentially important information to remember.
You will receive the current summary and the your latest actions. Combine them, adding relevant key information from the latest development in 1st person past tense and keeping the summary concise.
Summary So Far:
"""
{current_memory}
"""
Latest Development:
"""
{new_events}
"""
'''
messages = [
{
"role": "user",
"content": prompt,
}
]
current_memory = create_chat_completion(messages, cfg.fast_llm_model)
message_to_return = {
"role": "system",
"content": f"This reminds you of these events from your past: \n{current_memory}",
}
return message_to_return

View File

@@ -68,7 +68,6 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate):
prompt (PromptGenerator): The prompt generator.
messages (List[str]): The list of messages.
"""
pass
def can_handle_post_planning(self) -> bool:
"""This method is called to check that the plugin can
@@ -116,7 +115,6 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate):
Returns:
Optional[str]: The resulting message.
"""
pass
def can_handle_post_instruction(self) -> bool:
"""This method is called to check that the plugin can
@@ -196,4 +194,56 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate):
Returns:
str: The resulting response.
"""
pass
def can_handle_text_embedding(self, text: str) -> bool:
"""This method is called to check that the plugin can
handle the text_embedding method.
Args:
text (str): The text to be convert to embedding.
Returns:
bool: True if the plugin can handle the text_embedding method."""
return False
def handle_text_embedding(self, text: str) -> list:
"""This method is called when the chat completion is done.
Args:
text (str): The text to be convert to embedding.
Returns:
list: The text embedding.
"""
def can_handle_user_input(self, user_input: str) -> bool:
"""This method is called to check that the plugin can
handle the user_input method.
Args:
user_input (str): The user input.
Returns:
bool: True if the plugin can handle the user_input method."""
return False
def user_input(self, user_input: str) -> str:
"""This method is called to request user input to the user.
Args:
user_input (str): The question or prompt to ask the user.
Returns:
str: The user input.
"""
def can_handle_report(self) -> bool:
"""This method is called to check that the plugin can
handle the report method.
Returns:
bool: True if the plugin can handle the report method."""
return False
def report(self, message: str) -> None:
"""This method is called to report a message to the user.
Args:
message (str): The message to report.
"""

View File

@@ -1,23 +1,29 @@
"""Handles loading of plugins."""
import importlib
import importlib.util
import inspect
import json
import os
import sys
import zipfile
from pathlib import Path
from typing import List, Optional, Tuple
from typing import List
from urllib.parse import urlparse
from zipimport import zipimporter
import openapi_python_client
import requests
from auto_gpt_plugin_template import AutoGPTPluginTemplate
from openapi_python_client.cli import Config as OpenAPIConfig
from openapi_python_client.config import Config as OpenAPIConfig
from autogpt.config import Config
from autogpt.config.config import Config
from autogpt.logs import logger
from autogpt.models.base_open_ai_plugin import BaseOpenAIPlugin
DEFAULT_PLUGINS_CONFIG_FILE = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "..", "plugins_config.yaml"
)
def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]:
"""
@@ -33,7 +39,7 @@ def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]:
result = []
with zipfile.ZipFile(zip_path, "r") as zfile:
for name in zfile.namelist():
if name.endswith("__init__.py"):
if name.endswith("__init__.py") and not name.startswith("__MACOSX"):
logger.debug(f"Found module '{name}' in the zipfile at: {name}")
result.append(name)
if len(result) == 0:
@@ -152,7 +158,7 @@ def initialize_openai_plugins(
)
prev_cwd = Path.cwd()
os.chdir(openai_plugin_client_dir)
Path("ai-plugin.json")
if not os.path.exists("client"):
client_results = openapi_python_client.create_new_client(
url=manifest_spec["manifest"]["api"]["url"],
@@ -170,9 +176,13 @@ def initialize_openai_plugins(
"client", "client/client/client.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
try:
spec.loader.exec_module(module)
finally:
os.chdir(prev_cwd)
client = module.Client(base_url=url)
os.chdir(prev_cwd)
manifest_spec["client"] = client
return manifests_specs
@@ -209,6 +219,33 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate
loaded_plugins = []
# Generic plugins
plugins_path_path = Path(cfg.plugins_dir)
plugins_config = cfg.plugins_config
# Directory-based plugins
for plugin_path in [f.path for f in os.scandir(cfg.plugins_dir) if f.is_dir()]:
# Avoid going into __pycache__ or other hidden directories
if plugin_path.startswith("__"):
continue
plugin_module_path = plugin_path.split(os.path.sep)
plugin_module_name = plugin_module_path[-1]
qualified_module_name = ".".join(plugin_module_path)
__import__(qualified_module_name)
plugin = sys.modules[qualified_module_name]
if not plugins_config.is_enabled(plugin_module_name):
logger.warn(f"Plugin {plugin_module_name} found but not configured")
continue
for _, class_obj in inspect.getmembers(plugin):
if (
hasattr(class_obj, "_abc_impl")
and AutoGPTPluginTemplate in class_obj.__bases__
):
loaded_plugins.append(class_obj())
# Zip-based plugins
for plugin in plugins_path_path.glob("*.zip"):
if moduleList := inspect_zip_for_modules(str(plugin), debug):
for module in moduleList:
@@ -217,6 +254,7 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate
logger.debug(f"Plugin: {plugin} Module: {module}")
zipped_package = zipimporter(str(plugin))
zipped_module = zipped_package.load_module(str(module.parent))
for key in dir(zipped_module):
if key.startswith("__"):
continue
@@ -225,9 +263,28 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate
if (
"_abc_impl" in a_keys
and a_module.__name__ != "AutoGPTPluginTemplate"
and denylist_allowlist_check(a_module.__name__, cfg)
):
loaded_plugins.append(a_module())
plugin_name = a_module.__name__
plugin_configured = plugins_config.get(plugin_name) is not None
plugin_enabled = plugins_config.is_enabled(plugin_name)
if plugin_configured and plugin_enabled:
logger.debug(
f"Loading plugin {plugin_name} as it was enabled in config."
)
loaded_plugins.append(a_module())
elif plugin_configured and not plugin_enabled:
logger.debug(
f"Not loading plugin {plugin_name} as it was disabled in config."
)
elif not plugin_configured:
logger.warn(
f"Not loading plugin {plugin_name} as it was not found in config. "
f"Please check your config. Starting with 0.4.1, plugins will not be loaded unless "
f"they are enabled in plugins_config.yaml. Zipped plugins should use the class "
f"name ({plugin_name}) as the key."
)
# OpenAI plugins
if cfg.plugins_openai:
manifests_specs = fetch_openai_plugins_manifest_and_spec(cfg)
@@ -236,33 +293,15 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate
manifests_specs, cfg, debug
)
for url, openai_plugin_meta in manifests_specs_clients.items():
if denylist_allowlist_check(url, cfg):
plugin = BaseOpenAIPlugin(openai_plugin_meta)
loaded_plugins.append(plugin)
if not plugins_config.is_enabled(url):
logger.warn(f"Plugin {plugin_module_name} found but not configured")
continue
plugin = BaseOpenAIPlugin(openai_plugin_meta)
loaded_plugins.append(plugin)
if loaded_plugins:
logger.info(f"\nPlugins found: {len(loaded_plugins)}\n" "--------------------")
for plugin in loaded_plugins:
logger.info(f"{plugin._name}: {plugin._version} - {plugin._description}")
return loaded_plugins
def denylist_allowlist_check(plugin_name: str, cfg: Config) -> bool:
"""Check if the plugin is in the allowlist or denylist.
Args:
plugin_name (str): Name of the plugin.
cfg (Config): Config object.
Returns:
True or False
"""
if plugin_name in cfg.plugins_denylist:
return False
if plugin_name in cfg.plugins_allowlist:
return True
ack = input(
f"WARNING: Plugin {plugin_name} found. But not in the"
f" allowlist... Load? ({cfg.authorise_key}/{cfg.exit_key}): "
)
return ack.lower() == cfg.authorise_key

View File

@@ -0,0 +1,14 @@
from typing import Any
class PluginConfig:
"""Class for holding configuration of a single plugin"""
def __init__(self, name: str, enabled: bool = False, config: dict[str, Any] = None):
self.name = name
self.enabled = enabled
# Arbitray config options for this plugin. API keys or plugin-specific options live here.
self.config = config or {}
def __repr__(self):
return f"PluginConfig('{self.name}', {self.enabled}, {str(self.config)}"

View File

@@ -0,0 +1,81 @@
import os
from typing import Any, Union
import yaml
from autogpt.config.config import Config
from autogpt.logs import logger
from autogpt.plugins.plugin_config import PluginConfig
class PluginsConfig:
"""Class for holding configuration of all plugins"""
def __init__(self, plugins_config: dict[str, Any]):
self.plugins = {}
for name, plugin in plugins_config.items():
if type(plugin) == dict:
self.plugins[name] = PluginConfig(
name,
plugin.get("enabled", False),
plugin.get("config", {}),
)
elif type(plugin) == PluginConfig:
self.plugins[name] = plugin
else:
raise ValueError(f"Invalid plugin config data type: {type(plugin)}")
def __repr__(self):
return f"PluginsConfig({self.plugins})"
def get(self, name: str) -> Union[PluginConfig, None]:
return self.plugins.get(name)
def is_enabled(self, name) -> bool:
plugin_config = self.plugins.get(name)
return plugin_config and plugin_config.enabled
@classmethod
def load_config(cls, global_config: Config) -> "PluginsConfig":
empty_config = cls({})
try:
config_data = cls.deserialize_config_file(global_config=global_config)
if type(config_data) != dict:
logger.error(
f"Expected plugins config to be a dict, got {type(config_data)}, continuing without plugins"
)
return empty_config
return cls(config_data)
except BaseException as e:
logger.error(
f"Plugin config is invalid, continuing without plugins. Error: {e}"
)
return empty_config
@classmethod
def deserialize_config_file(cls, global_config: Config) -> dict[str, Any]:
plugins_config_path = global_config.plugins_config_file
if not os.path.exists(plugins_config_path):
logger.warn("plugins_config.yaml does not exist, creating base config.")
cls.create_empty_plugins_config(global_config=global_config)
with open(plugins_config_path, "r") as f:
return yaml.load(f, Loader=yaml.FullLoader)
@staticmethod
def create_empty_plugins_config(global_config: Config):
"""Create an empty plugins_config.yaml file. Fill it with values from old env variables."""
base_config = {}
# Backwards-compatibility shim
for plugin_name in global_config.plugins_denylist:
base_config[plugin_name] = {"enabled": False, "config": {}}
for plugin_name in global_config.plugins_allowlist:
base_config[plugin_name] = {"enabled": True, "config": {}}
with open(global_config.plugins_config_file, "w+") as f:
f.write(yaml.dump(base_config))
return base_config

View File

@@ -1,170 +1,234 @@
"""Text processing functions"""
from typing import Dict, Generator, Optional
from math import ceil
from typing import Optional
import spacy
from selenium.webdriver.remote.webdriver import WebDriver
import tiktoken
from autogpt.config import Config
from autogpt.llm import count_message_tokens, create_chat_completion
from autogpt.llm.base import ChatSequence
from autogpt.llm.providers.openai import OPEN_AI_MODELS
from autogpt.llm.utils import count_string_tokens, create_chat_completion
from autogpt.logs import logger
from autogpt.memory import get_memory
from autogpt.utils import batch
CFG = Config()
def _max_chunk_length(model: str, max: Optional[int] = None) -> int:
model_max_input_tokens = OPEN_AI_MODELS[model].max_tokens - 1
if max is not None and max > 0:
return min(max, model_max_input_tokens)
return model_max_input_tokens
def must_chunk_content(
text: str, for_model: str, max_chunk_length: Optional[int] = None
) -> bool:
return count_string_tokens(text, for_model) > _max_chunk_length(
for_model, max_chunk_length
)
def chunk_content(
content: str,
for_model: str,
max_chunk_length: Optional[int] = None,
with_overlap=True,
):
"""Split content into chunks of approximately equal token length."""
MAX_OVERLAP = 200 # limit overlap to save tokens
if not must_chunk_content(content, for_model, max_chunk_length):
yield content, count_string_tokens(content, for_model)
return
max_chunk_length = max_chunk_length or _max_chunk_length(for_model)
tokenizer = tiktoken.encoding_for_model(for_model)
tokenized_text = tokenizer.encode(content)
total_length = len(tokenized_text)
n_chunks = ceil(total_length / max_chunk_length)
chunk_length = ceil(total_length / n_chunks)
overlap = min(max_chunk_length - chunk_length, MAX_OVERLAP) if with_overlap else 0
for token_batch in batch(tokenized_text, chunk_length + overlap, overlap):
yield tokenizer.decode(token_batch), len(token_batch)
def summarize_text(
text: str, instruction: Optional[str] = None, question: Optional[str] = None
) -> tuple[str, None | list[tuple[str, str]]]:
"""Summarize text using the OpenAI API
Args:
text (str): The text to summarize
instruction (str): Additional instruction for summarization, e.g. "focus on information related to polar bears", "omit personal information contained in the text"
Returns:
str: The summary of the text
list[(summary, chunk)]: Text chunks and their summary, if the text was chunked.
None otherwise.
"""
if not text:
raise ValueError("No text to summarize")
if instruction and question:
raise ValueError("Parameters 'question' and 'instructions' cannot both be set")
model = CFG.fast_llm_model
if question:
instruction = (
f'include any information that can be used to answer the question "{question}". '
"Do not directly answer the question itself"
)
summarization_prompt = ChatSequence.for_model(model)
token_length = count_string_tokens(text, model)
logger.info(f"Text length: {token_length} tokens")
# reserve 50 tokens for summary prompt, 500 for the response
max_chunk_length = _max_chunk_length(model) - 550
logger.info(f"Max chunk length: {max_chunk_length} tokens")
if not must_chunk_content(text, model, max_chunk_length):
# summarization_prompt.add("user", text)
summarization_prompt.add(
"user",
"Write a concise summary of the following text"
f"{f'; {instruction}' if instruction is not None else ''}:"
"\n\n\n"
f'LITERAL TEXT: """{text}"""'
"\n\n\n"
"CONCISE SUMMARY: The text is best summarized as"
# "Only respond with a concise summary or description of the user message."
)
logger.debug(f"Summarizing with {model}:\n{summarization_prompt.dump()}\n")
summary = create_chat_completion(
summarization_prompt, temperature=0, max_tokens=500
)
logger.debug(f"\n{'-'*16} SUMMARY {'-'*17}\n{summary}\n{'-'*42}\n")
return summary.strip(), None
summaries: list[str] = []
chunks = list(split_text(text, for_model=model, max_chunk_length=max_chunk_length))
for i, (chunk, chunk_length) in enumerate(chunks):
logger.info(
f"Summarizing chunk {i + 1} / {len(chunks)} of length {chunk_length} tokens"
)
summary, _ = summarize_text(chunk, instruction)
summaries.append(summary)
logger.info(f"Summarized {len(chunks)} chunks")
summary, _ = summarize_text("\n\n".join(summaries))
return summary.strip(), [
(summaries[i], chunks[i][0]) for i in range(0, len(chunks))
]
def split_text(
text: str,
max_length: int = CFG.browse_chunk_max_length,
model: str = CFG.fast_llm_model,
question: str = "",
) -> Generator[str, None, None]:
"""Split text into chunks of a maximum length
for_model: str = CFG.fast_llm_model,
with_overlap=True,
max_chunk_length: Optional[int] = None,
):
"""Split text into chunks of sentences, with each chunk not exceeding the maximum length
Args:
text (str): The text to split
max_length (int, optional): The maximum length of each chunk. Defaults to 8192.
for_model (str): The model to chunk for; determines tokenizer and constraints
max_length (int, optional): The maximum length of each chunk
Yields:
str: The next chunk of text
Raises:
ValueError: If the text is longer than the maximum length
ValueError: when a sentence is longer than the maximum length
"""
flatened_paragraphs = " ".join(text.split("\n"))
nlp = spacy.load(CFG.browse_spacy_language_model)
max_length = _max_chunk_length(for_model, max_chunk_length)
# flatten paragraphs to improve performance
text = text.replace("\n", " ")
text_length = count_string_tokens(text, for_model)
if text_length < max_length:
yield text, text_length
return
n_chunks = ceil(text_length / max_length)
target_chunk_length = ceil(text_length / n_chunks)
nlp: spacy.language.Language = spacy.load(CFG.browse_spacy_language_model)
nlp.add_pipe("sentencizer")
doc = nlp(flatened_paragraphs)
sentences = [sent.text.strip() for sent in doc.sents]
doc = nlp(text)
sentences = [sentence.text.strip() for sentence in doc.sents]
current_chunk = []
current_chunk: list[str] = []
current_chunk_length = 0
last_sentence = None
last_sentence_length = 0
for sentence in sentences:
message_with_additional_sentence = [
create_message(" ".join(current_chunk) + " " + sentence, question)
]
i = 0
while i < len(sentences):
sentence = sentences[i]
sentence_length = count_string_tokens(sentence, for_model)
expected_chunk_length = current_chunk_length + 1 + sentence_length
expected_token_usage = (
count_message_tokens(messages=message_with_additional_sentence, model=model)
+ 1
)
if expected_token_usage <= max_length:
if (
expected_chunk_length < max_length
# try to create chunks of approximately equal size
and expected_chunk_length - (sentence_length / 2) < target_chunk_length
):
current_chunk.append(sentence)
else:
yield " ".join(current_chunk)
current_chunk = [sentence]
message_this_sentence_only = [
create_message(" ".join(current_chunk), question)
current_chunk_length = expected_chunk_length
elif sentence_length < max_length:
if last_sentence:
yield " ".join(current_chunk), current_chunk_length
current_chunk = []
current_chunk_length = 0
if with_overlap:
overlap_max_length = max_length - sentence_length - 1
if last_sentence_length < overlap_max_length:
current_chunk += [last_sentence]
current_chunk_length += last_sentence_length + 1
elif overlap_max_length > 5:
# add as much from the end of the last sentence as fits
current_chunk += [
list(
chunk_content(
last_sentence,
for_model,
overlap_max_length,
)
).pop()[0],
]
current_chunk_length += overlap_max_length + 1
current_chunk += [sentence]
current_chunk_length += sentence_length
else: # sentence longer than maximum length -> chop up and try again
sentences[i : i + 1] = [
chunk
for chunk, _ in chunk_content(sentence, for_model, target_chunk_length)
]
expected_token_usage = (
count_message_tokens(messages=message_this_sentence_only, model=model)
+ 1
)
if expected_token_usage > max_length:
raise ValueError(
f"Sentence is too long in webpage: {expected_token_usage} tokens."
)
continue
i += 1
last_sentence = sentence
last_sentence_length = sentence_length
if current_chunk:
yield " ".join(current_chunk)
def summarize_text(
url: str, text: str, question: str, driver: Optional[WebDriver] = None
) -> str:
"""Summarize text using the OpenAI API
Args:
url (str): The url of the text
text (str): The text to summarize
question (str): The question to ask the model
driver (WebDriver): The webdriver to use to scroll the page
Returns:
str: The summary of the text
"""
if not text:
return "Error: No text to summarize"
model = CFG.fast_llm_model
text_length = len(text)
logger.info(f"Text length: {text_length} characters")
summaries = []
chunks = list(
split_text(
text, max_length=CFG.browse_chunk_max_length, model=model, question=question
),
)
scroll_ratio = 1 / len(chunks)
for i, chunk in enumerate(chunks):
if driver:
scroll_to_percentage(driver, scroll_ratio * i)
logger.info(f"Adding chunk {i + 1} / {len(chunks)} to memory")
memory_to_add = f"Source: {url}\n" f"Raw content part#{i + 1}: {chunk}"
memory = get_memory(CFG)
memory.add(memory_to_add)
messages = [create_message(chunk, question)]
tokens_for_chunk = count_message_tokens(messages, model)
logger.info(
f"Summarizing chunk {i + 1} / {len(chunks)} of length {len(chunk)} characters, or {tokens_for_chunk} tokens"
)
summary = create_chat_completion(
model=model,
messages=messages,
)
summaries.append(summary)
logger.info(
f"Added chunk {i + 1} summary to memory, of length {len(summary)} characters"
)
memory_to_add = f"Source: {url}\n" f"Content summary part#{i + 1}: {summary}"
memory.add(memory_to_add)
logger.info(f"Summarized {len(chunks)} chunks.")
combined_summary = "\n".join(summaries)
messages = [create_message(combined_summary, question)]
return create_chat_completion(
model=model,
messages=messages,
)
def scroll_to_percentage(driver: WebDriver, ratio: float) -> None:
"""Scroll to a percentage of the page
Args:
driver (WebDriver): The webdriver to use
ratio (float): The percentage to scroll to
Raises:
ValueError: If the ratio is not between 0 and 1
"""
if ratio < 0 or ratio > 1:
raise ValueError("Percentage should be between 0 and 1")
driver.execute_script(f"window.scrollTo(0, document.body.scrollHeight * {ratio});")
def create_message(chunk: str, question: str) -> Dict[str, str]:
"""Create a message for the chat completion
Args:
chunk (str): The chunk of text to summarize
question (str): The question to answer
Returns:
Dict[str, str]: The message to send to the chat completion
"""
return {
"role": "user",
"content": f'"""{chunk}""" Using the above text, answer the following'
f' question: "{question}" -- if the question cannot be answered using the text,'
" summarize the text.",
}
yield " ".join(current_chunk), current_chunk_length

View File

@@ -0,0 +1,29 @@
#########################Setup.py#################################
DEFAULT_SYSTEM_PROMPT_AICONFIG_AUTOMATIC = """
Your task is to devise up to 5 highly effective goals and an appropriate role-based name (_GPT) for an autonomous agent, ensuring that the goals are optimally aligned with the successful completion of its assigned task.
The user will provide the task, you will provide only the output in the exact format specified below with no explanation or conversation.
Example input:
Help me with marketing my business
Example output:
Name: CMOGPT
Description: a professional digital marketer AI that assists Solopreneurs in growing their businesses by providing world-class expertise in solving marketing problems for SaaS, content products, agencies, and more.
Goals:
- Engage in effective problem-solving, prioritization, planning, and supporting execution to address your marketing needs as your virtual Chief Marketing Officer.
- Provide specific, actionable, and concise advice to help you make informed decisions without the use of platitudes or overly wordy explanations.
- Identify and prioritize quick wins and cost-effective campaigns that maximize results with minimal time and budget investment.
- Proactively take the lead in guiding you and offering suggestions when faced with unclear information or uncertainty to ensure your marketing strategy remains on track.
"""
DEFAULT_TASK_PROMPT_AICONFIG_AUTOMATIC = (
"Task: '{{user_prompt}}'\n"
"Respond only with the output in the exact format specified in the system prompt, with no explanation or conversation.\n"
)
DEFAULT_USER_DESIRE_PROMPT = "Write a wikipedia style article about the project: https://github.com/significant-gravitas/Auto-GPT" # Default prompt

View File

@@ -1,6 +1,10 @@
""" A module for generating custom prompt strings."""
import json
from typing import Any, Callable, Dict, List, Optional
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
from autogpt.json_utils.utilities import llm_response_schema
if TYPE_CHECKING:
from autogpt.commands.command import CommandRegistry
class PromptGenerator:
@@ -19,19 +23,9 @@ class PromptGenerator:
self.resources = []
self.performance_evaluation = []
self.goals = []
self.command_registry = None
self.command_registry: CommandRegistry | None = None
self.name = "Bob"
self.role = "AI"
self.response_format = {
"thoughts": {
"text": "thought",
"reasoning": "reasoning",
"plan": "- short bulleted\n- list that conveys\n- long-term plan",
"criticism": "constructive self-criticism",
"speak": "thoughts summary to say to user",
},
"command": {"name": "command name", "args": {"arg name": "value"}},
}
def add_constraint(self, constraint: str) -> None:
"""
@@ -141,7 +135,6 @@ class PromptGenerator:
Returns:
str: The generated prompt string.
"""
formatted_response_format = json.dumps(self.response_format, indent=4)
return (
f"Constraints:\n{self._generate_numbered_list(self.constraints)}\n\n"
"Commands:\n"
@@ -149,7 +142,6 @@ class PromptGenerator:
f"Resources:\n{self._generate_numbered_list(self.resources)}\n\n"
"Performance Evaluation:\n"
f"{self._generate_numbered_list(self.performance_evaluation)}\n\n"
"You should only respond in JSON format as described below \nResponse"
f" Format: \n{formatted_response_format} \nEnsure the response can be"
" parsed by Python json.loads"
"Respond with only valid JSON conforming to the following schema: \n"
f"{llm_response_schema()}\n"
)

View File

@@ -2,7 +2,8 @@ from colorama import Fore
from autogpt.config.ai_config import AIConfig
from autogpt.config.config import Config
from autogpt.llm import ApiManager
from autogpt.config.prompt_config import PromptConfig
from autogpt.llm.api_manager import ApiManager
from autogpt.logs import logger
from autogpt.prompts.generator import PromptGenerator
from autogpt.setup import prompt_user
@@ -10,9 +11,7 @@ from autogpt.utils import clean_input
CFG = Config()
DEFAULT_TRIGGERING_PROMPT = (
"Determine which next command to use, and respond using the format specified above:"
)
DEFAULT_TRIGGERING_PROMPT = "Determine exactly one command to use, and respond using the JSON schema specified previously:"
def build_default_prompt_generator() -> PromptGenerator:
@@ -27,55 +26,21 @@ def build_default_prompt_generator() -> PromptGenerator:
# Initialize the PromptGenerator object
prompt_generator = PromptGenerator()
# Initialize the PromptConfig object and load the file set in the main config (default: prompts_settings.yaml)
prompt_config = PromptConfig(CFG.prompt_settings_file)
# Add constraints to the PromptGenerator object
prompt_generator.add_constraint(
"~4000 word limit for short term memory. Your short term memory is short, so"
" immediately save important information to files."
)
prompt_generator.add_constraint(
"If you are unsure how you previously did something or want to recall past"
" events, thinking about similar events will help you remember."
)
prompt_generator.add_constraint("No user assistance")
prompt_generator.add_constraint(
'Exclusively use the commands listed in double quotes e.g. "command name"'
)
# Define the command list
commands = [
("Task Complete (Shutdown)", "task_complete", {"reason": "<reason>"}),
]
# Add commands to the PromptGenerator object
for command_label, command_name, args in commands:
prompt_generator.add_command(command_label, command_name, args)
for constraint in prompt_config.constraints:
prompt_generator.add_constraint(constraint)
# Add resources to the PromptGenerator object
prompt_generator.add_resource(
"Internet access for searches and information gathering."
)
prompt_generator.add_resource("Long Term memory management.")
prompt_generator.add_resource(
"GPT-3.5 powered Agents for delegation of simple tasks."
)
prompt_generator.add_resource("File output.")
for resource in prompt_config.resources:
prompt_generator.add_resource(resource)
# Add performance evaluations to the PromptGenerator object
prompt_generator.add_performance_evaluation(
"Continuously review and analyze your actions to ensure you are performing to"
" the best of your abilities."
)
prompt_generator.add_performance_evaluation(
"Constructively self-criticize your big-picture behavior constantly."
)
prompt_generator.add_performance_evaluation(
"Reflect on past decisions and strategies to refine your approach."
)
prompt_generator.add_performance_evaluation(
"Every command has a cost, so be smart and efficient. Aim to complete tasks in"
" the least number of steps."
)
prompt_generator.add_performance_evaluation("Write all code to a file.")
for performance_evaluation in prompt_config.performance_evaluations:
prompt_generator.add_performance_evaluation(performance_evaluation)
return prompt_generator
@@ -117,6 +82,12 @@ Continue ({CFG.authorise_key}/{CFG.exit_key}): """
config = prompt_user()
config.save(CFG.ai_settings_file)
if CFG.restrict_to_workspace:
logger.typewriter_log(
"NOTE:All files/directories created by this agent can be found inside its workspace at:",
Fore.YELLOW,
f"{CFG.workspace_path}",
)
# set the total api budget
api_manager = ApiManager()
api_manager.set_total_budget(config.api_budget)

View File

@@ -2,12 +2,19 @@
import re
from colorama import Fore, Style
from jinja2 import Template
from autogpt import utils
from autogpt.config import Config
from autogpt.config.ai_config import AIConfig
from autogpt.llm import create_chat_completion
from autogpt.llm.base import ChatSequence, Message
from autogpt.llm.chat import create_chat_completion
from autogpt.logs import logger
from autogpt.prompts.default_prompts import (
DEFAULT_SYSTEM_PROMPT_AICONFIG_AUTOMATIC,
DEFAULT_TASK_PROMPT_AICONFIG_AUTOMATIC,
DEFAULT_USER_DESIRE_PROMPT,
)
CFG = Config()
@@ -42,7 +49,7 @@ def prompt_user() -> AIConfig:
)
if user_desire == "":
user_desire = "Write a wikipedia style article about the project: https://github.com/significant-gravitas/Auto-GPT" # Default prompt
user_desire = DEFAULT_USER_DESIRE_PROMPT # Default prompt
# If user desire contains "--manual"
if "--manual" in user_desire:
@@ -164,39 +171,20 @@ def generate_aiconfig_automatic(user_prompt) -> AIConfig:
AIConfig: The AIConfig object tailored to the user's input
"""
system_prompt = """
Your task is to devise up to 5 highly effective goals and an appropriate role-based name (_GPT) for an autonomous agent, ensuring that the goals are optimally aligned with the successful completion of its assigned task.
The user will provide the task, you will provide only the output in the exact format specified below with no explanation or conversation.
Example input:
Help me with marketing my business
Example output:
Name: CMOGPT
Description: a professional digital marketer AI that assists Solopreneurs in growing their businesses by providing world-class expertise in solving marketing problems for SaaS, content products, agencies, and more.
Goals:
- Engage in effective problem-solving, prioritization, planning, and supporting execution to address your marketing needs as your virtual Chief Marketing Officer.
- Provide specific, actionable, and concise advice to help you make informed decisions without the use of platitudes or overly wordy explanations.
- Identify and prioritize quick wins and cost-effective campaigns that maximize results with minimal time and budget investment.
- Proactively take the lead in guiding you and offering suggestions when faced with unclear information or uncertainty to ensure your marketing strategy remains on track.
"""
system_prompt = DEFAULT_SYSTEM_PROMPT_AICONFIG_AUTOMATIC
prompt_ai_config_automatic = Template(
DEFAULT_TASK_PROMPT_AICONFIG_AUTOMATIC
).render(user_prompt=user_prompt)
# Call LLM with the string as user input
messages = [
{
"role": "system",
"content": system_prompt,
},
{
"role": "user",
"content": f"Task: '{user_prompt}'\nRespond only with the output in the exact format specified in the system prompt, with no explanation or conversation.\n",
},
]
output = create_chat_completion(messages, CFG.fast_llm_model)
output = create_chat_completion(
ChatSequence.for_model(
CFG.fast_llm_model,
[
Message("system", system_prompt),
Message("user", prompt_ai_config_automatic),
],
)
)
# Debug LLM Output
logger.debug(f"AI Config Generator Raw Output: {output}")

View File

@@ -20,5 +20,3 @@ class AbstractSingleton(abc.ABC, metaclass=Singleton):
"""
Abstract singleton class for ensuring only one instance of a class.
"""
pass

View File

@@ -1,5 +1,6 @@
"""Base class for all voice classes."""
import abc
import re
from threading import Lock
from autogpt.singleton import AbstractSingleton
@@ -29,6 +30,11 @@ class VoiceBase(AbstractSingleton):
text (str): The text to say.
voice_index (int): The index of the voice to use.
"""
text = re.sub(
r"\b(?:https?://[-\w_.]+/?\w[-\w_.]*\.(?:[-\w_.]+/?\w[-\w_.]*\.)?[a-z]+(?:/[-\w_.%]+)*\b(?!\.))",
"",
text,
)
with self._mutex:
return self._speech(text, voice_index)
@@ -37,7 +43,6 @@ class VoiceBase(AbstractSingleton):
"""
Setup the voices, API key, etc.
"""
pass
@abc.abstractmethod
def _speech(self, text: str, voice_index: int = 0) -> bool:
@@ -47,4 +52,3 @@ class VoiceBase(AbstractSingleton):
Args:
text (str): The text to play.
"""
pass

View File

@@ -4,7 +4,7 @@ import os
import requests
from playsound import playsound
from autogpt.config import Config
from autogpt.config.config import Config
from autogpt.speech.base import VoiceBase
PLACEHOLDERS = {"your-voice-id"}
@@ -38,11 +38,11 @@ class ElevenLabsSpeech(VoiceBase):
"xi-api-key": cfg.elevenlabs_api_key,
}
self._voices = default_voices.copy()
if cfg.elevenlabs_voice_1_id in voice_options:
cfg.elevenlabs_voice_1_id = voice_options[cfg.elevenlabs_voice_1_id]
if cfg.elevenlabs_voice_id in voice_options:
cfg.elevenlabs_voice_id = voice_options[cfg.elevenlabs_voice_id]
if cfg.elevenlabs_voice_2_id in voice_options:
cfg.elevenlabs_voice_2_id = voice_options[cfg.elevenlabs_voice_2_id]
self._use_custom_voice(cfg.elevenlabs_voice_1_id, 0)
self._use_custom_voice(cfg.elevenlabs_voice_id, 0)
self._use_custom_voice(cfg.elevenlabs_voice_2_id, 1)
def _use_custom_voice(self, voice, voice_index) -> None:

View File

@@ -2,12 +2,12 @@
import threading
from threading import Semaphore
from autogpt.config import Config
from autogpt.config.config import Config
from autogpt.speech.base import VoiceBase
from autogpt.speech.brian import BrianSpeech
from autogpt.speech.eleven_labs import ElevenLabsSpeech
from autogpt.speech.gtts import GTTSVoice
from autogpt.speech.macos_tts import MacOSTTS
from autogpt.speech.stream_elements_speech import StreamElementsSpeech
_QUEUE_SEMAPHORE = Semaphore(
1
@@ -33,14 +33,14 @@ def say_text(text: str, voice_index: int = 0) -> None:
def _get_voice_engine(config: Config) -> tuple[VoiceBase, VoiceBase]:
"""Get the voice engine to use for the given configuration"""
default_voice_engine = GTTSVoice()
if config.elevenlabs_api_key:
tts_provider = config.text_to_speech_provider
if tts_provider == "elevenlabs":
voice_engine = ElevenLabsSpeech()
elif config.use_mac_os_tts == "True":
elif tts_provider == "macos":
voice_engine = MacOSTTS()
elif config.use_brian_tts == "True":
voice_engine = BrianSpeech()
elif tts_provider == "streamelements":
voice_engine = StreamElementsSpeech()
else:
voice_engine = GTTSVoice()
return default_voice_engine, voice_engine
return GTTSVoice(), voice_engine

View File

@@ -7,24 +7,24 @@ from playsound import playsound
from autogpt.speech.base import VoiceBase
class BrianSpeech(VoiceBase):
"""Brian speech module for autogpt"""
class StreamElementsSpeech(VoiceBase):
"""Streamelements speech module for autogpt"""
def _setup(self) -> None:
"""Setup the voices, API key, etc."""
pass
def _speech(self, text: str, _: int = 0) -> bool:
"""Speak text using Brian with the streamelements API
def _speech(self, text: str, voice: str, _: int = 0) -> bool:
"""Speak text using the streamelements API
Args:
text (str): The text to speak
voice (str): The voice to use
Returns:
bool: True if the request was successful, False otherwise
"""
tts_url = (
f"https://api.streamelements.com/kappa/v2/speech?voice=Brian&text={text}"
f"https://api.streamelements.com/kappa/v2/speech?voice={voice}&text={text}"
)
response = requests.get(tts_url)

View File

@@ -8,13 +8,20 @@ import time
class Spinner:
"""A simple spinner class"""
def __init__(self, message: str = "Loading...", delay: float = 0.1) -> None:
def __init__(
self,
message: str = "Loading...",
delay: float = 0.1,
plain_output: bool = False,
) -> None:
"""Initialize the spinner class
Args:
message (str): The message to display.
delay (float): The delay between each spinner update.
plain_output (bool): Whether to display the spinner or not.
"""
self.plain_output = plain_output
self.spinner = itertools.cycle(["-", "/", "|", "\\"])
self.delay = delay
self.message = message
@@ -23,11 +30,17 @@ class Spinner:
def spin(self) -> None:
"""Spin the spinner"""
if self.plain_output:
self.print_message()
return
while self.running:
sys.stdout.write(f"{next(self.spinner)} {self.message}\r")
sys.stdout.flush()
self.print_message()
time.sleep(self.delay)
sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r")
def print_message(self):
sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r")
sys.stdout.write(f"{next(self.spinner)} {self.message}\r")
sys.stdout.flush()
def __enter__(self):
"""Start the spinner"""
@@ -57,9 +70,7 @@ class Spinner:
new_message (str): New message to display.
delay (float): The delay in seconds between each spinner update.
"""
time.sleep(delay)
sys.stdout.write(
f"\r{' ' * (len(self.message) + 2)}\r"
) # Clear the current message
sys.stdout.flush()
self.delay = delay
self.message = new_message
if self.plain_output:
self.print_message()

View File

@@ -1,4 +1,5 @@
import functools
import re
from typing import Any, Callable
from urllib.parse import urljoin, urlparse
@@ -7,7 +8,7 @@ from requests.compat import urljoin
def validate_url(func: Callable[..., Any]) -> Any:
"""The method decorator validate_url is used to validate urls for any command that requires
a url as an arugment"""
a url as an argument"""
@functools.wraps(func)
def wrapper(url: str, *args, **kwargs) -> Any:
@@ -23,13 +24,16 @@ def validate_url(func: Callable[..., Any]) -> Any:
ValueError if the url fails any of the validation tests
"""
# Most basic check if the URL is valid:
if not url.startswith("http://") and not url.startswith("https://"):
if not re.match(r"^https?://", url):
raise ValueError("Invalid URL format")
if not is_valid_url(url):
raise ValueError("Missing Scheme or Network location")
# Restrict access to local files
if check_local_file_access(url):
raise ValueError("Access to local files is restricted")
# Check URL length
if len(url) > 2000:
raise ValueError("URL is too long")
return func(sanitize_url(url), *args, **kwargs)

View File

@@ -1,19 +1,26 @@
import os
import re
import requests
import yaml
from colorama import Fore
from colorama import Fore, Style
from git.repo import Repo
from autogpt.logs import logger
# Use readline if available (for clean_input)
try:
import readline
except ImportError:
pass
from prompt_toolkit import ANSI, PromptSession
from prompt_toolkit.history import InMemoryHistory
from autogpt.config import Config
from autogpt.logs import logger
session = PromptSession(history=InMemoryHistory())
def batch(iterable, max_batch_length: int, overlap: int = 0):
"""Batch data from iterable into slices of length N. The last batch may be shorter."""
# batched('ABCDEFG', 3) --> ABC DEF G
if max_batch_length < 1:
raise ValueError("n must be at least one")
for i in range(0, len(iterable), max_batch_length - overlap):
yield iterable[i : i + max_batch_length]
def clean_input(prompt: str = "", talk=False):
@@ -49,7 +56,7 @@ def clean_input(prompt: str = "", talk=False):
# ask for input, default when just pressing Enter is y
logger.info("Asking user via keyboard...")
answer = input(prompt)
answer = session.prompt(ANSI(prompt))
return answer
except KeyboardInterrupt:
logger.info("You interrupted Auto-GPT")
@@ -107,15 +114,69 @@ def get_current_git_branch() -> str:
return ""
def get_latest_bulletin() -> str:
exists = os.path.exists("CURRENT_BULLETIN.md")
def get_latest_bulletin() -> tuple[str, bool]:
exists = os.path.exists("data/CURRENT_BULLETIN.md")
current_bulletin = ""
if exists:
current_bulletin = open("CURRENT_BULLETIN.md", "r", encoding="utf-8").read()
current_bulletin = open(
"data/CURRENT_BULLETIN.md", "r", encoding="utf-8"
).read()
new_bulletin = get_bulletin_from_web()
is_new_news = new_bulletin != current_bulletin
is_new_news = new_bulletin != "" and new_bulletin != current_bulletin
news_header = Fore.YELLOW + "Welcome to Auto-GPT!\n"
if new_bulletin or current_bulletin:
news_header += (
"Below you'll find the latest Auto-GPT News and updates regarding features!\n"
"If you don't wish to see this message, you "
"can run Auto-GPT with the *--skip-news* flag.\n"
)
if new_bulletin and is_new_news:
open("CURRENT_BULLETIN.md", "w", encoding="utf-8").write(new_bulletin)
return f" {Fore.RED}::UPDATED:: {Fore.CYAN}{new_bulletin}{Fore.RESET}"
return current_bulletin
open("data/CURRENT_BULLETIN.md", "w", encoding="utf-8").write(new_bulletin)
current_bulletin = f"{Fore.RED}::NEW BULLETIN::{Fore.RESET}\n\n{new_bulletin}"
return f"{news_header}\n{current_bulletin}", is_new_news
def markdown_to_ansi_style(markdown: str):
ansi_lines: list[str] = []
for line in markdown.split("\n"):
line_style = ""
if line.startswith("# "):
line_style += Style.BRIGHT
else:
line = re.sub(
r"(?<!\*)\*(\*?[^*]+\*?)\*(?!\*)",
rf"{Style.BRIGHT}\1{Style.NORMAL}",
line,
)
if re.match(r"^#+ ", line) is not None:
line_style += Fore.CYAN
line = re.sub(r"^#+ ", "", line)
ansi_lines.append(f"{line_style}{line}{Style.RESET_ALL}")
return "\n".join(ansi_lines)
def get_legal_warning() -> str:
legal_text = """
## DISCLAIMER AND INDEMNIFICATION AGREEMENT
### PLEASE READ THIS DISCLAIMER AND INDEMNIFICATION AGREEMENT CAREFULLY BEFORE USING THE AUTOGPT SYSTEM. BY USING THE AUTOGPT SYSTEM, YOU AGREE TO BE BOUND BY THIS AGREEMENT.
## Introduction
AutoGPT (the "System") is a project that connects a GPT-like artificial intelligence system to the internet and allows it to automate tasks. While the System is designed to be useful and efficient, there may be instances where the System could perform actions that may cause harm or have unintended consequences.
## No Liability for Actions of the System
The developers, contributors, and maintainers of the AutoGPT project (collectively, the "Project Parties") make no warranties or representations, express or implied, about the System's performance, accuracy, reliability, or safety. By using the System, you understand and agree that the Project Parties shall not be liable for any actions taken by the System or any consequences resulting from such actions.
## User Responsibility and Respondeat Superior Liability
As a user of the System, you are responsible for supervising and monitoring the actions of the System while it is operating on your
behalf. You acknowledge that using the System could expose you to potential liability including but not limited to respondeat superior and you agree to assume all risks and liabilities associated with such potential liability.
## Indemnification
By using the System, you agree to indemnify, defend, and hold harmless the Project Parties from and against any and all claims, liabilities, damages, losses, or expenses (including reasonable attorneys' fees and costs) arising out of or in connection with your use of the System, including, without limitation, any actions taken by the System on your behalf, any failure to properly supervise or monitor the System, and any resulting harm or unintended consequences.
"""
return legal_text

View File

@@ -120,7 +120,8 @@ class Workspace:
logger.debug(f"Resolved root as '{root}'")
if relative_path.is_absolute():
# Allow exception for absolute paths if they are contained in your workspace directory.
if relative_path.is_absolute() and not relative_path.is_relative_to(root):
raise ValueError(
f"Attempted to access absolute path '{relative_path}' in workspace '{root}'."
)

View File

@@ -75,14 +75,13 @@ Needs improvement.
Not what I need."""
# TODO: add questions above, to distract it even more.
command = f"{sys.executable} -m autogpt"
command = [sys.executable, "-m", "autogpt"]
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
)
stdout_output, stderr_output = process.communicate(input_data.encode())

View File

@@ -1,37 +1,36 @@
import argparse
import logging
from autogpt.commands.file_operations import ingest_file, search_files
from autogpt.commands.file_operations import ingest_file, list_files
from autogpt.config import Config
from autogpt.memory import get_memory
from autogpt.memory.vector import VectorMemory, get_memory
cfg = Config()
def configure_logging():
logging.basicConfig(
filemode="a",
format="%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s",
datefmt="%H:%M:%S",
level=logging.DEBUG,
handlers=[
logging.FileHandler(filename="log-ingestion.txt"),
logging.FileHandler(filename="log-ingestion.txt", mode="a"),
logging.StreamHandler(),
],
)
return logging.getLogger("AutoGPT-Ingestion")
def ingest_directory(directory, memory, args):
def ingest_directory(directory: str, memory: VectorMemory, args):
"""
Ingest all files in a directory by calling the ingest_file function for each file.
:param directory: The directory containing the files to ingest
:param memory: An object with an add() method to store the chunks in memory
"""
global logger
logger = logging.getLogger("AutoGPT-Ingestion")
try:
files = search_files(directory)
files = list_files(directory)
for file in files:
ingest_file(file, memory, args.max_length, args.overlap)
except Exception as e:
@@ -68,7 +67,6 @@ def main() -> None:
help="The max_length of each chunk when ingesting files (default: 4000)",
default=4000,
)
args = parser.parse_args()
# Initialize memory

Some files were not shown because too many files have changed in this diff Show More