create-leafmesh 2.1.0__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.
- create_leafmesh/__init__.py +3 -0
- create_leafmesh/cli.py +252 -0
- create_leafmesh/create.py +106 -0
- create_leafmesh/templates/Dockerfile +21 -0
- create_leafmesh/templates/README.md +309 -0
- create_leafmesh/templates/agency/__init__.py +0 -0
- create_leafmesh/templates/agency/advisor_agent.py +151 -0
- create_leafmesh/templates/agency/external_agents.py +278 -0
- create_leafmesh/templates/agency/fallback_researcher_agent.py +80 -0
- create_leafmesh/templates/agency/greeter_agent.py +79 -0
- create_leafmesh/templates/agency/processor_agent.py +90 -0
- create_leafmesh/templates/agency/researcher_agent.py +99 -0
- create_leafmesh/templates/agency/scheduler_agent.py +67 -0
- create_leafmesh/templates/agency/tools.py +123 -0
- create_leafmesh/templates/claude_skills/leafmesh/SKILL.md +2049 -0
- create_leafmesh/templates/claude_skills/leafmesh/agent-config-fields.md +1309 -0
- create_leafmesh/templates/claude_skills/leafmesh/examples.md +537 -0
- create_leafmesh/templates/claude_skills/leafmesh/reference.md +492 -0
- create_leafmesh/templates/configs/config.yaml +1028 -0
- create_leafmesh/templates/docker-compose.yml +28 -0
- create_leafmesh/templates/dockerignore +17 -0
- create_leafmesh/templates/env +109 -0
- create_leafmesh/templates/gitignore +33 -0
- create_leafmesh/templates/hitl_stub_receiver.py +149 -0
- create_leafmesh/templates/main.py +105 -0
- create_leafmesh/templates/requirements.txt +10 -0
- create_leafmesh-2.1.0.dist-info/METADATA +6 -0
- create_leafmesh-2.1.0.dist-info/RECORD +31 -0
- create_leafmesh-2.1.0.dist-info/WHEEL +5 -0
- create_leafmesh-2.1.0.dist-info/entry_points.txt +2 -0
- create_leafmesh-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# LeafMesh Agent Patterns — Copy-Paste Templates
|
|
2
|
+
|
|
3
|
+
## Pattern 1: Customer Support Hub (Router + Specialists)
|
|
4
|
+
|
|
5
|
+
### config.yaml
|
|
6
|
+
```yaml
|
|
7
|
+
entry_points:
|
|
8
|
+
- name: "support"
|
|
9
|
+
target: "router_agent"
|
|
10
|
+
condition: "always"
|
|
11
|
+
|
|
12
|
+
agents:
|
|
13
|
+
router_agent:
|
|
14
|
+
agent_type: "llm"
|
|
15
|
+
model: "gpt-4o-mini"
|
|
16
|
+
prompt: |
|
|
17
|
+
You are a customer support router. Classify the customer's intent
|
|
18
|
+
and respond with: intent (billing/technical/general), urgency (low/medium/high),
|
|
19
|
+
and a brief summary.
|
|
20
|
+
yields: {intent: string, urgency: string, summary: string}
|
|
21
|
+
can_call:
|
|
22
|
+
- agent: "billing_agent"
|
|
23
|
+
condition: "intent == 'billing'"
|
|
24
|
+
- agent: "technical_agent"
|
|
25
|
+
condition: "intent == 'technical'"
|
|
26
|
+
- agent: "general_agent"
|
|
27
|
+
condition: "intent == 'general'"
|
|
28
|
+
|
|
29
|
+
billing_agent:
|
|
30
|
+
agent_type: "llm"
|
|
31
|
+
model: "gpt-4o"
|
|
32
|
+
prompt: "You are a billing specialist. Help resolve billing inquiries."
|
|
33
|
+
tools: ["lookup_account", "check_invoice"]
|
|
34
|
+
tool_categories: ["data"]
|
|
35
|
+
|
|
36
|
+
technical_agent:
|
|
37
|
+
agent_type: "llm"
|
|
38
|
+
model: "gpt-4o"
|
|
39
|
+
prompt: "You are a technical support specialist."
|
|
40
|
+
tools: ["check_status", "search_kb"]
|
|
41
|
+
memory: true
|
|
42
|
+
|
|
43
|
+
general_agent:
|
|
44
|
+
agent_type: "llm"
|
|
45
|
+
model: "gpt-4o-mini"
|
|
46
|
+
prompt: "You are a friendly general support agent."
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### agency/router_agent.py
|
|
50
|
+
```python
|
|
51
|
+
from leafmesh import pre_compose
|
|
52
|
+
|
|
53
|
+
def detect_language(input_data, context):
|
|
54
|
+
msg = input_data.get("message", "")
|
|
55
|
+
return {"language": "en", "char_count": len(msg)}
|
|
56
|
+
|
|
57
|
+
@pre_compose(context_processor=detect_language)
|
|
58
|
+
async def router_agent(llm_response, input_data, context):
|
|
59
|
+
return {
|
|
60
|
+
"intent": llm_response.get("intent", "general"),
|
|
61
|
+
"urgency": llm_response.get("urgency", "low"),
|
|
62
|
+
"summary": llm_response.get("summary", input_data.get("message", "")),
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Pattern 2: Research Pipeline (Fan-Out + Fan-In)
|
|
69
|
+
|
|
70
|
+
### config.yaml
|
|
71
|
+
```yaml
|
|
72
|
+
entry_points:
|
|
73
|
+
- name: "research"
|
|
74
|
+
target: "coordinator_agent"
|
|
75
|
+
condition: "always"
|
|
76
|
+
|
|
77
|
+
agents:
|
|
78
|
+
coordinator_agent:
|
|
79
|
+
agent_type: "llm"
|
|
80
|
+
model: "gpt-4o-mini"
|
|
81
|
+
prompt: "Break down the research query into sub-questions."
|
|
82
|
+
can_call:
|
|
83
|
+
- {agent: "web_researcher"}
|
|
84
|
+
- {agent: "data_analyst"}
|
|
85
|
+
- {agent: "domain_expert"}
|
|
86
|
+
|
|
87
|
+
web_researcher:
|
|
88
|
+
agent_type: "llm"
|
|
89
|
+
model: "gpt-4o"
|
|
90
|
+
prompt: "Research the web for relevant information on the topic."
|
|
91
|
+
tools: ["web_search"]
|
|
92
|
+
parallel: true
|
|
93
|
+
|
|
94
|
+
data_analyst:
|
|
95
|
+
agent_type: "programmatic"
|
|
96
|
+
parallel: true
|
|
97
|
+
|
|
98
|
+
domain_expert:
|
|
99
|
+
agent_type: "llm"
|
|
100
|
+
model: "claude-sonnet-4-5-20250929"
|
|
101
|
+
prompt: "Provide domain expertise and analysis."
|
|
102
|
+
|
|
103
|
+
synthesizer_agent:
|
|
104
|
+
agent_type: "llm"
|
|
105
|
+
model: "gpt-4o"
|
|
106
|
+
prompt: "Synthesize all research findings into a coherent report."
|
|
107
|
+
wait_for: "web_researcher AND data_analyst AND domain_expert?"
|
|
108
|
+
wait_for_timeout: 120
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### agency/synthesizer_agent.py
|
|
112
|
+
```python
|
|
113
|
+
from leafmesh import chain
|
|
114
|
+
|
|
115
|
+
def add_citations(result, context):
|
|
116
|
+
sources = result.get("sources", [])
|
|
117
|
+
result["citation_count"] = len(sources)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def format_report(result, context):
|
|
121
|
+
result["format"] = "markdown"
|
|
122
|
+
result["report"] = f"# Research Report\n\n{result.get('synthesis', '')}"
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
@chain(add_citations, format_report)
|
|
126
|
+
async def synthesizer_agent(llm_response, input_data, context):
|
|
127
|
+
upstream = input_data.get("upstream_yields", {})
|
|
128
|
+
web = upstream.get("web_researcher", {})
|
|
129
|
+
data = upstream.get("data_analyst", {})
|
|
130
|
+
domain = upstream.get("domain_expert", {}) # May be empty (optional)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"synthesis": llm_response,
|
|
134
|
+
"sources": web.get("sources", []) + data.get("sources", []),
|
|
135
|
+
"confidence": 0.9 if domain else 0.7,
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Pattern 3: HITL Dual-Mode (Human Reviews Agent Output)
|
|
142
|
+
|
|
143
|
+
System triggers a workflow, agent processes, human reviews before continuing.
|
|
144
|
+
|
|
145
|
+
### config.yaml
|
|
146
|
+
```yaml
|
|
147
|
+
entry_points:
|
|
148
|
+
- name: "greet_user"
|
|
149
|
+
target: "greeter_agent"
|
|
150
|
+
- name: "human_contact"
|
|
151
|
+
target: "client"
|
|
152
|
+
|
|
153
|
+
agents:
|
|
154
|
+
client:
|
|
155
|
+
name: "client"
|
|
156
|
+
agent_type: "human"
|
|
157
|
+
human_interface: "webhook" # required to actually use webhook_config below
|
|
158
|
+
communication_type: "dual"
|
|
159
|
+
human_timeout_seconds: 300
|
|
160
|
+
webhook_config:
|
|
161
|
+
outbound_url: "http://127.0.0.1:9999/human-notify"
|
|
162
|
+
outbound_headers:
|
|
163
|
+
Content-Type: "application/json"
|
|
164
|
+
outbound_timeout: 30
|
|
165
|
+
# inbound_endpoint is auto-derived from entry_points — no need to set it
|
|
166
|
+
max_retries: 1
|
|
167
|
+
retry_delay: 2
|
|
168
|
+
can_call:
|
|
169
|
+
- agent: "greeter_agent"
|
|
170
|
+
condition: "not calling_agent_response.from_agent"
|
|
171
|
+
- agent: "processor_agent"
|
|
172
|
+
condition: "calling_agent_response.from_agent == 'greeter_agent'"
|
|
173
|
+
|
|
174
|
+
greeter_agent:
|
|
175
|
+
agent_type: "llm"
|
|
176
|
+
model: "gpt-4o-mini"
|
|
177
|
+
communication_type: "dual"
|
|
178
|
+
can_call:
|
|
179
|
+
- agent: "client" # Routes to human for review
|
|
180
|
+
|
|
181
|
+
processor_agent:
|
|
182
|
+
agent_type: "programmatic"
|
|
183
|
+
can_call:
|
|
184
|
+
- agent: "researcher_agent"
|
|
185
|
+
condition: "calling_agent_response.item_count > 0"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### How it works
|
|
189
|
+
```
|
|
190
|
+
Scenario 1 (system-initiated):
|
|
191
|
+
POST /api/mesh/request {"entry_point": "greet_user", ...}
|
|
192
|
+
-> greeter -> client (HITL, webhook sent)
|
|
193
|
+
-> human responds via POST /webhook/greet_user
|
|
194
|
+
-> from_agent=="greeter_agent" -> processor -> ...
|
|
195
|
+
|
|
196
|
+
Scenario 2 (human-initiated):
|
|
197
|
+
POST /webhook/human_contact {"message": "I need help"}
|
|
198
|
+
-> client (no from_agent -> greeter)
|
|
199
|
+
-> greeter (dual callback -> client HITL, webhook sent)
|
|
200
|
+
-> human responds -> from_agent=="greeter_agent" -> processor -> ...
|
|
201
|
+
|
|
202
|
+
Scenario 3 (same session, new message):
|
|
203
|
+
POST /webhook/human_contact {"session_id": "existing", "message": "Now check refund"}
|
|
204
|
+
-> session not paused -> new request, conversation history preserved
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Testing HITL locally
|
|
208
|
+
```bash
|
|
209
|
+
# Terminal 1: stub receiver (captures outbound webhooks)
|
|
210
|
+
python hitl_stub_receiver.py
|
|
211
|
+
|
|
212
|
+
# Terminal 2: mesh server
|
|
213
|
+
python main.py
|
|
214
|
+
|
|
215
|
+
# Terminal 3: trigger + respond
|
|
216
|
+
SECRET=$(curl -s http://127.0.0.1:18820/api/webhook/secret | jq -r .secret)
|
|
217
|
+
curl -X POST http://127.0.0.1:18820/api/mesh/request \
|
|
218
|
+
-H "Content-Type: application/json" \
|
|
219
|
+
-d '{"entry_point": "greet_user", "data": {"message": "Help me"}}'
|
|
220
|
+
|
|
221
|
+
# ... stub prints session_id, then respond:
|
|
222
|
+
BODY='{"session_id": "SESSION_ID", "decision": "approved", "message": "Proceed"}'
|
|
223
|
+
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
|
|
224
|
+
curl -X POST http://127.0.0.1:18820/webhook/greet_user \
|
|
225
|
+
-H "Content-Type: application/json" \
|
|
226
|
+
-H "X-LeafMesh-Signature: sha256=$SIG" \
|
|
227
|
+
-d "$BODY"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Pattern 4: Scheduled Background Jobs
|
|
233
|
+
|
|
234
|
+
### config.yaml
|
|
235
|
+
```yaml
|
|
236
|
+
agents:
|
|
237
|
+
daily_report_agent:
|
|
238
|
+
agent_type: "programmatic"
|
|
239
|
+
wake_up: "0 9 * * *" # Every day at 9 AM
|
|
240
|
+
communication_type: "execute" # Fire-and-forget
|
|
241
|
+
|
|
242
|
+
hourly_monitor_agent:
|
|
243
|
+
agent_type: "llm"
|
|
244
|
+
model: "gpt-4o-mini"
|
|
245
|
+
prompt: "Analyze system metrics and flag anomalies."
|
|
246
|
+
wake_up: "0 * * * *" # Every hour
|
|
247
|
+
tools: ["check_metrics", "send_alert"]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### agency/daily_report_agent.py
|
|
251
|
+
```python
|
|
252
|
+
async def daily_report_agent(llm_response, input_data, context):
|
|
253
|
+
from datetime import datetime, timezone
|
|
254
|
+
return {
|
|
255
|
+
"report": "Daily system health: all green",
|
|
256
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
257
|
+
"checks": ["redis_ok", "agents_healthy", "sessions_active"],
|
|
258
|
+
"status": "completed",
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Pattern 5: Multi-Model Strategy (Cost Optimization)
|
|
265
|
+
|
|
266
|
+
### config.yaml
|
|
267
|
+
```yaml
|
|
268
|
+
agents:
|
|
269
|
+
triage_agent:
|
|
270
|
+
agent_type: "llm"
|
|
271
|
+
model: "gpt-4o-mini" # Cheap, fast triage
|
|
272
|
+
optimization_strategy: "speed"
|
|
273
|
+
prompt: "Classify query complexity: simple, moderate, or complex."
|
|
274
|
+
can_call:
|
|
275
|
+
- agent: "simple_handler"
|
|
276
|
+
condition: "complexity == 'simple'"
|
|
277
|
+
- agent: "complex_handler"
|
|
278
|
+
condition: "complexity == 'complex'"
|
|
279
|
+
- agent: "moderate_handler"
|
|
280
|
+
condition: "complexity == 'moderate'"
|
|
281
|
+
|
|
282
|
+
simple_handler:
|
|
283
|
+
agent_type: "llm"
|
|
284
|
+
model: "gpt-4o-mini" # Cheap for simple queries
|
|
285
|
+
optimization_strategy: "cost"
|
|
286
|
+
|
|
287
|
+
moderate_handler:
|
|
288
|
+
agent_type: "llm"
|
|
289
|
+
model: "gpt-4o" # Balanced
|
|
290
|
+
optimization_strategy: "cost"
|
|
291
|
+
|
|
292
|
+
complex_handler:
|
|
293
|
+
agent_type: "llm"
|
|
294
|
+
model: "claude-sonnet-4-5-20250929" # Best quality for hard tasks
|
|
295
|
+
optimization_strategy: "performance"
|
|
296
|
+
reasoning: true # Enable chain-of-thought
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Pattern 6: External Framework Integration
|
|
302
|
+
|
|
303
|
+
### CrewAI (connector-only, no Python needed)
|
|
304
|
+
```yaml
|
|
305
|
+
agents:
|
|
306
|
+
crewai_research:
|
|
307
|
+
agent_type: "external"
|
|
308
|
+
framework: "crewai"
|
|
309
|
+
connector_config:
|
|
310
|
+
endpoint: "http://localhost:9000"
|
|
311
|
+
api_key: "${CREWAI_API_KEY}" # Bearer Token
|
|
312
|
+
# user_api_key: "${CREWAI_USER_API_KEY}" # User Bearer Token (preferred over api_key)
|
|
313
|
+
poll_interval: 2.0
|
|
314
|
+
max_poll_seconds: 300
|
|
315
|
+
can_call:
|
|
316
|
+
- agent: "internal_processor"
|
|
317
|
+
yields: {result: object}
|
|
318
|
+
inputs: {task: string}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Programmatic + Connector (connector-only, no Python needed)
|
|
322
|
+
```yaml
|
|
323
|
+
agents:
|
|
324
|
+
zapier_sheets:
|
|
325
|
+
agent_type: "programmatic"
|
|
326
|
+
integration: "zapier"
|
|
327
|
+
connector_config:
|
|
328
|
+
connection: "google_sheets"
|
|
329
|
+
action: "create_spreadsheet_row"
|
|
330
|
+
api_key: "${ZAPIER_API_KEY}"
|
|
331
|
+
yields: {status: string}
|
|
332
|
+
inputs: {row_data: object}
|
|
333
|
+
|
|
334
|
+
n8n_workflow:
|
|
335
|
+
agent_type: "programmatic"
|
|
336
|
+
integration: "n8n"
|
|
337
|
+
connector_config:
|
|
338
|
+
webhook_url: "http://localhost:5678/webhook/my-workflow"
|
|
339
|
+
mode: "callback"
|
|
340
|
+
callback_timeout: 120
|
|
341
|
+
yields: {result: object}
|
|
342
|
+
inputs: {data: object}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The connector response is returned as-is. To post-process, add `@sdk.intelligence()`:
|
|
346
|
+
|
|
347
|
+
### Programmatic + Connector + Python (post-process connector result)
|
|
348
|
+
```python
|
|
349
|
+
async def zapier_sheets(connector_response, input_data, context):
|
|
350
|
+
# connector_response = Zapier's raw response
|
|
351
|
+
return {
|
|
352
|
+
"status": "logged" if connector_response.get("success") else "failed",
|
|
353
|
+
"row_id": connector_response.get("content", {}).get("id"),
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Using Zapier as @pre_compose helper (enrichment before LLM)
|
|
358
|
+
```python
|
|
359
|
+
from leafmesh import pre_compose, zapier
|
|
360
|
+
|
|
361
|
+
@pre_compose(
|
|
362
|
+
context_processor=zapier(
|
|
363
|
+
action="slack_send_message",
|
|
364
|
+
api_key="${ZAPIER_NLA_API_KEY}",
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
async def notification_agent(llm_response, input_data, context):
|
|
368
|
+
slack_result = context["prepared_data"]["business_context"]
|
|
369
|
+
return {"notified": True, "channel": slack_result.get("channel")}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Pattern 7: Memory-Aware Agent (Learning from History)
|
|
375
|
+
|
|
376
|
+
### config.yaml
|
|
377
|
+
```yaml
|
|
378
|
+
agents:
|
|
379
|
+
advisor_agent:
|
|
380
|
+
agent_type: "llm"
|
|
381
|
+
model: "gpt-4o"
|
|
382
|
+
prompt: |
|
|
383
|
+
You are a financial advisor. Use the memory of past interactions
|
|
384
|
+
to provide personalized, context-aware advice. Reference previous
|
|
385
|
+
conversations when relevant.
|
|
386
|
+
memory: true
|
|
387
|
+
memory_limit: 20 # Load last 20 feed posts
|
|
388
|
+
tools: ["market_data", "portfolio_lookup"]
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### agency/advisor_agent.py
|
|
392
|
+
```python
|
|
393
|
+
from leafmesh import chain_with_results
|
|
394
|
+
|
|
395
|
+
def check_portfolio(result, context):
|
|
396
|
+
memory = context.get("memory_posts", [])
|
|
397
|
+
prior_topics = [p.get("content", "") for p in memory[-5:]]
|
|
398
|
+
result["prior_context"] = prior_topics
|
|
399
|
+
return result
|
|
400
|
+
|
|
401
|
+
def personalize(result, context):
|
|
402
|
+
if result.get("prior_context"):
|
|
403
|
+
result["personalized"] = True
|
|
404
|
+
result["greeting"] = "Welcome back! Continuing from where we left off..."
|
|
405
|
+
return result
|
|
406
|
+
|
|
407
|
+
@chain_with_results(check_portfolio, personalize)
|
|
408
|
+
async def advisor_agent(llm_response, input_data, context):
|
|
409
|
+
memory_posts = context.get("memory_posts", [])
|
|
410
|
+
return {
|
|
411
|
+
"advice": llm_response,
|
|
412
|
+
"session_count": len(memory_posts),
|
|
413
|
+
"confidence": 0.85,
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Pattern 8: Conditional Processing with @conditional_chain
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from leafmesh import conditional_chain
|
|
423
|
+
|
|
424
|
+
def needs_translation(result, context):
|
|
425
|
+
return result.get("language") != "en"
|
|
426
|
+
|
|
427
|
+
def translate_to_english(result, context):
|
|
428
|
+
result["original_language"] = result["language"]
|
|
429
|
+
result["translated"] = True
|
|
430
|
+
# In real code, call translation API here
|
|
431
|
+
return result
|
|
432
|
+
|
|
433
|
+
def add_disclaimer(result, context):
|
|
434
|
+
result["disclaimer"] = "This response was auto-translated"
|
|
435
|
+
return result
|
|
436
|
+
|
|
437
|
+
@conditional_chain(needs_translation, translate_to_english, add_disclaimer)
|
|
438
|
+
async def intake_agent(llm_response, input_data, context):
|
|
439
|
+
return {
|
|
440
|
+
"response": llm_response,
|
|
441
|
+
"language": input_data.get("language", "en"),
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Pattern 9: Custom Global Tools
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
from leafmesh import global_tool
|
|
451
|
+
import httpx
|
|
452
|
+
|
|
453
|
+
@global_tool(
|
|
454
|
+
name="fetch_weather",
|
|
455
|
+
description="Get current weather for a city",
|
|
456
|
+
category="web",
|
|
457
|
+
timeout_seconds=10,
|
|
458
|
+
)
|
|
459
|
+
async def fetch_weather(city: str) -> dict:
|
|
460
|
+
async with httpx.AsyncClient() as client:
|
|
461
|
+
resp = await client.get(f"https://wttr.in/{city}?format=j1")
|
|
462
|
+
data = resp.json()
|
|
463
|
+
current = data.get("current_condition", [{}])[0]
|
|
464
|
+
return {
|
|
465
|
+
"city": city,
|
|
466
|
+
"temp_c": current.get("temp_C"),
|
|
467
|
+
"description": current.get("weatherDesc", [{}])[0].get("value"),
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@global_tool(
|
|
471
|
+
name="db_query",
|
|
472
|
+
description="Run a read-only database query",
|
|
473
|
+
category="data",
|
|
474
|
+
allowed_agents=["analyst_agent", "report_agent"],
|
|
475
|
+
requires_confirmation=True,
|
|
476
|
+
timeout_seconds=30,
|
|
477
|
+
)
|
|
478
|
+
async def db_query(sql: str) -> dict:
|
|
479
|
+
# Validate read-only
|
|
480
|
+
if any(kw in sql.upper() for kw in ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]):
|
|
481
|
+
return {"error": "Only SELECT queries allowed"}
|
|
482
|
+
# Execute query...
|
|
483
|
+
return {"rows": [], "count": 0}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Quick Reference: Decorator Stacking
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
# All decorators can be combined. Execution order (bottom to top):
|
|
492
|
+
|
|
493
|
+
@chain(step1, step2) # 4. Post-process pipeline
|
|
494
|
+
@compose( # 3. Shape per-target payloads
|
|
495
|
+
agent_a=lambda r, c: {"key": r["x"]},
|
|
496
|
+
agent_b=lambda r, c: {"key": r["y"]},
|
|
497
|
+
)
|
|
498
|
+
@pre_compose( # 1. Prepare inputs (before LLM)
|
|
499
|
+
context_processor=enrich,
|
|
500
|
+
input_processor=clean,
|
|
501
|
+
)
|
|
502
|
+
async def my_agent(llm_response, input_data, context):
|
|
503
|
+
return {"x": 1, "y": 2} # 2. Agent logic + LLM
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Docker Deployment
|
|
507
|
+
|
|
508
|
+
```yaml
|
|
509
|
+
# docker-compose.yml
|
|
510
|
+
services:
|
|
511
|
+
redis:
|
|
512
|
+
image: redis:7-alpine
|
|
513
|
+
ports: ["6379:6379"]
|
|
514
|
+
healthcheck:
|
|
515
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
516
|
+
|
|
517
|
+
app:
|
|
518
|
+
build: .
|
|
519
|
+
ports: ["18820:18820"]
|
|
520
|
+
env_file: .env
|
|
521
|
+
depends_on:
|
|
522
|
+
redis:
|
|
523
|
+
condition: service_healthy
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
```dockerfile
|
|
527
|
+
# Dockerfile
|
|
528
|
+
FROM python:3.13-slim
|
|
529
|
+
WORKDIR /app
|
|
530
|
+
COPY requirements.txt .
|
|
531
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
532
|
+
COPY configs/ configs/
|
|
533
|
+
COPY agency/ agency/
|
|
534
|
+
COPY main.py .
|
|
535
|
+
EXPOSE 18820
|
|
536
|
+
CMD ["python", "main.py"]
|
|
537
|
+
```
|