Skip to content

Commit

Permalink
feat(agent): Detect python version
Browse files Browse the repository at this point in the history
  • Loading branch information
RezaRahemtola committed Jan 11, 2025
1 parent fae5f75 commit bc8e20e
Show file tree
Hide file tree
Showing 6 changed files with 548 additions and 438 deletions.
80 changes: 28 additions & 52 deletions libertai_client/commands/agent.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,51 @@
import json
import os
import zipfile
from enum import Enum
from typing import Annotated

import aiohttp
import pathspec
import rich
import typer
from dotenv import dotenv_values
from libertai_utils.interfaces.agent import UpdateAgentResponse
from rich.console import Console

from libertai_client.config import config
from libertai_client.utils.agent import parse_agent_config_env
from libertai_client.interfaces.agent import AgentPythonPackageManager, AgentUsageType
from libertai_client.utils.agent import parse_agent_config_env, create_agent_zip
from libertai_client.utils.python import detect_python_project_version
from libertai_client.utils.system import get_full_path
from libertai_client.utils.typer import AsyncTyper

app = AsyncTyper(name="agent", help="Deploy and manage agents")

err_console = Console(stderr=True)

AGENT_ZIP_BLACKLIST = [".git", ".idea", ".vscode"]
AGENT_ZIP_WHITELIST = [".env"]


def create_agent_zip(src_dir: str, zip_name: str):
# Read and parse the .gitignore file
with open(get_full_path(src_dir, ".gitignore"), "r") as gitignore_file:
gitignore_patterns = gitignore_file.read()
spec = pathspec.PathSpec.from_lines(
"gitwildmatch", gitignore_patterns.splitlines() + AGENT_ZIP_BLACKLIST
)

with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(src_dir):
for file in files:
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, src_dir)

# Check if the file matches any .gitignore pattern
if (
not spec.match_file(relative_path)
or relative_path in AGENT_ZIP_WHITELIST
):
zipf.write(file_path, arcname=relative_path)


class AgentPythonPackageManager(str, Enum):
poetry = "poetry"
pip = "pip"


class AgentUsageType(str, Enum):
fastapi = "fastapi"
python = "python"


@app.command()
async def deploy(
path: Annotated[
str, typer.Option(help="Path to the root of your repository", prompt=True)
] = ".",
path: Annotated[str, typer.Argument(help="Path to the root of your project")] = ".",
python_version: Annotated[
str, typer.Option(help="Version to deploy with", prompt=True)
] = "3.11",
str | None, typer.Option(help="Version to deploy with", prompt=False)
] = None,
package_manager: Annotated[
AgentPythonPackageManager, typer.Option(case_sensitive=False, prompt=True)
] = AgentPythonPackageManager.pip.value, # type: ignore
AgentPythonPackageManager | None,
typer.Option(
help="Package manager used to handle dependencies",
case_sensitive=False,
prompt=False,
),
] = None,
usage_type: Annotated[
AgentUsageType, typer.Option(case_sensitive=False, prompt=True)
] = AgentUsageType.fastapi.value, # type: ignore
AgentUsageType,
typer.Option(
help="How the agent is called", case_sensitive=False, prompt=False
),
] = AgentUsageType.fastapi,
):
"""
Deploy or redeploy an agent
"""

# TODO: try to detect package manager, show detected value and ask user for the confirmation or change
# Same for python version

# TODO: allow user to give a custom deployment script URL

try:
Expand All @@ -88,8 +55,17 @@ async def deploy(
err_console.print(f"[red]{error}")
raise typer.Exit(1)

agent_zip_path = "/tmp/libertai-agent.zip"
# TODO: try to detect package manager, show detected value and ask user for the confirmation or change
if package_manager is None:
package_manager = AgentPythonPackageManager.poetry

if python_version is None:
# Trying to find the python version
detected_python_version = detect_python_project_version(path, package_manager)
# Confirming the version with the user (or asking if none found)
python_version = typer.prompt("Python version", default=detected_python_version)

agent_zip_path = "/tmp/libertai-agent.zip"
create_agent_zip(path, agent_zip_path)

data = aiohttp.FormData()
Expand Down
12 changes: 12 additions & 0 deletions libertai_client/interfaces/agent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
from enum import Enum

from pydantic import BaseModel


class AgentConfig(BaseModel):
id: str
secret: str


class AgentPythonPackageManager(str, Enum):
poetry = "poetry"
pip = "pip"


class AgentUsageType(str, Enum):
fastapi = "fastapi"
python = "python"
32 changes: 32 additions & 0 deletions libertai_client/utils/agent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import os
import zipfile

from pathspec import pathspec

from libertai_client.interfaces.agent import AgentConfig
from libertai_client.utils.system import get_full_path


def parse_agent_config_env(env: dict[str, str | None]) -> AgentConfig:
Expand All @@ -11,3 +17,29 @@ def parse_agent_config_env(env: dict[str, str | None]) -> AgentConfig:
)

return AgentConfig(id=agent_id, secret=agent_secret)


AGENT_ZIP_BLACKLIST = [".git", ".idea", ".vscode"]
AGENT_ZIP_WHITELIST = [".env"]


def create_agent_zip(src_dir: str, zip_name: str):
# Read and parse the .gitignore file
with open(get_full_path(src_dir, ".gitignore"), "r") as gitignore_file:
gitignore_patterns = gitignore_file.read()
spec = pathspec.PathSpec.from_lines(
"gitwildmatch", gitignore_patterns.splitlines() + AGENT_ZIP_BLACKLIST
)

with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(src_dir):
for file in files:
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, src_dir)

# Check if the file matches any .gitignore pattern
if (
not spec.match_file(relative_path)
or relative_path in AGENT_ZIP_WHITELIST
):
zipf.write(file_path, arcname=relative_path)
64 changes: 64 additions & 0 deletions libertai_client/utils/python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import re
import tomllib

import requests
from poetry.core.constraints.version import Version
from poetry.core.constraints.version.parser import parse_constraint

from libertai_client.interfaces.agent import AgentPythonPackageManager
from libertai_client.utils.system import get_full_path


def __fetch_real_python_versions() -> list[str]:
response = requests.get(
"https://api.github.com/repos/python/cpython/tags?per_page=100"
)
if response.status_code == 200:
releases = response.json()
versions = [str(release["name"]).removeprefix("v") for release in releases]
exact_versions = [v for v in versions if re.match(r"^\d+\.\d+\.\d+$", v)]
return exact_versions
else:
return []


def detect_python_project_version(
project_path: str,
package_manager: AgentPythonPackageManager,
) -> str | None:
if package_manager == AgentPythonPackageManager.poetry:
pyproject_path = get_full_path(project_path, "pyproject.toml")
with open(pyproject_path, "rb") as file:
pyproject_data = tomllib.load(file)

# The version might be a range, let's try to find an exact version that is in this range
version_range = pyproject_data["tool"]["poetry"]["dependencies"]["python"]
real_python_versions = __fetch_real_python_versions()

constraint = parse_constraint(version_range)
for version in real_python_versions:
if constraint.allows(Version.parse(version)):
return version

# Checking common venv folders config
for venv_folder in ["venv", ".venv"]:
try:
venv_config_path = get_full_path(project_path, f"{venv_folder}/pyvenv.cfg")
with open(venv_config_path, "r") as file:
for line in file:
if line.startswith("version"):
return line.split("=")[1].strip()
except FileNotFoundError:
pass
#
# # Checking if we have a .python-version file, for example created by pyenv
try:
version_file_path = get_full_path(project_path, ".python-version")
with open(version_file_path, "r") as file:
return file.readline().strip()
except FileNotFoundError:
pass

# TODO: if pyproject, look in pyproject.toml
# TODO: if pip, look in requirements.txt
return None
Loading

0 comments on commit bc8e20e

Please sign in to comment.