pygent 0.1.10__py3-none-any.whl → 0.1.11__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 +2 -1
- pygent/agent.py +10 -4
- pygent/errors.py +6 -0
- pygent/models.py +11 -7
- pygent/openai_compat.py +12 -3
- pygent/runtime.py +28 -20
- pygent/tools.py +10 -0
- {pygent-0.1.10.dist-info → pygent-0.1.11.dist-info}/METADATA +1 -1
- pygent-0.1.11.dist-info/RECORD +17 -0
- pygent-0.1.10.dist-info/RECORD +0 -16
- {pygent-0.1.10.dist-info → pygent-0.1.11.dist-info}/WHEEL +0 -0
- {pygent-0.1.10.dist-info → pygent-0.1.11.dist-info}/entry_points.txt +0 -0
- {pygent-0.1.10.dist-info → pygent-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {pygent-0.1.10.dist-info → pygent-0.1.11.dist-info}/top_level.txt +0 -0
pygent/__init__.py
CHANGED
@@ -8,5 +8,6 @@ except _metadata.PackageNotFoundError: # pragma: no cover - fallback for tests
|
|
8
8
|
|
9
9
|
from .agent import Agent, run_interactive # noqa: E402,F401, must come after __version__
|
10
10
|
from .models import Model, OpenAIModel # noqa: E402,F401
|
11
|
+
from .errors import PygentError, APIError # noqa: E402,F401
|
11
12
|
|
12
|
-
__all__ = ["Agent", "run_interactive", "Model", "OpenAIModel"]
|
13
|
+
__all__ = ["Agent", "run_interactive", "Model", "OpenAIModel", "PygentError", "APIError"]
|
pygent/agent.py
CHANGED
@@ -10,6 +10,7 @@ from typing import Any, Dict, List
|
|
10
10
|
|
11
11
|
from rich.console import Console
|
12
12
|
from rich.panel import Panel
|
13
|
+
from rich.markdown import Markdown
|
13
14
|
|
14
15
|
from .runtime import Runtime
|
15
16
|
from .tools import TOOL_SCHEMAS, execute_tool
|
@@ -19,6 +20,10 @@ DEFAULT_MODEL = os.getenv("PYGENT_MODEL", "gpt-4.1-mini")
|
|
19
20
|
SYSTEM_MSG = (
|
20
21
|
"You are Pygent, a sandboxed coding assistant.\n"
|
21
22
|
"Respond with JSON when you need to use a tool."
|
23
|
+
"If you need to stop, call the `stop` tool.\n"
|
24
|
+
"You can use the following tools:\n"
|
25
|
+
f"{json.dumps(TOOL_SCHEMAS, indent=2)}\n"
|
26
|
+
"You can also use the `continue` tool to continue the conversation.\n"
|
22
27
|
)
|
23
28
|
|
24
29
|
console = Console()
|
@@ -46,7 +51,8 @@ class Agent:
|
|
46
51
|
self.history.append({"role": "tool", "content": output, "tool_call_id": call.id})
|
47
52
|
console.print(Panel(output, title=f"tool:{call.function.name}"))
|
48
53
|
else:
|
49
|
-
|
54
|
+
markdown_response = Markdown(assistant_msg.content)
|
55
|
+
console.print(Panel(markdown_response, title="Resposta do Agente", title_align="left", border_style="cyan"))
|
50
56
|
return assistant_msg
|
51
57
|
|
52
58
|
def run_until_stop(self, user_msg: str, max_steps: int = 10) -> None:
|
@@ -56,7 +62,7 @@ class Agent:
|
|
56
62
|
for _ in range(max_steps):
|
57
63
|
assistant_msg = self.step(msg)
|
58
64
|
calls = assistant_msg.tool_calls or []
|
59
|
-
if any(c.function.name
|
65
|
+
if any(c.function.name in ("stop", "continue") for c in calls):
|
60
66
|
break
|
61
67
|
msg = "continue"
|
62
68
|
|
@@ -66,9 +72,9 @@ def run_interactive(use_docker: bool | None = None) -> None: # pragma: no cover
|
|
66
72
|
console.print("[bold green]Pygent[/] iniciado. (digite /exit para sair)")
|
67
73
|
try:
|
68
74
|
while True:
|
69
|
-
user_msg = console.input("[cyan]
|
75
|
+
user_msg = console.input("[cyan]user> [/]" )
|
70
76
|
if user_msg.strip() in {"/exit", "quit", "q"}:
|
71
77
|
break
|
72
|
-
agent.
|
78
|
+
agent.run_until_stop(user_msg)
|
73
79
|
finally:
|
74
80
|
agent.runtime.cleanup()
|
pygent/errors.py
ADDED
pygent/models.py
CHANGED
@@ -10,6 +10,7 @@ except ModuleNotFoundError: # pragma: no cover - fallback to bundled client
|
|
10
10
|
from . import openai_compat as openai
|
11
11
|
|
12
12
|
from .openai_compat import Message
|
13
|
+
from .errors import APIError
|
13
14
|
|
14
15
|
|
15
16
|
class Model(Protocol):
|
@@ -24,10 +25,13 @@ class OpenAIModel:
|
|
24
25
|
"""Default model using the OpenAI-compatible API."""
|
25
26
|
|
26
27
|
def chat(self, messages: List[Dict[str, Any]], model: str, tools: Any) -> Message:
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
28
|
+
try:
|
29
|
+
resp = openai.chat.completions.create(
|
30
|
+
model=model,
|
31
|
+
messages=messages,
|
32
|
+
tools=tools,
|
33
|
+
tool_choice="auto",
|
34
|
+
)
|
35
|
+
return resp.choices[0].message
|
36
|
+
except Exception as exc:
|
37
|
+
raise APIError(str(exc)) from exc
|
pygent/openai_compat.py
CHANGED
@@ -2,7 +2,9 @@ import os
|
|
2
2
|
import json
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from typing import Any, Dict, List
|
5
|
-
from urllib import request
|
5
|
+
from urllib import request, error
|
6
|
+
|
7
|
+
from .errors import APIError
|
6
8
|
|
7
9
|
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
8
10
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
@@ -39,8 +41,15 @@ def _post(path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
39
41
|
if OPENAI_API_KEY:
|
40
42
|
headers["Authorization"] = f"Bearer {OPENAI_API_KEY}"
|
41
43
|
req = request.Request(f"{OPENAI_BASE_URL}{path}", data=data, headers=headers)
|
42
|
-
|
43
|
-
|
44
|
+
try:
|
45
|
+
with request.urlopen(req) as resp:
|
46
|
+
return json.loads(resp.read().decode())
|
47
|
+
except error.HTTPError as exc: # pragma: no cover - network dependent
|
48
|
+
raise APIError(f"HTTP error {exc.code}: {exc.reason}") from exc
|
49
|
+
except error.URLError as exc: # pragma: no cover - network dependent
|
50
|
+
raise APIError(f"Connection error: {exc.reason}") from exc
|
51
|
+
except Exception as exc: # pragma: no cover - fallback
|
52
|
+
raise APIError(str(exc)) from exc
|
44
53
|
|
45
54
|
|
46
55
|
class _ChatCompletions:
|
pygent/runtime.py
CHANGED
@@ -54,29 +54,37 @@ class Runtime:
|
|
54
54
|
caller can display what was run.
|
55
55
|
"""
|
56
56
|
if self._use_docker and self.container is not None:
|
57
|
-
|
57
|
+
try:
|
58
|
+
res = self.container.exec_run(
|
59
|
+
cmd,
|
60
|
+
workdir="/workspace",
|
61
|
+
demux=True,
|
62
|
+
tty=False,
|
63
|
+
stdin=False,
|
64
|
+
timeout=timeout,
|
65
|
+
)
|
66
|
+
stdout, stderr = (
|
67
|
+
res.output if isinstance(res.output, tuple) else (res.output, b"")
|
68
|
+
)
|
69
|
+
output = (stdout or b"").decode() + (stderr or b"").decode()
|
70
|
+
return f"$ {cmd}\n{output}"
|
71
|
+
except Exception as exc:
|
72
|
+
return f"$ {cmd}\n[error] {exc}"
|
73
|
+
try:
|
74
|
+
proc = subprocess.run(
|
58
75
|
cmd,
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
76
|
+
shell=True,
|
77
|
+
cwd=self.base_dir,
|
78
|
+
capture_output=True,
|
79
|
+
text=True,
|
80
|
+
stdin=subprocess.DEVNULL,
|
63
81
|
timeout=timeout,
|
64
82
|
)
|
65
|
-
stdout
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
return f"$ {cmd}\n{
|
70
|
-
proc = subprocess.run(
|
71
|
-
cmd,
|
72
|
-
shell=True,
|
73
|
-
cwd=self.base_dir,
|
74
|
-
capture_output=True,
|
75
|
-
text=True,
|
76
|
-
stdin=subprocess.DEVNULL,
|
77
|
-
timeout=timeout,
|
78
|
-
)
|
79
|
-
return f"$ {cmd}\n{proc.stdout + proc.stderr}"
|
83
|
+
return f"$ {cmd}\n{proc.stdout + proc.stderr}"
|
84
|
+
except subprocess.TimeoutExpired:
|
85
|
+
return f"$ {cmd}\n[timeout after {timeout}s]"
|
86
|
+
except Exception as exc:
|
87
|
+
return f"$ {cmd}\n[error] {exc}"
|
80
88
|
|
81
89
|
def write_file(self, path: Union[str, Path], content: str) -> str:
|
82
90
|
p = self.base_dir / path
|
pygent/tools.py
CHANGED
@@ -43,6 +43,14 @@ TOOL_SCHEMAS = [
|
|
43
43
|
"parameters": {"type": "object", "properties": {}},
|
44
44
|
},
|
45
45
|
},
|
46
|
+
{
|
47
|
+
"type": "function",
|
48
|
+
"function": {
|
49
|
+
"name": "continue",
|
50
|
+
"description": "Continue the conversation.",
|
51
|
+
"parameters": {"type": "object", "properties": {}},
|
52
|
+
},
|
53
|
+
},
|
46
54
|
]
|
47
55
|
|
48
56
|
# --------------- dispatcher ---------------
|
@@ -57,4 +65,6 @@ def execute_tool(call: Any, rt: Runtime) -> str: # pragma: no cover, Any→open
|
|
57
65
|
return rt.write_file(**args)
|
58
66
|
if name == "stop":
|
59
67
|
return "Stopping."
|
68
|
+
if name == "continue":
|
69
|
+
return "Continuing the conversation."
|
60
70
|
return f"⚠️ unknown tool {name}"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pygent
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.11
|
4
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
5
|
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
6
|
Project-URL: Documentation, https://marianochaves.github.io/pygent
|
@@ -0,0 +1,17 @@
|
|
1
|
+
pygent/__init__.py,sha256=mB_J_GPxLJEk_vHr9mj_4ttLIoSw1F-T1sKEitvRNNU,530
|
2
|
+
pygent/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
3
|
+
pygent/agent.py,sha256=F8XzKzsPFrfgXpqizyOtpKERCy00FQYqI5SHFkOV8_Q,2897
|
4
|
+
pygent/cli.py,sha256=Hz2FZeNMVhxoT5DjCqphXla3TisGJtPEz921LEcpxrA,527
|
5
|
+
pygent/errors.py,sha256=s5FBg_v94coSgMh7cfkP4hVXafViGYgCY8QiT698-c4,155
|
6
|
+
pygent/models.py,sha256=j3670gjUtvQRGZ5wqGDcQ7ZJVTdT5WiwL7nWTokeYzg,1141
|
7
|
+
pygent/openai_compat.py,sha256=cyWFtXt6sDfOlsZd3FuRxbcZMm3WU-DLPBQpbmcuiW8,2617
|
8
|
+
pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
|
9
|
+
pygent/runtime.py,sha256=xD77DPFUfmbAwsIA5SjlEmj_TimFtN3j-6h53aUVdF0,3821
|
10
|
+
pygent/tools.py,sha256=2_mKDVYbiPzHqUW18ROAsvMmiZXgXd5bjlXTYyPJaHM,2032
|
11
|
+
pygent/ui.py,sha256=xqPAvweghPOBBvoD72HzhN6zlXew_3inb8AN7Ck2zpQ,1328
|
12
|
+
pygent-0.1.11.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
|
13
|
+
pygent-0.1.11.dist-info/METADATA,sha256=9MEoVJl3vxrzYjZYl_r-uyBdULn3y7G6Pzkcvolbw0E,878
|
14
|
+
pygent-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
+
pygent-0.1.11.dist-info/entry_points.txt,sha256=b9j216E5UpuMrQWRZrwyEmacNEAYvw1tCKkZqdIVIOc,70
|
16
|
+
pygent-0.1.11.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
|
17
|
+
pygent-0.1.11.dist-info/RECORD,,
|
pygent-0.1.10.dist-info/RECORD
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
pygent/__init__.py,sha256=_YO8FYMUMAWlRYCa6OBsfmJ9P4mCiYoOqFBLrQDP8qQ,442
|
2
|
-
pygent/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
3
|
-
pygent/agent.py,sha256=vmTNM1SOCR8e3CV4zBT9Uyzj3LdDglZekaYupjlpl0I,2487
|
4
|
-
pygent/cli.py,sha256=Hz2FZeNMVhxoT5DjCqphXla3TisGJtPEz921LEcpxrA,527
|
5
|
-
pygent/models.py,sha256=_3Y1Z5wL6FUqzC-EOjZe3Vkcq4SzbPdGz6TbshcEB98,992
|
6
|
-
pygent/openai_compat.py,sha256=mS6ntl70jpVH3JzfNYEDhg-z7QIQcMqQTuEV5ja7VOo,2173
|
7
|
-
pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
|
8
|
-
pygent/runtime.py,sha256=hlFnqtsex8HFy3ndz9dI4cypaxH8vMW7y09OK0j4YmI,3435
|
9
|
-
pygent/tools.py,sha256=8-jvqYeiJOlZ2ku1MTnBnK2O1m90hkrboq9aZrMORr0,1732
|
10
|
-
pygent/ui.py,sha256=xqPAvweghPOBBvoD72HzhN6zlXew_3inb8AN7Ck2zpQ,1328
|
11
|
-
pygent-0.1.10.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
|
12
|
-
pygent-0.1.10.dist-info/METADATA,sha256=BqILS0kmRlGPSEJF2BKFwTAHBbEkcLCoQhxeLDAFgoQ,878
|
13
|
-
pygent-0.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
14
|
-
pygent-0.1.10.dist-info/entry_points.txt,sha256=b9j216E5UpuMrQWRZrwyEmacNEAYvw1tCKkZqdIVIOc,70
|
15
|
-
pygent-0.1.10.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
|
16
|
-
pygent-0.1.10.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|