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.
Files changed (30) hide show
  1. {pygent-0.1.10 → pygent-0.1.12}/PKG-INFO +1 -1
  2. {pygent-0.1.10 → pygent-0.1.12}/README.md +1 -0
  3. {pygent-0.1.10 → pygent-0.1.12}/pygent/__init__.py +12 -1
  4. {pygent-0.1.10 → pygent-0.1.12}/pygent/agent.py +21 -10
  5. pygent-0.1.12/pygent/errors.py +6 -0
  6. {pygent-0.1.10 → pygent-0.1.12}/pygent/models.py +11 -7
  7. {pygent-0.1.10 → pygent-0.1.12}/pygent/openai_compat.py +12 -3
  8. {pygent-0.1.10 → pygent-0.1.12}/pygent/runtime.py +28 -20
  9. pygent-0.1.12/pygent/tools.py +99 -0
  10. {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/PKG-INFO +1 -1
  11. {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/SOURCES.txt +2 -0
  12. {pygent-0.1.10 → pygent-0.1.12}/pyproject.toml +2 -2
  13. {pygent-0.1.10 → pygent-0.1.12}/tests/test_autorun.py +13 -1
  14. {pygent-0.1.10 → pygent-0.1.12}/tests/test_custom_model.py +13 -1
  15. pygent-0.1.12/tests/test_error_handling.py +41 -0
  16. pygent-0.1.12/tests/test_runtime.py +41 -0
  17. {pygent-0.1.10 → pygent-0.1.12}/tests/test_tools.py +35 -2
  18. {pygent-0.1.10 → pygent-0.1.12}/tests/test_version.py +12 -1
  19. pygent-0.1.10/pygent/tools.py +0 -60
  20. pygent-0.1.10/tests/test_runtime.py +0 -25
  21. {pygent-0.1.10 → pygent-0.1.12}/LICENSE +0 -0
  22. {pygent-0.1.10 → pygent-0.1.12}/pygent/__main__.py +0 -0
  23. {pygent-0.1.10 → pygent-0.1.12}/pygent/cli.py +0 -0
  24. {pygent-0.1.10 → pygent-0.1.12}/pygent/py.typed +0 -0
  25. {pygent-0.1.10 → pygent-0.1.12}/pygent/ui.py +0 -0
  26. {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/dependency_links.txt +0 -0
  27. {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/entry_points.txt +0 -0
  28. {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/requires.txt +0 -0
  29. {pygent-0.1.10 → pygent-0.1.12}/pygent.egg-info/top_level.txt +0 -0
  30. {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.10
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__ = ["Agent", "run_interactive", "Model", "OpenAIModel"]
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 .tools import TOOL_SCHEMAS, execute_tool
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
- history: List[Dict[str, Any]] = field(default_factory=lambda: [
35
- {"role": "system", "content": SYSTEM_MSG}
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(self.history, self.model_name, TOOL_SCHEMAS)
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
- console.print(assistant_msg.content)
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 == "stop" for c in calls):
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]vc> [/]" )
80
+ user_msg = console.input("[cyan]user> [/]" )
70
81
  if user_msg.strip() in {"/exit", "quit", "q"}:
71
82
  break
72
- agent.step(user_msg)
83
+ agent.run_until_stop(user_msg)
73
84
  finally:
74
85
  agent.runtime.cleanup()
@@ -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."""
@@ -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
@@ -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:
@@ -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
- 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
@@ -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.10
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.10"
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", # colored output (optional)
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
+
@@ -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