pygent 0.1.11__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 (29) hide show
  1. {pygent-0.1.11 → pygent-0.1.12}/PKG-INFO +1 -1
  2. {pygent-0.1.11 → pygent-0.1.12}/README.md +1 -0
  3. {pygent-0.1.11 → pygent-0.1.12}/pygent/__init__.py +11 -1
  4. {pygent-0.1.11 → pygent-0.1.12}/pygent/agent.py +12 -7
  5. pygent-0.1.12/pygent/tools.py +99 -0
  6. {pygent-0.1.11 → pygent-0.1.12}/pygent.egg-info/PKG-INFO +1 -1
  7. {pygent-0.1.11 → pygent-0.1.12}/pyproject.toml +1 -1
  8. {pygent-0.1.11 → pygent-0.1.12}/tests/test_tools.py +22 -1
  9. pygent-0.1.11/pygent/tools.py +0 -70
  10. {pygent-0.1.11 → pygent-0.1.12}/LICENSE +0 -0
  11. {pygent-0.1.11 → pygent-0.1.12}/pygent/__main__.py +0 -0
  12. {pygent-0.1.11 → pygent-0.1.12}/pygent/cli.py +0 -0
  13. {pygent-0.1.11 → pygent-0.1.12}/pygent/errors.py +0 -0
  14. {pygent-0.1.11 → pygent-0.1.12}/pygent/models.py +0 -0
  15. {pygent-0.1.11 → pygent-0.1.12}/pygent/openai_compat.py +0 -0
  16. {pygent-0.1.11 → pygent-0.1.12}/pygent/py.typed +0 -0
  17. {pygent-0.1.11 → pygent-0.1.12}/pygent/runtime.py +0 -0
  18. {pygent-0.1.11 → pygent-0.1.12}/pygent/ui.py +0 -0
  19. {pygent-0.1.11 → pygent-0.1.12}/pygent.egg-info/SOURCES.txt +0 -0
  20. {pygent-0.1.11 → pygent-0.1.12}/pygent.egg-info/dependency_links.txt +0 -0
  21. {pygent-0.1.11 → pygent-0.1.12}/pygent.egg-info/entry_points.txt +0 -0
  22. {pygent-0.1.11 → pygent-0.1.12}/pygent.egg-info/requires.txt +0 -0
  23. {pygent-0.1.11 → pygent-0.1.12}/pygent.egg-info/top_level.txt +0 -0
  24. {pygent-0.1.11 → pygent-0.1.12}/setup.cfg +0 -0
  25. {pygent-0.1.11 → pygent-0.1.12}/tests/test_autorun.py +0 -0
  26. {pygent-0.1.11 → pygent-0.1.12}/tests/test_custom_model.py +0 -0
  27. {pygent-0.1.11 → pygent-0.1.12}/tests/test_error_handling.py +0 -0
  28. {pygent-0.1.11 → pygent-0.1.12}/tests/test_runtime.py +0 -0
  29. {pygent-0.1.11 → pygent-0.1.12}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygent
3
- Version: 0.1.11
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
 
@@ -9,5 +9,15 @@ except _metadata.PackageNotFoundError: # pragma: no cover - fallback for tests
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
11
  from .errors import PygentError, APIError # noqa: E402,F401
12
+ from .tools import register_tool, tool # noqa: E402,F401
12
13
 
13
- __all__ = ["Agent", "run_interactive", "Model", "OpenAIModel", "PygentError", "APIError"]
14
+ __all__ = [
15
+ "Agent",
16
+ "run_interactive",
17
+ "Model",
18
+ "OpenAIModel",
19
+ "PygentError",
20
+ "APIError",
21
+ "register_tool",
22
+ "tool",
23
+ ]
@@ -13,7 +13,7 @@ from rich.panel import Panel
13
13
  from rich.markdown import Markdown
14
14
 
15
15
  from .runtime import Runtime
16
- from .tools import TOOL_SCHEMAS, execute_tool
16
+ from . import tools
17
17
  from .models import Model, OpenAIModel
18
18
 
19
19
  DEFAULT_MODEL = os.getenv("PYGENT_MODEL", "gpt-4.1-mini")
@@ -22,7 +22,7 @@ SYSTEM_MSG = (
22
22
  "Respond with JSON when you need to use a tool."
23
23
  "If you need to stop, call the `stop` tool.\n"
24
24
  "You can use the following tools:\n"
25
- f"{json.dumps(TOOL_SCHEMAS, indent=2)}\n"
25
+ f"{json.dumps(tools.TOOL_SCHEMAS, indent=2)}\n"
26
26
  "You can also use the `continue` tool to continue the conversation.\n"
27
27
  )
28
28
 
@@ -36,18 +36,23 @@ class Agent:
36
36
  runtime: Runtime = field(default_factory=Runtime)
37
37
  model: Model = field(default_factory=OpenAIModel)
38
38
  model_name: str = DEFAULT_MODEL
39
- history: List[Dict[str, Any]] = field(default_factory=lambda: [
40
- {"role": "system", "content": SYSTEM_MSG}
41
- ])
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})
42
45
 
43
46
  def step(self, user_msg: str):
44
47
  self.history.append({"role": "user", "content": user_msg})
45
- 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
+ )
46
51
  self.history.append(assistant_msg)
47
52
 
48
53
  if assistant_msg.tool_calls:
49
54
  for call in assistant_msg.tool_calls:
50
- output = execute_tool(call, self.runtime)
55
+ output = tools.execute_tool(call, self.runtime)
51
56
  self.history.append({"role": "tool", "content": output, "tool_call_id": call.id})
52
57
  console.print(Panel(output, title=f"tool:{call.function.name}"))
53
58
  else:
@@ -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.11
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pygent"
3
- version = "0.1.11"
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"
@@ -26,7 +26,7 @@ sys.modules.setdefault('rich.syntax', syntax_mod) # Adicionado
26
26
 
27
27
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
28
28
 
29
- from pygent import tools
29
+ from pygent import tools, register_tool
30
30
 
31
31
  class DummyRuntime:
32
32
  def bash(self, cmd: str):
@@ -53,3 +53,24 @@ def test_execute_write_file():
53
53
  })()
54
54
  assert tools.execute_tool(call, DummyRuntime()) == 'wrote foo.txt'
55
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
+
@@ -1,70 +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
- "type": "function",
48
- "function": {
49
- "name": "continue",
50
- "description": "Continue the conversation.",
51
- "parameters": {"type": "object", "properties": {}},
52
- },
53
- },
54
- ]
55
-
56
- # --------------- dispatcher ---------------
57
-
58
- def execute_tool(call: Any, rt: Runtime) -> str: # pragma: no cover, Any→openai.types.ToolCall
59
- name = call.function.name
60
- args: Dict[str, Any] = json.loads(call.function.arguments)
61
-
62
- if name == "bash":
63
- return rt.bash(**args)
64
- if name == "write_file":
65
- return rt.write_file(**args)
66
- if name == "stop":
67
- return "Stopping."
68
- if name == "continue":
69
- return "Continuing the conversation."
70
- return f"⚠️ unknown tool {name}"
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
File without changes
File without changes
File without changes