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,151 @@
|
|
|
1
|
+
"""LLM Agent — Auto-discovered with @chain + @compose (OR fan-in advisor).
|
|
2
|
+
|
|
3
|
+
auto_discover matches function name 'advisor_agent' to YAML agent config.
|
|
4
|
+
|
|
5
|
+
Features showcased:
|
|
6
|
+
- @chain: sequential post-processing pipeline (validate_inputs -> score_priorities)
|
|
7
|
+
- @compose: inject helper functions into context (report shaping, alert shaping)
|
|
8
|
+
- wait_for: "processor_agent AND (researcher_agent OR fallback_researcher_agent)"
|
|
9
|
+
- OR fan-in: whichever research agent finishes first satisfies the expression
|
|
10
|
+
- optimization_strategy: "performance" — prioritizes quality over cost
|
|
11
|
+
|
|
12
|
+
Execution flow:
|
|
13
|
+
1. Fan-in: SDK waits for processor_agent AND (researcher OR fallback_researcher)
|
|
14
|
+
2. fallback_researcher_agent is instant → OR resolves immediately
|
|
15
|
+
3. LLM analyzes combined upstream results
|
|
16
|
+
4. This function post-processes, picking the best research source
|
|
17
|
+
5. @compose injects shape_report and shape_alerts into context
|
|
18
|
+
6. @chain runs: validate_inputs -> score_priorities (sequential pipeline)
|
|
19
|
+
|
|
20
|
+
Fan-in patterns (all supported by the SDK):
|
|
21
|
+
wait_for: "A AND B" — waits for both A and B
|
|
22
|
+
wait_for: "A OR B" — waits for either A or B (first wins)
|
|
23
|
+
wait_for: "A AND B?" — A required, B optional
|
|
24
|
+
wait_for: "A AND (B OR C)" — A required + first of B or C (used here)
|
|
25
|
+
wait_for: "A AND (B OR C) AND D?" — A required + first of B/C + D optional
|
|
26
|
+
"""
|
|
27
|
+
from leafmesh import chain, compose, LeafMeshLogger
|
|
28
|
+
|
|
29
|
+
logger = LeafMeshLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_inputs(result, context):
|
|
33
|
+
"""Chain step 1: Validate that upstream data is present and well-formed."""
|
|
34
|
+
if not result.get("recommendations"):
|
|
35
|
+
result["recommendations"] = ["No specific recommendations available"]
|
|
36
|
+
if not result.get("risk_assessment"):
|
|
37
|
+
result["risk_assessment"] = "Unable to assess — insufficient data"
|
|
38
|
+
result["validated"] = True
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def score_priorities(result, context):
|
|
43
|
+
"""Chain step 2: Score and rank recommendations by priority."""
|
|
44
|
+
recs = result.get("recommendations", [])
|
|
45
|
+
result["priority_score"] = min(1.0, len(recs) * 0.2) if recs else 0.0
|
|
46
|
+
result["next_steps"] = recs[:3] if recs else ["Gather more data"]
|
|
47
|
+
result["scored"] = True
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def shape_report(data, context):
|
|
52
|
+
"""Compose helper: Shape output for report consumption."""
|
|
53
|
+
return {
|
|
54
|
+
"title": "Advisory Report",
|
|
55
|
+
"recommendations": data.get("recommendations", []),
|
|
56
|
+
"risk_assessment": data.get("risk_assessment", ""),
|
|
57
|
+
"priority_score": data.get("priority_score", 0),
|
|
58
|
+
"project": "{{project_name}}",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def shape_alerts(data, context):
|
|
63
|
+
"""Compose helper: Shape output for alerting systems."""
|
|
64
|
+
score = data.get("priority_score", 0)
|
|
65
|
+
return {
|
|
66
|
+
"alert_level": "high" if score > 0.7 else "medium" if score > 0.4 else "low",
|
|
67
|
+
"summary": data.get("risk_assessment", ""),
|
|
68
|
+
"action_required": score > 0.5,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@chain(validate_inputs, score_priorities)
|
|
73
|
+
@compose(report=shape_report, alerts=shape_alerts)
|
|
74
|
+
async def advisor_agent(llm_response, input_data, context):
|
|
75
|
+
"""Analyze upstream results and produce actionable recommendations.
|
|
76
|
+
|
|
77
|
+
At this point:
|
|
78
|
+
- Fan-in complete: processor_agent AND (researcher OR fallback) finished
|
|
79
|
+
- input_data["upstream_yields"] has yields from all contributing agents
|
|
80
|
+
- context["report"] and context["alerts"] are injected by @compose
|
|
81
|
+
- After this function, @chain runs validate_inputs -> score_priorities
|
|
82
|
+
"""
|
|
83
|
+
# ─── Fan-in upstream yields ───
|
|
84
|
+
# For OR fan-in, the SDK includes yields from whichever agents completed.
|
|
85
|
+
# Check both research agents — prefer LLM researcher (higher confidence).
|
|
86
|
+
upstream_yields = input_data.get("upstream_yields", {})
|
|
87
|
+
processor_yields = upstream_yields.get("processor_agent", {})
|
|
88
|
+
researcher_yields = upstream_yields.get("researcher_agent", {})
|
|
89
|
+
fallback_yields = upstream_yields.get("fallback_researcher_agent", {})
|
|
90
|
+
|
|
91
|
+
# ─── Pick the best research source ───
|
|
92
|
+
# LLM researcher has confidence 0.85, fallback has 0.6
|
|
93
|
+
skipped_agents = []
|
|
94
|
+
if researcher_yields and researcher_yields.get("status") == "researched":
|
|
95
|
+
research_data = researcher_yields
|
|
96
|
+
research_source = "researcher_agent"
|
|
97
|
+
research_confidence = 0.85
|
|
98
|
+
elif fallback_yields and fallback_yields.get("status") == "researched":
|
|
99
|
+
research_data = fallback_yields
|
|
100
|
+
research_source = "fallback_researcher_agent"
|
|
101
|
+
research_confidence = 0.6
|
|
102
|
+
skipped_agents.append("researcher_agent")
|
|
103
|
+
else:
|
|
104
|
+
research_data = {}
|
|
105
|
+
research_source = "none"
|
|
106
|
+
research_confidence = 0.0
|
|
107
|
+
skipped_agents.extend(["researcher_agent", "fallback_researcher_agent"])
|
|
108
|
+
|
|
109
|
+
# ─── Build recommendations from upstream data ───
|
|
110
|
+
processed_items = processor_yields.get("processed_items", [])
|
|
111
|
+
findings = research_data.get("findings", [])
|
|
112
|
+
|
|
113
|
+
recommendations = []
|
|
114
|
+
if processed_items:
|
|
115
|
+
urgent = [i for i in processed_items if i.get("category") == "urgent"]
|
|
116
|
+
if urgent:
|
|
117
|
+
recommendations.append(f"Address {len(urgent)} urgent item(s) immediately")
|
|
118
|
+
recommendations.append(f"Review {len(processed_items)} processed items")
|
|
119
|
+
|
|
120
|
+
if findings:
|
|
121
|
+
recommendations.append(f"Incorporate {len(findings)} research finding(s)")
|
|
122
|
+
|
|
123
|
+
risk = "low"
|
|
124
|
+
if any(i.get("category") == "urgent" for i in processed_items):
|
|
125
|
+
risk = "elevated — urgent items detected"
|
|
126
|
+
|
|
127
|
+
logger.info(f"Advisory complete — {len(recommendations)} recommendations, risk={risk}, source={research_source}")
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"recommendations": recommendations,
|
|
131
|
+
"risk_assessment": risk,
|
|
132
|
+
"research_source": research_source,
|
|
133
|
+
"research_confidence": research_confidence,
|
|
134
|
+
"skipped_agents": skipped_agents,
|
|
135
|
+
"source_data": {
|
|
136
|
+
"processed_count": len(processed_items),
|
|
137
|
+
"finding_count": len(findings),
|
|
138
|
+
},
|
|
139
|
+
"status": "advised",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ─── Alternative: @chain_with_results ───
|
|
143
|
+
# If you want to accumulate results from each chain step:
|
|
144
|
+
#
|
|
145
|
+
# from leafmesh import chain_with_results
|
|
146
|
+
#
|
|
147
|
+
# @chain_with_results(validate_inputs, score_priorities)
|
|
148
|
+
# async def advisor_agent(llm_response, input_data, context):
|
|
149
|
+
# # After execution, context["chain_results"] has a list of
|
|
150
|
+
# # each step's return value: [validate_result, score_result]
|
|
151
|
+
# ...
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""External Agent Reference — ALL external integrations documented here.
|
|
2
|
+
|
|
3
|
+
NOTE: This file is named external_agents.py (NOT *_agent.py) so auto_discover
|
|
4
|
+
skips it. Everything here is COMMENTED OUT — uncomment what you need.
|
|
5
|
+
|
|
6
|
+
Each section includes:
|
|
7
|
+
- Required environment variables
|
|
8
|
+
- YAML config to add to config.yaml
|
|
9
|
+
- Python implementation code
|
|
10
|
+
|
|
11
|
+
All connectors are included with `pip install leafmesh` — no extra installs needed.
|
|
12
|
+
|
|
13
|
+
For each integration, you have TWO options:
|
|
14
|
+
1. Add the YAML config to config.yaml as an external agent
|
|
15
|
+
2. Use helper factories to wire into an existing agent's pipeline
|
|
16
|
+
(works with @pre_compose, @chain, and @conditional_chain)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ===================================================================
|
|
20
|
+
# 1. CREWAI — Delegate to a CrewAI crew
|
|
21
|
+
# ===================================================================
|
|
22
|
+
#
|
|
23
|
+
# YAML config (add to agents: section in config.yaml):
|
|
24
|
+
# crewai_research:
|
|
25
|
+
# name: "crewai_research"
|
|
26
|
+
# agent_type: "external"
|
|
27
|
+
# framework: "crewai"
|
|
28
|
+
# description: "Delegates research tasks to a CrewAI crew"
|
|
29
|
+
# connector_config:
|
|
30
|
+
# endpoint: "http://localhost:9000"
|
|
31
|
+
# api_key: "${CREWAI_API_KEY:}"
|
|
32
|
+
# yields:
|
|
33
|
+
# research_result: "string"
|
|
34
|
+
# sources: "list"
|
|
35
|
+
# can_call:
|
|
36
|
+
# - agent: "advisor_agent"
|
|
37
|
+
#
|
|
38
|
+
# Python (create agency/crewai_research_agent.py):
|
|
39
|
+
# from leafmesh import pre_compose
|
|
40
|
+
#
|
|
41
|
+
# async def crewai_research_agent(external_response, input_data, context):
|
|
42
|
+
# """Post-process CrewAI crew results."""
|
|
43
|
+
# return {
|
|
44
|
+
# "research_result": external_response.get("output", ""),
|
|
45
|
+
# "sources": external_response.get("sources", []),
|
|
46
|
+
# }
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ===================================================================
|
|
50
|
+
# 2. LANGGRAPH — Delegate to a LangGraph graph
|
|
51
|
+
# ===================================================================
|
|
52
|
+
#
|
|
53
|
+
# YAML config:
|
|
54
|
+
# langgraph_workflow:
|
|
55
|
+
# name: "langgraph_workflow"
|
|
56
|
+
# agent_type: "external"
|
|
57
|
+
# framework: "langgraph"
|
|
58
|
+
# description: "Runs a LangGraph stateful workflow"
|
|
59
|
+
# connector_config:
|
|
60
|
+
# endpoint: "${LANGGRAPH_API_URL:http://localhost:8123}"
|
|
61
|
+
# api_key: "${LANGGRAPH_API_KEY:}"
|
|
62
|
+
# graph_id: "my_graph"
|
|
63
|
+
# yields:
|
|
64
|
+
# workflow_result: "object"
|
|
65
|
+
#
|
|
66
|
+
# Python (create agency/langgraph_workflow_agent.py):
|
|
67
|
+
# async def langgraph_workflow_agent(external_response, input_data, context):
|
|
68
|
+
# return {"workflow_result": external_response}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ===================================================================
|
|
72
|
+
# 3. AUTOGEN — Delegate to an AutoGen agent group
|
|
73
|
+
# ===================================================================
|
|
74
|
+
#
|
|
75
|
+
# YAML config:
|
|
76
|
+
# autogen_team:
|
|
77
|
+
# name: "autogen_team"
|
|
78
|
+
# agent_type: "external"
|
|
79
|
+
# framework: "autogen"
|
|
80
|
+
# description: "Multi-agent conversation via AutoGen"
|
|
81
|
+
# connector_config:
|
|
82
|
+
# endpoint: "http://localhost:8081"
|
|
83
|
+
# yields:
|
|
84
|
+
# team_result: "string"
|
|
85
|
+
#
|
|
86
|
+
# Python (create agency/autogen_team_agent.py):
|
|
87
|
+
# async def autogen_team_agent(external_response, input_data, context):
|
|
88
|
+
# return {"team_result": external_response.get("output", "")}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ===================================================================
|
|
92
|
+
# 4. A2A (Agent-to-Agent Protocol) — Google's interop standard
|
|
93
|
+
# ===================================================================
|
|
94
|
+
#
|
|
95
|
+
# YAML config:
|
|
96
|
+
# a2a_partner:
|
|
97
|
+
# name: "a2a_partner"
|
|
98
|
+
# agent_type: "external"
|
|
99
|
+
# framework: "a2a"
|
|
100
|
+
# description: "Communicates with an A2A-compatible agent"
|
|
101
|
+
# connector_config:
|
|
102
|
+
# endpoint: "http://partner-agent:8080"
|
|
103
|
+
# agent_card_url: "http://partner-agent:8080/.well-known/agent.json"
|
|
104
|
+
# yields:
|
|
105
|
+
# partner_response: "object"
|
|
106
|
+
#
|
|
107
|
+
# Python (create agency/a2a_partner_agent.py):
|
|
108
|
+
# async def a2a_partner_agent(external_response, input_data, context):
|
|
109
|
+
# return {"partner_response": external_response}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ===================================================================
|
|
113
|
+
# 5. MCP (Model Context Protocol) — Tool servers
|
|
114
|
+
# ===================================================================
|
|
115
|
+
#
|
|
116
|
+
# Two transport modes: HTTP (SSE) and stdio
|
|
117
|
+
#
|
|
118
|
+
# YAML config (HTTP mode):
|
|
119
|
+
# mcp_tools:
|
|
120
|
+
# name: "mcp_tools"
|
|
121
|
+
# agent_type: "external"
|
|
122
|
+
# framework: "mcp"
|
|
123
|
+
# description: "Access tools from an MCP server"
|
|
124
|
+
# connector_config:
|
|
125
|
+
# transport: "http"
|
|
126
|
+
# endpoint: "http://localhost:3000/sse"
|
|
127
|
+
# yields:
|
|
128
|
+
# tool_results: "object"
|
|
129
|
+
#
|
|
130
|
+
# YAML config (stdio mode):
|
|
131
|
+
# mcp_local:
|
|
132
|
+
# name: "mcp_local"
|
|
133
|
+
# agent_type: "external"
|
|
134
|
+
# framework: "mcp"
|
|
135
|
+
# description: "Local MCP tool server via stdio"
|
|
136
|
+
# connector_config:
|
|
137
|
+
# transport: "stdio"
|
|
138
|
+
# command: "npx"
|
|
139
|
+
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
140
|
+
# yields:
|
|
141
|
+
# tool_results: "object"
|
|
142
|
+
#
|
|
143
|
+
# Python (create agency/mcp_tools_agent.py):
|
|
144
|
+
# async def mcp_tools_agent(external_response, input_data, context):
|
|
145
|
+
# return {"tool_results": external_response}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ===================================================================
|
|
149
|
+
# 6. ZAPIER — Natural Language Actions
|
|
150
|
+
# ===================================================================
|
|
151
|
+
#
|
|
152
|
+
# Set env: ZAPIER_NLA_API_KEY=your-key
|
|
153
|
+
#
|
|
154
|
+
# YAML config:
|
|
155
|
+
# zapier_actions:
|
|
156
|
+
# name: "zapier_actions"
|
|
157
|
+
# agent_type: "external"
|
|
158
|
+
# framework: "zapier"
|
|
159
|
+
# description: "Trigger Zapier NLA actions"
|
|
160
|
+
# connector_config:
|
|
161
|
+
# api_key: "${ZAPIER_NLA_API_KEY:}"
|
|
162
|
+
# yields:
|
|
163
|
+
# action_result: "object"
|
|
164
|
+
#
|
|
165
|
+
# Python (create agency/zapier_actions_agent.py):
|
|
166
|
+
# async def zapier_actions_agent(external_response, input_data, context):
|
|
167
|
+
# return {"action_result": external_response}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ===================================================================
|
|
171
|
+
# 7. COMPOSIO — Managed tool integrations (GitHub, Slack, etc.)
|
|
172
|
+
# ===================================================================
|
|
173
|
+
#
|
|
174
|
+
# Set env: COMPOSIO_API_KEY=your-key
|
|
175
|
+
#
|
|
176
|
+
# YAML config:
|
|
177
|
+
# composio_tools:
|
|
178
|
+
# name: "composio_tools"
|
|
179
|
+
# agent_type: "external"
|
|
180
|
+
# framework: "composio"
|
|
181
|
+
# description: "Access Composio-managed integrations"
|
|
182
|
+
# connector_config:
|
|
183
|
+
# api_key: "${COMPOSIO_API_KEY:}"
|
|
184
|
+
# actions: ["github_star_repo", "slack_send_message"]
|
|
185
|
+
# yields:
|
|
186
|
+
# integration_result: "object"
|
|
187
|
+
#
|
|
188
|
+
# Python (create agency/composio_tools_agent.py):
|
|
189
|
+
# async def composio_tools_agent(external_response, input_data, context):
|
|
190
|
+
# return {"integration_result": external_response}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ===================================================================
|
|
194
|
+
# 8. N8N — Workflow automation
|
|
195
|
+
# ===================================================================
|
|
196
|
+
#
|
|
197
|
+
# Set env: N8N_BASE_URL=http://localhost:5678, N8N_API_KEY=your-key
|
|
198
|
+
#
|
|
199
|
+
# YAML config:
|
|
200
|
+
# n8n_workflow:
|
|
201
|
+
# name: "n8n_workflow"
|
|
202
|
+
# agent_type: "external"
|
|
203
|
+
# framework: "n8n"
|
|
204
|
+
# description: "Trigger n8n workflows"
|
|
205
|
+
# connector_config:
|
|
206
|
+
# base_url: "${N8N_BASE_URL:http://localhost:5678}"
|
|
207
|
+
# api_key: "${N8N_API_KEY:}"
|
|
208
|
+
# workflow_id: "your-workflow-id"
|
|
209
|
+
# yields:
|
|
210
|
+
# workflow_result: "object"
|
|
211
|
+
#
|
|
212
|
+
# Python (create agency/n8n_workflow_agent.py):
|
|
213
|
+
# async def n8n_workflow_agent(external_response, input_data, context):
|
|
214
|
+
# return {"workflow_result": external_response}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ===================================================================
|
|
218
|
+
# 9. HELPER FACTORIES — Wire integrations into agent decorators
|
|
219
|
+
#
|
|
220
|
+
# These work with @pre_compose (before LLM), @chain (after agent),
|
|
221
|
+
# and @conditional_chain (conditional post-processing).
|
|
222
|
+
# ===================================================================
|
|
223
|
+
#
|
|
224
|
+
# from leafmesh import pre_compose, chain, conditional_chain, mcp, zapier, composio, n8n
|
|
225
|
+
#
|
|
226
|
+
# # ── @pre_compose: run BEFORE the LLM call ──
|
|
227
|
+
# @pre_compose(context_processor=mcp("https://mcp.example.com", "get_weather", args={"city": "NYC"}))
|
|
228
|
+
# def weather_agent(llm_response, input_data, context):
|
|
229
|
+
# return {"result": llm_response}
|
|
230
|
+
#
|
|
231
|
+
# # ── @chain: run AFTER the agent completes ──
|
|
232
|
+
# @chain(zapier("send_slack_message", connection="slack"))
|
|
233
|
+
# async def notify_agent(llm_response, input_data, context):
|
|
234
|
+
# return {"message": llm_response.get("summary")}
|
|
235
|
+
#
|
|
236
|
+
# # ── @conditional_chain: run conditionally after the agent ──
|
|
237
|
+
# @conditional_chain(lambda r, ctx: r.get("needs_enrichment"), composio("GITHUB_STAR_REPO"))
|
|
238
|
+
# async def enricher(llm_response, input_data, context):
|
|
239
|
+
# return {"needs_enrichment": True, "data": llm_response}
|
|
240
|
+
#
|
|
241
|
+
# # ── n8n webhook as a chain step ──
|
|
242
|
+
# @chain(n8n("https://my-n8n.example.com/webhook/notify"))
|
|
243
|
+
# async def alert_agent(llm_response, input_data, context):
|
|
244
|
+
# return {"event": "completed", "data": llm_response}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ===================================================================
|
|
248
|
+
# 10. CUSTOM CONNECTOR — Build your own integration
|
|
249
|
+
# ===================================================================
|
|
250
|
+
#
|
|
251
|
+
# from leafmesh.external.connectors.base_connector import ExternalConnector
|
|
252
|
+
#
|
|
253
|
+
# class MyCustomConnector(ExternalConnector):
|
|
254
|
+
# """Custom connector for your proprietary system."""
|
|
255
|
+
#
|
|
256
|
+
# async def connect(self):
|
|
257
|
+
# """Initialize connection to the external system."""
|
|
258
|
+
# self.client = await create_my_client(self.config)
|
|
259
|
+
#
|
|
260
|
+
# async def execute(self, input_data: dict) -> dict:
|
|
261
|
+
# """Send data to the external system and return results."""
|
|
262
|
+
# response = await self.client.run(input_data)
|
|
263
|
+
# return {"result": response}
|
|
264
|
+
#
|
|
265
|
+
# async def disconnect(self):
|
|
266
|
+
# """Clean up resources."""
|
|
267
|
+
# await self.client.close()
|
|
268
|
+
#
|
|
269
|
+
# # Register in connector_registry:
|
|
270
|
+
# from leafmesh.external.connectors.connector_registry import ConnectorRegistry
|
|
271
|
+
# ConnectorRegistry.register("my_system", MyCustomConnector)
|
|
272
|
+
#
|
|
273
|
+
# # Then use in YAML:
|
|
274
|
+
# # my_agent:
|
|
275
|
+
# # agent_type: "external"
|
|
276
|
+
# # framework: "my_system"
|
|
277
|
+
# # connector_config:
|
|
278
|
+
# # endpoint: "http://my-system:8080"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Programmatic Agent — Fast template-based research fallback (no LLM).
|
|
2
|
+
|
|
3
|
+
auto_discover matches function name 'fallback_researcher_agent' to YAML agent config.
|
|
4
|
+
|
|
5
|
+
Features showcased:
|
|
6
|
+
- Programmatic agent: pure Python, no LLM call, instant response
|
|
7
|
+
- OR fan-in: advisor_agent uses wait_for: "processor_agent AND (researcher_agent OR fallback_researcher_agent)"
|
|
8
|
+
- Race pattern: this agent completes instantly while researcher_agent waits for LLM
|
|
9
|
+
- Mirrors researcher_agent's output schema so advisor_agent can consume either
|
|
10
|
+
|
|
11
|
+
Execution flow:
|
|
12
|
+
1. processor_agent fans out to researcher_agent AND fallback_researcher_agent
|
|
13
|
+
2. This agent returns immediately with template-based findings
|
|
14
|
+
3. researcher_agent is still waiting for LLM response
|
|
15
|
+
4. advisor_agent's OR expression resolves — it proceeds with fallback data
|
|
16
|
+
5. researcher_agent result arrives later (already resolved, ignored by fan-in)
|
|
17
|
+
"""
|
|
18
|
+
from leafmesh import chain, LeafMeshLogger
|
|
19
|
+
|
|
20
|
+
logger = LeafMeshLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def enrich_findings(result, context):
|
|
24
|
+
"""Chain step: Add analysis metadata to template findings."""
|
|
25
|
+
findings = result.get("findings", [])
|
|
26
|
+
result["analysis"] = {
|
|
27
|
+
"total_findings": len(findings),
|
|
28
|
+
"confidence_score": 0.6,
|
|
29
|
+
"data_quality": "template",
|
|
30
|
+
"source_type": "fallback_template",
|
|
31
|
+
}
|
|
32
|
+
result["report"] = {
|
|
33
|
+
"title": "Fallback Research Findings",
|
|
34
|
+
"confidence": 0.6,
|
|
35
|
+
"quality": "template",
|
|
36
|
+
"finding_count": len(findings),
|
|
37
|
+
"formatted": True,
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@chain(enrich_findings)
|
|
43
|
+
async def fallback_researcher_agent(llm_response, input_data, context):
|
|
44
|
+
"""Instant template-based research — no LLM needed.
|
|
45
|
+
|
|
46
|
+
Provides a fast fallback so the advisor_agent can proceed immediately
|
|
47
|
+
via OR fan-in without waiting for the slower LLM-based researcher_agent.
|
|
48
|
+
"""
|
|
49
|
+
upstream_yields = input_data.get("upstream_yields", {})
|
|
50
|
+
processor_yields = upstream_yields.get("processor_agent", {})
|
|
51
|
+
|
|
52
|
+
calling_data = input_data.get("calling_agent_response", {})
|
|
53
|
+
processed_items = calling_data.get("processed_items", [])
|
|
54
|
+
|
|
55
|
+
# Build template-based findings from upstream data
|
|
56
|
+
findings = []
|
|
57
|
+
for item in processed_items:
|
|
58
|
+
findings.append({
|
|
59
|
+
"source": "template",
|
|
60
|
+
"content": item.get("item", ""),
|
|
61
|
+
"recommendation": f"Review and address: {item.get('item', 'unknown')}",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if not findings:
|
|
65
|
+
findings.append({
|
|
66
|
+
"source": "template",
|
|
67
|
+
"content": "No specific items to research",
|
|
68
|
+
"recommendation": "Gather more information from the user",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
logger.info(f"Fallback research — {len(findings)} template findings")
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"findings": findings,
|
|
75
|
+
"query": input_data.get("message", input_data.get("research_topic", "")),
|
|
76
|
+
"upstream_item_count": processor_yields.get("item_count", 0),
|
|
77
|
+
"source_agent": "fallback_researcher_agent",
|
|
78
|
+
"source_type": "fallback_template",
|
|
79
|
+
"status": "researched",
|
|
80
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""LLM Agent — Auto-discovered with @pre_compose (all 3 processor types).
|
|
2
|
+
|
|
3
|
+
auto_discover matches function name 'greeter_agent' to YAML agent config.
|
|
4
|
+
|
|
5
|
+
Execution flow:
|
|
6
|
+
1. Processors run BEFORE the LLM call (data preparation)
|
|
7
|
+
2. context_parts (care, sentiment, guardrails) shape the LLM's tone
|
|
8
|
+
3. LLM generates response using YAML prompt + prepared data
|
|
9
|
+
4. This function post-processes the LLM response
|
|
10
|
+
|
|
11
|
+
Processor results in context["prepared_data"]:
|
|
12
|
+
- context_processor → context["prepared_data"]["business_context"]
|
|
13
|
+
- input_processor → context["prepared_data"]["clean_user_input"]
|
|
14
|
+
- others_processor → context["prepared_data"]["others"]
|
|
15
|
+
"""
|
|
16
|
+
from leafmesh import pre_compose, LeafMeshLogger
|
|
17
|
+
|
|
18
|
+
logger = LeafMeshLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def process_context(input_data, context):
|
|
22
|
+
"""context_processor: Prepare business context for the LLM prompt.
|
|
23
|
+
Runs before LLM — enriches the prompt with relevant domain info."""
|
|
24
|
+
return {
|
|
25
|
+
"domain": "task_management",
|
|
26
|
+
"session_id": context.get("session_id", "unknown"),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def clean_input(input_data, context):
|
|
31
|
+
"""input_processor: Extract and clean the user's message.
|
|
32
|
+
Runs before LLM — normalizes input for consistent processing."""
|
|
33
|
+
raw = input_data.get("message", "")
|
|
34
|
+
return {"user_message": raw.strip()}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_extras(input_data, context):
|
|
38
|
+
"""others_processor: Load auxiliary data (feature flags, config, etc.).
|
|
39
|
+
Runs before LLM — provides supplementary data."""
|
|
40
|
+
return {"timestamp_requested": "message" in input_data}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pre_compose(
|
|
44
|
+
context_processor=process_context,
|
|
45
|
+
input_processor=clean_input,
|
|
46
|
+
others_processor=load_extras,
|
|
47
|
+
)
|
|
48
|
+
async def greeter_agent(llm_response, input_data, context):
|
|
49
|
+
"""Post-process the LLM's response.
|
|
50
|
+
|
|
51
|
+
At this point:
|
|
52
|
+
- Processors already ran (before LLM call)
|
|
53
|
+
- context_parts (care, sentiment, guardrails) already shaped the LLM's tone
|
|
54
|
+
- llm_response has the LLM's output
|
|
55
|
+
- context["prepared_data"] has processor results
|
|
56
|
+
"""
|
|
57
|
+
prepared = context.get("prepared_data", {})
|
|
58
|
+
|
|
59
|
+
# ─── Memory access (add memory: true to this agent's YAML to enable) ───
|
|
60
|
+
# memory_posts = context.get("memory_posts", [])
|
|
61
|
+
# Each post has "role" (user/assistant) and "content" (text).
|
|
62
|
+
# See researcher_agent.py for a working memory example.
|
|
63
|
+
|
|
64
|
+
# ─── Upstream yields access ───
|
|
65
|
+
# If this agent is called by another agent (e.g., client → greeter_agent),
|
|
66
|
+
# the SDK injects upstream yields as input_data["upstream_yields"].
|
|
67
|
+
# upstream_yields = input_data.get("upstream_yields", {})
|
|
68
|
+
# client_yields = upstream_yields.get("client", {})
|
|
69
|
+
|
|
70
|
+
user_input = input_data.get("message", "")
|
|
71
|
+
logger.info(f"Greeting user — input: {user_input[:80]}")
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"greeting": llm_response if llm_response else "Hello!",
|
|
75
|
+
"user_input": user_input,
|
|
76
|
+
"detected_sentiment": "neutral",
|
|
77
|
+
"domain": prepared.get("business_context", {}).get("domain", "general"),
|
|
78
|
+
"status": "greeted",
|
|
79
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Programmatic Agent — Pure Python, no LLM, @conditional_chain branching.
|
|
2
|
+
|
|
3
|
+
auto_discover matches function name 'processor_agent' to YAML agent config.
|
|
4
|
+
parallel: true is set in config.yaml — enables concurrent execution.
|
|
5
|
+
|
|
6
|
+
All agent functions use the 3-param signature: (llm_response, input_data, context)
|
|
7
|
+
For programmatic agents, llm_response is always None (no LLM is called).
|
|
8
|
+
|
|
9
|
+
Data flow:
|
|
10
|
+
greeter_agent returns output → SDK evaluates can_call rules →
|
|
11
|
+
SDK passes output as input_data to this agent automatically.
|
|
12
|
+
|
|
13
|
+
Upstream yields:
|
|
14
|
+
When greeter_agent stores yields (greeting, user_input, detected_sentiment),
|
|
15
|
+
the SDK injects them as input_data["upstream_yields"][calling_agent_name].
|
|
16
|
+
This gives downstream agents typed access to structured data from upstream.
|
|
17
|
+
"""
|
|
18
|
+
from leafmesh import conditional_chain, LeafMeshLogger
|
|
19
|
+
|
|
20
|
+
logger = LeafMeshLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handle_urgent(result, context):
|
|
24
|
+
"""Conditional chain: runs only when urgent items exist."""
|
|
25
|
+
urgent = [i for i in result.get("processed_items", []) if i["category"] == "urgent"]
|
|
26
|
+
result["urgent_items"] = urgent
|
|
27
|
+
result["alert"] = f"{len(urgent)} urgent item(s) flagged"
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def handle_normal(result, context):
|
|
32
|
+
"""Conditional chain: runs when no urgent items."""
|
|
33
|
+
result["alert"] = "No urgent items"
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@conditional_chain(
|
|
38
|
+
lambda result, ctx: any(i["category"] == "urgent" for i in result.get("processed_items", [])),
|
|
39
|
+
handle_urgent,
|
|
40
|
+
handle_normal,
|
|
41
|
+
)
|
|
42
|
+
async def processor_agent(llm_response, input_data, context):
|
|
43
|
+
"""Process the greeter's output into structured action items.
|
|
44
|
+
|
|
45
|
+
Pure Python processing — no LLM cost.
|
|
46
|
+
llm_response is always None for programmatic agents.
|
|
47
|
+
With parallel: true, multiple invocations run concurrently.
|
|
48
|
+
|
|
49
|
+
After this function returns, @conditional_chain runs either
|
|
50
|
+
handle_urgent or handle_normal based on the result.
|
|
51
|
+
Then SDK evaluates can_call rules to chain to the next agent.
|
|
52
|
+
"""
|
|
53
|
+
# SDK passes the calling agent's output directly as input_data.
|
|
54
|
+
# "calling_agent_response" is a virtual variable only in YAML condition evaluation.
|
|
55
|
+
|
|
56
|
+
# ─── Upstream yields access ───
|
|
57
|
+
# When greeter_agent has yields defined in YAML, the SDK auto-injects them:
|
|
58
|
+
upstream_yields = input_data.get("upstream_yields", {})
|
|
59
|
+
greeter_yields = upstream_yields.get("greeter_agent", {})
|
|
60
|
+
# greeter_yields has: greeting, user_input, detected_sentiment, status
|
|
61
|
+
detected_sentiment = greeter_yields.get("detected_sentiment", "unknown")
|
|
62
|
+
|
|
63
|
+
# In HITL flow (greeter → client → processor), input_data comes from the
|
|
64
|
+
# human agent's output — look for human_message first, then greeter yields,
|
|
65
|
+
# then direct user_input (non-HITL path: greeter → processor).
|
|
66
|
+
user_input = (
|
|
67
|
+
input_data.get("human_message", "")
|
|
68
|
+
or greeter_yields.get("user_input", "")
|
|
69
|
+
or input_data.get("user_input", "")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
raw_items = [item.strip() for item in user_input.split(",") if item.strip()]
|
|
73
|
+
|
|
74
|
+
categorized = []
|
|
75
|
+
for item in raw_items:
|
|
76
|
+
categorized.append({
|
|
77
|
+
"item": item,
|
|
78
|
+
"category": "urgent" if any(w in item.lower() for w in ["asap", "urgent", "now"]) else "normal",
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
urgent_count = sum(1 for i in categorized if i["category"] == "urgent")
|
|
82
|
+
logger.info(f"Processed {len(categorized)} items ({urgent_count} urgent)")
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"processed_items": categorized,
|
|
86
|
+
"item_count": len(categorized),
|
|
87
|
+
"source_agent": "greeter_agent",
|
|
88
|
+
"upstream_sentiment": detected_sentiment,
|
|
89
|
+
"status": "processed",
|
|
90
|
+
}
|