pygent 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygent/__init__.py +1 -1
- pygent/agent.py +7 -4
- pygent/cli.py +9 -2
- pygent/openai_compat.py +71 -0
- pygent/runtime.py +60 -27
- pygent/tools.py +1 -1
- pygent-0.1.2.dist-info/METADATA +19 -0
- pygent-0.1.2.dist-info/RECORD +13 -0
- pygent-0.1.0.dist-info/METADATA +0 -15
- pygent-0.1.0.dist-info/RECORD +0 -12
- {pygent-0.1.0.dist-info → pygent-0.1.2.dist-info}/WHEEL +0 -0
- {pygent-0.1.0.dist-info → pygent-0.1.2.dist-info}/entry_points.txt +0 -0
- {pygent-0.1.0.dist-info → pygent-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {pygent-0.1.0.dist-info → pygent-0.1.2.dist-info}/top_level.txt +0 -0
pygent/__init__.py
CHANGED
pygent/agent.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""Orchestration layer: receives messages, calls the OpenAI-compatible backend and dispatches tools."""
|
2
2
|
|
3
3
|
import json
|
4
4
|
import os
|
@@ -8,7 +8,10 @@ import time
|
|
8
8
|
from dataclasses import dataclass, field
|
9
9
|
from typing import Any, Dict, List
|
10
10
|
|
11
|
-
|
11
|
+
try:
|
12
|
+
import openai # type: ignore
|
13
|
+
except ModuleNotFoundError: # pragma: no cover - fallback to bundled client
|
14
|
+
from . import openai_compat as openai
|
12
15
|
from rich.console import Console
|
13
16
|
from rich.panel import Panel
|
14
17
|
|
@@ -55,8 +58,8 @@ class Agent:
|
|
55
58
|
console.print(assistant_msg.content)
|
56
59
|
|
57
60
|
|
58
|
-
def run_interactive() -> None: # pragma: no cover
|
59
|
-
agent = Agent()
|
61
|
+
def run_interactive(use_docker: bool | None = None) -> None: # pragma: no cover
|
62
|
+
agent = Agent(runtime=Runtime(use_docker=use_docker))
|
60
63
|
console.print("[bold green]Pygent[/] iniciado. (digite /exit para sair)")
|
61
64
|
try:
|
62
65
|
while True:
|
pygent/cli.py
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
-
"""
|
1
|
+
"""Command-line entry point for Pygent."""
|
2
|
+
import argparse
|
3
|
+
|
2
4
|
from .agent import run_interactive
|
3
5
|
|
4
6
|
def main() -> None: # pragma: no cover
|
5
|
-
|
7
|
+
parser = argparse.ArgumentParser(prog="pygent")
|
8
|
+
parser.add_argument("--docker", dest="use_docker", action="store_true", help="run commands in a Docker container")
|
9
|
+
parser.add_argument("--no-docker", dest="use_docker", action="store_false", help="run locally")
|
10
|
+
parser.set_defaults(use_docker=None)
|
11
|
+
args = parser.parse_args()
|
12
|
+
run_interactive(use_docker=args.use_docker)
|
pygent/openai_compat.py
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Any, Dict, List
|
5
|
+
from urllib import request
|
6
|
+
|
7
|
+
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
8
|
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class ToolCallFunction:
|
12
|
+
name: str
|
13
|
+
arguments: str
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class ToolCall:
|
17
|
+
id: str
|
18
|
+
type: str
|
19
|
+
function: ToolCallFunction
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class Message:
|
23
|
+
role: str
|
24
|
+
content: str | None = None
|
25
|
+
tool_calls: List[ToolCall] | None = None
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class Choice:
|
29
|
+
message: Message
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class ChatCompletion:
|
33
|
+
choices: List[Choice]
|
34
|
+
|
35
|
+
|
36
|
+
def _post(path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
37
|
+
data = json.dumps(payload).encode()
|
38
|
+
headers = {"Content-Type": "application/json"}
|
39
|
+
if OPENAI_API_KEY:
|
40
|
+
headers["Authorization"] = f"Bearer {OPENAI_API_KEY}"
|
41
|
+
req = request.Request(f"{OPENAI_BASE_URL}{path}", data=data, headers=headers)
|
42
|
+
with request.urlopen(req) as resp:
|
43
|
+
return json.loads(resp.read().decode())
|
44
|
+
|
45
|
+
|
46
|
+
class _ChatCompletions:
|
47
|
+
def create(self, model: str, messages: List[Dict[str, Any]], tools: Any = None, tool_choice: str | None = "auto") -> ChatCompletion:
|
48
|
+
payload: Dict[str, Any] = {"model": model, "messages": messages}
|
49
|
+
if tools is not None:
|
50
|
+
payload["tools"] = tools
|
51
|
+
if tool_choice is not None:
|
52
|
+
payload["tool_choice"] = tool_choice
|
53
|
+
raw = _post("/chat/completions", payload)
|
54
|
+
choices: List[Choice] = []
|
55
|
+
for ch in raw.get("choices", []):
|
56
|
+
msg_data = ch.get("message", {})
|
57
|
+
tool_calls = []
|
58
|
+
for tc in msg_data.get("tool_calls", []):
|
59
|
+
func = ToolCallFunction(**tc.get("function", {}))
|
60
|
+
tool_calls.append(ToolCall(id=tc.get("id", ""), type=tc.get("type", ""), function=func))
|
61
|
+
msg = Message(role=msg_data.get("role", ""), content=msg_data.get("content"), tool_calls=tool_calls or None)
|
62
|
+
choices.append(Choice(message=msg))
|
63
|
+
return ChatCompletion(choices=choices)
|
64
|
+
|
65
|
+
|
66
|
+
class _Chat:
|
67
|
+
def __init__(self) -> None:
|
68
|
+
self.completions = _ChatCompletions()
|
69
|
+
|
70
|
+
|
71
|
+
chat = _Chat()
|
pygent/runtime.py
CHANGED
@@ -1,43 +1,75 @@
|
|
1
|
-
"""
|
1
|
+
"""Run commands in a Docker container, falling back to local execution if needed."""
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
import os
|
5
5
|
import shutil
|
6
|
-
import tempfile
|
7
6
|
import subprocess
|
7
|
+
import tempfile
|
8
8
|
import uuid
|
9
9
|
from pathlib import Path
|
10
10
|
from typing import Union
|
11
11
|
|
12
|
-
|
12
|
+
try: # Docker may not be available (e.g. Windows without Docker)
|
13
|
+
import docker # type: ignore
|
14
|
+
except Exception: # pragma: no cover - optional dependency
|
15
|
+
docker = None
|
13
16
|
|
14
17
|
|
15
18
|
class Runtime:
|
16
|
-
"""
|
19
|
+
"""Executes commands in a Docker container or locally if Docker is unavailable."""
|
17
20
|
|
18
|
-
def __init__(self, image: str | None = None) -> None:
|
21
|
+
def __init__(self, image: str | None = None, use_docker: bool | None = None) -> None:
|
19
22
|
self.base_dir = Path(tempfile.mkdtemp(prefix="pygent_"))
|
20
23
|
self.image = image or os.getenv("PYGENT_IMAGE", "python:3.12-slim")
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
24
|
+
env_opt = os.getenv("PYGENT_USE_DOCKER")
|
25
|
+
if use_docker is None:
|
26
|
+
use_docker = (env_opt != "0") if env_opt is not None else True
|
27
|
+
self._use_docker = bool(docker) and use_docker
|
28
|
+
if self._use_docker:
|
29
|
+
try:
|
30
|
+
self.client = docker.from_env()
|
31
|
+
self.container = self.client.containers.run(
|
32
|
+
self.image,
|
33
|
+
name=f"pygent-{uuid.uuid4().hex[:8]}",
|
34
|
+
command="sleep infinity",
|
35
|
+
volumes={str(self.base_dir): {"bind": "/workspace", "mode": "rw"}},
|
36
|
+
working_dir="/workspace",
|
37
|
+
detach=True,
|
38
|
+
tty=True,
|
39
|
+
network_disabled=True,
|
40
|
+
mem_limit="512m",
|
41
|
+
pids_limit=256,
|
42
|
+
)
|
43
|
+
except Exception:
|
44
|
+
self._use_docker = False
|
45
|
+
if not self._use_docker:
|
46
|
+
self.client = None
|
47
|
+
self.container = None
|
34
48
|
|
35
49
|
# ---------------- public API ----------------
|
36
50
|
def bash(self, cmd: str, timeout: int = 30) -> str:
|
37
|
-
"""
|
38
|
-
|
39
|
-
|
40
|
-
|
51
|
+
"""Run a command in the container or locally and return the output."""
|
52
|
+
if self._use_docker and self.container is not None:
|
53
|
+
res = self.container.exec_run(
|
54
|
+
cmd,
|
55
|
+
workdir="/workspace",
|
56
|
+
demux=True,
|
57
|
+
tty=False,
|
58
|
+
timeout=timeout,
|
59
|
+
)
|
60
|
+
stdout, stderr = (
|
61
|
+
res.output if isinstance(res.output, tuple) else (res.output, b"")
|
62
|
+
)
|
63
|
+
return (stdout or b"").decode() + (stderr or b"").decode()
|
64
|
+
proc = subprocess.run(
|
65
|
+
cmd,
|
66
|
+
shell=True,
|
67
|
+
cwd=self.base_dir,
|
68
|
+
capture_output=True,
|
69
|
+
text=True,
|
70
|
+
timeout=timeout,
|
71
|
+
)
|
72
|
+
return proc.stdout + proc.stderr
|
41
73
|
|
42
74
|
def write_file(self, rel_path: Union[str, Path], content: str) -> str:
|
43
75
|
p = self.base_dir / rel_path
|
@@ -46,8 +78,9 @@ class Runtime:
|
|
46
78
|
return f"Wrote {p.relative_to(self.base_dir)}"
|
47
79
|
|
48
80
|
def cleanup(self) -> None:
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
81
|
+
if self._use_docker and self.container is not None:
|
82
|
+
try:
|
83
|
+
self.container.kill()
|
84
|
+
finally:
|
85
|
+
self.container.remove(force=True)
|
86
|
+
shutil.rmtree(self.base_dir, ignore_errors=True)
|
pygent/tools.py
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pygent
|
3
|
+
Version: 0.1.2
|
4
|
+
Summary: Pygent is a minimalist coding assistant that runs commands in a Docker container when available and falls back to local execution. See https://marianochaves.github.io/pygent for documentation and https://github.com/marianochaves/pygent for the source code.
|
5
|
+
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
|
+
Project-URL: Documentation, https://marianochaves.github.io/pygent
|
7
|
+
Project-URL: Repository, https://github.com/marianochaves/pygent
|
8
|
+
Requires-Python: >=3.9
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Dist: rich>=13.7.0
|
11
|
+
Provides-Extra: llm
|
12
|
+
Requires-Dist: openai>=1.0.0; extra == "llm"
|
13
|
+
Provides-Extra: test
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
15
|
+
Provides-Extra: docs
|
16
|
+
Requires-Dist: mkdocs; extra == "docs"
|
17
|
+
Provides-Extra: docker
|
18
|
+
Requires-Dist: docker>=7.0.0; extra == "docker"
|
19
|
+
Dynamic: license-file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
pygent/__init__.py,sha256=3YOE3tjTGEc987Vz-TqmYwQ4ogLwTmue642Enf4NVBg,360
|
2
|
+
pygent/agent.py,sha256=s80JFgtDZ4tNEroh0qIJ0A_8QAiYyORD82R6KsM6vEo,2235
|
3
|
+
pygent/cli.py,sha256=Hz2FZeNMVhxoT5DjCqphXla3TisGJtPEz921LEcpxrA,527
|
4
|
+
pygent/openai_compat.py,sha256=mS6ntl70jpVH3JzfNYEDhg-z7QIQcMqQTuEV5ja7VOo,2173
|
5
|
+
pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
|
6
|
+
pygent/runtime.py,sha256=33y4jieNeyZ-9nxtVlOmO236u2fDAAv2GaaEWTQDdm8,3173
|
7
|
+
pygent/tools.py,sha256=Ru2_voFgPUVc6YgBTRVByn7vWTxXAXT-loAWFMkXHno,1460
|
8
|
+
pygent-0.1.2.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
|
9
|
+
pygent-0.1.2.dist-info/METADATA,sha256=utdsiVHvR3fMjqGeNvBNC4oMrD0Ytav6CYyrgAyjnLM,857
|
10
|
+
pygent-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
pygent-0.1.2.dist-info/entry_points.txt,sha256=ivw-s2f1abmFsbL4173DP1IuMS7sNxQ6gZuDLdu_jKQ,43
|
12
|
+
pygent-0.1.2.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
|
13
|
+
pygent-0.1.2.dist-info/RECORD,,
|
pygent-0.1.0.dist-info/METADATA
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: pygent
|
3
|
-
Version: 0.1.0
|
4
|
-
Summary: A minimal agentic coding assistant that runs each task in a secure container.
|
5
|
-
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
|
-
Requires-Python: >=3.9
|
7
|
-
License-File: LICENSE
|
8
|
-
Requires-Dist: openai>=1.0.0
|
9
|
-
Requires-Dist: docker>=7.0.0
|
10
|
-
Requires-Dist: rich>=13.7.0
|
11
|
-
Provides-Extra: test
|
12
|
-
Requires-Dist: pytest; extra == "test"
|
13
|
-
Provides-Extra: docs
|
14
|
-
Requires-Dist: mkdocs; extra == "docs"
|
15
|
-
Dynamic: license-file
|
pygent-0.1.0.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
pygent/__init__.py,sha256=25N0WIIaSg0e3XXoDGl_OSBN2ADl-8cHm0C0EOR_opw,359
|
2
|
-
pygent/agent.py,sha256=vOV0Zx3JzeRtJb-QRdBr-DzFIPVIt2bcZb8xeHlOxI0,2003
|
3
|
-
pygent/cli.py,sha256=qOuIHAtt--NFbPRl-RuGSK7mfFj-4CtU4rRr6KYPgKc,139
|
4
|
-
pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
|
5
|
-
pygent/runtime.py,sha256=jaLhAT-2Wo6DxmPksFyHvVaCNOOu_c7BgrENsIuwTlI,1897
|
6
|
-
pygent/tools.py,sha256=1mXaPHFtZwT9w8thDeneH-Ryd9CjViiWOeDE1BtRF6I,1471
|
7
|
-
pygent-0.1.0.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
|
8
|
-
pygent-0.1.0.dist-info/METADATA,sha256=Rjva3PKq9hN83Piv8w7ppyNZpgzOnWOjGOYXh7BI7v4,468
|
9
|
-
pygent-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
-
pygent-0.1.0.dist-info/entry_points.txt,sha256=ivw-s2f1abmFsbL4173DP1IuMS7sNxQ6gZuDLdu_jKQ,43
|
11
|
-
pygent-0.1.0.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
|
12
|
-
pygent-0.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|