Compare commits

...

68 Commits

Author SHA1 Message Date
Aarushi
64ecd28804 Merge branch 'master' into zamilmajdy/simplify-ai-block 2024-07-10 13:17:09 +01:00
Aarushi
d6cbb48609 feat(agpt_builder) Fix data persistance on agent execution (#7363)
* fixing issue with data vanishing when executing agent

* fix rebasing

* add unique key

* reset to neutral & failed colours
2024-07-10 12:47:55 +01:00
Toran Bruce Richards
cac41edafc feat: Update LlmCallBlock to handle different response formats 2024-07-10 12:00:56 +01:00
Toran Bruce Richards
6cd5007857 Update response_format in LlmCallBlock to handle json parameter 2024-07-10 11:49:45 +01:00
Zamil Majdy
d1badceb34 Merge remote-tracking branch 'origin/zamilmajdy/simplify-ai-block' into zamilmajdy/simplify-ai-block 2024-07-10 17:02:32 +07:00
Zamil Majdy
c24cfc3718 feat(rnd): Simplify AI block 2024-07-10 17:02:19 +07:00
Zamil Majdy
fb7480304a Merge branch 'master' into zamilmajdy/simplify-ai-block 2024-07-10 13:58:55 +04:00
Zamil Majdy
eb097eefab feat(rnd): Simplify AI block 2024-07-10 16:57:42 +07:00
Swifty
3789b00479 feat(autogpt_server): Expose rest api via websocket (#7350)
* Add in websocket event types

* adding in api endpoints

* Updated ws messages
2024-07-10 10:54:18 +01:00
Swifty
f94e81f48b feat(builder) Add save Agent functionality (#7361)
Add save functionality
2024-07-10 10:01:12 +01:00
Zamil Majdy
e10c4ee4cd fix(doc): Fix Auto GPT server Running The Server doc (#7360) 2024-07-10 10:31:52 +02:00
Reinier van der Leer
81dee568cb feat(autogpt_builder): Add basic layout with nav (#7317)
* feat(agent_builder): Add shad/cn + Radix Icons + Tailwind

* move favicon.ico to static folder

* delete empty custominput.css

* feat(agent_builder): Add basic app layout with nav

* Revert unwanted changes

* Fix /build + Flow layout issues

- Add `className` passthrough to `Flow` component
- Fix /build page layout

* unfix build/page.tsx indentation for git tracking

---------

Co-authored-by: Toran Bruce Richards <toran.richards@gmail.com>
2024-07-09 17:33:12 +01:00
Bently
7929f1a4ac Reads data from .env (#7357) 2024-07-09 17:20:31 +01:00
Bently
0a28c72bad Fix issue with node inputs (#7356) 2024-07-09 16:21:13 +01:00
Toran Bruce Richards
b9861a5308 fix(autogpt_builder): Update AutoGPT logo in page component (#7354)
feat(autogpt_builder): Update AutoGPT logo in page component
2024-07-09 16:10:13 +01:00
Zamil Majdy
af3a2bb5f5 fix(doc): Fix Auto GPT server Running The Server doc (#7355) 2024-07-09 16:09:43 +01:00
Bently
2f174837bd Add input for agent name & description (#7351) 2024-07-09 15:44:36 +01:00
Aarushi
b30eaf653a feat(autogpt_builder) Remove submit button (#7353)
remove submit button
2024-07-09 15:23:36 +01:00
Zamil Majdy
d9c9b22886 feat(rnd): Add Agent Block sample/test input output for auto-generated unit testing for block (#7310)
Sample test input and output on the block can serve as documentation and auto-generated unit-testing code for the agent block.

What's within the scope of this change:
Adding the fields for block test (input, output, mocks), and its execution.

What's still outside the scope:
Handling of mock and stub for a block using sensitive credentials or network calls or 3rd-party connections.
2024-07-09 13:08:42 +02:00
Zamil Majdy
ff71b0beb7 fix(rnd): Avoid expensive call of get_service_client on block execution (#7349) 2024-07-09 13:07:57 +02:00
Swifty
57cc8b69e9 fix(agent_server): Fix async issue with executor integration test (#7348)
Fix execution completed check
2024-07-09 17:09:44 +07:00
Aarushi
7ce0c655d0 feat(autogpt_builder) Hook up execution progress animation to live execution data (#7347)
* hooked up animation to actual execution data

* remove console log
2024-07-09 09:32:38 +01:00
Bently
1e755f9e8d feat(agent_builder): Updates to Builder GUI + Nodes (#7345)
* Updates to Builder GUI + Nodes

* fix apiUrl back to local host
2024-07-08 21:33:07 +01:00
Swifty
f9bedb0fd9 fix(autogpt_server): Changed inconstant timestamps to all use UTC (#7343)
* Changed timestamps to utc

* made other datetime.now timezone aware
2024-07-08 17:09:38 +02:00
Swifty
a32bc72314 feast(agent server) Add populate db command (#7342)
* Add populate db command

* Added in a reddit graph creation command
2024-07-08 17:09:20 +02:00
Reinier van der Leer
227092b669 feat(autogpt_builder): Enable loading an existing flow (#7338)
- Add `flowID` query parameter
- Add logic to load a flow from the server into the builder
2024-07-07 01:39:05 +02:00
Reinier van der Leer
39556a71cc feat(autogpt_builder): Improve server error handling in AutoGPTServerAPI 2024-07-06 01:03:23 -06:00
Reinier van der Leer
1fb8c1adac fix(autogpt_builder): Fix date conversion in AutoGPTServerAPI client
Explicitly parse dates in JSON response object in `getFlowExecutionInfo(..)`
2024-07-06 01:01:41 -06:00
Swifty
37e1780d76 feat(agent server): Added websocket communication (#7291)
* Refactor on the link structure and API

* Refactor on the link structure and API

* Cleanup IDS

* Remove run_id

* Update block interface

* Added websockets dependency

* Adding routes

* Adding in websocket code

* Added cli to test the websocket

* Added an outline of the message formats I plan on using

* Added webscoket message types

* Updated poetry lock

* Adding subscription logic

* Updating subscription mechanisms

* update cli

* Send updates to server

* Get single execution data

* Fix type hints and renamed function

* add callback function and type hints

* fix type hints

* Updated manager to use property

* Added in websocket updates

* Added connection manager tests

* Added tests for ws_api

* trying to work around process issues

* test formatting

* Added a create and execute command for the cli

* Updated send format

* websockets command working

* cli update

* Added model.py

* feat: Update server.py and manager.py

- Initialize blocks in AgentServer lifespan context
- Remove unnecessary await in AgentServer get_graph_blocks
- Fix type hinting in manager.py
- Validate input data in validate_exec function

* fix tests

* feat: Add autogpt_server.blocks.sample and autogpt_server.blocks.text modules

This commit adds the `autogpt_server.blocks.sample` and `autogpt_server.blocks.text` modules to the project. These modules contain blocks that are used in the execution of the Autogpt server. The `ParrotBlock` and `PrintingBlock` classes are imported from `autogpt_server.blocks.sample`, while the `TextFormatterBlock` class is imported from `autogpt_server.blocks.text`. This addition enhances the functionality of the server by providing additional blocks for text processing and sample operations.

* fixed circular import issue

* Update readme

---------

Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
2024-07-05 17:02:26 +02:00
Reinier van der Leer
0df2199c42 feat(autogpt_server): Add GET /graphs/{graph_id}/executions endpoint (#7330)
* feat(autogpt_builder): Add `AutoGPTServerAPI` client

* migrate API calls in Flow.tsx to new API client

* feat(autogpt_server): Add `/graphs/{graph_id}/executions` endpoint

In `data/execution.py`:
- Add `list_executions` function
- Rename `get_executions` to `get_execution_results`

In `server/server.py`:
- Add route
- Add `AgentServer.list_graph_runs`
- Rename `AgentServer.get_executions` to `get_run_execution_results`

* feat(autogpt_builder): Add `listFlowRunIDs` endpoint to `AutoGPTServerAPI` client

* Move `Schema` to `types.ts` and rename to `ObjectSchema`
2024-07-05 10:51:27 +01:00
Reinier van der Leer
200800312a feat(autogpt_builder): Add AutoGPTServerAPI client (#7328)
- Add fully typed `AutoGPTServerAPI` client in lib/autogpt_server_api.ts
- Migrate API calls in `Flow.tsx` to new API client
2024-07-05 11:03:59 +02:00
Reinier van der Leer
b7a90ce768 fix(autogpt_builder): Unignore lib/ and add lib/utils.ts 2024-07-04 17:40:59 -06:00
Reinier van der Leer
f359ed0983 feat(agent_builder): Add shad/cn UI library (#7316)
- Add shad/cn + Radix Icons + Tailwind
- move favicon.ico to static folder
- delete empty custominput.css

---------

Co-authored-by: Bentlybro <tomnoon9@gmail.com>
2024-07-05 08:14:45 +02:00
Aarushi
6456285753 feat(autogpt_builder) Update custom node to handle deeply nested structures (#7319)
* feat(rnd): Add type hint and strong pydantic type validation for block input/output + add reddit agent-blocks.

* feat(rnd): Add type hint and strong pydantic type validation for block input/output + add reddit agent-blocks.

* Fix reddit block

* Fix serialization

* Eliminate deprecated class property

* Remove RedditCredentialsBlock

* Cache jsonschema computation, add dictionary construction

* Add dict_split and list_split to output, add more blocks

* Add objc_split for completeness, int both input and output

* Update reddit block

* Add reddit test (untested)

* Resolved json issue on pydantic

* Add creds check on client

* Add dict <--> pydantic object flexibility

* Fix error retry

* Skip reddit test

* Code cleanup

* Chang prompt

* Make this work

* Fix linting

* Hide input_links and output_links from Node

* Add docs

* updating UI to handle deeply nested data structures for reddit usecase

* changing expected key in reddit post to comment

---------

Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
2024-07-04 17:54:41 +01:00
Zamil Majdy
833944e228 feat(rnd): Add strong pydantic type & composite data extraction for Block input/output schema + add reddit agent-blocks (#7288)
* feat(rnd): Add type hint and strong pydantic type validation for block input/output + add reddit agent-blocks.

* feat(rnd): Add type hint and strong pydantic type validation for block input/output + add reddit agent-blocks.

* Fix reddit block

* Fix serialization

* Eliminate deprecated class property

* Remove RedditCredentialsBlock

* Cache jsonschema computation, add dictionary construction

* Add dict_split and list_split to output, add more blocks

* Add objc_split for completeness, int both input and output

* Update reddit block

* Add reddit test (untested)

* Resolved json issue on pydantic

* Add creds check on client

* Add dict <--> pydantic object flexibility

* Fix error retry

* Skip reddit test

* Code cleanup

* Chang prompt

* Make this work

* Fix linting

* Hide input_links and output_links from Node

* Add docs

---------

Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com>
2024-07-04 11:37:28 +01:00
Reinier van der Leer
db0e726954 fix(agent, benchmark): Specify path_type=Path for CLI path options/arguments
Without `path_type=Path`, an option/argument with `type=click.Path()` will return a `str`.
2024-07-03 15:21:04 -06:00
Krzysztof Czerwinski
08612cc3bf refactor(agent, forge): Move tests from autogpt to forge (#7247)
- Move `autogpt/tests/vcr_cassettes` submodule to `forge/tests/vcr_cassettes`
- Remove not needed markers from `pyproject.toml`: `"requires_openai_api_key", "requires_huggingface_api_key"`
- Update relevant GitHub workflows

Moved relevant tests from `autogpt/tests` to appropiate directories:
- Component tests to their respective component dirs
- `autogpt/tests/unit/test_web_search.py` → `forge/components/web/test_search.py`
- `autogpt/tests/unit/test_git_commands.py` → `forge/components/git_operations/test_git_operations.py`
- `autogpt/tests/unit/test_file_operations.py` → `forge/components/file_manager/test_file_manager.py`
- `autogpt/tests/integration/test_image_gen.py` → `forge/components/image_gen/test_image_gen.py`
- `autogpt/tests/integration/test_web_selenium.py` → `forge/components/web/test_selenium.py`
- `autogpt/tests/integration/test_execute_code.py` → `forge/components/code_executor/test_code_executor.py`
- `autogpt/tests/unit/test_s3_file_storage.py` → `forge/file_storage/test_s3_file_storage.py`
- `autogpt/tests/unit/test_gcs_file_storage.py` → `forge/file_storage/test_gcs_file_storage.py`
- `autogpt/tests/unit/test_local_file_storage.py` → `forge/file_storage/test_local_file_storage.py`
- `autogpt/tests/unit/test_json.py` → `forge/json/test_parsing.py`
- `autogpt/tests/unit/test_logs.py` → `forge/logging/test_utils.py`
- `autogpt/tests/unit/test_url_validation.py` → `forge/utils/test_url_validator.py`
- `autogpt/tests/unit/test_text_file_parsers.py` → `forge/utils/test_file_operations.py`

- (Re)moved dependencies from `autogpt/pyproject.toml` that were only used in these test files.

Also:
- Added `load_env_vars` fixture to `forge/conftest.py`
- Fixed a type error in `forge/components/web/test_search.py`
- Merged `autogpt/.gitattributes` into root `.gitattributes`

---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
2024-07-04 02:09:01 +02:00
Reinier van der Leer
7415e24fc3 fix(forge): Add google-api-python-client-stubs and fix type error (#7303)
- Add `google-api-python-client-stubs` dev dependency
- Add version specification to `google-api-python-client` dependency
- Fix type error (by ignoring it) in forge/components/web/search.py
2024-07-03 23:26:37 +02:00
Dimitri Brooks
ecb054af56 feat: Add support for Claude 3.5 Sonnet (#7301)
add support for Claude 3.5 Sonnet
2024-07-03 16:02:14 +02:00
Krzysztof Czerwinski
39f70b0c83 docs(agent, forge): Update component configuration docs (#7232) 2024-07-03 09:50:38 +01:00
Krzysztof Czerwinski
7cb4d4a903 feat(forge, agent, benchmark): Upgrade to Pydantic v2 (#7280)
Update Pydantic dependency of `autogpt`, `forge` and `benchmark` to `^2.7`
[Pydantic Migration Guide](https://docs.pydantic.dev/2.7/migration/)

- Migrate usages of now-deprecated functions to their replacements
- Update `Field` definitions
  - Ellipsis `...` for required fields is deprecated
  - `Field` no longer supports extra `kwargs`, replace use of this feature with field metadata
- Replace `Config` class for specifying model configuration with `model_config = ConfigDict(..)`
- Removed `ModelContainer` in `BaseAgent`, component configuration dict is now directly serialized using Pydantic v2 helper functions
- Forked `agent-protocol` and updated `packages/client/python` for Pydantic v2 support: https://github.com/Significant-Gravitas/agent-protocol

---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
2024-07-02 20:45:32 +02:00
Aarushi
8feaced92e (rnd) Add support for multiple, dynamic inputs (#7296)
multi dynamic inputs
2024-07-02 09:56:33 +01:00
Reinier van der Leer
97e4cceb94 feat(agent, forge): Markdown-formatted history -> message history (#7228)
- Implement message based history in `ActionHistoryComponent`
- Make non-summarized message count configurable (`ActionHistoryComponent.full_message_count`)
- Run `ActionHistoryComponent` after `SystemComponent` so that history messages are last in the prompt
- Omit final instruction message if prompt already contains assistant messages
- Filter `raw_message` from `ActionProposal.schema()`

---------

Co-authored-by: Krzysztof Czerwinski <kpczerwinski@gmail.com>
2024-07-02 03:47:55 +02:00
Reinier van der Leer
2fa4fd23af fix(agent): Make build dependencies optional to unbreak install (#7298)
* Create optional `build` dependency group
* Move `cx-freeze` dependency to `build` dependency group

To include the `build` group when installing dependencies, run `poetry install --with=build`.

Fixes #7297 (`cx-freeze` dependency install fails after #7271)
2024-07-02 02:08:02 +02:00
angeousta
976ea7cd3c fix readme (broken link) (#7293)
Update README.md
2024-07-01 09:44:46 +02:00
Bently
d5ab83aa34 Updating api calls in AutoGPT builder (#7275)
* update api endpoints

* get multi-node working + fix node output

* updated multi-node running + re-add "wire" on rebuild

* Fix node data mapping

* removed getStatusValue
2024-06-28 10:39:26 +01:00
Reinier van der Leer
cbae8b5c14 chore(agent, forge, benchmark): Clean up dependencies (#7286)
* Remove unused dependencies
* Move dependencies for moved code from `autogpt` to `forge`
* Loosen dependency for `uvicorn` to improve compatibility
2024-06-28 02:21:36 +02:00
Aarushi
854080f7af ci(builder): Add CI for AutoGPT Builder (#7267)
Add a simple Node/NPM linting workflow for `rnd/autogpt_builder/`

---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
2024-06-28 01:30:34 +02:00
Reinier van der Leer
fbb3891e79 chore(forge, agent, benchmark): Update pytest-asyncio to v0.23.x
Resolves #7283
2024-06-27 14:09:36 -06:00
Reinier van der Leer
4d8ee65ca7 fix(pre-commit): Add benchmark/poetry.lock to "Typecheck - Benchmark" trigger file pattern 2024-06-27 14:05:12 -06:00
Aarushi
6093acc813 Add more functionality to Nodes (#7278)
updating node behaviour
2024-06-27 17:03:10 +01:00
Nicholas Tindle
785a40ff9d feat(server, autogpt): Add Example files and update build option (#7271) 2024-06-27 09:56:21 -05:00
Zamil Majdy
2bc22c5450 feat(rnd): Add support for dynamic input as list for AgentServer Block (#7268)
On AgentServer, To create a Block like StringFormatterBlock or LllmCallBlock, we need some way to dynamically link input pins and aggregate them into a single list input. This will give a better experience for the user to construct an input and link it from the output of the other nodes. The scope of this change is adding support for that in the least intrusive way.

Proposal
To differentiate the input list name and its singular entry we are using the $_<index> prefix. For example:
For the input items: list[int], you can set a pin items with values like [1,2,3,4]. But you can also add input pins like items_$_0 or items_$_4 with values 1 or 2, which will be appended to the items input in alphabetical order.
The execution engine will guarantee to wait for the execution until all the input pin value is produced, so input pin with list input will produce fix-sized list.
2024-06-27 21:51:34 +07:00
Aarushi
cdc658695f Add reactflow component in AutoGPT builder (#7270)
* Getting started with nextjs

* fix linting

* remove gitignore for package.json

* pulling in reactflow components

* updating css

* use environment variables

* clean up css / ui a lil

* Fixed nodes/run button animation

so they are always visible

---------

Co-authored-by: Bentlybro <tomnoon9@gmail.com>
2024-06-27 10:14:25 +01:00
Aarushi
dd960f9306 Add support for nextjs based app (#7266)
* Getting started with nextjs

* fix linting

* remove gitignore for package.json
2024-06-27 10:02:54 +01:00
Krzysztof Czerwinski
6e1c9d44a4 Update Agent Server README.md 2024-06-26 19:41:12 +02:00
Zamil Majdy
26bcb26bb7 feat(rnd): Refactor AgentServer Node Input/Output Relation & Block output interface (#7231)
### Background

The current implementation of AgentServer doesn't allow for a single pin to be connected to multiple nodes, this will be problematic when you have a single output node that needs to be propagated into many nodes. Or multiple nodes that possibly feed the data into a single pin (first come first serve).

This infra change is also part of the preparation for changing the `block` interface to return a stream of output instead of a single output.  Treating blocks as streams requires this capability.

### Changes 🏗️

* Update block run interface from returning `(output_name, output_data)` to `Generator[(output_name, output_data)]`
* Removed `agent` term in the API, replace it with `graph` for consistency.
* Reintroduced `AgentNodeExecutionInputOutput`. `AgentNodeExecution` input & output will be a list of `AgentNodeExecutionInputOutput` which describes the input & output data of its execution. Making an execution has 1-many relation to its input output data.
* Propagating the relation and block interface change into the execution engine.
2024-06-26 12:41:55 +02:00
Nicholas Tindle
f04ddceacf docs(forge): Update and rename QUICKSTART.md to FORGE-QUICKSTART.md (#7215) 2024-06-25 19:03:05 -05:00
Bently
3e01b19d6f chore(forge): Update duckduckgo-search to v6.1.7 (#7254)
This should (for now) mitigate the RateLimitErrors that people have been experiencing.
---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
2024-06-25 03:24:10 +02:00
Zamil Majdy
9f1e521857 feat(rnd): Add AutoGPT server scheduling service (#7226)
### Background

Agent execution should be able to be triggered in a recurring manner.
This PR introduced an ExecutionScheduling service, a process responsible for managing the execution schedule and triggering its execution based on a predefined cron expression.

### Changes 🏗️

* Added `scheduler.py` / `ExecutionScheduler` implementation.
* Added scheduler test.
* Added `AgentExecutionSchedule` table and its logical model & prisma queries.
* Moved `add_execution` from API server to `execution_manager`
2024-06-24 09:41:02 +07:00
Zamil Majdy
d9226888b2 feat(rnd): Add node metadata on Agent Server Node object (#7237) 2024-06-21 17:50:50 +01:00
Zamil Majdy
210d7738b9 feat(rnd): Add IPC support on autogpt_server (#7212)
### Background

This PR adds support on IPC on autogpt_server.
To make this happen, there are a couple of refactoring efforts being made (will be described in the `Changes` section).
Currently, there are three independent processes:

```
AgentServer ----> ExecutionManager
    | 
     --> ExecutionScheduler
```


### Changes 🏗️

* Added Pyro5 for IPC support.
* Introduced `AppService`: a class to construct an independent process that can expose a method to other running processes (this is analogous to a microservice).
* Introduced `AppProcess`: used by `AppService` a class for creating a child process that can be executed in the background.
* Adapting existing codebase to user `AppService`.
2024-06-19 22:49:47 +07:00
Krzysztof Czerwinski
c19ab2b24f feat(forge): Component-specific configuration (#7170)
Remove many env vars and use component-level configuration that could be loaded from file instead.

### Changed

- `BaseAgent` provides `serialize_configs` and `deserialize_configs` that can save and load all component configuration as json `str`. Deserialized components/values overwrite existing values, so not all values need to be present in the serialized config.
- Decoupled `forge/content_processing/text.py` from `Config`
- Kept `execute_local_commands` in `Config` because it's needed to know if OS info should be included in the prompt
- Updated docs to reflect changes
- Renamed `Config` to `AppConfig`

### Added

- Added `ConfigurableComponent` class for components and following configs:
  - `ActionHistoryConfiguration`
  - `CodeExecutorConfiguration`
  - `FileManagerConfiguration` - now file manager allows to have multiple agents using the same workspace
  - `GitOperationsConfiguration`
  - `ImageGeneratorConfiguration`
  - `WebSearchConfiguration`
  - `WebSeleniumConfiguration`
- `BaseConfig` in `forge` and moved `Config` (now inherits from `BaseConfig`) back to `autogpt`
- Required `config_class` attribute for the `ConfigurableComponent` class that should be set to configuration class for a component
`--component-config-file` CLI option and `COMPONENT_CONFIG_FILE` env var and field in `Config`. This option allows to load configuration from a specific file, CLI option takes precedence over env var.
- Added comments to config models

### Removed

- Unused `change_agent_id` method from `FileManagerComponent`
- Unused `allow_downloads` from `Config` and CLI options (it should be in web component config if needed)
- CLI option `--browser-name` (the option is inside `WebSeleniumConfiguration`)
- Unused `workspace_directory` from CLI options
- No longer needed variables from `Config` and docs
- Unused fields from `Config`: `image_size`, `audio_to_text_provider`, `huggingface_audio_to_text_model`
- Removed `files` and `workspace` class attributes from `FileManagerComponent`
2024-06-19 09:14:01 +01:00
Reinier van der Leer
02dc198a9f fix(agent): Fix resumption from mid-cycle state in CLI mode (#7224)
When an agent is resumed from a mid-cycle state (having made a proposal but not executed it yet), we need to use the previously determined `current_episode.action` proposal instead of calling `agent.propose_action()` again.
2024-06-18 19:30:24 +02:00
Reinier van der Leer
227cf41612 fix(agent, forge): Make LLM API key check provider-agnostic (#7220)
* Rename `assert_config_has_openai_api_key` to `assert_config_has_required_llm_api_keys`
* Make OpenAI credential check conditional (only if an OpenAI model is selected in the config)
* Implement checks for Groq and Anthropic credentials
* Use API calls for Groq and OpenAI credential checks to make sure the keys are valid
2024-06-14 20:56:03 +02:00
Reinier van der Leer
66f373fb57 docs: Update CONTRIBUTING.md
- Add reference to Code Guidelines
- Reorder list of directives to more logical order
2024-06-14 07:45:14 -07:00
Nicholas Tindle
8f3ed733b9 chore(rnd): Fix warning during server tests (#7213) 2024-06-14 06:56:53 -05:00
Krzysztof Czerwinski
9f71cd2437 feat(forge): Unbreak forge agent (#7196)
Revert some changes to fix forge agent and enable components support.
- Rename forge `Agent` to `ProtocolAgent`
- Bring back and update `forge/app.py` and `forge/agent/forge_agent.py`
- `ForgeAgent` inherits from `BaseAgent`, supports component execution and runs the same pipelines as autogpt Agent
- Update forge version from 0.1.0 to 0.2.0
- Update code comments
2024-06-12 13:45:00 +01:00
214 changed files with 14222 additions and 3378 deletions

3
.gitattributes vendored
View File

@@ -3,3 +3,6 @@ frontend/build/** linguist-generated
**/poetry.lock linguist-generated
docs/_javascript/** linguist-vendored
# Exclude VCR cassettes from stats
forge/tests/vcr_cassettes/**/**.y*ml linguist-generated

View File

@@ -0,0 +1,36 @@
name: AutoGPT Builder CI
on:
push:
branches: [ master ]
paths:
- '.github/workflows/autogpt-builder-ci.yml'
- 'rnd/autogpt_builder/**'
pull_request:
paths:
- '.github/workflows/autogpt-builder-ci.yml'
- 'rnd/autogpt_builder/**'
defaults:
run:
shell: bash
working-directory: rnd/autogpt_builder
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '21'
- name: Install dependencies
run: |
npm install
- name: Run lint
run: |
npm run lint

View File

@@ -6,13 +6,11 @@ on:
paths:
- '.github/workflows/autogpt-ci.yml'
- 'autogpt/**'
- '!autogpt/tests/vcr_cassettes'
pull_request:
branches: [ master, development, release-* ]
paths:
- '.github/workflows/autogpt-ci.yml'
- 'autogpt/**'
- '!autogpt/tests/vcr_cassettes'
concurrency:
group: ${{ format('autogpt-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
@@ -73,37 +71,6 @@ jobs:
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') }}
env:
PR_BASE: ${{ github.event.pull_request.base.ref }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
cassette_base_branch="${PR_BASE}"
cd tests/vcr_cassettes
if ! git ls-remote --exit-code --heads origin $cassette_base_branch ; then
cassette_base_branch="master"
fi
if git ls-remote --exit-code --heads origin $cassette_branch ; then
git fetch origin $cassette_branch
git fetch origin $cassette_base_branch
git checkout $cassette_branch
# Pick non-conflicting cassette updates from the base branch
git merge --no-commit --strategy-option=ours origin/$cassette_base_branch
echo "Using cassettes from mirror branch '$cassette_branch'," \
"synced to upstream branch '$cassette_base_branch'."
else
git checkout -b $cassette_branch
echo "Branch '$cassette_branch' does not exist in cassette submodule." \
"Using cassettes from '$cassette_base_branch'."
fi
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -163,80 +130,6 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
flags: autogpt-agent,${{ runner.os }}
- 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"
if [ "${{ runner.os }}" = 'macOS' ]; then
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64)
else
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64 -w0)
fi
git config "$config_key" \
"Authorization: Basic $base64_pat"
cd tests/vcr_cassettes
git config "$config_key" \
"Authorization: Basic $base64_pat"
echo "config_key=$config_key" >> $GITHUB_OUTPUT
- id: push_cassettes
name: Push updated cassettes
# For pull requests, push updated cassettes even when tests fail
if: github.event_name == 'push' || (! github.event.pull_request.head.repo.fork && (success() || failure()))
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if [ "${{ startsWith(github.event_name, 'pull_request') }}" = "true" ]; then
is_pull_request=true
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
else
cassette_branch="${{ github.ref_name }}"
fi
cd tests/vcr_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/vcr_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..."
echo $TOKEN | gh auth login --with-token
gh issue edit $PR_NUMBER --add-label "behaviour change"
gh issue comment $PR_NUMBER --body "You changed AutoGPT's behaviour on ${{ runner.os }}. The cassettes have been updated and will be merged to the submodule when this Pull Request gets merged."
fi
- name: Upload logs to artifact
if: always()
uses: actions/upload-artifact@v4

View File

@@ -6,13 +6,11 @@ on:
paths:
- '.github/workflows/autogpt-docker-ci.yml'
- 'autogpt/**'
- '!autogpt/tests/vcr_cassettes'
pull_request:
branches: [ master, development, release-* ]
paths:
- '.github/workflows/autogpt-docker-ci.yml'
- 'autogpt/**'
- '!autogpt/tests/vcr_cassettes'
concurrency:
group: ${{ format('autogpt-docker-ci-{0}', github.head_ref && format('pr-{0}', github.event.pull_request.number) || github.sha) }}

View File

@@ -6,13 +6,11 @@ on:
paths:
- ".github/workflows/autogpt-server-ci.yml"
- "rnd/autogpt_server/**"
- "!autogpt/tests/vcr_cassettes"
pull_request:
branches: [master, development, release-*]
paths:
- ".github/workflows/autogpt-server-ci.yml"
- "rnd/autogpt_server/**"
- "!autogpt/tests/vcr_cassettes"
concurrency:
group: ${{ format('autogpt-server-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
@@ -265,4 +263,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: autogptserver-AppImage-${{ matrix.platform-os }}
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/build/*.AppImage
path: /Users/runner/work/AutoGPT/AutoGPT/rnd/autogpt_server/dist/*.AppImage

View File

@@ -6,11 +6,13 @@ on:
paths:
- '.github/workflows/forge-ci.yml'
- 'forge/**'
- '!forge/tests/vcr_cassettes'
pull_request:
branches: [ master, development, release-* ]
paths:
- '.github/workflows/forge-ci.yml'
- 'forge/**'
- '!forge/tests/vcr_cassettes'
concurrency:
group: ${{ format('forge-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
@@ -66,6 +68,37 @@ jobs:
fetch-depth: 0
submodules: true
- name: Checkout cassettes
if: ${{ startsWith(github.event_name, 'pull_request') }}
env:
PR_BASE: ${{ github.event.pull_request.base.ref }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
cassette_base_branch="${PR_BASE}"
cd tests/vcr_cassettes
if ! git ls-remote --exit-code --heads origin $cassette_base_branch ; then
cassette_base_branch="master"
fi
if git ls-remote --exit-code --heads origin $cassette_branch ; then
git fetch origin $cassette_branch
git fetch origin $cassette_base_branch
git checkout $cassette_branch
# Pick non-conflicting cassette updates from the base branch
git merge --no-commit --strategy-option=ours origin/$cassette_base_branch
echo "Using cassettes from mirror branch '$cassette_branch'," \
"synced to upstream branch '$cassette_base_branch'."
else
git checkout -b $cassette_branch
echo "Branch '$cassette_branch' does not exist in cassette submodule." \
"Using cassettes from '$cassette_base_branch'."
fi
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -121,6 +154,80 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
flags: forge,${{ runner.os }}
- 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"
if [ "${{ runner.os }}" = 'macOS' ]; then
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64)
else
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64 -w0)
fi
git config "$config_key" \
"Authorization: Basic $base64_pat"
cd tests/vcr_cassettes
git config "$config_key" \
"Authorization: Basic $base64_pat"
echo "config_key=$config_key" >> $GITHUB_OUTPUT
- id: push_cassettes
name: Push updated cassettes
# For pull requests, push updated cassettes even when tests fail
if: github.event_name == 'push' || (! github.event.pull_request.head.repo.fork && (success() || failure()))
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if [ "${{ startsWith(github.event_name, 'pull_request') }}" = "true" ]; then
is_pull_request=true
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
else
cassette_branch="${{ github.ref_name }}"
fi
cd tests/vcr_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/vcr_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..."
echo $TOKEN | gh auth login --with-token
gh issue edit $PR_NUMBER --add-label "behaviour change"
gh issue comment $PR_NUMBER --body "You changed AutoGPT's behaviour on ${{ runner.os }}. The cassettes have been updated and will be merged to the submodule when this Pull Request gets merged."
fi
- name: Upload logs to artifact
if: always()
uses: actions/upload-artifact@v4

View File

@@ -5,7 +5,7 @@ on:
push:
branches: [ master, development, release-* ]
paths-ignore:
- 'autogpt/tests/vcr_cassettes'
- 'forge/tests/vcr_cassettes'
- 'benchmark/reports/**'
# So that the `dirtyLabel` is removed if conflicts are resolve
# We recommend `pull_request_target` so that github secrets are available.

View File

@@ -9,7 +9,7 @@ on:
- 'forge/**'
- 'benchmark/**'
- '**.py'
- '!autogpt/tests/vcr_cassettes'
- '!forge/tests/vcr_cassettes'
pull_request:
branches: [ master, development, release-* ]
paths:
@@ -18,7 +18,7 @@ on:
- 'forge/**'
- 'benchmark/**'
- '**.py'
- '!autogpt/tests/vcr_cassettes'
- '!forge/tests/vcr_cassettes'
concurrency:
group: ${{ format('lint-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}

5
.gitignore vendored
View File

@@ -32,7 +32,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -162,7 +161,7 @@ agbenchmark/reports/
# Nodejs
package-lock.json
package.json
# Allow for locally private items
# private
@@ -170,3 +169,5 @@ pri*
# ignore
ig*
.github_access_token
LICENSE.rtf
rnd/autogpt_server/settings.py

4
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "autogpt/tests/vcr_cassettes"]
path = autogpt/tests/vcr_cassettes
[submodule "forge/tests/vcr_cassettes"]
path = forge/tests/vcr_cassettes
url = https://github.com/Significant-Gravitas/Auto-GPT-test-cassettes

View File

@@ -97,7 +97,7 @@ repos:
alias: pyright-benchmark
entry: poetry -C benchmark run pyright
args: [-p, benchmark, benchmark]
files: ^benchmark/(agbenchmark|tests)/
files: ^benchmark/(agbenchmark/|tests/|poetry\.lock$)
types: [file]
language: system
pass_filenames: false

View File

@@ -1,5 +1,5 @@
# AutoGPT Contribution Guide
If you are reading this, you are probably looking for our **[contribution guide]**,
If you are reading this, you are probably looking for the full **[contribution guide]**,
which is part of our [wiki].
Also check out our [🚀 Roadmap][roadmap] for information about our priorities and associated tasks.
@@ -15,15 +15,17 @@ Also check out our [🚀 Roadmap][roadmap] for information about our priorities
2. We encourage you to collaborate with fellow community members on some of our bigger
[todo's][roadmap]!
* We highly recommend to post your idea and discuss it in the [dev channel].
4. Create a draft PR when starting work on bigger changes.
3. Please also consider contributing something other than code; see the
[contribution guide] for options.
3. Create a draft PR when starting work on bigger changes.
4. Adhere to the [Code Guidelines]
5. Clearly explain your changes when submitting a PR.
6. Don't submit broken code: test/validate your changes.
7. Avoid making unnecessary changes, especially if they're purely based on your personal
preferences. Doing so is the maintainers' job. ;-)
8. Please also consider contributing something other than code; see the
[contribution guide] for options.
[dev channel]: https://discord.com/channels/1092243196446249134/1095817829405704305
[code guidelines]: https://github.com/Significant-Gravitas/AutoGPT/wiki/Contributing#code-guidelines
If you wish to involve with the project (beyond just contributing PRs), please read the
wiki page about [Catalyzing](https://github.com/Significant-Gravitas/AutoGPT/wiki/Catalyzing).

View File

@@ -2,11 +2,11 @@
> For the complete getting started [tutorial series](https://aiedge.medium.com/autogpt-forge-e3de53cc58ec) <- click here
Welcome to the Quickstart Guide! This guide will walk you through the process of setting up and running your own AutoGPT agent. Whether you're a seasoned AI developer or just starting out, this guide will provide you with the necessary steps to jumpstart your journey in the world of AI development with AutoGPT.
Welcome to the Quickstart Guide! This guide will walk you through setting up, building, and running your own AutoGPT agent. Whether you're a seasoned AI developer or just starting out, this guide will provide you with the steps to jumpstart your journey in AI development with AutoGPT.
## System Requirements
This project supports Linux (Debian based), Mac, and Windows Subsystem for Linux (WSL). If you are using a Windows system, you will need to install WSL. You can find the installation instructions for WSL [here](https://learn.microsoft.com/en-us/windows/wsl/).
This project supports Linux (Debian-based), Mac, and Windows Subsystem for Linux (WSL). If you use a Windows system, you must install WSL. You can find the installation instructions for WSL [here](https://learn.microsoft.com/en-us/windows/wsl/).
## Getting Setup
@@ -18,11 +18,11 @@ This project supports Linux (Debian based), Mac, and Windows Subsystem for Linux
- In the top-right corner of the page, click Fork.
![Create Fork UI](docs/content/imgs/quickstart/002_fork.png)
- On the next page, select your GitHub account to create the fork under.
- On the next page, select your GitHub account to create the fork.
- Wait for the forking process to complete. You now have a copy of the repository in your GitHub account.
2. **Clone the Repository**
To clone the repository, you need to have Git installed on your system. If you don't have Git installed, you can download it from [here](https://git-scm.com/downloads). Once you have Git installed, follow these steps:
To clone the repository, you need to have Git installed on your system. If you don't have Git installed, download it from [here](https://git-scm.com/downloads). Once you have Git installed, follow these steps:
- Open your terminal.
- Navigate to the directory where you want to clone the repository.
- Run the git clone command for the fork you just created
@@ -34,11 +34,11 @@ This project supports Linux (Debian based), Mac, and Windows Subsystem for Linux
![Open the Project in your IDE](docs/content/imgs/quickstart/004_ide.png)
4. **Setup the Project**
Next we need to setup the required dependencies. We have a tool for helping you do all the tasks you need to on the repo.
Next, we need to set up the required dependencies. We have a tool to help you perform all the tasks on the repo.
It can be accessed by running the `run` command by typing `./run` in the terminal.
The first command you need to use is `./run setup` This will guide you through the process of setting up your system.
Initially you will get instructions for installing flutter, chrome and setting up your github access token like the following image:
The first command you need to use is `./run setup.` This will guide you through setting up your system.
Initially, you will get instructions for installing Flutter and Chrome and setting up your GitHub access token like the following image:
![Setup the Project](docs/content/imgs/quickstart/005_setup.png)
@@ -47,7 +47,7 @@ This project supports Linux (Debian based), Mac, and Windows Subsystem for Linux
If you're a Windows user and experience issues after installing WSL, follow the steps below to resolve them.
#### Update WSL
Run the following command in Powershell or Command Prompt to:
Run the following command in Powershell or Command Prompt:
1. Enable the optional WSL and Virtual Machine Platform components.
2. Download and install the latest Linux kernel.
3. Set WSL 2 as the default.
@@ -73,7 +73,7 @@ dos2unix ./run
After executing the above commands, running `./run setup` should work successfully.
#### Store Project Files within the WSL File System
If you continue to experience issues, consider storing your project files within the WSL file system instead of the Windows file system. This method avoids issues related to path translations and permissions and provides a more consistent development environment.
If you continue to experience issues, consider storing your project files within the WSL file system instead of the Windows file system. This method avoids path translations and permissions issues and provides a more consistent development environment.
You can keep running the command to get feedback on where you are up to with your setup.
When setup has been completed, the command will return an output like this:
@@ -83,7 +83,7 @@ When setup has been completed, the command will return an output like this:
## Creating Your Agent
After completing the setup, the next step is to create your agent template.
Execute the command `./run agent create YOUR_AGENT_NAME`, where `YOUR_AGENT_NAME` should be replaced with a name of your choosing.
Execute the command `./run agent create YOUR_AGENT_NAME`, where `YOUR_AGENT_NAME` should be replaced with your chosen name.
Tips for naming your agent:
* Give it its own unique name, or name it after yourself
@@ -101,21 +101,21 @@ This starts the agent on the URL: `http://localhost:8000/`
![Start the Agent](docs/content/imgs/quickstart/009_start_agent.png)
The frontend can be accessed from `http://localhost:8000/`, you will first need to login using either a google account or your github account.
The front end can be accessed from `http://localhost:8000/`; first, you must log in using either a Google account or your GitHub account.
![Login](docs/content/imgs/quickstart/010_login.png)
Upon logging in you will get a page that looks something like this. With your task history down the left hand side of the page and the 'chat' window to send tasks to your agent.
Upon logging in, you will get a page that looks something like this: your task history down the left-hand side of the page, and the 'chat' window to send tasks to your agent.
![Login](docs/content/imgs/quickstart/011_home.png)
When you have finished with your agent, or if you just need to restart it, use Ctl-C to end the session then you can re-run the start command.
When you have finished with your agent or just need to restart it, use Ctl-C to end the session. Then, you can re-run the start command.
If you are having issues and want to ensure the agent has been stopped there is a `./run agent stop` command which will kill the process using port 8000, which should be the agent.
If you are having issues and want to ensure the agent has been stopped, there is a `./run agent stop` command, which will kill the process using port 8000, which should be the agent.
## Benchmarking your Agent
The benchmarking system can also be accessed using the cli too:
The benchmarking system can also be accessed using the CLI too:
```bash
agpt % ./run benchmark
@@ -163,7 +163,7 @@ The benchmark has been split into different categories of skills you can test yo
![Login](docs/content/imgs/quickstart/012_tests.png)
Finally you can run the benchmark with
Finally, you can run the benchmark with
```bash
./run benchmark start YOUR_AGENT_NAME

View File

@@ -24,7 +24,7 @@ Be part of the revolution! **AutoGPT** is here to stay, at the forefront of AI i
&ensp;|&ensp;
**🚀 [Contributing](CONTRIBUTING.md)**
&ensp;|&ensp;
**🛠️ [Build your own Agent - Quickstart](QUICKSTART.md)**
**🛠️ [Build your own Agent - Quickstart](FORGE-QUICKSTART.md)**
## 🧱 Building blocks

BIN
assets/gpt_dark_RGB.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -15,8 +15,8 @@
## This helps us to spot and solve problems earlier & faster. (Default: DISABLED)
# TELEMETRY_OPT_IN=true
## EXECUTE_LOCAL_COMMANDS - Allow local command execution (Default: False)
# EXECUTE_LOCAL_COMMANDS=False
## COMPONENT_CONFIG_FILE - Path to the json config file (Default: None)
# COMPONENT_CONFIG_FILE=
### Workspace ###
@@ -44,9 +44,6 @@
### Miscellaneous ###
## USER_AGENT - Define the user-agent used by the requests library to browse website (string)
# USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"
## AUTHORISE COMMAND KEY - Key to authorise commands
# AUTHORISE_COMMAND_KEY=y
@@ -96,38 +93,12 @@
## EMBEDDING_MODEL - Model to use for creating embeddings
# EMBEDDING_MODEL=text-embedding-3-small
################################################################################
### 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 AutoGPT (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 AutoGPT (Default: None)
# SHELL_ALLOWLIST=
################################################################################
### IMAGE GENERATION PROVIDER
################################################################################
### Common
## IMAGE_PROVIDER - Image provider (Default: dalle)
# IMAGE_PROVIDER=dalle
## IMAGE_SIZE - Image size (Default: 256)
# IMAGE_SIZE=256
### Huggingface (IMAGE_PROVIDER=huggingface)
## 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=
@@ -136,19 +107,6 @@
## SD_WEBUI_AUTH - Stable Diffusion Web UI username:password pair (Default: None)
# SD_WEBUI_AUTH=
## SD_WEBUI_URL - Stable Diffusion Web UI API URL (Default: http://localhost:7860)
# SD_WEBUI_URL=http://localhost:7860
################################################################################
### AUDIO TO TEXT PROVIDER
################################################################################
## 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
################################################################################
### GITHUB
################################################################################
@@ -163,18 +121,6 @@
### WEB BROWSING
################################################################################
## HEADLESS_BROWSER - Whether to run the browser in headless mode (default: True)
# 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 (Default: 3000)
# BROWSE_CHUNK_MAX_LENGTH=3000
## 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_API_KEY - Google API key (Default: None)
# GOOGLE_API_KEY=
@@ -198,13 +144,6 @@
## ELEVENLABS_VOICE_ID - Eleven Labs voice ID (Example: None)
# ELEVENLABS_VOICE_ID=
################################################################################
### CHAT MESSAGES
################################################################################
## CHAT_MESSAGES_ENABLED - Enable chat messages (Default: False)
# CHAT_MESSAGES_ENABLED=False
################################################################################
### LOGGING
################################################################################

View File

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

View File

@@ -68,10 +68,6 @@ Options:
continuous mode
--speak Enable Speak Mode
--debug Enable Debug Mode
-b, --browser-name TEXT Specifies which web-browser to use when
using selenium to scrape the web.
--allow-downloads Dangerous: Allows AutoGPT to download files
natively.
--skip-news Specifies whether to suppress the output of
latest news on startup.
--install-plugin-deps Installs external dependencies for 3rd party
@@ -90,6 +86,7 @@ Options:
--override-directives If specified, --constraint, --resource and
--best-practice will override the AI's
directives instead of being appended to them
--component-config-file TEXT Path to the json configuration file.
--help Show this message and exit.
```
</details>
@@ -111,10 +108,6 @@ Usage: python -m autogpt serve [OPTIONS]
Options:
--debug Enable Debug Mode
-b, --browser-name TEXT Specifies which web-browser to use when using
selenium to scrape the web.
--allow-downloads Dangerous: Allows AutoGPT to download files
natively.
--install-plugin-deps Installs external dependencies for 3rd party
plugins.
--help Show this message and exit.

View File

@@ -2,17 +2,17 @@ from typing import Optional
from forge.config.ai_directives import AIDirectives
from forge.config.ai_profile import AIProfile
from forge.config.config import Config
from forge.file_storage.base import FileStorage
from forge.llm.providers import MultiProvider
from autogpt.agents.agent import Agent, AgentConfiguration, AgentSettings
from autogpt.app.config import AppConfig
def create_agent(
agent_id: str,
task: str,
app_config: Config,
app_config: AppConfig,
file_storage: FileStorage,
llm_provider: MultiProvider,
ai_profile: Optional[AIProfile] = None,
@@ -38,7 +38,7 @@ def create_agent(
def configure_agent_with_state(
state: AgentSettings,
app_config: Config,
app_config: AppConfig,
file_storage: FileStorage,
llm_provider: MultiProvider,
) -> Agent:
@@ -51,7 +51,7 @@ def configure_agent_with_state(
def _configure_agent(
app_config: Config,
app_config: AppConfig,
llm_provider: MultiProvider,
file_storage: FileStorage,
agent_id: str = "",
@@ -80,7 +80,7 @@ def _configure_agent(
settings=agent_state,
llm_provider=llm_provider,
file_storage=file_storage,
legacy_config=app_config,
app_config=app_config,
)
@@ -89,7 +89,7 @@ def create_agent_state(
task: str,
ai_profile: AIProfile,
directives: AIDirectives,
app_config: Config,
app_config: AppConfig,
) -> AgentSettings:
return AgentSettings(
agent_id=agent_id,
@@ -104,5 +104,5 @@ def create_agent_state(
allow_fs_access=not app_config.restrict_to_workspace,
use_functions_api=app_config.openai_functions,
),
history=Agent.default_settings.history.copy(deep=True),
history=Agent.default_settings.history.model_copy(deep=True),
)

View File

@@ -6,7 +6,7 @@ from forge.file_storage.base import FileStorage
if TYPE_CHECKING:
from autogpt.agents.agent import Agent
from forge.config.config import Config
from autogpt.app.config import AppConfig
from forge.llm.providers import MultiProvider
from .configurators import _configure_agent
@@ -16,7 +16,7 @@ from .profile_generator import generate_agent_profile_for_task
async def generate_agent_for_task(
agent_id: str,
task: str,
app_config: Config,
app_config: AppConfig,
file_storage: FileStorage,
llm_provider: MultiProvider,
) -> Agent:

View File

@@ -3,7 +3,6 @@ import logging
from forge.config.ai_directives import AIDirectives
from forge.config.ai_profile import AIProfile
from forge.config.config import Config
from forge.llm.prompting import ChatPrompt, LanguageModelClassification, PromptStrategy
from forge.llm.providers import MultiProvider
from forge.llm.providers.schema import (
@@ -14,6 +13,8 @@ from forge.llm.providers.schema import (
from forge.models.config import SystemConfiguration, UserConfigurable
from forge.models.json_schema import JSONSchema
from autogpt.app.config import AppConfig
logger = logging.getLogger(__name__)
@@ -136,7 +137,7 @@ class AgentProfileGeneratorConfiguration(SystemConfiguration):
required=True,
),
},
).dict()
).model_dump()
)
@@ -155,7 +156,7 @@ class AgentProfileGenerator(PromptStrategy):
self._model_classification = model_classification
self._system_prompt_message = system_prompt
self._user_prompt_template = user_prompt_template
self._create_agent_function = CompletionModelFunction.parse_obj(
self._create_agent_function = CompletionModelFunction.model_validate(
create_agent_function
)
@@ -212,7 +213,7 @@ class AgentProfileGenerator(PromptStrategy):
async def generate_agent_profile_for_task(
task: str,
app_config: Config,
app_config: AppConfig,
llm_provider: MultiProvider,
) -> tuple[AIProfile, AIDirectives]:
"""Generates an AIConfig object from the given string.
@@ -221,7 +222,7 @@ async def generate_agent_profile_for_task(
AIConfig: The AIConfig object tailored to the user's input
"""
agent_profile_generator = AgentProfileGenerator(
**AgentProfileGenerator.default_configuration.dict() # HACK
**AgentProfileGenerator.default_configuration.model_dump() # HACK
)
prompt = agent_profile_generator.build_prompt(task)

View File

@@ -26,10 +26,10 @@ class MyAgent(Agent):
settings: AgentSettings,
llm_provider: MultiProvider
file_storage: FileStorage,
legacy_config: Config,
app_config: AppConfig,
):
# Call the parent constructor to bring in the default components
super().__init__(settings, llm_provider, file_storage, legacy_config)
super().__init__(settings, llm_provider, file_storage, app_config)
# Add your custom component
self.my_component = MyComponent()
```

View File

@@ -18,7 +18,11 @@ from forge.components.action_history import (
ActionHistoryComponent,
EpisodicActionHistory,
)
from forge.components.code_executor.code_executor import CodeExecutorComponent
from forge.components.action_history.action_history import ActionHistoryConfiguration
from forge.components.code_executor.code_executor import (
CodeExecutorComponent,
CodeExecutorConfiguration,
)
from forge.components.context.context import AgentContext, ContextComponent
from forge.components.file_manager import FileManagerComponent
from forge.components.git_operations import GitOperationsComponent
@@ -58,7 +62,7 @@ from .prompt_strategies.one_shot import (
)
if TYPE_CHECKING:
from forge.config.config import Config
from autogpt.app.config import AppConfig
logger = logging.getLogger(__name__)
@@ -91,12 +95,14 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
settings: AgentSettings,
llm_provider: MultiProvider,
file_storage: FileStorage,
legacy_config: Config,
app_config: AppConfig,
):
super().__init__(settings)
self.llm_provider = llm_provider
prompt_config = OneShotAgentPromptStrategy.default_configuration.copy(deep=True)
prompt_config = OneShotAgentPromptStrategy.default_configuration.model_copy(
deep=True
)
prompt_config.use_functions_api = (
settings.config.use_functions_api
# Anthropic currently doesn't support tools + prefilling :(
@@ -107,33 +113,41 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
# Components
self.system = SystemComponent()
self.history = ActionHistoryComponent(
settings.history,
self.send_token_limit,
lambda x: self.llm_provider.count_tokens(x, self.llm.name),
legacy_config,
llm_provider,
).run_after(WatchdogComponent)
self.user_interaction = UserInteractionComponent(legacy_config)
self.file_manager = FileManagerComponent(settings, file_storage)
self.history = (
ActionHistoryComponent(
settings.history,
lambda x: self.llm_provider.count_tokens(x, self.llm.name),
llm_provider,
ActionHistoryConfiguration(
model_name=app_config.fast_llm, max_tokens=self.send_token_limit
),
)
.run_after(WatchdogComponent)
.run_after(SystemComponent)
)
if not app_config.noninteractive_mode:
self.user_interaction = UserInteractionComponent()
self.file_manager = FileManagerComponent(file_storage, settings)
self.code_executor = CodeExecutorComponent(
self.file_manager.workspace,
settings,
legacy_config,
CodeExecutorConfiguration(
docker_container_name=f"{settings.agent_id}_sandbox"
),
)
self.git_ops = GitOperationsComponent(legacy_config)
self.image_gen = ImageGeneratorComponent(
self.file_manager.workspace, legacy_config
self.git_ops = GitOperationsComponent()
self.image_gen = ImageGeneratorComponent(self.file_manager.workspace)
self.web_search = WebSearchComponent()
self.web_selenium = WebSeleniumComponent(
llm_provider,
app_config.app_data_dir,
)
self.web_search = WebSearchComponent(legacy_config)
self.web_selenium = WebSeleniumComponent(legacy_config, llm_provider, self.llm)
self.context = ContextComponent(self.file_manager.workspace, settings.context)
self.watchdog = WatchdogComponent(settings.config, settings.history).run_after(
ContextComponent
)
self.event_history = settings.history
self.legacy_config = legacy_config
self.app_config = app_config
async def propose_action(self) -> OneShotAgentActionProposal:
"""Proposes the next action to execute, based on the task and current state.
@@ -148,7 +162,7 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
constraints = await self.run_pipeline(DirectiveProvider.get_constraints)
best_practices = await self.run_pipeline(DirectiveProvider.get_best_practices)
directives = self.state.directives.copy(deep=True)
directives = self.state.directives.model_copy(deep=True)
directives.resources += resources
directives.constraints += constraints
directives.best_practices += best_practices
@@ -166,7 +180,7 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
ai_profile=self.state.ai_profile,
ai_directives=directives,
commands=function_specs_from_commands(self.commands),
include_os_info=self.legacy_config.execute_local_commands,
include_os_info=self.code_executor.config.execute_local_commands,
)
logger.debug(f"Executing prompt:\n{dump_prompt(prompt)}")
@@ -277,7 +291,7 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
command
for command in self.commands
if not any(
name in self.legacy_config.disabled_commands for name in command.names
name in self.app_config.disabled_commands for name in command.names
)
]

View File

@@ -28,15 +28,13 @@ _RESPONSE_INTERFACE_NAME = "AssistantResponse"
class AssistantThoughts(ModelWithSummary):
observations: str = Field(
..., description="Relevant observations from your last action (if any)"
description="Relevant observations from your last action (if any)"
)
text: str = Field(..., description="Thoughts")
reasoning: str = Field(..., description="Reasoning behind the thoughts")
self_criticism: str = Field(..., description="Constructive self-criticism")
plan: list[str] = Field(
..., description="Short list that conveys the long-term plan"
)
speak: str = Field(..., description="Summary of thoughts, to say to user")
text: str = Field(description="Thoughts")
reasoning: str = Field(description="Reasoning behind the thoughts")
self_criticism: str = Field(description="Constructive self-criticism")
plan: list[str] = Field(description="Short list that conveys the long-term plan")
speak: str = Field(description="Summary of thoughts, to say to user")
def summary(self) -> str:
return self.text
@@ -96,7 +94,9 @@ class OneShotAgentPromptStrategy(PromptStrategy):
logger: Logger,
):
self.config = configuration
self.response_schema = JSONSchema.from_dict(OneShotAgentActionProposal.schema())
self.response_schema = JSONSchema.from_dict(
OneShotAgentActionProposal.model_json_schema()
)
self.logger = logger
@property
@@ -182,7 +182,7 @@ class OneShotAgentPromptStrategy(PromptStrategy):
)
def response_format_instruction(self, use_functions_api: bool) -> tuple[str, str]:
response_schema = self.response_schema.copy(deep=True)
response_schema = self.response_schema.model_copy(deep=True)
assert response_schema.properties
if use_functions_api and "use_tool" in response_schema.properties:
del response_schema.properties["use_tool"]
@@ -274,5 +274,8 @@ class OneShotAgentPromptStrategy(PromptStrategy):
raise InvalidAgentResponseError("Assistant did not use a tool")
assistant_reply_dict["use_tool"] = response.tool_calls[0].function
parsed_response = OneShotAgentActionProposal.parse_obj(assistant_reply_dict)
parsed_response = OneShotAgentActionProposal.model_validate(
assistant_reply_dict
)
parsed_response.raw_message = response.copy()
return parsed_response

View File

@@ -23,7 +23,6 @@ from forge.agent_protocol.models import (
TaskRequestBody,
TaskStepsListResponse,
)
from forge.config.config import Config
from forge.file_storage import FileStorage
from forge.llm.providers import ModelProviderBudget, MultiProvider
from forge.models.action import ActionErrorResult, ActionSuccessResult
@@ -35,6 +34,7 @@ from sentry_sdk import set_user
from autogpt.agent_factory.configurators import configure_agent_with_state, create_agent
from autogpt.agents.agent_manager import AgentManager
from autogpt.app.config import AppConfig
from autogpt.app.utils import is_port_free
logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ class AgentProtocolServer:
def __init__(
self,
app_config: Config,
app_config: AppConfig,
database: AgentDB,
file_storage: FileStorage,
llm_provider: MultiProvider,
@@ -314,7 +314,7 @@ class AgentProtocolServer:
""
if tool_result is None
else (
orjson.loads(tool_result.json())
orjson.loads(tool_result.model_dump_json())
if not isinstance(tool_result, ActionErrorResult)
else {
"error": str(tool_result.error),
@@ -327,7 +327,7 @@ class AgentProtocolServer:
if last_proposal and tool_result
else {}
),
**assistant_response.dict(),
**assistant_response.model_dump(),
}
task_cumulative_cost = agent.llm_provider.get_incurred_cost()
@@ -451,7 +451,9 @@ class AgentProtocolServer:
"""
task_llm_budget = self._task_budgets[task.task_id]
task_llm_provider_config = self.llm_provider._configuration.copy(deep=True)
task_llm_provider_config = self.llm_provider._configuration.model_copy(
deep=True
)
_extra_request_headers = task_llm_provider_config.extra_request_headers
_extra_request_headers["AP-TaskID"] = task.task_id
if step_id:
@@ -459,7 +461,7 @@ class AgentProtocolServer:
if task.additional_input and (user_id := task.additional_input.get("user_id")):
_extra_request_headers["AutoGPT-UserID"] = user_id
settings = self.llm_provider._settings.copy()
settings = self.llm_provider._settings.model_copy()
settings.budget = task_llm_budget
settings.configuration = task_llm_provider_config
task_llm_provider = self.llm_provider.__class__(

View File

@@ -28,24 +28,6 @@ def cli(ctx: click.Context):
help="Defines the number of times to run in continuous mode",
)
@click.option("--speak", is_flag=True, help="Enable Speak Mode")
@click.option(
"-b",
"--browser-name",
help="Specifies which web-browser to use when using selenium to scrape the web.",
)
@click.option(
"--allow-downloads",
is_flag=True,
help="Dangerous: Allows AutoGPT to download files natively.",
)
@click.option(
# TODO: this is a hidden option for now, necessary for integration testing.
# We should make this public once we're ready to roll out agent specific workspaces.
"--workspace-directory",
"-w",
type=click.Path(file_okay=False),
hidden=True,
)
@click.option(
"--install-plugin-deps",
is_flag=True,
@@ -128,13 +110,15 @@ def cli(ctx: click.Context):
),
type=click.Choice([i.value for i in LogFormatName]),
)
@click.option(
"--component-config-file",
help="Path to a json configuration file",
type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path),
)
def run(
continuous: bool,
continuous_limit: Optional[int],
speak: bool,
browser_name: Optional[str],
allow_downloads: bool,
workspace_directory: Optional[Path],
install_plugin_deps: bool,
skip_news: bool,
skip_reprompt: bool,
@@ -148,6 +132,7 @@ def run(
log_level: Optional[str],
log_format: Optional[str],
log_file_format: Optional[str],
component_config_file: Optional[Path],
) -> None:
"""
Sets up and runs an agent, based on the task specified by the user, or resumes an
@@ -165,10 +150,7 @@ def run(
log_level=log_level,
log_format=log_format,
log_file_format=log_file_format,
browser_name=browser_name,
allow_downloads=allow_downloads,
skip_news=skip_news,
workspace_directory=workspace_directory,
install_plugin_deps=install_plugin_deps,
override_ai_name=ai_name,
override_ai_role=ai_role,
@@ -176,20 +158,11 @@ def run(
constraints=list(constraint),
best_practices=list(best_practice),
override_directives=override_directives,
component_config_file=component_config_file,
)
@cli.command()
@click.option(
"-b",
"--browser-name",
help="Specifies which web-browser to use when using selenium to scrape the web.",
)
@click.option(
"--allow-downloads",
is_flag=True,
help="Dangerous: Allows AutoGPT to download files natively.",
)
@click.option(
"--install-plugin-deps",
is_flag=True,
@@ -217,8 +190,6 @@ def run(
type=click.Choice([i.value for i in LogFormatName]),
)
def serve(
browser_name: Optional[str],
allow_downloads: bool,
install_plugin_deps: bool,
debug: bool,
log_level: Optional[str],
@@ -237,8 +208,6 @@ def serve(
log_level=log_level,
log_format=log_format,
log_file_format=log_file_format,
browser_name=browser_name,
allow_downloads=allow_downloads,
install_plugin_deps=install_plugin_deps,
)

View File

@@ -0,0 +1,221 @@
"""Configuration class to store the state of bools for different scripts access."""
from __future__ import annotations
import logging
import os
import re
from pathlib import Path
from typing import Optional, Union
import forge
from forge.config.base import BaseConfig
from forge.llm.providers import CHAT_MODELS, ModelName
from forge.llm.providers.openai import OpenAICredentials, OpenAIModelName
from forge.logging.config import LoggingConfig
from forge.models.config import Configurable, UserConfigurable
from pydantic import SecretStr, ValidationInfo, field_validator
logger = logging.getLogger(__name__)
PROJECT_ROOT = Path(forge.__file__).parent.parent
AZURE_CONFIG_FILE = Path("azure.yaml")
GPT_4_MODEL = OpenAIModelName.GPT4
GPT_3_MODEL = OpenAIModelName.GPT3
class AppConfig(BaseConfig):
name: str = "Auto-GPT configuration"
description: str = "Default configuration for the Auto-GPT application."
########################
# Application Settings #
########################
project_root: Path = PROJECT_ROOT
app_data_dir: Path = project_root / "data"
skip_news: bool = False
skip_reprompt: bool = False
authorise_key: str = UserConfigurable(default="y", from_env="AUTHORISE_COMMAND_KEY")
exit_key: str = UserConfigurable(default="n", from_env="EXIT_KEY")
noninteractive_mode: bool = False
logging: LoggingConfig = LoggingConfig()
component_config_file: Optional[Path] = UserConfigurable(
default=None, from_env="COMPONENT_CONFIG_FILE"
)
##########################
# Agent Control Settings #
##########################
# Model configuration
fast_llm: ModelName = UserConfigurable(
default=OpenAIModelName.GPT3,
from_env="FAST_LLM",
)
smart_llm: ModelName = UserConfigurable(
default=OpenAIModelName.GPT4_TURBO,
from_env="SMART_LLM",
)
temperature: float = UserConfigurable(default=0, from_env="TEMPERATURE")
openai_functions: bool = UserConfigurable(
default=False, from_env=lambda: os.getenv("OPENAI_FUNCTIONS", "False") == "True"
)
embedding_model: str = UserConfigurable(
default="text-embedding-3-small", from_env="EMBEDDING_MODEL"
)
# Run loop configuration
continuous_mode: bool = False
continuous_limit: int = 0
############
# Commands #
############
# General
disabled_commands: list[str] = UserConfigurable(
default_factory=list,
from_env=lambda: _safe_split(os.getenv("DISABLED_COMMANDS")),
)
# File ops
restrict_to_workspace: bool = UserConfigurable(
default=True,
from_env=lambda: os.getenv("RESTRICT_TO_WORKSPACE", "True") == "True",
)
###############
# Credentials #
###############
# OpenAI
openai_credentials: Optional[OpenAICredentials] = None
azure_config_file: Optional[Path] = UserConfigurable(
default=AZURE_CONFIG_FILE, from_env="AZURE_CONFIG_FILE"
)
@field_validator("openai_functions")
def validate_openai_functions(cls, value: bool, info: ValidationInfo):
if value:
smart_llm = info.data["smart_llm"]
assert CHAT_MODELS[smart_llm].has_function_call_api, (
f"Model {smart_llm} does not support tool calling. "
"Please disable OPENAI_FUNCTIONS or choose a suitable model."
)
return value
class ConfigBuilder(Configurable[AppConfig]):
default_settings = AppConfig()
@classmethod
def build_config_from_env(cls, project_root: Path = PROJECT_ROOT) -> AppConfig:
"""Initialize the Config class"""
config = cls.build_agent_configuration()
config.project_root = project_root
# Make relative paths absolute
for k in {
"azure_config_file", # TODO: move from project root
}:
setattr(config, k, project_root / getattr(config, k))
if (
config.openai_credentials
and config.openai_credentials.api_type == SecretStr("azure")
and (config_file := config.azure_config_file)
):
config.openai_credentials.load_azure_config(config_file)
return config
async def assert_config_has_required_llm_api_keys(config: AppConfig) -> None:
"""
Check if API keys (if required) are set for the configured SMART_LLM and FAST_LLM.
"""
from forge.llm.providers.anthropic import AnthropicModelName
from forge.llm.providers.groq import GroqModelName
from pydantic import ValidationError
if set((config.smart_llm, config.fast_llm)).intersection(AnthropicModelName):
from forge.llm.providers.anthropic import AnthropicCredentials
try:
credentials = AnthropicCredentials.from_env()
except ValidationError as e:
if "api_key" in str(e):
logger.error(
"Set your Anthropic API key in .env or as an environment variable"
)
logger.info(
"For further instructions: "
"https://docs.agpt.co/autogpt/setup/#anthropic"
)
raise ValueError("Anthropic is unavailable: can't load credentials") from e
key_pattern = r"^sk-ant-api03-[\w\-]{95}"
# If key is set, but it looks invalid
if not re.search(key_pattern, credentials.api_key.get_secret_value()):
logger.warning(
"Possibly invalid Anthropic API key! "
f"Configured Anthropic API key does not match pattern '{key_pattern}'. "
"If this is a valid key, please report this warning to the maintainers."
)
if set((config.smart_llm, config.fast_llm)).intersection(GroqModelName):
from forge.llm.providers.groq import GroqProvider
from groq import AuthenticationError
try:
groq = GroqProvider()
await groq.get_available_models()
except ValidationError as e:
if "api_key" not in str(e):
raise
logger.error("Set your Groq API key in .env or as an environment variable")
logger.info(
"For further instructions: https://docs.agpt.co/autogpt/setup/#groq"
)
raise ValueError("Groq is unavailable: can't load credentials")
except AuthenticationError as e:
logger.error("The Groq API key is invalid!")
logger.info(
"For instructions to get and set a new API key: "
"https://docs.agpt.co/autogpt/setup/#groq"
)
raise ValueError("Groq is unavailable: invalid API key") from e
if set((config.smart_llm, config.fast_llm)).intersection(OpenAIModelName):
from forge.llm.providers.openai import OpenAIProvider
from openai import AuthenticationError
try:
openai = OpenAIProvider()
await openai.get_available_models()
except ValidationError as e:
if "api_key" not in str(e):
raise
logger.error(
"Set your OpenAI API key in .env or as an environment variable"
)
logger.info(
"For further instructions: https://docs.agpt.co/autogpt/setup/#openai"
)
raise ValueError("OpenAI is unavailable: can't load credentials")
except AuthenticationError as e:
logger.error("The OpenAI API key is invalid!")
logger.info(
"For instructions to get and set a new API key: "
"https://docs.agpt.co/autogpt/setup/#openai"
)
raise ValueError("OpenAI is unavailable: invalid API key") from e
def _safe_split(s: Union[str, None], sep: str = ",") -> list[str]:
"""Split a string by a separator. Return an empty list if the string is None."""
if s is None:
return []
return s.split(sep)

View File

@@ -5,20 +5,18 @@ import logging
from typing import Literal, Optional
import click
from colorama import Back, Style
from forge.config.config import GPT_3_MODEL, Config
from forge.llm.providers import ModelName, MultiProvider
from autogpt.app.config import GPT_3_MODEL, AppConfig
logger = logging.getLogger(__name__)
async def apply_overrides_to_config(
config: Config,
config: AppConfig,
continuous: bool = False,
continuous_limit: Optional[int] = None,
skip_reprompt: bool = False,
browser_name: Optional[str] = None,
allow_downloads: bool = False,
skip_news: bool = False,
) -> None:
"""Updates the config object with the given arguments.
@@ -33,8 +31,6 @@ async def apply_overrides_to_config(
log_level (int): The global log level for the application.
log_format (str): The format for the log(s).
log_file_format (str): Override the format for the log file.
browser_name (str): The name of the browser to use for scraping the web.
allow_downloads (bool): Whether to allow AutoGPT to download files natively.
skips_news (bool): Whether to suppress the output of latest news on startup.
"""
config.continuous_mode = False
@@ -61,23 +57,6 @@ async def apply_overrides_to_config(
if skip_reprompt:
config.skip_reprompt = True
if browser_name:
config.selenium_web_browser = browser_name
if allow_downloads:
logger.warning(
msg=f"{Back.LIGHTYELLOW_EX}"
"AutoGPT will now be able to download and save files to your machine."
f"{Back.RESET}"
" It is recommended that you monitor any files it downloads carefully.",
)
logger.warning(
msg=f"{Back.RED + Style.BRIGHT}"
"NEVER OPEN FILES YOU AREN'T SURE OF!"
f"{Style.RESET_ALL}",
)
config.allow_downloads = True
if skip_news:
config.skip_news = True

View File

@@ -21,7 +21,6 @@ from forge.components.code_executor.code_executor import (
)
from forge.config.ai_directives import AIDirectives
from forge.config.ai_profile import AIProfile
from forge.config.config import Config, ConfigBuilder, assert_config_has_openai_api_key
from forge.file_storage import FileStorageBackendName, get_storage
from forge.llm.providers import MultiProvider
from forge.logging.config import configure_logging
@@ -34,6 +33,11 @@ from forge.utils.exceptions import AgentTerminated, InvalidAgentResponseError
from autogpt.agent_factory.configurators import configure_agent_with_state, create_agent
from autogpt.agents.agent_manager import AgentManager
from autogpt.agents.prompt_strategies.one_shot import AssistantThoughts
from autogpt.app.config import (
AppConfig,
ConfigBuilder,
assert_config_has_required_llm_api_keys,
)
if TYPE_CHECKING:
from autogpt.agents.agent import Agent
@@ -62,10 +66,7 @@ async def run_auto_gpt(
log_level: Optional[str] = None,
log_format: Optional[str] = None,
log_file_format: Optional[str] = None,
browser_name: Optional[str] = None,
allow_downloads: bool = False,
skip_news: bool = False,
workspace_directory: Optional[Path] = None,
install_plugin_deps: bool = False,
override_ai_name: Optional[str] = None,
override_ai_role: Optional[str] = None,
@@ -73,6 +74,7 @@ async def run_auto_gpt(
constraints: Optional[list[str]] = None,
best_practices: Optional[list[str]] = None,
override_directives: bool = False,
component_config_file: Optional[Path] = None,
):
# Set up configuration
config = ConfigBuilder.build_config_from_env()
@@ -98,16 +100,13 @@ async def run_auto_gpt(
tts_config=config.tts_config,
)
# TODO: fill in llm values here
assert_config_has_openai_api_key(config)
await assert_config_has_required_llm_api_keys(config)
await apply_overrides_to_config(
config=config,
continuous=continuous,
continuous_limit=continuous_limit,
skip_reprompt=skip_reprompt,
browser_name=browser_name,
allow_downloads=allow_downloads,
skip_news=skip_news,
)
@@ -132,15 +131,12 @@ async def run_auto_gpt(
print_python_version_info(logger)
print_attribute("Smart LLM", config.smart_llm)
print_attribute("Fast LLM", config.fast_llm)
print_attribute("Browser", config.selenium_web_browser)
if config.continuous_mode:
print_attribute("Continuous Mode", "ENABLED", title_color=Fore.YELLOW)
if continuous_limit:
print_attribute("Continuous Limit", config.continuous_limit)
if config.tts_config.speak_mode:
print_attribute("Speak Mode", "ENABLED")
if config.allow_downloads:
print_attribute("Native Downloading", "ENABLED")
if we_are_running_in_a_docker_container() or is_docker_available():
print_attribute("Code Execution", "ENABLED")
else:
@@ -327,6 +323,14 @@ async def run_auto_gpt(
# )
# ).add_done_callback(update_agent_directives)
# Load component configuration from file
if _config_file := component_config_file or config.component_config_file:
try:
logger.info(f"Loading component configuration from {_config_file}")
agent.load_component_configs(_config_file.read_text())
except Exception as e:
logger.error(f"Could not load component configuration: {e}")
#################
# Run the Agent #
#################
@@ -353,8 +357,6 @@ async def run_auto_gpt_server(
log_level: Optional[str] = None,
log_format: Optional[str] = None,
log_file_format: Optional[str] = None,
browser_name: Optional[str] = None,
allow_downloads: bool = False,
install_plugin_deps: bool = False,
):
from .agent_protocol_server import AgentProtocolServer
@@ -380,13 +382,10 @@ async def run_auto_gpt_server(
tts_config=config.tts_config,
)
# TODO: fill in llm values here
assert_config_has_openai_api_key(config)
await assert_config_has_required_llm_api_keys(config)
await apply_overrides_to_config(
config=config,
browser_name=browser_name,
allow_downloads=allow_downloads,
)
llm_provider = _configure_llm_provider(config)
@@ -411,7 +410,7 @@ async def run_auto_gpt_server(
)
def _configure_llm_provider(config: Config) -> MultiProvider:
def _configure_llm_provider(config: AppConfig) -> MultiProvider:
multi_provider = MultiProvider()
for model in [config.smart_llm, config.fast_llm]:
# Ensure model providers for configured LLMs are available
@@ -451,15 +450,15 @@ async def run_interaction_loop(
None
"""
# These contain both application config and agent config, so grab them here.
legacy_config = agent.legacy_config
app_config = agent.app_config
ai_profile = agent.state.ai_profile
logger = logging.getLogger(__name__)
cycle_budget = cycles_remaining = _get_cycle_budget(
legacy_config.continuous_mode, legacy_config.continuous_limit
app_config.continuous_mode, app_config.continuous_limit
)
spinner = Spinner(
"Thinking...", plain_output=legacy_config.logging.plain_console_output
"Thinking...", plain_output=app_config.logging.plain_console_output
)
stop_reason = None
@@ -508,22 +507,25 @@ async def run_interaction_loop(
########
handle_stop_signal()
# Have the agent determine the next action to take.
with spinner:
try:
action_proposal = await agent.propose_action()
except InvalidAgentResponseError as e:
logger.warning(f"The agent's thoughts could not be parsed: {e}")
consecutive_failures += 1
if consecutive_failures >= 3:
logger.error(
"The agent failed to output valid thoughts"
f" {consecutive_failures} times in a row. Terminating..."
)
raise AgentTerminated(
"The agent failed to output valid thoughts"
f" {consecutive_failures} times in a row."
)
continue
if not (_ep := agent.event_history.current_episode) or _ep.result:
with spinner:
try:
action_proposal = await agent.propose_action()
except InvalidAgentResponseError as e:
logger.warning(f"The agent's thoughts could not be parsed: {e}")
consecutive_failures += 1
if consecutive_failures >= 3:
logger.error(
"The agent failed to output valid thoughts"
f" {consecutive_failures} times in a row. Terminating..."
)
raise AgentTerminated(
"The agent failed to output valid thoughts"
f" {consecutive_failures} times in a row."
)
continue
else:
action_proposal = _ep.action
consecutive_failures = 0
@@ -534,7 +536,7 @@ async def run_interaction_loop(
update_user(
ai_profile,
action_proposal,
speak_mode=legacy_config.tts_config.speak_mode,
speak_mode=app_config.tts_config.speak_mode,
)
##################
@@ -543,7 +545,7 @@ async def run_interaction_loop(
handle_stop_signal()
if cycles_remaining == 1: # Last cycle
feedback_type, feedback, new_cycles_remaining = await get_user_feedback(
legacy_config,
app_config,
ai_profile,
)
@@ -654,7 +656,7 @@ def update_user(
async def get_user_feedback(
config: Config,
config: AppConfig,
ai_profile: AIProfile,
) -> tuple[UserFeedback, str, int | None]:
"""Gets the user's feedback on the assistant's reply.

View File

@@ -4,9 +4,10 @@ from typing import Optional
from forge.config.ai_directives import AIDirectives
from forge.config.ai_profile import AIProfile
from forge.config.config import Config
from forge.logging.utils import print_attribute
from autogpt.app.config import AppConfig
from .input import clean_input
logger = logging.getLogger(__name__)
@@ -46,7 +47,7 @@ def apply_overrides_to_ai_settings(
async def interactively_revise_ai_settings(
ai_profile: AIProfile,
directives: AIDirectives,
app_config: Config,
app_config: AppConfig,
):
"""Interactively revise the AI settings.

913
autogpt/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,44 +20,24 @@ serve = "autogpt.app.cli:serve"
[tool.poetry.dependencies]
python = "^3.10"
anthropic = "^0.25.1"
autogpt-forge = { path = "../forge", develop = true }
# autogpt-forge = {git = "https://github.com/Significant-Gravitas/AutoGPT.git", subdirectory = "forge"}
beautifulsoup4 = "^4.12.2"
charset-normalizer = "^3.1.0"
click = "*"
colorama = "^0.4.6"
distro = "^1.8.0"
en-core-web-sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl" }
fastapi = "^0.109.1"
ftfy = "^6.1.1"
google-api-python-client = "*"
gitpython = "^3.1.32"
hypercorn = "^0.14.4"
inflection = "*"
jsonschema = "*"
numpy = "*"
openai = "^1.7.2"
orjson = "^3.8.10"
Pillow = "*"
pydantic = "*"
python-docx = "*"
pydantic = "^2.7.2"
python-dotenv = "^1.0.0"
pyyaml = "^6.0"
readability-lxml = "^0.8.1"
requests = "*"
sentry-sdk = "^1.40.4"
spacy = "^3.7.4"
tenacity = "^8.2.2"
# OpenAI and Generic plugins import
openapi-python-client = "^0.14.0"
# Benchmarking
agbenchmark = { path = "../benchmark", optional = true }
# agbenchmark = {git = "https://github.com/Significant-Gravitas/AutoGPT.git", subdirectory = "benchmark", optional = true}
psycopg2-binary = "^2.9.9"
multidict = "6.0.5"
cx-freeze = "7.0.0"
[tool.poetry.extras]
benchmark = ["agbenchmark"]
@@ -65,27 +45,28 @@ benchmark = ["agbenchmark"]
[tool.poetry.group.dev.dependencies]
black = "^23.12.1"
flake8 = "^7.0.0"
gitpython = "^3.1.32"
isort = "^5.13.1"
pre-commit = "*"
pyright = "^1.1.364"
types-beautifulsoup4 = "*"
# Type stubs
types-colorama = "*"
types-Markdown = "*"
types-Pillow = "*"
# Testing
asynctest = "*"
coverage = "*"
pytest = "*"
pytest-asyncio = "*"
pytest-benchmark = "*"
pytest-cov = "*"
pytest-integration = "*"
pytest-mock = "*"
pytest-recording = "*"
pytest-xdist = "*"
vcrpy = { git = "https://github.com/Significant-Gravitas/vcrpy.git", rev = "master" }
[tool.poetry.group.build]
optional = true
[tool.poetry.group.build.dependencies]
cx-freeze = { git = "https://github.com/ntindle/cx_Freeze.git", rev = "main" }
# HACK: switch to cx-freeze release package after #2442 and #2472 are merged: https://github.com/marcelotduarte/cx_Freeze/pulls?q=is:pr+%232442+OR+%232472+
# cx-freeze = { version = "^7.2.0", optional = true }
[build-system]
@@ -108,7 +89,3 @@ skip_glob = ["data"]
pythonVersion = "3.10"
exclude = ["data/**", "**/node_modules", "**/__pycache__", "**/.*"]
ignore = ["../forge/**"]
[tool.pytest.ini_options]
markers = ["slow", "requires_openai_api_key", "requires_huggingface_api_key"]

View File

@@ -19,7 +19,7 @@ from autogpt.app.utils import coroutine
help="Path to the git repository",
)
@coroutine
async def generate_release_notes(repo_path: Optional[Path] = None):
async def generate_release_notes(repo_path: Optional[str | Path] = None):
logger = logging.getLogger(generate_release_notes.name) # pyright: ignore
repo = Repo(repo_path, search_parent_directories=True)

View File

@@ -1,7 +1,9 @@
import platform
from pathlib import Path
from pkgutil import iter_modules
from shutil import which
from typing import Union
from cx_Freeze import Executable, setup
from cx_Freeze import Executable, setup # type: ignore
packages = [
m.name
@@ -11,11 +13,47 @@ packages = [
and ("poetry" in m.module_finder.path) # type: ignore
]
icon = (
"../../assets/gpt_dark_RGB.icns"
if which("sips")
else "../../assets/gpt_dark_RGB.ico"
)
# set the icon based on the platform
icon = "../../assets/gpt_dark_RGB.ico"
if platform.system() == "Darwin":
icon = "../../assets/gpt_dark_RGB.icns"
elif platform.system() == "Linux":
icon = "../../assets/gpt_dark_RGB.png"
def txt_to_rtf(input_file: Union[str, Path], output_file: Union[str, Path]) -> None:
"""
Convert a text file to RTF format.
Args:
input_file (Union[str, Path]): Path to the input text file.
output_file (Union[str, Path]): Path to the output RTF file.
Returns:
None
"""
input_path = Path(input_file)
output_path = Path(output_file)
with input_path.open("r", encoding="utf-8") as txt_file:
content = txt_file.read()
# RTF header
rtf = r"{\rtf1\ansi\deff0 {\fonttbl {\f0 Times New Roman;}}\f0\fs24 "
# Replace newlines with RTF newline
rtf += content.replace("\n", "\\par ")
# Close RTF document
rtf += "}"
with output_path.open("w", encoding="utf-8") as rtf_file:
rtf_file.write(rtf)
# Convert LICENSE to LICENSE.rtf
license_file = "LICENSE.rtf"
txt_to_rtf("../LICENSE", license_file)
setup(
@@ -55,6 +93,7 @@ setup(
"target_name": "AutoGPT",
"add_to_path": True,
"install_icon": "../assets/gpt_dark_RGB.ico",
"license_file": license_file,
},
},
)

View File

@@ -6,7 +6,6 @@ from pathlib import Path
import pytest
from forge.config.ai_profile import AIProfile
from forge.config.config import Config, ConfigBuilder
from forge.file_storage.local import (
FileStorage,
FileStorageConfiguration,
@@ -16,11 +15,11 @@ from forge.llm.providers import MultiProvider
from forge.logging.config import configure_logging
from autogpt.agents.agent import Agent, AgentConfiguration, AgentSettings
from autogpt.app.config import AppConfig, ConfigBuilder
from autogpt.app.main import _configure_llm_provider
pytest_plugins = [
"tests.integration.agent_factory",
"tests.vcr",
]
@@ -62,7 +61,7 @@ def config(
@pytest.fixture(scope="session")
def setup_logger(config: Config):
def setup_logger():
configure_logging(
debug=True,
log_dir=Path(__file__).parent / "logs",
@@ -71,12 +70,14 @@ def setup_logger(config: Config):
@pytest.fixture
def llm_provider(config: Config) -> MultiProvider:
def llm_provider(config: AppConfig) -> MultiProvider:
return _configure_llm_provider(config)
@pytest.fixture
def agent(config: Config, llm_provider: MultiProvider, storage: FileStorage) -> Agent:
def agent(
config: AppConfig, llm_provider: MultiProvider, storage: FileStorage
) -> Agent:
ai_profile = AIProfile(
ai_name="Base",
ai_role="A base AI",
@@ -94,13 +95,13 @@ def agent(config: Config, llm_provider: MultiProvider, storage: FileStorage) ->
allow_fs_access=not config.restrict_to_workspace,
use_functions_api=config.openai_functions,
),
history=Agent.default_settings.history.copy(deep=True),
history=Agent.default_settings.history.model_copy(deep=True),
)
agent = Agent(
settings=agent_settings,
llm_provider=llm_provider,
file_storage=storage,
legacy_config=config,
app_config=config,
)
return agent

View File

@@ -2,15 +2,15 @@ from pathlib import Path
import pytest
from forge.config.ai_profile import AIProfile
from forge.config.config import Config
from forge.file_storage import FileStorageBackendName, get_storage
from forge.llm.providers import MultiProvider
from autogpt.agents.agent import Agent, AgentConfiguration, AgentSettings
from autogpt.app.config import AppConfig
@pytest.fixture
def dummy_agent(config: Config, llm_provider: MultiProvider):
def dummy_agent(config: AppConfig, llm_provider: MultiProvider):
ai_profile = AIProfile(
ai_name="Dummy Agent",
ai_role="Dummy Role",
@@ -28,7 +28,7 @@ def dummy_agent(config: Config, llm_provider: MultiProvider):
smart_llm=config.smart_llm,
use_functions_api=config.openai_functions,
),
history=Agent.default_settings.history.copy(deep=True),
history=Agent.default_settings.history.model_copy(deep=True),
)
local = config.file_storage_backend == FileStorageBackendName.LOCAL
@@ -44,7 +44,7 @@ def dummy_agent(config: Config, llm_provider: MultiProvider):
settings=agent_settings,
llm_provider=llm_provider,
file_storage=file_storage,
legacy_config=config,
app_config=config,
)
return agent

View File

@@ -3,8 +3,8 @@ from unittest.mock import patch
import pytest
from forge.config.ai_directives import AIDirectives
from forge.config.ai_profile import AIProfile
from forge.config.config import Config
from autogpt.app.config import AppConfig
from autogpt.app.setup import (
apply_overrides_to_ai_settings,
interactively_revise_ai_settings,
@@ -39,7 +39,7 @@ async def test_apply_overrides_to_ai_settings():
@pytest.mark.asyncio
async def test_interactively_revise_ai_settings(config: Config):
async def test_interactively_revise_ai_settings(config: AppConfig):
ai_profile = AIProfile(ai_name="Test AI", ai_role="Test Role")
directives = AIDirectives(
resources=["Resource1"],

View File

@@ -8,15 +8,15 @@ from typing import Any
from unittest import mock
import pytest
from forge.config.config import GPT_3_MODEL, GPT_4_MODEL, Config, ConfigBuilder
from openai.pagination import AsyncPage
from openai.types import Model
from pydantic import SecretStr
from autogpt.app.config import GPT_3_MODEL, GPT_4_MODEL, AppConfig, ConfigBuilder
from autogpt.app.configurator import apply_overrides_to_config
def test_initial_values(config: Config) -> None:
def test_initial_values(config: AppConfig) -> None:
"""
Test if the initial values of the config class attributes are set correctly.
"""
@@ -29,7 +29,7 @@ def test_initial_values(config: Config) -> None:
@pytest.mark.asyncio
@mock.patch("openai.resources.models.AsyncModels.list")
async def test_fallback_to_gpt3_if_gpt4_not_available(
mock_list_models: Any, config: Config
mock_list_models: Any, config: AppConfig
) -> None:
"""
Test if models update to gpt-3.5-turbo if gpt-4 is not available.
@@ -51,7 +51,7 @@ async def test_fallback_to_gpt3_if_gpt4_not_available(
assert config.smart_llm == GPT_3_MODEL
def test_missing_azure_config(config: Config) -> None:
def test_missing_azure_config(config: AppConfig) -> None:
assert config.openai_credentials is not None
config_file = config.app_data_dir / "azure_config.yaml"
@@ -68,7 +68,7 @@ def test_missing_azure_config(config: Config) -> None:
@pytest.fixture
def config_with_azure(config: Config):
def config_with_azure(config: AppConfig):
config_file = config.app_data_dir / "azure_config.yaml"
config_file.write_text(
f"""
@@ -91,7 +91,7 @@ azure_model_map:
del os.environ["AZURE_CONFIG_FILE"]
def test_azure_config(config_with_azure: Config) -> None:
def test_azure_config(config_with_azure: AppConfig) -> None:
assert (credentials := config_with_azure.openai_credentials) is not None
assert credentials.api_type == SecretStr("azure")
assert credentials.api_version == SecretStr("2023-06-01-preview")

View File

@@ -97,7 +97,9 @@ def start():
help="Write log output to a file instead of the terminal.",
)
# @click.argument(
# "agent_path", type=click.Path(exists=True, file_okay=False), required=False
# "agent_path",
# type=click.Path(exists=True, file_okay=False, path_type=Path),
# required=False,
# )
def run(
maintain: bool,
@@ -276,7 +278,9 @@ def list_challenges(include_unavailable: bool, only_names: bool, output_json: bo
return
if output_json:
click.echo(json.dumps([json.loads(c.info.json()) for c in challenges]))
click.echo(
json.dumps([json.loads(c.info.model_dump_json()) for c in challenges])
)
return
headers = tuple(
@@ -324,7 +328,7 @@ def info(name: str, json: bool):
continue
if json:
click.echo(challenge.info.json())
click.echo(challenge.info.model_dump_json())
break
pretty_print_model(challenge.info)

View File

@@ -16,7 +16,7 @@ from agent_protocol_client import AgentApi, ApiClient, ApiException, Configurati
from agent_protocol_client.models import Task, TaskRequestBody
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Extra, ValidationError
from pydantic import BaseModel, ConfigDict, ValidationError
from agbenchmark.challenges import ChallengeInfo
from agbenchmark.config import AgentBenchmarkConfig
@@ -52,7 +52,9 @@ while challenge_spec_files:
logger.debug(f"Loading {challenge_relpath}...")
try:
challenge_info = ChallengeInfo.parse_file(challenge_spec_file)
challenge_info = ChallengeInfo.model_validate_json(
challenge_spec_file.read_text()
)
except ValidationError as e:
if logging.getLogger().level == logging.DEBUG:
logger.warning(f"Spec file {challenge_relpath} failed to load:\n{e}")
@@ -64,7 +66,7 @@ while challenge_spec_files:
challenge_info.eval_id = str(uuid.uuid4())
# this will sort all the keys of the JSON systematically
# so that the order is always the same
write_pretty_json(challenge_info.dict(), challenge_spec_file)
write_pretty_json(challenge_info.model_dump(), challenge_spec_file)
CHALLENGES[challenge_info.eval_id] = challenge_info
@@ -111,8 +113,7 @@ class CreateReportRequest(BaseModel):
# category: Optional[str] = []
mock: Optional[bool] = False
class Config:
extra = Extra.forbid # this will forbid any extra fields
model_config = ConfigDict(extra="forbid")
updates_list = []
@@ -153,7 +154,7 @@ def setup_fastapi_app(agbenchmark_config: AgentBenchmarkConfig) -> FastAPI:
pids = find_agbenchmark_without_uvicorn()
logger.info(f"pids already running with agbenchmark: {pids}")
logger.debug(f"Request to /reports: {body.dict()}")
logger.debug(f"Request to /reports: {body.model_dump()}")
# Start the benchmark in a separate thread
benchmark_process = Process(
@@ -326,7 +327,9 @@ def setup_fastapi_app(agbenchmark_config: AgentBenchmarkConfig) -> FastAPI:
config={},
)
logger.debug(f"Returning evaluation data:\n{eval_info.json(indent=4)}")
logger.debug(
f"Returning evaluation data:\n{eval_info.model_dump_json(indent=4)}"
)
return eval_info
except ApiException as e:
logger.error(f"Error {e} whilst trying to evaluate task: {task_id}")

View File

@@ -15,7 +15,13 @@ from agent_protocol_client import Configuration as ClientConfig
from agent_protocol_client import Step
from colorama import Fore, Style
from openai import _load_client as get_openai_client
from pydantic import BaseModel, Field, constr, validator
from pydantic import (
BaseModel,
Field,
StringConstraints,
ValidationInfo,
field_validator,
)
from agbenchmark.agent_api_interface import download_agent_artifacts_into_folder
from agbenchmark.agent_interface import copy_challenge_artifacts_into_workspace
@@ -46,7 +52,9 @@ class BuiltinChallengeSpec(BaseModel):
class Info(BaseModel):
difficulty: DifficultyLevel
description: Annotated[str, constr(regex=r"^Tests if the agent can.*")]
description: Annotated[
str, StringConstraints(pattern=r"^Tests if the agent can.*")
]
side_effects: list[str] = Field(default_factory=list)
info: Info
@@ -60,23 +68,26 @@ class BuiltinChallengeSpec(BaseModel):
class Eval(BaseModel):
type: str
scoring: Optional[Literal["percentage", "scale", "binary"]]
template: Optional[Literal["rubric", "reference", "question", "custom"]]
examples: Optional[str]
scoring: Optional[Literal["percentage", "scale", "binary"]] = None
template: Optional[
Literal["rubric", "reference", "question", "custom"]
] = None
examples: Optional[str] = None
@validator("scoring", "template", always=True)
def validate_eval_fields(cls, v, values, field):
if "type" in values and values["type"] == "llm":
if v is None:
@field_validator("scoring", "template")
def validate_eval_fields(cls, value, info: ValidationInfo):
field_name = info.field_name
if "type" in info.data and info.data["type"] == "llm":
if value is None:
raise ValueError(
f"{field.name} must be provided when eval type is 'llm'"
f"{field_name} must be provided when eval type is 'llm'"
)
else:
if v is not None:
if value is not None:
raise ValueError(
f"{field.name} should only exist when eval type is 'llm'"
f"{field_name} should only exist when eval type is 'llm'"
)
return v
return value
eval: Eval
@@ -142,7 +153,7 @@ class BuiltinChallenge(BaseChallenge):
@classmethod
def from_challenge_spec_file(cls, spec_file: Path) -> type["BuiltinChallenge"]:
challenge_spec = BuiltinChallengeSpec.parse_file(spec_file)
challenge_spec = BuiltinChallengeSpec.model_validate_json(spec_file.read_text())
challenge_spec.spec_file = spec_file
return cls.from_challenge_spec(challenge_spec)
@@ -187,7 +198,7 @@ class BuiltinChallenge(BaseChallenge):
task_id = step.task_id
n_steps += 1
steps.append(step.copy())
steps.append(step.model_copy())
if step.additional_output:
agent_task_cost = step.additional_output.get(
"task_total_cost",

View File

@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Optional
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
# Models for the request and response payloads
@@ -10,7 +10,7 @@ class ShipPlacement(BaseModel):
start: dict # {"row": int, "column": str}
direction: str
@validator("start")
@field_validator("start")
def validate_start(cls, start):
row, column = start.get("row"), start.get("column")

View File

@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Optional
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
# Models for the request and response payloads
@@ -10,7 +10,7 @@ class ShipPlacement(BaseModel):
start: dict # {"row": int, "column": str}
direction: str
@validator("start")
@field_validator("start")
def validate_start(cls, start):
row, column = start.get("row"), start.get("column")

View File

@@ -6,7 +6,7 @@ from typing import ClassVar, Iterator, Literal
import pytest
import requests
from agent_protocol_client import AgentApi, Step
from pydantic import BaseModel, ValidationError, validator
from pydantic import BaseModel, ValidationError, ValidationInfo, field_validator
from agbenchmark.config import AgentBenchmarkConfig
from agbenchmark.utils.data_types import Category, EvalResult
@@ -183,7 +183,7 @@ class WebArenaChallengeSpec(BaseModel):
"""The JungleGym site (base URL) at which to start"""
require_login: bool
require_reset: bool
storage_state: str | None
storage_state: str | None = None
intent: str
intent_template: str
@@ -195,36 +195,36 @@ class WebArenaChallengeSpec(BaseModel):
class EvalSet(BaseModel):
class StringMatchEvalSet(BaseModel):
exact_match: str | None
fuzzy_match: list[str] | None
must_include: list[str] | None
exact_match: str | None = None
fuzzy_match: list[str] | None = None
must_include: list[str] | None = None
reference_answers: StringMatchEvalSet | None
reference_answers: StringMatchEvalSet | None = None
"""For string_match eval, a set of criteria to judge the final answer"""
reference_answer_raw_annotation: str | None
string_note: str | None
annotation_note: str | None
reference_answer_raw_annotation: str | None = None
string_note: str | None = None
annotation_note: str | None = None
reference_url: str | None
reference_url: str | None = None
"""For url_match eval, the last URL that should be visited"""
url_note: str | None
url_note: str | None = None
program_html: list[ProgramHtmlEval]
"""For program_html eval, a list of criteria to judge the site state by"""
eval_types: list[EvalType]
@validator("eval_types")
def check_eval_parameters(cls, v: list[EvalType], values):
if "string_match" in v and not values.get("reference_answers"):
@field_validator("eval_types")
def check_eval_parameters(cls, value: list[EvalType], info: ValidationInfo):
if "string_match" in value and not info.data["reference_answers"]:
raise ValueError("'string_match' eval_type requires reference_answers")
if "url_match" in v and not values.get("reference_url"):
if "url_match" in value and not info.data["reference_url"]:
raise ValueError("'url_match' eval_type requires reference_url")
if "program_html" in v and not values.get("program_html"):
if "program_html" in value and not info.data["program_html"]:
raise ValueError(
"'program_html' eval_type requires at least one program_html eval"
)
return v
return value
@property
def evaluators(self) -> list[_Eval]:
@@ -292,7 +292,7 @@ class WebArenaChallenge(BaseChallenge):
results = requests.get(source_url).json()["data"]
if not results:
raise ValueError(f"Could not fetch challenge {source_uri}")
return cls.from_challenge_spec(WebArenaChallengeSpec.parse_obj(results[0]))
return cls.from_challenge_spec(WebArenaChallengeSpec.model_validate(results[0]))
@classmethod
def from_challenge_spec(
@@ -500,7 +500,7 @@ def load_webarena_challenges(
skipped = 0
for entry in challenge_dicts:
try:
challenge_spec = WebArenaChallengeSpec.parse_obj(entry)
challenge_spec = WebArenaChallengeSpec.model_validate(entry)
except ValidationError as e:
failed += 1
logger.warning(f"Error validating WebArena challenge entry: {entry}")

View File

@@ -4,7 +4,8 @@ from datetime import datetime
from pathlib import Path
from typing import Optional
from pydantic import BaseSettings, Field, validator
from pydantic import Field, ValidationInfo, field_validator
from pydantic_settings import BaseSettings
def _calculate_info_test_path(base_path: Path, benchmark_start_time: datetime) -> Path:
@@ -57,7 +58,7 @@ class AgentBenchmarkConfig(BaseSettings, extra="allow"):
subject application exposes an Agent Protocol compliant API.
"""
agbenchmark_config_dir: Path = Field(..., exclude=True)
agbenchmark_config_dir: Path = Field(exclude=True)
"""Path to the agbenchmark_config folder of the subject agent application."""
categories: list[str] | None = None
@@ -101,11 +102,11 @@ class AgentBenchmarkConfig(BaseSettings, extra="allow"):
def config_file(self) -> Path:
return self.agbenchmark_config_dir / "config.json"
@validator("reports_folder", pre=True, always=True)
def set_reports_folder(cls, v, values):
if not v:
return values["agbenchmark_config_dir"] / "reports"
return v
@field_validator("reports_folder", mode="before")
def set_reports_folder(cls, value: Path, info: ValidationInfo):
if not value:
return info.data["agbenchmark_config_dir"] / "reports"
return value
def get_report_dir(self, benchmark_start_time: datetime) -> Path:
return _calculate_info_test_path(self.reports_folder, benchmark_start_time)

View File

@@ -111,17 +111,19 @@ class SessionReportManager(BaseReportManager):
def save(self) -> None:
with self.report_file.open("w") as f:
if self.report:
f.write(self.report.json(indent=4))
f.write(self.report.model_dump_json(indent=4))
else:
json.dump({k: v.dict() for k, v in self.tests.items()}, f, indent=4)
json.dump(
{k: v.model_dump() for k, v in self.tests.items()}, f, indent=4
)
def load(self) -> None:
super().load()
if "tests" in self.tests:
self.report = Report.parse_obj(self.tests)
self.report = Report.model_validate(self.tests)
else:
self.tests = {n: Test.parse_obj(d) for n, d in self.tests.items()}
self.tests = {n: Test.model_validate(d) for n, d in self.tests.items()}
def add_test_report(self, test_name: str, test_report: Test) -> None:
if self.report:
@@ -155,7 +157,7 @@ class SessionReportManager(BaseReportManager):
total_cost=self.get_total_costs(),
),
tests=copy.copy(self.tests),
config=config.dict(exclude={"reports_folder"}, exclude_none=True),
config=config.model_dump(exclude={"reports_folder"}, exclude_none=True),
)
agent_categories = get_highest_achieved_difficulty_per_category(self.report)

View File

@@ -27,7 +27,7 @@ def get_reports_data(report_path: str) -> dict[str, Any]:
with open(Path(subdir) / file, "r") as f:
# Load the JSON data from the file
json_data = json.load(f)
converted_data = Report.parse_obj(json_data)
converted_data = Report.model_validate(json_data)
# get the last directory name in the path as key
reports_data[subdir_name] = converted_data

View File

@@ -6,7 +6,13 @@ import logging
from typing import Annotated, Any, Dict, List
from agent_protocol_client import Step
from pydantic import BaseModel, Field, constr, validator
from pydantic import (
BaseModel,
Field,
StringConstraints,
ValidationInfo,
field_validator,
)
datetime_format = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$"
logger = logging.getLogger(__name__)
@@ -30,20 +36,20 @@ class TestResult(BaseModel):
cost: float | None = None
"""The (known) cost incurred by the run, e.g. from using paid LLM APIs"""
@validator("fail_reason")
def success_xor_fail_reason(cls, v: str | None, values: dict[str, Any]):
if bool(v) == bool(values["success"]):
@field_validator("fail_reason")
def success_xor_fail_reason(cls, value, info: ValidationInfo):
if bool(value) == bool(info.data["success"]):
logger.error(
"Error validating `success ^ fail_reason` on TestResult: "
f"success = {repr(values['success'])}; "
f"fail_reason = {repr(v)}"
f"success = {repr(info.data['success'])}; "
f"fail_reason = {repr(value)}"
)
if v:
success = values["success"]
if value:
success = info.data["success"]
assert not success, "fail_reason must only be specified if success=False"
else:
assert values["success"], "fail_reason is required if success=False"
return v
assert info.data["success"], "fail_reason is required if success=False"
return value
class TestMetrics(BaseModel):
@@ -88,7 +94,7 @@ class Test(BaseModel):
class ReportBase(BaseModel):
command: str
completion_time: str | None = None
benchmark_start_time: Annotated[str, constr(regex=datetime_format)]
benchmark_start_time: Annotated[str, StringConstraints(pattern=datetime_format)]
metrics: MetricsOverall
config: Dict[str, str | dict[str, str]]
agent_git_commit_sha: str | None = None

View File

@@ -1,7 +1,7 @@
"""Model definitions for use in the API"""
from typing import Annotated
from pydantic import BaseModel, constr
from pydantic import BaseModel, StringConstraints
datetime_format = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$"
@@ -37,7 +37,7 @@ class RunDetails(BaseModel):
run_id: str | None = None
command: str
completion_time: str | None = None
benchmark_start_time: Annotated[str, constr(regex=datetime_format)]
benchmark_start_time: Annotated[str, StringConstraints(pattern=datetime_format)]
class BenchmarkRun(BaseModel):

View File

@@ -45,7 +45,7 @@ def update_regression_tests(
# if the last 3 tests were successful, add to the regression tests
test_report.metrics.is_regression = True
SingletonReportManager().REGRESSION_MANAGER.add_test(
test_name, test_report.dict(include={"difficulty", "data_path"})
test_name, test_report.model_dump(include={"difficulty", "data_path"})
)

View File

@@ -7,10 +7,9 @@ from pydantic import BaseModel, Field
class TaskRequestBody(BaseModel):
input: str = Field(
...,
min_length=1,
description="Input prompt for the task.",
example="Write the words you receive to the file 'output.txt'.",
examples=["Write the words you receive to the file 'output.txt'."],
)
additional_input: Optional[dict[str, Any]] = Field(default_factory=dict)

View File

@@ -220,7 +220,7 @@ class DependencyManager(object):
labels = {}
for item in self.items:
assert item.cls and issubclass(item.cls, BaseChallenge)
data = item.cls.info.dict()
data = item.cls.info.model_dump()
node_name = get_name(item)
data["name"] = node_name

View File

@@ -135,7 +135,7 @@ def pretty_print_model(model: BaseModel, include_header: bool = True) -> None:
if include_header:
# Try to find the ID and/or name attribute of the model
id, name = None, None
for attr, value in model.dict().items():
for attr, value in model.model_dump().items():
if attr == "id" or attr.endswith("_id"):
id = value
if attr.endswith("name"):
@@ -148,8 +148,8 @@ def pretty_print_model(model: BaseModel, include_header: bool = True) -> None:
)
indent = " " * 2
k_col_width = max(len(k) for k in model.dict().keys())
for k, v in model.dict().items():
k_col_width = max(len(k) for k in model.model_dump().keys())
for k, v in model.model_dump().items():
v_fmt = repr(v)
if v is None or v == "":
v_fmt = click.style(v_fmt, fg="black")

400
benchmark/poetry.lock generated
View File

@@ -5,17 +5,22 @@ name = "agent-protocol-client"
version = "1.1.0"
description = "Agent Communication Protocol Client"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "agent_protocol_client-1.1.0-py3-none-any.whl", hash = "sha256:0e8c6c97244189666ed18e320410abddce8c9dfb75437da1e590bbef3b6268be"},
{file = "agent_protocol_client-1.1.0.tar.gz", hash = "sha256:aa7e1042de1249477fdc29c2df08a44f2233dade9c02c1279e37c98e9d3a0d72"},
]
python-versions = "^3.7"
files = []
develop = false
[package.dependencies]
aiohttp = ">=3.8.4,<4.0.0"
pydantic = ">=1.10.5,<2.0.0"
python-dateutil = ">=2.8.2,<3.0.0"
urllib3 = ">=1.25.3,<2.0.0"
aiohttp = "^3.8.4"
pydantic = ">=1.10.5, <3.0.0"
python-dateutil = "^2.8.2"
urllib3 = "^1.25.3"
[package.source]
type = "git"
url = "https://github.com/Significant-Gravitas/agent-protocol.git"
reference = "HEAD"
resolved_reference = "beb098517b0b9e255024d1b57df236f0329f4b1c"
subdirectory = "packages/client/python"
[[package]]
name = "aiohttp"
@@ -127,6 +132,17 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.2.0"
@@ -1440,85 +1456,101 @@ files = [
[[package]]
name = "multidict"
version = "6.0.4"
version = "6.0.5"
description = "multidict implementation"
optional = false
python-versions = ">=3.7"
files = [
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
{file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"},
{file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"},
{file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"},
{file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"},
{file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"},
{file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"},
{file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"},
{file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"},
{file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"},
{file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"},
{file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"},
{file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"},
{file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"},
{file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"},
{file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"},
{file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"},
{file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"},
{file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"},
{file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"},
{file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"},
{file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
{file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
[[package]]
@@ -2048,55 +2080,132 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.13"
description = "Data validation and settings management using python type hints"
version = "2.7.4"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"},
{file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"},
{file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"},
{file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"},
{file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"},
{file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"},
{file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"},
{file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"},
{file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"},
{file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"},
{file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"},
{file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"},
{file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"},
{file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"},
{file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"},
{file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"},
{file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"},
{file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"},
{file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"},
{file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"},
{file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"},
{file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"},
{file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"},
{file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"},
{file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"},
{file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"},
{file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"},
{file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"},
{file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"},
{file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"},
{file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"},
{file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"},
{file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"},
{file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"},
{file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"},
{file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"},
{file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
{file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
]
[package.dependencies]
typing-extensions = ">=4.2.0"
annotated-types = ">=0.4.0"
pydantic-core = "2.18.4"
typing-extensions = ">=4.6.1"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.18.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"},
{file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"},
{file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"},
{file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"},
{file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"},
{file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"},
{file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"},
{file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"},
{file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"},
{file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"},
{file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"},
{file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"},
{file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"},
{file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"},
{file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"},
{file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"},
{file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"},
{file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"},
{file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"},
{file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"},
{file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"},
{file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"},
{file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"},
{file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"},
{file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"},
{file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"},
{file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"},
{file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"},
{file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"},
{file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"},
{file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"},
{file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"},
{file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.3.4"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"},
{file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"},
]
[package.dependencies]
pydantic = ">=2.7.0"
python-dotenv = ">=0.21.0"
[package.extras]
toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pyflakes"
@@ -2192,21 +2301,21 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
[[package]]
name = "pytest-asyncio"
version = "0.21.1"
version = "0.23.7"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
{file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
{file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
]
[package.dependencies]
pytest = ">=7.0.0"
pytest = ">=7.0.0,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-cov"
@@ -2628,31 +2737,6 @@ exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
trio = ">=0.11"
wsproto = ">=0.14"
[[package]]
name = "types-requests"
version = "2.31.0.6"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.7"
files = [
{file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"},
{file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"},
]
[package.dependencies]
types-urllib3 = "*"
[[package]]
name = "types-urllib3"
version = "1.26.25.14"
description = "Typing stubs for urllib3"
optional = false
python-versions = "*"
files = [
{file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"},
{file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"},
]
[[package]]
name = "typing-extensions"
version = "4.9.0"
@@ -2864,4 +2948,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "4a980e6d8f54a2f7f6a3c55d4f40ac3a4b27b5ac6573dd2a39e11213a4b126dd"
content-hash = "26bd75befe5223095b65be293086edf52f34f9043e49107c80a105dc0387dd6a"

View File

@@ -9,43 +9,47 @@ packages = [{ include = "agbenchmark" }]
[tool.poetry.dependencies]
python = "^3.10"
pytest = "^7.3.2"
requests = "^2.31.0"
openai = "^1.7.2"
pydantic = "^1.10.9"
python-dotenv = "^1.0.0"
agent-protocol-client = {git = "https://github.com/Significant-Gravitas/agent-protocol.git", subdirectory = "packages/client/python"}
click = "^8.1.3"
types-requests = "^2.31.0.1"
click-default-group = "^1.2.4"
colorama = "^0.4.6"
fastapi = "^0.109.1"
gitpython = "^3.1.32"
httpx = "^0.24.0"
matplotlib = "^3.7.2"
# Multidict 6.0.4 fails to install and is a dependency of aiohttp which is a depenedency of agent-protocol-client
multidict = "^6.0.5"
networkx = "^3.1"
openai = "^1.7.2"
pandas = "^2.0.3"
pexpect = "^4.8.0"
psutil = "^5.9.5"
matplotlib = "^3.7.2"
pandas = "^2.0.3"
gitpython = "^3.1.32"
networkx = "^3.1"
colorama = "^0.4.6"
pyvis = "^0.3.2"
selenium = "^4.11.2"
pytest-asyncio = "^0.21.1"
uvicorn = "^0.23.2"
fastapi = "^0.109.1"
pydantic = "^2.7.2"
pydantic-settings = "^2.3.4"
pytest = "^7.3.2"
pytest-asyncio = "^0.23.3"
python-dotenv = "^1.0.0"
python-multipart = "^0.0.7"
toml = "^0.10.2"
# helicone = "^1.0.9" # incompatible with openai@^1.0.0
httpx = "^0.24.0"
agent-protocol-client = "^1.1.0"
click-default-group = "^1.2.4"
pyvis = "^0.3.2"
requests = "^2.31.0"
selenium = "^4.11.2"
tabulate = "^0.9.0"
toml = "^0.10.2"
uvicorn = ">=0.23.2,<1"
[tool.poetry.group.dev.dependencies]
black = "^23.12.1"
flake8 = "^7.0.0"
isort = "^5.13.1"
pyright = "^1.1.364"
pandas = "^2.0.3"
pre-commit = "^3.3.3"
# Testing
pytest-cov = "^5.0.0"
# Dependencies for stuff in reports/
gspread = "^5.10.0"
oauth2client = "^4.1.3"
pre-commit = "^3.3.3"
pytest-cov = "^5.0.0"
[tool.poetry.scripts]
agbenchmark = "agbenchmark.__main__:cli"

View File

@@ -1,20 +1,24 @@
#!/usr/bin/env python3
from pathlib import Path
import click
from agbenchmark.reports.processing.report_types import Report
@click.command()
@click.argument("report_json_file", type=click.Path(exists=True, dir_okay=False))
def print_markdown_report(report_json_file: str):
@click.argument(
"report_json_file", type=click.Path(exists=True, dir_okay=False, path_type=Path)
)
def print_markdown_report(report_json_file: Path):
"""
Generates a Markdown report from a given report.json file.
:param report_json_file: Path to the report.json file.
:return: A string containing the Markdown formatted report.
"""
report = Report.parse_file(report_json_file)
report = Report.model_validate_json(report_json_file.read_text())
# Header and metadata
click.echo("# Benchmark Report")

View File

@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field
class Metrics(BaseModel):
difficulty: str
success: bool
success_percent: float = Field(..., alias="success_%")
success_percent: float = Field(alias="success_%")
run_time: Optional[str] = None
fail_reason: Optional[str] = None
attempted: Optional[bool] = None
@@ -100,7 +100,7 @@ def get_reports():
# Load the JSON data from the file
json_data = json.load(f)
print(f"Processing {report_file}")
report = Report.parse_obj(json_data)
report = Report.model_validate(json_data)
for test_name, test_data in report.tests.items():
test_json = {

9
cli.py
View File

@@ -149,10 +149,11 @@ def start(agent_name: str, no_setup: bool):
setup_process.wait()
click.echo()
subprocess.Popen(["./run_benchmark", "serve"], cwd=agent_dir)
click.echo("⌛ (Re)starting benchmark server...")
wait_until_conn_ready(8080)
click.echo()
# FIXME: Doesn't work: Command not found: agbenchmark
# subprocess.Popen(["./run_benchmark", "serve"], cwd=agent_dir)
# click.echo("⌛ (Re)starting benchmark server...")
# wait_until_conn_ready(8080)
# click.echo()
subprocess.Popen(["./run"], cwd=agent_dir)
click.echo(f"⌛ (Re)starting agent '{agent_name}'...")

View File

@@ -1,63 +0,0 @@
# 🖼 Image Generation configuration
| Config variable | Values | |
| ---------------- | ------------------------------- | -------------------- |
| `IMAGE_PROVIDER` | `dalle` `huggingface` `sdwebui` | **default: `dalle`** |
## DALL-e
In `.env`, make sure `IMAGE_PROVIDER` is commented (or set to `dalle`):
```ini
# IMAGE_PROVIDER=dalle # this is the default
```
Further optional configuration:
| Config variable | Values | |
| ---------------- | ------------------ | -------------- |
| `IMAGE_SIZE` | `256` `512` `1024` | default: `256` |
## Hugging Face
To use text-to-image models from Hugging Face, you need a Hugging Face API token.
Link to the appropriate settings page: [Hugging Face > Settings > Tokens](https://huggingface.co/settings/tokens)
Once you have an API token, uncomment and adjust these variables in your `.env`:
```ini
IMAGE_PROVIDER=huggingface
HUGGINGFACE_API_TOKEN=your-huggingface-api-token
```
Further optional configuration:
| Config variable | Values | |
| ------------------------- | ---------------------- | ---------------------------------------- |
| `HUGGINGFACE_IMAGE_MODEL` | see [available models] | default: `CompVis/stable-diffusion-v1-4` |
[available models]: https://huggingface.co/models?pipeline_tag=text-to-image
## Stable Diffusion WebUI
It is possible to use your own self-hosted Stable Diffusion WebUI with AutoGPT:
```ini
IMAGE_PROVIDER=sdwebui
```
!!! note
Make sure you are running WebUI with `--api` enabled.
Further optional configuration:
| Config variable | Values | |
| --------------- | ----------------------- | -------------------------------- |
| `SD_WEBUI_URL` | URL to your WebUI | default: `http://127.0.0.1:7860` |
| `SD_WEBUI_AUTH` | `{username}:{password}` | *Note: do not copy the braces!* |
## Selenium
```shell
sudo Xvfb :10 -ac -screen 0 1024x768x24 & DISPLAY=:10 <YOUR_CLIENT>
```

View File

@@ -1,21 +1,18 @@
# Configuration
Configuration is controlled through the `Config` object. You can set configuration variables via the `.env` file. If you don't have a `.env` file, create a copy of `.env.template` in your `AutoGPT` folder and name it `.env`.
Configuration of sensitive settings such as API credentials is done through environment variables.
You can set configuration variables via the `.env` file. If you don't have a `.env` file, create a copy of `.env.template` in your `AutoGPT` folder and name it `.env`.
## Environment Variables
- `AUDIO_TO_TEXT_PROVIDER`: Audio To Text Provider. Only option currently is `huggingface`. Default: huggingface
- `AUTHORISE_COMMAND_KEY`: Key response accepted when authorising commands. Default: y
- `ANTHROPIC_API_KEY`: Set this if you want to use Anthropic models with AutoGPT
- `AZURE_CONFIG_FILE`: Location of the Azure Config file relative to the AutoGPT root directory. Default: azure.yaml
- `BROWSE_CHUNK_MAX_LENGTH`: When browsing website, define the length of chunks to summarize. Default: 3000
- `BROWSE_SPACY_LANGUAGE_MODEL`: [spaCy language model](https://spacy.io/usage/models) to use when creating chunks. Default: en_core_web_sm
- `CHAT_MESSAGES_ENABLED`: Enable chat messages. Optional
- `DISABLED_COMMANDS`: Commands to disable. Use comma separated names of commands. See the list of commands from built-in components [here](../../forge/components/built-in-components.md). Default: None
- `COMPONENT_CONFIG_FILE`: Path to the component configuration file (json) for an agent. Optional
- `DISABLED_COMMANDS`: Commands to disable. Use comma separated names of commands. See the list of commands from built-in components [here](../components/components.md). Default: None
- `ELEVENLABS_API_KEY`: ElevenLabs API Key. Optional.
- `ELEVENLABS_VOICE_ID`: ElevenLabs Voice ID. Optional.
- `EMBEDDING_MODEL`: LLM Model to use for embedding tasks. Default: `text-embedding-3-small`
- `EXECUTE_LOCAL_COMMANDS`: If shell commands should be executed locally. Default: False
- `EXIT_KEY`: Exit key accepted to exit. Default: n
- `FAST_LLM`: LLM Model to use for most tasks. Default: `gpt-3.5-turbo-0125`
- `GITHUB_API_KEY`: [Github API Key](https://github.com/settings/tokens). Optional.
@@ -23,26 +20,15 @@ Configuration is controlled through the `Config` object. You can set configurati
- `GOOGLE_API_KEY`: Google API key. Optional.
- `GOOGLE_CUSTOM_SEARCH_ENGINE_ID`: [Google custom search engine ID](https://programmablesearchengine.google.com/controlpanel/all). Optional.
- `GROQ_API_KEY`: Set this if you want to use Groq models with AutoGPT
- `HEADLESS_BROWSER`: Use a headless browser while AutoGPT uses a web browser. Setting to `False` will allow you to see AutoGPT operate the browser. Default: True
- `HUGGINGFACE_API_TOKEN`: HuggingFace API, to be used for both image generation and audio to text. Optional.
- `HUGGINGFACE_AUDIO_TO_TEXT_MODEL`: HuggingFace audio to text model. Default: CompVis/stable-diffusion-v1-4
- `HUGGINGFACE_IMAGE_MODEL`: HuggingFace model to use for image generation. Default: CompVis/stable-diffusion-v1-4
- `IMAGE_PROVIDER`: Image provider. Options are `dalle`, `huggingface`, and `sdwebui`. Default: dalle
- `IMAGE_SIZE`: Default size of image to generate. Default: 256
- `OPENAI_API_KEY`: *REQUIRED*- Your [OpenAI API Key](https://platform.openai.com/account/api-keys).
- `OPENAI_API_KEY`: Set this if you want to use OpenAI models; [OpenAI API Key](https://platform.openai.com/account/api-keys).
- `OPENAI_ORGANIZATION`: Organization ID in OpenAI. Optional.
- `PLAIN_OUTPUT`: Plain output, which disables the spinner. Default: False
- `RESTRICT_TO_WORKSPACE`: The restrict file reading and writing to the workspace directory. Default: True
- `SD_WEBUI_AUTH`: Stable Diffusion Web UI username:password pair. Optional.
- `SD_WEBUI_URL`: Stable Diffusion Web UI URL. Default: http://localhost:7860
- `SHELL_ALLOWLIST`: List of shell commands that ARE allowed to be executed by AutoGPT. Only applies if `SHELL_COMMAND_CONTROL` is set to `allowlist`. Default: None
- `SHELL_COMMAND_CONTROL`: Whether to use `allowlist` or `denylist` to determine what shell commands can be executed (Default: denylist)
- `SHELL_DENYLIST`: List of shell commands that ARE NOT allowed to be executed by AutoGPT. Only applies if `SHELL_COMMAND_CONTROL` is set to `denylist`. Default: sudo,su
- `SMART_LLM`: LLM Model to use for "smart" tasks. Default: `gpt-4-turbo-preview`
- `STREAMELEMENTS_VOICE`: StreamElements voice to use. Default: Brian
- `TEMPERATURE`: Value of temperature given to OpenAI. Value from 0 to 2. Lower is more deterministic, higher is more random. See https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature
- `TEXT_TO_SPEECH_PROVIDER`: Text to Speech Provider. Options are `gtts`, `macos`, `elevenlabs`, and `streamelements`. Default: gtts
- `USER_AGENT`: User-Agent given when browsing websites. Default: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"
- `USE_AZURE`: Use Azure's LLM Default: False
- `USE_WEB_BROWSER`: Which web browser to use. Options are `chrome`, `firefox`, `safari` or `edge` Default: chrome
- `WIPE_REDIS_ON_START`: Wipes data / index on start. Default: True

View File

@@ -2,36 +2,36 @@
!!! note
This section is optional. Use the official Google API if search attempts return
error 429. To use the `google_official_search` command, you need to set up your
Google API key in your environment variables.
error 429. To use the `google` command, you need to set up your
Google API key in your environment variables or pass it with configuration to the [`WebSearchComponent`](../../forge/components/built-in-components.md).
Create your project:
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
2. If you don't already have an account, create one and log in
3. Create a new project by clicking on the *Select a Project* dropdown at the top of the
1. If you don't already have an account, create one and log in
1. Create a new project by clicking on the *Select a Project* dropdown at the top of the
page and clicking *New Project*
4. Give it a name and click *Create*
5. Set up a custom search API and add to your .env file:
5. Go to the [APIs & Services Dashboard](https://console.cloud.google.com/apis/dashboard)
6. Click *Enable APIs and Services*
7. Search for *Custom Search API* and click on it
8. Click *Enable*
9. Go to the [Credentials](https://console.cloud.google.com/apis/credentials) page
10. Click *Create Credentials*
11. Choose *API Key*
12. Copy the API key
13. Set it as the `GOOGLE_API_KEY` in your `.env` file
14. [Enable](https://console.developers.google.com/apis/api/customsearch.googleapis.com)
1. Give it a name and click *Create*
1. Set up a custom search API and add to your .env file:
1. Go to the [APIs & Services Dashboard](https://console.cloud.google.com/apis/dashboard)
1. Click *Enable APIs and Services*
1. Search for *Custom Search API* and click on it
1. Click *Enable*
1. Go to the [Credentials](https://console.cloud.google.com/apis/credentials) page
1. Click *Create Credentials*
1. Choose *API Key*
1. Copy the API key
1. Set it as the `GOOGLE_API_KEY` in your `.env` file
1. [Enable](https://console.developers.google.com/apis/api/customsearch.googleapis.com)
the Custom Search API on your project. (Might need to wait few minutes to propagate.)
Set up a custom search engine and add to your .env file:
15. Go to the [Custom Search Engine](https://cse.google.com/cse/all) page
16. Click *Add*
17. Set up your search engine by following the prompts.
1. Go to the [Custom Search Engine](https://cse.google.com/cse/all) page
1. Click *Add*
1. Set up your search engine by following the prompts.
You can choose to search the entire web or specific sites
18. Once you've created your search engine, click on *Control Panel*
19. Click *Basics*
20. Copy the *Search engine ID*
21. Set it as the `CUSTOM_SEARCH_ENGINE_ID` in your `.env` file
1. Once you've created your search engine, click on *Control Panel*
1. Click *Basics*
1. Copy the *Search engine ID*
1. Set it as the `CUSTOM_SEARCH_ENGINE_ID` in your `.env` file
_Remember that your free daily custom search quota allows only up to 100 searches. To increase this limit, you need to assign a billing account to the project to profit from up to 10K daily searches._

View File

@@ -71,18 +71,25 @@
- ./logs:/app/logs
## uncomment following lines if you want to make use of these files
## you must have them existing in the same folder as this docker-compose.yml
## component configuration file
#- type: bind
# source: ./config.json
# target: /app/config.json
```
</details>
4. Download [`.env.template`][.env.template] and save it as `.env` in the AutoGPT folder.
5. Follow the standard [configuration instructions](../index.md#completing-the-setup),
1. Download [`.env.template`][.env.template] and save it as `.env` in the AutoGPT folder.
2. Follow the standard [configuration instructions](../index.md#completing-the-setup),
from step 3 onwards and excluding `poetry install` steps.
6. Pull the latest image from [Docker Hub]
3. Pull the latest image from [Docker Hub]
```shell
docker pull significantgravitas/auto-gpt
```
4. _Optional: mount configuration file._
If you have component configuration file, for example `config.json`, place it in `autogpt/data/` directory. Or place it in `autogpt/` and uncomment the line in `docker-compose.yml` that mounts it.
To learn more about configuring, see [Component configuration](../../forge/components/components.md#json-configuration)
!!! note "Docker only supports headless browsing"
AutoGPT uses a browser in headless mode by default: `HEADLESS_BROWSER=True`.

View File

@@ -50,7 +50,13 @@ Since we don't ship AutoGPT as a desktop application, you'll need to download th
### Completing the Setup
Once you have cloned or downloaded the project, you can find the AutoGPT Agent in the
`autogpt/` folder. In this folder:
`autogpt/` folder.
Inside this folder you can configure the AutoGPT application with an `.env` file and (optionally) a JSON configuration file:
- `.env` for environment variables, which are mostly used for sensitive data like API keys
- a JSON configuration file to customize certain features of AutoGPT's [Components](../../forge/components/introduction.md)
See the [Configuration](../configuration/options.md) reference for a list of available environment variables.
1. Find the file named `.env.template`. This file may
be hidden by default in some operating systems due to the dot prefix. To reveal
@@ -71,6 +77,9 @@ Once you have cloned or downloaded the project, you can find the AutoGPT Agent i
6. Save and close the `.env` file.
7. _Optional: run `poetry install` to install all required dependencies._ The
application also checks for and installs any required dependencies when it starts.
8. _Optional: configure the JSON file (e.g. `config.json`) with your desired settings._
The application will use default settings if you don't provide a JSON configuration file.
Learn how to [set up the JSON configuration file](../../forge/components/components.md#json-configuration)
You should now be able to explore the CLI (`./autogpt.sh --help`) and run the application.
@@ -79,7 +88,6 @@ See the [user guide](../usage.md) for further instructions.
[show hidden files/Windows]: https://support.microsoft.com/en-us/windows/view-hidden-files-and-folders-in-windows-97fbc472-c603-9d90-91d0-1166d1d9f4b5
[show hidden files/macOS]: https://www.pcmag.com/how-to/how-to-access-your-macs-hidden-files
## Setting up LLM providers
You can use AutoGPT with any of the following LLM providers.

View File

@@ -60,10 +60,6 @@ Options:
--debug Enable Debug Mode
--gpt3only Enable GPT3.5 Only Mode
--gpt4only Enable GPT4 Only Mode
-b, --browser-name TEXT Specifies which web-browser to use when
using selenium to scrape the web.
--allow-downloads Dangerous: Allows AutoGPT to download files
natively.
--skip-news Specifies whether to suppress the output of
latest news on startup.
--install-plugin-deps Installs external dependencies for 3rd party
@@ -82,6 +78,7 @@ Options:
--override-directives If specified, --constraint, --resource and
--best-practice will override the AI's
directives instead of being appended to them
--component-config-file TEXT Path to the json configuration file.
--help Show this message and exit.
```
</details>
@@ -128,10 +125,6 @@ Options:
--debug Enable Debug Mode
--gpt3only Enable GPT3.5 Only Mode
--gpt4only Enable GPT4 Only Mode
-b, --browser-name TEXT Specifies which web-browser to use when using
selenium to scrape the web.
--allow-downloads Dangerous: Allows AutoGPT to download files
natively.
--install-plugin-deps Installs external dependencies for 3rd party
plugins.
--help Show this message and exit.

View File

@@ -1,26 +1,34 @@
# Built-in Components
This page lists all [🧩 Components](./components.md) and [⚙️ Protocols](./protocols.md) they implement that are natively provided. They are used by the AutoGPT agent.
Some components have additional configuration options listed in the table, see [Component configuration](./components.md/#component-configuration) to learn more.
!!! note
If a configuration field uses environment variable, it still can be passed using configuration model. ### Value from the configuration takes precedence over env var! Env var will be only applied if value in the configuration is not set.
## `SystemComponent`
Essential component to allow an agent to finish.
**DirectiveProvider**
### DirectiveProvider
- Constraints about API budget
**MessageProvider**
### MessageProvider
- Current time and date
- Remaining API budget and warnings if budget is low
**CommandProvider**
### CommandProvider
- `finish` used when task is completed
## `UserInteractionComponent`
Adds ability to interact with user in CLI.
**CommandProvider**
### CommandProvider
- `ask_user` used to ask user for input
## `FileManagerComponent`
@@ -28,10 +36,21 @@ Adds ability to interact with user in CLI.
Adds ability to read and write persistent files to local storage, Google Cloud Storage or Amazon's S3.
Necessary for saving and loading agent's state (preserving session).
**DirectiveProvider**
### `FileManagerConfiguration`
| Config variable | Details | Type | Default |
| ---------------- | -------------------------------------- | ----- | ---------------------------------- |
| `storage_path` | Path to agent files, e.g. state | `str` | `agents/{agent_id}/`[^1] |
| `workspace_path` | Path to files that agent has access to | `str` | `agents/{agent_id}/workspace/`[^1] |
[^1] This option is set dynamically during component construction as opposed to by default inside the configuration model, `{agent_id}` is replaced with the agent's unique identifier.
### DirectiveProvider
- Resource information that it's possible to read and write files
**CommandProvider**
### CommandProvider
- `read_file` used to read file
- `write_file` used to write file
- `list_folder` lists all files in a folder
@@ -40,7 +59,18 @@ Necessary for saving and loading agent's state (preserving session).
Lets the agent execute non-interactive Shell commands and Python code. Python execution works only if Docker is available.
**CommandProvider**
### `CodeExecutorConfiguration`
| Config variable | Details | Type | Default |
| ------------------------ | ---------------------------------------------------- | --------------------------- | ----------------- |
| `execute_local_commands` | Enable shell command execution | `bool` | `False` |
| `shell_command_control` | Controls which list is used | `"allowlist" \| "denylist"` | `"allowlist"` |
| `shell_allowlist` | List of allowed shell commands | `List[str]` | `[]` |
| `shell_denylist` | List of prohibited shell commands | `List[str]` | `[]` |
| `docker_container_name` | Name of the Docker container used for code execution | `str` | `"agent_sandbox"` |
### CommandProvider
- `execute_shell` execute shell command
- `execute_shell_popen` execute shell command with popen
- `execute_python_code` execute Python code
@@ -50,38 +80,93 @@ Lets the agent execute non-interactive Shell commands and Python code. Python ex
Keeps track of agent's actions and their outcomes. Provides their summary to the prompt.
**MessageProvider**
### `ActionHistoryConfiguration`
| Config variable | Details | Type | Default |
| ---------------------- | ------------------------------------------------------- | ----------- | ------------------ |
| `model_name` | Name of the llm model used to compress the history | `ModelName` | `"gpt-3.5-turbo"` |
| `max_tokens` | Maximum number of tokens to use for the history summary | `int` | `1024` |
| `spacy_language_model` | Language model used for summary chunking using spacy | `str` | `"en_core_web_sm"` |
| `full_message_count` | Number of cycles to include unsummarized in the prompt | `int` | `4` |
### MessageProvider
- Agent's progress summary
**AfterParse**
### AfterParse
- Register agent's action
**ExecutionFailuer**
### ExecutionFailure
- Rewinds the agent's action, so it isn't saved
**AfterExecute**
### AfterExecute
- Saves the agent's action result in the history
## `GitOperationsComponent`
**CommandProvider**
Adds ability to iteract with git repositories and GitHub.
### `GitOperationsConfiguration`
| Config variable | Details | Type | Default |
| ----------------- | ----------------------------------------- | ----- | ------- |
| `github_username` | GitHub username, *ENV:* `GITHUB_USERNAME` | `str` | `None` |
| `github_api_key` | GitHub API key, *ENV:* `GITHUB_API_KEY` | `str` | `None` |
### CommandProvider
- `clone_repository` used to clone a git repository
## `ImageGeneratorComponent`
Adds ability to generate images using various providers, see [Image Generation configuration](./../configuration/imagegen.md) to learn more.
Adds ability to generate images using various providers.
### Hugging Face
To use text-to-image models from Hugging Face, you need a Hugging Face API token.
Link to the appropriate settings page: [Hugging Face > Settings > Tokens](https://huggingface.co/settings/tokens)
### Stable Diffusion WebUI
It is possible to use your own self-hosted Stable Diffusion WebUI with AutoGPT. ### Make sure you are running WebUI with `--api` enabled.
### `ImageGeneratorConfiguration`
| Config variable | Details | Type | Default |
| ------------------------- | ------------------------------------------------------------- | --------------------------------------- | --------------------------------- |
| `image_provider` | Image generation provider | `"dalle" \| "huggingface" \| "sdwebui"` | `"dalle"` |
| `huggingface_image_model` | Hugging Face image model, see [available models] | `str` | `"CompVis/stable-diffusion-v1-4"` |
| `huggingface_api_token` | Hugging Face API token, *ENV:* `HUGGINGFACE_API_TOKEN` | `str` | `None` |
| `sd_webui_url` | URL to self-hosted Stable Diffusion WebUI | `str` | `"http://localhost:7860"` |
| `sd_webui_auth` | Basic auth for Stable Diffusion WebUI, *ENV:* `SD_WEBUI_AUTH` | `str` of format `{username}:{password}` | `None` |
[available models]: https://huggingface.co/models?pipeline_tag=text-to-image
### CommandProvider
**CommandProvider**
- `generate_image` used to generate an image given a prompt
## `WebSearchComponent`
Allows agent to search the web.
Allows agent to search the web. Google credentials aren't required for DuckDuckGo. [Instructions how to set up Google API key](../../AutoGPT/configuration/search.md)
### `WebSearchConfiguration`
| Config variable | Details | Type | Default |
| -------------------------------- | ----------------------------------------------------------------------- | ----- | ------- |
| `google_api_key` | Google API key, *ENV:* `GOOGLE_API_KEY` | `str` | `None` |
| `google_custom_search_engine_id` | Google Custom Search Engine ID, *ENV:* `GOOGLE_CUSTOM_SEARCH_ENGINE_ID` | `str` | `None` |
| `duckduckgo_max_attempts` | Maximum number of attempts to search using DuckDuckGo | `int` | `3` |
### DirectiveProvider
**DirectiveProvider**
- Resource information that it's possible to search the web
**CommandProvider**
### CommandProvider
- `search_web` used to search the web using DuckDuckGo
- `google` used to search the web using Google, requires API key
@@ -89,20 +174,34 @@ Allows agent to search the web.
Allows agent to read websites using Selenium.
**DirectiveProvider**
### `WebSeleniumConfiguration`
| Config variable | Details | Type | Default |
| ----------------------------- | ------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `model_name` | Name of the llm model used to read websites | `ModelName` | `"gpt-3.5-turbo"` |
| `web_browser` | Web browser used by Selenium | `"chrome" \| "firefox" \| "safari" \| "edge"` | `"chrome"` |
| `headless` | Run browser in headless mode | `bool` | `True` |
| `user_agent` | User agent used by the browser | `str` | `"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"` |
| `browse_spacy_language_model` | Spacy language model used for chunking text | `str` | `"en_core_web_sm"` |
### DirectiveProvider
- Resource information that it's possible to read websites
**CommandProvider**
### CommandProvider
- `read_website` used to read a specific url and look for specific topics or answer a question
## `ContextComponent`
Adds ability to keep up-to-date file and folder content in the prompt.
**MessageProvider**
### MessageProvider
- Content of elements in the context
**CommandProvider**
### CommandProvider
- `open_file` used to open a file into context
- `open_folder` used to open a folder into context
- `close_context_item` remove an item from the context
@@ -111,5 +210,6 @@ Adds ability to keep up-to-date file and folder content in the prompt.
Watches if agent is looping and switches to smart mode if necessary.
**AfterParse**
### AfterParse
- Investigates what happened and switches to smart mode if necessary

View File

@@ -37,7 +37,7 @@ Since components are regular classes you can pass data (including other componen
For example we can pass a config object and then retrieve an API key from it when needed:
```py
class ConfigurableComponent(MessageProvider):
class DataComponent(MessageProvider):
def __init__(self, config: Config):
self.config = config
@@ -51,6 +51,35 @@ class ConfigurableComponent(MessageProvider):
!!! note
Component-specific configuration handling isn't implemented yet.
## Configuring components
Components can be configured using a pydantic model.
To make component configurable, it must inherit from `ConfigurableComponent[BM]` where `BM` is the configuration class inheriting from pydantic's `BaseModel`.
You should pass the configuration instance to the `ConfigurableComponent`'s `__init__` or set its `config` property directly.
Using configuration allows you to load confugration from a file, and also serialize and deserialize it easily for any agent.
To learn more about configuration, including storing sensitive information and serialization see [Component Configuration](./components.md#component-configuration).
```py
# Example component configuration
class UserGreeterConfiguration(BaseModel):
user_name: str
class UserGreeterComponent(MessageProvider, ConfigurableComponent[UserGreeterConfiguration]):
def __init__(self):
# Creating configuration instance
# You could also pass it to the component constructor
# e.g. `def __init__(self, config: UserGreeterConfiguration):`
config = UserGreeterConfiguration(user_name="World")
# Passing the configuration instance to the parent class
UserGreeterComponent.__init__(self, config)
# This has the same effect as the line above:
# self.config = UserGreeterConfiguration(user_name="World")
def get_messages(self) -> Iterator[ChatMessage]:
# You can use the configuration like a regular model
yield ChatMessage.system(f"Hello, {self.config.user_name}!")
```
## Providing commands
To extend what an agent can do, you need to provide commands using `CommandProvider` protocol. For example to allow agent to multiply two numbers, you can create a component like this:
@@ -148,12 +177,12 @@ It gives an ability for the agent to ask user for input in the terminal.
yield self.ask_user
```
5. Since agent isn't always running in the terminal or interactive mode, we need to disable this component by setting `self._enabled` when it's not possible to ask for user input.
5. Since agent isn't always running in the terminal or interactive mode, we need to disable this component by setting `self._enabled=False` when it's not possible to ask for user input.
```py
def __init__(self, config: Config):
def __init__(self, interactive_mode: bool):
self.config = config
self._enabled = not config.noninteractive_mode
self._enabled = interactive_mode
```
The final component should look like this:
@@ -164,10 +193,10 @@ class MyUserInteractionComponent(CommandProvider):
"""Provides commands to interact with the user."""
# We pass config to check if we're in noninteractive mode
def __init__(self, config: Config):
def __init__(self, interactive_mode: bool):
self.config = config
# 5.
self._enabled = not config.noninteractive_mode
self._enabled = interactive_mode
# 4.
def get_commands(self) -> Iterator[Command]:
@@ -205,10 +234,10 @@ class MyAgent(Agent):
settings: AgentSettings,
llm_provider: MultiProvider,
file_storage: FileStorage,
legacy_config: Config,
app_config: Config,
):
# Call the parent constructor to bring in the default components
super().__init__(settings, llm_provider, file_storage, legacy_config)
super().__init__(settings, llm_provider, file_storage, app_config)
# Disable the default user interaction component by overriding it
self.user_interaction = MyUserInteractionComponent()
```
@@ -222,14 +251,14 @@ class MyAgent(Agent):
settings: AgentSettings,
llm_provider: MultiProvider,
file_storage: FileStorage,
legacy_config: Config,
app_config: Config,
):
# Call the parent constructor to bring in the default components
super().__init__(settings, llm_provider, file_storage, legacy_config)
super().__init__(settings, llm_provider, file_storage, app_config)
# Disable the default user interaction component
self.user_interaction = None
# Add our own component
self.my_user_interaction = MyUserInteractionComponent(legacy_config)
self.my_user_interaction = MyUserInteractionComponent(app_config)
```
## Learn more

View File

@@ -1,10 +1,16 @@
# Component Agents
!!! important
[Legacy plugins] no longer work with AutoGPT. They have been replaced by components,
although we're still working on a new system to load plug-in components.
[Legacy plugins]: https://github.com/Significant-Gravitas/Auto-GPT-Plugins
This guide explains the component-based architecture of AutoGPT agents. It's a new way of building agents that is more flexible and easier to extend. Components replace some agent's logic and plugins with a more modular and composable system.
Agent is composed of *components*, and each *component* implements a range of *protocols* (interfaces), each one providing a specific functionality, e.g. additional commands or messages. Each *protocol* is handled in a specific order, defined by the agent. This allows for a clear separation of concerns and a more modular design.
This system is simple, flexible, requires basically no configuration, and doesn't hide any data - anything can still be passed or accessed directly from or between components.
This system is simple, flexible, and doesn't hide any data - anything can still be passed or accessed directly from or between components.
### Definitions & Guides

View File

@@ -19,16 +19,16 @@ Forge is a ready-to-go template for *your* agent application. All the boilerplat
### 🚀 **Get Started!**
The best way to get started is to fork or download the AutoGPT repository and look at the example agent in `forge/forge/agent/forge_agent.py`.
This can work as a starting point for your own agent.
Agents are built using *components* which provide different functionality, see the [Component Introduction](./components/introduction.md). You can find built-in components in `forge/forge/components/`.
!!! warning
The tutorial series below is out of date.
The getting started [tutorial series](https://aiedge.medium.com/autogpt-forge-e3de53cc58ec) will guide you through the process of setting up your project all the way through to building a generalist agent.
1. [AutoGPT Forge: A Comprehensive Guide to Your First Steps](https://aiedge.medium.com/autogpt-forge-a-comprehensive-guide-to-your-first-steps-a1dfdf46e3b4)
2. [AutoGPT Forge: The Blueprint of an AI Agent](https://aiedge.medium.com/autogpt-forge-the-blueprint-of-an-ai-agent-75cd72ffde6)
3. [AutoGPT Forge: Interacting with your Agent](https://aiedge.medium.com/autogpt-forge-interacting-with-your-agent-1214561b06b)
4. [AutoGPT Forge: Crafting Intelligent Agent Logic](https://medium.com/@aiedge/autogpt-forge-crafting-intelligent-agent-logic-bc5197b14cb4)
Coming soon:
5. Interacting with and Benchmarking your Agent
6. Abilities
7. The Planning Loop
8. Memories

View File

@@ -15,7 +15,6 @@ nav:
- Options: AutoGPT/configuration/options.md
- Search: AutoGPT/configuration/search.md
- Voice: AutoGPT/configuration/voice.md
- Image Generation: AutoGPT/configuration/imagegen.md
- Usage: AutoGPT/usage.md
- Help us improve AutoGPT:
- Share your debug logs with us: AutoGPT/share-your-logs.md

41
forge/conftest.py Normal file
View File

@@ -0,0 +1,41 @@
import uuid
from pathlib import Path
import pytest
from forge.file_storage.base import FileStorage, FileStorageConfiguration
from forge.file_storage.local import LocalFileStorage
pytest_plugins = [
"tests.vcr",
]
@pytest.fixture(scope="session", autouse=True)
def load_env_vars():
from dotenv import load_dotenv
load_dotenv()
@pytest.fixture()
def tmp_project_root(tmp_path: Path) -> Path:
return tmp_path
@pytest.fixture()
def app_data_dir(tmp_project_root: Path) -> Path:
dir = tmp_project_root / "data"
dir.mkdir(parents=True, exist_ok=True)
return dir
@pytest.fixture()
def storage(app_data_dir: Path) -> FileStorage:
storage = LocalFileStorage(
FileStorageConfiguration(
root=Path(f"{app_data_dir}/{str(uuid.uuid4())}"), restrict_to_root=False
)
)
storage.initialize()
return storage

View File

@@ -27,7 +27,7 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
888 "Y88P" 888 "Y88888 "Y8888
888
Y8b d88P
"Y88P" v0.1.0
"Y88P" v0.2.0
\n"""
if __name__ == "__main__":

View File

@@ -18,12 +18,14 @@ from typing import (
)
from colorama import Fore
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field, ValidationInfo, field_validator
from pydantic_core import from_json, to_json
from forge.agent import protocols
from forge.agent.components import (
AgentComponent,
ComponentEndpointError,
ConfigurableComponent,
EndpointPipelineError,
)
from forge.config.ai_directives import AIDirectives
@@ -70,10 +72,10 @@ class BaseAgentConfiguration(SystemConfiguration):
`0` to stop the agent.
"""
cycles_remaining = cycle_budget
cycles_remaining: int = cycle_budget
"""The number of cycles remaining within the `cycle_budget`."""
cycle_count = 0
cycle_count: int = 0
"""The number of cycles that the agent has run since its initialization."""
send_token_limit: Optional[int] = None
@@ -82,14 +84,11 @@ class BaseAgentConfiguration(SystemConfiguration):
defaults to 75% of `llm.max_tokens`.
"""
summary_max_tlength: Optional[int] = None
# TODO: move to ActionHistoryConfiguration
@validator("use_functions_api")
def validate_openai_functions(cls, v: bool, values: dict[str, Any]):
if v:
smart_llm = values["smart_llm"]
fast_llm = values["fast_llm"]
@field_validator("use_functions_api")
def validate_openai_functions(cls, value: bool, info: ValidationInfo):
if value:
smart_llm = info.data["smart_llm"]
fast_llm = info.data["fast_llm"]
assert all(
[
not any(s in name for s in {"-0301", "-0314"})
@@ -99,7 +98,7 @@ class BaseAgentConfiguration(SystemConfiguration):
f"Model {smart_llm} does not support OpenAI Functions. "
"Please disable OPENAI_FUNCTIONS or choose a suitable model."
)
return v
return value
class BaseAgentSettings(SystemSettings):
@@ -272,6 +271,28 @@ class BaseAgent(Generic[AnyProposal], metaclass=AgentMeta):
raise e
return method_result
def dump_component_configs(self) -> str:
configs: dict[str, Any] = {}
for component in self.components:
if isinstance(component, ConfigurableComponent):
config_type_name = component.config.__class__.__name__
configs[config_type_name] = component.config
return to_json(configs).decode()
def load_component_configs(self, serialized_configs: str):
configs_dict: dict[str, dict[str, Any]] = from_json(serialized_configs)
for component in self.components:
if not isinstance(component, ConfigurableComponent):
continue
config_type = type(component.config)
config_type_name = config_type.__name__
if config_type_name in configs_dict:
# Parse the serialized data and update the existing config
updated_data = configs_dict[config_type_name]
data = {**component.config.model_dump(), **updated_data}
component.config = component.config.__class__(**data)
def _collect_components(self):
components = [
getattr(self, attr)
@@ -325,7 +346,7 @@ class BaseAgent(Generic[AnyProposal], metaclass=AgentMeta):
copied_item = item.copy()
elif isinstance(item, BaseModel):
# Deep copy for Pydantic models (deep=True to also copy nested models)
copied_item = item.copy(deep=True)
copied_item = item.model_copy(deep=True)
else:
# Deep copy for other objects
copied_item = copy.deepcopy(item)

View File

@@ -1,9 +1,14 @@
from __future__ import annotations
from abc import ABC
from typing import Callable, TypeVar
from typing import Callable, ClassVar, Generic, Optional, TypeVar
T = TypeVar("T", bound="AgentComponent")
from pydantic import BaseModel
from forge.models.config import _update_user_config_from_env, deep_update
AC = TypeVar("AC", bound="AgentComponent")
BM = TypeVar("BM", bound=BaseModel)
class AgentComponent(ABC):
@@ -24,7 +29,7 @@ class AgentComponent(ABC):
"""Return the reason this component is disabled."""
return self._disabled_reason
def run_after(self: T, *components: type[AgentComponent] | AgentComponent) -> T:
def run_after(self: AC, *components: type[AgentComponent] | AgentComponent) -> AC:
"""Set the components that this component should run after."""
for component in components:
t = component if isinstance(component, type) else type(component)
@@ -33,6 +38,39 @@ class AgentComponent(ABC):
return self
class ConfigurableComponent(ABC, Generic[BM]):
"""A component that can be configured with a Pydantic model."""
config_class: ClassVar[type[BM]] # type: ignore
def __init__(self, configuration: Optional[BM]):
self._config: Optional[BM] = None
if configuration is not None:
self.config = configuration
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if getattr(cls, "config_class", None) is None:
raise NotImplementedError(
f"ConfigurableComponent subclass {cls.__name__} "
"must define config_class class attribute."
)
@property
def config(self) -> BM:
if not hasattr(self, "_config") or self._config is None:
self.config = self.config_class()
return self._config # type: ignore
@config.setter
def config(self, config: BM):
if not hasattr(self, "_config") or self._config is None:
# Load configuration from environment variables
updated = _update_user_config_from_env(config)
config = self.config_class(**deep_update(config.model_dump(), updated))
self._config = config
class ComponentEndpointError(Exception):
"""Error of a single protocol method on a component."""

View File

@@ -0,0 +1,232 @@
import inspect
import logging
from typing import Any, Optional
from uuid import uuid4
from forge.agent.base import BaseAgent, BaseAgentSettings
from forge.agent.protocols import (
AfterExecute,
CommandProvider,
DirectiveProvider,
MessageProvider,
)
from forge.agent_protocol.agent import ProtocolAgent
from forge.agent_protocol.database.db import AgentDB
from forge.agent_protocol.models.task import (
Step,
StepRequestBody,
Task,
TaskRequestBody,
)
from forge.command.command import Command
from forge.components.system.system import SystemComponent
from forge.config.ai_profile import AIProfile
from forge.file_storage.base import FileStorage
from forge.llm.prompting.schema import ChatPrompt
from forge.llm.prompting.utils import dump_prompt
from forge.llm.providers.schema import AssistantChatMessage, AssistantFunctionCall
from forge.llm.providers.utils import function_specs_from_commands
from forge.models.action import (
ActionErrorResult,
ActionProposal,
ActionResult,
ActionSuccessResult,
)
from forge.utils.exceptions import AgentException, AgentTerminated
logger = logging.getLogger(__name__)
class ForgeAgent(ProtocolAgent, BaseAgent):
"""
The goal of the Forge is to take care of the boilerplate code,
so you can focus on agent design.
There is a great paper surveying the agent landscape: https://arxiv.org/abs/2308.11432
Which I would highly recommend reading as it will help you understand the possibilities.
ForgeAgent provides component support; https://docs.agpt.co/forge/components/introduction/
Using Components is a new way of building agents that is more flexible and easier to extend.
Components replace some agent's logic and plugins with a more modular and composable system.
""" # noqa: E501
def __init__(self, database: AgentDB, workspace: FileStorage):
"""
The database is used to store tasks, steps and artifact metadata.
The workspace is used to store artifacts (files).
"""
# An example agent information; you can modify this to suit your needs
state = BaseAgentSettings(
name="Forge Agent",
description="The Forge Agent is a generic agent that can solve tasks.",
agent_id=str(uuid4()),
ai_profile=AIProfile(
ai_name="ForgeAgent", ai_role="Generic Agent", ai_goals=["Solve tasks"]
),
task="Solve tasks",
)
# ProtocolAgent adds the Agent Protocol (API) functionality
ProtocolAgent.__init__(self, database, workspace)
# BaseAgent provides the component handling functionality
BaseAgent.__init__(self, state)
# AGENT COMPONENTS
# Components provide additional functionality to the agent
# There are NO components added by default in the BaseAgent
# You can create your own components or add existing ones
# Built-in components:
# https://docs.agpt.co/forge/components/built-in-components/
# System component provides "finish" command and adds some prompt information
self.system = SystemComponent()
async def create_task(self, task_request: TaskRequestBody) -> Task:
"""
The agent protocol, which is the core of the Forge,
works by creating a task and then executing steps for that task.
This method is called when the agent is asked to create a task.
We are hooking into function to add a custom log message.
Though you can do anything you want here.
"""
task = await super().create_task(task_request)
logger.info(
f"📦 Task created with ID: {task.task_id} and "
f"input: {task.input[:40]}{'...' if len(task.input) > 40 else ''}"
)
return task
async def execute_step(self, task_id: str, step_request: StepRequestBody) -> Step:
"""
Preffered method to add agent logic is to add custom components:
https://docs.agpt.co/forge/components/creating-components/
Outdated tutorial on how to add custom logic:
https://aiedge.medium.com/autogpt-forge-e3de53cc58ec
The agent protocol, which is the core of the Forge, works by creating a task and then
executing steps for that task. This method is called when the agent is asked to execute
a step.
The task that is created contains an input string, for the benchmarks this is the task
the agent has been asked to solve and additional input, which is a dictionary and
could contain anything.
If you want to get the task use:
```
task = await self.db.get_task(task_id)
```
The step request body is essentially the same as the task request and contains an input
string, for the benchmarks this is the task the agent has been asked to solve and
additional input, which is a dictionary and could contain anything.
You need to implement logic that will take in this step input and output the completed step
as a step object. You can do everything in a single step or you can break it down into
multiple steps. Returning a request to continue in the step output, the user can then decide
if they want the agent to continue or not.
""" # noqa: E501
step = await self.db.create_step(
task_id=task_id, input=step_request, is_last=False
)
proposal = await self.propose_action()
output = await self.execute(proposal)
if isinstance(output, ActionSuccessResult):
step.output = str(output.outputs)
elif isinstance(output, ActionErrorResult):
step.output = output.reason
return step
async def propose_action(self) -> ActionProposal:
self.reset_trace()
# Get directives
directives = self.state.directives.model_copy(deep=True)
directives.resources += await self.run_pipeline(DirectiveProvider.get_resources)
directives.constraints += await self.run_pipeline(
DirectiveProvider.get_constraints
)
directives.best_practices += await self.run_pipeline(
DirectiveProvider.get_best_practices
)
# Get commands
self.commands = await self.run_pipeline(CommandProvider.get_commands)
# Get messages
messages = await self.run_pipeline(MessageProvider.get_messages)
prompt: ChatPrompt = ChatPrompt(
messages=messages, functions=function_specs_from_commands(self.commands)
)
logger.debug(f"Executing prompt:\n{dump_prompt(prompt)}")
# Call the LLM and parse result
# THIS NEEDS TO BE REPLACED WITH YOUR LLM CALL/LOGIC
# Have a look at autogpt/agents/agent.py for an example (complete_and_parse)
proposal = ActionProposal(
thoughts="I cannot solve the task!",
use_tool=AssistantFunctionCall(
name="finish", arguments={"reason": "Unimplemented logic"}
),
raw_message=AssistantChatMessage(
content="finish(reason='Unimplemented logic')"
),
)
self.config.cycle_count += 1
return proposal
async def execute(self, proposal: Any, user_feedback: str = "") -> ActionResult:
tool = proposal.use_tool
# Get commands
self.commands = await self.run_pipeline(CommandProvider.get_commands)
# Execute the command
try:
command: Optional[Command] = None
for c in reversed(self.commands):
if tool.name in c.names:
command = c
if command is None:
raise AgentException(f"Command {tool.name} not found")
command_result = command(**tool.arguments)
if inspect.isawaitable(command_result):
command_result = await command_result
result = ActionSuccessResult(outputs=command_result)
except AgentTerminated:
result = ActionSuccessResult(outputs="Agent terminated or finished")
except AgentException as e:
result = ActionErrorResult.from_exception(e)
logger.warning(f"{tool} raised an error: {e}")
await self.run_pipeline(AfterExecute.after_execute, result)
logger.debug("\n".join(self.trace))
return result
async def do_not_execute(
self, denied_proposal: Any, user_feedback: str
) -> ActionResult:
result = ActionErrorResult(reason="Action denied")
await self.run_pipeline(AfterExecute.after_execute, result)
logger.debug("\n".join(self.trace))
return result

View File

@@ -28,7 +28,7 @@ from forge.file_storage.base import FileStorage
logger = logging.getLogger(__name__)
class Agent:
class ProtocolAgent:
def __init__(self, database: AgentDB, workspace: FileStorage):
self.db = database
self.workspace = workspace

View File

@@ -3,25 +3,20 @@ from pathlib import Path
import pytest
from fastapi import UploadFile
from forge.agent_protocol.database.db import AgentDB
from forge.agent_protocol.models.task import (
StepRequestBody,
Task,
TaskListResponse,
TaskRequestBody,
)
from forge.file_storage.base import FileStorageConfiguration
from forge.file_storage.local import LocalFileStorage
from .agent import Agent
from .agent import ProtocolAgent
from .database.db import AgentDB
from .models.task import StepRequestBody, Task, TaskListResponse, TaskRequestBody
@pytest.fixture
def agent(test_workspace: Path):
def agent(tmp_project_root: Path):
db = AgentDB("sqlite:///test.db")
config = FileStorageConfiguration(root=test_workspace)
config = FileStorageConfiguration(root=tmp_project_root)
workspace = LocalFileStorage(config)
return Agent(db, workspace)
return ProtocolAgent(db, workspace)
@pytest.fixture
@@ -33,7 +28,7 @@ def file_upload():
@pytest.mark.asyncio
async def test_create_task(agent: Agent):
async def test_create_task(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -42,7 +37,7 @@ async def test_create_task(agent: Agent):
@pytest.mark.asyncio
async def test_list_tasks(agent: Agent):
async def test_list_tasks(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -52,7 +47,7 @@ async def test_list_tasks(agent: Agent):
@pytest.mark.asyncio
async def test_get_task(agent: Agent):
async def test_get_task(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -63,7 +58,7 @@ async def test_get_task(agent: Agent):
@pytest.mark.xfail(reason="execute_step is not implemented")
@pytest.mark.asyncio
async def test_execute_step(agent: Agent):
async def test_execute_step(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -78,7 +73,7 @@ async def test_execute_step(agent: Agent):
@pytest.mark.xfail(reason="execute_step is not implemented")
@pytest.mark.asyncio
async def test_get_step(agent: Agent):
async def test_get_step(agent: ProtocolAgent):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -92,7 +87,7 @@ async def test_get_step(agent: Agent):
@pytest.mark.asyncio
async def test_list_artifacts(agent: Agent):
async def test_list_artifacts(agent: ProtocolAgent):
tasks = await agent.list_tasks()
assert tasks.tasks, "No tasks in test.db"
@@ -101,7 +96,7 @@ async def test_list_artifacts(agent: Agent):
@pytest.mark.asyncio
async def test_create_artifact(agent: Agent, file_upload: UploadFile):
async def test_create_artifact(agent: ProtocolAgent, file_upload: UploadFile):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)
@@ -116,7 +111,7 @@ async def test_create_artifact(agent: Agent, file_upload: UploadFile):
@pytest.mark.asyncio
async def test_create_and_get_artifact(agent: Agent, file_upload: UploadFile):
async def test_create_and_get_artifact(agent: ProtocolAgent, file_upload: UploadFile):
task_request = TaskRequestBody(
input="test_input", additional_input={"input": "additional_test_input"}
)

View File

@@ -24,7 +24,7 @@ from .models import (
)
if TYPE_CHECKING:
from forge.agent.agent import Agent
from .agent import ProtocolAgent
base_router = APIRouter()
logger = logging.getLogger(__name__)
@@ -73,7 +73,7 @@ async def create_agent_task(request: Request, task_request: TaskRequestBody) ->
"artifacts": [],
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
task = await agent.create_task(task_request)
@@ -124,7 +124,7 @@ async def list_agent_tasks(
}
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
tasks = await agent.list_tasks(page, page_size)
return tasks
@@ -185,7 +185,7 @@ async def get_agent_task(request: Request, task_id: str) -> Task:
]
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
task = await agent.get_task(task_id)
return task
@@ -239,7 +239,7 @@ async def list_agent_task_steps(
}
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
steps = await agent.list_steps(task_id, page, page_size)
return steps
@@ -298,7 +298,7 @@ async def execute_agent_task_step(
...
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
# An empty step request represents a yes to continue command
if not step_request:
@@ -337,7 +337,7 @@ async def get_agent_task_step(request: Request, task_id: str, step_id: str) -> S
...
}
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
step = await agent.get_step(task_id, step_id)
return step
@@ -388,7 +388,7 @@ async def list_agent_task_artifacts(
}
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
artifacts = await agent.list_artifacts(task_id, page, page_size)
return artifacts
@@ -430,7 +430,7 @@ async def upload_agent_task_artifacts(
"file_name": "main.py"
}
""" # noqa: E501
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
if file is None:
raise HTTPException(status_code=400, detail="File must be specified")
@@ -468,7 +468,7 @@ async def download_agent_task_artifact(
Response:
<file_content_of_artifact>
"""
agent: "Agent" = request["agent"]
agent: "ProtocolAgent" = request["agent"]
try:
return await agent.get_artifact(task_id, artifact_id)
except Exception:

View File

@@ -1,38 +1,34 @@
from datetime import datetime
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
class Artifact(BaseModel):
created_at: datetime = Field(
...,
description="The creation datetime of the task.",
example="2023-01-01T00:00:00Z",
json_encoders={datetime: lambda v: v.isoformat()},
examples=["2023-01-01T00:00:00Z"],
)
modified_at: datetime = Field(
...,
description="The modification datetime of the task.",
example="2023-01-01T00:00:00Z",
json_encoders={datetime: lambda v: v.isoformat()},
examples=["2023-01-01T00:00:00Z"],
)
artifact_id: str = Field(
...,
description="ID of the artifact.",
example="b225e278-8b4c-4f99-a696-8facf19f0e56",
examples=["b225e278-8b4c-4f99-a696-8facf19f0e56"],
)
agent_created: bool = Field(
...,
description="Whether the artifact has been created by the agent.",
example=False,
examples=[False],
)
relative_path: str = Field(
...,
description="Relative path of the artifact in the agents workspace.",
example="/my_folder/my_other_folder/",
examples=["/my_folder/my_other_folder/"],
)
file_name: str = Field(
...,
description="Filename of the artifact.",
example="main.py",
examples=["main.py"],
)
model_config = ConfigDict(
json_encoders={datetime: lambda v: v.isoformat()},
)

View File

@@ -2,7 +2,7 @@ from pydantic import BaseModel, Field
class Pagination(BaseModel):
total_items: int = Field(..., description="Total number of items.", example=42)
total_pages: int = Field(..., description="Total number of pages.", example=97)
current_page: int = Field(..., description="Current_page page number.", example=1)
page_size: int = Field(..., description="Number of items per page.", example=25)
total_items: int = Field(description="Total number of items.", examples=[42])
total_pages: int = Field(description="Total number of pages.", examples=[97])
current_page: int = Field(description="Current_page page number.", examples=[1])
page_size: int = Field(description="Number of items per page.", examples=[25])

View File

@@ -4,7 +4,7 @@ from datetime import datetime
from enum import Enum
from typing import Any, List, Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
from .artifact import Artifact
from .pagination import Pagination
@@ -12,48 +12,48 @@ from .pagination import Pagination
class TaskRequestBody(BaseModel):
input: str = Field(
...,
min_length=1,
description="Input prompt for the task.",
example="Write the words you receive to the file 'output.txt'.",
examples=["Write the words you receive to the file 'output.txt'."],
)
additional_input: dict[str, Any] = Field(default_factory=dict)
class Task(TaskRequestBody):
created_at: datetime = Field(
...,
description="The creation datetime of the task.",
example="2023-01-01T00:00:00Z",
json_encoders={datetime: lambda v: v.isoformat()},
examples=["2023-01-01T00:00:00Z"],
)
modified_at: datetime = Field(
...,
description="The modification datetime of the task.",
example="2023-01-01T00:00:00Z",
json_encoders={datetime: lambda v: v.isoformat()},
examples=["2023-01-01T00:00:00Z"],
)
task_id: str = Field(
...,
description="The ID of the task.",
example="50da533e-3904-4401-8a07-c49adf88b5eb",
examples=["50da533e-3904-4401-8a07-c49adf88b5eb"],
)
artifacts: list[Artifact] = Field(
default_factory=list,
description="A list of artifacts that the task has produced.",
example=[
examples=[
"7a49f31c-f9c6-4346-a22c-e32bc5af4d8e",
"ab7b4091-2560-4692-a4fe-d831ea3ca7d6",
],
)
model_config = ConfigDict(
json_encoders={datetime: lambda v: v.isoformat()},
)
class StepRequestBody(BaseModel):
name: Optional[str] = Field(
default=None, description="The name of the task step.", example="Write to file"
default=None,
description="The name of the task step.",
examples=["Write to file"],
)
input: str = Field(
..., description="Input prompt for the step.", example="Washington"
description="Input prompt for the step.", examples=["Washington"]
)
additional_input: dict[str, Any] = Field(default_factory=dict)
@@ -66,40 +66,44 @@ class StepStatus(Enum):
class Step(StepRequestBody):
created_at: datetime = Field(
...,
description="The creation datetime of the task.",
example="2023-01-01T00:00:00Z",
json_encoders={datetime: lambda v: v.isoformat()},
examples=[
"2023-01-01T00:00:00Z",
],
)
modified_at: datetime = Field(
...,
description="The modification datetime of the task.",
example="2023-01-01T00:00:00Z",
json_encoders={datetime: lambda v: v.isoformat()},
examples=[
"2023-01-01T00:00:00Z",
],
)
task_id: str = Field(
...,
description="The ID of the task this step belongs to.",
example="50da533e-3904-4401-8a07-c49adf88b5eb",
examples=[
"50da533e-3904-4401-8a07-c49adf88b5eb",
],
)
step_id: str = Field(
...,
description="The ID of the task step.",
example="6bb1801a-fd80-45e8-899a-4dd723cc602e",
examples=[
"6bb1801a-fd80-45e8-899a-4dd723cc602e",
],
)
name: Optional[str] = Field(
default=None, description="The name of the task step.", example="Write to file"
default=None,
description="The name of the task step.",
examples=["Write to file"],
)
status: StepStatus = Field(
..., description="The status of the task step.", example="created"
description="The status of the task step.", examples=["created"]
)
output: Optional[str] = Field(
default=None,
description="Output of the task step.",
example=(
examples=[
"I am going to use the write_to_file command and write Washington "
"to a file called output.txt <write_to_file('output.txt', 'Washington')"
),
],
)
additional_output: Optional[dict[str, Any]] = None
artifacts: list[Artifact] = Field(
@@ -107,7 +111,11 @@ class Step(StepRequestBody):
description="A list of artifacts that the step has produced.",
)
is_last: bool = Field(
..., description="Whether this is the last step in the task.", example=True
description="Whether this is the last step in the task.", examples=[True]
)
model_config = ConfigDict(
json_encoders={datetime: lambda v: v.isoformat()},
)

13
forge/forge/app.py Normal file
View File

@@ -0,0 +1,13 @@
import os
from pathlib import Path
from forge.agent.forge_agent import ForgeAgent
from forge.agent_protocol.database.db import AgentDB
from forge.file_storage import FileStorageBackendName, get_storage
database_name = os.getenv("DATABASE_STRING")
workspace = get_storage(FileStorageBackendName.LOCAL, root_path=Path("workspace"))
database = AgentDB(database_name, debug_enabled=False)
agent = ForgeAgent(database=database, workspace=workspace)
app = agent.get_agent_app()

View File

@@ -30,6 +30,115 @@ class MyAgent(BaseAgent):
self.some_component = SomeComponent(self.hello_component)
```
## Component configuration
Each component can have its own configuration defined using a regular pydantic `BaseModel`.
To ensure the configuration is loaded from the file correctly, the component must inherit from `ConfigurableComponent[BM]` where `BM` is the configuration model it uses.
`ConfigurableComponent` provides a `config` attribute that holds the configuration instance.
It's possible to either set the `config` attribute directly or pass the configuration instance to the component's constructor.
Extra configuration (i.e. for components that are not part of the agent) can be passed and will be silently ignored. Extra config won't be applied even if the component is added later.
To see the configuration of built-in components visit [Built-in Components](./built-in-components.md).
```py
from pydantic import BaseModel
from forge.agent.components import ConfigurableComponent
class MyConfig(BaseModel):
some_value: str
class MyComponent(AgentComponent, ConfigurableComponent[MyConfig]):
def __init__(self, config: MyConfig):
super().__init__(config)
# This has the same effect as above:
# self.config = config
def get_some_value(self) -> str:
# Access the configuration like a regular model
return self.config.some_value
```
### Sensitive information
While it's possible to pass sensitive data directly in code to the configuration it's recommended to use `UserConfigurable(from_env="ENV_VAR_NAME", exclude=True)` field for sensitive data like API keys.
The data will be loaded from the environment variable but keep in mind that value passed in code takes precedence.
All fields, even excluded ones (`exclude=True`) will be loaded when the configuration is loaded from the file.
Exclusion allows you to skip them during *serialization*, non excluded `SecretStr` will be serialized literally as a `"**********"` string.
```py
from pydantic import BaseModel, SecretStr
from forge.models.config import UserConfigurable
class SensitiveConfig(BaseModel):
api_key: SecretStr = UserConfigurable(from_env="API_KEY", exclude=True)
```
### Configuration serialization
`BaseAgent` provides two methods:
1. `dump_component_configs`: Serializes all components' configurations as json string.
1. `load_component_configs`: Deserializes json string to configuration and applies it.
### JSON configuration
You can specify a JSON file (e.g. `config.json`) to use for the configuration when launching an agent.
This file contains settings for individual [Components](../components/introduction.md) that AutoGPT uses.
To specify the file use `--component-config-file` CLI option, for example to use `config.json`:
```shell
./autogpt.sh run --component-config-file config.json
```
!!! note
If you're using Docker to run AutoGPT, you need to mount or copy the configuration file to the container.
See [Docker Guide](../../AutoGPT/setup/docker.md) for more information.
### Example JSON configuration
You can copy configuration you want to change, for example to `autogpt/config.json` and modify it to your needs.
*Most configuration has default values, it's better to set only values you want to modify.*
You can see the available configuration fields and default values in [Build-in Components](./built-in-components.md).
You can set sensitive variables in the `.json` file as well but it's recommended to use environment variables instead.
```json
{
"CodeExecutorConfiguration": {
"execute_local_commands": false,
"shell_command_control": "allowlist",
"shell_allowlist": ["cat", "echo"],
"shell_denylist": [],
"docker_container_name": "agent_sandbox"
},
"FileManagerConfiguration": {
"storage_path": "agents/AutoGPT/",
"workspace_path": "agents/AutoGPT/workspace"
},
"GitOperationsConfiguration": {
"github_username": null
},
"ActionHistoryConfiguration": {
"model_name": "gpt-3.5-turbo",
"max_tokens": 1024,
"spacy_language_model": "en_core_web_sm"
},
"ImageGeneratorConfiguration": {
"image_provider": "dalle",
"huggingface_image_model": "CompVis/stable-diffusion-v1-4",
"sd_webui_url": "http://localhost:7860"
},
"WebSearchConfiguration": {
"duckduckgo_max_attempts": 3
},
"WebSeleniumConfiguration": {
"model_name": "gpt-3.5-turbo",
"web_browser": "chrome",
"headless": true,
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
"browse_spacy_language_model": "en_core_web_sm"
}
}
```
## Ordering components
The execution order of components is important because some may depend on the results of the previous ones.
@@ -72,6 +181,7 @@ class MyAgent(Agent):
## Disabling components
You can control which components are enabled by setting their `_enabled` attribute.
Components are *enabled* by default.
Either provide a `bool` value or a `Callable[[], bool]`, will be checked each time
the component is about to be executed. This way you can dynamically enable or disable
components based on some conditions.

View File

View File

@@ -1,41 +1,95 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Iterator, Optional
from typing import Callable, Iterator, Optional
from pydantic import BaseModel
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import AfterExecute, AfterParse, MessageProvider
from forge.llm.prompting.utils import indent
from forge.llm.providers import ChatMessage, MultiProvider
if TYPE_CHECKING:
from forge.config.config import Config
from forge.llm.providers.multi import ModelName
from forge.llm.providers.openai import OpenAIModelName
from forge.llm.providers.schema import ToolResultMessage
from .model import ActionResult, AnyProposal, Episode, EpisodicActionHistory
class ActionHistoryComponent(MessageProvider, AfterParse[AnyProposal], AfterExecute):
class ActionHistoryConfiguration(BaseModel):
model_name: ModelName = OpenAIModelName.GPT3
"""Name of the llm model used to compress the history"""
max_tokens: int = 1024
"""Maximum number of tokens to use up with generated history messages"""
spacy_language_model: str = "en_core_web_sm"
"""Language model used for summary chunking using spacy"""
full_message_count: int = 4
"""Number of latest non-summarized messages to include in the history"""
class ActionHistoryComponent(
MessageProvider,
AfterParse[AnyProposal],
AfterExecute,
ConfigurableComponent[ActionHistoryConfiguration],
):
"""Keeps track of the event history and provides a summary of the steps."""
config_class = ActionHistoryConfiguration
def __init__(
self,
event_history: EpisodicActionHistory[AnyProposal],
max_tokens: int,
count_tokens: Callable[[str], int],
legacy_config: Config,
llm_provider: MultiProvider,
config: Optional[ActionHistoryConfiguration] = None,
) -> None:
ConfigurableComponent.__init__(self, config)
self.event_history = event_history
self.max_tokens = max_tokens
self.count_tokens = count_tokens
self.legacy_config = legacy_config
self.llm_provider = llm_provider
def get_messages(self) -> Iterator[ChatMessage]:
if progress := self._compile_progress(
self.event_history.episodes,
self.max_tokens,
self.count_tokens,
):
yield ChatMessage.system(f"## Progress on your Task so far\n\n{progress}")
messages: list[ChatMessage] = []
step_summaries: list[str] = []
tokens: int = 0
n_episodes = len(self.event_history.episodes)
# Include a summary for all except a few latest steps
for i, episode in enumerate(reversed(self.event_history.episodes)):
# Use full format for a few steps, summary or format for older steps
if i < self.config.full_message_count:
messages.insert(0, episode.action.raw_message)
tokens += self.count_tokens(str(messages[0])) # HACK
if episode.result:
result_message = self._make_result_message(episode, episode.result)
messages.insert(1, result_message)
tokens += self.count_tokens(str(result_message)) # HACK
continue
elif episode.summary is None:
step_content = indent(episode.format(), 2).strip()
else:
step_content = episode.summary
step = f"* Step {n_episodes - i}: {step_content}"
if self.config.max_tokens and self.count_tokens:
step_tokens = self.count_tokens(step)
if tokens + step_tokens > self.config.max_tokens:
break
tokens += step_tokens
step_summaries.insert(0, step)
if step_summaries:
step_summaries_fmt = "\n\n".join(step_summaries)
yield ChatMessage.system(
f"## Progress on your Task so far\n"
"Here is a summary of the steps that you have executed so far, "
"use this as your consideration for determining the next action!\n"
f"{step_summaries_fmt}"
)
yield from messages
def after_parse(self, result: AnyProposal) -> None:
self.event_history.register_action(result)
@@ -43,9 +97,44 @@ class ActionHistoryComponent(MessageProvider, AfterParse[AnyProposal], AfterExec
async def after_execute(self, result: ActionResult) -> None:
self.event_history.register_result(result)
await self.event_history.handle_compression(
self.llm_provider, self.legacy_config
self.llm_provider, self.config.model_name, self.config.spacy_language_model
)
@staticmethod
def _make_result_message(episode: Episode, result: ActionResult) -> ChatMessage:
if result.status == "success":
return (
ToolResultMessage(
content=str(result.outputs),
tool_call_id=episode.action.raw_message.tool_calls[0].id,
)
if episode.action.raw_message.tool_calls
else ChatMessage.user(
f"{episode.action.use_tool.name} returned: "
+ (
f"```\n{result.outputs}\n```"
if "\n" in str(result.outputs)
else f"`{result.outputs}`"
)
)
)
elif result.status == "error":
return (
ToolResultMessage(
content=f"{result.reason}\n\n{result.error or ''}".strip(),
is_error=True,
tool_call_id=episode.action.raw_message.tool_calls[0].id,
)
if episode.action.raw_message.tool_calls
else ChatMessage.user(
f"{episode.action.use_tool.name} raised an error: ```\n"
f"{result.reason}\n"
"```"
)
)
else:
return ChatMessage.user(result.feedback)
def _compile_progress(
self,
episode_history: list[Episode[AnyProposal]],
@@ -60,8 +149,8 @@ class ActionHistoryComponent(MessageProvider, AfterParse[AnyProposal], AfterExec
n_episodes = len(episode_history)
for i, episode in enumerate(reversed(episode_history)):
# Use full format for the latest 4 steps, summary or format for older steps
if i < 4 or episode.summary is None:
# Use full format for a few latest steps, summary or format for older steps
if i < self.config.full_message_count or episode.summary is None:
step_content = indent(episode.format(), 2).strip()
else:
step_content = episode.summary

View File

@@ -3,20 +3,19 @@ from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, Generic
from pydantic import Field
from pydantic.generics import GenericModel
from pydantic import BaseModel, Field
from forge.content_processing.text import summarize_text
from forge.llm.prompting.utils import format_numbered_list, indent
from forge.llm.providers.multi import ModelName
from forge.models.action import ActionResult, AnyProposal
from forge.models.utils import ModelWithSummary
if TYPE_CHECKING:
from forge.config.config import Config
from forge.llm.providers import MultiProvider
class Episode(GenericModel, Generic[AnyProposal]):
class Episode(BaseModel, Generic[AnyProposal]):
action: AnyProposal
result: ActionResult | None
summary: str | None = None
@@ -52,7 +51,7 @@ class Episode(GenericModel, Generic[AnyProposal]):
return executed_action + action_result
class EpisodicActionHistory(GenericModel, Generic[AnyProposal]):
class EpisodicActionHistory(BaseModel, Generic[AnyProposal]):
"""Utility container for an action history"""
episodes: list[Episode[AnyProposal]] = Field(default_factory=list)
@@ -108,7 +107,10 @@ class EpisodicActionHistory(GenericModel, Generic[AnyProposal]):
self.cursor = len(self.episodes)
async def handle_compression(
self, llm_provider: MultiProvider, app_config: Config
self,
llm_provider: MultiProvider,
model_name: ModelName,
spacy_model: str,
) -> None:
"""Compresses each episode in the action history using an LLM.
@@ -131,7 +133,8 @@ class EpisodicActionHistory(GenericModel, Generic[AnyProposal]):
episode.format(),
instruction=compress_instruction,
llm_provider=llm_provider,
config=app_config,
model_name=model_name,
spacy_model=spacy_model,
)
for episode in episodes_to_summarize
]

View File

@@ -1,13 +1,6 @@
from .code_executor import (
ALLOWLIST_CONTROL,
DENYLIST_CONTROL,
CodeExecutionError,
CodeExecutorComponent,
)
from .code_executor import CodeExecutionError, CodeExecutorComponent
__all__ = [
"ALLOWLIST_CONTROL",
"DENYLIST_CONTROL",
"CodeExecutionError",
"CodeExecutorComponent",
]

View File

@@ -5,16 +5,16 @@ import shlex
import string
import subprocess
from pathlib import Path
from typing import Iterator
from typing import Iterator, Literal, Optional
import docker
from docker.errors import DockerException, ImageNotFound, NotFound
from docker.models.containers import Container as DockerContainer
from pydantic import BaseModel, Field
from forge.agent import BaseAgentSettings
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import CommandProvider
from forge.command import Command, command
from forge.config.config import Config
from forge.file_storage import FileStorage
from forge.models.json_schema import JSONSchema
from forge.utils.exceptions import (
@@ -25,9 +25,6 @@ from forge.utils.exceptions import (
logger = logging.getLogger(__name__)
ALLOWLIST_CONTROL = "allowlist"
DENYLIST_CONTROL = "denylist"
def we_are_running_in_a_docker_container() -> bool:
"""Check if we are running in a Docker container
@@ -56,15 +53,47 @@ class CodeExecutionError(CommandExecutionError):
"""The operation (an attempt to run arbitrary code) returned an error"""
class CodeExecutorComponent(CommandProvider):
class CodeExecutorConfiguration(BaseModel):
execute_local_commands: bool = False
"""Enable shell command execution"""
shell_command_control: Literal["allowlist", "denylist"] = "allowlist"
"""Controls which list is used"""
shell_allowlist: list[str] = Field(default_factory=list)
"""List of allowed shell commands"""
shell_denylist: list[str] = Field(default_factory=list)
"""List of prohibited shell commands"""
docker_container_name: str = "agent_sandbox"
"""Name of the Docker container used for code execution"""
class CodeExecutorComponent(
CommandProvider, ConfigurableComponent[CodeExecutorConfiguration]
):
"""Provides commands to execute Python code and shell commands."""
config_class = CodeExecutorConfiguration
def __init__(
self, workspace: FileStorage, state: BaseAgentSettings, config: Config
self,
workspace: FileStorage,
config: Optional[CodeExecutorConfiguration] = None,
):
ConfigurableComponent.__init__(self, config)
self.workspace = workspace
self.state = state
self.legacy_config = config
# Change container name if it's empty or default to prevent different agents
# from using the same container
default_container_name = self.config.model_fields[
"docker_container_name"
].default
if (
not self.config.docker_container_name
or self.config.docker_container_name == default_container_name
):
random_suffix = "".join(random.choices(string.ascii_lowercase, k=8))
self.config.docker_container_name = (
f"{default_container_name}_{random_suffix}"
)
if not we_are_running_in_a_docker_container() and not is_docker_available():
logger.info(
@@ -72,7 +101,7 @@ class CodeExecutorComponent(CommandProvider):
"The code execution commands will not be available."
)
if not self.legacy_config.execute_local_commands:
if not self.config.execute_local_commands:
logger.info(
"Local shell commands are disabled. To enable them,"
" set EXECUTE_LOCAL_COMMANDS to 'True' in your config file."
@@ -83,7 +112,7 @@ class CodeExecutorComponent(CommandProvider):
yield self.execute_python_code
yield self.execute_python_file
if self.legacy_config.execute_local_commands:
if self.config.execute_local_commands:
yield self.execute_shell
yield self.execute_shell_popen
@@ -192,7 +221,7 @@ class CodeExecutorComponent(CommandProvider):
logger.debug("App is not running in a Docker container")
return self._run_python_code_in_docker(file_path, args)
def validate_command(self, command_line: str, config: Config) -> tuple[bool, bool]:
def validate_command(self, command_line: str) -> tuple[bool, bool]:
"""Check whether a command is allowed and whether it may be executed in a shell.
If shell command control is enabled, we disallow executing in a shell, because
@@ -211,10 +240,10 @@ class CodeExecutorComponent(CommandProvider):
command_name = shlex.split(command_line)[0]
if config.shell_command_control == ALLOWLIST_CONTROL:
return command_name in config.shell_allowlist, False
elif config.shell_command_control == DENYLIST_CONTROL:
return command_name not in config.shell_denylist, False
if self.config.shell_command_control == "allowlist":
return command_name in self.config.shell_allowlist, False
elif self.config.shell_command_control == "denylist":
return command_name not in self.config.shell_denylist, False
else:
return True, True
@@ -238,9 +267,7 @@ class CodeExecutorComponent(CommandProvider):
Returns:
str: The output of the command
"""
allow_execute, allow_shell = self.validate_command(
command_line, self.legacy_config
)
allow_execute, allow_shell = self.validate_command(command_line)
if not allow_execute:
logger.info(f"Command '{command_line}' not allowed")
raise OperationNotAllowedError("This shell command is not allowed.")
@@ -287,9 +314,7 @@ class CodeExecutorComponent(CommandProvider):
Returns:
str: Description of the fact that the process started and its id
"""
allow_execute, allow_shell = self.validate_command(
command_line, self.legacy_config
)
allow_execute, allow_shell = self.validate_command(command_line)
if not allow_execute:
logger.info(f"Command '{command_line}' not allowed")
raise OperationNotAllowedError("This shell command is not allowed.")
@@ -320,12 +345,10 @@ class CodeExecutorComponent(CommandProvider):
"""Run a Python script in a Docker container"""
file_path = self.workspace.get_path(filename)
try:
assert self.state.agent_id, "Need Agent ID to attach Docker container"
client = docker.from_env()
image_name = "python:3-alpine"
container_is_fresh = False
container_name = f"{self.state.agent_id}_sandbox"
container_name = self.config.docker_container_name
with self.workspace.mount() as local_path:
try:
container: DockerContainer = client.containers.get(

View File

@@ -4,20 +4,20 @@ import tempfile
from pathlib import Path
import pytest
from forge.components.code_executor.code_executor import (
ALLOWLIST_CONTROL,
from forge.file_storage.base import FileStorage
from forge.utils.exceptions import InvalidArgumentError, OperationNotAllowedError
from .code_executor import (
CodeExecutorComponent,
is_docker_available,
we_are_running_in_a_docker_container,
)
from forge.utils.exceptions import InvalidArgumentError, OperationNotAllowedError
from autogpt.agents.agent import Agent
@pytest.fixture
def code_executor_component(agent: Agent):
return agent.code_executor
def code_executor_component(storage: FileStorage):
return CodeExecutorComponent(storage)
@pytest.fixture
@@ -26,10 +26,8 @@ def random_code(random_string) -> str:
@pytest.fixture
def python_test_file(agent: Agent, random_code: str):
temp_file = tempfile.NamedTemporaryFile(
dir=agent.file_manager.workspace.root, suffix=".py"
)
def python_test_file(storage: FileStorage, random_code: str):
temp_file = tempfile.NamedTemporaryFile(dir=storage.root, suffix=".py")
temp_file.write(str.encode(random_code))
temp_file.flush()
@@ -38,10 +36,8 @@ def python_test_file(agent: Agent, random_code: str):
@pytest.fixture
def python_test_args_file(agent: Agent):
temp_file = tempfile.NamedTemporaryFile(
dir=agent.file_manager.workspace.root, suffix=".py"
)
def python_test_args_file(storage: FileStorage):
temp_file = tempfile.NamedTemporaryFile(dir=storage.root, suffix=".py")
temp_file.write(str.encode("import sys\nprint(sys.argv[1], sys.argv[2])"))
temp_file.flush()
@@ -58,7 +54,6 @@ def test_execute_python_file(
code_executor_component: CodeExecutorComponent,
python_test_file: Path,
random_string: str,
agent: Agent,
):
if not (is_docker_available() or we_are_running_in_a_docker_container()):
pytest.skip("Docker is not available")
@@ -71,7 +66,6 @@ def test_execute_python_file_args(
code_executor_component: CodeExecutorComponent,
python_test_args_file: Path,
random_string: str,
agent: Agent,
):
if not (is_docker_available() or we_are_running_in_a_docker_container()):
pytest.skip("Docker is not available")
@@ -89,7 +83,6 @@ async def test_execute_python_code(
code_executor_component: CodeExecutorComponent,
random_code: str,
random_string: str,
agent: Agent,
):
if not (is_docker_available() or we_are_running_in_a_docker_container()):
pytest.skip("Docker is not available")
@@ -98,16 +91,12 @@ async def test_execute_python_code(
assert result.replace("\r", "") == f"Hello {random_string}!\n"
def test_execute_python_file_invalid(
code_executor_component: CodeExecutorComponent, agent: Agent
):
def test_execute_python_file_invalid(code_executor_component: CodeExecutorComponent):
with pytest.raises(InvalidArgumentError):
code_executor_component.execute_python_file(Path("not_python.txt"))
def test_execute_python_file_not_found(
code_executor_component: CodeExecutorComponent, agent: Agent
):
def test_execute_python_file_not_found(code_executor_component: CodeExecutorComponent):
with pytest.raises(
FileNotFoundError,
match=r"python: can't open file '([a-zA-Z]:)?[/\\\-\w]*notexist.py': "
@@ -117,52 +106,56 @@ def test_execute_python_file_not_found(
def test_execute_shell(
code_executor_component: CodeExecutorComponent, random_string: str, agent: Agent
code_executor_component: CodeExecutorComponent, random_string: str
):
code_executor_component.config.shell_command_control = "allowlist"
code_executor_component.config.shell_allowlist = ["echo"]
result = code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
assert f"Hello {random_string}!" in result
def test_execute_shell_local_commands_not_allowed(
code_executor_component: CodeExecutorComponent, random_string: str, agent: Agent
code_executor_component: CodeExecutorComponent, random_string: str
):
result = code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
assert f"Hello {random_string}!" in result
with pytest.raises(OperationNotAllowedError, match="not allowed"):
code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
def test_execute_shell_denylist_should_deny(
code_executor_component: CodeExecutorComponent, agent: Agent, random_string: str
code_executor_component: CodeExecutorComponent, random_string: str
):
agent.legacy_config.shell_denylist = ["echo"]
code_executor_component.config.shell_command_control = "denylist"
code_executor_component.config.shell_denylist = ["echo"]
with pytest.raises(OperationNotAllowedError, match="not allowed"):
code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
def test_execute_shell_denylist_should_allow(
code_executor_component: CodeExecutorComponent, agent: Agent, random_string: str
code_executor_component: CodeExecutorComponent, random_string: str
):
agent.legacy_config.shell_denylist = ["cat"]
code_executor_component.config.shell_command_control = "denylist"
code_executor_component.config.shell_denylist = ["cat"]
result = code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
assert "Hello" in result and random_string in result
def test_execute_shell_allowlist_should_deny(
code_executor_component: CodeExecutorComponent, agent: Agent, random_string: str
code_executor_component: CodeExecutorComponent, random_string: str
):
agent.legacy_config.shell_command_control = ALLOWLIST_CONTROL
agent.legacy_config.shell_allowlist = ["cat"]
code_executor_component.config.shell_command_control = "allowlist"
code_executor_component.config.shell_allowlist = ["cat"]
with pytest.raises(OperationNotAllowedError, match="not allowed"):
code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
def test_execute_shell_allowlist_should_allow(
code_executor_component: CodeExecutorComponent, agent: Agent, random_string: str
code_executor_component: CodeExecutorComponent, random_string: str
):
agent.legacy_config.shell_command_control = ALLOWLIST_CONTROL
agent.legacy_config.shell_allowlist = ["echo"]
code_executor_component.config.shell_command_control = "allowlist"
code_executor_component.config.shell_allowlist = ["echo"]
result = code_executor_component.execute_shell(f"echo 'Hello {random_string}!'")
assert "Hello" in result and random_string in result

View File

@@ -3,7 +3,10 @@ import os
from pathlib import Path
from typing import Iterator, Optional
from pydantic import BaseModel, ConfigDict
from forge.agent import BaseAgentSettings
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import CommandProvider, DirectiveProvider
from forge.command import Command, command
from forge.file_storage.base import FileStorage
@@ -13,67 +16,91 @@ from forge.utils.file_operations import decode_textual_file
logger = logging.getLogger(__name__)
class FileManagerComponent(DirectiveProvider, CommandProvider):
class FileManagerConfiguration(BaseModel):
storage_path: str
"""Path to agent files, e.g. state"""
workspace_path: str
"""Path to files that agent has access to"""
model_config = ConfigDict(
# Prevent mutation of the configuration
# as this wouldn't be reflected in the file storage
frozen=False
)
class FileManagerComponent(
DirectiveProvider, CommandProvider, ConfigurableComponent[FileManagerConfiguration]
):
"""
Adds general file manager (e.g. Agent state),
workspace manager (e.g. Agent output files) support and
commands to perform operations on files and folders.
"""
files: FileStorage
"""Agent-related files, e.g. state, logs.
Use `workspace` to access the agent's workspace files."""
workspace: FileStorage
"""Workspace that the agent has access to, e.g. for reading/writing files.
Use `files` to access agent-related files, e.g. state, logs."""
config_class = FileManagerConfiguration
STATE_FILE = "state.json"
"""The name of the file where the agent's state is stored."""
def __init__(self, state: BaseAgentSettings, file_storage: FileStorage):
self.state = state
def __init__(
self,
file_storage: FileStorage,
agent_state: BaseAgentSettings,
config: Optional[FileManagerConfiguration] = None,
):
"""Initialise the FileManagerComponent.
Either `agent_id` or `config` must be provided.
if not state.agent_id:
Args:
file_storage (FileStorage): The file storage instance to use.
state (BaseAgentSettings): The agent's state.
config (FileManagerConfiguration, optional): The configuration for
the file manager. Defaults to None.
"""
if not agent_state.agent_id:
raise ValueError("Agent must have an ID.")
self.files = file_storage.clone_with_subroot(f"agents/{state.agent_id}/")
self.workspace = file_storage.clone_with_subroot(
f"agents/{state.agent_id}/workspace"
)
self.agent_state = agent_state
if not config:
storage_path = f"agents/{self.agent_state.agent_id}/"
workspace_path = f"agents/{self.agent_state.agent_id}/workspace"
ConfigurableComponent.__init__(
self,
FileManagerConfiguration(
storage_path=storage_path, workspace_path=workspace_path
),
)
else:
ConfigurableComponent.__init__(self, config)
self.storage = file_storage.clone_with_subroot(self.config.storage_path)
"""Agent-related files, e.g. state, logs.
Use `workspace` to access the agent's workspace files."""
self.workspace = file_storage.clone_with_subroot(self.config.workspace_path)
"""Workspace that the agent has access to, e.g. for reading/writing files.
Use `storage` to access agent-related files, e.g. state, logs."""
self._file_storage = file_storage
async def save_state(self, save_as: Optional[str] = None) -> None:
"""Save the agent's state to the state file."""
state: BaseAgentSettings = getattr(self, "state")
if save_as:
temp_id = state.agent_id
state.agent_id = save_as
self._file_storage.make_dir(f"agents/{save_as}")
async def save_state(self, save_as_id: Optional[str] = None) -> None:
"""Save the agent's data and state."""
if save_as_id:
self._file_storage.make_dir(f"agents/{save_as_id}")
# Save state
await self._file_storage.write_file(
f"agents/{save_as}/{self.STATE_FILE}", state.json()
f"agents/{save_as_id}/{self.STATE_FILE}",
self.agent_state.model_dump_json(),
)
# Copy workspace
self._file_storage.copy(
f"agents/{temp_id}/workspace",
f"agents/{save_as}/workspace",
self.config.workspace_path,
f"agents/{save_as_id}/workspace",
)
state.agent_id = temp_id
else:
await self.files.write_file(self.files.root / self.STATE_FILE, state.json())
def change_agent_id(self, new_id: str):
"""Change the agent's ID and update the file storage accordingly."""
state: BaseAgentSettings = getattr(self, "state")
# Rename the agent's files and workspace
self._file_storage.rename(f"agents/{state.agent_id}", f"agents/{new_id}")
# Update the file storage objects
self.files = self._file_storage.clone_with_subroot(f"agents/{new_id}/")
self.workspace = self._file_storage.clone_with_subroot(
f"agents/{new_id}/workspace"
)
state.agent_id = new_id
await self.storage.write_file(
self.storage.root / self.STATE_FILE, self.agent_state.model_dump_json()
)
def get_resources(self) -> Iterator[str]:
yield "The ability to read and write files."

View File

@@ -2,9 +2,11 @@ import os
from pathlib import Path
import pytest
from forge.agent.base import BaseAgentSettings
from forge.file_storage import FileStorage
from autogpt.agents.agent import Agent
from . import FileManagerComponent
@pytest.fixture()
@@ -13,8 +15,13 @@ def file_content():
@pytest.fixture
def file_manager_component(agent: Agent):
return agent.file_manager
def file_manager_component(storage: FileStorage):
return FileManagerComponent(
storage,
BaseAgentSettings(
agent_id="TestAgent", name="TestAgent", description="Test Agent description"
),
)
@pytest.fixture()
@@ -41,15 +48,14 @@ def test_nested_file(storage: FileStorage):
async def test_read_file(
test_file_path: Path,
file_content,
file_manager_component,
agent: Agent,
file_manager_component: FileManagerComponent,
):
await agent.file_manager.workspace.write_file(test_file_path.name, file_content)
await file_manager_component.workspace.write_file(test_file_path.name, file_content)
content = file_manager_component.read_file(test_file_path.name)
assert content.replace("\r", "") == file_content
def test_read_file_not_found(file_manager_component):
def test_read_file_not_found(file_manager_component: FileManagerComponent):
filename = "does_not_exist.txt"
with pytest.raises(FileNotFoundError):
file_manager_component.read_file(filename)
@@ -57,12 +63,12 @@ def test_read_file_not_found(file_manager_component):
@pytest.mark.asyncio
async def test_write_to_file_relative_path(
test_file_name: Path, file_manager_component, agent: Agent
test_file_name: Path, file_manager_component: FileManagerComponent
):
new_content = "This is new content.\n"
await file_manager_component.write_to_file(test_file_name, new_content)
with open(
agent.file_manager.workspace.get_path(test_file_name), "r", encoding="utf-8"
file_manager_component.workspace.get_path(test_file_name), "r", encoding="utf-8"
) as f:
content = f.read()
assert content == new_content
@@ -70,7 +76,7 @@ async def test_write_to_file_relative_path(
@pytest.mark.asyncio
async def test_write_to_file_absolute_path(
test_file_path: Path, file_manager_component
test_file_path: Path, file_manager_component: FileManagerComponent
):
new_content = "This is new content.\n"
await file_manager_component.write_to_file(test_file_path, new_content)
@@ -80,18 +86,18 @@ async def test_write_to_file_absolute_path(
@pytest.mark.asyncio
async def test_list_files(file_manager_component, agent: Agent):
async def test_list_files(file_manager_component: FileManagerComponent):
# Create files A and B
file_a_name = "file_a.txt"
file_b_name = "file_b.txt"
test_directory = Path("test_directory")
await agent.file_manager.workspace.write_file(file_a_name, "This is file A.")
await agent.file_manager.workspace.write_file(file_b_name, "This is file B.")
await file_manager_component.workspace.write_file(file_a_name, "This is file A.")
await file_manager_component.workspace.write_file(file_b_name, "This is file B.")
# Create a subdirectory and place a copy of file_a in it
agent.file_manager.workspace.make_dir(test_directory)
await agent.file_manager.workspace.write_file(
file_manager_component.workspace.make_dir(test_directory)
await file_manager_component.workspace.write_file(
test_directory / file_a_name, "This is file A in the subdirectory."
)
@@ -101,10 +107,10 @@ async def test_list_files(file_manager_component, agent: Agent):
assert os.path.join(test_directory, file_a_name) in files
# Clean up
agent.file_manager.workspace.delete_file(file_a_name)
agent.file_manager.workspace.delete_file(file_b_name)
agent.file_manager.workspace.delete_file(test_directory / file_a_name)
agent.file_manager.workspace.delete_dir(test_directory)
file_manager_component.workspace.delete_file(file_a_name)
file_manager_component.workspace.delete_file(file_b_name)
file_manager_component.workspace.delete_file(test_directory / file_a_name)
file_manager_component.workspace.delete_dir(test_directory)
# Case 2: Search for a file that does not exist and make sure we don't throw
non_existent_file = "non_existent_file.txt"

View File

@@ -1,23 +1,36 @@
from pathlib import Path
from typing import Iterator
from typing import Iterator, Optional
from git.repo import Repo
from pydantic import BaseModel, SecretStr
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import CommandProvider
from forge.command import Command, command
from forge.config.config import Config
from forge.models.config import UserConfigurable
from forge.models.json_schema import JSONSchema
from forge.utils.exceptions import CommandExecutionError
from forge.utils.url_validator import validate_url
class GitOperationsComponent(CommandProvider):
class GitOperationsConfiguration(BaseModel):
github_username: Optional[str] = UserConfigurable(None, from_env="GITHUB_USERNAME")
github_api_key: Optional[SecretStr] = UserConfigurable(
None, from_env="GITHUB_API_KEY", exclude=True
)
class GitOperationsComponent(
CommandProvider, ConfigurableComponent[GitOperationsConfiguration]
):
"""Provides commands to perform Git operations."""
def __init__(self, config: Config):
self._enabled = bool(config.github_username and config.github_api_key)
config_class = GitOperationsConfiguration
def __init__(self, config: Optional[GitOperationsConfiguration] = None):
ConfigurableComponent.__init__(self, config)
self._enabled = bool(self.config.github_username and self.config.github_api_key)
self._disabled_reason = "Configure github_username and github_api_key."
self.legacy_config = config
def get_commands(self) -> Iterator[Command]:
yield self.clone_repository
@@ -48,9 +61,13 @@ class GitOperationsComponent(CommandProvider):
str: The result of the clone operation.
"""
split_url = url.split("//")
auth_repo_url = (
f"//{self.legacy_config.github_username}:"
f"{self.legacy_config.github_api_key}@".join(split_url)
api_key = (
self.config.github_api_key.get_secret_value()
if self.config.github_api_key
else None
)
auth_repo_url = f"//{self.config.github_username}:" f"{api_key}@".join(
split_url
)
try:
Repo.clone_from(url=auth_repo_url, to_path=clone_path)

View File

@@ -1,11 +1,11 @@
import pytest
from forge.components.git_operations import GitOperationsComponent
from forge.file_storage.base import FileStorage
from forge.utils.exceptions import CommandExecutionError
from git.exc import GitCommandError
from git.repo.base import Repo
from autogpt.agents.agent import Agent
from forge.file_storage.base import FileStorage
from forge.utils.exceptions import CommandExecutionError
from . import GitOperationsComponent
@pytest.fixture
@@ -14,15 +14,14 @@ def mock_clone_from(mocker):
@pytest.fixture
def git_ops_component(agent: Agent):
return agent.git_ops
def git_ops_component():
return GitOperationsComponent()
def test_clone_auto_gpt_repository(
git_ops_component: GitOperationsComponent,
storage: FileStorage,
mock_clone_from,
agent: Agent,
):
mock_clone_from.return_value = None
@@ -37,7 +36,7 @@ def test_clone_auto_gpt_repository(
assert clone_result == expected_output
mock_clone_from.assert_called_once_with(
url=f"{scheme}{agent.legacy_config.github_username}:{agent.legacy_config.github_api_key}@{repo}", # noqa: E501
url=f"{scheme}{git_ops_component.config.github_username}:{git_ops_component.config.github_api_key}@{repo}", # noqa: E501
to_path=clone_path,
)
@@ -46,7 +45,6 @@ def test_clone_repository_error(
git_ops_component: GitOperationsComponent,
storage: FileStorage,
mock_clone_from,
agent: Agent,
):
url = "https://github.com/this-repository/does-not-exist.git"
clone_path = storage.get_path("does-not-exist")

View File

@@ -1,5 +1,3 @@
"""Commands to generate images based on text input"""
import io
import json
import logging
@@ -7,35 +5,61 @@ import time
import uuid
from base64 import b64decode
from pathlib import Path
from typing import Iterator
from typing import Iterator, Literal, Optional
import requests
from openai import OpenAI
from PIL import Image
from pydantic import BaseModel, SecretStr
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import CommandProvider
from forge.command import Command, command
from forge.config.config import Config
from forge.file_storage import FileStorage
from forge.llm.providers.openai import OpenAICredentials
from forge.models.config import UserConfigurable
from forge.models.json_schema import JSONSchema
logger = logging.getLogger(__name__)
class ImageGeneratorComponent(CommandProvider):
class ImageGeneratorConfiguration(BaseModel):
image_provider: Literal["dalle", "huggingface", "sdwebui"] = "dalle"
huggingface_image_model: str = "CompVis/stable-diffusion-v1-4"
huggingface_api_token: Optional[SecretStr] = UserConfigurable(
None, from_env="HUGGINGFACE_API_TOKEN", exclude=True
)
sd_webui_url: str = "http://localhost:7860"
sd_webui_auth: Optional[SecretStr] = UserConfigurable(
None, from_env="SD_WEBUI_AUTH", exclude=True
)
class ImageGeneratorComponent(
CommandProvider, ConfigurableComponent[ImageGeneratorConfiguration]
):
"""A component that provides commands to generate images from text prompts."""
def __init__(self, workspace: FileStorage, config: Config):
self._enabled = bool(config.image_provider)
config_class = ImageGeneratorConfiguration
def __init__(
self,
workspace: FileStorage,
config: Optional[ImageGeneratorConfiguration] = None,
openai_credentials: Optional[OpenAICredentials] = None,
):
"""openai_credentials only needed for `dalle` provider."""
ConfigurableComponent.__init__(self, config)
self.openai_credentials = openai_credentials
self._enabled = bool(self.config.image_provider)
self._disabled_reason = "No image provider set."
self.workspace = workspace
self.legacy_config = config
def get_commands(self) -> Iterator[Command]:
if (
self.legacy_config.openai_credentials
or self.legacy_config.huggingface_api_token
or self.legacy_config.sd_webui_auth
self.openai_credentials
or self.config.huggingface_api_token
or self.config.sd_webui_auth
):
yield self.generate_image
@@ -48,7 +72,7 @@ class ImageGeneratorComponent(CommandProvider):
),
"size": JSONSchema(
type=JSONSchema.Type.INTEGER,
description="The size of the image",
description="The size of the image [256, 512, 1024]",
required=False,
),
},
@@ -65,22 +89,21 @@ class ImageGeneratorComponent(CommandProvider):
str: The filename of the image
"""
filename = self.workspace.root / f"{str(uuid.uuid4())}.jpg"
cfg = self.legacy_config
if cfg.openai_credentials and (
cfg.image_provider == "dalle"
or not (cfg.huggingface_api_token or cfg.sd_webui_url)
if self.openai_credentials and (
self.config.image_provider == "dalle"
or not (self.config.huggingface_api_token or self.config.sd_webui_url)
):
return self.generate_image_with_dalle(prompt, filename, size)
elif cfg.huggingface_api_token and (
cfg.image_provider == "huggingface"
or not (cfg.openai_credentials or cfg.sd_webui_url)
elif self.config.huggingface_api_token and (
self.config.image_provider == "huggingface"
or not (self.openai_credentials or self.config.sd_webui_url)
):
return self.generate_image_with_hf(prompt, filename)
elif cfg.sd_webui_url and (
cfg.image_provider == "sdwebui" or cfg.sd_webui_auth
elif self.config.sd_webui_url and (
self.config.image_provider == "sdwebui" or self.config.sd_webui_auth
):
return self.generate_image_with_sd_webui(prompt, filename, size)
@@ -96,13 +119,15 @@ class ImageGeneratorComponent(CommandProvider):
Returns:
str: The filename of the image
"""
API_URL = f"https://api-inference.huggingface.co/models/{self.legacy_config.huggingface_image_model}" # noqa: E501
if self.legacy_config.huggingface_api_token is None:
API_URL = f"https://api-inference.huggingface.co/models/{self.config.huggingface_image_model}" # noqa: E501
if self.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 {self.legacy_config.huggingface_api_token}",
"Authorization": (
f"Bearer {self.config.huggingface_api_token.get_secret_value()}"
),
"X-Use-Cache": "false",
}
@@ -156,7 +181,7 @@ class ImageGeneratorComponent(CommandProvider):
Returns:
str: The filename of the image
"""
assert self.legacy_config.openai_credentials # otherwise this tool is disabled
assert self.openai_credentials # otherwise this tool is disabled
# Check for supported image sizes
if size not in [256, 512, 1024]:
@@ -169,7 +194,10 @@ class ImageGeneratorComponent(CommandProvider):
# TODO: integrate in `forge.llm.providers`(?)
response = OpenAI(
api_key=self.legacy_config.openai_credentials.api_key.get_secret_value()
api_key=self.openai_credentials.api_key.get_secret_value(),
organization=self.openai_credentials.organization.get_secret_value()
if self.openai_credentials.organization
else None,
).images.generate(
prompt=prompt,
n=1,
@@ -208,13 +236,13 @@ class ImageGeneratorComponent(CommandProvider):
"""
# Create a session and set the basic auth if needed
s = requests.Session()
if self.legacy_config.sd_webui_auth:
username, password = self.legacy_config.sd_webui_auth.split(":")
if self.config.sd_webui_auth:
username, password = self.config.sd_webui_auth.get_secret_value().split(":")
s.auth = (username, password or "")
# Generate the images
response = requests.post(
f"{self.legacy_config.sd_webui_url}/sdapi/v1/txt2img",
f"{self.config.sd_webui_url}/sdapi/v1/txt2img",
json={
"prompt": prompt,
"negative_prompt": negative_prompt,

View File

@@ -4,15 +4,33 @@ from pathlib import Path
from unittest.mock import patch
import pytest
from forge.components.image_gen import ImageGeneratorComponent
from PIL import Image
from pydantic import SecretStr, ValidationError
from autogpt.agents.agent import Agent
from forge.components.image_gen import ImageGeneratorComponent
from forge.components.image_gen.image_gen import ImageGeneratorConfiguration
from forge.file_storage.base import FileStorage
from forge.llm.providers.openai import OpenAICredentials
@pytest.fixture
def image_gen_component(agent: Agent):
return agent.image_gen
def image_gen_component(storage: FileStorage):
try:
cred = OpenAICredentials.from_env()
except ValidationError:
cred = OpenAICredentials(api_key=SecretStr("test"))
return ImageGeneratorComponent(storage, openai_credentials=cred)
@pytest.fixture
def huggingface_image_gen_component(storage: FileStorage):
config = ImageGeneratorConfiguration(
image_provider="huggingface",
huggingface_api_token=SecretStr("1"),
huggingface_image_model="CompVis/stable-diffusion-v1-4",
)
return ImageGeneratorComponent(storage, config=config)
@pytest.fixture(params=[256, 512, 1024])
@@ -21,20 +39,14 @@ def image_size(request):
return request.param
@pytest.mark.requires_openai_api_key
@pytest.mark.vcr
def test_dalle(
image_gen_component: ImageGeneratorComponent,
agent: Agent,
storage,
image_size,
cached_openai_client,
):
"""Test DALL-E image generation."""
generate_and_validate(
image_gen_component,
agent,
storage,
image_provider="dalle",
image_size=image_size,
)
@@ -44,23 +56,18 @@ def test_dalle(
reason="The image is too big to be put in a cassette for a CI pipeline. "
"We're looking into a solution."
)
@pytest.mark.requires_huggingface_api_key
@pytest.mark.parametrize(
"image_model",
["CompVis/stable-diffusion-v1-4", "stabilityai/stable-diffusion-2-1"],
)
def test_huggingface(
image_gen_component: ImageGeneratorComponent,
agent: Agent,
storage,
image_size,
image_model,
):
"""Test HuggingFace image generation."""
generate_and_validate(
image_gen_component,
agent,
storage,
image_provider="huggingface",
image_size=image_size,
hugging_face_image_model=image_model,
@@ -68,14 +75,10 @@ def test_huggingface(
@pytest.mark.xfail(reason="SD WebUI call does not work.")
def test_sd_webui(
image_gen_component: ImageGeneratorComponent, agent: Agent, storage, image_size
):
def test_sd_webui(image_gen_component: ImageGeneratorComponent, image_size):
"""Test SD WebUI image generation."""
generate_and_validate(
image_gen_component,
agent,
storage,
image_provider="sd_webui",
image_size=image_size,
)
@@ -83,7 +86,7 @@ def test_sd_webui(
@pytest.mark.xfail(reason="SD WebUI call does not work.")
def test_sd_webui_negative_prompt(
image_gen_component: ImageGeneratorComponent, storage, image_size
image_gen_component: ImageGeneratorComponent, image_size
):
gen_image = functools.partial(
image_gen_component.generate_image_with_sd_webui,
@@ -114,17 +117,15 @@ def lst(txt):
def generate_and_validate(
image_gen_component: ImageGeneratorComponent,
agent: Agent,
storage,
image_size,
image_provider,
hugging_face_image_model=None,
**kwargs,
):
"""Generate an image and validate the output."""
agent.legacy_config.image_provider = image_provider
image_gen_component.config.image_provider = image_provider
if hugging_face_image_model:
agent.legacy_config.huggingface_image_model = hugging_face_image_model
image_gen_component.config.huggingface_image_model = hugging_face_image_model
prompt = "astronaut riding a horse"
image_path = lst(image_gen_component.generate_image(prompt, image_size, **kwargs))
@@ -149,9 +150,7 @@ def generate_and_validate(
)
@pytest.mark.parametrize("delay", [10, 0])
def test_huggingface_fail_request_with_delay(
image_gen_component: ImageGeneratorComponent,
agent: Agent,
storage,
huggingface_image_gen_component: ImageGeneratorComponent,
image_size,
image_model,
return_text,
@@ -173,14 +172,12 @@ def test_huggingface_fail_request_with_delay(
mock_post.return_value.ok = False
mock_post.return_value.text = return_text
agent.legacy_config.image_provider = "huggingface"
agent.legacy_config.huggingface_api_token = "mock-api-key"
agent.legacy_config.huggingface_image_model = image_model
huggingface_image_gen_component.config.huggingface_image_model = image_model
prompt = "astronaut riding a horse"
with patch("time.sleep") as mock_sleep:
# Verify request fails.
result = image_gen_component.generate_image(prompt, image_size)
result = huggingface_image_gen_component.generate_image(prompt, image_size)
assert result == "Error creating image."
# Verify retry was called with delay if delay is in return_text
@@ -191,10 +188,8 @@ def test_huggingface_fail_request_with_delay(
def test_huggingface_fail_request_no_delay(
mocker, image_gen_component: ImageGeneratorComponent, agent: Agent
mocker, huggingface_image_gen_component: ImageGeneratorComponent
):
agent.legacy_config.huggingface_api_token = "1"
# Mock requests.post
mock_post = mocker.patch("requests.post")
mock_post.return_value.status_code = 500
@@ -206,10 +201,9 @@ def test_huggingface_fail_request_no_delay(
# Mock time.sleep
mock_sleep = mocker.patch("time.sleep")
agent.legacy_config.image_provider = "huggingface"
agent.legacy_config.huggingface_image_model = "CompVis/stable-diffusion-v1-4"
result = image_gen_component.generate_image("astronaut riding a horse", 512)
result = huggingface_image_gen_component.generate_image(
"astronaut riding a horse", 512
)
assert result == "Error creating image."
@@ -218,10 +212,8 @@ def test_huggingface_fail_request_no_delay(
def test_huggingface_fail_request_bad_json(
mocker, image_gen_component: ImageGeneratorComponent, agent: Agent
mocker, huggingface_image_gen_component: ImageGeneratorComponent
):
agent.legacy_config.huggingface_api_token = "1"
# Mock requests.post
mock_post = mocker.patch("requests.post")
mock_post.return_value.status_code = 500
@@ -231,10 +223,9 @@ def test_huggingface_fail_request_bad_json(
# Mock time.sleep
mock_sleep = mocker.patch("time.sleep")
agent.legacy_config.image_provider = "huggingface"
agent.legacy_config.huggingface_image_model = "CompVis/stable-diffusion-v1-4"
result = image_gen_component.generate_image("astronaut riding a horse", 512)
result = huggingface_image_gen_component.generate_image(
"astronaut riding a horse", 512
)
assert result == "Error creating image."
@@ -243,17 +234,14 @@ def test_huggingface_fail_request_bad_json(
def test_huggingface_fail_request_bad_image(
mocker, image_gen_component: ImageGeneratorComponent, agent: Agent
mocker, huggingface_image_gen_component: ImageGeneratorComponent
):
agent.legacy_config.huggingface_api_token = "1"
# Mock requests.post
mock_post = mocker.patch("requests.post")
mock_post.return_value.status_code = 200
agent.legacy_config.image_provider = "huggingface"
agent.legacy_config.huggingface_image_model = "CompVis/stable-diffusion-v1-4"
result = image_gen_component.generate_image("astronaut riding a horse", 512)
result = huggingface_image_gen_component.generate_image(
"astronaut riding a horse", 512
)
assert result == "Error creating image."

View File

@@ -4,7 +4,6 @@ import click
from forge.agent.protocols import CommandProvider
from forge.command import Command, command
from forge.config.config import Config
from forge.models.json_schema import JSONSchema
from forge.utils.const import ASK_COMMAND
@@ -12,9 +11,6 @@ from forge.utils.const import ASK_COMMAND
class UserInteractionComponent(CommandProvider):
"""Provides commands to interact with the user."""
def __init__(self, config: Config):
self._enabled = not config.noninteractive_mode
def get_commands(self) -> Iterator[Command]:
yield self.ask_user

View File

@@ -22,7 +22,7 @@ class WatchdogComponent(AfterParse[AnyProposal]):
def __init__(
self,
config: "BaseAgentConfiguration",
config: BaseAgentConfiguration,
event_history: EpisodicActionHistory[AnyProposal],
):
self.config = config

View File

@@ -1,30 +1,44 @@
import json
import logging
import time
from typing import Iterator
from typing import Iterator, Optional
from duckduckgo_search import DDGS
from pydantic import BaseModel, SecretStr
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import CommandProvider, DirectiveProvider
from forge.command import Command, command
from forge.config.config import Config
from forge.models.config import UserConfigurable
from forge.models.json_schema import JSONSchema
from forge.utils.exceptions import ConfigurationError
DUCKDUCKGO_MAX_ATTEMPTS = 3
logger = logging.getLogger(__name__)
class WebSearchComponent(DirectiveProvider, CommandProvider):
class WebSearchConfiguration(BaseModel):
google_api_key: Optional[SecretStr] = UserConfigurable(
None, from_env="GOOGLE_API_KEY", exclude=True
)
google_custom_search_engine_id: Optional[SecretStr] = UserConfigurable(
None, from_env="GOOGLE_CUSTOM_SEARCH_ENGINE_ID", exclude=True
)
duckduckgo_max_attempts: int = 3
class WebSearchComponent(
DirectiveProvider, CommandProvider, ConfigurableComponent[WebSearchConfiguration]
):
"""Provides commands to search the web."""
def __init__(self, config: Config):
self.legacy_config = config
config_class = WebSearchConfiguration
def __init__(self, config: Optional[WebSearchConfiguration] = None):
ConfigurableComponent.__init__(self, config)
if (
not self.legacy_config.google_api_key
or not self.legacy_config.google_custom_search_engine_id
not self.config.google_api_key
or not self.config.google_custom_search_engine_id
):
logger.info(
"Configure google_api_key and custom_search_engine_id "
@@ -37,10 +51,7 @@ class WebSearchComponent(DirectiveProvider, CommandProvider):
def get_commands(self) -> Iterator[Command]:
yield self.web_search
if (
self.legacy_config.google_api_key
and self.legacy_config.google_custom_search_engine_id
):
if self.config.google_api_key and self.config.google_custom_search_engine_id:
yield self.google
@command(
@@ -74,7 +85,7 @@ class WebSearchComponent(DirectiveProvider, CommandProvider):
search_results = []
attempts = 0
while attempts < DUCKDUCKGO_MAX_ATTEMPTS:
while attempts < self.config.duckduckgo_max_attempts:
if not query:
return json.dumps(search_results)
@@ -136,17 +147,25 @@ class WebSearchComponent(DirectiveProvider, CommandProvider):
from googleapiclient.errors import HttpError
try:
# Get the Google API key and Custom Search Engine ID from the config file
api_key = self.legacy_config.google_api_key
custom_search_engine_id = self.legacy_config.google_custom_search_engine_id
# Should be the case if this command is enabled:
assert self.config.google_api_key
assert self.config.google_custom_search_engine_id
# Initialize the Custom Search API service
service = build("customsearch", "v1", developerKey=api_key)
service = build(
"customsearch",
"v1",
developerKey=self.config.google_api_key.get_secret_value(),
)
# Send the search query and retrieve the results
result = (
service.cse()
.list(q=query, cx=custom_search_engine_id, num=num_results)
.list(
q=query,
cx=self.config.google_custom_search_engine_id.get_secret_value(),
num=num_results,
)
.execute()
)
@@ -154,7 +173,7 @@ class WebSearchComponent(DirectiveProvider, CommandProvider):
search_results = result.get("items", [])
# Create a list of only the URLs from the search results
search_results_links = [item["link"] for item in search_results]
search_results_links = [item["link"] for item in search_results] # type: ignore # noqa
except HttpError as e:
# Handle errors in the API call

View File

@@ -3,10 +3,11 @@ import logging
import re
from pathlib import Path
from sys import platform
from typing import Iterator, Type
from typing import Iterator, Literal, Optional, Type
from urllib.request import urlretrieve
from bs4 import BeautifulSoup
from pydantic import BaseModel
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.chrome.service import Service as ChromeDriverService
@@ -27,12 +28,14 @@ from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from webdriver_manager.microsoft import EdgeChromiumDriverManager as EdgeDriverManager
from forge.agent.components import ConfigurableComponent
from forge.agent.protocols import CommandProvider, DirectiveProvider
from forge.command import Command, command
from forge.config.config import Config
from forge.content_processing.html import extract_hyperlinks, format_hyperlinks
from forge.content_processing.text import extract_information, summarize_text
from forge.llm.providers import ChatModelInfo, MultiProvider
from forge.llm.providers import MultiProvider
from forge.llm.providers.multi import ModelName
from forge.llm.providers.openai import OpenAIModelName
from forge.models.json_schema import JSONSchema
from forge.utils.exceptions import CommandExecutionError, TooMuchOutputError
from forge.utils.url_validator import validate_url
@@ -51,18 +54,38 @@ class BrowsingError(CommandExecutionError):
"""An error occurred while trying to browse the page"""
class WebSeleniumComponent(DirectiveProvider, CommandProvider):
class WebSeleniumConfiguration(BaseModel):
model_name: ModelName = OpenAIModelName.GPT3
"""Name of the llm model used to read websites"""
web_browser: Literal["chrome", "firefox", "safari", "edge"] = "chrome"
"""Web browser used by Selenium"""
headless: bool = True
"""Run browser in headless mode"""
user_agent: str = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"
)
"""User agent used by the browser"""
browse_spacy_language_model: str = "en_core_web_sm"
"""Spacy language model used for chunking text"""
class WebSeleniumComponent(
DirectiveProvider, CommandProvider, ConfigurableComponent[WebSeleniumConfiguration]
):
"""Provides commands to browse the web using Selenium."""
config_class = WebSeleniumConfiguration
def __init__(
self,
config: Config,
llm_provider: MultiProvider,
model_info: ChatModelInfo,
data_dir: Path,
config: Optional[WebSeleniumConfiguration] = None,
):
self.legacy_config = config
ConfigurableComponent.__init__(self, config)
self.llm_provider = llm_provider
self.model_info = model_info
self.data_dir = data_dir
def get_resources(self) -> Iterator[str]:
yield "Ability to read websites."
@@ -129,7 +152,7 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
"""
driver = None
try:
driver = await self.open_page_in_browser(url, self.legacy_config)
driver = await self.open_page_in_browser(url)
text = self.scrape_text_with_selenium(driver)
links = self.scrape_links_with_selenium(driver, url)
@@ -141,7 +164,7 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
elif get_raw_content:
if (
output_tokens := self.llm_provider.count_tokens(
text, self.model_info.name
text, self.config.model_name
)
) > MAX_RAW_CONTENT_LENGTH:
oversize_factor = round(output_tokens / MAX_RAW_CONTENT_LENGTH, 1)
@@ -228,7 +251,7 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
return format_hyperlinks(hyperlinks)
async def open_page_in_browser(self, url: str, config: Config) -> WebDriver:
async def open_page_in_browser(self, url: str) -> WebDriver:
"""Open a browser window and load a web page using Selenium
Params:
@@ -248,11 +271,11 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
"safari": SafariOptions,
}
options: BrowserOptions = options_available[config.selenium_web_browser]()
options.add_argument(f"user-agent={config.user_agent}")
options: BrowserOptions = options_available[self.config.web_browser]()
options.add_argument(f"user-agent={self.config.user_agent}")
if isinstance(options, FirefoxOptions):
if config.selenium_headless:
if self.config.headless:
options.headless = True # type: ignore
options.add_argument("--disable-gpu")
driver = FirefoxDriver(
@@ -274,13 +297,11 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
options.add_argument("--remote-debugging-port=9222")
options.add_argument("--no-sandbox")
if config.selenium_headless:
if self.config.headless:
options.add_argument("--headless=new")
options.add_argument("--disable-gpu")
self._sideload_chrome_extensions(
options, config.app_data_dir / "assets" / "crx"
)
self._sideload_chrome_extensions(options, self.data_dir / "assets" / "crx")
if (chromium_driver_path := Path("/usr/bin/chromedriver")).exists():
chrome_service = ChromeDriverService(str(chromium_driver_path))
@@ -361,7 +382,8 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
text,
topics_of_interest=topics_of_interest,
llm_provider=self.llm_provider,
config=self.legacy_config,
model_name=self.config.model_name,
spacy_model=self.config.browse_spacy_language_model,
)
return "\n".join(f"* {i}" for i in information)
else:
@@ -369,6 +391,7 @@ class WebSeleniumComponent(DirectiveProvider, CommandProvider):
text,
question=question,
llm_provider=self.llm_provider,
config=self.legacy_config,
model_name=self.config.model_name,
spacy_model=self.config.browse_spacy_language_model,
)
return result

View File

@@ -1,16 +1,23 @@
import json
import pytest
from forge.components.web.search import WebSearchComponent
from forge.utils.exceptions import ConfigurationError
from googleapiclient.errors import HttpError
from httplib2 import Response
from pydantic import SecretStr
from autogpt.agents.agent import Agent
from forge.utils.exceptions import ConfigurationError
from . import WebSearchComponent
@pytest.fixture
def web_search_component(agent: Agent):
return agent.web_search
def web_search_component():
component = WebSearchComponent()
if component.config.google_api_key is None:
component.config.google_api_key = SecretStr("test")
if component.config.google_custom_search_engine_id is None:
component.config.google_custom_search_engine_id = SecretStr("test")
return component
@pytest.mark.parametrize(
@@ -131,16 +138,11 @@ def test_google_official_search_errors(
error_msg,
web_search_component: WebSearchComponent,
):
class resp:
def __init__(self, _status, _reason):
self.status = _status
self.reason = _reason
response_content = {
"error": {"code": http_code, "message": error_msg, "reason": "backendError"}
}
error = HttpError(
resp=resp(http_code, error_msg),
resp=Response({"status": http_code, "reason": error_msg}),
content=str.encode(json.dumps(response_content)),
uri="https://www.googleapis.com/customsearch/v1?q=invalid+query&cx",
)

View File

@@ -1,19 +1,20 @@
import pytest
from forge.components.web.selenium import BrowsingError, WebSeleniumComponent
from pathlib import Path
from autogpt.agents.agent import Agent
import pytest
from forge.llm.providers.multi import MultiProvider
from . import BrowsingError, WebSeleniumComponent
@pytest.fixture
def web_selenium_component(agent: Agent):
return agent.web_selenium
def web_selenium_component(app_data_dir: Path):
return WebSeleniumComponent(MultiProvider(), app_data_dir)
@pytest.mark.vcr
@pytest.mark.requires_openai_api_key
@pytest.mark.asyncio
async def test_browse_website_nonexistent_url(
web_selenium_component: WebSeleniumComponent, cached_openai_client: None
web_selenium_component: WebSeleniumComponent,
):
url = "https://auto-gpt-thinks-this-website-does-not-exist.com"
question = "How to execute a barrel roll"

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