162 lines
4.6 KiB
Python
Executable file
162 lines
4.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
agent-scaffold — generate PRD.md + SOUL.md + MEMORY.md for a new agent
|
|
from m2-memory context.
|
|
|
|
Usage:
|
|
python3 scaffold.py <agent-name> "<description>"
|
|
python3 scaffold.py gesy-rates-fetcher "Monitors GESY rates for GMI Clinic DRG billing"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
MEMORY_API = os.environ.get("M2_MEMORY_API_URL", "http://172.18.0.20:8000")
|
|
AGENT_ID = os.environ.get("M2_MEMORY_AGENT_ID", "m2")
|
|
API_KEY = os.environ.get("M2_MEMORY_API_KEY")
|
|
OUT_BASE = Path.home() / "agents"
|
|
|
|
|
|
def search_memory(query: str, limit: int = 10) -> list[dict]:
|
|
url = f"{MEMORY_API}/memory/search"
|
|
body = json.dumps({
|
|
"query": query,
|
|
"agent_id": AGENT_ID,
|
|
"routing_strategy": "deep",
|
|
"limit": limit,
|
|
}).encode()
|
|
headers = {"Content-Type": "application/json"}
|
|
if API_KEY:
|
|
headers["X-API-Key"] = API_KEY
|
|
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return json.loads(r.read()).get("results", [])
|
|
|
|
|
|
def fmt_memory_block(results: list[dict]) -> str:
|
|
lines = []
|
|
for r in results:
|
|
ts = (r.get("timestamp") or "")[:10]
|
|
score = r.get("score", 0)
|
|
content = (r.get("content") or "").replace("\n", " ")[:200]
|
|
lines.append(f"- [{ts} score={score:.2f}] {content}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_prd(name: str, description: str, memories: list[dict]) -> str:
|
|
mem_block = fmt_memory_block(memories)
|
|
return f"""# {name.replace("-", " ").title()} — PRD
|
|
|
|
**Agent ID:** {name}
|
|
**Description:** {description}
|
|
**Generated from:** m2-memory search ({len(memories)} relevant memories)
|
|
|
|
---
|
|
|
|
## Problem
|
|
|
|
> TODO: Refine based on memory context below
|
|
|
|
{description}
|
|
|
|
## Relevant Memory Context
|
|
|
|
{mem_block}
|
|
|
|
## Personas
|
|
|
|
| Persona | Goal |
|
|
|---------|------|
|
|
| TODO | TODO |
|
|
|
|
## Functional Requirements
|
|
|
|
1. TODO — derived from memory context above
|
|
2.
|
|
3.
|
|
|
|
## Success Metrics
|
|
|
|
- TODO
|
|
|
|
## Open Items
|
|
|
|
- TODO — check memory for unresolved gaps
|
|
|
|
## Technical Notes
|
|
|
|
- Agent runtime: OpenClaw / Hermes
|
|
- Memory: m2-memory (agent_id={name})
|
|
- Deploy: Coolify stack on Machine.Machine fleet
|
|
"""
|
|
|
|
|
|
def generate_soul(name: str, description: str) -> str:
|
|
title = name.replace("-", " ").title()
|
|
return f"""# {title} — Agent Identity
|
|
|
|
You are **{title}**, part of the Machine.Machine agent fleet.
|
|
|
|
{description}
|
|
|
|
## Values
|
|
- TODO: define 3 core values for this agent
|
|
|
|
## Communication style
|
|
- TODO: define tone and output format
|
|
|
|
## Scope boundaries
|
|
- You do not TODO
|
|
- You do not TODO
|
|
"""
|
|
|
|
|
|
def generate_memory_seed(memories: list[dict]) -> str:
|
|
lines = ["# Seed Memories\n",
|
|
"These memories are pre-loaded from m2-memory at agent creation time.\n"]
|
|
for r in memories[:10]:
|
|
ts = (r.get("timestamp") or "")[:19]
|
|
mtype = r.get("memory_type", "semantic")
|
|
content = (r.get("content") or "").strip()[:500]
|
|
lines.append(f"## [{mtype} {ts}]\n{content}\n")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument("name", help="agent slug (e.g. gesy-rates-fetcher)")
|
|
p.add_argument("description", help="one-line description of what the agent does")
|
|
p.add_argument("--out", default=None, help="output directory (default: ~/agents/<name>)")
|
|
p.add_argument("--limit", type=int, default=10, help="memory search limit")
|
|
args = p.parse_args()
|
|
|
|
out_dir = Path(args.out) if args.out else OUT_BASE / args.name
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
print(f"Searching m2-memory for: {args.description}", file=sys.stderr)
|
|
try:
|
|
memories = search_memory(args.description, args.limit)
|
|
print(f" Found {len(memories)} relevant memories", file=sys.stderr)
|
|
except Exception as e:
|
|
print(f" Memory search failed ({e}) — generating without context", file=sys.stderr)
|
|
memories = []
|
|
|
|
(out_dir / "PRD.md").write_text(generate_prd(args.name, args.description, memories))
|
|
(out_dir / "SOUL.md").write_text(generate_soul(args.name, args.description))
|
|
(out_dir / "MEMORY.md").write_text(generate_memory_seed(memories))
|
|
|
|
print(f"\nScaffolded: {out_dir}")
|
|
print(f" PRD.md — {(out_dir / 'PRD.md').stat().st_size} bytes")
|
|
print(f" SOUL.md — {(out_dir / 'SOUL.md').stat().st_size} bytes")
|
|
print(f" MEMORY.md — {(out_dir / 'MEMORY.md').stat().st_size} bytes")
|
|
print(f"\nNext: review + fill TODOs, then commit to git.machinemachine.ai/machine.machine/specs/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|