backbone-agents 4.0.0a1__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.
- backbone_agents/__init__.py +6 -0
- backbone_agents/cli.py +236 -0
- backbone_agents/cli_v4.py +197 -0
- backbone_agents/compilers/__init__.py +36 -0
- backbone_agents/compilers/base.py +68 -0
- backbone_agents/compilers/langgraph.py +575 -0
- backbone_agents/compilers/pydantic_ai.py +269 -0
- backbone_agents/linter.py +239 -0
- backbone_agents/templates/__init__.py +24 -0
- backbone_agents/templates/debate.json +38 -0
- backbone_agents/templates/router.json +34 -0
- backbone_agents/templates/sequential.json +34 -0
- backbone_agents-4.0.0a1.dist-info/METADATA +190 -0
- backbone_agents-4.0.0a1.dist-info/RECORD +17 -0
- backbone_agents-4.0.0a1.dist-info/WHEEL +4 -0
- backbone_agents-4.0.0a1.dist-info/entry_points.txt +2 -0
- backbone_agents-4.0.0a1.dist-info/licenses/LICENSE +21 -0
backbone_agents/cli.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# backbone.py — CLI موحّد لنظام Backbone System
|
|
4
|
+
# الاستخدام: python backbone.py <command> [args]
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import importlib.util
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
PROJECTS_DIR = os.path.join(HERE, "10_projects")
|
|
14
|
+
EXPORTER = os.path.join(HERE, "export_langgraph.py")
|
|
15
|
+
EXPORTER_PYDANTIC = os.path.join(HERE, "export_pydantic_ai.py")
|
|
16
|
+
LINTER = os.path.join(HERE, "linter.py")
|
|
17
|
+
|
|
18
|
+
# تحميل .env
|
|
19
|
+
def load_env():
|
|
20
|
+
env_path = os.path.join(HERE, ".env")
|
|
21
|
+
if os.path.exists(env_path):
|
|
22
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
23
|
+
for line in f:
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if line and not line.startswith("#") and "=" in line:
|
|
26
|
+
k, v = line.split("=", 1)
|
|
27
|
+
os.environ.setdefault(k.strip(), v.strip())
|
|
28
|
+
|
|
29
|
+
def load_module(name, path):
|
|
30
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
31
|
+
mod = importlib.util.module_from_spec(spec)
|
|
32
|
+
spec.loader.exec_module(mod)
|
|
33
|
+
return mod
|
|
34
|
+
|
|
35
|
+
def get_project_path(name):
|
|
36
|
+
p = os.path.join(PROJECTS_DIR, name, "contracts.json")
|
|
37
|
+
if not os.path.exists(p):
|
|
38
|
+
print(f"❌ المشروع غير موجود: {p}")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
return p
|
|
41
|
+
|
|
42
|
+
# =====================================================================
|
|
43
|
+
# الأوامر
|
|
44
|
+
# =====================================================================
|
|
45
|
+
def cmd_lint(args):
|
|
46
|
+
linter = load_module("linter", LINTER)
|
|
47
|
+
path = get_project_path(args.project)
|
|
48
|
+
result = linter.lint_project(path)
|
|
49
|
+
linter.print_report(result, args.project)
|
|
50
|
+
sys.exit(0 if result["passed"] or args.lenient else 2)
|
|
51
|
+
|
|
52
|
+
def cmd_export(args):
|
|
53
|
+
linter = load_module("linter", LINTER)
|
|
54
|
+
path = get_project_path(args.project)
|
|
55
|
+
result = linter.lint_project(path)
|
|
56
|
+
linter.print_report(result, args.project)
|
|
57
|
+
|
|
58
|
+
if not result["passed"] and not args.force:
|
|
59
|
+
print("❌ العقد معيب — توقف التوليد. استخدم --force للتخطي.")
|
|
60
|
+
sys.exit(2)
|
|
61
|
+
|
|
62
|
+
if not result["passed"] and args.force:
|
|
63
|
+
print("⚠️ متابعة مع --force…")
|
|
64
|
+
|
|
65
|
+
print()
|
|
66
|
+
os.environ["BACKBONE_PROJECT"] = args.project
|
|
67
|
+
|
|
68
|
+
exporter_name = getattr(args, "exporter", "langgraph") or "langgraph"
|
|
69
|
+
if exporter_name == "pydantic":
|
|
70
|
+
pyd_mod = load_module("export_pydantic_ai", EXPORTER_PYDANTIC)
|
|
71
|
+
pyd_mod.export_pydantic_ai(args.project)
|
|
72
|
+
else:
|
|
73
|
+
exporter = load_module("export_langgraph", EXPORTER)
|
|
74
|
+
# --force لأن Linter اجتاز بالفعل هنا
|
|
75
|
+
exporter.export_to_langgraph(force=True)
|
|
76
|
+
|
|
77
|
+
def cmd_run(args):
|
|
78
|
+
# 1) lint
|
|
79
|
+
linter = load_module("linter", LINTER)
|
|
80
|
+
path = get_project_path(args.project)
|
|
81
|
+
result = linter.lint_project(path)
|
|
82
|
+
linter.print_report(result, args.project)
|
|
83
|
+
if not result["passed"] and not args.force:
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
|
|
86
|
+
# 2) export
|
|
87
|
+
print()
|
|
88
|
+
os.environ["BACKBONE_PROJECT"] = args.project
|
|
89
|
+
exporter = load_module("export_langgraph", EXPORTER)
|
|
90
|
+
exporter.export_to_langgraph(force=args.force)
|
|
91
|
+
|
|
92
|
+
# 3) run
|
|
93
|
+
print()
|
|
94
|
+
load_env()
|
|
95
|
+
if not os.getenv("GOOGLE_API_KEY"):
|
|
96
|
+
print("❌ GOOGLE_API_KEY غير محمّل")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
gen_path = os.path.join(HERE, "06_exporters", "generated_graph.py")
|
|
100
|
+
mod = load_module("generated_graph", gen_path)
|
|
101
|
+
|
|
102
|
+
app = mod.app
|
|
103
|
+
extract_text = mod.extract_text
|
|
104
|
+
HumanMessage = mod.HumanMessage
|
|
105
|
+
MODEL = getattr(mod, "MODEL_NAME", "?")
|
|
106
|
+
|
|
107
|
+
# بناء الحالة الأولية
|
|
108
|
+
topic = args.topic
|
|
109
|
+
initial_state = {
|
|
110
|
+
"messages": [HumanMessage(content=topic)],
|
|
111
|
+
"current_agent": "",
|
|
112
|
+
"next_step": "",
|
|
113
|
+
"round_number": 0,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
print("=" * 72)
|
|
117
|
+
print(f"🚀 تشغيل المشروع: {args.project}")
|
|
118
|
+
print(f"النموذج: {MODEL} | الموضوع: {topic[:80]}")
|
|
119
|
+
print("=" * 72)
|
|
120
|
+
|
|
121
|
+
step = 0
|
|
122
|
+
try:
|
|
123
|
+
for update in app.stream(initial_state, {"recursion_limit": 50}):
|
|
124
|
+
step += 1
|
|
125
|
+
for node_name, delta in update.items():
|
|
126
|
+
print(f"\n─── [خطوة {step}] 🔹 {node_name} ───")
|
|
127
|
+
for m in delta.get("messages", []) if isinstance(delta, dict) else []:
|
|
128
|
+
if hasattr(m, "content"):
|
|
129
|
+
print(extract_text(m))
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f"\n❌ خطأ: {type(e).__name__}: {e}")
|
|
132
|
+
sys.exit(3)
|
|
133
|
+
|
|
134
|
+
print("\n" + "=" * 72)
|
|
135
|
+
print("✅ اكتمل التشغيل")
|
|
136
|
+
print("=" * 72)
|
|
137
|
+
|
|
138
|
+
def cmd_patterns(args):
|
|
139
|
+
print("📐 الأنماط المدعومة في v3.0:")
|
|
140
|
+
print(" 1. router — وكيل تصنيف + توجيه شرطي")
|
|
141
|
+
print(" 2. debate — مناظرة 3 أدوار + حلقة + حكم")
|
|
142
|
+
print(" 3. sequential — تدفق خطي A → B → C → END")
|
|
143
|
+
|
|
144
|
+
def cmd_projects(args):
|
|
145
|
+
print(f"📂 المشاريع في {PROJECTS_DIR}:")
|
|
146
|
+
if not os.path.isdir(PROJECTS_DIR):
|
|
147
|
+
print(" (المجلد غير موجود)")
|
|
148
|
+
return
|
|
149
|
+
for p in sorted(os.listdir(PROJECTS_DIR)):
|
|
150
|
+
path = os.path.join(PROJECTS_DIR, p, "contracts.json")
|
|
151
|
+
if os.path.exists(path):
|
|
152
|
+
try:
|
|
153
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
154
|
+
data = json.load(f)
|
|
155
|
+
pattern = data.get("project_metadata", {}).get("selected_pattern", "?")
|
|
156
|
+
n_agents = len(data.get("agents", []))
|
|
157
|
+
print(f" - {p:40} [{pattern:10}] {n_agents} وكيل")
|
|
158
|
+
except Exception:
|
|
159
|
+
print(f" - {p:40} [JSON معطوب]")
|
|
160
|
+
|
|
161
|
+
def cmd_status(args):
|
|
162
|
+
print("🎯 Backbone System — الحالة")
|
|
163
|
+
print("=" * 50)
|
|
164
|
+
print(f"📁 المجلد: {HERE}")
|
|
165
|
+
print(f"🐍 Python: {sys.version.split()[0]}")
|
|
166
|
+
print()
|
|
167
|
+
# فحص .env
|
|
168
|
+
has_key = bool(os.getenv("GOOGLE_API_KEY"))
|
|
169
|
+
print(f"🔑 GOOGLE_API_KEY: {'✅' if has_key else '❌'}")
|
|
170
|
+
print(f"🤖 النموذج: {os.getenv('BACKBONE_LLM_MODEL', 'gemini-2.5-flash')}")
|
|
171
|
+
print()
|
|
172
|
+
# فحص الملفات
|
|
173
|
+
files = [
|
|
174
|
+
("المولّد", EXPORTER),
|
|
175
|
+
("Linter", LINTER),
|
|
176
|
+
("CLI", os.path.abspath(__file__)),
|
|
177
|
+
]
|
|
178
|
+
for name, path in files:
|
|
179
|
+
ok = "✅" if os.path.exists(path) else "❌"
|
|
180
|
+
print(f" {ok} {name}: {os.path.basename(path)}")
|
|
181
|
+
# مشاريع
|
|
182
|
+
if os.path.isdir(PROJECTS_DIR):
|
|
183
|
+
n = len([p for p in os.listdir(PROJECTS_DIR)
|
|
184
|
+
if os.path.exists(os.path.join(PROJECTS_DIR, p, "contracts.json"))])
|
|
185
|
+
print(f" 📂 {n} مشروع")
|
|
186
|
+
|
|
187
|
+
# =====================================================================
|
|
188
|
+
# Main
|
|
189
|
+
# =====================================================================
|
|
190
|
+
def main():
|
|
191
|
+
load_env()
|
|
192
|
+
|
|
193
|
+
parser = argparse.ArgumentParser(
|
|
194
|
+
prog="backbone",
|
|
195
|
+
description="Backbone System — CLI موحّد"
|
|
196
|
+
)
|
|
197
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
198
|
+
|
|
199
|
+
# lint
|
|
200
|
+
p_lint = sub.add_parser("lint", help="فحص عقد مشروع")
|
|
201
|
+
p_lint.add_argument("project")
|
|
202
|
+
p_lint.add_argument("--lenient", action="store_true")
|
|
203
|
+
p_lint.set_defaults(func=cmd_lint)
|
|
204
|
+
|
|
205
|
+
# export
|
|
206
|
+
p_exp = sub.add_parser("export", help="فحص + توليد كود")
|
|
207
|
+
p_exp.add_argument("project")
|
|
208
|
+
p_exp.add_argument("--force", action="store_true")
|
|
209
|
+
p_exp.add_argument("--exporter", choices=["langgraph", "pydantic"],
|
|
210
|
+
default="langgraph", help="المُصدّر الهدف")
|
|
211
|
+
p_exp.set_defaults(func=cmd_export)
|
|
212
|
+
|
|
213
|
+
# run
|
|
214
|
+
p_run = sub.add_parser("run", help="فحص + توليد + تشغيل")
|
|
215
|
+
p_run.add_argument("project")
|
|
216
|
+
p_run.add_argument("--topic", required=True)
|
|
217
|
+
p_run.add_argument("--force", action="store_true")
|
|
218
|
+
p_run.set_defaults(func=cmd_run)
|
|
219
|
+
|
|
220
|
+
# patterns
|
|
221
|
+
p_pat = sub.add_parser("patterns", help="عرض الأنماط")
|
|
222
|
+
p_pat.set_defaults(func=cmd_patterns)
|
|
223
|
+
|
|
224
|
+
# projects
|
|
225
|
+
p_proj = sub.add_parser("projects", help="عرض المشاريع")
|
|
226
|
+
p_proj.set_defaults(func=cmd_projects)
|
|
227
|
+
|
|
228
|
+
# status
|
|
229
|
+
p_st = sub.add_parser("status", help="حالة النظام")
|
|
230
|
+
p_st.set_defaults(func=cmd_status)
|
|
231
|
+
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
args.func(args)
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Backbone Agents CLI v4.0 — REGISTRY-based."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .linter import lint_project, print_report
|
|
14
|
+
from .compilers import REGISTRY, get as get_compiler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_projects_dir(cli_arg):
|
|
18
|
+
if cli_arg:
|
|
19
|
+
return Path(cli_arg).expanduser().resolve()
|
|
20
|
+
env = os.getenv("BACKBONE_PROJECTS_DIR")
|
|
21
|
+
if env:
|
|
22
|
+
return Path(env).expanduser().resolve()
|
|
23
|
+
return (Path.cwd() / "10_projects").resolve()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_contract(projects_dir, project):
|
|
27
|
+
path = projects_dir / project / "contracts.json"
|
|
28
|
+
if not path.exists():
|
|
29
|
+
print(f"❌ المشروع غير موجود: {path}", file=sys.stderr)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(path.read_text(encoding="utf-8")), path
|
|
33
|
+
except json.JSONDecodeError as e:
|
|
34
|
+
print(f"❌ JSON معطوب في {path}: {e}", file=sys.stderr)
|
|
35
|
+
sys.exit(2)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd_lint(args):
|
|
39
|
+
projects_dir = resolve_projects_dir(args.projects_dir)
|
|
40
|
+
contract, path = load_contract(projects_dir, args.project)
|
|
41
|
+
result = lint_project(str(path))
|
|
42
|
+
print_report(result, args.project)
|
|
43
|
+
sys.exit(0 if result["passed"] or args.lenient else 2)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cmd_export(args):
|
|
47
|
+
projects_dir = resolve_projects_dir(args.projects_dir)
|
|
48
|
+
contract, path = load_contract(projects_dir, args.project)
|
|
49
|
+
result = lint_project(str(path))
|
|
50
|
+
print_report(result, args.project)
|
|
51
|
+
if not result["passed"] and not args.force:
|
|
52
|
+
print("❌ العقد معيب — توقف التوليد. استخدم --force.", file=sys.stderr)
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
target = args.target
|
|
55
|
+
if target not in REGISTRY:
|
|
56
|
+
print(f"❌ المُصدّر غير متاح: {target}. المتاح: {sorted(REGISTRY)}", file=sys.stderr)
|
|
57
|
+
sys.exit(2)
|
|
58
|
+
output_dir = (Path(args.output_dir).expanduser().resolve()
|
|
59
|
+
if args.output_dir else (projects_dir / args.project / "generated"))
|
|
60
|
+
print(f"\n🔧 تصدير إلى {target} → {output_dir}")
|
|
61
|
+
paths = get_compiler(target).compile(contract, output_dir)
|
|
62
|
+
for p in paths:
|
|
63
|
+
print(f" ✅ {p}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_run(args):
|
|
67
|
+
args.output_dir = None
|
|
68
|
+
cmd_export(args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_patterns(args):
|
|
72
|
+
print("📐 الأنماط المدعومة:")
|
|
73
|
+
print(" 1. router — وكيل تصنيف + توجيه شرطي")
|
|
74
|
+
print(" 2. debate — مناظرة 3 أدوار + حلقة + حكم")
|
|
75
|
+
print(" 3. sequential — تدفق خطي A → B → C → END")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_projects(args):
|
|
79
|
+
projects_dir = resolve_projects_dir(args.projects_dir)
|
|
80
|
+
print(f"📂 المشاريع في {projects_dir}:")
|
|
81
|
+
if not projects_dir.is_dir():
|
|
82
|
+
print(" (المجلد غير موجود)")
|
|
83
|
+
return
|
|
84
|
+
for entry in sorted(projects_dir.iterdir()):
|
|
85
|
+
contract = entry / "contracts.json"
|
|
86
|
+
if not contract.is_file():
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
data = json.loads(contract.read_text(encoding="utf-8"))
|
|
90
|
+
pattern = data.get("project_metadata", {}).get("selected_pattern", "?")
|
|
91
|
+
n = len(data.get("agents", []))
|
|
92
|
+
print(f" - {entry.name:40} [{pattern:10}] {n} وكيل")
|
|
93
|
+
except Exception:
|
|
94
|
+
print(f" - {entry.name:40} [JSON معطوب]")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_status(args):
|
|
98
|
+
print("🎯 Backbone Agents — الحالة")
|
|
99
|
+
print("=" * 50)
|
|
100
|
+
print(f"📦 الإصدار: {__version__}")
|
|
101
|
+
print(f"🐍 Python: {sys.version.split()[0]}")
|
|
102
|
+
print(f"🔑 GOOGLE_API_KEY: {'✅' if os.getenv('GOOGLE_API_KEY') else '❌'}")
|
|
103
|
+
print(f"🤖 النموذج: {os.getenv('BACKBONE_LLM_MODEL', 'gemini-3.5-flash-lite')}")
|
|
104
|
+
print()
|
|
105
|
+
print(f"🔧 المُصدّرات المسجلة ({len(REGISTRY)}):")
|
|
106
|
+
for name, plugin in sorted(REGISTRY.items()):
|
|
107
|
+
patterns = ", ".join(plugin.supported_patterns)
|
|
108
|
+
print(f" ✅ {name:15} → {patterns}")
|
|
109
|
+
print()
|
|
110
|
+
projects_dir = resolve_projects_dir(getattr(args, "projects_dir", None))
|
|
111
|
+
print(f"📂 مجلد المشاريع: {projects_dir}")
|
|
112
|
+
if projects_dir.is_dir():
|
|
113
|
+
count = sum(1 for p in projects_dir.iterdir() if (p / "contracts.json").is_file())
|
|
114
|
+
print(f" {count} مشروع")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_init(args):
|
|
118
|
+
from .templates import get_template, TEMPLATES
|
|
119
|
+
if args.template not in TEMPLATES:
|
|
120
|
+
print(f"❌ القالب غير معروف: {args.template}", file=sys.stderr)
|
|
121
|
+
print(f" المتاح: {TEMPLATES}", file=sys.stderr)
|
|
122
|
+
sys.exit(2)
|
|
123
|
+
projects_dir = resolve_projects_dir(args.projects_dir)
|
|
124
|
+
project_dir = projects_dir / args.name
|
|
125
|
+
if project_dir.exists():
|
|
126
|
+
print(f"❌ المشروع موجود مسبقاً: {project_dir}", file=sys.stderr)
|
|
127
|
+
sys.exit(2)
|
|
128
|
+
project_dir.mkdir(parents=True)
|
|
129
|
+
contract = get_template(args.template)
|
|
130
|
+
contract["project_metadata"]["name"] = args.name
|
|
131
|
+
(project_dir / "contracts.json").write_text(
|
|
132
|
+
json.dumps(contract, ensure_ascii=False, indent=2),
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
)
|
|
135
|
+
print(f"✅ مشروع جديد: {project_dir}")
|
|
136
|
+
print(f" العقد: contracts.json (نمط: {args.template})")
|
|
137
|
+
print(f" التالي: backbone lint {args.name} --projects-dir {projects_dir}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_parser():
|
|
141
|
+
parser = argparse.ArgumentParser(
|
|
142
|
+
prog="backbone",
|
|
143
|
+
description="Backbone Agents — Agent Compiler (v4.0)",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
146
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
147
|
+
|
|
148
|
+
p = sub.add_parser("lint", help="فحص عقد مشروع")
|
|
149
|
+
p.add_argument("project")
|
|
150
|
+
p.add_argument("--lenient", action="store_true")
|
|
151
|
+
p.add_argument("--projects-dir", default=None)
|
|
152
|
+
p.set_defaults(func=cmd_lint)
|
|
153
|
+
|
|
154
|
+
p = sub.add_parser("export", help="فحص + توليد كود")
|
|
155
|
+
p.add_argument("project")
|
|
156
|
+
p.add_argument("--target", choices=list(REGISTRY.keys()) or ["langgraph"], default="langgraph")
|
|
157
|
+
p.add_argument("--output-dir", default=None)
|
|
158
|
+
p.add_argument("--force", action="store_true")
|
|
159
|
+
p.add_argument("--projects-dir", default=None)
|
|
160
|
+
p.set_defaults(func=cmd_export)
|
|
161
|
+
|
|
162
|
+
p = sub.add_parser("run", help="فحص + توليد (التنفيذ في v4.4)")
|
|
163
|
+
p.add_argument("project")
|
|
164
|
+
p.add_argument("--target", default="langgraph")
|
|
165
|
+
p.add_argument("--topic", required=False)
|
|
166
|
+
p.add_argument("--force", action="store_true")
|
|
167
|
+
p.add_argument("--projects-dir", default=None)
|
|
168
|
+
p.set_defaults(func=cmd_run)
|
|
169
|
+
|
|
170
|
+
p = sub.add_parser("patterns", help="عرض الأنماط")
|
|
171
|
+
p.set_defaults(func=cmd_patterns)
|
|
172
|
+
|
|
173
|
+
p = sub.add_parser("projects", help="عرض المشاريع")
|
|
174
|
+
p.add_argument("--projects-dir", default=None)
|
|
175
|
+
p.set_defaults(func=cmd_projects)
|
|
176
|
+
|
|
177
|
+
p = sub.add_parser("status", help="حالة النظام")
|
|
178
|
+
p.add_argument("--projects-dir", default=None)
|
|
179
|
+
p.set_defaults(func=cmd_status)
|
|
180
|
+
|
|
181
|
+
p = sub.add_parser("init", help="إنشاء مشروع جديد من قالب")
|
|
182
|
+
p.add_argument("name")
|
|
183
|
+
p.add_argument("--template", choices=["router", "debate", "sequential"], default="router")
|
|
184
|
+
p.add_argument("--projects-dir", default=None)
|
|
185
|
+
p.set_defaults(func=cmd_init)
|
|
186
|
+
|
|
187
|
+
return parser
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main(argv=None):
|
|
191
|
+
parser = build_parser()
|
|
192
|
+
args = parser.parse_args(argv)
|
|
193
|
+
args.func(args)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Compiler plugins — each framework implements CompilerPlugin.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
from backbone_agents.compilers import REGISTRY, get, register
|
|
5
|
+
|
|
6
|
+
Plugins are auto-registered on import, but registration is defensive:
|
|
7
|
+
if a plugin's framework dependencies are missing, it is silently skipped.
|
|
8
|
+
This keeps `pip install -e .` working before optional deps are installed.
|
|
9
|
+
"""
|
|
10
|
+
from .base import CompilerPlugin, REGISTRY, get, register, write_generated
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CompilerPlugin",
|
|
14
|
+
"REGISTRY",
|
|
15
|
+
"get",
|
|
16
|
+
"register",
|
|
17
|
+
"write_generated",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _auto_register() -> None:
|
|
22
|
+
"""Best-effort registration of built-in plugins."""
|
|
23
|
+
try:
|
|
24
|
+
from .langgraph import LangGraphCompiler
|
|
25
|
+
register(LangGraphCompiler())
|
|
26
|
+
except ImportError:
|
|
27
|
+
pass # langgraph not installed — plugin unavailable
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from .pydantic_ai import PydanticAICompiler
|
|
31
|
+
register(PydanticAICompiler())
|
|
32
|
+
except ImportError:
|
|
33
|
+
pass # pydantic-ai not installed — plugin unavailable
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_auto_register()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Compiler plugin protocol — Layer 3 of v4.0 architecture.
|
|
2
|
+
|
|
3
|
+
Every framework exporter (LangGraph, Pydantic AI, future frameworks)
|
|
4
|
+
implements this Protocol. The CLI (Layer 5) never imports a concrete
|
|
5
|
+
exporter directly — it dispatches through this interface.
|
|
6
|
+
|
|
7
|
+
This is the architectural seam that makes the project "framework-agnostic":
|
|
8
|
+
adding a new framework means adding one file under compilers/ and
|
|
9
|
+
registering it in REGISTRY — zero changes to cli.py or linter.py.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class CompilerPlugin(Protocol):
|
|
19
|
+
"""Contract that every framework compiler must satisfy."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
"""Short identifier: 'langgraph' | 'pydantic_ai'."""
|
|
23
|
+
|
|
24
|
+
supported_patterns: tuple[str, ...]
|
|
25
|
+
"""Patterns this compiler can emit: ('router', 'debate', 'sequential')."""
|
|
26
|
+
|
|
27
|
+
def compile(self, contract: dict, output_dir: Path) -> list[Path]:
|
|
28
|
+
"""Compile a contract into framework-specific source files.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
contract: Parsed contracts.json (dict).
|
|
32
|
+
output_dir: Directory to write generated files into.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of generated file paths (absolute).
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: if contract.pattern not in supported_patterns.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Registry — populated by compilers/__init__.py
|
|
44
|
+
REGISTRY: dict[str, "CompilerPlugin"] = {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def register(plugin: CompilerPlugin) -> None:
|
|
48
|
+
"""Register a compiler plugin by its .name."""
|
|
49
|
+
REGISTRY[plugin.name] = plugin
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get(name: str) -> CompilerPlugin:
|
|
53
|
+
"""Look up a registered compiler; raise KeyError if unknown."""
|
|
54
|
+
if name not in REGISTRY:
|
|
55
|
+
raise KeyError(
|
|
56
|
+
f"Unknown compiler '{name}'. Available: {sorted(REGISTRY)}"
|
|
57
|
+
)
|
|
58
|
+
return REGISTRY[name]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_generated(
|
|
62
|
+
output_dir: Path, filename: str, lines: list[str]
|
|
63
|
+
) -> Path:
|
|
64
|
+
"""Write generated code lines to output_dir/filename; return path."""
|
|
65
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
path = output_dir / filename
|
|
67
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
68
|
+
return path
|