pygent 0.1.14__py3-none-any.whl → 0.1.15__py3-none-any.whl

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/__init__.py CHANGED
@@ -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",
pygent/agent.py CHANGED
@@ -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
 
pygent/cli.py CHANGED
@@ -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)
pygent/config.py ADDED
@@ -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
pygent/runtime.py CHANGED
@@ -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.
pygent/task_manager.py CHANGED
@@ -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.
@@ -0,0 +1,19 @@
1
+ pygent/__init__.py,sha256=PnTyTP8ObRQfKN0_8-BendG2gGiZhPz5iP9URrlPVwU,776
2
+ pygent/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ pygent/agent.py,sha256=0yo2K47PgvQJgH_y2xCCkjINks1pYoTQpztPn2J0srk,4437
4
+ pygent/cli.py,sha256=MixuFYGWdZXka6p6ccl4uEoGKFAS5l3xDdUPzcq7y3g,670
5
+ pygent/config.py,sha256=4Yn7kV62RaXa-DI5IqurgWvFZNNykjS0PS4MsUrFEzo,1636
6
+ pygent/errors.py,sha256=s5FBg_v94coSgMh7cfkP4hVXafViGYgCY8QiT698-c4,155
7
+ pygent/models.py,sha256=j3670gjUtvQRGZ5wqGDcQ7ZJVTdT5WiwL7nWTokeYzg,1141
8
+ pygent/openai_compat.py,sha256=cyWFtXt6sDfOlsZd3FuRxbcZMm3WU-DLPBQpbmcuiW8,2617
9
+ pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
10
+ pygent/runtime.py,sha256=dXk4mcYWdc3UzWN4WgyH-fsAUOlqR9L7cYAAf55Gu50,5121
11
+ pygent/task_manager.py,sha256=pO4A9nAfeUylHc2h94K5JUPXTy8MXEtBbw_oYb8oOQk,5106
12
+ pygent/tools.py,sha256=xHpUpG2QcBEvwTilpw8BDlYWY3KG9mItSt0FoKNkohA,5585
13
+ pygent/ui.py,sha256=xqPAvweghPOBBvoD72HzhN6zlXew_3inb8AN7Ck2zpQ,1328
14
+ pygent-0.1.15.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
15
+ pygent-0.1.15.dist-info/METADATA,sha256=Im7H9V0OHX487HxfIrYvN4m8HNElXzdT6kViNizeAw8,4344
16
+ pygent-0.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ pygent-0.1.15.dist-info/entry_points.txt,sha256=b9j216E5UpuMrQWRZrwyEmacNEAYvw1tCKkZqdIVIOc,70
18
+ pygent-0.1.15.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
19
+ pygent-0.1.15.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- pygent/__init__.py,sha256=8TB-wzDeQV0YANON-q7sUkzlIyAmTN6gyz4R-axmoYU,724
2
- pygent/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- pygent/agent.py,sha256=5aY5HRlnLNwZzU_JD4bdEd9P9JLK3mtV4NJl7go9igY,4077
4
- pygent/cli.py,sha256=Hz2FZeNMVhxoT5DjCqphXla3TisGJtPEz921LEcpxrA,527
5
- pygent/errors.py,sha256=s5FBg_v94coSgMh7cfkP4hVXafViGYgCY8QiT698-c4,155
6
- pygent/models.py,sha256=j3670gjUtvQRGZ5wqGDcQ7ZJVTdT5WiwL7nWTokeYzg,1141
7
- pygent/openai_compat.py,sha256=cyWFtXt6sDfOlsZd3FuRxbcZMm3WU-DLPBQpbmcuiW8,2617
8
- pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
9
- pygent/runtime.py,sha256=KckSdTSBIooz181ef8krXbSgqTxIaYDMdCy0ryOi7KQ,4386
10
- pygent/task_manager.py,sha256=kQ2wwBkhPtpluPzKe9T9Hiv1NoxUA3-sEvuIiPqxq90,4167
11
- pygent/tools.py,sha256=xHpUpG2QcBEvwTilpw8BDlYWY3KG9mItSt0FoKNkohA,5585
12
- pygent/ui.py,sha256=xqPAvweghPOBBvoD72HzhN6zlXew_3inb8AN7Ck2zpQ,1328
13
- pygent-0.1.14.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
14
- pygent-0.1.14.dist-info/METADATA,sha256=Et3MB9nI0I9Wb9eWEAmKUrd52rS6H9TluAarCzQsiOk,4278
15
- pygent-0.1.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- pygent-0.1.14.dist-info/entry_points.txt,sha256=b9j216E5UpuMrQWRZrwyEmacNEAYvw1tCKkZqdIVIOc,70
17
- pygent-0.1.14.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
18
- pygent-0.1.14.dist-info/RECORD,,