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,3 @@
1
+ """create-leafmesh — Project scaffolding tool for LeafMesh."""
2
+
3
+ __version__ = "2.1.0"
create_leafmesh/cli.py ADDED
@@ -0,0 +1,252 @@
1
+ """CLI entry point for create-leafmesh."""
2
+
3
+ import argparse
4
+ import re
5
+ import sys
6
+ import time
7
+
8
+ from create_leafmesh import __version__
9
+ from create_leafmesh.create import create_project
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # ANSI helpers (graceful fallback on terminals that don't support color)
13
+ # ---------------------------------------------------------------------------
14
+ _CYAN = "\033[36m"
15
+ _GREEN = "\033[32m"
16
+ _WHITE = "\033[97m"
17
+ _DIM = "\033[2m"
18
+ _BOLD = "\033[1m"
19
+ _RESET = "\033[0m"
20
+
21
+ def _c(code: str, text: str) -> str:
22
+ """Wrap text in an ANSI code, reset after."""
23
+ return f"{code}{text}{_RESET}"
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # ASCII art — block letters for LEAFMESH
28
+ # ---------------------------------------------------------------------------
29
+ BANNER = r"""
30
+ ██╗ ███████╗ █████╗ ███████╗███╗ ███╗███████╗███████╗██╗ ██╗
31
+ ██║ ██╔════╝██╔══██╗██╔════╝████╗ ████║██╔════╝██╔════╝██║ ██║
32
+ ██║ █████╗ ███████║█████╗ ██╔████╔██║█████╗ ███████╗███████║
33
+ ██║ ██╔══╝ ██╔══██║██╔══╝ ██║╚██╔╝██║██╔══╝ ╚════██║██╔══██║
34
+ ███████╗███████╗██║ ██║██║ ██║ ╚═╝ ██║███████╗███████║██║ ██║
35
+ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝
36
+ """
37
+
38
+ def print_banner(version: str) -> None:
39
+ print()
40
+ print(_c(_CYAN, BANNER), end="")
41
+ pad = " " * 2
42
+ print(f"{pad} create-leafmesh {_c(_DIM, 'v' + version)} "
43
+ f"{_c(_DIM, 'Agent mesh scaffolding tool')}")
44
+ print(f"{pad} " + "-" * 55)
45
+ print()
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Step progress printer
50
+ # ---------------------------------------------------------------------------
51
+ def step(n: int, total: int, label: str, done: bool = False) -> None:
52
+ status = _c(_GREEN, "[ OK ]") if done else _c(_DIM, "[ ]")
53
+ print(f" {status} [{n}/{total}] {label}")
54
+
55
+
56
+ def step_done(n: int, total: int, label: str) -> None:
57
+ print(f"\033[1A\r {_c(_GREEN, '[ OK ]')} [{n}/{total}] {label}")
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # File tree renderer
62
+ # ---------------------------------------------------------------------------
63
+ FILE_TREE = """\
64
+ {name}/
65
+ |-- agency/
66
+ | |-- greeter_agent.py
67
+ | |-- processor_agent.py
68
+ | |-- researcher_agent.py
69
+ | |-- advisor_agent.py
70
+ | |-- fallback_researcher_agent.py
71
+ | `-- scheduler_agent.py
72
+ |-- configs/
73
+ | `-- config.yaml
74
+ |-- main.py
75
+ |-- hitl_stub_receiver.py
76
+ |-- requirements.txt
77
+ |-- .env
78
+ |-- Dockerfile
79
+ `-- docker-compose.yml"""
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Next-steps box
84
+ # ---------------------------------------------------------------------------
85
+ def print_success_box(project_name: str, missing_keys: list[str]) -> None:
86
+ w = 58
87
+
88
+ def row(text: str = "") -> str:
89
+ return f" | {text:<{w - 4}}|"
90
+
91
+ def div() -> str:
92
+ return " +" + "-" * (w - 2) + "+"
93
+
94
+ def header(text: str) -> str:
95
+ return " | " + _c(_GREEN, _c(_BOLD, f"{text:<{w - 4}}")) + "|"
96
+
97
+ print()
98
+ print(div())
99
+ print(header(f"Project ready: {project_name}"))
100
+ print(div())
101
+ print(row())
102
+ print(row(f" cd {project_name}"))
103
+ print(row(f" pip install -r requirements.txt"))
104
+ if missing_keys:
105
+ print(row())
106
+ print(row(f" # Add to .env before starting:"))
107
+ for k in missing_keys:
108
+ print(row(f" # {k}=<your-key>"))
109
+ print(row())
110
+ print(row(f" Terminal 1: python hitl_stub_receiver.py"))
111
+ print(row(f" Terminal 2: python main.py"))
112
+ print(row())
113
+ print(row(f" Open: http://127.0.0.1:18820/docs"))
114
+ print(row())
115
+ print(div())
116
+ print(row(f" Docs: https://leafcraft.ai/docs"))
117
+ print(div())
118
+ print()
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Validation / prompts
123
+ # ---------------------------------------------------------------------------
124
+ def validate_project_name(name: str) -> str:
125
+ if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", name):
126
+ raise argparse.ArgumentTypeError(
127
+ f"Invalid project name '{name}'. "
128
+ "Must start with a letter and contain only alphanumeric characters, "
129
+ "hyphens, or underscores."
130
+ )
131
+ return name
132
+
133
+
134
+ def prompt_project_name() -> str:
135
+ while True:
136
+ name = input(" Project name: ").strip()
137
+ if not name:
138
+ print(" Name cannot be empty.")
139
+ continue
140
+ if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", name):
141
+ print(" Must start with a letter, letters/numbers/hyphens/underscores only.")
142
+ continue
143
+ return name
144
+
145
+
146
+ def prompt_env_vars() -> dict:
147
+ w = 55
148
+ print()
149
+ print(" " + "-" * w)
150
+ print(f" {'Environment setup':^{w}}")
151
+ print(f" {'(press Enter to skip any field)':^{w}}")
152
+ print(" " + "-" * w)
153
+ print()
154
+
155
+ env_vars: dict = {}
156
+
157
+ fields = [
158
+ ("LEAFMESH_LICENSE_KEY", "License key (https://leafcraft.ai): "),
159
+ ("LEAFMESH_ENV_TOKEN", "Env token (https://leafcraft.ai): "),
160
+ ("OPENAI_API_KEY", "OpenAI key (https://platform.openai.com): "),
161
+ ("ANTHROPIC_API_KEY", "Anthropic key (optional): "),
162
+ ]
163
+
164
+ for key, label in fields:
165
+ val = input(f" {label}").strip()
166
+ if val:
167
+ env_vars[key] = val
168
+
169
+ print()
170
+ return env_vars
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Main create flow
175
+ # ---------------------------------------------------------------------------
176
+ def _run_create(project_name: str | None, output_dir: str, no_git: bool) -> int:
177
+ print_banner(__version__)
178
+
179
+ if project_name is None:
180
+ project_name = prompt_project_name()
181
+ print()
182
+ else:
183
+ try:
184
+ project_name = validate_project_name(project_name)
185
+ except argparse.ArgumentTypeError as e:
186
+ print(f" Error: {e}")
187
+ return 1
188
+
189
+ env_vars = prompt_env_vars()
190
+
191
+ # Hand off to create_project, passing our step-printer so it can report progress
192
+ print(f" Creating {_c(_BOLD, project_name + '/')}")
193
+ print()
194
+
195
+ result = create_project(
196
+ project_name,
197
+ output_dir,
198
+ init_git=not no_git,
199
+ env_vars=env_vars,
200
+ step_fn=step,
201
+ step_done_fn=step_done,
202
+ )
203
+
204
+ if result != 0:
205
+ return result
206
+
207
+ # File tree — print line by line with dim so pipe chars aren't swallowed
208
+ print()
209
+ for line in FILE_TREE.format(name=project_name).splitlines():
210
+ print(_c(_DIM, line))
211
+
212
+ # Success box
213
+ missing = [k for k in ("LEAFMESH_LICENSE_KEY", "OPENAI_API_KEY") if k not in env_vars]
214
+ print_success_box(project_name, missing)
215
+
216
+ return 0
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # CLI entry point
221
+ # ---------------------------------------------------------------------------
222
+ def main(argv: list[str] | None = None) -> int:
223
+ parser = argparse.ArgumentParser(
224
+ prog="create-leafmesh",
225
+ description="Project scaffolding tool for LeafMesh",
226
+ epilog="""
227
+ examples:
228
+ create-leafmesh my-project Create a new project
229
+ create-leafmesh my-project -o /tmp Create in a specific directory
230
+ create-leafmesh Interactive mode
231
+ """,
232
+ formatter_class=argparse.RawDescriptionHelpFormatter,
233
+ )
234
+ parser.add_argument("--version", action="version", version=f"create-leafmesh {__version__}")
235
+ parser.add_argument("project_name", nargs="?", default=None, help="Project name")
236
+ parser.add_argument("-o", "--output-dir", default=".", help="Output directory (default: current)")
237
+ parser.add_argument("--no-git", action="store_true", help="Skip git init")
238
+
239
+ args = parser.parse_args(argv)
240
+ try:
241
+ return _run_create(args.project_name, args.output_dir, args.no_git)
242
+ except KeyboardInterrupt:
243
+ print("\n\n Cancelled.\n")
244
+ return 0
245
+
246
+
247
+ if __name__ == "__main__":
248
+ try:
249
+ sys.exit(main())
250
+ except KeyboardInterrupt:
251
+ print("\n\n Cancelled.\n")
252
+ sys.exit(0)
@@ -0,0 +1,106 @@
1
+ """Project scaffolding logic."""
2
+
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Callable
8
+
9
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
10
+
11
+ RENAME_MAP = {
12
+ "env": ".env",
13
+ "gitignore": ".gitignore",
14
+ "dockerignore": ".dockerignore",
15
+ }
16
+
17
+ SUBSTITUTE_EXTENSIONS = {".py", ".yaml", ".yml", ".txt", ".md", ".env", ""}
18
+
19
+ ENV_PLACEHOLDERS = {
20
+ "LEAFMESH_LICENSE_KEY": "your-license-key-here",
21
+ "LEAFMESH_ENV_TOKEN": "your-env-token-here",
22
+ "OPENAI_API_KEY": "your-key-here",
23
+ "ANTHROPIC_API_KEY": "your-anthropic-key",
24
+ }
25
+
26
+ # Step labels — order matches execution
27
+ _STEPS = [
28
+ "Copying template files",
29
+ "Writing .env",
30
+ "Setting up git repository",
31
+ "Installing Claude Code skill",
32
+ ]
33
+ _TOTAL = len(_STEPS)
34
+
35
+
36
+ def _noop(*args, **kwargs) -> None:
37
+ pass
38
+
39
+
40
+ def create_project(
41
+ project_name: str,
42
+ output_dir: str,
43
+ *,
44
+ init_git: bool = True,
45
+ env_vars: dict | None = None,
46
+ step_fn: Callable = _noop,
47
+ step_done_fn: Callable = _noop,
48
+ ) -> int:
49
+ target = Path(output_dir).resolve() / project_name
50
+
51
+ if target.exists():
52
+ print(f"\n Error: '{target}' already exists.\n")
53
+ return 1
54
+
55
+ # Step 1 — copy template
56
+ step_fn(1, _TOTAL, _STEPS[0])
57
+ shutil.copytree(TEMPLATES_DIR, target)
58
+ for path in target.rglob("*"):
59
+ if not path.is_file():
60
+ continue
61
+ if path.suffix not in SUBSTITUTE_EXTENSIONS:
62
+ continue
63
+ text = path.read_text(encoding="utf-8")
64
+ if "{{project_name}}" in text:
65
+ path.write_text(text.replace("{{project_name}}", project_name), encoding="utf-8")
66
+ step_done_fn(1, _TOTAL, _STEPS[0])
67
+
68
+ # Step 2 — .env
69
+ step_fn(2, _TOTAL, _STEPS[1])
70
+ if env_vars:
71
+ env_file = target / "env"
72
+ if env_file.exists():
73
+ text = env_file.read_text(encoding="utf-8")
74
+ for key, value in env_vars.items():
75
+ placeholder = ENV_PLACEHOLDERS.get(key)
76
+ if placeholder and placeholder in text:
77
+ text = text.replace(placeholder, value)
78
+ text = text.replace(f"# {key}={value}", f"{key}={value}")
79
+ elif f"# {key}=" in text:
80
+ text = re.sub(rf"# {re.escape(key)}=.*", f"{key}={value}", text)
81
+ env_file.write_text(text, encoding="utf-8")
82
+ for old_name, new_name in RENAME_MAP.items():
83
+ src = target / old_name
84
+ if src.exists():
85
+ src.rename(target / new_name)
86
+ step_done_fn(2, _TOTAL, _STEPS[1])
87
+
88
+ # Step 3 — git
89
+ step_fn(3, _TOTAL, _STEPS[2])
90
+ if init_git:
91
+ try:
92
+ subprocess.run(["git", "init", str(target)], capture_output=True, check=True)
93
+ except (subprocess.CalledProcessError, FileNotFoundError):
94
+ pass
95
+ step_done_fn(3, _TOTAL, _STEPS[2])
96
+
97
+ # Step 4 — Claude skill
98
+ step_fn(4, _TOTAL, _STEPS[3])
99
+ claude_skills_src = target / "claude_skills"
100
+ if claude_skills_src.exists():
101
+ claude_dir = target / ".claude" / "skills"
102
+ claude_dir.parent.mkdir(parents=True, exist_ok=True)
103
+ claude_skills_src.rename(claude_dir)
104
+ step_done_fn(4, _TOTAL, _STEPS[3])
105
+
106
+ return 0
@@ -0,0 +1,21 @@
1
+ FROM python:3.13-slim AS base
2
+
3
+ WORKDIR /app
4
+
5
+ # System dependencies for compiled packages (grpcio, etc.)
6
+ RUN apt-get update && \
7
+ apt-get install -y --no-install-recommends gcc libffi-dev && \
8
+ rm -rf /var/lib/apt/lists/*
9
+
10
+ # Install Python dependencies
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy project files
15
+ COPY configs/ configs/
16
+ COPY agency/ agency/
17
+ COPY main.py .
18
+
19
+ EXPOSE 18820
20
+
21
+ CMD ["python", "main.py"]
@@ -0,0 +1,309 @@
1
+ # {{project_name}}
2
+
3
+ A LeafMesh multi-agent project with Human-in-the-Loop (HITL), fan-in/fan-out patterns, smart memory, scheduled agents, and more.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # 1. Install dependencies
9
+ python -m venv .venv && source .venv/bin/activate
10
+ pip install -r requirements.txt
11
+
12
+ # 2. Configure environment
13
+ # Edit .env with your API keys (OPENAI_API_KEY, LEAFMESH_LICENSE_KEY, Redis)
14
+
15
+ # 3. Start Redis (if not already running)
16
+ docker compose up redis -d
17
+ # Or: redis-server
18
+
19
+ # 4. Start the mesh
20
+ python main.py
21
+ ```
22
+
23
+ The mesh starts an API server at **http://127.0.0.1:18820**. Visit `/docs` for interactive API docs.
24
+
25
+ ## Project Structure
26
+
27
+ ```
28
+ {{project_name}}/
29
+ configs/
30
+ config.yaml # Agent definitions, mesh wiring, HITL config
31
+ agency/
32
+ greeter_agent.py # LLM agent with @pre_compose
33
+ processor_agent.py # Programmatic agent with @conditional_chain
34
+ researcher_agent.py # LLM agent with @chain_with_results + smart memory
35
+ fallback_researcher_agent.py # Programmatic fast fallback (race pattern)
36
+ advisor_agent.py # LLM fan-in agent with @chain + @compose
37
+ scheduler_agent.py # Cron-scheduled agent (daily reports)
38
+ tools.py # Custom tools (@global_tool, @tool)
39
+ external_agents.py # Integration reference (CrewAI, LangGraph, etc.)
40
+ main.py # Entry point
41
+ hitl_stub_receiver.py # Webhook stub for testing HITL locally
42
+ requirements.txt
43
+ .env # API keys and config
44
+ Dockerfile
45
+ docker-compose.yml # Redis + app (one command)
46
+ ```
47
+
48
+ ## Agent Flow
49
+
50
+ ```
51
+ HITL Scenario 1 (system-initiated)
52
+ ===================================
53
+ API request greeter_agent (LLM)
54
+ "greet_user" ─────────> |
55
+ v
56
+ client (human agent, HITL)
57
+ [outbound webhook → human reviews → inbound webhook]
58
+ |
59
+ v
60
+ processor_agent (programmatic, parallel)
61
+ ┌──┼──┐
62
+ v v v
63
+ researcher fallback advisor (waits)
64
+ (LLM) (instant) |
65
+ └──────────────┘
66
+ advisor_agent (OR fan-in)
67
+ [processor AND (researcher OR fallback)]
68
+
69
+
70
+ HITL Scenario 2 (human-initiated)
71
+ ==================================
72
+ Webhook client (human agent)
73
+ "human_contact" ──────> | (no from_agent → route to greeter)
74
+ v
75
+ greeter_agent (LLM)
76
+ | (dual callback → client)
77
+ v
78
+ client (HITL, from_agent = "greeter_agent")
79
+ [outbound webhook → human reviews → inbound webhook]
80
+ | (from_agent == "greeter_agent" → route to processor)
81
+ v
82
+ processor_agent → researcher + fallback → advisor
83
+ ```
84
+
85
+ ## HITL Walkthrough
86
+
87
+ ### Prerequisites
88
+
89
+ You need **3 terminals** open:
90
+
91
+ | Terminal | Purpose |
92
+ |----------|---------|
93
+ | Terminal 1 | HITL stub receiver (captures outbound webhooks) |
94
+ | Terminal 2 | LeafMesh server |
95
+ | Terminal 3 | curl commands (trigger mesh + respond as human) |
96
+
97
+ ### Step 1: Start the Stub Receiver
98
+
99
+ ```bash
100
+ # Terminal 1
101
+ python hitl_stub_receiver.py
102
+ ```
103
+
104
+ This listens on port 9999 and prints outbound webhook payloads when the human agent is invoked.
105
+
106
+ ### Step 2: Start the Mesh
107
+
108
+ ```bash
109
+ # Terminal 2
110
+ python main.py
111
+ ```
112
+
113
+ Wait for "{{project_name}} is running" in the logs.
114
+
115
+ ### Step 3: Get Your Webhook Signing Secret
116
+
117
+ ```bash
118
+ # Terminal 3
119
+ curl http://127.0.0.1:18820/api/webhook/secret
120
+ ```
121
+
122
+ Save the `secret` value — you'll need it to sign inbound webhook responses.
123
+
124
+ ### Scenario 1: System-Initiated HITL
125
+
126
+ The system triggers a workflow, and the human agent is called mid-flow for review.
127
+
128
+ **Trigger the mesh:**
129
+
130
+ ```bash
131
+ # Terminal 3
132
+ curl -X POST http://127.0.0.1:18820/api/mesh/request \
133
+ -H "Content-Type: application/json" \
134
+ -d '{"entry_point": "greet_user", "data": {"message": "I need help with item1, item2, item3"}}'
135
+ ```
136
+
137
+ **What happens:**
138
+ 1. `greeter_agent` processes the message via LLM
139
+ 2. `greeter_agent` routes to `client` (human agent)
140
+ 3. SDK sends outbound webhook to the stub receiver (Terminal 1)
141
+ 4. The stub prints a curl command to respond
142
+
143
+ **Respond as the human** (compute HMAC + timestamp + nonce — required by the SDK):
144
+
145
+ ```bash
146
+ # Terminal 3 — sign and respond
147
+ # Replace SESSION_ID with the session_id from the stub output
148
+ # Replace SECRET with the value from /api/webhook/secret
149
+
150
+ BODY='{"session_id": "SESSION_ID", "decision": "approved", "message": "Looks good, proceed with item1, item2, item3"}'
151
+ SECRET="your-secret-here"
152
+ TS=$(date +%s)
153
+ NONCE=$(python3 -c 'import secrets; print(secrets.token_urlsafe(16))')
154
+
155
+ # Signed material: "{timestamp}.{nonce}.{body}"
156
+ SIG=$(printf '%s' "$TS.$NONCE.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
157
+
158
+ curl -X POST http://127.0.0.1:18820/webhook/greet_user \
159
+ -H "Content-Type: application/json" \
160
+ -H "X-LeafMesh-Signature: sha256=$SIG" \
161
+ -H "X-LeafMesh-Timestamp: $TS" \
162
+ -H "X-LeafMesh-Nonce: $NONCE" \
163
+ -d "$BODY"
164
+ ```
165
+
166
+ **Result:** The human's response flows through `processor_agent` → `researcher_agent` + `fallback_researcher_agent` → `advisor_agent`.
167
+
168
+ ### Scenario 2: Human-Initiated HITL
169
+
170
+ The human initiates contact, the system processes it, and the human reviews before final processing.
171
+
172
+ **Trigger via webhook:**
173
+
174
+ ```bash
175
+ # Terminal 3
176
+ BODY='{"message": "I want to report an issue with item1, item2"}'
177
+ SECRET="your-secret-here"
178
+ TS=$(date +%s)
179
+ NONCE=$(python3 -c 'import secrets; print(secrets.token_urlsafe(16))')
180
+ SIG=$(printf '%s' "$TS.$NONCE.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
181
+
182
+ curl -X POST http://127.0.0.1:18820/webhook/human_contact \
183
+ -H "Content-Type: application/json" \
184
+ -H "X-LeafMesh-Signature: sha256=$SIG" \
185
+ -H "X-LeafMesh-Timestamp: $TS" \
186
+ -H "X-LeafMesh-Nonce: $NONCE" \
187
+ -d "$BODY"
188
+ ```
189
+
190
+ **What happens:**
191
+ 1. `client` receives the message (no `from_agent` → routes to `greeter_agent`)
192
+ 2. `greeter_agent` processes via LLM and responds back to `client`
193
+ 3. SDK sends outbound webhook to the stub receiver (Terminal 1)
194
+ 4. The stub prints a curl command to respond
195
+
196
+ **Respond as the human** (same pattern as Scenario 1 — use the session_id from the stub output).
197
+
198
+ **Result:** `from_agent == "greeter_agent"` → routes to `processor_agent` → full chain completes.
199
+
200
+ ### How `from_agent` Routing Works
201
+
202
+ The `client` agent's `can_call` uses conditions to determine where to route:
203
+
204
+ ```yaml
205
+ can_call:
206
+ - agent: "greeter_agent"
207
+ condition: "not calling_agent_response.from_agent" # No caller → greet first
208
+ - agent: "processor_agent"
209
+ condition: "calling_agent_response.from_agent == 'greeter_agent'" # Greeter done → process
210
+ ```
211
+
212
+ When an agent calls the human agent, the SDK stores which agent called (`called_by`) in Redis. When the human responds via webhook, the SDK includes `from_agent` in the output data so `can_call` conditions can route accordingly.
213
+
214
+ ## Key Concepts
215
+
216
+ ### Agent Types
217
+
218
+ | Type | Description | Example |
219
+ |------|-------------|---------|
220
+ | `human` | Human-in-the-loop via webhook/channel | `client` |
221
+ | `llm` | LLM-powered with prompt + tools | `greeter_agent`, `researcher_agent` |
222
+ | `programmatic` | Pure Python logic, no LLM | `processor_agent`, `scheduler_agent` |
223
+ | `external` | Delegates to CrewAI, LangGraph, etc. | See `external_agents.py` |
224
+
225
+ ### Decorators
226
+
227
+ | Decorator | Purpose | Example Agent |
228
+ |-----------|---------|---------------|
229
+ | `@pre_compose(fn1, fn2, ...)` | Pre-process input before LLM call | `greeter_agent` |
230
+ | `@chain(fn1, fn2, ...)` | Sequential post-processing pipeline | `advisor_agent` |
231
+ | `@chain_with_results(fn1, fn2)` | Accumulate results across steps | `researcher_agent` |
232
+ | `@conditional_chain(cond, true_fn, false_fn)` | Branch based on condition | `processor_agent` |
233
+ | `@compose(key=fn, ...)` | Inject shaped data per downstream agent | `advisor_agent` |
234
+
235
+ ### Fan-In Patterns (`wait_for`)
236
+
237
+ ```yaml
238
+ wait_for: "A AND B" # Wait for both
239
+ wait_for: "A OR B" # First wins
240
+ wait_for: "A AND B?" # A required, B optional
241
+ wait_for: "A AND (B OR C)" # A required + first of B or C
242
+ ```
243
+
244
+ ### Smart Memory
245
+
246
+ ```yaml
247
+ memory:
248
+ strategy: "hybrid" # recency | relevance | hybrid
249
+ limit: 10 # Max entries per invocation
250
+ cross_session: true # Persist across sessions
251
+ relevance_weight: 0.6
252
+ recency_weight: 0.4
253
+ ```
254
+
255
+ ### Upstream Yields
256
+
257
+ Agents define `yields` in YAML. The SDK auto-stores them in Redis and injects them into downstream agents as `input_data["upstream_yields"][agent_name]`.
258
+
259
+ ### Scheduled Agents
260
+
261
+ ```yaml
262
+ scheduler_agent:
263
+ wake_up: "0 9 * * *" # Cron expression (9 AM UTC daily)
264
+ ```
265
+
266
+ Also triggerable on-demand via `mesh_call("scheduled_report", data)`.
267
+
268
+ ## API Endpoints
269
+
270
+ | Method | Path | Description |
271
+ |--------|------|-------------|
272
+ | POST | `/api/mesh/request` | Trigger agent workflows via entry points |
273
+ | POST | `/api/mesh/stream` | SSE stream of LLM response |
274
+ | POST | `/webhook/{entry_point}` | Webhook (new task or human HITL response) |
275
+ | GET | `/api/mesh/entry_points` | List configured entry points |
276
+ | GET | `/api/agents/` | List all agents |
277
+ | GET | `/api/sessions/` | List active sessions |
278
+ | GET | `/api/sessions/{id}/history` | Conversation history |
279
+ | GET | `/api/webhook/secret` | HMAC signing secret |
280
+ | GET | `/health` | Health check |
281
+ | GET | `/docs` | Interactive API docs (ReDoc) |
282
+
283
+ ## Docker
284
+
285
+ ```bash
286
+ # Start Redis + app
287
+ docker compose up --build
288
+
289
+ # Or just Redis (run app locally)
290
+ docker compose up redis -d
291
+ ```
292
+
293
+ ## Configuration
294
+
295
+ All agent wiring is in `configs/config.yaml`. Key sections:
296
+
297
+ - **`entry_points`** — Named portals into the mesh
298
+ - **`agents`** — Agent definitions (type, model, can_call, yields, etc.)
299
+ - **`manager`** — Built-in coordinator with learning-based routing and escalation
300
+ - **`mesh`** — Timeout and cloud LLM provider configs
301
+
302
+ ## Observability
303
+
304
+ Observability auto-enables when `LEAFMESH_LICENSE_KEY` is set in `.env`. Traces, metrics, and logs flow automatically. Set `LEAFMESH_ENV_TOKEN` to group telemetry by environment.
305
+
306
+ ## Links
307
+
308
+ - [LeafMesh SDK](https://pypi.org/project/leafmesh/)
309
+ - [LeafCraft Studios](https://leafcraft.ai)
File without changes