1
0
forked from clan/clan-core

clana: init

This commit is contained in:
DavHau 2024-03-08 14:40:55 +07:00
parent e6b494a849
commit 3d4b7902e6
9 changed files with 279 additions and 20 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
export OPENAI_API_KEY=$(rbw get openai-api-key)

1
.env.template Normal file
View File

@ -0,0 +1 @@
export OPENAI_API_KEY=$(rbw get openai-api-key)

4
.envrc
View File

@ -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

View File

@ -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)

View 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,
)

View 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}",
},
]

View 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.
}

View 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
];
};
};
}

View File

@ -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)