pygent 0.1.14__tar.gz → 0.1.15__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.14 → pygent-0.1.15}/PKG-INFO +2 -1
- {pygent-0.1.14 → pygent-0.1.15}/README.md +1 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/__init__.py +3 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/agent.py +17 -9
- {pygent-0.1.14 → pygent-0.1.15}/pygent/cli.py +5 -1
- pygent-0.1.15/pygent/config.py +40 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/runtime.py +21 -1
- {pygent-0.1.14 → pygent-0.1.15}/pygent/task_manager.py +23 -3
- {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/PKG-INFO +2 -1
- {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/SOURCES.txt +2 -0
- {pygent-0.1.14 → pygent-0.1.15}/pyproject.toml +1 -1
- pygent-0.1.15/tests/test_config.py +56 -0
- {pygent-0.1.14 → pygent-0.1.15}/LICENSE +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/__main__.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/errors.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/models.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/openai_compat.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/py.typed +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/tools.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent/ui.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/dependency_links.txt +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/entry_points.txt +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/requires.txt +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/top_level.txt +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/setup.cfg +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_autorun.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_custom_model.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_error_handling.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_runtime.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_tasks.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_tools.py +0 -0
- {pygent-0.1.14 → pygent-0.1.15}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pygent
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.15
|
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
|
@@ -68,6 +68,7 @@ pygent
|
|
68
68
|
Use `--docker` to run commands inside a container (requires
|
69
69
|
`pygent[docker]`). Use `--no-docker` or set `PYGENT_USE_DOCKER=0`
|
70
70
|
to force local execution.
|
71
|
+
Pass `--config path/to/pygent.toml` to load settings from a file.
|
71
72
|
|
72
73
|
Type messages normally; use `/exit` to end the session. Each command is executed
|
73
74
|
in the container and the result shown in the terminal.
|
@@ -46,6 +46,7 @@ pygent
|
|
46
46
|
Use `--docker` to run commands inside a container (requires
|
47
47
|
`pygent[docker]`). Use `--no-docker` or set `PYGENT_USE_DOCKER=0`
|
48
48
|
to force local execution.
|
49
|
+
Pass `--config path/to/pygent.toml` to load settings from a file.
|
49
50
|
|
50
51
|
Type messages normally; use `/exit` to end the session. Each command is executed
|
51
52
|
in the container and the result shown in the terminal.
|
@@ -1,6 +1,8 @@
|
|
1
1
|
"""Pygent package."""
|
2
2
|
from importlib import metadata as _metadata
|
3
3
|
|
4
|
+
from .config import load_config
|
5
|
+
|
4
6
|
try:
|
5
7
|
__version__: str = _metadata.version(__name__)
|
6
8
|
except _metadata.PackageNotFoundError: # pragma: no cover - fallback for tests
|
@@ -15,6 +17,7 @@ from .task_manager import TaskManager # noqa: E402,F401
|
|
15
17
|
__all__ = [
|
16
18
|
"Agent",
|
17
19
|
"run_interactive",
|
20
|
+
"load_config",
|
18
21
|
"Model",
|
19
22
|
"OpenAIModel",
|
20
23
|
"PygentError",
|
@@ -16,15 +16,20 @@ from .runtime import Runtime
|
|
16
16
|
from . import tools
|
17
17
|
from .models import Model, OpenAIModel
|
18
18
|
|
19
|
+
DEFAULT_PERSONA = os.getenv("PYGENT_PERSONA", "You are Pygent, a sandboxed coding assistant.")
|
20
|
+
|
21
|
+
def build_system_msg(persona: str) -> str:
|
22
|
+
return (
|
23
|
+
f"{persona}\n"
|
24
|
+
"Respond with JSON when you need to use a tool."
|
25
|
+
"If you need to stop or finished you task, call the `stop` tool.\n"
|
26
|
+
"You can use the following tools:\n"
|
27
|
+
f"{json.dumps(tools.TOOL_SCHEMAS, indent=2)}\n"
|
28
|
+
"You can also use the `continue` tool to request user input or continue the conversation.\n"
|
29
|
+
)
|
30
|
+
|
19
31
|
DEFAULT_MODEL = os.getenv("PYGENT_MODEL", "gpt-4.1-mini")
|
20
|
-
SYSTEM_MSG = (
|
21
|
-
"You are Pygent, a sandboxed coding assistant.\n"
|
22
|
-
"Respond with JSON when you need to use a tool."
|
23
|
-
"If you need to stop or finished you task, 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 request user input or continue the conversation.\n"
|
27
|
-
)
|
32
|
+
SYSTEM_MSG = build_system_msg(DEFAULT_PERSONA)
|
28
33
|
|
29
34
|
console = Console()
|
30
35
|
|
@@ -36,10 +41,13 @@ class Agent:
|
|
36
41
|
runtime: Runtime = field(default_factory=Runtime)
|
37
42
|
model: Model = field(default_factory=OpenAIModel)
|
38
43
|
model_name: str = DEFAULT_MODEL
|
39
|
-
|
44
|
+
persona: str = DEFAULT_PERSONA
|
45
|
+
system_msg: str = field(default_factory=lambda: build_system_msg(DEFAULT_PERSONA))
|
40
46
|
history: List[Dict[str, Any]] = field(default_factory=list)
|
41
47
|
|
42
48
|
def __post_init__(self) -> None:
|
49
|
+
if not self.system_msg:
|
50
|
+
self.system_msg = build_system_msg(self.persona)
|
43
51
|
if not self.history:
|
44
52
|
self.history.append({"role": "system", "content": self.system_msg})
|
45
53
|
|
@@ -1,12 +1,16 @@
|
|
1
1
|
"""Command-line entry point for Pygent."""
|
2
2
|
import argparse
|
3
3
|
|
4
|
-
from .
|
4
|
+
from .config import load_config
|
5
5
|
|
6
6
|
def main() -> None: # pragma: no cover
|
7
7
|
parser = argparse.ArgumentParser(prog="pygent")
|
8
8
|
parser.add_argument("--docker", dest="use_docker", action="store_true", help="run commands in a Docker container")
|
9
9
|
parser.add_argument("--no-docker", dest="use_docker", action="store_false", help="run locally")
|
10
|
+
parser.add_argument("-c", "--config", help="path to configuration file")
|
10
11
|
parser.set_defaults(use_docker=None)
|
11
12
|
args = parser.parse_args()
|
13
|
+
load_config(args.config)
|
14
|
+
from .agent import run_interactive
|
15
|
+
|
12
16
|
run_interactive(use_docker=args.use_docker)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import os
|
2
|
+
import tomllib
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, Dict
|
5
|
+
|
6
|
+
DEFAULT_CONFIG_FILES = [
|
7
|
+
Path("pygent.toml"),
|
8
|
+
Path.home() / ".pygent.toml",
|
9
|
+
]
|
10
|
+
|
11
|
+
def load_config(path: str | os.PathLike[str] | None = None) -> Dict[str, Any]:
|
12
|
+
"""Load configuration from a TOML file and set environment variables.
|
13
|
+
|
14
|
+
Environment variables already set take precedence over file values.
|
15
|
+
Returns the configuration dictionary.
|
16
|
+
"""
|
17
|
+
config: Dict[str, Any] = {}
|
18
|
+
paths = [Path(path)] if path else DEFAULT_CONFIG_FILES
|
19
|
+
for p in paths:
|
20
|
+
if p.is_file():
|
21
|
+
with p.open("rb") as fh:
|
22
|
+
try:
|
23
|
+
data = tomllib.load(fh)
|
24
|
+
except Exception:
|
25
|
+
continue
|
26
|
+
config.update(data)
|
27
|
+
# update environment without overwriting existing values
|
28
|
+
if "persona" in config and "PYGENT_PERSONA" not in os.environ:
|
29
|
+
os.environ["PYGENT_PERSONA"] = str(config["persona"])
|
30
|
+
if "task_personas" in config and "PYGENT_TASK_PERSONAS" not in os.environ:
|
31
|
+
if isinstance(config["task_personas"], list):
|
32
|
+
os.environ["PYGENT_TASK_PERSONAS"] = os.pathsep.join(str(p) for p in config["task_personas"])
|
33
|
+
else:
|
34
|
+
os.environ["PYGENT_TASK_PERSONAS"] = str(config["task_personas"])
|
35
|
+
if "initial_files" in config and "PYGENT_INIT_FILES" not in os.environ:
|
36
|
+
if isinstance(config["initial_files"], list):
|
37
|
+
os.environ["PYGENT_INIT_FILES"] = os.pathsep.join(str(p) for p in config["initial_files"])
|
38
|
+
else:
|
39
|
+
os.environ["PYGENT_INIT_FILES"] = str(config["initial_files"])
|
40
|
+
return config
|
@@ -18,8 +18,18 @@ except Exception: # pragma: no cover - optional dependency
|
|
18
18
|
class Runtime:
|
19
19
|
"""Executes commands in a Docker container or locally if Docker is unavailable."""
|
20
20
|
|
21
|
-
def __init__(
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
image: str | None = None,
|
24
|
+
use_docker: bool | None = None,
|
25
|
+
initial_files: list[str] | None = None,
|
26
|
+
) -> None:
|
22
27
|
self.base_dir = Path(tempfile.mkdtemp(prefix="pygent_"))
|
28
|
+
if initial_files is None:
|
29
|
+
env_files = os.getenv("PYGENT_INIT_FILES")
|
30
|
+
if env_files:
|
31
|
+
initial_files = [f.strip() for f in env_files.split(os.pathsep) if f.strip()]
|
32
|
+
self._initial_files = initial_files or []
|
23
33
|
self.image = image or os.getenv("PYGENT_IMAGE", "python:3.12-slim")
|
24
34
|
env_opt = os.getenv("PYGENT_USE_DOCKER")
|
25
35
|
if use_docker is None:
|
@@ -46,6 +56,16 @@ class Runtime:
|
|
46
56
|
self.client = None
|
47
57
|
self.container = None
|
48
58
|
|
59
|
+
# populate workspace with initial files
|
60
|
+
for fp in self._initial_files:
|
61
|
+
src = Path(fp).expanduser()
|
62
|
+
dest = self.base_dir / src.name
|
63
|
+
if src.is_dir():
|
64
|
+
shutil.copytree(src, dest, dirs_exist_ok=True)
|
65
|
+
elif src.exists():
|
66
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
67
|
+
shutil.copy(src, dest)
|
68
|
+
|
49
69
|
# ---------------- public API ----------------
|
50
70
|
def bash(self, cmd: str, timeout: int = 30) -> str:
|
51
71
|
"""Run a command in the container or locally and return the output.
|
@@ -30,14 +30,24 @@ class TaskManager:
|
|
30
30
|
|
31
31
|
def __init__(
|
32
32
|
self,
|
33
|
-
agent_factory: Callable[
|
33
|
+
agent_factory: Callable[..., "Agent"] | None = None,
|
34
34
|
max_tasks: int | None = None,
|
35
|
+
personas: list[str] | None = None,
|
35
36
|
) -> None:
|
36
37
|
from .agent import Agent # local import to avoid circular dependency
|
37
38
|
|
38
39
|
env_max = os.getenv("PYGENT_MAX_TASKS")
|
39
40
|
self.max_tasks = max_tasks if max_tasks is not None else int(env_max or "3")
|
40
|
-
|
41
|
+
if agent_factory is None:
|
42
|
+
self.agent_factory = lambda p=None: Agent(persona=p)
|
43
|
+
else:
|
44
|
+
self.agent_factory = agent_factory
|
45
|
+
env_personas = os.getenv("PYGENT_TASK_PERSONAS")
|
46
|
+
if personas is None and env_personas:
|
47
|
+
personas = [p.strip() for p in env_personas.split(os.pathsep) if p.strip()]
|
48
|
+
default_persona = os.getenv("PYGENT_PERSONA", "You are Pygent, a sandboxed coding assistant.")
|
49
|
+
self.personas = personas or [default_persona]
|
50
|
+
self._persona_idx = 0
|
41
51
|
self.tasks: Dict[str, Task] = {}
|
42
52
|
self._lock = threading.Lock()
|
43
53
|
|
@@ -67,7 +77,17 @@ class TaskManager:
|
|
67
77
|
env = os.getenv("PYGENT_TASK_TIMEOUT")
|
68
78
|
task_timeout = float(env) if env else 60*20 # default 20 minutes
|
69
79
|
|
70
|
-
|
80
|
+
persona = self.personas[self._persona_idx % len(self.personas)]
|
81
|
+
self._persona_idx += 1
|
82
|
+
try:
|
83
|
+
agent = self.agent_factory(persona)
|
84
|
+
except TypeError:
|
85
|
+
agent = self.agent_factory()
|
86
|
+
setattr(agent, "persona", persona)
|
87
|
+
if not getattr(agent, "system_msg", None):
|
88
|
+
from .agent import build_system_msg # lazy import
|
89
|
+
|
90
|
+
agent.system_msg = build_system_msg(persona)
|
71
91
|
setattr(agent.runtime, "task_depth", parent_depth + 1)
|
72
92
|
if files:
|
73
93
|
for fp in files:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pygent
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.15
|
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
|
@@ -68,6 +68,7 @@ pygent
|
|
68
68
|
Use `--docker` to run commands inside a container (requires
|
69
69
|
`pygent[docker]`). Use `--no-docker` or set `PYGENT_USE_DOCKER=0`
|
70
70
|
to force local execution.
|
71
|
+
Pass `--config path/to/pygent.toml` to load settings from a file.
|
71
72
|
|
72
73
|
Type messages normally; use `/exit` to end the session. Each command is executed
|
73
74
|
in the container and the result shown in the terminal.
|
@@ -5,6 +5,7 @@ pygent/__init__.py
|
|
5
5
|
pygent/__main__.py
|
6
6
|
pygent/agent.py
|
7
7
|
pygent/cli.py
|
8
|
+
pygent/config.py
|
8
9
|
pygent/errors.py
|
9
10
|
pygent/models.py
|
10
11
|
pygent/openai_compat.py
|
@@ -20,6 +21,7 @@ pygent.egg-info/entry_points.txt
|
|
20
21
|
pygent.egg-info/requires.txt
|
21
22
|
pygent.egg-info/top_level.txt
|
22
23
|
tests/test_autorun.py
|
24
|
+
tests/test_config.py
|
23
25
|
tests/test_custom_model.py
|
24
26
|
tests/test_error_handling.py
|
25
27
|
tests/test_runtime.py
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pygent"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.15"
|
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
|
readme = "README.md"
|
6
6
|
authors = [ { name = "Mariano Chaves", email = "mchaves.software@gmail.com" } ]
|
@@ -0,0 +1,56 @@
|
|
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
|
+
|
8
|
+
# minimal mocks for rich
|
9
|
+
rich_mod = types.ModuleType('rich')
|
10
|
+
console_mod = types.ModuleType('console')
|
11
|
+
panel_mod = types.ModuleType('panel')
|
12
|
+
markdown_mod = types.ModuleType('markdown')
|
13
|
+
console_mod.Console = lambda *a, **k: None
|
14
|
+
panel_mod.Panel = lambda *a, **k: None
|
15
|
+
markdown_mod.Markdown = lambda *a, **k: None
|
16
|
+
sys.modules.setdefault('rich', rich_mod)
|
17
|
+
sys.modules.setdefault('rich.console', console_mod)
|
18
|
+
sys.modules.setdefault('rich.panel', panel_mod)
|
19
|
+
sys.modules.setdefault('rich.markdown', markdown_mod)
|
20
|
+
|
21
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
22
|
+
|
23
|
+
from pygent.config import load_config
|
24
|
+
from pygent.runtime import Runtime
|
25
|
+
from pygent.task_manager import TaskManager
|
26
|
+
|
27
|
+
|
28
|
+
def test_load_config(tmp_path, monkeypatch):
|
29
|
+
cfg = tmp_path / "pygent.toml"
|
30
|
+
cfg.write_text('persona="bot"\ntask_personas=["a","b"]\ninitial_files=["seed.txt"]')
|
31
|
+
(tmp_path / "seed.txt").write_text("seed")
|
32
|
+
monkeypatch.chdir(tmp_path)
|
33
|
+
monkeypatch.delenv("PYGENT_PERSONA", raising=False)
|
34
|
+
monkeypatch.delenv("PYGENT_TASK_PERSONAS", raising=False)
|
35
|
+
monkeypatch.delenv("PYGENT_INIT_FILES", raising=False)
|
36
|
+
load_config()
|
37
|
+
assert os.getenv("PYGENT_PERSONA") == "bot"
|
38
|
+
assert os.getenv("PYGENT_TASK_PERSONAS") == os.pathsep.join(["a", "b"])
|
39
|
+
assert os.getenv("PYGENT_INIT_FILES") == "seed.txt"
|
40
|
+
rt = Runtime(use_docker=False)
|
41
|
+
assert (rt.base_dir / "seed.txt").exists()
|
42
|
+
rt.cleanup()
|
43
|
+
|
44
|
+
|
45
|
+
def test_task_manager_personas(monkeypatch):
|
46
|
+
created = []
|
47
|
+
def factory(p):
|
48
|
+
created.append(p)
|
49
|
+
ag = types.SimpleNamespace(runtime=Runtime(use_docker=False), model=None, persona=p)
|
50
|
+
ag.run_until_stop = lambda *a, **k: None
|
51
|
+
return ag
|
52
|
+
tm = TaskManager(agent_factory=factory, personas=["one","two"])
|
53
|
+
tm.start_task("noop", Runtime(use_docker=False))
|
54
|
+
tm.start_task("noop", Runtime(use_docker=False))
|
55
|
+
tm.tasks[next(iter(tm.tasks))].thread.join()
|
56
|
+
assert created == ["one","two"]
|
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
|
File without changes
|
File without changes
|