pygent 0.1.11__tar.gz → 0.1.13__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.13/PKG-INFO +132 -0
- {pygent-0.1.11 → pygent-0.1.13}/README.md +2 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/__init__.py +13 -1
- {pygent-0.1.11 → pygent-0.1.13}/pygent/agent.py +14 -9
- {pygent-0.1.11 → pygent-0.1.13}/pygent/runtime.py +18 -0
- pygent-0.1.13/pygent/task_manager.py +109 -0
- pygent-0.1.13/pygent/tools.py +186 -0
- pygent-0.1.13/pygent.egg-info/PKG-INFO +132 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent.egg-info/SOURCES.txt +2 -0
- {pygent-0.1.11 → pygent-0.1.13}/pyproject.toml +2 -1
- pygent-0.1.13/tests/test_tasks.py +187 -0
- {pygent-0.1.11 → pygent-0.1.13}/tests/test_tools.py +22 -1
- pygent-0.1.11/PKG-INFO +0 -20
- pygent-0.1.11/pygent/tools.py +0 -70
- pygent-0.1.11/pygent.egg-info/PKG-INFO +0 -20
- {pygent-0.1.11 → pygent-0.1.13}/LICENSE +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/__main__.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/cli.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/errors.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/models.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/openai_compat.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/py.typed +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent/ui.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent.egg-info/dependency_links.txt +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent.egg-info/entry_points.txt +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent.egg-info/requires.txt +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/pygent.egg-info/top_level.txt +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/setup.cfg +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/tests/test_autorun.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/tests/test_custom_model.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/tests/test_error_handling.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/tests/test_runtime.py +0 -0
- {pygent-0.1.11 → pygent-0.1.13}/tests/test_version.py +0 -0
pygent-0.1.13/PKG-INFO
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pygent
|
3
|
+
Version: 0.1.13
|
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
|
+
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
|
+
Project-URL: Documentation, https://marianochaves.github.io/pygent
|
7
|
+
Project-URL: Repository, https://github.com/marianochaves/pygent
|
8
|
+
Requires-Python: >=3.9
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Dist: rich>=13.7.0
|
12
|
+
Requires-Dist: openai>=1.0.0
|
13
|
+
Provides-Extra: test
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
15
|
+
Provides-Extra: docs
|
16
|
+
Requires-Dist: mkdocs; extra == "docs"
|
17
|
+
Provides-Extra: docker
|
18
|
+
Requires-Dist: docker>=7.0.0; extra == "docker"
|
19
|
+
Provides-Extra: ui
|
20
|
+
Requires-Dist: gradio; extra == "ui"
|
21
|
+
Dynamic: license-file
|
22
|
+
|
23
|
+
# Pygent
|
24
|
+
|
25
|
+
Pygent is a coding assistant that executes each request inside an isolated Docker container whenever possible. If Docker is unavailable (for instance on some Windows setups) the commands are executed locally instead. Full documentation is available in the `docs/` directory and at [marianochaves.github.io/pygent](https://marianochaves.github.io/pygent/).
|
26
|
+
|
27
|
+
## Features
|
28
|
+
|
29
|
+
* Runs commands in ephemeral containers (default image `python:3.12-slim`).
|
30
|
+
* Integrates with OpenAI-compatible models to orchestrate each step.
|
31
|
+
* Persists the conversation history during the session.
|
32
|
+
* Provides a small Python API for use in other projects.
|
33
|
+
* Optional web interface via `pygent-ui`.
|
34
|
+
* Register your own tools and customise the system prompt.
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
|
38
|
+
Installing from source is recommended:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
pip install -e .
|
42
|
+
```
|
43
|
+
|
44
|
+
Python ≥ 3.9 is required. The package now bundles the `openai` client for model access.
|
45
|
+
To run commands in Docker containers also install `pygent[docker]`.
|
46
|
+
|
47
|
+
## Configuration
|
48
|
+
|
49
|
+
Behaviour can be adjusted via environment variables (see `docs/configuration.md` for a complete list):
|
50
|
+
|
51
|
+
* `OPENAI_API_KEY` – key used to access the OpenAI API.
|
52
|
+
Set this to your API key or a key from any compatible provider.
|
53
|
+
* `OPENAI_BASE_URL` – base URL for OpenAI-compatible APIs
|
54
|
+
(defaults to ``https://api.openai.com/v1``).
|
55
|
+
* `PYGENT_MODEL` – model name used for requests (default `gpt-4.1-mini`).
|
56
|
+
* `PYGENT_IMAGE` – Docker image to create the container (default `python:3.12-slim`).
|
57
|
+
* `PYGENT_USE_DOCKER` – set to `0` to disable Docker and run locally.
|
58
|
+
* `PYGENT_MAX_TASKS` – maximum number of concurrent delegated tasks (default `3`).
|
59
|
+
|
60
|
+
## CLI usage
|
61
|
+
|
62
|
+
After installing run:
|
63
|
+
|
64
|
+
```bash
|
65
|
+
pygent
|
66
|
+
```
|
67
|
+
|
68
|
+
Use `--docker` to run commands inside a container (requires
|
69
|
+
`pygent[docker]`). Use `--no-docker` or set `PYGENT_USE_DOCKER=0`
|
70
|
+
to force local execution.
|
71
|
+
|
72
|
+
Type messages normally; use `/exit` to end the session. Each command is executed
|
73
|
+
in the container and the result shown in the terminal.
|
74
|
+
Interactive programs that expect input (e.g. running `python` without a script)
|
75
|
+
are not supported and will exit immediately.
|
76
|
+
For a minimal web interface run `pygent-ui` instead (requires `pygent[ui]`).
|
77
|
+
|
78
|
+
|
79
|
+
## API usage
|
80
|
+
|
81
|
+
You can also interact directly with the Python code:
|
82
|
+
|
83
|
+
```python
|
84
|
+
from pygent import Agent
|
85
|
+
|
86
|
+
ag = Agent()
|
87
|
+
ag.step("echo 'Hello World'")
|
88
|
+
# ... more steps
|
89
|
+
ag.runtime.cleanup()
|
90
|
+
```
|
91
|
+
|
92
|
+
See the [examples](https://github.com/marianochaves/pygent/tree/main/examples) folder for more complete scripts. Models can be swapped by
|
93
|
+
passing an object implementing the ``Model`` interface when creating the
|
94
|
+
``Agent``. The default uses an OpenAI-compatible API, but custom models are
|
95
|
+
easy to plug in.
|
96
|
+
|
97
|
+
### Using OpenAI and other providers
|
98
|
+
|
99
|
+
Set your OpenAI key:
|
100
|
+
|
101
|
+
```bash
|
102
|
+
export OPENAI_API_KEY="sk-..."
|
103
|
+
```
|
104
|
+
|
105
|
+
To use a different provider, set `OPENAI_BASE_URL` to the provider
|
106
|
+
endpoint and keep `OPENAI_API_KEY` pointing to the correct key:
|
107
|
+
|
108
|
+
```bash
|
109
|
+
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
|
110
|
+
export OPENAI_API_KEY="your-provider-key"
|
111
|
+
```
|
112
|
+
|
113
|
+
## Development
|
114
|
+
|
115
|
+
1. Install the test dependencies:
|
116
|
+
|
117
|
+
```bash
|
118
|
+
pip install -e .[test]
|
119
|
+
```
|
120
|
+
|
121
|
+
2. Run the test suite:
|
122
|
+
|
123
|
+
```bash
|
124
|
+
pytest
|
125
|
+
```
|
126
|
+
|
127
|
+
Use `mkdocs serve` to build the documentation locally.
|
128
|
+
|
129
|
+
## License
|
130
|
+
|
131
|
+
This project is released under the MIT license. See the `LICENSE` file for details.
|
132
|
+
|
@@ -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
|
|
@@ -32,6 +33,7 @@ Behaviour can be adjusted via environment variables (see `docs/configuration.md`
|
|
32
33
|
* `PYGENT_MODEL` – model name used for requests (default `gpt-4.1-mini`).
|
33
34
|
* `PYGENT_IMAGE` – Docker image to create the container (default `python:3.12-slim`).
|
34
35
|
* `PYGENT_USE_DOCKER` – set to `0` to disable Docker and run locally.
|
36
|
+
* `PYGENT_MAX_TASKS` – maximum number of concurrent delegated tasks (default `3`).
|
35
37
|
|
36
38
|
## CLI usage
|
37
39
|
|
@@ -9,5 +9,17 @@ 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
|
13
|
+
from .task_manager import TaskManager # noqa: E402,F401
|
12
14
|
|
13
|
-
__all__ = [
|
15
|
+
__all__ = [
|
16
|
+
"Agent",
|
17
|
+
"run_interactive",
|
18
|
+
"Model",
|
19
|
+
"OpenAIModel",
|
20
|
+
"PygentError",
|
21
|
+
"APIError",
|
22
|
+
"register_tool",
|
23
|
+
"tool",
|
24
|
+
"TaskManager",
|
25
|
+
]
|
@@ -13,17 +13,17 @@ from rich.panel import Panel
|
|
13
13
|
from rich.markdown import Markdown
|
14
14
|
|
15
15
|
from .runtime import Runtime
|
16
|
-
from .
|
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")
|
20
20
|
SYSTEM_MSG = (
|
21
21
|
"You are Pygent, a sandboxed coding assistant.\n"
|
22
22
|
"Respond with JSON when you need to use a tool."
|
23
|
-
"If you need to stop, call the `stop` tool.\n"
|
23
|
+
"If you need to stop or finished you task, call the `stop` tool.\n"
|
24
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"
|
25
|
+
f"{json.dumps(tools.TOOL_SCHEMAS, indent=2)}\n"
|
26
|
+
"You can also use the `continue` tool to request user input or continue the conversation.\n"
|
27
27
|
)
|
28
28
|
|
29
29
|
console = Console()
|
@@ -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
|
-
|
40
|
-
|
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(
|
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:
|
@@ -92,6 +92,24 @@ class Runtime:
|
|
92
92
|
p.write_text(content, encoding="utf-8")
|
93
93
|
return f"Wrote {p.relative_to(self.base_dir)}"
|
94
94
|
|
95
|
+
def read_file(self, path: Union[str, Path], binary: bool = False) -> str:
|
96
|
+
"""Return the contents of a file relative to the workspace."""
|
97
|
+
|
98
|
+
p = self.base_dir / path
|
99
|
+
if not p.exists():
|
100
|
+
return f"file {p.relative_to(self.base_dir)} not found"
|
101
|
+
data = p.read_bytes()
|
102
|
+
if binary:
|
103
|
+
import base64
|
104
|
+
|
105
|
+
return base64.b64encode(data).decode()
|
106
|
+
try:
|
107
|
+
return data.decode()
|
108
|
+
except UnicodeDecodeError:
|
109
|
+
import base64
|
110
|
+
|
111
|
+
return base64.b64encode(data).decode()
|
112
|
+
|
95
113
|
def cleanup(self) -> None:
|
96
114
|
if self._use_docker and self.container is not None:
|
97
115
|
try:
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
"""Manage background tasks executed by sub-agents."""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import shutil
|
7
|
+
import threading
|
8
|
+
import uuid
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from typing import Callable, Dict, TYPE_CHECKING
|
11
|
+
|
12
|
+
from .runtime import Runtime
|
13
|
+
|
14
|
+
if TYPE_CHECKING: # pragma: no cover - for type hints only
|
15
|
+
from .agent import Agent
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Task:
|
20
|
+
"""Represents a delegated task."""
|
21
|
+
|
22
|
+
id: str
|
23
|
+
agent: "Agent"
|
24
|
+
thread: threading.Thread
|
25
|
+
status: str = field(default="running")
|
26
|
+
|
27
|
+
|
28
|
+
class TaskManager:
|
29
|
+
"""Launch agents asynchronously and track their progress."""
|
30
|
+
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
agent_factory: Callable[[], "Agent"] | None = None,
|
34
|
+
max_tasks: int | None = None,
|
35
|
+
) -> None:
|
36
|
+
from .agent import Agent # local import to avoid circular dependency
|
37
|
+
|
38
|
+
env_max = os.getenv("PYGENT_MAX_TASKS")
|
39
|
+
self.max_tasks = max_tasks if max_tasks is not None else int(env_max or "3")
|
40
|
+
self.agent_factory = agent_factory or Agent
|
41
|
+
self.tasks: Dict[str, Task] = {}
|
42
|
+
self._lock = threading.Lock()
|
43
|
+
|
44
|
+
def start_task(
|
45
|
+
self,
|
46
|
+
prompt: str,
|
47
|
+
parent_rt: Runtime,
|
48
|
+
files: list[str] | None = None,
|
49
|
+
parent_depth: int = 0,
|
50
|
+
) -> str:
|
51
|
+
"""Create a new agent and run ``prompt`` asynchronously."""
|
52
|
+
|
53
|
+
if parent_depth >= 1:
|
54
|
+
raise RuntimeError("nested delegation is not allowed")
|
55
|
+
|
56
|
+
with self._lock:
|
57
|
+
active = sum(t.status == "running" for t in self.tasks.values())
|
58
|
+
if active >= self.max_tasks:
|
59
|
+
raise RuntimeError(f"max {self.max_tasks} tasks reached")
|
60
|
+
|
61
|
+
agent = self.agent_factory()
|
62
|
+
setattr(agent.runtime, "task_depth", parent_depth + 1)
|
63
|
+
if files:
|
64
|
+
for fp in files:
|
65
|
+
src = parent_rt.base_dir / fp
|
66
|
+
dest = agent.runtime.base_dir / fp
|
67
|
+
if src.is_dir():
|
68
|
+
shutil.copytree(src, dest, dirs_exist_ok=True)
|
69
|
+
elif src.exists():
|
70
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
71
|
+
shutil.copy(src, dest)
|
72
|
+
task_id = uuid.uuid4().hex[:8]
|
73
|
+
task = Task(id=task_id, agent=agent, thread=None) # type: ignore[arg-type]
|
74
|
+
|
75
|
+
def run() -> None:
|
76
|
+
try:
|
77
|
+
agent.run_until_stop(prompt)
|
78
|
+
task.status = "finished"
|
79
|
+
except Exception as exc: # pragma: no cover - error propagation
|
80
|
+
task.status = f"error: {exc}"
|
81
|
+
|
82
|
+
t = threading.Thread(target=run, daemon=True)
|
83
|
+
task.thread = t
|
84
|
+
with self._lock:
|
85
|
+
self.tasks[task_id] = task
|
86
|
+
t.start()
|
87
|
+
return task_id
|
88
|
+
|
89
|
+
def status(self, task_id: str) -> str:
|
90
|
+
with self._lock:
|
91
|
+
task = self.tasks.get(task_id)
|
92
|
+
if not task:
|
93
|
+
return f"Task {task_id} not found"
|
94
|
+
return task.status
|
95
|
+
|
96
|
+
def collect_file(self, rt: Runtime, task_id: str, path: str) -> str:
|
97
|
+
"""Copy a file from a task workspace into ``rt``."""
|
98
|
+
|
99
|
+
with self._lock:
|
100
|
+
task = self.tasks.get(task_id)
|
101
|
+
if not task:
|
102
|
+
return f"Task {task_id} not found"
|
103
|
+
src = task.agent.runtime.base_dir / path
|
104
|
+
if not src.exists():
|
105
|
+
return f"file {path} not found"
|
106
|
+
dest = rt.base_dir / path
|
107
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
108
|
+
shutil.copy(src, dest)
|
109
|
+
return f"Retrieved {dest.relative_to(rt.base_dir)}"
|
@@ -0,0 +1,186 @@
|
|
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
|
+
from .task_manager import TaskManager
|
9
|
+
|
10
|
+
_task_manager: TaskManager | None = None
|
11
|
+
|
12
|
+
|
13
|
+
def _get_manager() -> TaskManager:
|
14
|
+
global _task_manager
|
15
|
+
if _task_manager is None:
|
16
|
+
_task_manager = TaskManager()
|
17
|
+
return _task_manager
|
18
|
+
|
19
|
+
|
20
|
+
# ---- registry ----
|
21
|
+
TOOLS: Dict[str, Callable[..., str]] = {}
|
22
|
+
TOOL_SCHEMAS: List[Dict[str, Any]] = []
|
23
|
+
|
24
|
+
|
25
|
+
def register_tool(
|
26
|
+
name: str, description: str, parameters: Dict[str, Any], func: Callable[..., str]
|
27
|
+
) -> None:
|
28
|
+
"""Register a new callable tool."""
|
29
|
+
if name in TOOLS:
|
30
|
+
raise ValueError(f"tool {name} already registered")
|
31
|
+
TOOLS[name] = func
|
32
|
+
TOOL_SCHEMAS.append(
|
33
|
+
{
|
34
|
+
"type": "function",
|
35
|
+
"function": {
|
36
|
+
"name": name,
|
37
|
+
"description": description,
|
38
|
+
"parameters": parameters,
|
39
|
+
},
|
40
|
+
}
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
def tool(name: str, description: str, parameters: Dict[str, Any]):
|
45
|
+
"""Decorator for registering a tool."""
|
46
|
+
|
47
|
+
def decorator(func: Callable[..., str]) -> Callable[..., str]:
|
48
|
+
register_tool(name, description, parameters, func)
|
49
|
+
return func
|
50
|
+
|
51
|
+
return decorator
|
52
|
+
|
53
|
+
|
54
|
+
def execute_tool(call: Any, rt: Runtime) -> str: # pragma: no cover
|
55
|
+
"""Dispatch a tool call."""
|
56
|
+
name = call.function.name
|
57
|
+
args: Dict[str, Any] = json.loads(call.function.arguments)
|
58
|
+
func = TOOLS.get(name)
|
59
|
+
if func is None:
|
60
|
+
return f"⚠️ unknown tool {name}"
|
61
|
+
return func(rt, **args)
|
62
|
+
|
63
|
+
|
64
|
+
# ---- built-ins ----
|
65
|
+
|
66
|
+
|
67
|
+
@tool(
|
68
|
+
name="bash",
|
69
|
+
description="Run a shell command inside the sandboxed container.",
|
70
|
+
parameters={
|
71
|
+
"type": "object",
|
72
|
+
"properties": {"cmd": {"type": "string", "description": "Command to execute"}},
|
73
|
+
"required": ["cmd"],
|
74
|
+
},
|
75
|
+
)
|
76
|
+
def _bash(rt: Runtime, cmd: str) -> str:
|
77
|
+
return rt.bash(cmd)
|
78
|
+
|
79
|
+
|
80
|
+
@tool(
|
81
|
+
name="write_file",
|
82
|
+
description="Create or overwrite a file in the workspace.",
|
83
|
+
parameters={
|
84
|
+
"type": "object",
|
85
|
+
"properties": {"path": {"type": "string"}, "content": {"type": "string"}},
|
86
|
+
"required": ["path", "content"],
|
87
|
+
},
|
88
|
+
)
|
89
|
+
def _write_file(rt: Runtime, path: str, content: str) -> str:
|
90
|
+
return rt.write_file(path, content)
|
91
|
+
|
92
|
+
|
93
|
+
@tool(
|
94
|
+
name="stop",
|
95
|
+
description="Stop the autonomous loop. This is a side-effect free tool that does not return any output. Use when finished some task or when you want to stop the agent.",
|
96
|
+
parameters={"type": "object", "properties": {}},
|
97
|
+
)
|
98
|
+
def _stop(rt: Runtime) -> str: # pragma: no cover - side-effect free
|
99
|
+
return "Stopping."
|
100
|
+
|
101
|
+
|
102
|
+
@tool(
|
103
|
+
name="continue",
|
104
|
+
description="Request user answer or input. If in your previous message you asked for user input, you can use this tool to continue the conversation.",
|
105
|
+
parameters={"type": "object", "properties": {}},
|
106
|
+
)
|
107
|
+
def _continue(rt: Runtime) -> str: # pragma: no cover - side-effect free
|
108
|
+
return "Continuing the conversation."
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
@tool(
|
114
|
+
name="delegate_task",
|
115
|
+
description="Create a background task using a new agent and return its ID.",
|
116
|
+
parameters={
|
117
|
+
"type": "object",
|
118
|
+
"properties": {
|
119
|
+
"prompt": {"type": "string", "description": "Instruction for the sub-agent"},
|
120
|
+
"files": {
|
121
|
+
"type": "array",
|
122
|
+
"items": {"type": "string"},
|
123
|
+
"description": "Files to copy to the sub-agent before starting",
|
124
|
+
},
|
125
|
+
},
|
126
|
+
"required": ["prompt"],
|
127
|
+
},
|
128
|
+
)
|
129
|
+
def _delegate_task(rt: Runtime, prompt: str, files: list[str] | None = None) -> str:
|
130
|
+
if getattr(rt, "task_depth", 0) >= 1:
|
131
|
+
return "error: delegation not allowed in sub-tasks"
|
132
|
+
try:
|
133
|
+
tid = _get_manager().start_task(
|
134
|
+
prompt,
|
135
|
+
parent_rt=rt,
|
136
|
+
files=files,
|
137
|
+
parent_depth=getattr(rt, "task_depth", 0),
|
138
|
+
)
|
139
|
+
except RuntimeError as exc:
|
140
|
+
return str(exc)
|
141
|
+
return f"started {tid}"
|
142
|
+
|
143
|
+
|
144
|
+
@tool(
|
145
|
+
name="task_status",
|
146
|
+
description="Check the status of a delegated task.",
|
147
|
+
parameters={
|
148
|
+
"type": "object",
|
149
|
+
"properties": {"task_id": {"type": "string"}},
|
150
|
+
"required": ["task_id"],
|
151
|
+
},
|
152
|
+
)
|
153
|
+
def _task_status(rt: Runtime, task_id: str) -> str:
|
154
|
+
return _get_manager().status(task_id)
|
155
|
+
|
156
|
+
|
157
|
+
@tool(
|
158
|
+
name="collect_file",
|
159
|
+
description="Retrieve a file from a delegated task into the main workspace.",
|
160
|
+
parameters={
|
161
|
+
"type": "object",
|
162
|
+
"properties": {
|
163
|
+
"task_id": {"type": "string"},
|
164
|
+
"path": {"type": "string"},
|
165
|
+
},
|
166
|
+
"required": ["task_id", "path"],
|
167
|
+
},
|
168
|
+
)
|
169
|
+
def _collect_file(rt: Runtime, task_id: str, path: str) -> str:
|
170
|
+
return _get_manager().collect_file(rt, task_id, path)
|
171
|
+
|
172
|
+
|
173
|
+
@tool(
|
174
|
+
name="download_file",
|
175
|
+
description="Return the contents of a file from the workspace (base64 if binary)",
|
176
|
+
parameters={
|
177
|
+
"type": "object",
|
178
|
+
"properties": {
|
179
|
+
"path": {"type": "string"},
|
180
|
+
"binary": {"type": "boolean", "default": False},
|
181
|
+
},
|
182
|
+
"required": ["path"],
|
183
|
+
},
|
184
|
+
)
|
185
|
+
def _download_file(rt: Runtime, path: str, binary: bool = False) -> str:
|
186
|
+
return rt.read_file(path, binary=binary)
|
@@ -0,0 +1,132 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pygent
|
3
|
+
Version: 0.1.13
|
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
|
+
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
|
+
Project-URL: Documentation, https://marianochaves.github.io/pygent
|
7
|
+
Project-URL: Repository, https://github.com/marianochaves/pygent
|
8
|
+
Requires-Python: >=3.9
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Dist: rich>=13.7.0
|
12
|
+
Requires-Dist: openai>=1.0.0
|
13
|
+
Provides-Extra: test
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
15
|
+
Provides-Extra: docs
|
16
|
+
Requires-Dist: mkdocs; extra == "docs"
|
17
|
+
Provides-Extra: docker
|
18
|
+
Requires-Dist: docker>=7.0.0; extra == "docker"
|
19
|
+
Provides-Extra: ui
|
20
|
+
Requires-Dist: gradio; extra == "ui"
|
21
|
+
Dynamic: license-file
|
22
|
+
|
23
|
+
# Pygent
|
24
|
+
|
25
|
+
Pygent is a coding assistant that executes each request inside an isolated Docker container whenever possible. If Docker is unavailable (for instance on some Windows setups) the commands are executed locally instead. Full documentation is available in the `docs/` directory and at [marianochaves.github.io/pygent](https://marianochaves.github.io/pygent/).
|
26
|
+
|
27
|
+
## Features
|
28
|
+
|
29
|
+
* Runs commands in ephemeral containers (default image `python:3.12-slim`).
|
30
|
+
* Integrates with OpenAI-compatible models to orchestrate each step.
|
31
|
+
* Persists the conversation history during the session.
|
32
|
+
* Provides a small Python API for use in other projects.
|
33
|
+
* Optional web interface via `pygent-ui`.
|
34
|
+
* Register your own tools and customise the system prompt.
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
|
38
|
+
Installing from source is recommended:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
pip install -e .
|
42
|
+
```
|
43
|
+
|
44
|
+
Python ≥ 3.9 is required. The package now bundles the `openai` client for model access.
|
45
|
+
To run commands in Docker containers also install `pygent[docker]`.
|
46
|
+
|
47
|
+
## Configuration
|
48
|
+
|
49
|
+
Behaviour can be adjusted via environment variables (see `docs/configuration.md` for a complete list):
|
50
|
+
|
51
|
+
* `OPENAI_API_KEY` – key used to access the OpenAI API.
|
52
|
+
Set this to your API key or a key from any compatible provider.
|
53
|
+
* `OPENAI_BASE_URL` – base URL for OpenAI-compatible APIs
|
54
|
+
(defaults to ``https://api.openai.com/v1``).
|
55
|
+
* `PYGENT_MODEL` – model name used for requests (default `gpt-4.1-mini`).
|
56
|
+
* `PYGENT_IMAGE` – Docker image to create the container (default `python:3.12-slim`).
|
57
|
+
* `PYGENT_USE_DOCKER` – set to `0` to disable Docker and run locally.
|
58
|
+
* `PYGENT_MAX_TASKS` – maximum number of concurrent delegated tasks (default `3`).
|
59
|
+
|
60
|
+
## CLI usage
|
61
|
+
|
62
|
+
After installing run:
|
63
|
+
|
64
|
+
```bash
|
65
|
+
pygent
|
66
|
+
```
|
67
|
+
|
68
|
+
Use `--docker` to run commands inside a container (requires
|
69
|
+
`pygent[docker]`). Use `--no-docker` or set `PYGENT_USE_DOCKER=0`
|
70
|
+
to force local execution.
|
71
|
+
|
72
|
+
Type messages normally; use `/exit` to end the session. Each command is executed
|
73
|
+
in the container and the result shown in the terminal.
|
74
|
+
Interactive programs that expect input (e.g. running `python` without a script)
|
75
|
+
are not supported and will exit immediately.
|
76
|
+
For a minimal web interface run `pygent-ui` instead (requires `pygent[ui]`).
|
77
|
+
|
78
|
+
|
79
|
+
## API usage
|
80
|
+
|
81
|
+
You can also interact directly with the Python code:
|
82
|
+
|
83
|
+
```python
|
84
|
+
from pygent import Agent
|
85
|
+
|
86
|
+
ag = Agent()
|
87
|
+
ag.step("echo 'Hello World'")
|
88
|
+
# ... more steps
|
89
|
+
ag.runtime.cleanup()
|
90
|
+
```
|
91
|
+
|
92
|
+
See the [examples](https://github.com/marianochaves/pygent/tree/main/examples) folder for more complete scripts. Models can be swapped by
|
93
|
+
passing an object implementing the ``Model`` interface when creating the
|
94
|
+
``Agent``. The default uses an OpenAI-compatible API, but custom models are
|
95
|
+
easy to plug in.
|
96
|
+
|
97
|
+
### Using OpenAI and other providers
|
98
|
+
|
99
|
+
Set your OpenAI key:
|
100
|
+
|
101
|
+
```bash
|
102
|
+
export OPENAI_API_KEY="sk-..."
|
103
|
+
```
|
104
|
+
|
105
|
+
To use a different provider, set `OPENAI_BASE_URL` to the provider
|
106
|
+
endpoint and keep `OPENAI_API_KEY` pointing to the correct key:
|
107
|
+
|
108
|
+
```bash
|
109
|
+
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
|
110
|
+
export OPENAI_API_KEY="your-provider-key"
|
111
|
+
```
|
112
|
+
|
113
|
+
## Development
|
114
|
+
|
115
|
+
1. Install the test dependencies:
|
116
|
+
|
117
|
+
```bash
|
118
|
+
pip install -e .[test]
|
119
|
+
```
|
120
|
+
|
121
|
+
2. Run the test suite:
|
122
|
+
|
123
|
+
```bash
|
124
|
+
pytest
|
125
|
+
```
|
126
|
+
|
127
|
+
Use `mkdocs serve` to build the documentation locally.
|
128
|
+
|
129
|
+
## License
|
130
|
+
|
131
|
+
This project is released under the MIT license. See the `LICENSE` file for details.
|
132
|
+
|
@@ -10,6 +10,7 @@ pygent/models.py
|
|
10
10
|
pygent/openai_compat.py
|
11
11
|
pygent/py.typed
|
12
12
|
pygent/runtime.py
|
13
|
+
pygent/task_manager.py
|
13
14
|
pygent/tools.py
|
14
15
|
pygent/ui.py
|
15
16
|
pygent.egg-info/PKG-INFO
|
@@ -22,5 +23,6 @@ tests/test_autorun.py
|
|
22
23
|
tests/test_custom_model.py
|
23
24
|
tests/test_error_handling.py
|
24
25
|
tests/test_runtime.py
|
26
|
+
tests/test_tasks.py
|
25
27
|
tests/test_tools.py
|
26
28
|
tests/test_version.py
|
@@ -1,7 +1,8 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pygent"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.13"
|
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
|
+
readme = "README.md"
|
5
6
|
authors = [ { name = "Mariano Chaves", email = "mchaves.software@gmail.com" } ]
|
6
7
|
requires-python = ">=3.9"
|
7
8
|
dependencies = [
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import types
|
4
|
+
import time
|
5
|
+
|
6
|
+
sys.modules.setdefault('openai', types.ModuleType('openai'))
|
7
|
+
sys.modules.setdefault('docker', types.ModuleType('docker'))
|
8
|
+
|
9
|
+
# mocks for rich
|
10
|
+
rich_mod = types.ModuleType('rich')
|
11
|
+
console_mod = types.ModuleType('console')
|
12
|
+
panel_mod = types.ModuleType('panel')
|
13
|
+
markdown_mod = types.ModuleType('markdown')
|
14
|
+
syntax_mod = types.ModuleType('syntax')
|
15
|
+
console_mod.Console = lambda *a, **k: type('C', (), {'print': lambda *a, **k: None})()
|
16
|
+
panel_mod.Panel = lambda *a, **k: None
|
17
|
+
markdown_mod.Markdown = lambda *a, **k: None
|
18
|
+
syntax_mod.Syntax = lambda *a, **k: None
|
19
|
+
sys.modules.setdefault('rich', rich_mod)
|
20
|
+
sys.modules.setdefault('rich.console', console_mod)
|
21
|
+
sys.modules.setdefault('rich.panel', panel_mod)
|
22
|
+
sys.modules.setdefault('rich.markdown', markdown_mod)
|
23
|
+
sys.modules.setdefault('rich.syntax', syntax_mod)
|
24
|
+
|
25
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
26
|
+
|
27
|
+
from pygent import Agent
|
28
|
+
from pygent import openai_compat
|
29
|
+
from pygent.task_manager import TaskManager
|
30
|
+
from pygent.runtime import Runtime
|
31
|
+
from pygent import tools
|
32
|
+
|
33
|
+
class DummyModel:
|
34
|
+
def __init__(self):
|
35
|
+
self.count = 0
|
36
|
+
def chat(self, messages, model, tool_schemas):
|
37
|
+
self.count += 1
|
38
|
+
if self.count == 1:
|
39
|
+
return openai_compat.Message(
|
40
|
+
role='assistant',
|
41
|
+
content=None,
|
42
|
+
tool_calls=[
|
43
|
+
openai_compat.ToolCall(
|
44
|
+
id='1',
|
45
|
+
type='function',
|
46
|
+
function=openai_compat.ToolCallFunction(
|
47
|
+
name='write_file',
|
48
|
+
arguments='{"path": "foo.txt", "content": "bar"}'
|
49
|
+
)
|
50
|
+
)
|
51
|
+
]
|
52
|
+
)
|
53
|
+
else:
|
54
|
+
return openai_compat.Message(
|
55
|
+
role='assistant',
|
56
|
+
content=None,
|
57
|
+
tool_calls=[
|
58
|
+
openai_compat.ToolCall(
|
59
|
+
id='2',
|
60
|
+
type='function',
|
61
|
+
function=openai_compat.ToolCallFunction(
|
62
|
+
name='stop',
|
63
|
+
arguments='{}'
|
64
|
+
)
|
65
|
+
)
|
66
|
+
]
|
67
|
+
)
|
68
|
+
|
69
|
+
class SlowModel(DummyModel):
|
70
|
+
"""DummyModel variant that sleeps to simulate long-running tasks."""
|
71
|
+
|
72
|
+
def chat(self, messages, model, tool_schemas):
|
73
|
+
time.sleep(0.1)
|
74
|
+
return super().chat(messages, model, tool_schemas)
|
75
|
+
|
76
|
+
def make_agent():
|
77
|
+
return Agent(runtime=Runtime(use_docker=False), model=DummyModel())
|
78
|
+
|
79
|
+
def make_slow_agent():
|
80
|
+
return Agent(runtime=Runtime(use_docker=False), model=SlowModel())
|
81
|
+
|
82
|
+
def test_delegate_and_collect_file(tmp_path):
|
83
|
+
tm = TaskManager(agent_factory=make_agent)
|
84
|
+
tools._task_manager = tm
|
85
|
+
|
86
|
+
rt = Runtime(use_docker=False)
|
87
|
+
task_id = tools._delegate_task(rt, prompt='run')
|
88
|
+
tid = task_id.split()[-1]
|
89
|
+
tm.tasks[tid].thread.join()
|
90
|
+
|
91
|
+
status = tools._task_status(Runtime(use_docker=False), task_id=tid)
|
92
|
+
assert status == 'finished'
|
93
|
+
|
94
|
+
main_rt = Runtime(use_docker=False)
|
95
|
+
msg = tools._collect_file(main_rt, task_id=tid, path='foo.txt')
|
96
|
+
assert 'Retrieved' in msg
|
97
|
+
copied = main_rt.base_dir / 'foo.txt'
|
98
|
+
assert copied.exists() and copied.read_text() == 'bar'
|
99
|
+
main_rt.cleanup()
|
100
|
+
|
101
|
+
|
102
|
+
def test_delegate_with_files():
|
103
|
+
tm = TaskManager(agent_factory=make_agent)
|
104
|
+
tools._task_manager = tm
|
105
|
+
|
106
|
+
rt = Runtime(use_docker=False)
|
107
|
+
rt.write_file("data.txt", "hello")
|
108
|
+
tid_msg = tools._delegate_task(rt, prompt="run", files=["data.txt"])
|
109
|
+
tid = tid_msg.split()[-1]
|
110
|
+
tm.tasks[tid].thread.join()
|
111
|
+
|
112
|
+
child_path = tm.tasks[tid].agent.runtime.base_dir / "data.txt"
|
113
|
+
assert child_path.exists() and child_path.read_text() == "hello"
|
114
|
+
|
115
|
+
|
116
|
+
def test_download_file():
|
117
|
+
rt = Runtime(use_docker=False)
|
118
|
+
rt.write_file("sample.txt", "hi")
|
119
|
+
content = tools._download_file(rt, path="sample.txt")
|
120
|
+
assert content == "hi"
|
121
|
+
|
122
|
+
|
123
|
+
class DelegateModel:
|
124
|
+
def __init__(self):
|
125
|
+
self.count = 0
|
126
|
+
def chat(self, messages, model, tool_schemas):
|
127
|
+
self.count += 1
|
128
|
+
if self.count == 1:
|
129
|
+
return openai_compat.Message(
|
130
|
+
role='assistant',
|
131
|
+
content=None,
|
132
|
+
tool_calls=[
|
133
|
+
openai_compat.ToolCall(
|
134
|
+
id='1',
|
135
|
+
type='function',
|
136
|
+
function=openai_compat.ToolCallFunction(
|
137
|
+
name='delegate_task',
|
138
|
+
arguments='{"prompt": "noop"}'
|
139
|
+
)
|
140
|
+
)
|
141
|
+
]
|
142
|
+
)
|
143
|
+
else:
|
144
|
+
return openai_compat.Message(
|
145
|
+
role='assistant',
|
146
|
+
content=None,
|
147
|
+
tool_calls=[
|
148
|
+
openai_compat.ToolCall(
|
149
|
+
id='2',
|
150
|
+
type='function',
|
151
|
+
function=openai_compat.ToolCallFunction(
|
152
|
+
name='stop',
|
153
|
+
arguments='{}'
|
154
|
+
)
|
155
|
+
)
|
156
|
+
]
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
def make_delegate_agent():
|
161
|
+
return Agent(runtime=Runtime(use_docker=False), model=DelegateModel())
|
162
|
+
|
163
|
+
|
164
|
+
def test_no_nested_delegation():
|
165
|
+
tm = TaskManager(agent_factory=make_delegate_agent, max_tasks=2)
|
166
|
+
tools._task_manager = tm
|
167
|
+
|
168
|
+
tid_msg = tools._delegate_task(Runtime(use_docker=False), prompt='run')
|
169
|
+
tid = tid_msg.split()[-1]
|
170
|
+
tm.tasks[tid].thread.join()
|
171
|
+
|
172
|
+
# No new tasks should have been created inside the sub-agent
|
173
|
+
assert len(tm.tasks) == 1
|
174
|
+
|
175
|
+
|
176
|
+
def test_task_limit():
|
177
|
+
tm = TaskManager(agent_factory=make_slow_agent, max_tasks=1)
|
178
|
+
tools._task_manager = tm
|
179
|
+
|
180
|
+
first = tools._delegate_task(Runtime(use_docker=False), prompt='run')
|
181
|
+
assert first.startswith('started')
|
182
|
+
tid = first.split()[-1]
|
183
|
+
|
184
|
+
second = tools._delegate_task(Runtime(use_docker=False), prompt='run')
|
185
|
+
assert 'max' in second
|
186
|
+
|
187
|
+
tm.tasks[tid].thread.join()
|
@@ -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
|
+
|
pygent-0.1.11/PKG-INFO
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: pygent
|
3
|
-
Version: 0.1.11
|
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
|
-
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
|
-
Project-URL: Documentation, https://marianochaves.github.io/pygent
|
7
|
-
Project-URL: Repository, https://github.com/marianochaves/pygent
|
8
|
-
Requires-Python: >=3.9
|
9
|
-
License-File: LICENSE
|
10
|
-
Requires-Dist: rich>=13.7.0
|
11
|
-
Requires-Dist: openai>=1.0.0
|
12
|
-
Provides-Extra: test
|
13
|
-
Requires-Dist: pytest; extra == "test"
|
14
|
-
Provides-Extra: docs
|
15
|
-
Requires-Dist: mkdocs; extra == "docs"
|
16
|
-
Provides-Extra: docker
|
17
|
-
Requires-Dist: docker>=7.0.0; extra == "docker"
|
18
|
-
Provides-Extra: ui
|
19
|
-
Requires-Dist: gradio; extra == "ui"
|
20
|
-
Dynamic: license-file
|
pygent-0.1.11/pygent/tools.py
DELETED
@@ -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}"
|
@@ -1,20 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: pygent
|
3
|
-
Version: 0.1.11
|
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
|
-
Author-email: Mariano Chaves <mchaves.software@gmail.com>
|
6
|
-
Project-URL: Documentation, https://marianochaves.github.io/pygent
|
7
|
-
Project-URL: Repository, https://github.com/marianochaves/pygent
|
8
|
-
Requires-Python: >=3.9
|
9
|
-
License-File: LICENSE
|
10
|
-
Requires-Dist: rich>=13.7.0
|
11
|
-
Requires-Dist: openai>=1.0.0
|
12
|
-
Provides-Extra: test
|
13
|
-
Requires-Dist: pytest; extra == "test"
|
14
|
-
Provides-Extra: docs
|
15
|
-
Requires-Dist: mkdocs; extra == "docs"
|
16
|
-
Provides-Extra: docker
|
17
|
-
Requires-Dist: docker>=7.0.0; extra == "docker"
|
18
|
-
Provides-Extra: ui
|
19
|
-
Requires-Dist: gradio; extra == "ui"
|
20
|
-
Dynamic: license-file
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|