ltcai 2.0.0 → 2.2.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.
- package/README.md +140 -589
- package/auto_setup.py +17 -17
- package/docs/CHANGELOG.md +99 -0
- package/docs/MULTI_AGENT_RUNTIME.md +23 -5
- package/docs/PLUGIN_SDK.md +21 -8
- package/docs/REALTIME_COLLABORATION.md +19 -6
- package/docs/V2_ARCHITECTURE.md +65 -33
- package/docs/WORKFLOW_DESIGNER.md +18 -8
- package/docs/architecture.md +127 -135
- package/docs/kg-schema.md +3 -3
- package/docs/public-deploy.md +2 -3
- package/knowledge_graph.py +2 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +57 -1
- package/latticeai/api/marketplace.py +81 -0
- package/latticeai/api/models.py +8 -0
- package/latticeai/api/plugins.py +1 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/workflow_designer.py +10 -1
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +178 -0
- package/latticeai/core/model_compat.py +7 -63
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/multi_agent.py +359 -68
- package/latticeai/core/plugins.py +29 -13
- package/latticeai/core/realtime.py +1 -1
- package/latticeai/core/workflow_engine.py +1 -1
- package/latticeai/core/workspace_os.py +257 -10
- package/latticeai/server_app.py +17 -5
- package/latticeai/services/model_catalog.py +105 -153
- package/latticeai/services/model_recommendation.py +28 -17
- package/latticeai/services/model_runtime.py +2 -2
- package/latticeai/services/platform_runtime.py +9 -5
- package/llm_router.py +80 -92
- package/ltcai_cli.py +2 -3
- package/package.json +2 -2
- package/static/agents.html +47 -3
- package/static/chat.html +5 -6
- package/static/plugins.html +51 -0
- package/static/scripts/chat.js +34 -36
- package/static/workflows.html +22 -0
- package/static/workspace.html +1 -1
- package/telegram_bot.py +1 -1
|
@@ -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.2.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.2.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
|
|
@@ -25,18 +25,13 @@ logger = logging.getLogger(__name__)
|
|
|
25
25
|
# ── Model family detection ────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
27
|
FAMILY_PATTERNS: List[Tuple[str, re.Pattern]] = [
|
|
28
|
-
("gpt-oss", re.compile(r"gpt[-_]?oss", re.I)),
|
|
29
28
|
("gemma", re.compile(r"gemma", re.I)),
|
|
30
29
|
("qwen", re.compile(r"qwen", re.I)),
|
|
31
30
|
("llama", re.compile(r"\bllama|meta[-_]?llama", re.I)),
|
|
32
|
-
("mistral", re.compile(r"mistral|mixtral", re.I)),
|
|
33
|
-
("phi", re.compile(r"\bphi[-_]?\d", re.I)),
|
|
34
|
-
("deepseek", re.compile(r"deepseek", re.I)),
|
|
35
|
-
("yi", re.compile(r"\byi[-_]?\d", re.I)),
|
|
36
31
|
("claude", re.compile(r"claude", re.I)),
|
|
37
|
-
("gpt
|
|
38
|
-
("
|
|
39
|
-
("
|
|
32
|
+
("gpt", re.compile(r"gpt[-_]?(?:4|5)|openai", re.I)),
|
|
33
|
+
("gemini", re.compile(r"gemini", re.I)),
|
|
34
|
+
("grok", re.compile(r"grok|x[-_]?ai", re.I)),
|
|
40
35
|
]
|
|
41
36
|
|
|
42
37
|
|
|
@@ -59,20 +54,6 @@ def detect_model_family(model_id: str) -> str:
|
|
|
59
54
|
DEFAULT_STOP = ["<|im_end|>", "<|endoftext|>", "</s>", "<|user|>", "<|assistant|>"]
|
|
60
55
|
|
|
61
56
|
FAMILY_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
62
|
-
"gpt-oss": {
|
|
63
|
-
"family": "gpt-oss",
|
|
64
|
-
"supports_system": True,
|
|
65
|
-
"supports_vision": False,
|
|
66
|
-
"chat_template": "gpt_oss",
|
|
67
|
-
"preferred_engines": ["ollama", "llamacpp", "vllm", "local_mlx"],
|
|
68
|
-
"temperature": 0.1,
|
|
69
|
-
"top_p": 0.9,
|
|
70
|
-
"max_tokens": 2048,
|
|
71
|
-
"stop_sequences": ["<|im_end|>", "<|end|>", "</s>", "<|user|>", "<|assistant|>"],
|
|
72
|
-
"disable_draft": True,
|
|
73
|
-
# trim_after_user_marker는 <|user|>가 살아있어야 동작하므로 strip_role_tokens보다 먼저 실행.
|
|
74
|
-
"postprocess": ["trim_after_user_marker", "strip_role_tokens"],
|
|
75
|
-
},
|
|
76
57
|
"gemma": {
|
|
77
58
|
"family": "gemma",
|
|
78
59
|
"supports_system": True,
|
|
@@ -89,7 +70,7 @@ FAMILY_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
|
89
70
|
"qwen": {
|
|
90
71
|
"family": "qwen",
|
|
91
72
|
"supports_system": True,
|
|
92
|
-
"supports_vision":
|
|
73
|
+
"supports_vision": True,
|
|
93
74
|
"chat_template": "qwen_chatml",
|
|
94
75
|
"preferred_engines": ["ollama", "local_mlx", "vllm"],
|
|
95
76
|
"temperature": 0.2,
|
|
@@ -102,7 +83,7 @@ FAMILY_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
|
102
83
|
"llama": {
|
|
103
84
|
"family": "llama",
|
|
104
85
|
"supports_system": True,
|
|
105
|
-
"supports_vision":
|
|
86
|
+
"supports_vision": True,
|
|
106
87
|
"chat_template": "tokenizer_default",
|
|
107
88
|
"preferred_engines": ["ollama", "local_mlx", "llamacpp", "vllm"],
|
|
108
89
|
"temperature": 0.2,
|
|
@@ -112,45 +93,6 @@ FAMILY_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
|
112
93
|
"disable_draft": False,
|
|
113
94
|
"postprocess": ["strip_role_tokens"],
|
|
114
95
|
},
|
|
115
|
-
"mistral": {
|
|
116
|
-
"family": "mistral",
|
|
117
|
-
"supports_system": False,
|
|
118
|
-
"supports_vision": False,
|
|
119
|
-
"chat_template": "tokenizer_default",
|
|
120
|
-
"preferred_engines": ["ollama", "local_mlx", "llamacpp"],
|
|
121
|
-
"temperature": 0.2,
|
|
122
|
-
"top_p": 0.9,
|
|
123
|
-
"max_tokens": 4096,
|
|
124
|
-
"stop_sequences": ["</s>", "[INST]", "[/INST]"],
|
|
125
|
-
"disable_draft": False,
|
|
126
|
-
"postprocess": ["strip_role_tokens"],
|
|
127
|
-
},
|
|
128
|
-
"phi": {
|
|
129
|
-
"family": "phi",
|
|
130
|
-
"supports_system": True,
|
|
131
|
-
"supports_vision": False,
|
|
132
|
-
"chat_template": "tokenizer_default",
|
|
133
|
-
"preferred_engines": ["ollama", "local_mlx"],
|
|
134
|
-
"temperature": 0.2,
|
|
135
|
-
"top_p": 0.9,
|
|
136
|
-
"max_tokens": 2048,
|
|
137
|
-
"stop_sequences": ["<|end|>", "<|endoftext|>"],
|
|
138
|
-
"disable_draft": False,
|
|
139
|
-
"postprocess": ["strip_role_tokens"],
|
|
140
|
-
},
|
|
141
|
-
"deepseek": {
|
|
142
|
-
"family": "deepseek",
|
|
143
|
-
"supports_system": True,
|
|
144
|
-
"supports_vision": False,
|
|
145
|
-
"chat_template": "tokenizer_default",
|
|
146
|
-
"preferred_engines": ["ollama", "local_mlx", "vllm"],
|
|
147
|
-
"temperature": 0.2,
|
|
148
|
-
"top_p": 0.9,
|
|
149
|
-
"max_tokens": 4096,
|
|
150
|
-
"stop_sequences": ["<|EOT|>", "</s>"],
|
|
151
|
-
"disable_draft": False,
|
|
152
|
-
"postprocess": ["strip_role_tokens"],
|
|
153
|
-
},
|
|
154
96
|
"unknown": {
|
|
155
97
|
"family": "unknown",
|
|
156
98
|
"supports_system": True,
|
|
@@ -316,6 +258,7 @@ class CompatProfile:
|
|
|
316
258
|
engine: Optional[str]
|
|
317
259
|
family: str
|
|
318
260
|
template: str
|
|
261
|
+
supports_vision: bool
|
|
319
262
|
stop: List[str]
|
|
320
263
|
temperature: float
|
|
321
264
|
top_p: float
|
|
@@ -362,6 +305,7 @@ def ensure_profile(model_id: str, engine: Optional[str] = None) -> CompatProfile
|
|
|
362
305
|
engine=(engine or "").strip().lower() or None,
|
|
363
306
|
family=base["family"],
|
|
364
307
|
template=base["chat_template"],
|
|
308
|
+
supports_vision=bool(base.get("supports_vision", False)),
|
|
365
309
|
stop=list(base["stop_sequences"]),
|
|
366
310
|
temperature=float(base["temperature"]),
|
|
367
311
|
top_p=float(base["top_p"]),
|
|
@@ -120,7 +120,7 @@ class ModelResolution:
|
|
|
120
120
|
if not provider:
|
|
121
121
|
provider = engine_hint or "local_mlx"
|
|
122
122
|
|
|
123
|
-
# alias 테이블 (예: {"
|
|
123
|
+
# alias 테이블 (예: {"gemma-4-12b-it-4bit": {"local_mlx": "mlx-community/...", "ollama": "hf.co/..."}})
|
|
124
124
|
resolved_model = model_name
|
|
125
125
|
if engine_aliases:
|
|
126
126
|
aliases = engine_aliases.get(model_name.lower())
|