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.
@@ -0,0 +1,6 @@
1
+ """Backbone Agents — Agent Compiler.
2
+
3
+ Contract is the single source of truth. Code is a projection.
4
+ """
5
+ __version__ = "4.0.0a1"
6
+ __all__ = ["__version__"]
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