pygent 0.1.10__tar.gz → 0.1.12__tar.gz
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-0.1.10 → pygent-0.1.12}/PKG-INFO +1 -1
- {pygent-0.1.10 → pygent-0.1.12}/README.md +1 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent/__init__.py +12 -1
- {pygent-0.1.10 → pygent-0.1.12}/pygent/agent.py +21 -10
- pygent-0.1.12/pygent/errors.py +6 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent/models.py +11 -7
- {pygent-0.1.10 → pygent-0.1.12}/pygent/openai_compat.py +12 -3
- {pygent-0.1.10 → pygent-0.1.12}/pygent/runtime.py +28 -20
- pygent-0.1.12/pygent/tools.py +99 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/PKG-INFO +1 -1
- {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/SOURCES.txt +2 -0
- {pygent-0.1.10 → pygent-0.1.12}/pyproject.toml +2 -2
- {pygent-0.1.10 → pygent-0.1.12}/tests/test_autorun.py +13 -1
- {pygent-0.1.10 → pygent-0.1.12}/tests/test_custom_model.py +13 -1
- pygent-0.1.12/tests/test_error_handling.py +41 -0
- pygent-0.1.12/tests/test_runtime.py +41 -0
- {pygent-0.1.10 → pygent-0.1.12}/tests/test_tools.py +35 -2
- {pygent-0.1.10 → pygent-0.1.12}/tests/test_version.py +12 -1
- pygent-0.1.10/pygent/tools.py +0 -60
- pygent-0.1.10/tests/test_runtime.py +0 -25
- {pygent-0.1.10 → pygent-0.1.12}/LICENSE +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent/__main__.py +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent/cli.py +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent/py.typed +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent/ui.py +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/dependency_links.txt +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/entry_points.txt +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/requires.txt +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/top_level.txt +0 -0
- {pygent-0.1.10 → pygent-0.1.12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pygent
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.12
|
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
|
@@ -9,6 +9,7 @@ Pygent is a coding assistant that executes each request inside an isolated Docke
|
|
9
9
|
* Persists the conversation history during the session.
|
10
10
|
* Provides a small Python API for use in other projects.
|
11
11
|
* Optional web interface via `pygent-ui`.
|
12
|
+
* Register your own tools and customise the system prompt.
|
12
13
|
|
13
14
|
## Installation
|
14
15
|
|
@@ -8,5 +8,16 @@ 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
|
12
|
+
from .tools import register_tool, tool # noqa: E402,F401
|
11
13
|
|
12
|
-
__all__ = [
|
14
|
+
__all__ = [
|
15
|
+
"Agent",
|
16
|
+
"run_interactive",
|
17
|
+
"Model",
|
18
|
+
"OpenAIModel",
|
19
|
+
"PygentError",
|
20
|
+
"APIError",
|
21
|
+
"register_tool",
|
22
|
+
"tool",
|
23
|
+
]
|
@@ -10,15 +10,20 @@ 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
|
-
from .
|
16
|
+
from . import tools
|
16
17
|
from .models import Model, OpenAIModel
|
17
18
|
|
18
19
|
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(tools.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()
|
@@ -31,22 +36,28 @@ class Agent:
|
|
31
36
|
runtime: Runtime = field(default_factory=Runtime)
|
32
37
|
model: Model = field(default_factory=OpenAIModel)
|
33
38
|
model_name: str = DEFAULT_MODEL
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
system_msg: str = SYSTEM_MSG
|
40
|
+
history: List[Dict[str, Any]] = field(default_factory=list)
|
41
|
+
|
42
|
+
def __post_init__(self) -> None:
|
43
|
+
if not self.history:
|
44
|
+
self.history.append({"role": "system", "content": self.system_msg})
|
37
45
|
|
38
46
|
def step(self, user_msg: str):
|
39
47
|
self.history.append({"role": "user", "content": user_msg})
|
40
|
-
assistant_msg = self.model.chat(
|
48
|
+
assistant_msg = self.model.chat(
|
49
|
+
self.history, self.model_name, tools.TOOL_SCHEMAS
|
50
|
+
)
|
41
51
|
self.history.append(assistant_msg)
|
42
52
|
|
43
53
|
if assistant_msg.tool_calls:
|
44
54
|
for call in assistant_msg.tool_calls:
|
45
|
-
output = execute_tool(call, self.runtime)
|
55
|
+
output = tools.execute_tool(call, self.runtime)
|
46
56
|
self.history.append({"role": "tool", "content": output, "tool_call_id": call.id})
|
47
57
|
console.print(Panel(output, title=f"tool:{call.function.name}"))
|
48
58
|
else:
|
49
|
-
|
59
|
+
markdown_response = Markdown(assistant_msg.content)
|
60
|
+
console.print(Panel(markdown_response, title="Resposta do Agente", title_align="left", border_style="cyan"))
|
50
61
|
return assistant_msg
|
51
62
|
|
52
63
|
def run_until_stop(self, user_msg: str, max_steps: int = 10) -> None:
|
@@ -56,7 +67,7 @@ class Agent:
|
|
56
67
|
for _ in range(max_steps):
|
57
68
|
assistant_msg = self.step(msg)
|
58
69
|
calls = assistant_msg.tool_calls or []
|
59
|
-
if any(c.function.name
|
70
|
+
if any(c.function.name in ("stop", "continue") for c in calls):
|
60
71
|
break
|
61
72
|
msg = "continue"
|
62
73
|
|
@@ -66,9 +77,9 @@ def run_interactive(use_docker: bool | None = None) -> None: # pragma: no cover
|
|
66
77
|
console.print("[bold green]Pygent[/] iniciado. (digite /exit para sair)")
|
67
78
|
try:
|
68
79
|
while True:
|
69
|
-
user_msg = console.input("[cyan]
|
80
|
+
user_msg = console.input("[cyan]user> [/]" )
|
70
81
|
if user_msg.strip() in {"/exit", "quit", "q"}:
|
71
82
|
break
|
72
|
-
agent.
|
83
|
+
agent.run_until_stop(user_msg)
|
73
84
|
finally:
|
74
85
|
agent.runtime.cleanup()
|
@@ -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
|
@@ -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:
|
@@ -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
|
@@ -0,0 +1,99 @@
|
|
1
|
+
"""Tool registry and helper utilities."""
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import json
|
5
|
+
from typing import Any, Callable, Dict, List
|
6
|
+
|
7
|
+
from .runtime import Runtime
|
8
|
+
|
9
|
+
|
10
|
+
# ---- registry ----
|
11
|
+
TOOLS: Dict[str, Callable[..., str]] = {}
|
12
|
+
TOOL_SCHEMAS: List[Dict[str, Any]] = []
|
13
|
+
|
14
|
+
|
15
|
+
def register_tool(
|
16
|
+
name: str, description: str, parameters: Dict[str, Any], func: Callable[..., str]
|
17
|
+
) -> None:
|
18
|
+
"""Register a new callable tool."""
|
19
|
+
if name in TOOLS:
|
20
|
+
raise ValueError(f"tool {name} already registered")
|
21
|
+
TOOLS[name] = func
|
22
|
+
TOOL_SCHEMAS.append(
|
23
|
+
{
|
24
|
+
"type": "function",
|
25
|
+
"function": {
|
26
|
+
"name": name,
|
27
|
+
"description": description,
|
28
|
+
"parameters": parameters,
|
29
|
+
},
|
30
|
+
}
|
31
|
+
)
|
32
|
+
|
33
|
+
|
34
|
+
def tool(name: str, description: str, parameters: Dict[str, Any]):
|
35
|
+
"""Decorator for registering a tool."""
|
36
|
+
|
37
|
+
def decorator(func: Callable[..., str]) -> Callable[..., str]:
|
38
|
+
register_tool(name, description, parameters, func)
|
39
|
+
return func
|
40
|
+
|
41
|
+
return decorator
|
42
|
+
|
43
|
+
|
44
|
+
def execute_tool(call: Any, rt: Runtime) -> str: # pragma: no cover
|
45
|
+
"""Dispatch a tool call."""
|
46
|
+
name = call.function.name
|
47
|
+
args: Dict[str, Any] = json.loads(call.function.arguments)
|
48
|
+
func = TOOLS.get(name)
|
49
|
+
if func is None:
|
50
|
+
return f"⚠️ unknown tool {name}"
|
51
|
+
return func(rt, **args)
|
52
|
+
|
53
|
+
|
54
|
+
# ---- built-ins ----
|
55
|
+
|
56
|
+
|
57
|
+
@tool(
|
58
|
+
name="bash",
|
59
|
+
description="Run a shell command inside the sandboxed container.",
|
60
|
+
parameters={
|
61
|
+
"type": "object",
|
62
|
+
"properties": {"cmd": {"type": "string", "description": "Command to execute"}},
|
63
|
+
"required": ["cmd"],
|
64
|
+
},
|
65
|
+
)
|
66
|
+
def _bash(rt: Runtime, cmd: str) -> str:
|
67
|
+
return rt.bash(cmd)
|
68
|
+
|
69
|
+
|
70
|
+
@tool(
|
71
|
+
name="write_file",
|
72
|
+
description="Create or overwrite a file in the workspace.",
|
73
|
+
parameters={
|
74
|
+
"type": "object",
|
75
|
+
"properties": {"path": {"type": "string"}, "content": {"type": "string"}},
|
76
|
+
"required": ["path", "content"],
|
77
|
+
},
|
78
|
+
)
|
79
|
+
def _write_file(rt: Runtime, path: str, content: str) -> str:
|
80
|
+
return rt.write_file(path, content)
|
81
|
+
|
82
|
+
|
83
|
+
@tool(
|
84
|
+
name="stop",
|
85
|
+
description="Stop the autonomous loop.",
|
86
|
+
parameters={"type": "object", "properties": {}},
|
87
|
+
)
|
88
|
+
def _stop(rt: Runtime) -> str: # pragma: no cover - side-effect free
|
89
|
+
return "Stopping."
|
90
|
+
|
91
|
+
|
92
|
+
@tool(
|
93
|
+
name="continue",
|
94
|
+
description="Continue the conversation.",
|
95
|
+
parameters={"type": "object", "properties": {}},
|
96
|
+
)
|
97
|
+
def _continue(rt: Runtime) -> str: # pragma: no cover - side-effect free
|
98
|
+
return "Continuing the conversation."
|
99
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pygent
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.12
|
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
|
@@ -5,6 +5,7 @@ pygent/__init__.py
|
|
5
5
|
pygent/__main__.py
|
6
6
|
pygent/agent.py
|
7
7
|
pygent/cli.py
|
8
|
+
pygent/errors.py
|
8
9
|
pygent/models.py
|
9
10
|
pygent/openai_compat.py
|
10
11
|
pygent/py.typed
|
@@ -19,6 +20,7 @@ pygent.egg-info/requires.txt
|
|
19
20
|
pygent.egg-info/top_level.txt
|
20
21
|
tests/test_autorun.py
|
21
22
|
tests/test_custom_model.py
|
23
|
+
tests/test_error_handling.py
|
22
24
|
tests/test_runtime.py
|
23
25
|
tests/test_tools.py
|
24
26
|
tests/test_version.py
|
@@ -1,11 +1,11 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pygent"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.12"
|
4
4
|
description = "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
|
authors = [ { name = "Mariano Chaves", email = "mchaves.software@gmail.com" } ]
|
6
6
|
requires-python = ">=3.9"
|
7
7
|
dependencies = [
|
8
|
-
"rich>=13.7.0",
|
8
|
+
"rich>=13.7.0",
|
9
9
|
"openai>=1.0.0",
|
10
10
|
]
|
11
11
|
|
@@ -4,14 +4,25 @@ import types
|
|
4
4
|
|
5
5
|
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
6
6
|
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
7
|
+
|
8
|
+
# --- Início da correção ---
|
7
9
|
rich_mod = types.ModuleType('rich')
|
8
10
|
console_mod = types.ModuleType('console')
|
9
|
-
console_mod.Console = lambda *a, **k: type('C', (), {'print': lambda *a, **k: None})()
|
10
11
|
panel_mod = types.ModuleType('panel')
|
12
|
+
markdown_mod = types.ModuleType('markdown') # Novo mock para rich.markdown
|
13
|
+
syntax_mod = types.ModuleType('syntax') # Novo mock para rich.syntax
|
14
|
+
|
15
|
+
console_mod.Console = lambda *a, **k: type('C', (), {'print': lambda *a, **k: None})()
|
11
16
|
panel_mod.Panel = lambda *a, **k: None
|
17
|
+
markdown_mod.Markdown = lambda *a, **k: None # Mock para rich.markdown.Markdown
|
18
|
+
syntax_mod.Syntax = lambda *a, **k: None # Mock para rich.syntax.Syntax
|
19
|
+
|
12
20
|
sys.modules.setdefault('rich', rich_mod)
|
13
21
|
sys.modules.setdefault('rich.console', console_mod)
|
14
22
|
sys.modules.setdefault('rich.panel', panel_mod)
|
23
|
+
sys.modules.setdefault('rich.markdown', markdown_mod) # Adicionado
|
24
|
+
sys.modules.setdefault('rich.syntax', syntax_mod) # Adicionado
|
25
|
+
# --- Fim da correção ---
|
15
26
|
|
16
27
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
17
28
|
|
@@ -67,3 +78,4 @@ def test_run_until_stop():
|
|
67
78
|
for msg in ag.history
|
68
79
|
if hasattr(msg, 'tool_calls') and msg.tool_calls
|
69
80
|
for call in msg.tool_calls)
|
81
|
+
|
@@ -4,14 +4,25 @@ import types
|
|
4
4
|
|
5
5
|
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
6
6
|
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
7
|
+
|
8
|
+
# --- Início da correção ---
|
7
9
|
rich_mod = types.ModuleType('rich')
|
8
10
|
console_mod = types.ModuleType('console')
|
9
|
-
console_mod.Console = lambda *a, **k: type('C', (), {'print': lambda *a, **k: None})()
|
10
11
|
panel_mod = types.ModuleType('panel')
|
12
|
+
markdown_mod = types.ModuleType('markdown') # Novo mock para rich.markdown
|
13
|
+
syntax_mod = types.ModuleType('syntax') # Novo mock para rich.syntax
|
14
|
+
|
15
|
+
console_mod.Console = lambda *a, **k: type('C', (), {'print': lambda *a, **k: None})()
|
11
16
|
panel_mod.Panel = lambda *a, **k: None
|
17
|
+
markdown_mod.Markdown = lambda *a, **k: None # Mock para rich.markdown.Markdown
|
18
|
+
syntax_mod.Syntax = lambda *a, **k: None # Mock para rich.syntax.Syntax
|
19
|
+
|
12
20
|
sys.modules.setdefault('rich', rich_mod)
|
13
21
|
sys.modules.setdefault('rich.console', console_mod)
|
14
22
|
sys.modules.setdefault('rich.panel', panel_mod)
|
23
|
+
sys.modules.setdefault('rich.markdown', markdown_mod) # Adicionado
|
24
|
+
sys.modules.setdefault('rich.syntax', syntax_mod) # Adicionado
|
25
|
+
# --- Fim da correção ---
|
15
26
|
|
16
27
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
17
28
|
|
@@ -26,3 +37,4 @@ def test_custom_model():
|
|
26
37
|
ag = Agent(model=DummyModel())
|
27
38
|
ag.step('hi')
|
28
39
|
assert ag.history[-1].content == 'ok'
|
40
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import types
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
# Stub external dependencies
|
7
|
+
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
8
|
+
rich_mod = types.ModuleType('rich')
|
9
|
+
console_mod = types.ModuleType('console')
|
10
|
+
console_mod.Console = lambda *a, **k: None
|
11
|
+
panel_mod = types.ModuleType('panel')
|
12
|
+
panel_mod.Panel = lambda *a, **k: None
|
13
|
+
sys.modules.setdefault('rich', rich_mod)
|
14
|
+
sys.modules.setdefault('rich.console', console_mod)
|
15
|
+
sys.modules.setdefault('rich.panel', panel_mod)
|
16
|
+
|
17
|
+
|
18
|
+
def test_openai_model_error():
|
19
|
+
openai_mod = types.ModuleType('openai')
|
20
|
+
class ChatComp:
|
21
|
+
def create(*a, **k):
|
22
|
+
raise RuntimeError('boom')
|
23
|
+
chat_mod = types.ModuleType('chat')
|
24
|
+
chat_mod.completions = ChatComp()
|
25
|
+
openai_mod.chat = chat_mod
|
26
|
+
sys.modules['openai'] = openai_mod
|
27
|
+
|
28
|
+
from pygent.models import OpenAIModel
|
29
|
+
from pygent.errors import APIError
|
30
|
+
|
31
|
+
model = OpenAIModel()
|
32
|
+
with pytest.raises(APIError):
|
33
|
+
model.chat([], 'gpt', None)
|
34
|
+
|
35
|
+
|
36
|
+
def test_bash_timeout():
|
37
|
+
from pygent.runtime import Runtime
|
38
|
+
rt = Runtime(use_docker=False)
|
39
|
+
out = rt.bash('sleep 5', timeout=0)
|
40
|
+
rt.cleanup()
|
41
|
+
assert '[timeout' in out
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import types
|
4
|
+
|
5
|
+
# Stub external dependencies
|
6
|
+
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
7
|
+
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
8
|
+
|
9
|
+
# --- Início da correção ---
|
10
|
+
# Criação de módulos mock para rich e seus submódulos
|
11
|
+
rich_mod = types.ModuleType('rich')
|
12
|
+
console_mod = types.ModuleType('console')
|
13
|
+
panel_mod = types.ModuleType('panel')
|
14
|
+
markdown_mod = types.ModuleType('markdown') # Novo mock para rich.markdown
|
15
|
+
syntax_mod = types.ModuleType('syntax') # Novo mock para rich.syntax
|
16
|
+
|
17
|
+
# Mocks para as classes e funções usadas de rich
|
18
|
+
console_mod.Console = lambda *a, **k: type('C', (), {'print': lambda *a, **k: None})()
|
19
|
+
panel_mod.Panel = lambda *a, **k: None
|
20
|
+
markdown_mod.Markdown = lambda *a, **k: None # Mock para rich.markdown.Markdown
|
21
|
+
syntax_mod.Syntax = lambda *a, **k: None # Mock para rich.syntax.Syntax
|
22
|
+
|
23
|
+
# Definindo os módulos mock no sys.modules
|
24
|
+
sys.modules.setdefault('rich', rich_mod)
|
25
|
+
sys.modules.setdefault('rich.console', console_mod)
|
26
|
+
sys.modules.setdefault('rich.panel', panel_mod)
|
27
|
+
sys.modules.setdefault('rich.markdown', markdown_mod) # Adicionado
|
28
|
+
sys.modules.setdefault('rich.syntax', syntax_mod) # Adicionado
|
29
|
+
# --- Fim da correção ---
|
30
|
+
|
31
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
32
|
+
|
33
|
+
from pygent.runtime import Runtime
|
34
|
+
|
35
|
+
|
36
|
+
def test_bash_includes_command():
|
37
|
+
rt = Runtime(use_docker=False)
|
38
|
+
out = rt.bash('echo hi')
|
39
|
+
rt.cleanup()
|
40
|
+
assert out.startswith('$ echo hi\n')
|
41
|
+
|
@@ -4,18 +4,29 @@ import types
|
|
4
4
|
|
5
5
|
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
6
6
|
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
7
|
+
|
8
|
+
# --- Início da correção ---
|
7
9
|
rich_mod = types.ModuleType('rich')
|
8
10
|
console_mod = types.ModuleType('console')
|
9
|
-
console_mod.Console = lambda *a, **k: None
|
10
11
|
panel_mod = types.ModuleType('panel')
|
12
|
+
markdown_mod = types.ModuleType('markdown') # Novo mock para rich.markdown
|
13
|
+
syntax_mod = types.ModuleType('syntax') # Novo mock para rich.syntax
|
14
|
+
|
15
|
+
console_mod.Console = lambda *a, **k: None
|
11
16
|
panel_mod.Panel = lambda *a, **k: None
|
17
|
+
markdown_mod.Markdown = lambda *a, **k: None # Mock para rich.markdown.Markdown
|
18
|
+
syntax_mod.Syntax = lambda *a, **k: None # Mock para rich.syntax.Syntax
|
19
|
+
|
12
20
|
sys.modules.setdefault('rich', rich_mod)
|
13
21
|
sys.modules.setdefault('rich.console', console_mod)
|
14
22
|
sys.modules.setdefault('rich.panel', panel_mod)
|
23
|
+
sys.modules.setdefault('rich.markdown', markdown_mod) # Adicionado
|
24
|
+
sys.modules.setdefault('rich.syntax', syntax_mod) # Adicionado
|
25
|
+
# --- Fim da correção ---
|
15
26
|
|
16
27
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
17
28
|
|
18
|
-
from pygent import tools
|
29
|
+
from pygent import tools, register_tool
|
19
30
|
|
20
31
|
class DummyRuntime:
|
21
32
|
def bash(self, cmd: str):
|
@@ -41,3 +52,25 @@ def test_execute_write_file():
|
|
41
52
|
})
|
42
53
|
})()
|
43
54
|
assert tools.execute_tool(call, DummyRuntime()) == 'wrote foo.txt'
|
55
|
+
|
56
|
+
|
57
|
+
def test_register_and_execute_custom_tool():
|
58
|
+
def hello(rt, name: str):
|
59
|
+
return f"hi {name}"
|
60
|
+
|
61
|
+
register_tool(
|
62
|
+
"hello",
|
63
|
+
"greet",
|
64
|
+
{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]},
|
65
|
+
hello,
|
66
|
+
)
|
67
|
+
|
68
|
+
call = type('Call', (), {
|
69
|
+
'function': type('Func', (), {
|
70
|
+
'name': 'hello',
|
71
|
+
'arguments': '{"name": "bob"}'
|
72
|
+
})
|
73
|
+
})()
|
74
|
+
assert tools.execute_tool(call, DummyRuntime()) == 'hi bob'
|
75
|
+
|
76
|
+
|
@@ -5,15 +5,26 @@ import types
|
|
5
5
|
# Stub external dependencies so the package can be imported without network
|
6
6
|
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
7
7
|
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
8
|
+
|
9
|
+
# --- Início da correção ---
|
8
10
|
rich_mod = types.ModuleType('rich')
|
9
11
|
console_mod = types.ModuleType('console')
|
10
|
-
console_mod.Console = lambda *a, **k: None
|
11
12
|
panel_mod = types.ModuleType('panel')
|
13
|
+
markdown_mod = types.ModuleType('markdown') # Novo mock para rich.markdown
|
14
|
+
syntax_mod = types.ModuleType('syntax') # Novo mock para rich.syntax
|
15
|
+
|
16
|
+
console_mod.Console = lambda *a, **k: None
|
12
17
|
panel_mod.Panel = lambda *a, **k: None
|
18
|
+
markdown_mod.Markdown = lambda *a, **k: None # Mock para rich.markdown.Markdown
|
19
|
+
syntax_mod.Syntax = lambda *a, **k: None # Mock para rich.syntax.Syntax
|
20
|
+
|
13
21
|
sys.modules.setdefault('rich', rich_mod)
|
14
22
|
sys.modules.setdefault('rich.console', console_mod)
|
15
23
|
sys.modules.setdefault('rich.panel', panel_mod)
|
24
|
+
sys.modules.setdefault('rich.markdown', markdown_mod)
|
25
|
+
sys.modules.setdefault('rich.syntax', syntax_mod)
|
16
26
|
|
17
27
|
def test_version_string():
|
18
28
|
pkg = importlib.import_module('pygent')
|
19
29
|
assert isinstance(pkg.__version__, str)
|
30
|
+
|
pygent-0.1.10/pygent/tools.py
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
"""Map of tools available to the agent."""
|
2
|
-
from __future__ import annotations
|
3
|
-
import json
|
4
|
-
from typing import Any, Dict
|
5
|
-
|
6
|
-
from .runtime import Runtime
|
7
|
-
|
8
|
-
TOOL_SCHEMAS = [
|
9
|
-
{
|
10
|
-
"type": "function",
|
11
|
-
"function": {
|
12
|
-
"name": "bash",
|
13
|
-
"description": "Run a shell command inside the sandboxed container.",
|
14
|
-
"parameters": {
|
15
|
-
"type": "object",
|
16
|
-
"properties": {
|
17
|
-
"cmd": {"type": "string", "description": "Command to execute"}
|
18
|
-
},
|
19
|
-
"required": ["cmd"],
|
20
|
-
},
|
21
|
-
},
|
22
|
-
},
|
23
|
-
{
|
24
|
-
"type": "function",
|
25
|
-
"function": {
|
26
|
-
"name": "write_file",
|
27
|
-
"description": "Create or overwrite a file in the workspace.",
|
28
|
-
"parameters": {
|
29
|
-
"type": "object",
|
30
|
-
"properties": {
|
31
|
-
"path": {"type": "string"},
|
32
|
-
"content": {"type": "string"},
|
33
|
-
},
|
34
|
-
"required": ["path", "content"],
|
35
|
-
},
|
36
|
-
},
|
37
|
-
},
|
38
|
-
{
|
39
|
-
"type": "function",
|
40
|
-
"function": {
|
41
|
-
"name": "stop",
|
42
|
-
"description": "Stop the autonomous loop.",
|
43
|
-
"parameters": {"type": "object", "properties": {}},
|
44
|
-
},
|
45
|
-
},
|
46
|
-
]
|
47
|
-
|
48
|
-
# --------------- dispatcher ---------------
|
49
|
-
|
50
|
-
def execute_tool(call: Any, rt: Runtime) -> str: # pragma: no cover, Any→openai.types.ToolCall
|
51
|
-
name = call.function.name
|
52
|
-
args: Dict[str, Any] = json.loads(call.function.arguments)
|
53
|
-
|
54
|
-
if name == "bash":
|
55
|
-
return rt.bash(**args)
|
56
|
-
if name == "write_file":
|
57
|
-
return rt.write_file(**args)
|
58
|
-
if name == "stop":
|
59
|
-
return "Stopping."
|
60
|
-
return f"⚠️ unknown tool {name}"
|
@@ -1,25 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import sys
|
3
|
-
import types
|
4
|
-
|
5
|
-
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
6
|
-
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
7
|
-
rich_mod = types.ModuleType('rich')
|
8
|
-
console_mod = types.ModuleType('console')
|
9
|
-
console_mod.Console = lambda *a, **k: None
|
10
|
-
panel_mod = types.ModuleType('panel')
|
11
|
-
panel_mod.Panel = lambda *a, **k: None
|
12
|
-
sys.modules.setdefault('rich', rich_mod)
|
13
|
-
sys.modules.setdefault('rich.console', console_mod)
|
14
|
-
sys.modules.setdefault('rich.panel', panel_mod)
|
15
|
-
|
16
|
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
17
|
-
|
18
|
-
from pygent.runtime import Runtime
|
19
|
-
|
20
|
-
|
21
|
-
def test_bash_includes_command():
|
22
|
-
rt = Runtime(use_docker=False)
|
23
|
-
out = rt.bash('echo hi')
|
24
|
-
rt.cleanup()
|
25
|
-
assert out.startswith('$ echo hi\n')
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|