Initial commit: hermes-swarm skill
This commit is contained in:
commit
30e3d05bec
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.DS_Store
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
swarm_outputs/
|
||||
*.log
|
||||
.idea/
|
||||
.vscode/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Hermes Swarm contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
60
README.md
Normal file
60
README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# hermes-swarm
|
||||
|
||||
Multi-agent discussion swarm for Hermes Agent. Replace one expensive reasoning model with several cheap models that argue, refine, and converge.
|
||||
|
||||
## What it does
|
||||
|
||||
- Runs a round-robin discussion between Hermes profiles via Redis Pub/Sub.
|
||||
- Each agent sees the full transcript before replying.
|
||||
- First agent synthesizes the final answer.
|
||||
- Saves transcript and final answer as Markdown.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
git clone https://forgejo.redtask.ru/youruser/hermes-swarm.git
|
||||
cd hermes-swarm
|
||||
```
|
||||
|
||||
Install dependency:
|
||||
|
||||
```bash
|
||||
pip install redis
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Start Redis:
|
||||
|
||||
```bash
|
||||
docker compose -f templates/docker-compose.redis.yml up -d
|
||||
```
|
||||
|
||||
2. Add Hermes profiles (see `templates/hermes-profiles.yaml`).
|
||||
|
||||
3. Run:
|
||||
|
||||
```bash
|
||||
python3 scripts/swarm_chat.py \
|
||||
--topic "Refactor this function to use async SQLAlchemy" \
|
||||
--agents swarm-architect swarm-critic swarm-coder \
|
||||
--rounds 3
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default |
|
||||
|----------|---------|
|
||||
| `REDIS_URL` | `redis://localhost:***@dataclass | `SWARM_OUTPUT_DIR` | `./swarm_outputs` |
|
||||
| `HERMES_WORKDIR` | `.` |
|
||||
| `HERMES_CMD` | `hermes` |
|
||||
|
||||
## Example use cases
|
||||
|
||||
- **Code review**: feed a diff, let critic + reviewer + coder discuss.
|
||||
- **Design decisions**: compare two approaches with architect + critic.
|
||||
- **Test planning**: let tester + coder + architect write a test strategy.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
160
SKILL.md
Normal file
160
SKILL.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
---
|
||||
name: hermes-swarm
|
||||
description: "Run a multi-agent discussion swarm using cheap models via Hermes CLI and Redis Pub/Sub."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [swarm, multi-agent, delegation, redis, collaboration, reasoning, cheap-models]
|
||||
related_skills: [subagent-driven-development, writing-plans, plan]
|
||||
---
|
||||
|
||||
# hermes-swarm
|
||||
|
||||
Use this skill when you want **several cheap models to discuss a problem and converge on a solution** instead of paying for one expensive reasoning model.
|
||||
|
||||
## When to use
|
||||
|
||||
- The task benefits from multiple perspectives (architect, critic, coder, tester).
|
||||
- You have access to several small/cheap models via Hermes profiles.
|
||||
- You want a reusable, self-hosted collaboration mechanism with no cloud orchestrator.
|
||||
- You can run Redis locally or in Docker.
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- One strong model is cheaper than 3-4 small calls (always benchmark cost).
|
||||
- The task is trivial or one-shot.
|
||||
- You need real-time synchronous chat between agents — this is round-robin, not live chat.
|
||||
|
||||
## What you get
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Redis Pub/Sub room │
|
||||
│ myproject:swarm:<random> │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
▼ ▼ ▼
|
||||
swarm- swarm- swarm-
|
||||
architect critic coder
|
||||
│ │ │
|
||||
└───────────┴───────────┘
|
||||
│
|
||||
[synthesizer] → final.md + transcript.md
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Hermes Agent** installed and `hermes` in `$PATH`.
|
||||
2. **Redis** running locally or reachable via `REDIS_URL`.
|
||||
3. At least **two Hermes profiles** configured with different models/roles.
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1. Install Redis (Docker)
|
||||
|
||||
```bash
|
||||
docker run -d --name redis-swarm \
|
||||
-p 6379:6379 \
|
||||
redis:7-alpine
|
||||
```
|
||||
|
||||
Or use the included `templates/docker-compose.redis.yml`.
|
||||
|
||||
### 2. Create Hermes profiles
|
||||
|
||||
Add a block like this to `~/.hermes/config.yaml` for each agent role:
|
||||
|
||||
```yaml
|
||||
profiles:
|
||||
swarm-architect:
|
||||
provider: openrouter # or any provider you use
|
||||
model: google/gemma-3-12b-it:cheap
|
||||
system_prompt: |
|
||||
You are the architect in a multi-agent engineering discussion.
|
||||
Look at the big picture, propose structure and design trade-offs.
|
||||
swarm-critic:
|
||||
provider: openrouter
|
||||
model: deepseek/deepseek-v3:free
|
||||
system_prompt: |
|
||||
You are the critic. Challenge assumptions, find flaws and risks.
|
||||
swarm-coder:
|
||||
provider: openrouter
|
||||
model: qwen/qwen-2.5-coder-32b-instruct
|
||||
system_prompt: |
|
||||
You are the coder. Turn ideas into concrete, working code.
|
||||
```
|
||||
|
||||
> Tip: keep these profiles cheap. The whole point is to replace one expensive call with several cheap ones.
|
||||
|
||||
### 3. Run the swarm
|
||||
|
||||
```bash
|
||||
cd /path/to/hermes-swarm
|
||||
python3 scripts/swarm_chat.py \
|
||||
--topic "Design a Python LRU cache for HTTP responses" \
|
||||
--agents swarm-architect swarm-critic swarm-coder \
|
||||
--rounds 3 \
|
||||
--room myproject:swarm:lru_cache
|
||||
```
|
||||
|
||||
Output files land in `./swarm_outputs/` by default:
|
||||
|
||||
- `<room>_final.md` — synthesized answer
|
||||
- `<room>_transcript.md` — full discussion
|
||||
|
||||
### 4. Tune environment variables
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|----------|---------|---------|
|
||||
| `REDIS_URL` | `redis://localhost:***@dataclass | `SWARM_OUTPUT_DIR` | `./swarm_outputs` | Where transcripts/final answers are saved |
|
||||
| `HERMES_WORKDIR` | current directory | Working dir passed to each Hermes agent |
|
||||
| `HERMES_CMD` | `hermes` | Hermes CLI binary |
|
||||
|
||||
## Architecture
|
||||
|
||||
The dispatcher runs **synchronously** in rounds:
|
||||
|
||||
1. **Seed** the Redis room with the topic and agent list.
|
||||
2. For each round, ask every agent to read the full history and reply.
|
||||
3. After the last round, ask the first agent (synthesizer) to write the final answer.
|
||||
4. Persist final answer and transcript to disk.
|
||||
|
||||
All messages are stored in a Redis sorted set (`room:history`), so agents can read the full context even if they are restarted.
|
||||
|
||||
## Extending roles
|
||||
|
||||
Edit `scripts/swarm_chat.py` or pass custom role labels with `--roles`. Built-in roles:
|
||||
|
||||
- `architect` — structure and trade-offs
|
||||
- `critic` — flaws, edge cases, risks
|
||||
- `coder` — concrete code
|
||||
- `tester` — tests and verification
|
||||
- `reviewer` — clarity and completeness
|
||||
- `planner` — actionable steps
|
||||
|
||||
## Cost tips
|
||||
|
||||
- Start with `--rounds 2` and 2-3 agents to measure token usage.
|
||||
- Use the cheapest models that understand your domain.
|
||||
- Compare cost vs. a single strong model on the same task.
|
||||
- Set `SWARM_OUTPUT_DIR` to your project folder so outputs become project artifacts.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
hermes-swarm/
|
||||
├── SKILL.md # this file
|
||||
├── README.md # installation guide
|
||||
├── scripts/
|
||||
│ └── swarm_chat.py # dispatcher
|
||||
├── templates/
|
||||
│ ├── docker-compose.redis.yml # Redis setup
|
||||
│ └── hermes-profiles.yaml # example profile config
|
||||
└── examples/
|
||||
├── code-review.sh # review a PR diff
|
||||
└── design-decision.sh # compare two approaches
|
||||
```
|
||||
26
examples/code-review.sh
Executable file
26
examples/code-review.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
# Example: use the swarm to review a PR diff.
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
TOPIC=$(cat <<'EOF'
|
||||
Review the following PR diff for correctness, performance, and style.
|
||||
Only discuss the diff; do not invent unrelated improvements.
|
||||
|
||||
EOF
|
||||
)
|
||||
|
||||
# Append the actual diff, e.g. from stdin or a file
|
||||
if [ -p /dev/stdin ]; then
|
||||
TOPIC="${TOPIC}$(cat)"
|
||||
else
|
||||
echo "Usage: cat diff.patch | ./examples/code-review.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 scripts/swarm_chat.py \
|
||||
--topic "$TOPIC" \
|
||||
--agents swarm-critic swarm-reviewer swarm-coder \
|
||||
--roles critic reviewer coder \
|
||||
--rounds 2 \
|
||||
--room project:swarm:code_review
|
||||
11
examples/design-decision.sh
Executable file
11
examples/design-decision.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
# Example: use the swarm to decide between two approaches.
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
python3 scripts/swarm_chat.py \
|
||||
--topic "Should we use SQLAlchemy 2.0 style or 1.x style in our FastAPI project? Compare maintainability, async support, and migration cost." \
|
||||
--agents swarm-architect swarm-critic swarm-coder \
|
||||
--roles architect critic coder \
|
||||
--rounds 3 \
|
||||
--room project:swarm:design_decision
|
||||
360
scripts/swarm_chat.py
Normal file
360
scripts/swarm_chat.py
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hermes Swarm Chat — reusable multi-agent discussion over Redis Pub/Sub.
|
||||
|
||||
Usage:
|
||||
python3 swarm_chat.py \
|
||||
--topic "Design a Python function that caches HTTP responses" \
|
||||
--agents architect critic coder \
|
||||
--rounds 3 \
|
||||
--room myproject:swarm:demo
|
||||
|
||||
Each agent is a separate Hermes profile. The dispatcher:
|
||||
1. seeds a Redis room with the topic,
|
||||
2. asks every agent in turn to read room history and respond,
|
||||
3. limits discussion to N rounds,
|
||||
4. asks the first agent (synthesizer) to write the final answer.
|
||||
|
||||
Environment:
|
||||
REDIS_URL Redis URI (default: redis://localhost:6379/0)
|
||||
HERMES_CMD Hermes binary (default: hermes)
|
||||
HERMES_WORKDIR Working directory passed to hermes chat (default: current dir)
|
||||
SWARM_OUTPUT_DIR Where to save transcripts/final answer (default: ./swarm_outputs)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import redis
|
||||
|
||||
|
||||
DEFAULT_ROOM_PREFIX = "hermes:swarm"
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||
HERMES_CMD = os.getenv("HERMES_CMD", "hermes")
|
||||
HERMES_WORKDIR = os.getenv("HERMES_WORKDIR", os.getcwd())
|
||||
OUTPUT_DIR = Path(os.getenv("SWARM_OUTPUT_DIR", "./swarm_outputs")).resolve()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SwarmMessage:
|
||||
id: str
|
||||
round: int
|
||||
agent: str
|
||||
role: str
|
||||
content: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
reply_to: Optional[str] = None
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps({
|
||||
"id": self.id,
|
||||
"round": self.round,
|
||||
"agent": self.agent,
|
||||
"role": self.role,
|
||||
"content": self.content,
|
||||
"timestamp": self.timestamp,
|
||||
"reply_to": self.reply_to,
|
||||
}, ensure_ascii=False, default=str)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "SwarmMessage":
|
||||
return cls(
|
||||
id=d["id"],
|
||||
round=d["round"],
|
||||
agent=d["agent"],
|
||||
role=d["role"],
|
||||
content=d["content"],
|
||||
timestamp=d.get("timestamp", time.time()),
|
||||
reply_to=d.get("reply_to"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentDef:
|
||||
name: str # Hermes profile name, e.g. swarm-architect
|
||||
role: str # human-readable role, e.g. architect
|
||||
prompt: str # system role instruction
|
||||
model: str = "" # informational only
|
||||
|
||||
|
||||
class RedisRoom:
|
||||
def __init__(self, room: str, redis_url: str = REDIS_URL):
|
||||
self.room = room
|
||||
self.control_key = f"{room}:control"
|
||||
self.r = redis.from_url(redis_url, decode_responses=True)
|
||||
self.pubsub = self.r.pubsub()
|
||||
self.pubsub.subscribe(room)
|
||||
|
||||
def post(self, msg: SwarmMessage) -> None:
|
||||
self.r.publish(self.room, msg.to_json())
|
||||
# Also keep durable history sorted by timestamp in a Redis sorted set.
|
||||
self.r.zadd(f"{self.room}:history", {msg.to_json(): msg.timestamp})
|
||||
|
||||
def history(self, since: float = 0) -> List[SwarmMessage]:
|
||||
items = self.r.zrangebyscore(f"{self.room}:history", since, "+inf")
|
||||
messages = []
|
||||
for raw in items:
|
||||
try:
|
||||
messages.append(SwarmMessage.from_dict(json.loads(raw)))
|
||||
except Exception:
|
||||
continue
|
||||
return messages
|
||||
|
||||
def clear(self) -> None:
|
||||
self.r.delete(f"{self.room}:history")
|
||||
self.r.delete(self.control_key)
|
||||
|
||||
def signal_stop(self, reason: str = "rounds_exhausted") -> None:
|
||||
self.r.set(self.control_key, json.dumps({"stop": True, "reason": reason}))
|
||||
|
||||
def should_stop(self) -> bool:
|
||||
raw = self.r.get(self.control_key)
|
||||
if isinstance(raw, str) and raw:
|
||||
try:
|
||||
return json.loads(raw).get("stop", False)
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class HermesAgentRunner:
|
||||
"""Runs a single Hermes agent via CLI and captures the text reply."""
|
||||
|
||||
def __init__(self, profile: str, timeout: int = 120, workdir: str = HERMES_WORKDIR, hermes_cmd: str = HERMES_CMD):
|
||||
self.profile = profile
|
||||
self.timeout = timeout
|
||||
self.workdir = workdir
|
||||
self.hermes_cmd = hermes_cmd
|
||||
|
||||
def ask(self, prompt: str) -> str:
|
||||
cmd = [
|
||||
self.hermes_cmd, "-p", self.profile, "chat",
|
||||
"-q", prompt,
|
||||
"--source", "swarm",
|
||||
"-Q", # quiet: suppress banner/spinner
|
||||
]
|
||||
env = os.environ.copy()
|
||||
# Force working directory so agents see consistent paths.
|
||||
env["HERMES_CWD"] = self.workdir
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=self.workdir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
# Strip ANSI escape codes.
|
||||
output = re.sub(r"\x1b\[[0-9;]*m", "", output)
|
||||
return self._extract_answer(output)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"[timeout: agent {self.profile} did not respond within {self.timeout}s]"
|
||||
except Exception as e:
|
||||
return f"[error running agent {self.profile}: {e}]"
|
||||
|
||||
@staticmethod
|
||||
def _extract_answer(output: str) -> str:
|
||||
lines = output.splitlines()
|
||||
drop_prefixes = (
|
||||
"Thinking", "Using model", "▔", "─", "┌", "└", "│",
|
||||
"You:", "Hermes:", "◆", "⚕", "Running", "Tool result",
|
||||
)
|
||||
cleaned = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
stripped = re.sub(r"\x1b\[[0-9;]*m", "", stripped)
|
||||
if any(stripped.startswith(p) for p in drop_prefixes):
|
||||
continue
|
||||
if "session_id:" in stripped or "⚠ tirith" in stripped:
|
||||
continue
|
||||
cleaned.append(line)
|
||||
while cleaned and not cleaned[-1].strip():
|
||||
cleaned.pop()
|
||||
text = "\n".join(cleaned).strip()
|
||||
if not text:
|
||||
text = re.sub(r"\x1b\[[0-9;]*m", "", output).strip()
|
||||
return text
|
||||
|
||||
|
||||
class SwarmChat:
|
||||
def __init__(self, room: RedisRoom, agents: List[AgentDef], topic: str, rounds: int = 3):
|
||||
self.room = room
|
||||
self.agents = agents
|
||||
self.topic = topic
|
||||
self.rounds = rounds
|
||||
self.runner = HermesAgentRunner(profile="") # profile set per turn
|
||||
|
||||
def _build_prompt(self, agent: AgentDef, round_no: int, history: List[SwarmMessage]) -> str:
|
||||
transcript = []
|
||||
for m in history:
|
||||
transcript.append(f"[Round {m.round} | {m.agent} ({m.role})]\n{m.content}\n")
|
||||
transcript_text = "\n".join(transcript) if transcript else "(no messages yet)"
|
||||
|
||||
return (
|
||||
f"You are participating in a multi-agent engineering discussion.\n\n"
|
||||
f"YOUR ROLE: {agent.role}\n"
|
||||
f"{agent.prompt}\n\n"
|
||||
f"DISCUSSION TOPIC:\n{self.topic}\n\n"
|
||||
f"CURRENT ROUND: {round_no} of {self.rounds}\n\n"
|
||||
f"PREVIOUS MESSAGES IN THIS ROOM:\n{transcript_text}\n\n"
|
||||
f"TASK:\n"
|
||||
f"Read the previous messages, then contribute your perspective as the {agent.role}. "
|
||||
f"Be concise (2-5 sentences). Do not repeat what others already said. "
|
||||
f"If you disagree, explain why and propose a better approach. "
|
||||
f"If the discussion has already converged, say so and add any final detail."
|
||||
)
|
||||
|
||||
def run(self) -> List[SwarmMessage]:
|
||||
seed = SwarmMessage(
|
||||
id=f"seed-{uuid.uuid4().hex[:8]}",
|
||||
round=0,
|
||||
agent="dispatcher",
|
||||
role="moderator",
|
||||
content=f"Discussion topic: {self.topic}. Agents: {[a.role for a in self.agents]}. Maximum rounds: {self.rounds}.",
|
||||
)
|
||||
self.room.post(seed)
|
||||
|
||||
for r in range(1, self.rounds + 1):
|
||||
if self.room.should_stop():
|
||||
print(f"[dispatcher] stop signal detected before round {r}")
|
||||
break
|
||||
print(f"\n=== Round {r}/{self.rounds} ===")
|
||||
for agent in self.agents:
|
||||
if self.room.should_stop():
|
||||
break
|
||||
history = self.room.history(since=0)
|
||||
prompt = self._build_prompt(agent, r, history)
|
||||
self.runner.profile = agent.name
|
||||
print(f"[ask {agent.name} ({agent.role})] ...", end="", flush=True)
|
||||
answer = self.runner.ask(prompt)
|
||||
print(f" ({len(answer)} chars)")
|
||||
msg = SwarmMessage(
|
||||
id=f"r{r}-{agent.role}-{uuid.uuid4().hex[:6]}",
|
||||
round=r,
|
||||
agent=agent.name,
|
||||
role=agent.role,
|
||||
content=answer,
|
||||
)
|
||||
self.room.post(msg)
|
||||
|
||||
self.room.signal_stop("rounds_exhausted")
|
||||
return self.room.history(since=0)
|
||||
|
||||
def synthesize(self) -> str:
|
||||
history = self.room.history(since=0)
|
||||
transcript = []
|
||||
for m in history:
|
||||
if m.agent == "dispatcher":
|
||||
continue
|
||||
transcript.append(f"[{m.agent} ({m.role}), round {m.round}]\n{m.content}\n")
|
||||
transcript_text = "\n".join(transcript)
|
||||
|
||||
synthesizer = self.agents[0]
|
||||
prompt = (
|
||||
f"You are the {synthesizer.role}. The multi-agent discussion below has finished.\n\n"
|
||||
f"ORIGINAL TOPIC:\n{self.topic}\n\n"
|
||||
f"FULL TRANSCRIPT:\n{transcript_text}\n\n"
|
||||
f"TASK:\n"
|
||||
f"Write a concise final answer (1-3 paragraphs) that incorporates the best ideas, resolves disagreements, "
|
||||
f"and gives the user a concrete, actionable result. Do not introduce fictional tools or APIs."
|
||||
)
|
||||
self.runner.profile = synthesizer.name
|
||||
print(f"\n[synthesize via {synthesizer.name}] ...", end="", flush=True)
|
||||
answer = self.runner.ask(prompt)
|
||||
print(f" ({len(answer)} chars)")
|
||||
return answer
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run a synchronous multi-agent discussion via Redis Pub/Sub.")
|
||||
parser.add_argument("--topic", required=True, help="The discussion topic / task.")
|
||||
parser.add_argument("--agents", nargs="+", required=True,
|
||||
help="Hermes profile names to use as agents, e.g. swarm-architect swarm-critic swarm-coder")
|
||||
parser.add_argument("--roles", nargs="+", default=None,
|
||||
help="Optional role labels matching --agents. If omitted, derived from profile names.")
|
||||
parser.add_argument("--rounds", type=int, default=3, help="Number of discussion rounds (default 3).")
|
||||
parser.add_argument("--room", default=None, help="Redis room key. Default: hermes:swarm:<random>.")
|
||||
parser.add_argument("--no-synth", action="store_true", help="Skip final synthesis.")
|
||||
parser.add_argument("--clear", action="store_true", help="Clear room history before starting.")
|
||||
parser.add_argument("--timeout", type=int, default=120, help="Timeout per agent call in seconds.")
|
||||
parser.add_argument("--output-dir", type=Path, default=None,
|
||||
help="Directory for transcripts/final output. Default: SWARM_OUTPUT_DIR or ./swarm_outputs")
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(args.agents) < 2:
|
||||
print("Need at least 2 agents for a discussion.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
room_name = args.room or f"{DEFAULT_ROOM_PREFIX}:{uuid.uuid4().hex[:8]}"
|
||||
room = RedisRoom(room_name)
|
||||
if args.clear:
|
||||
room.clear()
|
||||
|
||||
roles = args.roles or [a.replace("swarm-", "") for a in args.agents]
|
||||
if len(roles) != len(args.agents):
|
||||
print("--roles must match --agents length.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
role_prompts = {
|
||||
"architect": "Look at the big picture, propose structure and design trade-offs.",
|
||||
"critic": "Challenge assumptions, find flaws, edge cases, and risks in others' proposals.",
|
||||
"coder": "Turn ideas into concrete code or commands. Prefer working examples.",
|
||||
"tester": "Identify what needs to be tested, write test cases, and verify edge cases.",
|
||||
"reviewer": "Check the proposed solution for clarity, completeness, and maintainability.",
|
||||
"planner": "Break the topic into actionable steps and suggest an execution order.",
|
||||
"default": "Contribute relevant expertise and help the group converge on a good answer.",
|
||||
}
|
||||
|
||||
agents = []
|
||||
for profile, role in zip(args.agents, roles):
|
||||
agents.append(AgentDef(
|
||||
name=profile,
|
||||
role=role,
|
||||
prompt=role_prompts.get(role, role_prompts["default"]),
|
||||
))
|
||||
|
||||
out_dir = args.output_dir or OUTPUT_DIR
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"Room: {room_name}")
|
||||
print(f"Agents: {[a.name + '(' + a.role + ')' for a in agents]}")
|
||||
print(f"Rounds: {args.rounds}")
|
||||
print(f"Output: {out_dir}")
|
||||
|
||||
swarm = SwarmChat(room, agents, args.topic, args.rounds)
|
||||
swarm.runner.timeout = args.timeout
|
||||
history = swarm.run()
|
||||
|
||||
print(f"\nTotal messages in room: {len(history)}")
|
||||
|
||||
if not args.no_synth:
|
||||
final = swarm.synthesize()
|
||||
print("\n=== FINAL SYNTHESIS ===")
|
||||
print(final)
|
||||
out_file = out_dir / f"{room_name.replace(':', '_')}_final.md"
|
||||
out_file.write_text(f"# {args.topic}\n\n{final}\n", encoding="utf-8")
|
||||
print(f"\nSaved synthesis to {out_file}")
|
||||
|
||||
tx_file = out_dir / f"{room_name.replace(':', '_')}_transcript.md"
|
||||
lines = [f"# {args.topic}\n\n## Room: {room_name}\n\n"]
|
||||
for m in history:
|
||||
lines.append(f"### Round {m.round} — {m.agent} ({m.role})\n\n{m.content}\n\n")
|
||||
tx_file.write_text("".join(lines), encoding="utf-8")
|
||||
print(f"Saved transcript to {tx_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
templates/docker-compose.redis.yml
Normal file
15
templates/docker-compose.redis.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
redis-swarm:
|
||||
image: redis:7-alpine
|
||||
container_name: redis-swarm
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis-swarm-data:/data
|
||||
|
||||
volumes:
|
||||
redis-swarm-data:
|
||||
48
templates/hermes-profiles.yaml
Normal file
48
templates/hermes-profiles.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Example Hermes profiles for the swarm skill.
|
||||
# Copy relevant blocks into ~/.hermes/config.yaml under the `profiles:` key.
|
||||
|
||||
profiles:
|
||||
swarm-architect:
|
||||
provider: openrouter
|
||||
model: google/gemma-3-12b-it:cheap
|
||||
system_prompt: |
|
||||
You are the architect in a multi-agent engineering discussion.
|
||||
Look at the big picture, propose structure and design trade-offs,
|
||||
and help the group converge on a clean, maintainable design.
|
||||
Be concise (2-5 sentences per reply).
|
||||
|
||||
swarm-critic:
|
||||
provider: openrouter
|
||||
model: deepseek/deepseek-v3:free
|
||||
system_prompt: |
|
||||
You are the critic in a multi-agent engineering discussion.
|
||||
Challenge assumptions, find flaws, edge cases, security risks,
|
||||
and non-obvious downsides of every proposal.
|
||||
Be concise (2-5 sentences per reply).
|
||||
|
||||
swarm-coder:
|
||||
provider: openrouter
|
||||
model: qwen/qwen-2.5-coder-32b-instruct
|
||||
system_prompt: |
|
||||
You are the coder in a multi-agent engineering discussion.
|
||||
Turn ideas into concrete, working code or commands.
|
||||
Prefer minimal, readable examples that compile/run.
|
||||
Be concise (2-5 sentences + short code blocks).
|
||||
|
||||
swarm-tester:
|
||||
provider: openrouter
|
||||
model: qwen/qwen-2.5-coder-32b-instruct
|
||||
system_prompt: |
|
||||
You are the tester in a multi-agent engineering discussion.
|
||||
Identify what needs to be tested, write test cases,
|
||||
and verify edge cases and failure modes.
|
||||
Be concise (2-5 sentences per reply).
|
||||
|
||||
swarm-reviewer:
|
||||
provider: openrouter
|
||||
model: google/gemma-3-12b-it:cheap
|
||||
system_prompt: |
|
||||
You are the reviewer in a multi-agent engineering discussion.
|
||||
Check the proposed solution for clarity, completeness,
|
||||
maintainability, and consistency with the original problem.
|
||||
Be concise (2-5 sentences per reply).
|
||||
Loading…
Reference in a new issue