ltcai 0.3.0 → 0.3.2
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 +47 -7
- package/docs/CHANGELOG.md +117 -0
- package/knowledge_graph_api.py +10 -2
- package/latticeai/api/security_dashboard.py +584 -0
- package/latticeai/core/audit.py +4 -1
- package/latticeai/core/graph_curator.py +473 -0
- package/latticeai/core/model_compat.py +450 -0
- package/latticeai/core/model_resolution.py +227 -0
- package/latticeai/core/timezones.py +80 -0
- package/package.json +2 -2
- package/server.py +265 -16
- package/static/account.html +2 -2
- package/static/admin.html +75 -1
- package/static/chat.html +2 -2
- package/static/graph.html +2 -2
- package/static/lattice-reference.css +82 -50
- package/static/scripts/account.js +10 -2
- package/static/scripts/admin.js +296 -0
- package/static/scripts/chat.js +173 -11
- package/static/scripts/graph.js +6 -2
- package/static/sw.js +1 -1
package/static/scripts/chat.js
CHANGED
|
@@ -11,6 +11,8 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
11
11
|
let currentUserNickname = "Guest";
|
|
12
12
|
let currentUserEmail = "";
|
|
13
13
|
let isAdmin = false;
|
|
14
|
+
let telegramHistorySyncEnabled = false;
|
|
15
|
+
let telegramHistorySyncInFlight = false;
|
|
14
16
|
|
|
15
17
|
// ── 멀티 LLM 파이프라인 상태 ──
|
|
16
18
|
let pipelineConfig = { planning: null, executing: null, reviewing: null };
|
|
@@ -517,6 +519,32 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
517
519
|
localStorage.setItem(scopedStorageKey('onboarding_complete'), 'true');
|
|
518
520
|
}
|
|
519
521
|
|
|
522
|
+
function consumeSetupAfterLoginFlag() {
|
|
523
|
+
try {
|
|
524
|
+
const params = new URLSearchParams(window.location.search);
|
|
525
|
+
const value = params.get('setup') === '1' || sessionStorage.getItem('ltcai_force_setup_after_login') === 'true';
|
|
526
|
+
sessionStorage.removeItem('ltcai_force_setup_after_login');
|
|
527
|
+
if (params.get('setup') === '1') {
|
|
528
|
+
const cleanUrl = `${window.location.pathname}${window.location.hash || ''}`;
|
|
529
|
+
window.history.replaceState({}, '', cleanUrl);
|
|
530
|
+
}
|
|
531
|
+
return value;
|
|
532
|
+
} catch (_) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function modelLoadedForChat() {
|
|
538
|
+
try {
|
|
539
|
+
const res = await apiFetch('/health');
|
|
540
|
+
if (!res.ok) return false;
|
|
541
|
+
const data = await res.json();
|
|
542
|
+
return Boolean(data.current_model || (data.loaded_models || []).length);
|
|
543
|
+
} catch (_) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
520
548
|
function workspaceLabel(kind) {
|
|
521
549
|
if (kind === 'company') return currentLang === 'ko' ? '회사 워크스페이스' : 'Company Workspace';
|
|
522
550
|
return currentLang === 'ko' ? '개인 워크스페이스' : 'Personal Workspace';
|
|
@@ -799,14 +827,20 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
799
827
|
let onboardingRecs = null;
|
|
800
828
|
let onboardingSelectedModel = null;
|
|
801
829
|
|
|
802
|
-
function startOnboardingIfNeeded(force = false) {
|
|
830
|
+
async function startOnboardingIfNeeded(force = false) {
|
|
803
831
|
updateWorkspaceModeUi();
|
|
804
|
-
|
|
832
|
+
const forceAfterLogin = consumeSetupAfterLoginFlag();
|
|
833
|
+
const hasLoadedModel = await modelLoadedForChat();
|
|
834
|
+
if (!force && !forceAfterLogin && onboardingComplete() && hasLoadedModel) {
|
|
805
835
|
document.getElementById('onboarding-overlay')?.classList.remove('open');
|
|
806
836
|
return;
|
|
807
837
|
}
|
|
808
838
|
document.getElementById('onboarding-overlay')?.classList.add('open');
|
|
809
|
-
|
|
839
|
+
if (forceAfterLogin || !hasLoadedModel) {
|
|
840
|
+
renderPcAnalysis();
|
|
841
|
+
} else {
|
|
842
|
+
renderOnboardingWorkspace();
|
|
843
|
+
}
|
|
810
844
|
}
|
|
811
845
|
|
|
812
846
|
function renderOnboardingShell(stepIndex, bodyHtml, actionsHtml = '') {
|
|
@@ -1260,7 +1294,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1260
1294
|
selectMode(mode);
|
|
1261
1295
|
setOnboardingComplete();
|
|
1262
1296
|
document.getElementById('onboarding-overlay')?.classList.remove('open');
|
|
1263
|
-
|
|
1297
|
+
showChat();
|
|
1264
1298
|
}
|
|
1265
1299
|
|
|
1266
1300
|
function switchAcctTab(tab) {
|
|
@@ -1435,7 +1469,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1435
1469
|
const icon = isUnavailable ? 'ti-lock' : (engineMissing || needsPull) ? 'ti-cloud-download' : verifyUnknown ? 'ti-activity' : 'ti-switch-3';
|
|
1436
1470
|
const cls = (engineMissing || needsPull) && isLocalEngine ? ' needs-pull' : '';
|
|
1437
1471
|
const action = isLocalEngine
|
|
1438
|
-
? `
|
|
1472
|
+
? `selectModelByCard('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`
|
|
1439
1473
|
: `loadSelectedModel('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`;
|
|
1440
1474
|
return `
|
|
1441
1475
|
<button class="model-option${cls}" ${isUnavailable ? 'disabled' : ''} onclick="${action}">
|
|
@@ -1635,7 +1669,17 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1635
1669
|
if (!res.ok) throw new Error(data.detail || '모델 로드에 실패했습니다.');
|
|
1636
1670
|
closeModelPanel();
|
|
1637
1671
|
await loadModelStatus();
|
|
1638
|
-
|
|
1672
|
+
// 피드백 #1/#2: 클라우드 경로도 백엔드 current를 단일 진실원으로 사용한다.
|
|
1673
|
+
const actualCurrent = resolveActualCurrent(data, modelId);
|
|
1674
|
+
setCurrentModel(actualCurrent);
|
|
1675
|
+
updateCurrentModelUI(actualCurrent);
|
|
1676
|
+
let statusLine = `모델을 <b>${escapeHtml(compactModelName(actualCurrent))}</b>로 전환했습니다.`;
|
|
1677
|
+
const compat = describeCompatibility(data);
|
|
1678
|
+
if (compat) {
|
|
1679
|
+
statusLine += `<br><span class="sensitivity-preview">${escapeHtml(compat.message)}</span>`;
|
|
1680
|
+
showModelCompatibilityWarning(data);
|
|
1681
|
+
}
|
|
1682
|
+
addMessage('ai', statusLine);
|
|
1639
1683
|
} catch (e) {
|
|
1640
1684
|
document.getElementById('model-list').innerHTML = `
|
|
1641
1685
|
<div class="sensitivity-preview">${escapeHtml(e.message)}</div>
|
|
@@ -1795,6 +1839,66 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1795
1839
|
if (buffer.trim()) dispatchBlock(buffer.trim());
|
|
1796
1840
|
}
|
|
1797
1841
|
|
|
1842
|
+
// 피드백 #1/#2: "사용자가 보는 현재 모델" === "실제로 채팅에 사용되는 모델".
|
|
1843
|
+
// 백엔드가 돌려준 current/resolution을 단일 진실원으로 사용한다.
|
|
1844
|
+
function resolveActualCurrent(finalData, fallbackId) {
|
|
1845
|
+
if (!finalData) return fallbackId || '';
|
|
1846
|
+
return (
|
|
1847
|
+
finalData.current
|
|
1848
|
+
|| (finalData.resolution && (finalData.resolution.expected_current || finalData.resolution.resolved_model))
|
|
1849
|
+
|| fallbackId
|
|
1850
|
+
|| ''
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function setCurrentModel(modelId) {
|
|
1855
|
+
if (!modelId) return;
|
|
1856
|
+
window.__latticeActiveModel = modelId;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function updateCurrentModelUI(modelId) {
|
|
1860
|
+
if (!modelId) return;
|
|
1861
|
+
const modelEl = document.getElementById('ops-model');
|
|
1862
|
+
if (modelEl) {
|
|
1863
|
+
modelEl.textContent = compactModelName(modelId);
|
|
1864
|
+
modelEl.title = modelId;
|
|
1865
|
+
}
|
|
1866
|
+
const metaEl = document.getElementById('ops-model-meta');
|
|
1867
|
+
if (metaEl && !metaEl.dataset.loaded) {
|
|
1868
|
+
metaEl.dataset.loaded = 'true';
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function describeCompatibility(finalData) {
|
|
1873
|
+
if (!finalData) return null;
|
|
1874
|
+
if (finalData.ready_to_chat === false) {
|
|
1875
|
+
const reason = (finalData.smoke_test && finalData.smoke_test.reason) || '채팅 호환성 검사 실패';
|
|
1876
|
+
return {
|
|
1877
|
+
severity: 'degraded',
|
|
1878
|
+
message: `⚠️ 채팅 호환성이 낮습니다 (${reason}). 다른 실행 엔진을 추천합니다.`,
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
if (finalData.compatibility_status === 'degraded') {
|
|
1882
|
+
return {
|
|
1883
|
+
severity: 'degraded',
|
|
1884
|
+
message: '⚠️ 모델은 로드됐지만 호환성 테스트가 degraded로 나왔습니다. 답변 품질이 일정하지 않을 수 있어요.',
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
if (finalData.compatibility_status === 'unknown') {
|
|
1888
|
+
return {
|
|
1889
|
+
severity: 'unknown',
|
|
1890
|
+
message: '호환성 테스트를 완료하지 못했습니다. 채팅이 가능하지만 답변 품질이 일정하지 않을 수 있어요.',
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
return null;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function showModelCompatibilityWarning(finalData) {
|
|
1897
|
+
const info = describeCompatibility(finalData);
|
|
1898
|
+
if (!info) return;
|
|
1899
|
+
try { showToast(info.message); } catch (_) {}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1798
1902
|
async function prepareAndLoadModel(encodedId, engine = '') {
|
|
1799
1903
|
const modelId = decodeURIComponent(encodedId);
|
|
1800
1904
|
const displayName = compactModelName(modelId);
|
|
@@ -1836,12 +1940,22 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1836
1940
|
if (!finalData) throw new Error('모델 준비 응답이 비어 있습니다.');
|
|
1837
1941
|
closeModelPanel();
|
|
1838
1942
|
await loadModelStatus();
|
|
1839
|
-
|
|
1943
|
+
const actualCurrent = resolveActualCurrent(finalData, modelId);
|
|
1944
|
+
setCurrentModel(actualCurrent);
|
|
1945
|
+
updateCurrentModelUI(actualCurrent);
|
|
1946
|
+
let statusLine = `<b>${escapeHtml(compactModelName(actualCurrent))}</b> 로드 되었습니다.`;
|
|
1947
|
+
const compat = describeCompatibility(finalData);
|
|
1948
|
+
if (compat) {
|
|
1949
|
+
statusLine += `<br><span class="sensitivity-preview">${escapeHtml(compat.message)}</span>`;
|
|
1950
|
+
}
|
|
1951
|
+
addMessage('ai', statusLine);
|
|
1952
|
+
return finalData;
|
|
1840
1953
|
} catch (e) {
|
|
1841
1954
|
document.getElementById('model-list').innerHTML = `
|
|
1842
1955
|
<div class="sensitivity-preview">${escapeHtml(e.message)}</div>
|
|
1843
1956
|
<button class="admin-action" onclick="openModelPanel()" style="margin-top: 12px;">목록으로 돌아가기</button>
|
|
1844
1957
|
`;
|
|
1958
|
+
throw e;
|
|
1845
1959
|
}
|
|
1846
1960
|
}
|
|
1847
1961
|
|
|
@@ -1849,6 +1963,38 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1849
1963
|
return prepareAndLoadModel(encodedId, engine);
|
|
1850
1964
|
}
|
|
1851
1965
|
|
|
1966
|
+
// 피드백 #1/#2: 모델 카드 클릭 → prepare/load → smoke test → current 반영 → 채팅 가능 여부 표시
|
|
1967
|
+
// 가 하나의 흐름으로 이어지도록 한다. encodedId/engine 또는 card 객체 양쪽 모두 받는다.
|
|
1968
|
+
async function selectModelByCard(modelIdOrCard, engineArg) {
|
|
1969
|
+
let encoded;
|
|
1970
|
+
let engine = engineArg || '';
|
|
1971
|
+
if (typeof modelIdOrCard === 'string') {
|
|
1972
|
+
encoded = modelIdOrCard.includes('%') ? modelIdOrCard : encodeURIComponent(modelIdOrCard);
|
|
1973
|
+
} else if (modelIdOrCard && modelIdOrCard.id) {
|
|
1974
|
+
encoded = encodeURIComponent(modelIdOrCard.id);
|
|
1975
|
+
if (!engine) {
|
|
1976
|
+
engine = modelIdOrCard.engine
|
|
1977
|
+
|| (Array.isArray(modelIdOrCard.engine_options) && modelIdOrCard.engine_options[0]?.engine)
|
|
1978
|
+
|| '';
|
|
1979
|
+
}
|
|
1980
|
+
} else {
|
|
1981
|
+
throw new Error('모델 카드가 비어 있습니다.');
|
|
1982
|
+
}
|
|
1983
|
+
const result = await prepareAndLoadModel(encoded, engine);
|
|
1984
|
+
if (result && result.current) {
|
|
1985
|
+
setCurrentModel(result.current);
|
|
1986
|
+
updateCurrentModelUI(result.current);
|
|
1987
|
+
}
|
|
1988
|
+
if (result && (result.ready_to_chat === false || result.compatibility_status === 'degraded')) {
|
|
1989
|
+
showModelCompatibilityWarning(result);
|
|
1990
|
+
}
|
|
1991
|
+
return result;
|
|
1992
|
+
}
|
|
1993
|
+
if (typeof window !== 'undefined') {
|
|
1994
|
+
window.selectModelByCard = selectModelByCard;
|
|
1995
|
+
window.prepareAndLoadModel = prepareAndLoadModel;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1852
1998
|
function fillVpcForm(config) {
|
|
1853
1999
|
if (!config) return;
|
|
1854
2000
|
document.getElementById('vpc-provider').value = config.provider || '';
|
|
@@ -1938,7 +2084,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1938
2084
|
if (!t) {
|
|
1939
2085
|
t = document.createElement('div');
|
|
1940
2086
|
t.id = 'ltcai-toast';
|
|
1941
|
-
t.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background
|
|
2087
|
+
t.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:rgba(255,255,255,0.96);color:#14162c;border:1px solid rgba(111,66,232,0.16);border-radius:10px;padding:10px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 8px 24px rgba(88,72,150,0.16);pointer-events:none;transition:opacity .2s;';
|
|
1942
2088
|
document.body.appendChild(t);
|
|
1943
2089
|
}
|
|
1944
2090
|
t.textContent = msg;
|
|
@@ -3264,10 +3410,13 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
3264
3410
|
}
|
|
3265
3411
|
|
|
3266
3412
|
async function syncTelegramHistory() {
|
|
3413
|
+
if (!telegramHistorySyncEnabled || telegramHistorySyncInFlight) return;
|
|
3414
|
+
telegramHistorySyncInFlight = true;
|
|
3267
3415
|
try {
|
|
3268
3416
|
const res = await apiFetch('/history');
|
|
3269
3417
|
if (!res.ok) return;
|
|
3270
3418
|
const history = await res.json();
|
|
3419
|
+
let added = false;
|
|
3271
3420
|
for (const item of history) {
|
|
3272
3421
|
if (item.source !== 'telegram') continue;
|
|
3273
3422
|
const key = `${item.timestamp || ''}:${item.role}:${item.content}`;
|
|
@@ -3278,9 +3427,13 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
3278
3427
|
? 'Lattice AI'
|
|
3279
3428
|
: (item.user_nickname || 'Telegram');
|
|
3280
3429
|
addMessage(role, item.content || '', null, sender);
|
|
3430
|
+
added = true;
|
|
3281
3431
|
}
|
|
3282
|
-
loadHistory();
|
|
3432
|
+
if (added) loadHistory();
|
|
3283
3433
|
} catch (e) { }
|
|
3434
|
+
finally {
|
|
3435
|
+
telegramHistorySyncInFlight = false;
|
|
3436
|
+
}
|
|
3284
3437
|
}
|
|
3285
3438
|
|
|
3286
3439
|
async function sendToAgent(text, extraCtx = '') {
|
|
@@ -3866,10 +4019,19 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
3866
4019
|
const res = await apiFetch('/runtime_features');
|
|
3867
4020
|
if (res.ok) {
|
|
3868
4021
|
const f = await res.json();
|
|
4022
|
+
telegramHistorySyncEnabled = Boolean(f.telegram_enabled);
|
|
3869
4023
|
if (!f.graph_enabled) {
|
|
3870
4024
|
const btn = document.getElementById('data-graph-btn');
|
|
3871
4025
|
if (btn) btn.style.display = 'none';
|
|
3872
4026
|
}
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
} catch (_) {}
|
|
4030
|
+
try {
|
|
4031
|
+
const res = await apiFetch('/health');
|
|
4032
|
+
if (res.ok) {
|
|
4033
|
+
const data = await res.json();
|
|
4034
|
+
telegramHistorySyncEnabled = Boolean(data.features?.telegram_enabled);
|
|
3873
4035
|
}
|
|
3874
4036
|
} catch (_) {}
|
|
3875
4037
|
})();
|
|
@@ -3878,7 +4040,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
3878
4040
|
loadVpcStatus();
|
|
3879
4041
|
restoreCurrentConversation();
|
|
3880
4042
|
syncTelegramHistory();
|
|
3881
|
-
setInterval(syncTelegramHistory,
|
|
4043
|
+
setInterval(syncTelegramHistory, 15000);
|
|
3882
4044
|
|
|
3883
4045
|
// ── 내 컴퓨터 ──────────────────────────────────────────────────
|
|
3884
4046
|
let cuAgentRunning = false;
|
|
@@ -4370,7 +4532,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
4370
4532
|
function _showComplete() {
|
|
4371
4533
|
_subtitle('설정 완료!');
|
|
4372
4534
|
_footInfo('');
|
|
4373
|
-
_footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus()">완료 ✓</button>`);
|
|
4535
|
+
_footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus();showChat()">완료 ✓</button>`);
|
|
4374
4536
|
}
|
|
4375
4537
|
|
|
4376
4538
|
// ── MCP 관리 모달 ────────────────────────────────────────────────────────
|
package/static/scripts/graph.js
CHANGED
|
@@ -324,8 +324,12 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
324
324
|
</div>
|
|
325
325
|
${rootRows ? `<div class="local-root-list">${rootRows}</div>` : ''}
|
|
326
326
|
<div class="local-option-row">
|
|
327
|
-
<
|
|
328
|
-
|
|
327
|
+
<button type="button" class="local-option-btn ${localState.includeOcr ? 'active' : ''}" onclick="setLocalOption('includeOcr', ${localState.includeOcr ? 'false' : 'true'})" aria-pressed="${localState.includeOcr ? 'true' : 'false'}">
|
|
328
|
+
<i class="ti ti-photo-scan"></i><span>${t('local_ocr')}</span>
|
|
329
|
+
</button>
|
|
330
|
+
<button type="button" class="local-option-btn ${localState.watchEnabled ? 'active' : ''}" onclick="setLocalOption('watchEnabled', ${localState.watchEnabled ? 'false' : 'true'})" aria-pressed="${localState.watchEnabled ? 'true' : 'false'}">
|
|
331
|
+
<i class="ti ti-refresh-dot"></i><span>${t('local_watch')}</span>
|
|
332
|
+
</button>
|
|
329
333
|
</div>
|
|
330
334
|
<div class="local-source-actions">
|
|
331
335
|
<button class="local-source-btn" ${localState.busy ? 'disabled' : ''} onclick="runLocalTree()" title="${t('local_tree')}"><i class="ti ti-folders"></i>${t('local_tree')}</button>
|
package/static/sw.js
CHANGED