ltcai 10.2.0 → 10.4.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 +40 -44
- package/docs/CHANGELOG.md +125 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MYPY_BACKLOG.md +65 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/SURFACE_PARITY.md +33 -6
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/context.py +23 -3
- package/lattice_brain/graph/_kg_common.py +164 -9
- package/lattice_brain/graph/_kg_contract.py +223 -0
- package/lattice_brain/graph/_kg_fsutil.py +2 -2
- package/lattice_brain/graph/curator.py +4 -4
- package/lattice_brain/graph/discovery.py +12 -1
- package/lattice_brain/graph/discovery_index.py +12 -1
- package/lattice_brain/graph/documents.py +17 -2
- package/lattice_brain/graph/ingest.py +12 -1
- package/lattice_brain/graph/proactive.py +9 -5
- package/lattice_brain/graph/projection.py +12 -1
- package/lattice_brain/graph/provenance.py +12 -1
- package/lattice_brain/graph/retrieval.py +12 -1
- package/lattice_brain/graph/retrieval_docgen.py +12 -1
- package/lattice_brain/graph/retrieval_reads.py +12 -1
- package/lattice_brain/graph/retrieval_vector.py +12 -1
- package/lattice_brain/graph/schema.py +6 -2
- package/lattice_brain/graph/write_master.py +12 -1
- package/lattice_brain/ingestion.py +1 -1
- package/lattice_brain/quality.py +5 -5
- package/lattice_brain/runtime/agent_runtime.py +7 -2
- package/lattice_brain/runtime/contracts.py +2 -1
- package/lattice_brain/runtime/hooks.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/workflow.py +10 -7
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +2 -2
- package/latticeai/api/chat.py +19 -15
- package/latticeai/api/chat_agent_http.py +19 -7
- package/latticeai/api/chat_helpers.py +9 -8
- package/latticeai/api/chat_stream.py +2 -2
- package/latticeai/api/computer_use.py +2 -2
- package/latticeai/api/hooks.py +4 -2
- package/latticeai/api/local_files.py +8 -4
- package/latticeai/api/mcp.py +2 -2
- package/latticeai/api/models.py +5 -5
- package/latticeai/api/permissions.py +2 -2
- package/latticeai/api/search.py +5 -3
- package/latticeai/api/security_dashboard.py +3 -5
- package/latticeai/api/setup.py +3 -3
- package/latticeai/api/static_routes.py +7 -2
- package/latticeai/api/tools.py +9 -4
- package/latticeai/api/workspace.py +25 -22
- package/latticeai/app_factory.py +132 -1346
- package/latticeai/cli/entrypoint.py +1 -1
- package/latticeai/core/agent.py +28 -14
- package/latticeai/core/audit.py +5 -3
- package/latticeai/core/embedding_providers.py +19 -6
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +2 -2
- package/latticeai/core/mcp_catalog.py +3 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/plugins.py +1 -1
- package/latticeai/core/project_sessions.py +1 -1
- package/latticeai/core/run_explain.py +6 -4
- package/latticeai/core/workspace_os.py +0 -4
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_state.py +1 -1
- package/latticeai/core/workspace_snapshots.py +3 -3
- package/latticeai/integrations/telegram_bot.py +52 -3
- package/latticeai/models/router.py +61 -34
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/build_phases.py +1417 -0
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/review_wiring.py +6 -1
- package/latticeai/runtime/runtime_context.py +276 -0
- package/latticeai/runtime/sso_config_runtime.py +1 -1
- package/latticeai/runtime/stages.py +8 -2
- package/latticeai/services/app_context.py +23 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +4 -2
- package/latticeai/services/cloud_streaming.py +5 -3
- package/latticeai/services/command_center.py +1 -1
- package/latticeai/services/evidence_actions.py +2 -1
- package/latticeai/services/hybrid_chat.py +1 -1
- package/latticeai/services/local_knowledge.py +25 -9
- package/latticeai/services/memory_service.py +8 -4
- package/latticeai/services/model_catalog.py +12 -12
- package/latticeai/services/model_engines.py +15 -17
- package/latticeai/services/model_loading.py +1 -1
- package/latticeai/services/model_runtime.py +31 -33
- package/latticeai/services/multimodal_streaming.py +10 -4
- package/latticeai/services/openai_compatible_adapter.py +4 -3
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +12 -3
- package/latticeai/services/search_service.py +1 -1
- package/latticeai/services/triggers.py +1 -1
- package/latticeai/setup/auto_setup.py +1 -1
- package/latticeai/setup/wizard.py +12 -3
- package/latticeai/tools/computer.py +3 -1
- package/latticeai/tools/documents.py +2 -2
- package/package.json +4 -2
- package/scripts/check_current_release_docs.mjs +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-CbdGD-2i.js → Act-BK__Y5VE.js} +1 -1
- package/static/app/assets/{AdminConsole-LgCkXpnf.js → AdminConsole-ZrTyN8_F.js} +1 -1
- package/static/app/assets/{Brain-CoPGJI1L.js → Brain-CYHDXpPB.js} +1 -1
- package/static/app/assets/{BrainHome-gVnaxSwp.js → BrainHome-bbJulb5g.js} +1 -1
- package/static/app/assets/{BrainSignals-ChxAYHtj.js → BrainSignals-CesSPtUP.js} +1 -1
- package/static/app/assets/{Capture-DBtgkHZg.js → Capture-p5upXmz2.js} +1 -1
- package/static/app/assets/{CommandPalette-Ovtv5I0Y.js → CommandPalette-0zkyC0RY.js} +1 -1
- package/static/app/assets/Library-Dt-vABI2.js +1 -0
- package/static/app/assets/{LivingBrain-CNz-6NSd.js → LivingBrain-Bz-h6LBH.js} +1 -1
- package/static/app/assets/{ProductFlow-ePX-Y73J.js → ProductFlow-DAZ7DJCE.js} +1 -1
- package/static/app/assets/{ReviewCard-C4mpvkwH.js → ReviewCard-Bv0WGbY6.js} +1 -1
- package/static/app/assets/{System-BvWNK1zc.js → System-BhrJQSD_.js} +1 -1
- package/static/app/assets/{activity-CiauPV_3.js → activity-AolpZJCH.js} +1 -1
- package/static/app/assets/{bot-Dwct-Q1x.js → bot-CsVsT2nd.js} +1 -1
- package/static/app/assets/{brain-BKDW1F0T.js → brain-vuppco5y.js} +1 -1
- package/static/app/assets/{button-CCB9Kuw7.js → button-1BP1PJZ3.js} +1 -1
- package/static/app/assets/{circle-pause-DNa8WHC5.js → circle-pause-5paeCOtq.js} +1 -1
- package/static/app/assets/{circle-play-8jmxn5mf.js → circle-play-By8mFYRV.js} +1 -1
- package/static/app/assets/{cpu-mxvF3V1I.js → cpu-DvUQxdKe.js} +1 -1
- package/static/app/assets/{download--SmCcx0p.js → download-CFqYG90a.js} +1 -1
- package/static/app/assets/{folder-open-D4Wy5roo.js → folder-open-CVSnp-Dp.js} +1 -1
- package/static/app/assets/{hard-drive-BwQcSPlS.js → hard-drive-Ckr4Vd6-.js} +1 -1
- package/static/app/assets/{index-DDV2YZwM.js → index--0zKF5Ow.js} +3 -3
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{input-DFhSjmLS.js → input-D-JKquXU.js} +1 -1
- package/static/app/assets/{network-Bts7VO94.js → network-DB88FZ2T.js} +1 -1
- package/static/app/assets/{primitives-BuSMEJY8.js → primitives-D8U9OaIo.js} +1 -1
- package/static/app/assets/search-B8jsVTw_.js +1 -0
- package/static/app/assets/{shield-alert-BfTO6X_G.js → shield-alert-DnsvO79P.js} +1 -1
- package/static/app/assets/{textarea-BjctW1oh.js → textarea-4RchwbTa.js} +1 -1
- package/static/app/assets/{useFocusTrap-CE43-LrA.js → useFocusTrap-CFJjy7cS.js} +1 -1
- package/static/app/assets/{useQuery-Bv3ejLL5.js → useQuery-3Reu-A4a.js} +1 -1
- package/static/app/assets/{users-CsNqLZAj.js → users-BTxYksxa.js} +1 -1
- package/static/app/assets/{utils-KFFdVG_t.js → utils-D02Vf-fw.js} +1 -1
- package/static/app/assets/{workspace-wdCvdyPF.js → workspace-CSJNgX-C.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-CBia2Fvm.js +0 -1
- package/static/app/assets/index-CQWdDU3z.css +0 -2
- package/static/app/assets/search-DZzxhWaQ.js +0 -1
|
@@ -29,7 +29,7 @@ def _local_ips() -> list[str]:
|
|
|
29
29
|
try:
|
|
30
30
|
hostname = socket.gethostname()
|
|
31
31
|
for info in socket.getaddrinfo(hostname, None):
|
|
32
|
-
addr = info[4][0]
|
|
32
|
+
addr = str(info[4][0])
|
|
33
33
|
if ":" not in addr and not addr.startswith("127."):
|
|
34
34
|
if addr not in ips:
|
|
35
35
|
ips.append(addr)
|
package/latticeai/core/agent.py
CHANGED
|
@@ -24,7 +24,17 @@ import json
|
|
|
24
24
|
import logging
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import
|
|
27
|
+
from typing import (
|
|
28
|
+
Any,
|
|
29
|
+
Awaitable,
|
|
30
|
+
Callable,
|
|
31
|
+
Dict,
|
|
32
|
+
FrozenSet,
|
|
33
|
+
List,
|
|
34
|
+
Mapping,
|
|
35
|
+
Optional,
|
|
36
|
+
Tuple,
|
|
37
|
+
)
|
|
28
38
|
|
|
29
39
|
from lattice_brain.runtime.contracts import (
|
|
30
40
|
runtime_boundary_contract,
|
|
@@ -149,10 +159,10 @@ class AgentDeps:
|
|
|
149
159
|
|
|
150
160
|
# ── tool port ────────────────────────────────────────────────────
|
|
151
161
|
execute_tool: Callable[[str, dict], dict]
|
|
152
|
-
policy_for: Callable[[str, dict],
|
|
153
|
-
risk_level: Callable[[
|
|
162
|
+
policy_for: Callable[[str, dict], Mapping[str, Any]] # name, args -> policy
|
|
163
|
+
risk_level: Callable[[Any], str] # policy -> "low"|"medium"|"high"
|
|
154
164
|
check_role: Callable[[str, str], None] # tool_name, user -> raises if not allowed
|
|
155
|
-
tool_governance:
|
|
165
|
+
tool_governance: Mapping[str, Mapping[str, Any]] # name -> policy (auto_approve set)
|
|
156
166
|
file_create_actions: FrozenSet[str]
|
|
157
167
|
|
|
158
168
|
# ── context / memory / audit ports ───────────────────────────────
|
|
@@ -246,18 +256,22 @@ class SingleAgentRuntime:
|
|
|
246
256
|
injected = getattr(self.deps, "phase_budgets", None)
|
|
247
257
|
if injected is not None:
|
|
248
258
|
return injected
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
cached = getattr(self, "_env_phase_budgets", None)
|
|
260
|
+
if cached is None:
|
|
261
|
+
cached = PhaseBudgets.from_env()
|
|
262
|
+
self._env_phase_budgets = cached
|
|
263
|
+
return cached
|
|
252
264
|
|
|
253
265
|
@property
|
|
254
266
|
def transcript_budget(self) -> TranscriptBudget:
|
|
255
267
|
injected = getattr(self.deps, "transcript_budget", None)
|
|
256
268
|
if injected is not None:
|
|
257
269
|
return injected
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
cached = getattr(self, "_env_transcript_budget", None)
|
|
271
|
+
if cached is None:
|
|
272
|
+
cached = TranscriptBudget.from_env()
|
|
273
|
+
self._env_transcript_budget = cached
|
|
274
|
+
return cached
|
|
261
275
|
|
|
262
276
|
# ── permission mode (v9.9.8) ─────────────────────────────────────
|
|
263
277
|
def resolve_permission_mode(
|
|
@@ -511,7 +525,7 @@ class SingleAgentRuntime:
|
|
|
511
525
|
break
|
|
512
526
|
continue
|
|
513
527
|
|
|
514
|
-
name = action.get("action")
|
|
528
|
+
name = str(action.get("action") or "")
|
|
515
529
|
thoughts = str(action.get("thoughts") or "")[:600]
|
|
516
530
|
args = action.get("args") or {}
|
|
517
531
|
|
|
@@ -748,7 +762,7 @@ class SingleAgentRuntime:
|
|
|
748
762
|
|
|
749
763
|
def _governor_review(
|
|
750
764
|
self, ctx: AgentRunContext, name: str, thoughts: str, args: dict,
|
|
751
|
-
policy:
|
|
765
|
+
policy: Mapping[str, Any], risk: str, current_user: str, request_workspace: Optional[str],
|
|
752
766
|
conversation_id: Optional[str] = None,
|
|
753
767
|
) -> Tuple[bool, bool]:
|
|
754
768
|
"""Central change-class governance: create-new runs with minimal
|
|
@@ -817,7 +831,7 @@ class SingleAgentRuntime:
|
|
|
817
831
|
|
|
818
832
|
def _blocked_by_gates(
|
|
819
833
|
self, ctx: AgentRunContext, req: Any, name: str, thoughts: str, args: dict,
|
|
820
|
-
policy:
|
|
834
|
+
policy: Mapping[str, Any], risk: str, current_user: str, governor_allows_additive: bool,
|
|
821
835
|
) -> bool:
|
|
822
836
|
"""Destructive / circuit-breaker / explicit-approval gates.
|
|
823
837
|
|
|
@@ -892,7 +906,7 @@ class SingleAgentRuntime:
|
|
|
892
906
|
|
|
893
907
|
def _dispatch_step(
|
|
894
908
|
self, ctx: AgentRunContext, name: str, thoughts: str, args: dict,
|
|
895
|
-
policy:
|
|
909
|
+
policy: Mapping[str, Any], risk: str, current_user: str,
|
|
896
910
|
) -> None:
|
|
897
911
|
"""Role check + shared tool lifecycle, recorded on the transcript either way."""
|
|
898
912
|
d = self.deps
|
package/latticeai/core/audit.py
CHANGED
|
@@ -87,7 +87,7 @@ def mask_sensitive_text(text: str, matches: List[Dict]) -> str:
|
|
|
87
87
|
|
|
88
88
|
def classify_sensitive_message(item: Dict, index: int) -> Dict:
|
|
89
89
|
content = str(item.get("content", ""))
|
|
90
|
-
found = []
|
|
90
|
+
found: List[Dict[str, Any]] = []
|
|
91
91
|
seen: set = set()
|
|
92
92
|
for rule in SENSITIVE_PATTERNS:
|
|
93
93
|
for match in re.finditer(rule["pattern"], content, flags=re.IGNORECASE):
|
|
@@ -104,9 +104,11 @@ def classify_sensitive_message(item: Dict, index: int) -> Dict:
|
|
|
104
104
|
})
|
|
105
105
|
severity = "none"
|
|
106
106
|
if found:
|
|
107
|
-
severity =
|
|
107
|
+
severity = str(
|
|
108
|
+
max(found, key=lambda m: SEVERITY_SCORE[str(m["severity"])])["severity"]
|
|
109
|
+
)
|
|
108
110
|
preview_text = content[:240]
|
|
109
|
-
preview_matches = [m for m in found if m["start"] < len(preview_text)]
|
|
111
|
+
preview_matches = [m for m in found if int(m["start"]) < len(preview_text)]
|
|
110
112
|
return {
|
|
111
113
|
"index": index,
|
|
112
114
|
"role": item.get("role", ""),
|
|
@@ -34,7 +34,7 @@ import math
|
|
|
34
34
|
import os
|
|
35
35
|
import struct
|
|
36
36
|
from dataclasses import dataclass, field
|
|
37
|
-
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
|
37
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
38
38
|
|
|
39
39
|
from latticeai.core.local_embeddings import DEFAULT_EMBEDDING_DIM, LocalEmbeddingModel
|
|
40
40
|
|
|
@@ -288,6 +288,17 @@ class HashEmbeddingProvider(EmbeddingProvider):
|
|
|
288
288
|
return {"status": "ok", "detail": "deterministic local fallback"}
|
|
289
289
|
|
|
290
290
|
|
|
291
|
+
def _as_float_list(value: Any) -> List[Any]:
|
|
292
|
+
"""A pooled embedding row as a flat list.
|
|
293
|
+
|
|
294
|
+
``mx.array.tolist()`` is typed as scalar-or-nested; at this call site the
|
|
295
|
+
array is always 1-D, so a scalar would be a bug worth surfacing.
|
|
296
|
+
"""
|
|
297
|
+
if isinstance(value, (int, float)):
|
|
298
|
+
raise EmbeddingUnavailable("MLX embedding produced a scalar, not a vector")
|
|
299
|
+
return list(value)
|
|
300
|
+
|
|
301
|
+
|
|
291
302
|
# ── shared base for remote/model-backed providers ─────────────────────────────
|
|
292
303
|
@dataclass
|
|
293
304
|
class _RemoteConfig:
|
|
@@ -332,7 +343,7 @@ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
332
343
|
def __init__(self, cfg: _RemoteConfig):
|
|
333
344
|
super().__init__(cfg)
|
|
334
345
|
self.model_id = f"mlx:{cfg.model}:{self.dim}"
|
|
335
|
-
self._encoder = None
|
|
346
|
+
self._encoder: Optional[Tuple[str, Any, Any]] = None
|
|
336
347
|
|
|
337
348
|
def _load(self):
|
|
338
349
|
if self._encoder is not None:
|
|
@@ -358,7 +369,7 @@ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
358
369
|
result = model(tokens)
|
|
359
370
|
pooled = result[0] if isinstance(result, (tuple, list)) else result
|
|
360
371
|
vec = mx.mean(pooled, axis=1)[0] if pooled.ndim == 3 else pooled[0]
|
|
361
|
-
out.append([float(x) for x in vec.tolist()])
|
|
372
|
+
out.append([float(x) for x in _as_float_list(vec.tolist())])
|
|
362
373
|
return out
|
|
363
374
|
except EmbeddingUnavailable:
|
|
364
375
|
raise
|
|
@@ -477,7 +488,7 @@ class CustomEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
477
488
|
super().__init__(cfg)
|
|
478
489
|
self._target_ref = str(cfg.extra.get("target") or os.getenv("LATTICEAI_EMBEDDING_CUSTOM_TARGET", ""))
|
|
479
490
|
self.model_id = f"custom:{cfg.model or self._target_ref or 'callable'}:{self.dim}"
|
|
480
|
-
self._fn = None
|
|
491
|
+
self._fn: Optional[Callable[..., Any]] = None
|
|
481
492
|
|
|
482
493
|
def _load(self):
|
|
483
494
|
if self._fn is not None:
|
|
@@ -587,8 +598,10 @@ def resolve_embedder(
|
|
|
587
598
|
"""
|
|
588
599
|
requested = str(provider or "hash").strip().lower() or "hash"
|
|
589
600
|
if requested in {"hash", "local", "fallback", ""}:
|
|
590
|
-
|
|
591
|
-
return ResolvedEmbedder(
|
|
601
|
+
hash_prov = HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
|
|
602
|
+
return ResolvedEmbedder(
|
|
603
|
+
hash_prov, "hash", "hash", False, hash_prov.health(), "deterministic local fallback"
|
|
604
|
+
)
|
|
592
605
|
|
|
593
606
|
try:
|
|
594
607
|
prov = build_embedding_provider(
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from copy import deepcopy
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
|
-
MARKETPLACE_VERSION = "10.
|
|
13
|
+
MARKETPLACE_VERSION = "10.4.0"
|
|
14
14
|
TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -242,7 +242,7 @@ class TemplateCatalog:
|
|
|
242
242
|
if not isinstance(payload, dict):
|
|
243
243
|
raise MarketplaceError("template import payload must be an object")
|
|
244
244
|
template = deepcopy(payload.get("template") or payload)
|
|
245
|
-
kind = _normalize_kind(template.get("kind") or payload.get("kind"))
|
|
245
|
+
kind = _normalize_kind(str(template.get("kind") or payload.get("kind") or ""))
|
|
246
246
|
if not template.get("id"):
|
|
247
247
|
raise MarketplaceError("template missing id")
|
|
248
248
|
if not template.get("name"):
|
|
@@ -431,7 +431,7 @@ def fast_postprocess(text: str, profile: Dict[str, Any]) -> str:
|
|
|
431
431
|
SMOKE_PROMPT = "한국어로 한 문장만 답해. 2+2는?"
|
|
432
432
|
|
|
433
433
|
|
|
434
|
-
def classify_smoke_response(text: str) -> Tuple[str, str]:
|
|
434
|
+
def classify_smoke_response(text: Optional[str]) -> Tuple[str, str]:
|
|
435
435
|
"""Smoke test 응답을 ok / degraded / failed 로 분류한다. (item 3-3)
|
|
436
436
|
|
|
437
437
|
- failed: 채팅에 쓸 수 없는 수준 (빈 응답, 특수/role 토큰 누출, 심한 반복,
|
|
@@ -123,7 +123,7 @@ class PluginManifest:
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def validate_manifest(data:
|
|
126
|
+
def validate_manifest(data: Any, *, path: str = "") -> Tuple[Optional[PluginManifest], List[str]]:
|
|
127
127
|
"""Validate a manifest dict. Returns ``(manifest_or_None, errors)``.
|
|
128
128
|
|
|
129
129
|
A manifest with errors still returns ``None`` so callers never accidentally
|
|
@@ -144,7 +144,7 @@ class ProjectSessionStore:
|
|
|
144
144
|
workspace_id: Optional[str] = None,
|
|
145
145
|
) -> Dict[str, Any]:
|
|
146
146
|
session_id = secrets.token_urlsafe(12)
|
|
147
|
-
record = {
|
|
147
|
+
record: Dict[str, Any] = {
|
|
148
148
|
"id": session_id,
|
|
149
149
|
"title": _clean(title, 200) or "프로젝트",
|
|
150
150
|
"goal": _clean(goal, 2000),
|
|
@@ -84,8 +84,10 @@ def _repaired_artifacts(steps: Sequence[Mapping[str, Any]]) -> List[str]:
|
|
|
84
84
|
meta = step.get("content_sanitize")
|
|
85
85
|
if not isinstance(meta, Mapping) or not meta.get("repaired"):
|
|
86
86
|
continue
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
raw_result = step.get("result")
|
|
88
|
+
raw_args = step.get("args")
|
|
89
|
+
result: Mapping[str, Any] = raw_result if isinstance(raw_result, Mapping) else {}
|
|
90
|
+
args: Mapping[str, Any] = raw_args if isinstance(raw_args, Mapping) else {}
|
|
89
91
|
path = result.get("path") or args.get("path")
|
|
90
92
|
if path and str(path) not in paths:
|
|
91
93
|
paths.append(str(path))
|
|
@@ -270,7 +272,7 @@ def explain_run(
|
|
|
270
272
|
repair_sum = sum(repair_totals.values())
|
|
271
273
|
|
|
272
274
|
strain_score = parse_errors * 2 + corrections + retries + repair_totals["format"]
|
|
273
|
-
strain = {
|
|
275
|
+
strain: Dict[str, Any] = {
|
|
274
276
|
"level": _strain_level(strain_score),
|
|
275
277
|
"score": strain_score,
|
|
276
278
|
"parse_errors": parse_errors,
|
|
@@ -336,7 +338,7 @@ def explain_run(
|
|
|
336
338
|
"만들어지지 않은 파일: " + ", ".join(missing_files[:4]),
|
|
337
339
|
"Files that were never written: " + ", ".join(missing_files[:4]),
|
|
338
340
|
))
|
|
339
|
-
strain_phrase = _STRAIN_PHRASES.get(strain["level"])
|
|
341
|
+
strain_phrase = _STRAIN_PHRASES.get(str(strain["level"]))
|
|
340
342
|
if strain_phrase is not None:
|
|
341
343
|
details.append(dict(strain_phrase))
|
|
342
344
|
|
|
@@ -515,10 +515,6 @@ class WorkspaceOSStore:
|
|
|
515
515
|
self.save_state(state)
|
|
516
516
|
self.record_timeline_event("workspace", "member_removed", {"workspace_id": workspace_id, "user_id": user_id})
|
|
517
517
|
return self._workspace_public(ws, actor)
|
|
518
|
-
ws["updated_at"] = _now()
|
|
519
|
-
self.save_state(state)
|
|
520
|
-
self.record_timeline_event("workspace", "member_removed", {"workspace_id": workspace_id, "user_id": user_id})
|
|
521
|
-
return self._workspace_public(ws, actor)
|
|
522
518
|
|
|
523
519
|
def set_active_workspace(self, workspace_id: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
524
520
|
state = self.load_state()
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Dict
|
|
12
12
|
|
|
13
|
-
WORKSPACE_OS_VERSION = "10.
|
|
13
|
+
WORKSPACE_OS_VERSION = "10.4.0"
|
|
14
14
|
|
|
15
15
|
# Workspace types separate single-user Personal workspaces from shared
|
|
16
16
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -71,7 +71,7 @@ def migrate_workspaces(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
71
71
|
for ws_id, ws in workspaces.items():
|
|
72
72
|
if not isinstance(ws, dict):
|
|
73
73
|
continue
|
|
74
|
-
ws_type = ws.get("type") if ws.get("type") in WORKSPACE_TYPES else "organization"
|
|
74
|
+
ws_type = str(ws.get("type")) if ws.get("type") in WORKSPACE_TYPES else "organization"
|
|
75
75
|
if ws_id == DEFAULT_WORKSPACE_ID:
|
|
76
76
|
ws_type = "personal"
|
|
77
77
|
base = new_workspace_record(
|
|
@@ -31,9 +31,9 @@ class WorkspaceSnapshots:
|
|
|
31
31
|
workspace_id: Optional[str] = None,
|
|
32
32
|
) -> Dict[str, Any]:
|
|
33
33
|
scope = self._store._resolve_scope(workspace_id)
|
|
34
|
-
graph_payload = {"nodes": [], "edges": []}
|
|
35
|
-
graph_stats = {}
|
|
36
|
-
local_sources = {"sources": []}
|
|
34
|
+
graph_payload: Dict[str, Any] = {"nodes": [], "edges": []}
|
|
35
|
+
graph_stats: Dict[str, Any] = {}
|
|
36
|
+
local_sources: Dict[str, Any] = {"sources": []}
|
|
37
37
|
if graph is not None:
|
|
38
38
|
graph_payload = graph.graph(limit=2000)
|
|
39
39
|
graph_stats = graph.stats()
|
|
@@ -7,6 +7,7 @@ import socket
|
|
|
7
7
|
import tempfile
|
|
8
8
|
import zipfile
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict
|
|
10
11
|
|
|
11
12
|
import httpx
|
|
12
13
|
|
|
@@ -678,6 +679,50 @@ def collect_generated_files(agent_data):
|
|
|
678
679
|
files.append((path, target))
|
|
679
680
|
return files
|
|
680
681
|
|
|
682
|
+
def format_artifact_card(data, *, language: str = "ko") -> str:
|
|
683
|
+
"""Artifact card summary for a finished run (v10.4.0).
|
|
684
|
+
|
|
685
|
+
Telegram used to send the produced files and nothing else, so a
|
|
686
|
+
deterministically repaired scaffold arrived looking exactly like clean
|
|
687
|
+
model output — SURFACE_PARITY recorded that as ◐. This renders the same
|
|
688
|
+
``artifacts[]`` flags the web cards show, and reports only what the server
|
|
689
|
+
said: an absent flag is never promoted into a claim of validity.
|
|
690
|
+
"""
|
|
691
|
+
if not isinstance(data, dict):
|
|
692
|
+
return ""
|
|
693
|
+
artifacts = data.get("artifacts")
|
|
694
|
+
if not isinstance(artifacts, list) or not artifacts:
|
|
695
|
+
return ""
|
|
696
|
+
korean = not str(language or "ko").startswith("en")
|
|
697
|
+
lines = ["📄 만든 파일" if korean else "📄 Files produced"]
|
|
698
|
+
for item in artifacts[:8]:
|
|
699
|
+
if not isinstance(item, dict):
|
|
700
|
+
continue
|
|
701
|
+
path = str(item.get("path") or "").strip()
|
|
702
|
+
if not path:
|
|
703
|
+
continue
|
|
704
|
+
notes = []
|
|
705
|
+
size = item.get("bytes")
|
|
706
|
+
if isinstance(size, (int, float)) and size > 0:
|
|
707
|
+
notes.append(f"{int(size):,} B")
|
|
708
|
+
if item.get("repaired") is True:
|
|
709
|
+
notes.append("자동 보정됨" if korean else "auto-repaired")
|
|
710
|
+
if item.get("valid") is False:
|
|
711
|
+
notes.append("검증 실패" if korean else "failed validation")
|
|
712
|
+
suffix = f" ({' · '.join(notes)})" if notes else ""
|
|
713
|
+
lines.append(f"• {path}{suffix}")
|
|
714
|
+
extra = len(artifacts) - 8
|
|
715
|
+
if extra > 0:
|
|
716
|
+
lines.append(f"… +{extra}")
|
|
717
|
+
return "\n".join(lines) if len(lines) > 1 else ""
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
async def send_artifact_card(client, chat_id, data, *, language: str = "ko") -> None:
|
|
721
|
+
text = format_artifact_card(data, language=language)
|
|
722
|
+
if text:
|
|
723
|
+
await send_message(client, chat_id, text)
|
|
724
|
+
|
|
725
|
+
|
|
681
726
|
def format_grounding_badge(data, *, language: str = "ko") -> str:
|
|
682
727
|
"""Answer-grounding badge line for a `/chat` reply (v9.9.7).
|
|
683
728
|
|
|
@@ -732,8 +777,10 @@ def format_proposals(payload) -> list:
|
|
|
732
777
|
item_id = str(raw.get("id") or "").strip()
|
|
733
778
|
if not item_id:
|
|
734
779
|
continue
|
|
735
|
-
|
|
736
|
-
|
|
780
|
+
raw_body = raw.get("payload")
|
|
781
|
+
raw_prov = raw.get("provenance")
|
|
782
|
+
body: Dict[str, Any] = raw_body if isinstance(raw_body, dict) else {}
|
|
783
|
+
provenance: Dict[str, Any] = raw_prov if isinstance(raw_prov, dict) else {}
|
|
737
784
|
path = str(body.get("path") or provenance.get("path") or "")
|
|
738
785
|
title = str(raw.get("title") or path or item_id)
|
|
739
786
|
change_class = str(body.get("change_class") or provenance.get("change_class") or "")
|
|
@@ -969,7 +1016,7 @@ async def handle_plan_callback(client, chat_id, data: str) -> None:
|
|
|
969
1016
|
# A callback is bound to the chat that received the plan. Even two allowed
|
|
970
1017
|
# chats cannot approve or cancel each other's plan by replaying callback
|
|
971
1018
|
# data.
|
|
972
|
-
if pending and int(pending.get("chat_id")) != int(chat_id):
|
|
1019
|
+
if pending and int(pending.get("chat_id") or 0) != int(chat_id):
|
|
973
1020
|
await send_message(client, chat_id, "❌ 다른 채팅의 작업은 처리할 수 없습니다.")
|
|
974
1021
|
return
|
|
975
1022
|
pending = _bot_pending_plans.pop(pause_id, None)
|
|
@@ -1003,6 +1050,7 @@ async def handle_plan_callback(client, chat_id, data: str) -> None:
|
|
|
1003
1050
|
await send_message(client, chat_id, str(ans))
|
|
1004
1051
|
if isinstance(data_r, dict):
|
|
1005
1052
|
await send_run_explanation(client, chat_id, data_r)
|
|
1053
|
+
await send_artifact_card(client, chat_id, data_r)
|
|
1006
1054
|
await send_generated_files(client, chat_id, collect_generated_files(data_r))
|
|
1007
1055
|
await send_preview_links(client, chat_id, collect_preview_urls(data_r))
|
|
1008
1056
|
except Exception as e:
|
|
@@ -1052,6 +1100,7 @@ async def process_ai_request(client, chat_id, user_text, image_data=None):
|
|
|
1052
1100
|
await send_grounding_badge(client, chat_id, data)
|
|
1053
1101
|
if not image_data and isinstance(data, dict):
|
|
1054
1102
|
await send_run_explanation(client, chat_id, data)
|
|
1103
|
+
await send_artifact_card(client, chat_id, data)
|
|
1055
1104
|
await send_generated_files(client, chat_id, collect_generated_files(data))
|
|
1056
1105
|
await send_preview_links(client, chat_id, collect_preview_urls(data))
|
|
1057
1106
|
except Exception as e:
|