feat: add psagentai agent with deployment and configuration

Initial project setup including the main agent script, configuration
files, profile JSON state, systemd service unit, startup script,
deployment documentation, and Python dependencies.

Includes .env, venv/, and __pycache__/ which should likely be
excluded via .gitignore to avoid committing secrets and artifacts.
This commit is contained in:
2026-10-25 17:05:13 -03:00
commit 6b3b6541a7
3792 changed files with 805563 additions and 0 deletions
@@ -0,0 +1 @@
from ._main import register_commands as register_commands
@@ -0,0 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from argparse import ArgumentParser
from . import migrate, fine_tunes
if TYPE_CHECKING:
from argparse import _SubParsersAction
def register_commands(parser: ArgumentParser, subparser: _SubParsersAction[ArgumentParser]) -> None:
migrate.register(subparser)
namespaced = parser.add_subparsers(title="Tools", help="Convenience client side tools")
fine_tunes.register(namespaced)
@@ -0,0 +1,63 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from argparse import ArgumentParser
from .._models import BaseModel
from ...lib._validators import (
get_validators,
write_out_file,
read_any_format,
apply_validators,
apply_necessary_remediation,
)
if TYPE_CHECKING:
from argparse import _SubParsersAction
def register(subparser: _SubParsersAction[ArgumentParser]) -> None:
sub = subparser.add_parser("fine_tunes.prepare_data")
sub.add_argument(
"-f",
"--file",
required=True,
help="JSONL, JSON, CSV, TSV, TXT or XLSX file containing prompt-completion examples to be analyzed."
"This should be the local file path.",
)
sub.add_argument(
"-q",
"--quiet",
required=False,
action="store_true",
help="Auto accepts all suggestions, without asking for user input. To be used within scripts.",
)
sub.set_defaults(func=prepare_data, args_model=PrepareDataArgs)
class PrepareDataArgs(BaseModel):
file: str
quiet: bool
def prepare_data(args: PrepareDataArgs) -> None:
sys.stdout.write("Analyzing...\n")
fname = args.file
auto_accept = args.quiet
df, remediation = read_any_format(fname)
apply_necessary_remediation(None, remediation)
validators = get_validators()
assert df is not None
apply_validators(
df,
fname,
remediation,
validators,
auto_accept,
write_out_file_func=write_out_file,
)
@@ -0,0 +1,181 @@
from __future__ import annotations
import os
import sys
import json
import shutil
import tarfile
import platform
import subprocess
from typing import TYPE_CHECKING, List
from pathlib import Path
from argparse import ArgumentParser
import httpx
from .._errors import CLIError, SilentCLIError
from .._models import BaseModel
if TYPE_CHECKING:
from argparse import _SubParsersAction
def register(subparser: _SubParsersAction[ArgumentParser]) -> None:
sub = subparser.add_parser("migrate")
sub.set_defaults(func=migrate, args_model=MigrateArgs, allow_unknown_args=True)
sub = subparser.add_parser("grit")
sub.set_defaults(func=grit, args_model=GritArgs, allow_unknown_args=True)
class GritArgs(BaseModel):
# internal
unknown_args: List[str] = []
def grit(args: GritArgs) -> None:
grit_path = install()
try:
subprocess.check_call([grit_path, *args.unknown_args])
except subprocess.CalledProcessError:
# stdout and stderr are forwarded by subprocess so an error will already
# have been displayed
raise SilentCLIError() from None
class MigrateArgs(BaseModel):
# internal
unknown_args: List[str] = []
def migrate(args: MigrateArgs) -> None:
grit_path = install()
try:
subprocess.check_call([grit_path, "apply", "openai", *args.unknown_args])
except subprocess.CalledProcessError:
# stdout and stderr are forwarded by subprocess so an error will already
# have been displayed
raise SilentCLIError() from None
# handles downloading the Grit CLI until they provide their own PyPi package
KEYGEN_ACCOUNT = "custodian-dev"
def _cache_dir() -> Path:
xdg = os.environ.get("XDG_CACHE_HOME")
if xdg is not None:
return Path(xdg)
return Path.home() / ".cache"
def _debug(message: str) -> None:
if not os.environ.get("DEBUG"):
return
sys.stdout.write(f"[DEBUG]: {message}\n")
def install() -> Path:
"""Installs the Grit CLI and returns the location of the binary"""
if sys.platform == "win32":
raise CLIError("Windows is not supported yet in the migration CLI")
platform = "macos" if sys.platform == "darwin" else "linux"
dir_name = _cache_dir() / "openai-python"
install_dir = dir_name / ".install"
target_dir = install_dir / "bin"
target_path = target_dir / "marzano"
temp_file = target_dir / "marzano.tmp"
if target_path.exists():
_debug(f"{target_path} already exists")
sys.stdout.flush()
return target_path
_debug(f"Using Grit CLI path: {target_path}")
target_dir.mkdir(parents=True, exist_ok=True)
if temp_file.exists():
temp_file.unlink()
arch = _get_arch()
_debug(f"Using architecture {arch}")
file_name = f"marzano-{platform}-{arch}"
meta_url = f"https://api.keygen.sh/v1/accounts/{KEYGEN_ACCOUNT}/artifacts/{file_name}"
sys.stdout.write(f"Retrieving Grit CLI metadata from {meta_url}\n")
with httpx.Client() as client:
response = client.get(meta_url) # pyright: ignore[reportUnknownMemberType]
data = response.json()
errors = data.get("errors")
if errors:
for error in errors:
sys.stdout.write(f"{error}\n")
raise CLIError("Could not locate Grit CLI binary - see above errors")
write_manifest(install_dir, data["data"]["relationships"]["release"]["data"]["id"])
link = data["data"]["links"]["redirect"]
_debug(f"Redirect URL {link}")
download_response = client.get(link) # pyright: ignore[reportUnknownMemberType]
with open(temp_file, "wb") as file:
for chunk in download_response.iter_bytes():
file.write(chunk)
unpacked_dir = target_dir / "cli-bin"
unpacked_dir.mkdir(parents=True, exist_ok=True)
with tarfile.open(temp_file, "r:gz") as archive:
archive.extractall(unpacked_dir)
for item in unpacked_dir.iterdir():
item.rename(target_dir / item.name)
shutil.rmtree(unpacked_dir)
os.remove(temp_file)
os.chmod(target_path, 0o755)
sys.stdout.flush()
return target_path
def _get_arch() -> str:
architecture = platform.machine().lower()
# Map the architecture names to Node.js equivalents
arch_map = {
"x86_64": "x64",
"amd64": "x64",
"armv7l": "arm",
"aarch64": "arm64",
}
return arch_map.get(architecture, architecture)
def write_manifest(install_path: Path, release: str) -> None:
manifest = {
"installPath": str(install_path),
"binaries": {
"marzano": {
"name": "marzano",
"release": release,
},
},
}
manifest_path = Path(install_path) / "manifests.json"
with open(manifest_path, "w") as f:
json.dump(manifest, f, indent=2)