ltcai 2.0.0 → 2.1.0

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,178 @@
1
+ """Marketplace foundation for local templates.
2
+
3
+ v2.1 intentionally does not add a cloud marketplace. This module provides the
4
+ portable template shape, metadata, export/import validation, and install hooks
5
+ that a future service can reuse.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from copy import deepcopy
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ MARKETPLACE_VERSION = "2.1.0"
15
+ TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
+
17
+
18
+ class MarketplaceError(Exception):
19
+ """Raised for invalid template operations."""
20
+
21
+
22
+ BUILTIN_TEMPLATES: Dict[str, List[Dict[str, Any]]] = {
23
+ "plugin": [
24
+ {
25
+ "id": "plugin-review-action",
26
+ "kind": "plugin",
27
+ "name": "Plugin Review Action",
28
+ "version": "1.0.0",
29
+ "description": "A permissioned plugin action template for review workflows.",
30
+ "metadata": {"category": "review", "installable": True},
31
+ "files": {
32
+ "plugin.json": {
33
+ "id": "plugin-review-action",
34
+ "name": "Plugin Review Action",
35
+ "version": "1.0.0",
36
+ "lattice_version": ">=2.1.0",
37
+ "permissions": ["read_workspace", "run_skills"],
38
+ "provides": {"skills": ["review_action"]},
39
+ }
40
+ },
41
+ }
42
+ ],
43
+ "workflow": [
44
+ {
45
+ "id": "workflow-agent-plugin-review",
46
+ "kind": "workflow",
47
+ "name": "Agent Plugin Review Workflow",
48
+ "version": "1.0.0",
49
+ "description": "Manual trigger into planner/executor/reviewer, plugin action, and output.",
50
+ "metadata": {"category": "agent-ops", "installable": True},
51
+ "definition": {
52
+ "name": "Agent Plugin Review Workflow",
53
+ "nodes": [
54
+ {"id": "trigger", "type": "trigger", "name": "Manual start", "config": {"trigger": "manual"}, "next": "agent"},
55
+ {"id": "agent", "type": "agent", "name": "Plan and execute", "config": {"goal": "Run agent platform review", "roles": ["planner", "executor", "reviewer"]}, "next": "plugin"},
56
+ {"id": "plugin", "type": "plugin", "name": "Plugin action", "config": {"plugin": "hello-world", "action": "run_skill", "args": {}}, "next": "output"},
57
+ {"id": "output", "type": "output", "name": "Output", "config": {}, "next": None},
58
+ ],
59
+ "metadata": {"template_id": "workflow-agent-plugin-review"},
60
+ },
61
+ }
62
+ ],
63
+ "agent": [
64
+ {
65
+ "id": "agent-planner-executor-reviewer",
66
+ "kind": "agent",
67
+ "name": "Planner Executor Reviewer",
68
+ "version": "1.0.0",
69
+ "description": "Default bounded planning, execution, review, and retry template.",
70
+ "metadata": {"category": "agent-ops", "installable": True},
71
+ "definition": {
72
+ "roles": ["planner", "executor", "reviewer"],
73
+ "max_retries": 2,
74
+ "constraints": ["workspace scoped", "no secret leakage", "replayable timeline"],
75
+ },
76
+ }
77
+ ],
78
+ }
79
+
80
+
81
+ def _normalize_kind(kind: str) -> str:
82
+ value = str(kind or "").strip().lower()
83
+ if value not in TEMPLATE_KINDS:
84
+ raise MarketplaceError(f"unknown template kind: {kind}")
85
+ return value
86
+
87
+
88
+ class TemplateCatalog:
89
+ """Local template catalog with export/import/install primitives."""
90
+
91
+ def __init__(self, templates: Optional[Dict[str, List[Dict[str, Any]]]] = None):
92
+ self.templates = templates or BUILTIN_TEMPLATES
93
+
94
+ def list_templates(self, kind: Optional[str] = None) -> Dict[str, Any]:
95
+ kinds = [_normalize_kind(kind)] if kind else list(TEMPLATE_KINDS)
96
+ templates = []
97
+ for item_kind in kinds:
98
+ templates.extend(deepcopy(self.templates.get(item_kind, [])))
99
+ return {
100
+ "marketplace_version": MARKETPLACE_VERSION,
101
+ "kinds": list(TEMPLATE_KINDS),
102
+ "templates": templates,
103
+ "total": len(templates),
104
+ }
105
+
106
+ def get_template(self, kind: str, template_id: str) -> Dict[str, Any]:
107
+ item_kind = _normalize_kind(kind)
108
+ for template in self.templates.get(item_kind, []):
109
+ if template.get("id") == template_id:
110
+ return deepcopy(template)
111
+ raise MarketplaceError(f"template not found: {item_kind}/{template_id}")
112
+
113
+ def export_template(self, kind: str, template_id: str) -> Dict[str, Any]:
114
+ template = self.get_template(kind, template_id)
115
+ return {
116
+ "lattice_template_export": MARKETPLACE_VERSION,
117
+ "kind": template["kind"],
118
+ "template": template,
119
+ "metadata": {
120
+ "exported_from": "local",
121
+ "template_id": template_id,
122
+ "template_version": template.get("version"),
123
+ },
124
+ }
125
+
126
+ def import_template(self, payload: Dict[str, Any]) -> Dict[str, Any]:
127
+ if not isinstance(payload, dict):
128
+ raise MarketplaceError("template import payload must be an object")
129
+ template = deepcopy(payload.get("template") or payload)
130
+ kind = _normalize_kind(template.get("kind") or payload.get("kind"))
131
+ if not template.get("id"):
132
+ raise MarketplaceError("template missing id")
133
+ if not template.get("name"):
134
+ raise MarketplaceError("template missing name")
135
+ template["kind"] = kind
136
+ template.setdefault("version", "1.0.0")
137
+ template.setdefault("metadata", {})
138
+ template["metadata"] = {**template["metadata"], "imported": True}
139
+ return template
140
+
141
+ def install_template(
142
+ self,
143
+ template: Dict[str, Any],
144
+ *,
145
+ store: Any,
146
+ user_email: Optional[str] = None,
147
+ workspace_id: Optional[str] = None,
148
+ graph: Any = None,
149
+ ) -> Dict[str, Any]:
150
+ imported = self.import_template(template)
151
+ kind = imported["kind"]
152
+ installed: Dict[str, Any] = {
153
+ "kind": kind,
154
+ "template_id": imported["id"],
155
+ "name": imported["name"],
156
+ "version": imported.get("version", "1.0.0"),
157
+ }
158
+ if kind == "workflow":
159
+ definition = imported.get("definition") or {}
160
+ workflow = store.create_workflow(
161
+ name=definition.get("name") or imported["name"],
162
+ steps=[{"action": node.get("type"), "node": node.get("id")} for node in definition.get("nodes", [])],
163
+ nodes=definition.get("nodes", []),
164
+ metadata={**(definition.get("metadata") or {}), "template_id": imported["id"]},
165
+ user_email=user_email,
166
+ graph=graph,
167
+ workspace_id=workspace_id,
168
+ )
169
+ installed["workflow_id"] = workflow["id"]
170
+ registry = store.mark_template_installed(
171
+ kind=kind,
172
+ template_id=imported["id"],
173
+ version=imported.get("version", "1.0.0"),
174
+ metadata=imported.get("metadata") or {},
175
+ workspace_id=workspace_id,
176
+ )
177
+ installed["registry"] = registry
178
+ return installed