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/LICENSE +21 -0
- package/README.md +151 -0
- package/bin/cli.js +102 -0
- package/package.json +55 -0
- package/src/agentverse.js +63 -0
- package/src/env.js +158 -0
- package/src/scaffold.js +316 -0
- package/src/seeds.js +12 -0
- package/src/skills.js +115 -0
- package/src/wizard.js +155 -0
- package/src/workers.js +455 -0
- package/templates/gitignore +44 -0
- package/templates/orchestrator-workers/agents/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/models/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/models/models.py +23 -0
- package/templates/orchestrator-workers/agents/orchestrator/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/services/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/services/state_service.py +22 -0
- package/templates/orchestrator-workers/requirements.txt +42 -0
- package/templates/single-agent/requirements.txt +42 -0
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
|
|
File without changes
|
|
File without changes
|
|
@@ -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 = ""
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|