forked from clan/clan-core
clana: init
This commit is contained in:
parent
e6b494a849
commit
3d4b7902e6
1
.env.template
Normal file
1
.env.template
Normal file
@ -0,0 +1 @@
|
||||
export OPENAI_API_KEY=$(rbw get openai-api-key)
|
4
.envrc
4
.envrc
@ -4,6 +4,10 @@ fi
|
||||
|
||||
watch_file .direnv/selected-shell
|
||||
|
||||
if [ -e .env ]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
if [ -e .direnv/selected-shell ]; then
|
||||
use flake .#$(cat .direnv/selected-shell)
|
||||
else
|
||||
|
@ -6,6 +6,8 @@ from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from clan_cli import clana
|
||||
|
||||
from . import backups, config, facts, flakes, flash, history, machines, secrets, vms
|
||||
from .custom_logger import setup_logging
|
||||
from .dirs import get_clan_flake_toplevel
|
||||
@ -110,6 +112,11 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
||||
)
|
||||
flash.register_parser(parser_flash)
|
||||
|
||||
parser_clana = subparsers.add_parser(
|
||||
"clana", help="Describe a VM with natural language and launch it"
|
||||
)
|
||||
clana.register_parser(parser_clana)
|
||||
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
|
51
pkgs/clan-cli/clan_cli/clan_openai.py
Normal file
51
pkgs/clan-cli/clan_cli/clan_openai.py
Normal file
@ -0,0 +1,51 @@
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
# Your OpenAI API key
|
||||
api_key: str = os.environ["OPENAI_API_KEY"]
|
||||
|
||||
# The URL to which the request is sent
|
||||
url: str = "https://api.openai.com/v1/chat/completions"
|
||||
|
||||
# The header includes the content type and the authorization with your API key
|
||||
headers: dict[str, str] = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
|
||||
|
||||
def complete(
|
||||
messages: list[dict[str, Any]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 1.0,
|
||||
) -> str:
|
||||
# Data to be sent in the request
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
# Create a request object with the URL and the headers
|
||||
req = urllib.request.Request(url, json.dumps(data).encode("utf-8"), headers)
|
||||
|
||||
# Make the request and read the response
|
||||
with urllib.request.urlopen(req) as response:
|
||||
response_body = response.read()
|
||||
resp_data = json.loads(response_body)
|
||||
return resp_data["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
def complete_prompt(
|
||||
prompt: str,
|
||||
system: str = "",
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 1.0,
|
||||
) -> str:
|
||||
return complete(
|
||||
[{"role": "system", "content": system}, {"role": "user", "content": prompt}],
|
||||
model,
|
||||
temperature,
|
||||
)
|
114
pkgs/clan-cli/clan_cli/clana/__init__.py
Normal file
114
pkgs/clan-cli/clan_cli/clana/__init__.py
Normal file
@ -0,0 +1,114 @@
|
||||
# !/usr/bin/env python3
|
||||
# A subcommand that interfaces with openai to generate nixos configurations and launches VMs with them.
|
||||
# The `clan clana` command allows the user to enter a prompt with the wishes for the VM and then generates a nixos configuration and launches a VM with it.
|
||||
# for now this POC should be stateless. A configuration.nix should be generated ina temporary directory and directly launched.
|
||||
# there should be no additional arguments.
|
||||
# THe prompt is read from stdin
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli import clan_openai
|
||||
from clan_cli.errors import ClanCmdError
|
||||
from clan_cli.vms.run import run_command
|
||||
|
||||
base_config = Path(__file__).parent.joinpath("base-config.nix").read_text()
|
||||
|
||||
system_msg = f"""
|
||||
Your name is clana, an assistant for creating NixOS configurations.
|
||||
Your task is to generate a NixOS configuration.nix file.
|
||||
Do not output any explanations or comments, not even when the user asks a question or provides feedback.
|
||||
Always provide only the content of the configuration.nix file.
|
||||
Don't use any nixos options for which you are not sure about their syntax.
|
||||
Generate a configuration.nix which has a very high probability of just working.
|
||||
The user who provides the prompt might have technical expertise, or none at all.
|
||||
Even a grandmother who has no idea about computers should be able to use this.
|
||||
Translate the users requirements to a working configuration.nix file.
|
||||
Don't set any options under `nix.`.
|
||||
The user should not have a password and log in automatically.
|
||||
|
||||
Take care specifically about:
|
||||
- specify every option only once within the same file. Otherwise it will lead to an error like this: error: attribute 'environment.systemPackages' already defined at [...]/configuration.nix:X:X
|
||||
- don't set a password for the user. it's already set in the base config
|
||||
|
||||
|
||||
Assume the following base config is already imported. Any option set in there is already configured and doesn't need to be specified anymore:
|
||||
|
||||
```nix
|
||||
{base_config}
|
||||
```
|
||||
|
||||
The base config will be imported by the system. No need to import it anymore.
|
||||
"""
|
||||
|
||||
|
||||
# takes a (sub)parser and configures it
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--show", action="store_true", help="show the configuration")
|
||||
parser.set_defaults(func=clana_command)
|
||||
|
||||
|
||||
def clana_command(args: argparse.Namespace) -> None:
|
||||
print("Please enter your wishes for the new computer: ")
|
||||
prompt = input()
|
||||
# prompt = "I want to email my grandchildren and watch them on facebook"
|
||||
print("Thank you. Generating your computer...")
|
||||
# config = clan_openai.complete(messages, model="gpt-4-turbo-preview").strip()
|
||||
config = Path(".direnv/configuration.nix").read_text()
|
||||
messages = [
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
conf_dir = Path("/tmp/clana")
|
||||
conf_dir.mkdir(exist_ok=True)
|
||||
for f in conf_dir.iterdir():
|
||||
f.unlink()
|
||||
(conf_dir / "flake.nix").write_bytes(
|
||||
Path(__file__).parent.joinpath("flake.nix.template").read_bytes()
|
||||
)
|
||||
with open(conf_dir / "base-config.nix", "w") as f:
|
||||
f.write(base_config)
|
||||
with open(conf_dir / "hardware-configuration.nix", "w") as f:
|
||||
f.write("{}")
|
||||
with open(conf_dir / "configuration.nix", "w") as f:
|
||||
f.write(
|
||||
"""
|
||||
{
|
||||
imports = [
|
||||
./base-config.nix
|
||||
./ai-config.nix
|
||||
];
|
||||
}
|
||||
"""
|
||||
)
|
||||
while True:
|
||||
config_orig = clan_openai.complete(
|
||||
messages, model="gpt-4-turbo-preview"
|
||||
).strip()
|
||||
# remove code blocks
|
||||
lines = config_orig.split("\n")
|
||||
if lines[0].startswith("```"):
|
||||
lines = lines[1:-1]
|
||||
config = "\n".join(lines)
|
||||
if args.show:
|
||||
print("Configuration generated:")
|
||||
print(config)
|
||||
print("Configuration generated. Launching...")
|
||||
with open(conf_dir / "ai-config.nix", "w") as f:
|
||||
f.write(config)
|
||||
|
||||
os.environ["NIXPKGS_ALLOW_UNFREE"] = "1"
|
||||
try:
|
||||
run_command(
|
||||
machine="clana-machine", flake=conf_dir, nix_options=["--impure"]
|
||||
)
|
||||
break
|
||||
except ClanCmdError as e:
|
||||
messages += [
|
||||
{"role": "assistant", "content": config_orig},
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"There was a problem that needs to be fixed:\n{e.cmd.stderr}",
|
||||
},
|
||||
]
|
60
pkgs/clan-cli/clan_cli/clana/base-config.nix
Normal file
60
pkgs/clan-cli/clan_cli/clana/base-config.nix
Normal file
@ -0,0 +1,60 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[
|
||||
# Include the results of the hardware scan.
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
|
||||
# Ensure that software properties (e.g., being unfree) are respected.
|
||||
nixpkgs.config = {
|
||||
allowUnfree = true;
|
||||
};
|
||||
|
||||
# Use the systemd-boot EFI boot loader.
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
networking.hostName = "clana"; # Define your hostname.
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
# Enable the X11 windowing system.
|
||||
services.xserver.enable = true;
|
||||
services.xserver.layout = "us";
|
||||
services.xserver.xkbOptions = "eurosign:e";
|
||||
|
||||
# Enable touchpad support.
|
||||
services.xserver.libinput.enable = true;
|
||||
|
||||
# Enable the KDE Desktop Environment.
|
||||
services.xserver.displayManager.sddm.enable = true;
|
||||
services.xserver.desktopManager.plasma5.enable = true;
|
||||
|
||||
# Enable sound.
|
||||
sound.enable = true;
|
||||
hardware.pulseaudio.enable = true;
|
||||
|
||||
# Autologin settings.
|
||||
services.xserver.displayManager.autoLogin.enable = true;
|
||||
services.xserver.displayManager.autoLogin.user = "user";
|
||||
|
||||
# User settings.
|
||||
users.users.user = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" ]; # Enable sudo for the user.
|
||||
uid = 1000;
|
||||
password = "hello";
|
||||
openssh.authorizedKeys.keys = [ ];
|
||||
};
|
||||
|
||||
# Enable firewall.
|
||||
networking.firewall.enable = true;
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ]; # HTTP and HTTPS
|
||||
|
||||
# Set time zone.
|
||||
time.timeZone = "UTC";
|
||||
|
||||
# System-wide settings.
|
||||
system.stateVersion = "22.05"; # Edit this to your NixOS release version.
|
||||
}
|
30
pkgs/clan-cli/clan_cli/clana/flake.nix.template
Normal file
30
pkgs/clan-cli/clan_cli/clana/flake.nix.template
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
description = "<Put your description here>";
|
||||
|
||||
inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core";
|
||||
|
||||
outputs = { self, clan-core, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
||||
clan = clan-core.lib.buildClan {
|
||||
directory = self;
|
||||
clanName = "clana-clan";
|
||||
machines.clana-machine = {
|
||||
imports = [
|
||||
./configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# all machines managed by cLAN
|
||||
inherit (clan) nixosConfigurations clanInternals;
|
||||
# add the cLAN cli tool to the dev shell
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
packages = [
|
||||
clan-core.packages.${system}.clan-cli
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
@ -3,7 +3,6 @@ import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
@ -177,28 +176,20 @@ def run_vm(vm: VmConfig, nix_options: list[str] = []) -> None:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunOptions:
|
||||
machine: str
|
||||
flake: Path
|
||||
nix_options: list[str] = field(default_factory=list)
|
||||
waypipe: bool = False
|
||||
def run_command(
|
||||
machine: str, flake: Path, option: list[str] = [], **args: argparse.Namespace
|
||||
) -> None:
|
||||
machine_obj: Machine = Machine(machine, flake)
|
||||
|
||||
vm: VmConfig = inspect_vm(machine=machine_obj)
|
||||
|
||||
run_vm(vm, option)
|
||||
|
||||
|
||||
def run_command(args: argparse.Namespace) -> None:
|
||||
run_options = RunOptions(
|
||||
machine=args.machine,
|
||||
flake=args.flake,
|
||||
nix_options=args.option,
|
||||
)
|
||||
|
||||
machine = Machine(run_options.machine, run_options.flake)
|
||||
|
||||
vm = inspect_vm(machine=machine)
|
||||
|
||||
run_vm(vm, run_options.nix_options)
|
||||
def _run_command(args: argparse.Namespace) -> None:
|
||||
run_command(**args.vars())
|
||||
|
||||
|
||||
def register_run_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("machine", type=str, help="machine in the flake to run")
|
||||
parser.set_defaults(func=run_command)
|
||||
parser.set_defaults(func=_run_command)
|
||||
|
Loading…
Reference in New Issue
Block a user