pygent 0.1.9__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 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
- console.print(assistant_msg.content)
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 == "stop" for c in calls):
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]vc> [/]" )
75
+ user_msg = console.input("[cyan]user> [/]" )
70
76
  if user_msg.strip() in {"/exit", "quit", "q"}:
71
77
  break
72
- agent.step(user_msg)
78
+ agent.run_until_stop(user_msg)
73
79
  finally:
74
80
  agent.runtime.cleanup()
pygent/errors.py ADDED
@@ -0,0 +1,6 @@
1
+ class PygentError(Exception):
2
+ """Base error for the Pygent package."""
3
+
4
+
5
+ class APIError(PygentError):
6
+ """Raised when the OpenAI API call fails."""
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
- resp = openai.chat.completions.create(
28
- model=model,
29
- messages=messages,
30
- tools=tools,
31
- tool_choice="auto",
32
- )
33
- return resp.choices[0].message
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
- with request.urlopen(req) as resp:
43
- return json.loads(resp.read().decode())
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,34 +54,42 @@ 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
- res = self.container.exec_run(
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
- workdir="/workspace",
60
- demux=True,
61
- tty=False,
62
- stdin=False,
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, stderr = (
66
- res.output if isinstance(res.output, tuple) else (res.output, b"")
67
- )
68
- output = (stdout or b"").decode() + (stderr or b"").decode()
69
- return f"$ {cmd}\n{output}"
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
83
91
  p.parent.mkdir(parents=True, exist_ok=True)
84
- p.write_text(content)
92
+ p.write_text(content, encoding="utf-8")
85
93
  return f"Wrote {p.relative_to(self.base_dir)}"
86
94
 
87
95
  def cleanup(self) -> None:
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.9
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,,
@@ -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=6VEV4DwpdfrhpGpUiBJr_O1vlo407o0Mvd8Mdhq964A,3417
9
- pygent/tools.py,sha256=8-jvqYeiJOlZ2ku1MTnBnK2O1m90hkrboq9aZrMORr0,1732
10
- pygent/ui.py,sha256=xqPAvweghPOBBvoD72HzhN6zlXew_3inb8AN7Ck2zpQ,1328
11
- pygent-0.1.9.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
12
- pygent-0.1.9.dist-info/METADATA,sha256=YzPl-3O1RvCKbCeZQTcJUNt2m3sw2G6THKEXpbPtM8o,877
13
- pygent-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pygent-0.1.9.dist-info/entry_points.txt,sha256=b9j216E5UpuMrQWRZrwyEmacNEAYvw1tCKkZqdIVIOc,70
15
- pygent-0.1.9.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
16
- pygent-0.1.9.dist-info/RECORD,,