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 +8 -4
- pygent/config.py +24 -7
- pygent/persona.py +8 -0
- pygent/task_manager.py +43 -10
- pygent/tools.py +54 -0
- {pygent-0.1.15.dist-info → pygent-0.1.16.dist-info}/METADATA +7 -1
- {pygent-0.1.15.dist-info → pygent-0.1.16.dist-info}/RECORD +11 -10
- {pygent-0.1.15.dist-info → pygent-0.1.16.dist-info}/WHEEL +0 -0
- {pygent-0.1.15.dist-info → pygent-0.1.16.dist-info}/entry_points.txt +0 -0
- {pygent-0.1.15.dist-info → pygent-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {pygent-0.1.15.dist-info → pygent-0.1.16.dist-info}/top_level.txt +0 -0
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 =
|
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:
|
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:
|
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 "
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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(
|
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
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[
|
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 = [
|
48
|
-
|
49
|
-
|
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
|
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
|
79
|
-
|
80
|
-
persona
|
81
|
-
|
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.
|
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` – set to `0` to disable Docker and run locally.
|
58
58
|
* `PYGENT_MAX_TASKS` – 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=
|
3
|
+
pygent/agent.py,sha256=55AouUMWIIL2ZPso9VHhCcGL3qRqvNXi42ynDWsdLvk,4589
|
4
4
|
pygent/cli.py,sha256=MixuFYGWdZXka6p6ccl4uEoGKFAS5l3xDdUPzcq7y3g,670
|
5
|
-
pygent/config.py,sha256=
|
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=
|
12
|
-
pygent/tools.py,sha256=
|
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
|
-
pygent-0.1.
|
16
|
-
pygent-0.1.
|
17
|
-
pygent-0.1.
|
18
|
-
pygent-0.1.
|
19
|
-
pygent-0.1.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|