pygent 0.1.15__py3-none-any.whl → 0.1.16__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/agent.py CHANGED
@@ -15,12 +15,16 @@ from rich.markdown import Markdown
15
15
  from .runtime import Runtime
16
16
  from . import tools
17
17
  from .models import Model, OpenAIModel
18
+ from .persona import Persona
18
19
 
19
- DEFAULT_PERSONA = os.getenv("PYGENT_PERSONA", "You are Pygent, a sandboxed coding assistant.")
20
+ DEFAULT_PERSONA = Persona(
21
+ os.getenv("PYGENT_PERSONA_NAME", "Pygent"),
22
+ os.getenv("PYGENT_PERSONA", "a sandboxed coding assistant."),
23
+ )
20
24
 
21
- def build_system_msg(persona: str) -> str:
25
+ def build_system_msg(persona: Persona) -> str:
22
26
  return (
23
- f"{persona}\n"
27
+ f"You are {persona.name}. {persona.description}\n"
24
28
  "Respond with JSON when you need to use a tool."
25
29
  "If you need to stop or finished you task, call the `stop` tool.\n"
26
30
  "You can use the following tools:\n"
@@ -41,7 +45,7 @@ class Agent:
41
45
  runtime: Runtime = field(default_factory=Runtime)
42
46
  model: Model = field(default_factory=OpenAIModel)
43
47
  model_name: str = DEFAULT_MODEL
44
- persona: str = DEFAULT_PERSONA
48
+ persona: Persona = field(default_factory=lambda: DEFAULT_PERSONA)
45
49
  system_msg: str = field(default_factory=lambda: build_system_msg(DEFAULT_PERSONA))
46
50
  history: List[Dict[str, Any]] = field(default_factory=list)
47
51
 
pygent/config.py CHANGED
@@ -1,13 +1,15 @@
1
1
  import os
2
+ import json
2
3
  import tomllib
3
4
  from pathlib import Path
4
- from typing import Any, Dict
5
+ from typing import Any, Dict, List, Mapping
5
6
 
6
7
  DEFAULT_CONFIG_FILES = [
7
8
  Path("pygent.toml"),
8
9
  Path.home() / ".pygent.toml",
9
10
  ]
10
11
 
12
+
11
13
  def load_config(path: str | os.PathLike[str] | None = None) -> Dict[str, Any]:
12
14
  """Load configuration from a TOML file and set environment variables.
13
15
 
@@ -27,14 +29,29 @@ def load_config(path: str | os.PathLike[str] | None = None) -> Dict[str, Any]:
27
29
  # update environment without overwriting existing values
28
30
  if "persona" in config and "PYGENT_PERSONA" not in os.environ:
29
31
  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"])
32
+ if "persona_name" in config and "PYGENT_PERSONA_NAME" not in os.environ:
33
+ os.environ["PYGENT_PERSONA_NAME"] = str(config["persona_name"])
34
+ if "task_personas" in config:
35
+ personas = config["task_personas"]
36
+ if isinstance(personas, list) and personas and isinstance(personas[0], Mapping):
37
+ if "PYGENT_TASK_PERSONAS_JSON" not in os.environ:
38
+ os.environ["PYGENT_TASK_PERSONAS_JSON"] = json.dumps(personas)
39
+ if "PYGENT_TASK_PERSONAS" not in os.environ:
40
+ os.environ["PYGENT_TASK_PERSONAS"] = os.pathsep.join(
41
+ str(p.get("name", "")) for p in personas
42
+ )
43
+ elif "PYGENT_TASK_PERSONAS" not in os.environ:
44
+ if isinstance(personas, list):
45
+ os.environ["PYGENT_TASK_PERSONAS"] = os.pathsep.join(
46
+ str(p) for p in personas
47
+ )
48
+ else:
49
+ os.environ["PYGENT_TASK_PERSONAS"] = str(personas)
35
50
  if "initial_files" in config and "PYGENT_INIT_FILES" not in os.environ:
36
51
  if isinstance(config["initial_files"], list):
37
- os.environ["PYGENT_INIT_FILES"] = os.pathsep.join(str(p) for p in config["initial_files"])
52
+ os.environ["PYGENT_INIT_FILES"] = os.pathsep.join(
53
+ str(p) for p in config["initial_files"]
54
+ )
38
55
  else:
39
56
  os.environ["PYGENT_INIT_FILES"] = str(config["initial_files"])
40
57
  return config
pygent/persona.py ADDED
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class Persona:
5
+ """Representa uma persona com nome e descricao."""
6
+ name: str
7
+ description: str
8
+
pygent/task_manager.py CHANGED
@@ -3,12 +3,15 @@ from __future__ import annotations
3
3
  """Manage background tasks executed by sub-agents."""
4
4
 
5
5
  import os
6
+ import json
6
7
  import shutil
7
8
  import threading
8
9
  import uuid
9
10
  from dataclasses import dataclass, field
10
11
  from typing import Callable, Dict, TYPE_CHECKING
11
12
 
13
+ from .persona import Persona
14
+
12
15
  from .runtime import Runtime
13
16
 
14
17
  if TYPE_CHECKING: # pragma: no cover - for type hints only
@@ -32,7 +35,7 @@ class TaskManager:
32
35
  self,
33
36
  agent_factory: Callable[..., "Agent"] | None = None,
34
37
  max_tasks: int | None = None,
35
- personas: list[str] | None = None,
38
+ personas: list[Persona] | None = None,
36
39
  ) -> None:
37
40
  from .agent import Agent # local import to avoid circular dependency
38
41
 
@@ -42,11 +45,33 @@ class TaskManager:
42
45
  self.agent_factory = lambda p=None: Agent(persona=p)
43
46
  else:
44
47
  self.agent_factory = agent_factory
48
+ env_personas_json = os.getenv("PYGENT_TASK_PERSONAS_JSON")
49
+ if personas is None and env_personas_json:
50
+ try:
51
+ data = json.loads(env_personas_json)
52
+ if isinstance(data, list):
53
+ personas = [
54
+ Persona(p.get("name", ""), p.get("description", ""))
55
+ for p in data
56
+ if isinstance(p, dict)
57
+ ]
58
+ except Exception:
59
+ personas = None
45
60
  env_personas = os.getenv("PYGENT_TASK_PERSONAS")
46
61
  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]
62
+ personas = [
63
+ Persona(p.strip(), "")
64
+ for p in env_personas.split(os.pathsep)
65
+ if p.strip()
66
+ ]
67
+ if personas is None:
68
+ personas = [
69
+ Persona(
70
+ os.getenv("PYGENT_PERSONA_NAME", "Pygent"),
71
+ os.getenv("PYGENT_PERSONA", "a sandboxed coding assistant."),
72
+ )
73
+ ]
74
+ self.personas = personas
50
75
  self._persona_idx = 0
51
76
  self.tasks: Dict[str, Task] = {}
52
77
  self._lock = threading.Lock()
@@ -59,8 +84,12 @@ class TaskManager:
59
84
  parent_depth: int = 0,
60
85
  step_timeout: float | None = None,
61
86
  task_timeout: float | None = None,
87
+ persona: Persona | str | None = None,
62
88
  ) -> str:
63
- """Create a new agent and run ``prompt`` asynchronously."""
89
+ """Create a new agent and run ``prompt`` asynchronously.
90
+
91
+ ``persona`` overrides the default rotation used for delegated tasks.
92
+ """
64
93
 
65
94
  if parent_depth >= 1:
66
95
  raise RuntimeError("nested delegation is not allowed")
@@ -72,13 +101,17 @@ class TaskManager:
72
101
 
73
102
  if step_timeout is None:
74
103
  env = os.getenv("PYGENT_STEP_TIMEOUT")
75
- step_timeout = float(env) if env else 60*5 # default 5 minutes
104
+ step_timeout = float(env) if env else 60 * 5 # default 5 minutes
76
105
  if task_timeout is None:
77
106
  env = os.getenv("PYGENT_TASK_TIMEOUT")
78
- task_timeout = float(env) if env else 60*20 # default 20 minutes
79
-
80
- persona = self.personas[self._persona_idx % len(self.personas)]
81
- self._persona_idx += 1
107
+ task_timeout = float(env) if env else 60 * 20 # default 20 minutes
108
+
109
+ if persona is None:
110
+ persona = self.personas[self._persona_idx % len(self.personas)]
111
+ self._persona_idx += 1
112
+ elif isinstance(persona, str):
113
+ match = next((p for p in self.personas if p.name == persona), None)
114
+ persona = match or Persona(persona, "")
82
115
  try:
83
116
  agent = self.agent_factory(persona)
84
117
  except TypeError:
pygent/tools.py CHANGED
@@ -122,6 +122,7 @@ def _continue(rt: Runtime) -> str: # pragma: no cover - side-effect free
122
122
  "items": {"type": "string"},
123
123
  "description": "Files to copy to the sub-agent before starting",
124
124
  },
125
+ "persona": {"type": "string", "description": "Persona for the sub-agent"},
125
126
  "timeout": {"type": "number", "description": "Max seconds for the task"},
126
127
  "step_timeout": {"type": "number", "description": "Time limit per step"},
127
128
  },
@@ -134,6 +135,7 @@ def _delegate_task(
134
135
  files: list[str] | None = None,
135
136
  timeout: float | None = None,
136
137
  step_timeout: float | None = None,
138
+ persona: str | None = None,
137
139
  ) -> str:
138
140
  if getattr(rt, "task_depth", 0) >= 1:
139
141
  return "error: delegation not allowed in sub-tasks"
@@ -145,12 +147,64 @@ def _delegate_task(
145
147
  parent_depth=getattr(rt, "task_depth", 0),
146
148
  step_timeout=step_timeout,
147
149
  task_timeout=timeout,
150
+ persona=persona,
148
151
  )
149
152
  except RuntimeError as exc:
150
153
  return str(exc)
151
154
  return f"started {tid}"
152
155
 
153
156
 
157
+ @tool(
158
+ name="delegate_persona_task",
159
+ description="Create a background task with a specific persona and return its ID.",
160
+ parameters={
161
+ "type": "object",
162
+ "properties": {
163
+ "prompt": {"type": "string", "description": "Instruction for the sub-agent"},
164
+ "persona": {"type": "string", "description": "Persona for the sub-agent"},
165
+ "files": {
166
+ "type": "array",
167
+ "items": {"type": "string"},
168
+ "description": "Files to copy to the sub-agent before starting",
169
+ },
170
+ "timeout": {"type": "number", "description": "Max seconds for the task"},
171
+ "step_timeout": {"type": "number", "description": "Time limit per step"},
172
+ },
173
+ "required": ["prompt", "persona"],
174
+ },
175
+ )
176
+ def _delegate_persona_task(
177
+ rt: Runtime,
178
+ prompt: str,
179
+ persona: str,
180
+ files: list[str] | None = None,
181
+ timeout: float | None = None,
182
+ step_timeout: float | None = None,
183
+ ) -> str:
184
+ return _delegate_task(
185
+ rt,
186
+ prompt=prompt,
187
+ files=files,
188
+ timeout=timeout,
189
+ step_timeout=step_timeout,
190
+ persona=persona,
191
+ )
192
+
193
+
194
+ @tool(
195
+ name="list_personas",
196
+ description="Return the available personas for delegated agents.",
197
+ parameters={"type": "object", "properties": {}},
198
+ )
199
+ def _list_personas(rt: Runtime) -> str:
200
+ """Return JSON list of personas."""
201
+ personas = [
202
+ {"name": p.name, "description": p.description}
203
+ for p in _get_manager().personas
204
+ ]
205
+ return json.dumps(personas)
206
+
207
+
154
208
  @tool(
155
209
  name="task_status",
156
210
  description="Check the status of a delegated task.",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygent
3
- Version: 0.1.15
3
+ Version: 0.1.16
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
@@ -57,6 +57,12 @@ Behaviour can be adjusted via environment variables (see `docs/configuration.md`
57
57
  * `PYGENT_USE_DOCKER` &ndash; set to `0` to disable Docker and run locally.
58
58
  * `PYGENT_MAX_TASKS` &ndash; maximum number of concurrent delegated tasks (default `3`).
59
59
 
60
+ Settings can also be read from a `pygent.toml` file. See
61
+ [examples/sample_config.toml](https://github.com/marianochaves/pygent/blob/main/examples/sample_config.toml)
62
+ and the accompanying
63
+ [config_file_example.py](https://github.com/marianochaves/pygent/blob/main/examples/config_file_example.py)
64
+ script for a working demonstration that generates tests using a delegated agent.
65
+
60
66
  ## CLI usage
61
67
 
62
68
  After installing run:
@@ -1,19 +1,20 @@
1
1
  pygent/__init__.py,sha256=PnTyTP8ObRQfKN0_8-BendG2gGiZhPz5iP9URrlPVwU,776
2
2
  pygent/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- pygent/agent.py,sha256=0yo2K47PgvQJgH_y2xCCkjINks1pYoTQpztPn2J0srk,4437
3
+ pygent/agent.py,sha256=55AouUMWIIL2ZPso9VHhCcGL3qRqvNXi42ynDWsdLvk,4589
4
4
  pygent/cli.py,sha256=MixuFYGWdZXka6p6ccl4uEoGKFAS5l3xDdUPzcq7y3g,670
5
- pygent/config.py,sha256=4Yn7kV62RaXa-DI5IqurgWvFZNNykjS0PS4MsUrFEzo,1636
5
+ pygent/config.py,sha256=vJ8-w935vBqT1gfZ6eBxu7Z0VbRkpmEUqJEhqUAD5ic,2341
6
6
  pygent/errors.py,sha256=s5FBg_v94coSgMh7cfkP4hVXafViGYgCY8QiT698-c4,155
7
7
  pygent/models.py,sha256=j3670gjUtvQRGZ5wqGDcQ7ZJVTdT5WiwL7nWTokeYzg,1141
8
8
  pygent/openai_compat.py,sha256=cyWFtXt6sDfOlsZd3FuRxbcZMm3WU-DLPBQpbmcuiW8,2617
9
+ pygent/persona.py,sha256=aHjRREHxkqK1uaUyWZuLUEh-RBecmMM5AlYgQsogfQw,152
9
10
  pygent/py.typed,sha256=0Wh72UpGSn4lSGW-u3xMV9kxcBHMdwE15IGUqiJTwqo,52
10
11
  pygent/runtime.py,sha256=dXk4mcYWdc3UzWN4WgyH-fsAUOlqR9L7cYAAf55Gu50,5121
11
- pygent/task_manager.py,sha256=pO4A9nAfeUylHc2h94K5JUPXTy8MXEtBbw_oYb8oOQk,5106
12
- pygent/tools.py,sha256=xHpUpG2QcBEvwTilpw8BDlYWY3KG9mItSt0FoKNkohA,5585
12
+ pygent/task_manager.py,sha256=w_G0B3bfRZHQbdlDsxOJfWFYZQTSiPGlI84lgxdC2Hk,6216
13
+ pygent/tools.py,sha256=B-Czn4dYqOf5Mq_vzKYphX18Z8KUJJwhWqutqZVv3JE,7304
13
14
  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,,
15
+ pygent-0.1.16.dist-info/licenses/LICENSE,sha256=rIktBU2VR4kHzsWul64cbom2zHIgGqYmABoZwSur6T8,1071
16
+ pygent-0.1.16.dist-info/METADATA,sha256=BubYT3xEcrbgLO8dIHe9kNTEzLnpYSaAC3cL79BuEm0,4721
17
+ pygent-0.1.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ pygent-0.1.16.dist-info/entry_points.txt,sha256=b9j216E5UpuMrQWRZrwyEmacNEAYvw1tCKkZqdIVIOc,70
19
+ pygent-0.1.16.dist-info/top_level.txt,sha256=P26IYsb-ThK5IkGP_bRuGJQ0Q_Y8JCcbYqVpvULdxDw,7
20
+ pygent-0.1.16.dist-info/RECORD,,