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.
Files changed (32) hide show
  1. {pygent-0.1.14 → pygent-0.1.15}/PKG-INFO +2 -1
  2. {pygent-0.1.14 → pygent-0.1.15}/README.md +1 -0
  3. {pygent-0.1.14 → pygent-0.1.15}/pygent/__init__.py +3 -0
  4. {pygent-0.1.14 → pygent-0.1.15}/pygent/agent.py +17 -9
  5. {pygent-0.1.14 → pygent-0.1.15}/pygent/cli.py +5 -1
  6. pygent-0.1.15/pygent/config.py +40 -0
  7. {pygent-0.1.14 → pygent-0.1.15}/pygent/runtime.py +21 -1
  8. {pygent-0.1.14 → pygent-0.1.15}/pygent/task_manager.py +23 -3
  9. {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/PKG-INFO +2 -1
  10. {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/SOURCES.txt +2 -0
  11. {pygent-0.1.14 → pygent-0.1.15}/pyproject.toml +1 -1
  12. pygent-0.1.15/tests/test_config.py +56 -0
  13. {pygent-0.1.14 → pygent-0.1.15}/LICENSE +0 -0
  14. {pygent-0.1.14 → pygent-0.1.15}/pygent/__main__.py +0 -0
  15. {pygent-0.1.14 → pygent-0.1.15}/pygent/errors.py +0 -0
  16. {pygent-0.1.14 → pygent-0.1.15}/pygent/models.py +0 -0
  17. {pygent-0.1.14 → pygent-0.1.15}/pygent/openai_compat.py +0 -0
  18. {pygent-0.1.14 → pygent-0.1.15}/pygent/py.typed +0 -0
  19. {pygent-0.1.14 → pygent-0.1.15}/pygent/tools.py +0 -0
  20. {pygent-0.1.14 → pygent-0.1.15}/pygent/ui.py +0 -0
  21. {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/dependency_links.txt +0 -0
  22. {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/entry_points.txt +0 -0
  23. {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/requires.txt +0 -0
  24. {pygent-0.1.14 → pygent-0.1.15}/pygent.egg-info/top_level.txt +0 -0
  25. {pygent-0.1.14 → pygent-0.1.15}/setup.cfg +0 -0
  26. {pygent-0.1.14 → pygent-0.1.15}/tests/test_autorun.py +0 -0
  27. {pygent-0.1.14 → pygent-0.1.15}/tests/test_custom_model.py +0 -0
  28. {pygent-0.1.14 → pygent-0.1.15}/tests/test_error_handling.py +0 -0
  29. {pygent-0.1.14 → pygent-0.1.15}/tests/test_runtime.py +0 -0
  30. {pygent-0.1.14 → pygent-0.1.15}/tests/test_tasks.py +0 -0
  31. {pygent-0.1.14 → pygent-0.1.15}/tests/test_tools.py +0 -0
  32. {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.14
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
- system_msg: str = SYSTEM_MSG
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 .agent import run_interactive
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__(self, image: str | None = None, use_docker: bool | None = None) -> None:
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[[], "Agent"] | None = None,
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
- self.agent_factory = agent_factory or Agent
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
- agent = self.agent_factory()
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.14
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.14"
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