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.
Files changed (44) hide show
  1. package/README.md +140 -589
  2. package/auto_setup.py +17 -17
  3. package/docs/CHANGELOG.md +99 -0
  4. package/docs/MULTI_AGENT_RUNTIME.md +23 -5
  5. package/docs/PLUGIN_SDK.md +21 -8
  6. package/docs/REALTIME_COLLABORATION.md +19 -6
  7. package/docs/V2_ARCHITECTURE.md +65 -33
  8. package/docs/WORKFLOW_DESIGNER.md +18 -8
  9. package/docs/architecture.md +127 -135
  10. package/docs/kg-schema.md +3 -3
  11. package/docs/public-deploy.md +2 -3
  12. package/knowledge_graph.py +2 -2
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/agents.py +57 -1
  15. package/latticeai/api/marketplace.py +81 -0
  16. package/latticeai/api/models.py +8 -0
  17. package/latticeai/api/plugins.py +1 -1
  18. package/latticeai/api/realtime.py +1 -1
  19. package/latticeai/api/workflow_designer.py +10 -1
  20. package/latticeai/core/config.py +1 -1
  21. package/latticeai/core/graph_curator.py +2 -2
  22. package/latticeai/core/marketplace.py +178 -0
  23. package/latticeai/core/model_compat.py +7 -63
  24. package/latticeai/core/model_resolution.py +1 -1
  25. package/latticeai/core/multi_agent.py +359 -68
  26. package/latticeai/core/plugins.py +29 -13
  27. package/latticeai/core/realtime.py +1 -1
  28. package/latticeai/core/workflow_engine.py +1 -1
  29. package/latticeai/core/workspace_os.py +257 -10
  30. package/latticeai/server_app.py +17 -5
  31. package/latticeai/services/model_catalog.py +105 -153
  32. package/latticeai/services/model_recommendation.py +28 -17
  33. package/latticeai/services/model_runtime.py +2 -2
  34. package/latticeai/services/platform_runtime.py +9 -5
  35. package/llm_router.py +80 -92
  36. package/ltcai_cli.py +2 -3
  37. package/package.json +2 -2
  38. package/static/agents.html +47 -3
  39. package/static/chat.html +5 -6
  40. package/static/plugins.html +51 -0
  41. package/static/scripts/chat.js +34 -36
  42. package/static/workflows.html +22 -0
  43. package/static/workspace.html +1 -1
  44. 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-4", re.compile(r"gpt[-_]?4", re.I)),
38
- ("gpt-3.5", re.compile(r"gpt[-_]?3\.?5", re.I)),
39
- ("o1", re.compile(r"\bo1[-_]?", re.I)),
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": False,
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": False,
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 테이블 (예: {"gpt-oss-20b": {"local_mlx": "mlx-community/...","ollama":"gpt-oss:20b"}})
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())