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.
Files changed (31) hide show
  1. create_leafmesh/__init__.py +3 -0
  2. create_leafmesh/cli.py +252 -0
  3. create_leafmesh/create.py +106 -0
  4. create_leafmesh/templates/Dockerfile +21 -0
  5. create_leafmesh/templates/README.md +309 -0
  6. create_leafmesh/templates/agency/__init__.py +0 -0
  7. create_leafmesh/templates/agency/advisor_agent.py +151 -0
  8. create_leafmesh/templates/agency/external_agents.py +278 -0
  9. create_leafmesh/templates/agency/fallback_researcher_agent.py +80 -0
  10. create_leafmesh/templates/agency/greeter_agent.py +79 -0
  11. create_leafmesh/templates/agency/processor_agent.py +90 -0
  12. create_leafmesh/templates/agency/researcher_agent.py +99 -0
  13. create_leafmesh/templates/agency/scheduler_agent.py +67 -0
  14. create_leafmesh/templates/agency/tools.py +123 -0
  15. create_leafmesh/templates/claude_skills/leafmesh/SKILL.md +2049 -0
  16. create_leafmesh/templates/claude_skills/leafmesh/agent-config-fields.md +1309 -0
  17. create_leafmesh/templates/claude_skills/leafmesh/examples.md +537 -0
  18. create_leafmesh/templates/claude_skills/leafmesh/reference.md +492 -0
  19. create_leafmesh/templates/configs/config.yaml +1028 -0
  20. create_leafmesh/templates/docker-compose.yml +28 -0
  21. create_leafmesh/templates/dockerignore +17 -0
  22. create_leafmesh/templates/env +109 -0
  23. create_leafmesh/templates/gitignore +33 -0
  24. create_leafmesh/templates/hitl_stub_receiver.py +149 -0
  25. create_leafmesh/templates/main.py +105 -0
  26. create_leafmesh/templates/requirements.txt +10 -0
  27. create_leafmesh-2.1.0.dist-info/METADATA +6 -0
  28. create_leafmesh-2.1.0.dist-info/RECORD +31 -0
  29. create_leafmesh-2.1.0.dist-info/WHEEL +5 -0
  30. create_leafmesh-2.1.0.dist-info/entry_points.txt +2 -0
  31. 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
+ }