nest-shell 0.1.0__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.
- nest_shell-0.1.0/.gitignore +10 -0
- nest_shell-0.1.0/PKG-INFO +40 -0
- nest_shell-0.1.0/README.md +13 -0
- nest_shell-0.1.0/nest_shell/__init__.py +19 -0
- nest_shell-0.1.0/nest_shell/agent.py +272 -0
- nest_shell-0.1.0/nest_shell/factories.py +632 -0
- nest_shell-0.1.0/nest_shell/llm.py +203 -0
- nest_shell-0.1.0/nest_shell/py.typed +0 -0
- nest_shell-0.1.0/nest_shell/templates.py +181 -0
- nest_shell-0.1.0/pyproject.toml +38 -0
- nest_shell-0.1.0/tests/test_imports.py +9 -0
- nest_shell-0.1.0/tests/test_shell.py +542 -0
- nest_shell-0.1.0/tests/test_templates.py +178 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nest-shell
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NEST shell: Tier 2 LLM-backed reference agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/mariagorskikh/nest
|
|
6
|
+
Project-URL: Repository, https://github.com/mariagorskikh/nest
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: nest-core
|
|
17
|
+
Requires-Dist: nest-sdk
|
|
18
|
+
Requires-Dist: openai>=1.0
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: anthropic>=0.30; extra == 'all'
|
|
21
|
+
Requires-Dist: litellm>=1.0; extra == 'all'
|
|
22
|
+
Provides-Extra: anthropic
|
|
23
|
+
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
|
|
24
|
+
Provides-Extra: litellm
|
|
25
|
+
Requires-Dist: litellm>=1.0; extra == 'litellm'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# nest-shell
|
|
29
|
+
|
|
30
|
+
NEST shell: Tier 2 LLM-backed reference agent
|
|
31
|
+
|
|
32
|
+
Part of [NEST](https://github.com/mariagorskikh/nest) (Network Environment for Swarm Testing), built at MIT Media Lab.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install nest-shell
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
See the [main repository](https://github.com/mariagorskikh/nest) for full documentation.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# nest-shell
|
|
2
|
+
|
|
3
|
+
NEST shell: Tier 2 LLM-backed reference agent
|
|
4
|
+
|
|
5
|
+
Part of [NEST](https://github.com/mariagorskikh/nest) (Network Environment for Swarm Testing), built at MIT Media Lab.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nest-shell
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See the [main repository](https://github.com/mariagorskikh/nest) for full documentation.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""NEST shell: Tier 2 LLM-backed reference agent."""
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
|
|
6
|
+
from nest_shell.agent import ShellAgent as ShellAgent
|
|
7
|
+
from nest_shell.agent import shell_marketplace_factory as shell_marketplace_factory
|
|
8
|
+
from nest_shell.factories import shell_auction_factory as shell_auction_factory
|
|
9
|
+
from nest_shell.factories import shell_consensus_factory as shell_consensus_factory
|
|
10
|
+
from nest_shell.factories import shell_reputation_factory as shell_reputation_factory
|
|
11
|
+
from nest_shell.factories import shell_supply_chain_factory as shell_supply_chain_factory
|
|
12
|
+
from nest_shell.factories import shell_voting_factory as shell_voting_factory
|
|
13
|
+
from nest_shell.llm import AnthropicBackend as AnthropicBackend
|
|
14
|
+
from nest_shell.llm import LiteLLMBackend as LiteLLMBackend
|
|
15
|
+
from nest_shell.llm import LLMBackend as LLMBackend
|
|
16
|
+
from nest_shell.llm import MockLLMBackend as MockLLMBackend
|
|
17
|
+
from nest_shell.llm import OpenAIBackend as OpenAIBackend
|
|
18
|
+
from nest_shell.templates import AgentTemplate as AgentTemplate
|
|
19
|
+
from nest_shell.templates import TemplateRegistry as TemplateRegistry
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Tier 2 LLM-backed shell agent.
|
|
3
|
+
|
|
4
|
+
Replaces hardcoded state-machine logic with LLM-driven decision making.
|
|
5
|
+
The agent maintains a conversation history and asks the LLM what to do
|
|
6
|
+
on each event.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
backend = MockLLMBackend()
|
|
11
|
+
agent = ShellAgent(AgentId("buyer-0"), role="buyer", backend=backend)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from nest_core.sim.agent import AgentContext, StateMachineAgent
|
|
20
|
+
from nest_core.types import AgentId
|
|
21
|
+
|
|
22
|
+
from nest_shell.llm import LLMBackend
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from nest_shell.templates import AgentTemplate
|
|
26
|
+
|
|
27
|
+
_DEFAULT_SYSTEM_PROMPT = """\
|
|
28
|
+
You are an agent in a multi-agent marketplace simulation.
|
|
29
|
+
Your role is: {role}
|
|
30
|
+
|
|
31
|
+
When you receive a message, decide what action to take.
|
|
32
|
+
Respond in this exact format:
|
|
33
|
+
|
|
34
|
+
ACTION: send
|
|
35
|
+
TO: <agent-id>
|
|
36
|
+
MESSAGE: <message-content>
|
|
37
|
+
|
|
38
|
+
Or if no action is needed:
|
|
39
|
+
ACTION: none
|
|
40
|
+
|
|
41
|
+
Rules:
|
|
42
|
+
- If you are a buyer, send buy requests to sellers.
|
|
43
|
+
- If you are a seller, respond to buy requests with "sold:" or "reject:".
|
|
44
|
+
- Always include the product and price in messages.
|
|
45
|
+
- Format: buy:<product>:<price> or sold:<product>:<price> or reject:<product>:<min_price>
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_action(response: str, sender: AgentId) -> dict[str, Any] | None:
|
|
50
|
+
"""Parse an LLM response into an action dict.
|
|
51
|
+
|
|
52
|
+
Example::
|
|
53
|
+
|
|
54
|
+
action = parse_action("ACTION: send\\nTO: seller-0\\nMESSAGE: buy:p:50", sender)
|
|
55
|
+
"""
|
|
56
|
+
response = response.strip()
|
|
57
|
+
|
|
58
|
+
action_match = re.search(r"ACTION:\s*(\w+)", response)
|
|
59
|
+
if not action_match:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
action_type = action_match.group(1).lower()
|
|
63
|
+
if action_type == "none":
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
if action_type == "send":
|
|
67
|
+
to_match = re.search(r"TO:\s*(.+)", response)
|
|
68
|
+
msg_match = re.search(r"MESSAGE:\s*(.+)", response)
|
|
69
|
+
|
|
70
|
+
if not msg_match:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
target_str = to_match.group(1).strip() if to_match else str(sender)
|
|
74
|
+
target_str = target_str.replace("{sender}", str(sender))
|
|
75
|
+
|
|
76
|
+
message = msg_match.group(1).strip()
|
|
77
|
+
message = message.replace("{sender}", str(sender))
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"action": "send",
|
|
81
|
+
"to": AgentId(target_str),
|
|
82
|
+
"message": message.encode("utf-8"),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ShellAgent(StateMachineAgent):
|
|
89
|
+
"""LLM-backed agent that uses a language model to decide actions.
|
|
90
|
+
|
|
91
|
+
Example::
|
|
92
|
+
|
|
93
|
+
agent = ShellAgent(
|
|
94
|
+
agent_id=AgentId("buyer-0"),
|
|
95
|
+
role="buyer",
|
|
96
|
+
backend=MockLLMBackend(),
|
|
97
|
+
)
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
agent_id: AgentId,
|
|
103
|
+
role: str,
|
|
104
|
+
backend: LLMBackend,
|
|
105
|
+
system_prompt: str | None = None,
|
|
106
|
+
num_sellers: int = 10,
|
|
107
|
+
rounds: int = 10,
|
|
108
|
+
template: AgentTemplate | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
self._id = agent_id
|
|
111
|
+
self._role = role
|
|
112
|
+
self._backend = backend
|
|
113
|
+
if template is not None:
|
|
114
|
+
self._system_prompt = template.system_prompt
|
|
115
|
+
else:
|
|
116
|
+
self._system_prompt = (system_prompt or _DEFAULT_SYSTEM_PROMPT).format(role=role)
|
|
117
|
+
self._history: list[dict[str, str]] = [
|
|
118
|
+
{"role": "system", "content": self._system_prompt},
|
|
119
|
+
]
|
|
120
|
+
self._num_sellers = num_sellers
|
|
121
|
+
self._rounds = rounds
|
|
122
|
+
self._round = 0
|
|
123
|
+
self._action_count = 0
|
|
124
|
+
|
|
125
|
+
async def on_start(self, ctx: AgentContext) -> None:
|
|
126
|
+
if self._role == "buyer":
|
|
127
|
+
seller_idx = ctx.rng.randint(0, self._num_sellers - 1)
|
|
128
|
+
seller = AgentId(f"seller-{seller_idx}")
|
|
129
|
+
price = ctx.rng.randint(10, 100)
|
|
130
|
+
|
|
131
|
+
self._history.append(
|
|
132
|
+
{
|
|
133
|
+
"role": "user",
|
|
134
|
+
"content": f"Simulation started. You are {self._id}. "
|
|
135
|
+
f"Send a buy request to a seller. "
|
|
136
|
+
f"Suggested target: {seller}, suggested price: {price}.",
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
response = await self._backend.complete(self._history)
|
|
141
|
+
self._history.append({"role": "assistant", "content": response})
|
|
142
|
+
|
|
143
|
+
action = parse_action(response, seller)
|
|
144
|
+
if action and action["action"] == "send":
|
|
145
|
+
await ctx.send(action["to"], action["message"])
|
|
146
|
+
self._action_count += 1
|
|
147
|
+
else:
|
|
148
|
+
await ctx.send(seller, f"buy:product-0:{price}".encode())
|
|
149
|
+
self._action_count += 1
|
|
150
|
+
|
|
151
|
+
async def on_message(self, ctx: AgentContext, sender: AgentId, payload: bytes) -> None:
|
|
152
|
+
msg = payload.decode("utf-8", errors="replace")
|
|
153
|
+
self._round += 1
|
|
154
|
+
|
|
155
|
+
if self._round > self._rounds:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
self._history.append(
|
|
159
|
+
{
|
|
160
|
+
"role": "user",
|
|
161
|
+
"content": f"Message from {sender}: {msg}",
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if len(self._history) > 20:
|
|
166
|
+
self._history = [self._history[0]] + self._history[-18:]
|
|
167
|
+
|
|
168
|
+
response = await self._backend.complete(self._history)
|
|
169
|
+
self._history.append({"role": "assistant", "content": response})
|
|
170
|
+
|
|
171
|
+
action = parse_action(response, sender)
|
|
172
|
+
if action and action["action"] == "send":
|
|
173
|
+
await ctx.send(action["to"], action["message"])
|
|
174
|
+
self._action_count += 1
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def action_count(self) -> int:
|
|
178
|
+
return self._action_count
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def history_length(self) -> int:
|
|
182
|
+
return len(self._history)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_template(
|
|
186
|
+
config: Any,
|
|
187
|
+
role: str,
|
|
188
|
+
scenario: str,
|
|
189
|
+
) -> AgentTemplate | None:
|
|
190
|
+
"""Resolve a template for a given role, if configured.
|
|
191
|
+
|
|
192
|
+
Example::
|
|
193
|
+
|
|
194
|
+
tpl = _resolve_template(config, "buyer", "marketplace")
|
|
195
|
+
"""
|
|
196
|
+
template_name: str = getattr(config.agents, "template", "")
|
|
197
|
+
if not template_name:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
from nest_shell.templates import TemplateRegistry
|
|
201
|
+
|
|
202
|
+
registry = TemplateRegistry()
|
|
203
|
+
|
|
204
|
+
if template_name == "auto":
|
|
205
|
+
try:
|
|
206
|
+
return registry.get_template(f"{scenario}-{role}")
|
|
207
|
+
except KeyError:
|
|
208
|
+
return None
|
|
209
|
+
try:
|
|
210
|
+
return registry.get_template(template_name)
|
|
211
|
+
except KeyError:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def shell_marketplace_factory(
|
|
216
|
+
config: Any,
|
|
217
|
+
plugins: dict[str, Any],
|
|
218
|
+
backend: LLMBackend | None = None,
|
|
219
|
+
) -> dict[AgentId, StateMachineAgent]:
|
|
220
|
+
"""Create shell agents for the marketplace scenario.
|
|
221
|
+
|
|
222
|
+
Example::
|
|
223
|
+
|
|
224
|
+
agents = shell_marketplace_factory(config, plugins, backend=MockLLMBackend())
|
|
225
|
+
"""
|
|
226
|
+
from nest_shell.llm import MockLLMBackend
|
|
227
|
+
|
|
228
|
+
if backend is None:
|
|
229
|
+
backend = MockLLMBackend()
|
|
230
|
+
|
|
231
|
+
task_config = config.task.config
|
|
232
|
+
rounds = task_config.get("rounds", 10)
|
|
233
|
+
|
|
234
|
+
agents: dict[AgentId, StateMachineAgent] = {}
|
|
235
|
+
|
|
236
|
+
if config.agents.roles:
|
|
237
|
+
buyer_count = 0
|
|
238
|
+
seller_count = 0
|
|
239
|
+
for role in config.agents.roles:
|
|
240
|
+
if role.name == "buyer":
|
|
241
|
+
buyer_count = role.count
|
|
242
|
+
elif role.name == "seller":
|
|
243
|
+
seller_count = role.count
|
|
244
|
+
else:
|
|
245
|
+
buyer_count = config.agents.count // 2
|
|
246
|
+
seller_count = config.agents.count - buyer_count
|
|
247
|
+
|
|
248
|
+
for i in range(seller_count):
|
|
249
|
+
aid = AgentId(f"seller-{i}")
|
|
250
|
+
tpl = _resolve_template(config, "seller", "marketplace")
|
|
251
|
+
agents[aid] = ShellAgent(
|
|
252
|
+
agent_id=aid,
|
|
253
|
+
role="seller",
|
|
254
|
+
backend=backend,
|
|
255
|
+
num_sellers=seller_count,
|
|
256
|
+
rounds=rounds,
|
|
257
|
+
template=tpl,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
for i in range(buyer_count):
|
|
261
|
+
aid = AgentId(f"buyer-{i}")
|
|
262
|
+
tpl = _resolve_template(config, "buyer", "marketplace")
|
|
263
|
+
agents[aid] = ShellAgent(
|
|
264
|
+
agent_id=aid,
|
|
265
|
+
role="buyer",
|
|
266
|
+
backend=backend,
|
|
267
|
+
num_sellers=seller_count,
|
|
268
|
+
rounds=rounds,
|
|
269
|
+
template=tpl,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return agents
|