Skip to content

Commit 451b050

Browse files
committed
feat(agent): Package manager detection and python version validation
1 parent bc8e20e commit 451b050

File tree

4 files changed

+127
-13
lines changed

4 files changed

+127
-13
lines changed

libertai_client/commands/agent.py

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Annotated
44

55
import aiohttp
6+
import questionary
67
import rich
78
import typer
89
from dotenv import dotenv_values
@@ -12,22 +13,45 @@
1213
from libertai_client.config import config
1314
from libertai_client.interfaces.agent import AgentPythonPackageManager, AgentUsageType
1415
from libertai_client.utils.agent import parse_agent_config_env, create_agent_zip
15-
from libertai_client.utils.python import detect_python_project_version
16+
from libertai_client.utils.python import (
17+
detect_python_project_version,
18+
detect_python_dependencies_management,
19+
validate_python_version,
20+
)
1621
from libertai_client.utils.system import get_full_path
1722
from libertai_client.utils.typer import AsyncTyper
1823

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

2126
err_console = Console(stderr=True)
2227

28+
dependencies_management_choices: list[questionary.Choice] = [
29+
questionary.Choice(
30+
title="poetry",
31+
value=AgentPythonPackageManager.poetry,
32+
description="poetry-style pyproject.toml and poetry.lock",
33+
),
34+
questionary.Choice(
35+
title="requirements.txt",
36+
value=AgentPythonPackageManager.pip,
37+
description="Any management tool that outputs a requirements.txt file (pip, pip-tools...)",
38+
),
39+
questionary.Choice(
40+
title="pyproject.toml",
41+
value="TODO",
42+
description="Any tool respecting the standard PEP 621 pyproject.toml (hatch, modern usage of setuptools...)",
43+
disabled="Coming soon",
44+
),
45+
]
46+
2347

2448
@app.command()
2549
async def deploy(
2650
path: Annotated[str, typer.Argument(help="Path to the root of your project")] = ".",
2751
python_version: Annotated[
2852
str | None, typer.Option(help="Version to deploy with", prompt=False)
2953
] = None,
30-
package_manager: Annotated[
54+
dependencies_management: Annotated[
3155
AgentPythonPackageManager | None,
3256
typer.Option(
3357
help="Package manager used to handle dependencies",
@@ -55,23 +79,50 @@ async def deploy(
5579
err_console.print(f"[red]{error}")
5680
raise typer.Exit(1)
5781

58-
# TODO: try to detect package manager, show detected value and ask user for the confirmation or change
59-
if package_manager is None:
60-
package_manager = AgentPythonPackageManager.poetry
82+
if dependencies_management is None:
83+
# Trying to find the way dependencies are managed
84+
detected_dependencies_management = detect_python_dependencies_management(path)
85+
# Confirming with the user (or asking if none found)
86+
dependencies_management = await questionary.select(
87+
"Dependencies management",
88+
choices=dependencies_management_choices,
89+
default=next(
90+
(
91+
choice
92+
for choice in dependencies_management_choices
93+
if choice.value == detected_dependencies_management.value
94+
),
95+
None,
96+
),
97+
show_description=True,
98+
).ask_async()
99+
if dependencies_management is None:
100+
err_console.print(
101+
"[red]You must select the way Python dependencies are managed."
102+
)
103+
raise typer.Exit(1)
61104

62105
if python_version is None:
63106
# Trying to find the python version
64-
detected_python_version = detect_python_project_version(path, package_manager)
107+
detected_python_version = detect_python_project_version(
108+
path, dependencies_management
109+
)
65110
# Confirming the version with the user (or asking if none found)
66-
python_version = typer.prompt("Python version", default=detected_python_version)
111+
python_version = await questionary.text(
112+
"Python version",
113+
default=detected_python_version
114+
if detected_python_version is not None
115+
else "",
116+
validate=validate_python_version,
117+
).ask_async()
67118

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

71122
data = aiohttp.FormData()
72123
data.add_field("secret", libertai_config.secret)
73124
data.add_field("python_version", python_version)
74-
data.add_field("package_manager", package_manager.value)
125+
data.add_field("package_manager", dependencies_management.value)
75126
data.add_field("usage_type", usage_type.value)
76127
data.add_field("code", open(agent_zip_path, "rb"), filename="libertai-agent.zip")
77128

@@ -83,10 +134,12 @@ async def deploy(
83134
) as response:
84135
if response.status == 200:
85136
response_data = UpdateAgentResponse(**json.loads(await response.text())) # noqa: F821
86-
# TODO: don't show /docs if deployed in python mode
87-
rich.print(
88-
f"[green]Agent successfully deployed on http://[{response_data.instance_ip}]:8000/docs"
137+
success_text = (
138+
f"Agent successfully deployed on http://[{response_data.instance_ip}]:8000/docs"
139+
if usage_type == AgentUsageType.fastapi
140+
else f"Agent successfully deployed on instance {response_data.instance_ip}"
89141
)
142+
rich.print(f"[green]{success_text}")
90143
else:
91144
error_message = await response.text()
92145
err_console.print(f"[red]Request failed\n{error_message}")

libertai_client/utils/python.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
from libertai_client.utils.system import get_full_path
1010

1111

12+
def validate_python_version(version: str) -> bool:
13+
if re.match(r"^3(?:\.\d+){0,2}$", version):
14+
return True
15+
return False
16+
17+
1218
def __fetch_real_python_versions() -> list[str]:
1319
response = requests.get(
1420
"https://api.github.com/repos/python/cpython/tags?per_page=100"
1521
)
1622
if response.status_code == 200:
1723
releases = response.json()
1824
versions = [str(release["name"]).removeprefix("v") for release in releases]
19-
exact_versions = [v for v in versions if re.match(r"^\d+\.\d+\.\d+$", v)]
25+
exact_versions = [v for v in versions if validate_python_version(v)]
2026
return exact_versions
2127
else:
2228
return []
@@ -62,3 +68,18 @@ def detect_python_project_version(
6268
# TODO: if pyproject, look in pyproject.toml
6369
# TODO: if pip, look in requirements.txt
6470
return None
71+
72+
73+
def detect_python_dependencies_management(
74+
project_path: str,
75+
) -> AgentPythonPackageManager:
76+
try:
77+
_poetry_lock_path = get_full_path(project_path, "poetry.lock")
78+
# Path was found without throwing an error, its poetry
79+
return AgentPythonPackageManager.poetry
80+
except FileNotFoundError:
81+
pass
82+
83+
# TODO: confirm with requirements.txt
84+
# TODO: handle pyproject.toml standard compatible package managers
85+
return AgentPythonPackageManager.pip

poetry.lock

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ python-dotenv = "^1.0.1"
2525
libertai-utils = "0.0.9"
2626
pathspec = "^0.12.1"
2727
poetry-core = "^2.0.0"
28+
questionary = "^2.1.0"
2829

2930
[tool.poetry.group.dev.dependencies]
3031
mypy = "^1.11.1"

0 commit comments

Comments
 (0)