mirror of
https://github.com/aljazceru/gpt-engineer.git
synced 2025-12-17 12:45:26 +01:00
First step in making gpt-engineer learn. Rename main_prompt -> prompt (#381)
* First step in collecting learnings * Rename prompts * remove requirements, use pip install -e . instead * Add requirements * Fix tests
This commit is contained in:
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Pytest Execution
|
name: Pip install and pytest
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -4,6 +4,7 @@
|
|||||||
[](https://github.com/AntonOsika/gpt-engineer)
|
[](https://github.com/AntonOsika/gpt-engineer)
|
||||||
[](https://twitter.com/AntonOsika)
|
[](https://twitter.com/AntonOsika)
|
||||||
|
|
||||||
|
|
||||||
**Specify what you want it to build, the AI asks for clarification, and then builds it.**
|
**Specify what you want it to build, the AI asks for clarification, and then builds it.**
|
||||||
|
|
||||||
GPT Engineer is made to be easy to adapt, extend, and make your agent learn how you want your code to look. It generates an entire codebase based on a prompt.
|
GPT Engineer is made to be easy to adapt, extend, and make your agent learn how you want your code to look. It generates an entire codebase based on a prompt.
|
||||||
@@ -40,34 +41,43 @@ With an api key that has GPT4 access run:
|
|||||||
|
|
||||||
- `export OPENAI_API_KEY=[your api key]`
|
- `export OPENAI_API_KEY=[your api key]`
|
||||||
|
|
||||||
|
|
||||||
**Run**:
|
**Run**:
|
||||||
|
|
||||||
- Create an empty folder. If inside the repo, you can run:
|
- Create an empty folder. If inside the repo, you can run:
|
||||||
- `cp -r projects/example/ projects/my-new-project`
|
- `cp -r projects/example/ projects/my-new-project`
|
||||||
- Fill in the `main_prompt` file in your new folder
|
- Fill in the `prompt` file in your new folder
|
||||||
- `gpt-engineer projects/my-new-project`
|
- `gpt-engineer projects/my-new-project`
|
||||||
- (Note, `gpt-engineer --help` lets you see all available options. For example `--steps use_feedback` lets you improve/fix code in a project)
|
- (Note, `gpt-engineer --help` lets you see all available options. For example `--steps use_feedback` lets you improve/fix code in a project)
|
||||||
|
|
||||||
**Results**:
|
By running gpt-engineer you agree to our [ToS](https://github.com/AntonOsika/gpt-engineer/TERMS_OF_USE.md).
|
||||||
|
|
||||||
|
**Results**
|
||||||
- Check the generated files in `projects/my-new-project/workspace`
|
- Check the generated files in `projects/my-new-project/workspace`
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
You can specify the "identity" of the AI agent by editing the files in the `identity` folder.
|
You can specify the "identity" of the AI agent by editing the files in the `preprompts` folder.
|
||||||
|
|
||||||
Editing the identity, and evolving the `main_prompt`, is currently how you make the agent remember things between projects.
|
Editing the `preprompts`, and evolving how you write the project prompt, is currently how you make the agent remember things between projects.
|
||||||
|
|
||||||
Each step in `steps.py` will have its communication history with GPT4 stored in the logs folder, and can be rerun with `scripts/rerun_edited_message_logs.py`.
|
Each step in `steps.py` will have its communication history with GPT4 stored in the logs folder, and can be rerun with `scripts/rerun_edited_message_logs.py`.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
The gpt-engineer community is building the **open platform for devs to tinker with and build their personal code-generation toolbox**.
|
||||||
|
|
||||||
We are building the open platform for devs to tinker with and build their personal code-generation toolbox.
|
If you are interested in contributing to this, we would be interested in having you!
|
||||||
|
|
||||||
If you want to contribute, please check out the [roadmap](https://github.com/AntonOsika/gpt-engineer/blob/main/ROADMAP.md), [projects](https://github.com/AntonOsika/gpt-engineer/projects?query=is%3Aopen) or [issues tab](https://github.com/AntonOsika/gpt-engineer/issues) in the GitHub repo. You are welcome to read the [contributing document](.github/CONTRIBUTING.md) and join our [Discord 💬](https://discord.gg/4t5vXHhu).
|
You can check for good first issues [here](https://github.com/AntonOsika/gpt-engineer/issues).
|
||||||
|
Contributing document [here](.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
We are currently looking for more maintainers and community organisers. Email <anton.osika@gmail.com> if you are interested in an official role.
|
We are currently looking for more maintainers and community organisers. Email anton.osika@gmail.com if you are interested in an official role.
|
||||||
|
|
||||||
|
If you want to see our broader ambitions, check out the [roadmap](https://github.com/AntonOsika/gpt-engineer/blob/main/ROADMAP.md), and join
|
||||||
|
[discord ](https://discord.gg/4t5vXHhu)
|
||||||
|
to get input on how you can contribute to it.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<https://github.com/AntonOsika/gpt-engineer/assets/4467025/6e362e45-4a94-4b0d-973d-393a31d92d9b>
|
https://github.com/AntonOsika/gpt-engineer/assets/4467025/6e362e45-4a94-4b0d-973d-393a31d92d9b
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Develop a weather app using Python and a weather API. Display current weather conditions for a given location, including temperature, humidity, and weather description.
|
|
||||||
85
gpt_engineer/collect.py
Normal file
85
gpt_engineer/collect.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dataclasses_json import dataclass_json
|
||||||
|
|
||||||
|
from gpt_engineer import steps
|
||||||
|
from gpt_engineer.db import DBs
|
||||||
|
from gpt_engineer.steps import Step
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass_json
|
||||||
|
@dataclass
|
||||||
|
class Learning:
|
||||||
|
model: str
|
||||||
|
temperature: float
|
||||||
|
steps: str
|
||||||
|
steps_file_hash: str
|
||||||
|
prompt: str
|
||||||
|
feedback: str | None
|
||||||
|
session: str
|
||||||
|
version: str = "0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def steps_file_hash():
|
||||||
|
with open(steps.__file__, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
return hashlib.sha256(content.encode("utf-8"), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_learning(
|
||||||
|
model: str, temperature: float, steps: list[Step], dbs: DBs
|
||||||
|
) -> Learning:
|
||||||
|
learning = Learning(
|
||||||
|
prompt=dbs.input["prompt"],
|
||||||
|
model=model,
|
||||||
|
temperature=temperature,
|
||||||
|
steps=json.dumps([step.__name__ for step in steps]),
|
||||||
|
steps_file_hash=steps_file_hash(),
|
||||||
|
feedback=dbs.input.get("feedback"),
|
||||||
|
session=get_session(),
|
||||||
|
)
|
||||||
|
return learning
|
||||||
|
|
||||||
|
|
||||||
|
def send_learnings(learning: Learning):
|
||||||
|
import rudderstack.analytics as rudder_analytics
|
||||||
|
|
||||||
|
rudder_analytics.write_key = "2Re4kqwL61GDp7S8ewe6K5dbogG"
|
||||||
|
rudder_analytics.dataPlaneUrl = "https://gptengineerezm.dataplane.rudderstack.com"
|
||||||
|
|
||||||
|
rudder_analytics.track(
|
||||||
|
user_id=learning.session,
|
||||||
|
event="learning",
|
||||||
|
properties=learning.to_dict(), # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
path = Path(tempfile.gettempdir()) / "gpt_engineer_user_id.txt"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if path.exists():
|
||||||
|
user_id = path.read_text()
|
||||||
|
else:
|
||||||
|
# random uuid:
|
||||||
|
user_id = str(random.randint(0, 2**32))
|
||||||
|
path.write_text(user_id)
|
||||||
|
return user_id
|
||||||
|
except IOError:
|
||||||
|
return "ephemeral_" + str(random.randint(0, 2**32))
|
||||||
|
|
||||||
|
|
||||||
|
def collect_learnings(model: str, temperature: float, steps: list[Step], dbs: DBs):
|
||||||
|
if os.environ.get("COLLECT_LEARNINGS_OPT_OUT") in ["true", "1"]:
|
||||||
|
print("COLLECT_LEARNINGS_OPT_OUT is set to true, not collecting learning")
|
||||||
|
return
|
||||||
|
|
||||||
|
learnings = extract_learning(model, temperature, steps, dbs)
|
||||||
|
send_learnings(learnings)
|
||||||
@@ -22,6 +22,12 @@ class DB:
|
|||||||
with full_path.open("r", encoding="utf-8") as f:
|
with full_path.open("r", encoding="utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
def __setitem__(self, key, val):
|
||||||
full_path = self.path / key
|
full_path = self.path / key
|
||||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import typer
|
|||||||
|
|
||||||
from gpt_engineer import steps
|
from gpt_engineer import steps
|
||||||
from gpt_engineer.ai import AI
|
from gpt_engineer.ai import AI
|
||||||
|
from gpt_engineer.collect import collect_learnings
|
||||||
from gpt_engineer.db import DB, DBs
|
from gpt_engineer.db import DB, DBs
|
||||||
from gpt_engineer.steps import STEPS
|
from gpt_engineer.steps import STEPS
|
||||||
|
|
||||||
@@ -56,10 +57,13 @@ def main(
|
|||||||
preprompts=DB(Path(__file__).parent / "preprompts"),
|
preprompts=DB(Path(__file__).parent / "preprompts"),
|
||||||
)
|
)
|
||||||
|
|
||||||
for step in STEPS[steps_config]:
|
steps = STEPS[steps_config]
|
||||||
|
for step in steps:
|
||||||
messages = step(ai, dbs)
|
messages = step(ai, dbs)
|
||||||
dbs.logs[step.__name__] = json.dumps(messages)
|
dbs.logs[step.__name__] = json.dumps(messages)
|
||||||
|
|
||||||
|
collect_learnings(model, temperature, steps, dbs)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import subprocess
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Callable, List, TypeVar
|
from typing import Callable, List, TypeVar
|
||||||
|
|
||||||
|
from termcolor import colored
|
||||||
|
|
||||||
from gpt_engineer.ai import AI
|
from gpt_engineer.ai import AI
|
||||||
from gpt_engineer.chat_to_files import to_files
|
from gpt_engineer.chat_to_files import to_files
|
||||||
from gpt_engineer.db import DBs
|
from gpt_engineer.db import DBs
|
||||||
@@ -19,12 +21,24 @@ def setup_sys_prompt(dbs):
|
|||||||
Step = TypeVar("Step", bound=Callable[[AI, DBs], List[dict]])
|
Step = TypeVar("Step", bound=Callable[[AI, DBs], List[dict]])
|
||||||
|
|
||||||
|
|
||||||
|
def get_prompt(dbs):
|
||||||
|
"""While we migrate we have this fallback getter"""
|
||||||
|
assert (
|
||||||
|
"prompt" in dbs.input or "main_prompt" in dbs.input
|
||||||
|
), "Please put your prompt in the file `prompt` in the project directory"
|
||||||
|
|
||||||
|
if "prompt" not in dbs.input:
|
||||||
|
print(
|
||||||
|
colored("Please put the prompt in the file `prompt`, not `main_prompt", "red")
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
|
||||||
|
return dbs.input.get("prompt", dbs.input["main_prompt"])
|
||||||
|
|
||||||
|
|
||||||
def simple_gen(ai: AI, dbs: DBs):
|
def simple_gen(ai: AI, dbs: DBs):
|
||||||
"""Run the AI on the main prompt and save the results"""
|
"""Run the AI on the main prompt and save the results"""
|
||||||
messages = ai.start(
|
messages = ai.start(setup_sys_prompt(dbs), get_prompt(dbs))
|
||||||
setup_sys_prompt(dbs),
|
|
||||||
dbs.input["main_prompt"],
|
|
||||||
)
|
|
||||||
to_files(messages[-1]["content"], dbs.workspace)
|
to_files(messages[-1]["content"], dbs.workspace)
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
@@ -34,22 +48,31 @@ def clarify(ai: AI, dbs: DBs):
|
|||||||
Ask the user if they want to clarify anything and save the results to the workspace
|
Ask the user if they want to clarify anything and save the results to the workspace
|
||||||
"""
|
"""
|
||||||
messages = [ai.fsystem(dbs.preprompts["qa"])]
|
messages = [ai.fsystem(dbs.preprompts["qa"])]
|
||||||
user = dbs.input["main_prompt"]
|
user_input = get_prompt(dbs)
|
||||||
while True:
|
while True:
|
||||||
messages = ai.next(messages, user)
|
messages = ai.next(messages, user_input)
|
||||||
|
|
||||||
if messages[-1]["content"].strip().lower().startswith("no"):
|
if messages[-1]["content"].strip().lower().startswith("no"):
|
||||||
print(" Nothing more to clarify.")
|
print("Nothing more to clarify.")
|
||||||
break
|
break
|
||||||
|
|
||||||
print()
|
print()
|
||||||
user = input('(answer in text, or "c" to move on)\n')
|
user_input = input('(answer in text, or "c" to move on)\n')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if not user or user == "c":
|
if not user_input or user_input == "c":
|
||||||
break
|
print("(letting gpt-engineer make its own assumptions)")
|
||||||
|
print()
|
||||||
|
messages = ai.next(
|
||||||
|
messages,
|
||||||
|
ai.fuser(
|
||||||
|
"Make your own assumptions and state them explicitly before starting"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
return messages
|
||||||
|
|
||||||
user += (
|
user_input += (
|
||||||
"\n\n"
|
"\n\n"
|
||||||
"Is anything else unclear? If yes, only answer in the form:\n"
|
"Is anything else unclear? If yes, only answer in the form:\n"
|
||||||
"{remaining unclear areas} remaining questions.\n"
|
"{remaining unclear areas} remaining questions.\n"
|
||||||
@@ -68,7 +91,7 @@ def gen_spec(ai: AI, dbs: DBs):
|
|||||||
"""
|
"""
|
||||||
messages = [
|
messages = [
|
||||||
ai.fsystem(setup_sys_prompt(dbs)),
|
ai.fsystem(setup_sys_prompt(dbs)),
|
||||||
ai.fsystem(f"Instructions: {dbs.input['main_prompt']}"),
|
ai.fsystem(f"Instructions: {dbs.input['prompt']}"),
|
||||||
]
|
]
|
||||||
|
|
||||||
messages = ai.next(messages, dbs.preprompts["spec"])
|
messages = ai.next(messages, dbs.preprompts["spec"])
|
||||||
@@ -105,7 +128,7 @@ def gen_unit_tests(ai: AI, dbs: DBs):
|
|||||||
"""
|
"""
|
||||||
messages = [
|
messages = [
|
||||||
ai.fsystem(setup_sys_prompt(dbs)),
|
ai.fsystem(setup_sys_prompt(dbs)),
|
||||||
ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
|
ai.fuser(f"Instructions: {dbs.input['prompt']}"),
|
||||||
ai.fuser(f"Specification:\n\n{dbs.memory['specification']}"),
|
ai.fuser(f"Specification:\n\n{dbs.memory['specification']}"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -136,7 +159,7 @@ def gen_code(ai: AI, dbs: DBs):
|
|||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
ai.fsystem(setup_sys_prompt(dbs)),
|
ai.fsystem(setup_sys_prompt(dbs)),
|
||||||
ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
|
ai.fuser(f"Instructions: {dbs.input['prompt']}"),
|
||||||
ai.fuser(f"Specification:\n\n{dbs.memory['specification']}"),
|
ai.fuser(f"Specification:\n\n{dbs.memory['specification']}"),
|
||||||
ai.fuser(f"Unit tests:\n\n{dbs.memory['unit_tests']}"),
|
ai.fuser(f"Unit tests:\n\n{dbs.memory['unit_tests']}"),
|
||||||
]
|
]
|
||||||
@@ -200,7 +223,7 @@ def gen_entrypoint(ai, dbs):
|
|||||||
def use_feedback(ai: AI, dbs: DBs):
|
def use_feedback(ai: AI, dbs: DBs):
|
||||||
messages = [
|
messages = [
|
||||||
ai.fsystem(setup_sys_prompt(dbs)),
|
ai.fsystem(setup_sys_prompt(dbs)),
|
||||||
ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
|
ai.fuser(f"Instructions: {dbs.input['prompt']}"),
|
||||||
ai.fassistant(dbs.workspace["all_output.txt"]),
|
ai.fassistant(dbs.workspace["all_output.txt"]),
|
||||||
ai.fsystem(dbs.preprompts["use_feedback"]),
|
ai.fsystem(dbs.preprompts["use_feedback"]),
|
||||||
]
|
]
|
||||||
@@ -213,7 +236,7 @@ def fix_code(ai: AI, dbs: DBs):
|
|||||||
code_output = json.loads(dbs.logs[gen_code.__name__])[-1]["content"]
|
code_output = json.loads(dbs.logs[gen_code.__name__])[-1]["content"]
|
||||||
messages = [
|
messages = [
|
||||||
ai.fsystem(setup_sys_prompt(dbs)),
|
ai.fsystem(setup_sys_prompt(dbs)),
|
||||||
ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
|
ai.fuser(f"Instructions: {dbs.input['prompt']}"),
|
||||||
ai.fuser(code_output),
|
ai.fuser(code_output),
|
||||||
ai.fsystem(dbs.preprompts["fix_code"]),
|
ai.fsystem(dbs.preprompts["fix_code"]),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ dependencies = [
|
|||||||
'ruff == 0.0.272',
|
'ruff == 0.0.272',
|
||||||
'termcolor==2.3.0',
|
'termcolor==2.3.0',
|
||||||
'typer == 0.9.0',
|
'typer == 0.9.0',
|
||||||
|
'requests == 2.28.2',
|
||||||
|
'rudder-sdk-python == 2.0.2',
|
||||||
|
'dataclasses-json == 0.5.7',
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
black==23.3.0
|
|
||||||
mypy==1.3.0
|
|
||||||
openai==0.27.8
|
|
||||||
pre-commit==3.3.3
|
|
||||||
pytest==7.3.1
|
|
||||||
ruff==0.0.272
|
|
||||||
termcolor==2.3.0
|
|
||||||
typer==0.9.0
|
|
||||||
@@ -56,7 +56,7 @@ def main(
|
|||||||
print("process", bench_folder.name, "finished with code", process.returncode)
|
print("process", bench_folder.name, "finished with code", process.returncode)
|
||||||
print("Running it. Original benchmark prompt:")
|
print("Running it. Original benchmark prompt:")
|
||||||
print()
|
print()
|
||||||
with open(bench_folder / "main_prompt") as f:
|
with open(bench_folder / "prompt") as f:
|
||||||
print(f.read())
|
print(f.read())
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def main():
|
|||||||
if benchmark.is_dir():
|
if benchmark.is_dir():
|
||||||
print(f"Cleaning {benchmark}")
|
print(f"Cleaning {benchmark}")
|
||||||
for path in benchmark.iterdir():
|
for path in benchmark.iterdir():
|
||||||
if path.name == "main_prompt":
|
if path.name in ["prompt", "main_prompt"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get filename of Path object
|
# Get filename of Path object
|
||||||
|
|||||||
36
tests/test_collect.py
Normal file
36
tests/test_collect.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import rudderstack.analytics as rudder_analytics
|
||||||
|
|
||||||
|
from gpt_engineer.collect import collect_learnings, extract_learning
|
||||||
|
from gpt_engineer.db import DB, DBs
|
||||||
|
from gpt_engineer.steps import gen_code
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_learnings(monkeypatch):
|
||||||
|
monkeypatch.setattr(os, "environ", {"COLLECT_LEARNINGS_OPT_OUT": "false"})
|
||||||
|
monkeypatch.setattr(rudder_analytics, "track", MagicMock())
|
||||||
|
|
||||||
|
model = "test_model"
|
||||||
|
temperature = 0.5
|
||||||
|
steps = [gen_code]
|
||||||
|
dbs = DBs(DB("/tmp"), DB("/tmp"), DB("/tmp"), DB("/tmp"), DB("/tmp"))
|
||||||
|
dbs.input = {
|
||||||
|
"prompt": "test prompt\n with newlines",
|
||||||
|
"feedback": "test feedback",
|
||||||
|
}
|
||||||
|
dbs.logs = {gen_code.__name__: "test logs"}
|
||||||
|
|
||||||
|
collect_learnings(model, temperature, steps, dbs)
|
||||||
|
|
||||||
|
learnings = extract_learning(model, temperature, steps, dbs)
|
||||||
|
assert rudder_analytics.track.call_count == 1
|
||||||
|
assert rudder_analytics.track.call_args[1]["event"] == "learning"
|
||||||
|
assert rudder_analytics.track.call_args[1]["properties"] == learnings.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main(["-v"])
|
||||||
@@ -44,7 +44,7 @@ def test_DBs_initialization(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_invalid_path():
|
def test_invalid_path():
|
||||||
with pytest.raises(PermissionError):
|
with pytest.raises((PermissionError, OSError)):
|
||||||
# Test with a path that will raise a permission error
|
# Test with a path that will raise a permission error
|
||||||
DB("/root/test")
|
DB("/root/test")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user