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.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ uv.lock
@@ -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