create-fetch-agent 0.1.0

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.
package/src/workers.js ADDED
@@ -0,0 +1,455 @@
1
+ import { seed } from "./seeds.js";
2
+
3
+ export const ORCHESTRATOR_PORT = 8003;
4
+
5
+ /**
6
+ * Deterministic port assignment.
7
+ *
8
+ * The orchestrator always owns 8003. Workers fill 8001, 8002, 8004, 8005, ...
9
+ * (skipping 8003) in order. This keeps ports stable across regenerations.
10
+ *
11
+ * @param {number} count number of workers
12
+ * @returns {number[]} ports, one per worker
13
+ */
14
+ export function workerPorts(count) {
15
+ const ports = [];
16
+ let p = 8001;
17
+ while (ports.length < count) {
18
+ if (p !== ORCHESTRATOR_PORT) ports.push(p);
19
+ p += 1;
20
+ }
21
+ return ports;
22
+ }
23
+
24
+ /**
25
+ * Render a single worker agent file. The `<name>_workflow` function is the
26
+ * explicit, pre-marked extension point an AI coding tool (or you) will fill in.
27
+ */
28
+ export function renderWorker(name, port) {
29
+ const U = name.toUpperCase();
30
+ return `from agents.models.config import ${U}_SEED
31
+ from agents.models.models import SharedAgentState
32
+ from uagents import Agent, Context
33
+
34
+ ${name} = Agent(
35
+ name="${name}",
36
+ seed=${U}_SEED,
37
+ port=${port},
38
+ mailbox=True,
39
+ publish_agent_details=True,
40
+ )
41
+
42
+
43
+ def ${name}_workflow(state: SharedAgentState) -> SharedAgentState:
44
+ """
45
+ This is ${name}'s specialized workflow — the one extension point you own.
46
+
47
+ In a real implementation this is where ${name}'s agentic logic lives:
48
+ LangGraph state machines, LangChain pipelines, RAG retrieval, tool use,
49
+ external API calls — whatever ${name} is an expert at. Read state.query,
50
+ do the work, and write the answer to state.result before returning.
51
+
52
+ TODO: replace the placeholder below with ${name}'s real logic.
53
+ """
54
+ state.result = f"Hello from ${name}: {state.query}"
55
+ return state
56
+
57
+
58
+ @${name}.on_message(SharedAgentState)
59
+ async def handle_message(ctx: Context, sender: str, state: SharedAgentState):
60
+ ctx.logger.info(f"Received state from orchestrator: query={state.query!r}")
61
+ state = ${name}_workflow(state)
62
+ await ctx.send(sender, state)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ ${name}.run()
67
+ `;
68
+ }
69
+
70
+ /**
71
+ * Render agents/models/config.py: seed + derived address per worker, plus the
72
+ * orchestrator seed. Addresses come from seeds via Identity.from_seed, so there
73
+ * are no hardcoded addresses anywhere in the project.
74
+ */
75
+ export function renderConfig(workerNames) {
76
+ const seedLines = [
77
+ ...workerNames.map((n) => `${n.toUpperCase()}_SEED = os.getenv("${n.toUpperCase()}_SEED_PHRASE")`),
78
+ `ORCHESTRATOR_SEED = os.getenv("ORCHESTRATOR_SEED_PHRASE")`,
79
+ ].join("\n");
80
+
81
+ const addressLines = workerNames
82
+ .map(
83
+ (n) =>
84
+ `${n.toUpperCase()}_ADDRESS = Identity.from_seed(seed=${n.toUpperCase()}_SEED, index=0).address`,
85
+ )
86
+ .join("\n");
87
+
88
+ return `import os
89
+
90
+ from dotenv import find_dotenv, load_dotenv
91
+ from uagents_core.identity import Identity
92
+
93
+ load_dotenv(find_dotenv())
94
+
95
+ ${seedLines}
96
+
97
+ ${addressLines}
98
+ `;
99
+ }
100
+
101
+ /**
102
+ * Render agents/orchestrator/chat_protocol.py. Routing branches are generated
103
+ * from the worker name list; everything else (ack, session-keyed state, the
104
+ * fallback) is preserved from the canonical template. All timestamps are
105
+ * timezone-aware.
106
+ */
107
+ export function renderChatProtocol(workerNames) {
108
+ const addressImports = workerNames
109
+ .map((n) => `${n.toUpperCase()}_ADDRESS`)
110
+ .join(", ");
111
+
112
+ const routing = workerNames
113
+ .map(
114
+ (n) =>
115
+ ` if "${n}" in text_lower:\n ctx.logger.info("Routing to ${n}")\n await ctx.send(${n.toUpperCase()}_ADDRESS, state)\n return`,
116
+ )
117
+ .join("\n\n");
118
+
119
+ const nameList = workerNames.join(", ");
120
+
121
+ return `from datetime import datetime, timezone
122
+ from uuid import uuid4
123
+
124
+ from agents.models.config import ${addressImports}
125
+ from agents.models.models import SharedAgentState
126
+ from agents.services.state_service import state_service
127
+ from uagents import Context, Protocol
128
+ from uagents_core.contrib.protocols.chat import (
129
+ ChatAcknowledgement,
130
+ ChatMessage,
131
+ EndSessionContent,
132
+ TextContent,
133
+ chat_protocol_spec,
134
+ )
135
+
136
+ chat_proto = Protocol(spec=chat_protocol_spec)
137
+
138
+
139
+ @chat_proto.on_message(ChatMessage)
140
+ async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
141
+ await ctx.send(
142
+ sender,
143
+ ChatAcknowledgement(
144
+ timestamp=datetime.now(tz=timezone.utc),
145
+ acknowledged_msg_id=msg.msg_id,
146
+ ),
147
+ )
148
+
149
+ text = " ".join(item.text for item in msg.content if isinstance(item, TextContent))
150
+ ctx.logger.info(f"Received: {text!r}")
151
+
152
+ chat_session_id = str(ctx.session)
153
+ state = state_service.get_state(chat_session_id)
154
+ if state is None:
155
+ state = SharedAgentState(
156
+ chat_session_id=chat_session_id,
157
+ query=text,
158
+ user_sender_address=sender,
159
+ )
160
+ state_service.set_state(chat_session_id, state)
161
+ else:
162
+ state.query = text
163
+ state.user_sender_address = sender
164
+
165
+ text_lower = text.lower()
166
+
167
+ ${routing}
168
+
169
+ # Fallback: no worker name matched the message.
170
+ await ctx.send(
171
+ sender,
172
+ ChatMessage(
173
+ timestamp=datetime.now(tz=timezone.utc),
174
+ msg_id=uuid4(),
175
+ content=[
176
+ TextContent(
177
+ type="text",
178
+ text="Mention one of: ${nameList} and I'll route your message to them.",
179
+ ),
180
+ EndSessionContent(type="end-session"),
181
+ ],
182
+ ),
183
+ )
184
+
185
+
186
+ @chat_proto.on_message(ChatAcknowledgement)
187
+ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledgement):
188
+ pass
189
+
190
+
191
+ def generate_orchestrator_response_from_state(state: SharedAgentState) -> str:
192
+ return state.result
193
+ `;
194
+ }
195
+
196
+ /**
197
+ * Render agents/orchestrator/orchestrator_agent.py. One orchestrator: the sole
198
+ * ASI:One bridge. It owns the chat protocol, relays worker results back to the
199
+ * user, and exposes /health + /message REST stubs for a custom frontend.
200
+ */
201
+ export function renderOrchestratorAgent() {
202
+ return `from datetime import datetime, timezone
203
+ from uuid import uuid4
204
+
205
+ from agents.models.config import ORCHESTRATOR_SEED
206
+ from agents.models.models import SharedAgentState
207
+ from agents.orchestrator.chat_protocol import (
208
+ chat_proto,
209
+ generate_orchestrator_response_from_state,
210
+ )
211
+ from uagents import Agent, Context, Model
212
+ from uagents_core.contrib.protocols.chat import (
213
+ ChatMessage,
214
+ EndSessionContent,
215
+ TextContent,
216
+ )
217
+
218
+ orchestrator = Agent(
219
+ name="orchestrator",
220
+ seed=ORCHESTRATOR_SEED,
221
+ port=${ORCHESTRATOR_PORT},
222
+ mailbox=True,
223
+ publish_agent_details=True,
224
+ )
225
+
226
+ orchestrator.include(chat_proto, publish_manifest=True)
227
+
228
+
229
+ class HealthResponse(Model):
230
+ status: str
231
+
232
+
233
+ class HttpMessagePost(Model):
234
+ content: str
235
+
236
+
237
+ class HttpMessageResponse(Model):
238
+ echo: str
239
+
240
+
241
+ @orchestrator.on_rest_get("/health", HealthResponse)
242
+ async def health(ctx: Context) -> HealthResponse:
243
+ """
244
+ REST health check. Visit http://localhost:${ORCHESTRATOR_PORT}/health
245
+
246
+ Add more endpoints with @orchestrator.on_rest_get() /
247
+ @orchestrator.on_rest_post() to build an API for a custom frontend.
248
+ """
249
+ return HealthResponse(status="ok healthy")
250
+
251
+
252
+ @orchestrator.on_rest_post("/message", HttpMessagePost, HttpMessageResponse)
253
+ async def message(ctx: Context, req: HttpMessagePost) -> HttpMessageResponse:
254
+ """
255
+ REST endpoint to send a message to the orchestrator from any HTTP client:
256
+
257
+ curl -X POST http://localhost:${ORCHESTRATOR_PORT}/message \\
258
+ -H "Content-Type: application/json" \\
259
+ -d '{"content": "Hello, orchestrator!"}'
260
+
261
+ Swap the echo for a call into the agent pipeline to get real responses.
262
+ """
263
+ return HttpMessageResponse(echo=req.content)
264
+
265
+
266
+ @orchestrator.on_message(SharedAgentState)
267
+ async def handle_agent_response(ctx: Context, sender: str, state: SharedAgentState):
268
+ """
269
+ Receives the completed SharedAgentState back from a worker. The orchestrator
270
+ is the sole bridge between the internal agent flow and ASI:One, so once a
271
+ worker finishes we relay the result straight back to the original user.
272
+ """
273
+ ctx.logger.info(
274
+ f"Received state back from worker: session={state.chat_session_id}, "
275
+ f"result={state.result!r}"
276
+ )
277
+ response = generate_orchestrator_response_from_state(state)
278
+ await ctx.send(
279
+ state.user_sender_address,
280
+ ChatMessage(
281
+ timestamp=datetime.now(tz=timezone.utc),
282
+ msg_id=uuid4(),
283
+ content=[
284
+ TextContent(type="text", text=response),
285
+ EndSessionContent(type="end-session"),
286
+ ],
287
+ ),
288
+ )
289
+
290
+
291
+ if __name__ == "__main__":
292
+ orchestrator.run()
293
+ `;
294
+ }
295
+
296
+ const MAKEFILE_HEADER = `# Each target runs one agent in the foreground. Open a separate terminal per
297
+ # agent: start the orchestrator first, then each worker.
298
+ #
299
+ # make orchestrator
300
+ # make <worker>
301
+ #
302
+ # The orchestrator is the only ASI:One bridge (port ${ORCHESTRATOR_PORT}). Workers receive
303
+ # the shared state, run their workflow, and send it back.
304
+ `;
305
+
306
+ /**
307
+ * Render the Makefile: one target per worker plus `make orchestrator`. Recipe
308
+ * lines MUST be tab-indented for GNU make.
309
+ */
310
+ export function renderMakefile(workerNames) {
311
+ const orchestratorTarget = `orchestrator:\n\tpython -m agents.orchestrator.orchestrator_agent\n`;
312
+ const workerTargets = workerNames
313
+ .map((n) => `${n}:\n\tpython -m agents.${n}.${n}_agent\n`)
314
+ .join("\n");
315
+
316
+ return `${MAKEFILE_HEADER}\n${orchestratorTarget}\n${workerTargets}`;
317
+ }
318
+
319
+ /**
320
+ * Render the .env contents for the orchestrator+workers project. One unique
321
+ * pre-generated seed phrase per agent, matching the names in config.py.
322
+ *
323
+ * @param {string[]} workerNames
324
+ * @param {() => string} seedFn injectable for deterministic tests
325
+ */
326
+ export function renderEnv(workerNames, seedFn = seed) {
327
+ const lines = [
328
+ "# Seed phrases are pre-generated and unique per agent.",
329
+ "# Keep this file private — each seed controls an agent's on-network identity.",
330
+ "",
331
+ `ORCHESTRATOR_SEED_PHRASE=${seedFn()}`,
332
+ ...workerNames.map((n) => `${n.toUpperCase()}_SEED_PHRASE=${seedFn()}`),
333
+ "",
334
+ ];
335
+ return lines.join("\n");
336
+ }
337
+
338
+ // ────────────────────────────────────────────────────────────────────────────
339
+ // Single agent
340
+ // ────────────────────────────────────────────────────────────────────────────
341
+
342
+ export const SINGLE_AGENT_PORT = 8000;
343
+
344
+ /**
345
+ * Render a self-contained, chat-enabled single agent. It is ASI:One ready out
346
+ * of the box: it speaks the chat protocol, so you can talk to it directly in the
347
+ * Agentverse inspector. `agent_workflow` is the one extension point you own.
348
+ */
349
+ export function renderSingleAgent(name, port = SINGLE_AGENT_PORT) {
350
+ return `import os
351
+ from datetime import datetime, timezone
352
+ from uuid import uuid4
353
+
354
+ from dotenv import find_dotenv, load_dotenv
355
+ from uagents import Agent, Context, Protocol
356
+ from uagents_core.contrib.protocols.chat import (
357
+ ChatAcknowledgement,
358
+ ChatMessage,
359
+ EndSessionContent,
360
+ TextContent,
361
+ chat_protocol_spec,
362
+ )
363
+
364
+ load_dotenv(find_dotenv())
365
+
366
+ AGENT_SEED = os.getenv("AGENT_SEED_PHRASE")
367
+
368
+ agent = Agent(
369
+ name="${name}",
370
+ seed=AGENT_SEED,
371
+ port=${port},
372
+ mailbox=True,
373
+ publish_agent_details=True,
374
+ )
375
+
376
+ chat_proto = Protocol(spec=chat_protocol_spec)
377
+
378
+
379
+ def agent_workflow(query: str) -> str:
380
+ """
381
+ Your agent's logic — the one extension point you own.
382
+
383
+ Read the user's query and return a response string. In a real implementation
384
+ this is where you'd call an LLM, run a RAG pipeline, hit an API, or use tools.
385
+
386
+ TODO: replace the placeholder below with your real logic.
387
+ """
388
+ return f"Hello from ${name}! You said: {query}"
389
+
390
+
391
+ @chat_proto.on_message(ChatMessage)
392
+ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage):
393
+ await ctx.send(
394
+ sender,
395
+ ChatAcknowledgement(
396
+ timestamp=datetime.now(tz=timezone.utc),
397
+ acknowledged_msg_id=msg.msg_id,
398
+ ),
399
+ )
400
+
401
+ text = " ".join(item.text for item in msg.content if isinstance(item, TextContent))
402
+ ctx.logger.info(f"Received: {text!r}")
403
+
404
+ answer = agent_workflow(text)
405
+
406
+ await ctx.send(
407
+ sender,
408
+ ChatMessage(
409
+ timestamp=datetime.now(tz=timezone.utc),
410
+ msg_id=uuid4(),
411
+ content=[
412
+ TextContent(type="text", text=answer),
413
+ EndSessionContent(type="end-session"),
414
+ ],
415
+ ),
416
+ )
417
+
418
+
419
+ @chat_proto.on_message(ChatAcknowledgement)
420
+ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
421
+ pass
422
+
423
+
424
+ agent.include(chat_proto, publish_manifest=True)
425
+
426
+
427
+ @agent.on_event("startup")
428
+ async def startup(ctx: Context):
429
+ ctx.logger.info(f"${name} started with address: {agent.address}")
430
+
431
+
432
+ if __name__ == "__main__":
433
+ agent.run()
434
+ `;
435
+ }
436
+
437
+ /**
438
+ * Render the .env for a single agent.
439
+ */
440
+ export function renderSingleEnv(seedFn = seed) {
441
+ return [
442
+ "# Seed phrase is pre-generated. Keep it private — it controls the agent's",
443
+ "# on-network identity (and therefore its address).",
444
+ "",
445
+ `AGENT_SEED_PHRASE=${seedFn()}`,
446
+ "",
447
+ ].join("\n");
448
+ }
449
+
450
+ /**
451
+ * Render the Makefile for a single agent.
452
+ */
453
+ export function renderSingleMakefile() {
454
+ return `# Run the agent in the foreground.\n#\n# make run\n\nrun:\n\tpython agent.py\n`;
455
+ }
@@ -0,0 +1,44 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+ *.egg
15
+
16
+ # Unit test / coverage reports
17
+ .pytest_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # Environments
22
+ .env
23
+ .venv
24
+ env/
25
+ venv/
26
+ ENV/
27
+
28
+ # uv
29
+ #uv.lock
30
+
31
+ # poetry
32
+ #poetry.lock
33
+
34
+ # Editors
35
+ .idea/
36
+ # .vscode/
37
+
38
+ # Cursor
39
+ .cursorignore
40
+ .cursorindexingignore
41
+
42
+ # Fetch.ai agent keys
43
+ private_keys.json
44
+ **/private_keys.json
@@ -0,0 +1,23 @@
1
+ from uagents import Model
2
+
3
+
4
+ class SharedAgentState(Model):
5
+ """
6
+ Shared communication contract between the orchestrator and all worker agents.
7
+
8
+ The orchestrator manages this state and forwards it to the appropriate worker.
9
+ The worker runs its workflow, writes its output to `result`, and sends the state
10
+ back.
11
+
12
+ Attributes:
13
+ chat_session_id: Identifies the originating chat session.
14
+ query: The user's request.
15
+ user_sender_address: ASI:One address of the original user, so the orchestrator
16
+ can relay the final response back.
17
+ result: Written by the worker once its workflow completes. Empty until then.
18
+ """
19
+
20
+ chat_session_id: str
21
+ query: str
22
+ user_sender_address: str
23
+ result: str = ""
@@ -0,0 +1,22 @@
1
+ from agents.models.models import SharedAgentState
2
+
3
+
4
+ class InMemoryStateService:
5
+ """
6
+ In-memory store for SharedAgentState keyed by chat_session_id.
7
+
8
+ Demonstrates the persistence pattern — swap this for a database or Redis
9
+ and nothing else in the pipeline needs to change.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ self._store: dict[str, SharedAgentState] = {}
14
+
15
+ def set_state(self, chat_session_id: str, state: SharedAgentState) -> None:
16
+ self._store[chat_session_id] = state
17
+
18
+ def get_state(self, chat_session_id: str) -> SharedAgentState | None:
19
+ return self._store.get(chat_session_id)
20
+
21
+
22
+ state_service = InMemoryStateService()
@@ -0,0 +1,42 @@
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.12.15
3
+ aiosignal==1.4.0
4
+ annotated-types==0.7.0
5
+ attrs==25.3.0
6
+ bech32==1.2.0
7
+ certifi==2025.8.3
8
+ charset-normalizer==3.4.3
9
+ click==8.2.1
10
+ cosmpy==0.11.1
11
+ distlib==0.4.0
12
+ ecdsa==0.19.1
13
+ filelock==3.19.1
14
+ frozenlist==1.7.0
15
+ googleapis-common-protos==1.70.0
16
+ grpcio==1.74.0
17
+ h11==0.16.0
18
+ idna==3.10
19
+ jsonschema==4.25.1
20
+ jsonschema-specifications==2025.9.1
21
+ multidict==6.6.4
22
+ platformdirs==4.4.0
23
+ propcache==0.3.2
24
+ protobuf==5.29.5
25
+ pycryptodome==3.23.0
26
+ pydantic==2.11.9
27
+ pydantic_core==2.33.2
28
+ python-dateutil==2.9.0.post0
29
+ referencing==0.36.2
30
+ requests==2.32.5
31
+ rpds-py==0.27.1
32
+ six==1.17.0
33
+ sortedcontainers==2.4.0
34
+ typing-inspection==0.4.1
35
+ typing_extensions==4.15.0
36
+ uagents==0.22.8
37
+ uagents-core==0.3.8
38
+ urllib3==2.5.0
39
+ uvicorn==0.35.0
40
+ virtualenv==20.34.0
41
+ yarl==1.20.1
42
+ python-dotenv==1.0.1
@@ -0,0 +1,42 @@
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.12.15
3
+ aiosignal==1.4.0
4
+ annotated-types==0.7.0
5
+ attrs==25.3.0
6
+ bech32==1.2.0
7
+ certifi==2025.8.3
8
+ charset-normalizer==3.4.3
9
+ click==8.2.1
10
+ cosmpy==0.11.1
11
+ distlib==0.4.0
12
+ ecdsa==0.19.1
13
+ filelock==3.19.1
14
+ frozenlist==1.7.0
15
+ googleapis-common-protos==1.70.0
16
+ grpcio==1.74.0
17
+ h11==0.16.0
18
+ idna==3.10
19
+ jsonschema==4.25.1
20
+ jsonschema-specifications==2025.9.1
21
+ multidict==6.6.4
22
+ platformdirs==4.4.0
23
+ propcache==0.3.2
24
+ protobuf==5.29.5
25
+ pycryptodome==3.23.0
26
+ pydantic==2.11.9
27
+ pydantic_core==2.33.2
28
+ python-dateutil==2.9.0.post0
29
+ referencing==0.36.2
30
+ requests==2.32.5
31
+ rpds-py==0.27.1
32
+ six==1.17.0
33
+ sortedcontainers==2.4.0
34
+ typing-inspection==0.4.1
35
+ typing_extensions==4.15.0
36
+ uagents==0.22.8
37
+ uagents-core==0.3.8
38
+ urllib3==2.5.0
39
+ uvicorn==0.35.0
40
+ virtualenv==20.34.0
41
+ yarl==1.20.1
42
+ python-dotenv==1.0.1