feat(assistant): implement spec 021 chat assistant flow with semantic contracts
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
# Lazy loading of route modules to avoid import issues in tests
|
||||
# This allows tests to import routes without triggering all module imports
|
||||
|
||||
__all__ = ['plugins', 'tasks', 'settings', 'connections', 'environments', 'mappings', 'migration', 'git', 'storage', 'admin', 'reports']
|
||||
|
||||
def __getattr__(name):
|
||||
if name in __all__:
|
||||
import importlib
|
||||
return importlib.import_module(f".{name}", __name__)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
# [DEF:backend.src.api.routes.__init__:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: routes, lazy-import, module-registry
|
||||
# @PURPOSE: Provide lazy route module loading to avoid heavyweight imports during tests.
|
||||
# @LAYER: API
|
||||
# @RELATION: DEPENDS_ON -> importlib
|
||||
# @INVARIANT: Only names listed in __all__ are importable via __getattr__.
|
||||
|
||||
__all__ = ['plugins', 'tasks', 'settings', 'connections', 'environments', 'mappings', 'migration', 'git', 'storage', 'admin', 'reports', 'assistant']
|
||||
|
||||
|
||||
# [DEF:__getattr__:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Lazily import route module by attribute name.
|
||||
# @PRE: name is module candidate exposed in __all__.
|
||||
# @POST: Returns imported submodule or raises AttributeError.
|
||||
def __getattr__(name):
|
||||
if name in __all__:
|
||||
import importlib
|
||||
return importlib.import_module(f".{name}", __name__)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
# [/DEF:__getattr__:Function]
|
||||
# [/DEF:backend.src.api.routes.__init__:Module]
|
||||
|
||||
371
backend/src/api/routes/__tests__/test_assistant_api.py
Normal file
371
backend/src/api/routes/__tests__/test_assistant_api.py
Normal file
@@ -0,0 +1,371 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, assistant, api, confirmation, status
|
||||
# @PURPOSE: Validate assistant API endpoint logic via direct async handler invocation.
|
||||
# @LAYER: UI (API Tests)
|
||||
# @RELATION: DEPENDS_ON -> backend.src.api.routes.assistant
|
||||
# @INVARIANT: Every test clears assistant in-memory state before execution.
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Force isolated sqlite databases for test module before dependencies import.
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_api.db")
|
||||
os.environ.setdefault("TASKS_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_tasks.db")
|
||||
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_auth.db")
|
||||
|
||||
from src.api.routes import assistant as assistant_module
|
||||
from src.models.assistant import (
|
||||
AssistantAuditRecord,
|
||||
AssistantConfirmationRecord,
|
||||
AssistantMessageRecord,
|
||||
)
|
||||
|
||||
|
||||
# [DEF:_run_async:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Execute async endpoint handler in synchronous test context.
|
||||
# @PRE: coroutine is awaitable endpoint invocation.
|
||||
# @POST: Returns coroutine result or raises propagated exception.
|
||||
def _run_async(coroutine):
|
||||
return asyncio.run(coroutine)
|
||||
|
||||
|
||||
# [/DEF:_run_async:Function]
|
||||
# [DEF:_FakeTask:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Lightweight task stub used by assistant API tests.
|
||||
class _FakeTask:
|
||||
def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
|
||||
self.id = task_id
|
||||
self.status = status
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
# [/DEF:_FakeTask:Class]
|
||||
# [DEF:_FakeTaskManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Minimal async-compatible TaskManager fixture for deterministic test flows.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self):
|
||||
self._created = []
|
||||
|
||||
async def create_task(self, plugin_id, params, user_id=None):
|
||||
task_id = f"task-{len(self._created) + 1}"
|
||||
task = _FakeTask(task_id=task_id, status="RUNNING", user_id=user_id)
|
||||
self._created.append((plugin_id, params, user_id, task))
|
||||
return task
|
||||
|
||||
def get_task(self, task_id):
|
||||
for _, _, _, task in self._created:
|
||||
if task.id == task_id:
|
||||
return task
|
||||
return None
|
||||
|
||||
def get_tasks(self, limit=20, offset=0):
|
||||
return [x[3] for x in self._created][offset : offset + limit]
|
||||
|
||||
|
||||
# [/DEF:_FakeTaskManager:Class]
|
||||
# [DEF:_FakeConfigManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Environment config fixture with dev/prod aliases for parser tests.
|
||||
class _FakeConfigManager:
|
||||
def get_environments(self):
|
||||
return [
|
||||
SimpleNamespace(id="dev", name="Development"),
|
||||
SimpleNamespace(id="prod", name="Production"),
|
||||
]
|
||||
|
||||
|
||||
# [/DEF:_FakeConfigManager:Class]
|
||||
# [DEF:_admin_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build admin principal fixture.
|
||||
# @PRE: Test harness requires authenticated admin-like principal object.
|
||||
# @POST: Returns user stub with Admin role.
|
||||
def _admin_user():
|
||||
role = SimpleNamespace(name="Admin", permissions=[])
|
||||
return SimpleNamespace(id="u-admin", username="admin", roles=[role])
|
||||
|
||||
|
||||
# [/DEF:_admin_user:Function]
|
||||
# [DEF:_limited_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build non-admin principal fixture.
|
||||
# @PRE: Test harness requires restricted principal for deny scenarios.
|
||||
# @POST: Returns user stub without admin privileges.
|
||||
def _limited_user():
|
||||
role = SimpleNamespace(name="Operator", permissions=[])
|
||||
return SimpleNamespace(id="u-limited", username="limited", roles=[role])
|
||||
|
||||
|
||||
# [/DEF:_limited_user:Function]
|
||||
# [DEF:_FakeQuery:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Minimal chainable query object for fake SQLAlchemy-like DB behavior in tests.
|
||||
class _FakeQuery:
|
||||
def __init__(self, rows):
|
||||
self._rows = list(rows)
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return list(self._rows)
|
||||
|
||||
def count(self):
|
||||
return len(self._rows)
|
||||
|
||||
def offset(self, offset):
|
||||
self._rows = self._rows[offset:]
|
||||
return self
|
||||
|
||||
def limit(self, limit):
|
||||
self._rows = self._rows[:limit]
|
||||
return self
|
||||
|
||||
|
||||
# [/DEF:_FakeQuery:Class]
|
||||
# [DEF:_FakeDb:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: In-memory fake database implementing subset of Session interface used by assistant routes.
|
||||
class _FakeDb:
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
self._confirmations = []
|
||||
self._audit = []
|
||||
|
||||
def add(self, row):
|
||||
table = getattr(row, "__tablename__", "")
|
||||
if table == "assistant_messages":
|
||||
self._messages.append(row)
|
||||
return
|
||||
if table == "assistant_confirmations":
|
||||
self._confirmations.append(row)
|
||||
return
|
||||
if table == "assistant_audit":
|
||||
self._audit.append(row)
|
||||
|
||||
def merge(self, row):
|
||||
table = getattr(row, "__tablename__", "")
|
||||
if table != "assistant_confirmations":
|
||||
self.add(row)
|
||||
return row
|
||||
|
||||
for i, existing in enumerate(self._confirmations):
|
||||
if getattr(existing, "id", None) == getattr(row, "id", None):
|
||||
self._confirmations[i] = row
|
||||
return row
|
||||
self._confirmations.append(row)
|
||||
return row
|
||||
|
||||
def query(self, model):
|
||||
if model is AssistantMessageRecord:
|
||||
return _FakeQuery(self._messages)
|
||||
if model is AssistantConfirmationRecord:
|
||||
return _FakeQuery(self._confirmations)
|
||||
if model is AssistantAuditRecord:
|
||||
return _FakeQuery(self._audit)
|
||||
return _FakeQuery([])
|
||||
|
||||
def commit(self):
|
||||
return None
|
||||
|
||||
def rollback(self):
|
||||
return None
|
||||
|
||||
|
||||
# [/DEF:_FakeDb:Class]
|
||||
# [DEF:_clear_assistant_state:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Reset in-memory assistant registries for isolation between tests.
|
||||
# @PRE: Assistant module globals may contain residues from previous test runs.
|
||||
# @POST: In-memory conversation/confirmation/audit dictionaries are empty.
|
||||
def _clear_assistant_state():
|
||||
assistant_module.CONVERSATIONS.clear()
|
||||
assistant_module.USER_ACTIVE_CONVERSATION.clear()
|
||||
assistant_module.CONFIRMATIONS.clear()
|
||||
assistant_module.ASSISTANT_AUDIT.clear()
|
||||
|
||||
|
||||
# [/DEF:_clear_assistant_state:Function]
|
||||
# [DEF:test_unknown_command_returns_needs_clarification:Function]
|
||||
# @PURPOSE: Unknown command should return clarification state and unknown intent.
|
||||
# @PRE: Fake dependencies provide admin user and deterministic task/config/db services.
|
||||
# @POST: Response state is needs_clarification and no execution side-effect occurs.
|
||||
def test_unknown_command_returns_needs_clarification():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(message="сделай что-нибудь"),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "needs_clarification"
|
||||
assert response.intent["domain"] == "unknown"
|
||||
|
||||
|
||||
# [/DEF:test_unknown_command_returns_needs_clarification:Function]
|
||||
# [DEF:test_non_admin_command_returns_denied:Function]
|
||||
# @PURPOSE: Non-admin user must receive denied state for privileged command.
|
||||
# @PRE: Limited principal executes privileged git branch command.
|
||||
# @POST: Response state is denied and operation is not executed.
|
||||
def test_non_admin_command_returns_denied():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="создай ветку feature/test для дашборда 12"
|
||||
),
|
||||
current_user=_limited_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "denied"
|
||||
|
||||
|
||||
# [/DEF:test_non_admin_command_returns_denied:Function]
|
||||
# [DEF:test_migration_to_prod_requires_confirmation_and_can_be_confirmed:Function]
|
||||
# @PURPOSE: Migration to prod must require confirmation and then start task after explicit confirm.
|
||||
# @PRE: Admin principal submits dangerous migration command.
|
||||
# @POST: Confirmation endpoint transitions flow to started state with task id.
|
||||
def test_migration_to_prod_requires_confirmation_and_can_be_confirmed():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
first = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 12"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert first.state == "needs_confirmation"
|
||||
assert first.confirmation_id
|
||||
|
||||
second = _run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=first.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert second.state == "started"
|
||||
assert second.task_id.startswith("task-")
|
||||
|
||||
|
||||
# [/DEF:test_migration_to_prod_requires_confirmation_and_can_be_confirmed:Function]
|
||||
# [DEF:test_status_query_returns_task_status:Function]
|
||||
# @PURPOSE: Task status command must surface current status text for existing task id.
|
||||
# @PRE: At least one task exists after confirmed operation.
|
||||
# @POST: Status query returns started/success and includes referenced task id.
|
||||
def test_status_query_returns_task_status():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
start = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 10"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
confirm = _run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=start.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
task_id = confirm.task_id
|
||||
|
||||
status_resp = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message=f"проверь статус задачи {task_id}"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert status_resp.state in {"started", "success"}
|
||||
assert task_id in status_resp.text
|
||||
|
||||
|
||||
# [/DEF:test_status_query_returns_task_status:Function]
|
||||
# [DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
|
||||
# @PURPOSE: Status command without explicit task_id should resolve to latest task for current user.
|
||||
# @PRE: User has at least one created task in task manager history.
|
||||
# @POST: Response references latest task status without explicit task id in command.
|
||||
def test_status_query_without_task_id_returns_latest_user_task():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
start = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 33"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
_run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=start.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
status_resp = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="покажи статус последней задачи"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert status_resp.state in {"started", "success"}
|
||||
assert "Последняя задача:" in status_resp.text
|
||||
|
||||
|
||||
# [/DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
306
backend/src/api/routes/__tests__/test_assistant_authz.py
Normal file
306
backend/src/api/routes/__tests__/test_assistant_authz.py
Normal file
@@ -0,0 +1,306 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_assistant_authz:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, assistant, authz, confirmation, rbac
|
||||
# @PURPOSE: Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.
|
||||
# @LAYER: UI (API Tests)
|
||||
# @RELATION: DEPENDS_ON -> backend.src.api.routes.assistant
|
||||
# @INVARIANT: Security-sensitive flows fail closed for unauthorized actors.
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Force isolated sqlite databases for test module before dependencies import.
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz.db")
|
||||
os.environ.setdefault("TASKS_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz_tasks.db")
|
||||
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_authz_auth.db")
|
||||
|
||||
from src.api.routes import assistant as assistant_module
|
||||
from src.models.assistant import (
|
||||
AssistantAuditRecord,
|
||||
AssistantConfirmationRecord,
|
||||
AssistantMessageRecord,
|
||||
)
|
||||
|
||||
|
||||
# [DEF:_run_async:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Execute async endpoint handler in synchronous test context.
|
||||
# @PRE: coroutine is awaitable endpoint invocation.
|
||||
# @POST: Returns coroutine result or raises propagated exception.
|
||||
def _run_async(coroutine):
|
||||
return asyncio.run(coroutine)
|
||||
|
||||
|
||||
# [/DEF:_run_async:Function]
|
||||
# [DEF:_FakeTask:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Lightweight task model used for assistant authz tests.
|
||||
class _FakeTask:
|
||||
def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
|
||||
self.id = task_id
|
||||
self.status = status
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
# [/DEF:_FakeTask:Class]
|
||||
# [DEF:_FakeTaskManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Minimal task manager for deterministic operation creation and lookup.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self):
|
||||
self._created = []
|
||||
|
||||
async def create_task(self, plugin_id, params, user_id=None):
|
||||
task_id = f"task-{len(self._created) + 1}"
|
||||
task = _FakeTask(task_id=task_id, status="RUNNING", user_id=user_id)
|
||||
self._created.append((plugin_id, params, user_id, task))
|
||||
return task
|
||||
|
||||
def get_task(self, task_id):
|
||||
for _, _, _, task in self._created:
|
||||
if task.id == task_id:
|
||||
return task
|
||||
return None
|
||||
|
||||
def get_tasks(self, limit=20, offset=0):
|
||||
return [x[3] for x in self._created][offset : offset + limit]
|
||||
|
||||
|
||||
# [/DEF:_FakeTaskManager:Class]
|
||||
# [DEF:_FakeConfigManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Provide deterministic environment aliases required by intent parsing.
|
||||
class _FakeConfigManager:
|
||||
def get_environments(self):
|
||||
return [
|
||||
SimpleNamespace(id="dev", name="Development"),
|
||||
SimpleNamespace(id="prod", name="Production"),
|
||||
]
|
||||
|
||||
|
||||
# [/DEF:_FakeConfigManager:Class]
|
||||
# [DEF:_admin_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build admin principal fixture.
|
||||
# @PRE: Test requires privileged principal for risky operations.
|
||||
# @POST: Returns admin-like user stub with Admin role.
|
||||
def _admin_user():
|
||||
role = SimpleNamespace(name="Admin", permissions=[])
|
||||
return SimpleNamespace(id="u-admin", username="admin", roles=[role])
|
||||
|
||||
|
||||
# [/DEF:_admin_user:Function]
|
||||
# [DEF:_other_admin_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build second admin principal fixture for ownership tests.
|
||||
# @PRE: Ownership mismatch scenario needs distinct authenticated actor.
|
||||
# @POST: Returns alternate admin-like user stub.
|
||||
def _other_admin_user():
|
||||
role = SimpleNamespace(name="Admin", permissions=[])
|
||||
return SimpleNamespace(id="u-admin-2", username="admin2", roles=[role])
|
||||
|
||||
|
||||
# [/DEF:_other_admin_user:Function]
|
||||
# [DEF:_limited_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build limited principal without required assistant execution privileges.
|
||||
# @PRE: Permission denial scenario needs non-admin actor.
|
||||
# @POST: Returns restricted user stub.
|
||||
def _limited_user():
|
||||
role = SimpleNamespace(name="Operator", permissions=[])
|
||||
return SimpleNamespace(id="u-limited", username="limited", roles=[role])
|
||||
|
||||
|
||||
# [/DEF:_limited_user:Function]
|
||||
# [DEF:_FakeQuery:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Minimal chainable query object for fake DB interactions.
|
||||
class _FakeQuery:
|
||||
def __init__(self, rows):
|
||||
self._rows = list(rows)
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return list(self._rows)
|
||||
|
||||
def limit(self, limit):
|
||||
self._rows = self._rows[:limit]
|
||||
return self
|
||||
|
||||
def offset(self, offset):
|
||||
self._rows = self._rows[offset:]
|
||||
return self
|
||||
|
||||
def count(self):
|
||||
return len(self._rows)
|
||||
|
||||
|
||||
# [/DEF:_FakeQuery:Class]
|
||||
# [DEF:_FakeDb:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: In-memory session substitute for assistant route persistence calls.
|
||||
class _FakeDb:
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
self._confirmations = []
|
||||
self._audit = []
|
||||
|
||||
def add(self, row):
|
||||
table = getattr(row, "__tablename__", "")
|
||||
if table == "assistant_messages":
|
||||
self._messages.append(row)
|
||||
elif table == "assistant_confirmations":
|
||||
self._confirmations.append(row)
|
||||
elif table == "assistant_audit":
|
||||
self._audit.append(row)
|
||||
|
||||
def merge(self, row):
|
||||
if getattr(row, "__tablename__", "") != "assistant_confirmations":
|
||||
self.add(row)
|
||||
return row
|
||||
|
||||
for i, existing in enumerate(self._confirmations):
|
||||
if getattr(existing, "id", None) == getattr(row, "id", None):
|
||||
self._confirmations[i] = row
|
||||
return row
|
||||
self._confirmations.append(row)
|
||||
return row
|
||||
|
||||
def query(self, model):
|
||||
if model is AssistantMessageRecord:
|
||||
return _FakeQuery(self._messages)
|
||||
if model is AssistantConfirmationRecord:
|
||||
return _FakeQuery(self._confirmations)
|
||||
if model is AssistantAuditRecord:
|
||||
return _FakeQuery(self._audit)
|
||||
return _FakeQuery([])
|
||||
|
||||
def commit(self):
|
||||
return None
|
||||
|
||||
def rollback(self):
|
||||
return None
|
||||
|
||||
|
||||
# [/DEF:_FakeDb:Class]
|
||||
# [DEF:_clear_assistant_state:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Reset assistant process-local state between test cases.
|
||||
# @PRE: Assistant globals may contain state from prior tests.
|
||||
# @POST: Assistant in-memory state dictionaries are cleared.
|
||||
def _clear_assistant_state():
|
||||
assistant_module.CONVERSATIONS.clear()
|
||||
assistant_module.USER_ACTIVE_CONVERSATION.clear()
|
||||
assistant_module.CONFIRMATIONS.clear()
|
||||
assistant_module.ASSISTANT_AUDIT.clear()
|
||||
|
||||
|
||||
# [/DEF:_clear_assistant_state:Function]
|
||||
# [DEF:test_confirmation_owner_mismatch_returns_403:Function]
|
||||
# @PURPOSE: Confirm endpoint should reject requests from user that does not own the confirmation token.
|
||||
# @PRE: Confirmation token is created by first admin actor.
|
||||
# @POST: Second actor receives 403 on confirm operation.
|
||||
def test_confirmation_owner_mismatch_returns_403():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
create = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 18"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert create.state == "needs_confirmation"
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=create.confirmation_id,
|
||||
current_user=_other_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert exc.value.status_code == 403
|
||||
|
||||
|
||||
# [/DEF:test_confirmation_owner_mismatch_returns_403:Function]
|
||||
# [DEF:test_expired_confirmation_cannot_be_confirmed:Function]
|
||||
# @PURPOSE: Expired confirmation token should be rejected and not create task.
|
||||
# @PRE: Confirmation token exists and is manually expired before confirm request.
|
||||
# @POST: Confirm endpoint raises 400 and no task is created.
|
||||
def test_expired_confirmation_cannot_be_confirmed():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
create = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 19"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = datetime.utcnow() - timedelta(minutes=1)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=create.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert exc.value.status_code == 400
|
||||
assert task_manager.get_tasks(limit=10, offset=0) == []
|
||||
|
||||
|
||||
# [/DEF:test_expired_confirmation_cannot_be_confirmed:Function]
|
||||
# [DEF:test_limited_user_cannot_launch_restricted_operation:Function]
|
||||
# @PURPOSE: Limited user should receive denied state for privileged operation.
|
||||
# @PRE: Restricted user attempts dangerous deploy command.
|
||||
# @POST: Assistant returns denied state and does not execute operation.
|
||||
def test_limited_user_cannot_launch_restricted_operation():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="задеплой дашборд 88 в production"
|
||||
),
|
||||
current_user=_limited_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "denied"
|
||||
|
||||
|
||||
# [/DEF:test_limited_user_cannot_launch_restricted_operation:Function]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_assistant_authz:Module]
|
||||
1130
backend/src/api/routes/assistant.py
Normal file
1130
backend/src/api/routes/assistant.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ import asyncio
|
||||
from .dependencies import get_task_manager, get_scheduler_service
|
||||
from .core.utils.network import NetworkError
|
||||
from .core.logger import logger, belief_scope
|
||||
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports
|
||||
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant
|
||||
from .api import auth
|
||||
|
||||
# [DEF:App:Global]
|
||||
@@ -119,11 +119,12 @@ app.include_router(environments.router, tags=["Environments"])
|
||||
app.include_router(mappings.router, prefix="/api/mappings", tags=["Mappings"])
|
||||
app.include_router(migration.router)
|
||||
app.include_router(git.router, prefix="/api/git", tags=["Git"])
|
||||
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
|
||||
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
|
||||
app.include_router(dashboards.router)
|
||||
app.include_router(datasets.router)
|
||||
app.include_router(reports.router)
|
||||
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
|
||||
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
|
||||
app.include_router(dashboards.router)
|
||||
app.include_router(datasets.router)
|
||||
app.include_router(reports.router)
|
||||
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
|
||||
|
||||
|
||||
# [DEF:api.include_routers:Action]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# [DEF:backend.src.core.database:Module]
|
||||
#
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: database, postgresql, sqlalchemy, session, persistence
|
||||
# @PURPOSE: Configures database connection and session management (PostgreSQL-first).
|
||||
# @LAYER: Core
|
||||
# @RELATION: DEPENDS_ON -> sqlalchemy
|
||||
# @RELATION: USES -> backend.src.models.mapping
|
||||
# @RELATION: USES -> backend.src.core.auth.config
|
||||
# @RELATION: DEPENDS_ON -> backend.src.models.mapping
|
||||
# @RELATION: DEPENDS_ON -> backend.src.core.auth.config
|
||||
#
|
||||
# @INVARIANT: A single engine instance is used for the entire application.
|
||||
|
||||
@@ -18,6 +19,7 @@ from ..models import task as _task_models # noqa: F401
|
||||
from ..models import auth as _auth_models # noqa: F401
|
||||
from ..models import config as _config_models # noqa: F401
|
||||
from ..models import llm as _llm_models # noqa: F401
|
||||
from ..models import assistant as _assistant_models # noqa: F401
|
||||
from .logger import belief_scope
|
||||
from .auth.config import auth_config
|
||||
import os
|
||||
@@ -72,18 +74,21 @@ auth_engine = _build_engine(AUTH_DATABASE_URL)
|
||||
# [/DEF:auth_engine:Variable]
|
||||
|
||||
# [DEF:SessionLocal:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: A session factory for the main mappings database.
|
||||
# @PRE: engine is initialized.
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
# [/DEF:SessionLocal:Class]
|
||||
|
||||
# [DEF:TasksSessionLocal:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: A session factory for the tasks execution database.
|
||||
# @PRE: tasks_engine is initialized.
|
||||
TasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)
|
||||
# [/DEF:TasksSessionLocal:Class]
|
||||
|
||||
# [DEF:AuthSessionLocal:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: A session factory for the authentication database.
|
||||
# @PRE: auth_engine is initialized.
|
||||
AuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)
|
||||
|
||||
74
backend/src/models/assistant.py
Normal file
74
backend/src/models/assistant.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# [DEF:backend.src.models.assistant:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: assistant, audit, confirmation, chat
|
||||
# @PURPOSE: SQLAlchemy models for assistant audit trail and confirmation tokens.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: DEPENDS_ON -> backend.src.models.mapping
|
||||
# @INVARIANT: Assistant records preserve immutable ids and creation timestamps.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, JSON, Text
|
||||
|
||||
from .mapping import Base
|
||||
|
||||
|
||||
# [DEF:AssistantAuditRecord:Class]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Store audit decisions and outcomes produced by assistant command handling.
|
||||
# @PRE: user_id must identify the actor for every record.
|
||||
# @POST: Audit payload remains available for compliance and debugging.
|
||||
class AssistantAuditRecord(Base):
|
||||
__tablename__ = "assistant_audit"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, index=True, nullable=False)
|
||||
conversation_id = Column(String, index=True, nullable=True)
|
||||
decision = Column(String, nullable=True)
|
||||
task_id = Column(String, nullable=True)
|
||||
message = Column(Text, nullable=True)
|
||||
payload = Column(JSON, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
# [/DEF:AssistantAuditRecord:Class]
|
||||
|
||||
|
||||
# [DEF:AssistantMessageRecord:Class]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Persist chat history entries for assistant conversations.
|
||||
# @PRE: user_id, conversation_id, role and text must be present.
|
||||
# @POST: Message row can be queried in chronological order.
|
||||
class AssistantMessageRecord(Base):
|
||||
__tablename__ = "assistant_messages"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, index=True, nullable=False)
|
||||
conversation_id = Column(String, index=True, nullable=False)
|
||||
role = Column(String, nullable=False) # user | assistant
|
||||
text = Column(Text, nullable=False)
|
||||
state = Column(String, nullable=True)
|
||||
task_id = Column(String, nullable=True)
|
||||
confirmation_id = Column(String, nullable=True)
|
||||
payload = Column(JSON, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
# [/DEF:AssistantMessageRecord:Class]
|
||||
|
||||
|
||||
# [DEF:AssistantConfirmationRecord:Class]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Persist risky operation confirmation tokens with lifecycle state.
|
||||
# @PRE: intent/dispatch and expiry timestamp must be provided.
|
||||
# @POST: State transitions can be tracked and audited.
|
||||
class AssistantConfirmationRecord(Base):
|
||||
__tablename__ = "assistant_confirmations"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, index=True, nullable=False)
|
||||
conversation_id = Column(String, index=True, nullable=False)
|
||||
state = Column(String, index=True, nullable=False, default="pending")
|
||||
intent = Column(JSON, nullable=False)
|
||||
dispatch = Column(JSON, nullable=False)
|
||||
expires_at = Column(DateTime, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
consumed_at = Column(DateTime, nullable=True)
|
||||
# [/DEF:AssistantConfirmationRecord:Class]
|
||||
# [/DEF:backend.src.models.assistant:Module]
|
||||
50
frontend/src/lib/api/assistant.js
Normal file
50
frontend/src/lib/api/assistant.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// [DEF:frontend.src.lib.api.assistant:Module]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: assistant, api, client, chat, confirmation
|
||||
// @PURPOSE: API client wrapper for assistant chat, confirmation actions, and history retrieval.
|
||||
// @LAYER: Infra-API
|
||||
// @RELATION: DEPENDS_ON -> frontend.src.lib.api.api_module
|
||||
// @INVARIANT: All assistant requests must use requestApi wrapper (no native fetch).
|
||||
|
||||
import { requestApi } from '$lib/api.js';
|
||||
|
||||
// [DEF:sendAssistantMessage:Function]
|
||||
// @PURPOSE: Send a user message to assistant orchestrator endpoint.
|
||||
// @PRE: payload.message is a non-empty string.
|
||||
// @POST: Returns assistant response object with deterministic state.
|
||||
export function sendAssistantMessage(payload) {
|
||||
return requestApi('/assistant/messages', 'POST', payload);
|
||||
}
|
||||
// [/DEF:sendAssistantMessage:Function]
|
||||
|
||||
// [DEF:confirmAssistantOperation:Function]
|
||||
// @PURPOSE: Confirm a pending risky assistant operation.
|
||||
// @PRE: confirmationId references an existing pending token.
|
||||
// @POST: Returns execution response (started/success/failed).
|
||||
export function confirmAssistantOperation(confirmationId) {
|
||||
return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST');
|
||||
}
|
||||
// [/DEF:confirmAssistantOperation:Function]
|
||||
|
||||
// [DEF:cancelAssistantOperation:Function]
|
||||
// @PURPOSE: Cancel a pending risky assistant operation.
|
||||
// @PRE: confirmationId references an existing pending token.
|
||||
// @POST: Operation is cancelled and cannot be executed by this token.
|
||||
export function cancelAssistantOperation(confirmationId) {
|
||||
return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST');
|
||||
}
|
||||
// [/DEF:cancelAssistantOperation:Function]
|
||||
|
||||
// [DEF:getAssistantHistory:Function]
|
||||
// @PURPOSE: Retrieve paginated assistant conversation history.
|
||||
// @PRE: page/pageSize are positive integers.
|
||||
// @POST: Returns a paginated payload with history items.
|
||||
export function getAssistantHistory(page = 1, pageSize = 20, conversationId = null) {
|
||||
const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
|
||||
if (conversationId) {
|
||||
params.append('conversation_id', conversationId);
|
||||
}
|
||||
return requestApi(`/assistant/history?${params.toString()}`, 'GET');
|
||||
}
|
||||
// [/DEF:getAssistantHistory:Function]
|
||||
// [/DEF:frontend.src.lib.api.assistant:Module]
|
||||
342
frontend/src/lib/components/assistant/AssistantChatPanel.svelte
Normal file
342
frontend/src/lib/components/assistant/AssistantChatPanel.svelte
Normal file
@@ -0,0 +1,342 @@
|
||||
<!-- [DEF:AssistantChatPanel:Component] -->
|
||||
<script>
|
||||
/**
|
||||
* @TIER: CRITICAL
|
||||
* @PURPOSE: Slide-out assistant chat panel for natural language command execution and task tracking.
|
||||
* @LAYER: UI
|
||||
* @RELATION: BINDS_TO -> assistantChatStore
|
||||
* @RELATION: CALLS -> frontend.src.lib.api.assistant
|
||||
* @RELATION: DISPATCHES -> taskDrawerStore
|
||||
* @SEMANTICS: assistant-chat, confirmation, long-running-task, progress-tracking
|
||||
* @INVARIANT: User commands and assistant responses are appended in chronological order.
|
||||
* @INVARIANT: Risky operations are executed only through explicit confirm action.
|
||||
*
|
||||
* @UX_STATE: Closed -> Panel is hidden.
|
||||
* @UX_STATE: LoadingHistory -> Existing conversation history is loading.
|
||||
* @UX_STATE: Idle -> Input is available and no request in progress.
|
||||
* @UX_STATE: Sending -> Input locked while request is pending.
|
||||
* @UX_STATE: Error -> Failed action rendered as assistant failed message.
|
||||
* @UX_FEEDBACK: Started operation surfaces task_id and quick action to open task drawer.
|
||||
* @UX_RECOVERY: User can retry command or action from input and action buttons.
|
||||
* @UX_TEST: LoadingHistory -> {openPanel: true, expected: loading block visible}
|
||||
* @UX_TEST: Sending -> {sendMessage: "branch", expected: send button disabled}
|
||||
* @UX_TEST: NeedsConfirmation -> {click: confirm action, expected: started response with task_id}
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/i18n';
|
||||
import Icon from '$lib/ui/Icon.svelte';
|
||||
import { openDrawerForTask } from '$lib/stores/taskDrawer.js';
|
||||
import {
|
||||
assistantChatStore,
|
||||
closeAssistantChat,
|
||||
setAssistantConversationId,
|
||||
} from '$lib/stores/assistantChat.js';
|
||||
import {
|
||||
sendAssistantMessage,
|
||||
confirmAssistantOperation,
|
||||
cancelAssistantOperation,
|
||||
getAssistantHistory,
|
||||
} from '$lib/api/assistant.js';
|
||||
|
||||
let input = '';
|
||||
let loading = false;
|
||||
let loadingHistory = false;
|
||||
let messages = [];
|
||||
let initialized = false;
|
||||
|
||||
$: isOpen = $assistantChatStore?.isOpen || false;
|
||||
$: conversationId = $assistantChatStore?.conversationId || null;
|
||||
|
||||
// [DEF:loadHistory:Function]
|
||||
/**
|
||||
* @PURPOSE: Load current conversation history when panel becomes visible.
|
||||
* @PRE: Panel is open and history request is not already running.
|
||||
* @POST: messages are populated from persisted history and conversation id is synchronized.
|
||||
* @SIDE_EFFECT: Performs API call to assistant history endpoint.
|
||||
*/
|
||||
async function loadHistory() {
|
||||
if (loadingHistory || !isOpen) return;
|
||||
loadingHistory = true;
|
||||
try {
|
||||
const history = await getAssistantHistory(1, 50, conversationId);
|
||||
messages = (history.items || []).map((msg) => ({
|
||||
...msg,
|
||||
actions: msg.actions || msg.metadata?.actions || [],
|
||||
}));
|
||||
if (history.conversation_id && history.conversation_id !== conversationId) {
|
||||
setAssistantConversationId(history.conversation_id);
|
||||
}
|
||||
initialized = true;
|
||||
console.log('[AssistantChatPanel][Coherence:OK] History loaded');
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load history', err);
|
||||
} finally {
|
||||
loadingHistory = false;
|
||||
}
|
||||
}
|
||||
// [/DEF:loadHistory:Function]
|
||||
|
||||
$: if (isOpen && !initialized) {
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
// [DEF:appendLocalUserMessage:Function]
|
||||
/**
|
||||
* @PURPOSE: Add optimistic local user message before backend response.
|
||||
* @PRE: text is non-empty command text.
|
||||
* @POST: user message appears at the end of messages list.
|
||||
*/
|
||||
function appendLocalUserMessage(text) {
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
message_id: `local-${Date.now()}`,
|
||||
role: 'user',
|
||||
text,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
// [/DEF:appendLocalUserMessage:Function]
|
||||
|
||||
// [DEF:appendAssistantResponse:Function]
|
||||
/**
|
||||
* @PURPOSE: Normalize and append assistant response payload to chat list.
|
||||
* @PRE: response follows assistant message response contract.
|
||||
* @POST: assistant message appended with state/task/actions metadata.
|
||||
*/
|
||||
function appendAssistantResponse(response) {
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
message_id: response.response_id,
|
||||
role: 'assistant',
|
||||
text: response.text,
|
||||
state: response.state,
|
||||
task_id: response.task_id || null,
|
||||
confirmation_id: response.confirmation_id || null,
|
||||
actions: response.actions || [],
|
||||
created_at: response.created_at,
|
||||
},
|
||||
];
|
||||
}
|
||||
// [/DEF:appendAssistantResponse:Function]
|
||||
|
||||
// [DEF:handleSend:Function]
|
||||
/**
|
||||
* @PURPOSE: Submit user command to assistant orchestration API.
|
||||
* @PRE: input contains a non-empty command and current request is not loading.
|
||||
* @POST: assistant response is rendered and conversation id is persisted in store.
|
||||
* @SIDE_EFFECT: Triggers backend command execution pipeline.
|
||||
*/
|
||||
async function handleSend() {
|
||||
const text = input.trim();
|
||||
if (!text || loading) return;
|
||||
|
||||
appendLocalUserMessage(text);
|
||||
input = '';
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await sendAssistantMessage({
|
||||
conversation_id: conversationId,
|
||||
message: text,
|
||||
});
|
||||
|
||||
if (response.conversation_id) {
|
||||
setAssistantConversationId(response.conversation_id);
|
||||
}
|
||||
|
||||
appendAssistantResponse(response);
|
||||
} catch (err) {
|
||||
appendAssistantResponse({
|
||||
response_id: `error-${Date.now()}`,
|
||||
text: err.message || 'Assistant request failed',
|
||||
state: 'failed',
|
||||
created_at: new Date().toISOString(),
|
||||
actions: [],
|
||||
});
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// [/DEF:handleSend:Function]
|
||||
|
||||
// [DEF:handleAction:Function]
|
||||
/**
|
||||
* @PURPOSE: Execute assistant action button behavior (open task/reports, confirm, cancel).
|
||||
* @PRE: action object is produced by assistant response contract.
|
||||
* @POST: UI navigation or follow-up assistant response is appended.
|
||||
* @SIDE_EFFECT: May navigate routes or call confirm/cancel API endpoints.
|
||||
*/
|
||||
async function handleAction(action, message) {
|
||||
try {
|
||||
if (action.type === 'open_task' && action.target) {
|
||||
openDrawerForTask(action.target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'open_reports') {
|
||||
goto('/reports');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'confirm' && message.confirmation_id) {
|
||||
const response = await confirmAssistantOperation(message.confirmation_id);
|
||||
appendAssistantResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'cancel' && message.confirmation_id) {
|
||||
const response = await cancelAssistantOperation(message.confirmation_id);
|
||||
appendAssistantResponse(response);
|
||||
}
|
||||
} catch (err) {
|
||||
appendAssistantResponse({
|
||||
response_id: `action-error-${Date.now()}`,
|
||||
text: err.message || 'Action failed',
|
||||
state: 'failed',
|
||||
created_at: new Date().toISOString(),
|
||||
actions: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
// [/DEF:handleAction:Function]
|
||||
|
||||
// [DEF:handleKeydown:Function]
|
||||
/**
|
||||
* @PURPOSE: Submit command by Enter while preserving multiline input with Shift+Enter.
|
||||
* @PRE: Keyboard event received from chat input.
|
||||
* @POST: handleSend is invoked when Enter is pressed without shift modifier.
|
||||
*/
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
// [/DEF:handleKeydown:Function]
|
||||
|
||||
// [DEF:stateClass:Function]
|
||||
/**
|
||||
* @PURPOSE: Map assistant state to visual badge style class.
|
||||
* @PRE: state is a nullable assistant state string.
|
||||
* @POST: Tailwind class string returned for badge rendering.
|
||||
*/
|
||||
function stateClass(state) {
|
||||
if (state === 'started') return 'bg-sky-100 text-sky-700 border-sky-200';
|
||||
if (state === 'success') return 'bg-emerald-100 text-emerald-700 border-emerald-200';
|
||||
if (state === 'needs_confirmation') return 'bg-amber-100 text-amber-700 border-amber-200';
|
||||
if (state === 'denied' || state === 'failed') return 'bg-rose-100 text-rose-700 border-rose-200';
|
||||
if (state === 'needs_clarification') return 'bg-violet-100 text-violet-700 border-violet-200';
|
||||
return 'bg-slate-100 text-slate-700 border-slate-200';
|
||||
}
|
||||
// [/DEF:stateClass:Function]
|
||||
|
||||
onMount(() => {
|
||||
initialized = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="fixed inset-0 z-[70] bg-slate-900/30" on:click={closeAssistantChat} aria-hidden="true"></div>
|
||||
|
||||
<aside class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl">
|
||||
<div class="flex h-14 items-center justify-between border-b border-slate-200 px-4">
|
||||
<div class="flex items-center gap-2 text-slate-800">
|
||||
<Icon name="activity" size={18} />
|
||||
<h2 class="text-sm font-semibold">{$t.assistant?.title || 'AI Assistant'}</h2>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
|
||||
on:click={closeAssistantChat}
|
||||
aria-label={$t.assistant?.close || 'Close assistant'}
|
||||
>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[calc(100%-56px)] flex-col">
|
||||
<div class="flex-1 space-y-3 overflow-y-auto p-4">
|
||||
{#if loadingHistory}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">{$t.assistant?.loading_history || 'Loading history...'}</div>
|
||||
{:else if messages.length === 0}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
|
||||
{$t.assistant?.try_commands || 'Try commands:'}
|
||||
<div class="mt-2 space-y-1 text-xs">
|
||||
<div>• сделай ветку feature/new-dashboard для дашборда 42</div>
|
||||
<div>• запусти миграцию с dev на prod для дашборда 42</div>
|
||||
<div>• проверь статус задачи task-123</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each messages as message (message.message_id)}
|
||||
<div class={message.role === 'user' ? 'ml-8' : 'mr-8'}>
|
||||
<div class="rounded-xl border p-3 {message.role === 'user' ? 'border-sky-200 bg-sky-50' : 'border-slate-200 bg-white'}">
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
{message.role === 'user' ? 'You' : 'Assistant'}
|
||||
</span>
|
||||
{#if message.state}
|
||||
<span class="rounded-md border px-2 py-0.5 text-[10px] font-medium {stateClass(message.state)}">
|
||||
{$t.assistant?.states?.[message.state] || message.state}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="whitespace-pre-wrap text-sm text-slate-800">{message.text}</div>
|
||||
|
||||
{#if message.task_id}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span class="rounded border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs text-slate-700">task_id: {message.task_id}</span>
|
||||
<button
|
||||
class="text-xs font-medium text-sky-700 hover:text-sky-900"
|
||||
on:click={() => openDrawerForTask(message.task_id)}
|
||||
>
|
||||
{$t.assistant?.open_task_drawer || 'Open Task Drawer'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if message.actions?.length}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{#each message.actions as action}
|
||||
<button
|
||||
class="rounded-md border border-slate-300 px-2.5 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100"
|
||||
on:click={() => handleAction(action, message)}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-200 p-3">
|
||||
<div class="flex items-end gap-2">
|
||||
<textarea
|
||||
bind:value={input}
|
||||
rows="2"
|
||||
placeholder={$t.assistant?.input_placeholder || 'Type a command...'}
|
||||
class="min-h-[52px] w-full resize-y rounded-lg border border-slate-300 px-3 py-2 text-sm outline-none transition focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
|
||||
on:keydown={handleKeydown}
|
||||
></textarea>
|
||||
<button
|
||||
class="rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
on:click={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
>
|
||||
{loading ? '...' : ($t.assistant?.send || 'Send')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
<!-- [/DEF:AssistantChatPanel:Component] -->
|
||||
@@ -0,0 +1,87 @@
|
||||
// [DEF:frontend.src.lib.components.assistant.__tests__.assistant_chat_integration:Module]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: assistant, integration-test, ux-contract, i18n
|
||||
// @PURPOSE: Contract-level integration checks for assistant chat panel implementation and localization wiring.
|
||||
// @LAYER: UI Tests
|
||||
// @RELATION: VERIFIES -> frontend/src/lib/components/assistant/AssistantChatPanel.svelte
|
||||
// @INVARIANT: Critical assistant UX states and action hooks remain present in component source.
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const COMPONENT_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src/lib/components/assistant/AssistantChatPanel.svelte',
|
||||
);
|
||||
const EN_LOCALE_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src/lib/i18n/locales/en.json',
|
||||
);
|
||||
const RU_LOCALE_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src/lib/i18n/locales/ru.json',
|
||||
);
|
||||
|
||||
// [DEF:readJson:Function]
|
||||
// @PURPOSE: Read and parse JSON fixture file from disk.
|
||||
// @PRE: filePath points to existing UTF-8 JSON file.
|
||||
// @POST: Returns parsed object representation.
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
}
|
||||
// [/DEF:readJson:Function]
|
||||
|
||||
// [DEF:assistant_chat_contract_tests:Function]
|
||||
// @PURPOSE: Validate assistant chat component contract and locale integration without DOM runtime dependency.
|
||||
// @PRE: Component and locale files exist in expected paths.
|
||||
// @POST: Contract checks guarantee assistant UI anchors and i18n wiring remain intact.
|
||||
describe('AssistantChatPanel integration contract', () => {
|
||||
it('contains semantic anchors and UX contract tags', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('<!-- [DEF' + ':AssistantChatPanel:Component] -->');
|
||||
expect(source).toContain('@TIER: CRITICAL');
|
||||
expect(source).toContain('@UX_STATE: LoadingHistory');
|
||||
expect(source).toContain('@UX_STATE: Sending');
|
||||
expect(source).toContain('@UX_STATE: Error');
|
||||
expect(source).toContain('@UX_FEEDBACK: Started operation surfaces task_id');
|
||||
expect(source).toContain('@UX_RECOVERY: User can retry command');
|
||||
expect(source).toContain('<!-- [/DEF' + ':AssistantChatPanel:Component] -->');
|
||||
});
|
||||
|
||||
it('keeps confirmation/task-tracking action hooks in place', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain("if (action.type === 'confirm' && message.confirmation_id)");
|
||||
expect(source).toContain("if (action.type === 'cancel' && message.confirmation_id)");
|
||||
expect(source).toContain("if (action.type === 'open_task' && action.target)");
|
||||
expect(source).toContain('openDrawerForTask(action.target)');
|
||||
expect(source).toContain("goto('/reports')");
|
||||
});
|
||||
|
||||
it('uses i18n bindings for assistant UI labels', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('$t.assistant?.title');
|
||||
expect(source).toContain('$t.assistant?.input_placeholder');
|
||||
expect(source).toContain('$t.assistant?.send');
|
||||
expect(source).toContain('$t.assistant?.states?.[message.state]');
|
||||
expect(source).toContain('$t.assistant?.open_task_drawer');
|
||||
});
|
||||
|
||||
it('provides assistant locale keys in both en and ru dictionaries', () => {
|
||||
const en = readJson(EN_LOCALE_PATH);
|
||||
const ru = readJson(RU_LOCALE_PATH);
|
||||
|
||||
expect(en.assistant.title).toBeTruthy();
|
||||
expect(en.assistant.send).toBeTruthy();
|
||||
expect(en.assistant.states.needs_confirmation).toBeTruthy();
|
||||
|
||||
expect(ru.assistant.title).toBeTruthy();
|
||||
expect(ru.assistant.send).toBeTruthy();
|
||||
expect(ru.assistant.states.needs_confirmation).toBeTruthy();
|
||||
});
|
||||
});
|
||||
// [/DEF:assistant_chat_contract_tests:Function]
|
||||
// [/DEF:frontend.src.lib.components.assistant.__tests__.assistant_chat_integration:Module]
|
||||
@@ -0,0 +1,50 @@
|
||||
// [DEF:frontend.src.lib.components.assistant.__tests__.assistant_confirmation_integration:Module]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: assistant, confirmation, integration-test, ux
|
||||
// @PURPOSE: Validate confirm/cancel UX contract bindings in assistant chat panel source.
|
||||
// @LAYER: UI Tests
|
||||
// @RELATION: VERIFIES -> frontend/src/lib/components/assistant/AssistantChatPanel.svelte
|
||||
// @INVARIANT: Confirm/cancel action handling must remain explicit and confirmation-id bound.
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const COMPONENT_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src/lib/components/assistant/AssistantChatPanel.svelte',
|
||||
);
|
||||
|
||||
// [DEF:assistant_confirmation_contract_tests:Function]
|
||||
// @PURPOSE: Assert that confirmation UX flow and API bindings are preserved in chat panel.
|
||||
// @PRE: Assistant panel source file exists and is readable.
|
||||
// @POST: Test guarantees explicit confirm/cancel guards and failed-action recovery path.
|
||||
describe('AssistantChatPanel confirmation integration contract', () => {
|
||||
it('contains confirmation action guards with confirmation_id checks', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain("if (action.type === 'confirm' && message.confirmation_id)");
|
||||
expect(source).toContain("if (action.type === 'cancel' && message.confirmation_id)");
|
||||
expect(source).toContain('confirmAssistantOperation(message.confirmation_id)');
|
||||
expect(source).toContain('cancelAssistantOperation(message.confirmation_id)');
|
||||
});
|
||||
|
||||
it('renders action buttons from assistant response payload', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('{#if message.actions?.length}');
|
||||
expect(source).toContain('{#each message.actions as action}');
|
||||
expect(source).toContain('{action.label}');
|
||||
expect(source).toContain('on:click={() => handleAction(action, message)}');
|
||||
});
|
||||
|
||||
it('keeps failed-action recovery response path', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain("response_id: `action-error-${Date.now()}`");
|
||||
expect(source).toContain("state: 'failed'");
|
||||
expect(source).toContain("text: err.message || 'Action failed'");
|
||||
});
|
||||
});
|
||||
// [/DEF:assistant_confirmation_contract_tests:Function]
|
||||
// [/DEF:frontend.src.lib.components.assistant.__tests__.assistant_confirmation_integration:Module]
|
||||
@@ -12,6 +12,8 @@
|
||||
* @UX_STATE: SearchFocused -> Search input expands
|
||||
* @UX_FEEDBACK: Activity badge shows count of running tasks
|
||||
* @UX_RECOVERY: Click outside closes dropdowns
|
||||
* @UX_TEST: SearchFocused -> {focus: search input, expected: focused style class applied}
|
||||
* @UX_TEST: ActivityClick -> {click: activity button, expected: task drawer opens}
|
||||
*/
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
@@ -25,6 +27,7 @@
|
||||
import { sidebarStore, toggleMobileSidebar } from "$lib/stores/sidebar.js";
|
||||
import { t } from "$lib/i18n";
|
||||
import { auth } from "$lib/auth/store.js";
|
||||
import { toggleAssistantChat } from "$lib/stores/assistantChat.js";
|
||||
import Icon from "$lib/ui/Icon.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -64,6 +67,10 @@
|
||||
dispatch("activityClick");
|
||||
}
|
||||
|
||||
function handleAssistantClick() {
|
||||
toggleAssistantChat();
|
||||
}
|
||||
|
||||
function handleSearchFocus() {
|
||||
isSearchFocused = true;
|
||||
}
|
||||
@@ -129,6 +136,16 @@
|
||||
|
||||
<!-- Nav Actions -->
|
||||
<div class="flex items-center gap-3 md:gap-4">
|
||||
<!-- Assistant -->
|
||||
<button
|
||||
class="rounded-lg p-2 text-slate-600 transition-colors hover:bg-slate-100"
|
||||
on:click={handleAssistantClick}
|
||||
aria-label={$t.assistant?.open || "Open assistant"}
|
||||
title={$t.assistant?.title || "AI Assistant"}
|
||||
>
|
||||
<Icon name="activity" size={22} />
|
||||
</button>
|
||||
|
||||
<!-- Activity Indicator -->
|
||||
<div
|
||||
class="relative cursor-pointer rounded-lg p-2 text-slate-600 transition-colors hover:bg-slate-100"
|
||||
|
||||
@@ -221,6 +221,24 @@
|
||||
"cron_hint": "e.g., 0 0 * * * for daily at midnight",
|
||||
"footer_text": "Task continues running in background"
|
||||
},
|
||||
"assistant": {
|
||||
"title": "AI Assistant",
|
||||
"open": "Open assistant",
|
||||
"close": "Close assistant",
|
||||
"send": "Send",
|
||||
"input_placeholder": "Type a command...",
|
||||
"loading_history": "Loading history...",
|
||||
"try_commands": "Try commands:",
|
||||
"open_task_drawer": "Open Task Drawer",
|
||||
"states": {
|
||||
"started": "Started",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"needs_confirmation": "Needs confirmation",
|
||||
"needs_clarification": "Needs clarification",
|
||||
"denied": "Denied"
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"management": "Connection Management",
|
||||
"add_new": "Add New Connection",
|
||||
@@ -339,4 +357,4 @@
|
||||
"select_role": "Select a role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,24 @@
|
||||
"cron_hint": "например, 0 0 * * * для ежедневного запуска в полночь",
|
||||
"footer_text": "Задача продолжает работать в фоновом режиме"
|
||||
},
|
||||
"assistant": {
|
||||
"title": "AI Ассистент",
|
||||
"open": "Открыть ассистента",
|
||||
"close": "Закрыть ассистента",
|
||||
"send": "Отправить",
|
||||
"input_placeholder": "Введите команду...",
|
||||
"loading_history": "Загрузка истории...",
|
||||
"try_commands": "Попробуйте команды:",
|
||||
"open_task_drawer": "Открыть Task Drawer",
|
||||
"states": {
|
||||
"started": "Запущено",
|
||||
"success": "Успешно",
|
||||
"failed": "Ошибка",
|
||||
"needs_confirmation": "Требует подтверждения",
|
||||
"needs_clarification": "Нужно уточнение",
|
||||
"denied": "Доступ запрещен"
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"management": "Управление подключениями",
|
||||
"add_new": "Добавить новое подключение",
|
||||
@@ -338,4 +356,4 @@
|
||||
"select_role": "Выберите роль"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
frontend/src/lib/stores/__tests__/assistantChat.test.js
Normal file
59
frontend/src/lib/stores/__tests__/assistantChat.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// [DEF:frontend.src.lib.stores.__tests__.assistantChat:Module]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: test, store, assistant, toggle, conversation
|
||||
// @PURPOSE: Validate assistant chat store visibility and conversation binding transitions.
|
||||
// @LAYER: UI Tests
|
||||
// @RELATION: DEPENDS_ON -> assistantChatStore
|
||||
// @INVARIANT: Each test starts from default closed state.
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
assistantChatStore,
|
||||
toggleAssistantChat,
|
||||
openAssistantChat,
|
||||
closeAssistantChat,
|
||||
setAssistantConversationId,
|
||||
} from '../assistantChat.js';
|
||||
|
||||
// [DEF:assistantChatStore_tests:Function]
|
||||
// @TIER: STANDARD
|
||||
// @PURPOSE: Group store unit scenarios for assistant panel behavior.
|
||||
// @PRE: Store can be reset to baseline state in beforeEach hook.
|
||||
// @POST: Open/close/toggle/conversation transitions are validated.
|
||||
describe('assistantChatStore', () => {
|
||||
beforeEach(() => {
|
||||
assistantChatStore.set({
|
||||
isOpen: false,
|
||||
conversationId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should open assistant panel', () => {
|
||||
openAssistantChat();
|
||||
const state = get(assistantChatStore);
|
||||
expect(state.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should close assistant panel', () => {
|
||||
openAssistantChat();
|
||||
closeAssistantChat();
|
||||
const state = get(assistantChatStore);
|
||||
expect(state.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle assistant panel state', () => {
|
||||
toggleAssistantChat();
|
||||
expect(get(assistantChatStore).isOpen).toBe(true);
|
||||
toggleAssistantChat();
|
||||
expect(get(assistantChatStore).isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should set conversation id', () => {
|
||||
setAssistantConversationId('conv-123');
|
||||
const state = get(assistantChatStore);
|
||||
expect(state.conversationId).toBe('conv-123');
|
||||
});
|
||||
});
|
||||
// [/DEF:assistantChatStore_tests:Function]
|
||||
// [/DEF:frontend.src.lib.stores.__tests__.assistantChat:Module]
|
||||
71
frontend/src/lib/stores/assistantChat.js
Normal file
71
frontend/src/lib/stores/assistantChat.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// [DEF:assistantChat:Store]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: assistant, store, ui-state, conversation
|
||||
// @PURPOSE: Control assistant chat panel visibility and active conversation binding.
|
||||
// @LAYER: UI
|
||||
// @RELATION: BINDS_TO -> AssistantChatPanel
|
||||
// @INVARIANT: conversationId persists while panel toggles unless explicitly reset.
|
||||
//
|
||||
// @UX_STATE: Closed -> Panel hidden.
|
||||
// @UX_STATE: Open -> Panel visible and interactive.
|
||||
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const initialState = {
|
||||
isOpen: false,
|
||||
conversationId: null,
|
||||
};
|
||||
|
||||
export const assistantChatStore = writable(initialState);
|
||||
|
||||
// [DEF:toggleAssistantChat:Function]
|
||||
// @PURPOSE: Toggle assistant panel visibility.
|
||||
// @PRE: Store is initialized.
|
||||
// @POST: isOpen value inverted.
|
||||
export function toggleAssistantChat() {
|
||||
assistantChatStore.update((state) => {
|
||||
const next = { ...state, isOpen: !state.isOpen };
|
||||
console.log(`[assistantChat][${next.isOpen ? 'Open' : 'Closed'}] toggleAssistantChat`);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
// [/DEF:toggleAssistantChat:Function]
|
||||
|
||||
// [DEF:openAssistantChat:Function]
|
||||
// @PURPOSE: Open assistant panel.
|
||||
// @PRE: Store is initialized.
|
||||
// @POST: isOpen = true.
|
||||
export function openAssistantChat() {
|
||||
assistantChatStore.update((state) => {
|
||||
const next = { ...state, isOpen: true };
|
||||
console.log('[assistantChat][Open] openAssistantChat');
|
||||
return next;
|
||||
});
|
||||
}
|
||||
// [/DEF:openAssistantChat:Function]
|
||||
|
||||
// [DEF:closeAssistantChat:Function]
|
||||
// @PURPOSE: Close assistant panel.
|
||||
// @PRE: Store is initialized.
|
||||
// @POST: isOpen = false.
|
||||
export function closeAssistantChat() {
|
||||
assistantChatStore.update((state) => {
|
||||
const next = { ...state, isOpen: false };
|
||||
console.log('[assistantChat][Closed] closeAssistantChat');
|
||||
return next;
|
||||
});
|
||||
}
|
||||
// [/DEF:closeAssistantChat:Function]
|
||||
|
||||
// [DEF:setAssistantConversationId:Function]
|
||||
// @PURPOSE: Bind current conversation id in UI state.
|
||||
// @PRE: conversationId is string-like identifier.
|
||||
// @POST: store.conversationId updated.
|
||||
export function setAssistantConversationId(conversationId) {
|
||||
assistantChatStore.update((state) => {
|
||||
console.log('[assistantChat][ConversationBound] setAssistantConversationId');
|
||||
return { ...state, conversationId };
|
||||
});
|
||||
}
|
||||
// [/DEF:setAssistantConversationId:Function]
|
||||
// [/DEF:assistantChat:Store]
|
||||
@@ -32,6 +32,7 @@
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
import TopNavbar from '$lib/components/layout/TopNavbar.svelte';
|
||||
import TaskDrawer from '$lib/components/layout/TaskDrawer.svelte';
|
||||
import AssistantChatPanel from '$lib/components/assistant/AssistantChatPanel.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.js';
|
||||
|
||||
@@ -71,6 +72,7 @@
|
||||
|
||||
<!-- Global Task Drawer -->
|
||||
<TaskDrawer />
|
||||
<AssistantChatPanel />
|
||||
</ProtectedRoute>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
46
specs/021-llm-project-assistant/checklists/requirements.md
Normal file
46
specs/021-llm-project-assistant/checklists/requirements.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Specification Quality Checklist: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Purpose**: Validate specification completeness, UX alignment, and safety before planning
|
||||
**Created**: 2026-02-23
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (framework internals, code-level design)
|
||||
- [x] Focused on user value and operational outcomes
|
||||
- [x] All mandatory sections completed
|
||||
- [x] User stories are independently testable
|
||||
|
||||
## UX Consistency
|
||||
|
||||
- [x] Functional requirements support happy path in `ux_reference.md`
|
||||
- [x] Error experience in UX is reflected in requirements (clarification, deny, timeout, failure)
|
||||
- [x] Response states are explicitly defined for frontend rendering
|
||||
|
||||
## Security & Safety
|
||||
|
||||
- [x] RBAC enforcement is mandatory for every execution path
|
||||
- [x] Risky operations require explicit confirmation
|
||||
- [x] Confirmation expiration/cancel behavior is defined
|
||||
- [x] No hidden execution path for unconfirmed dangerous actions
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No `[NEEDS CLARIFICATION]` markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Edge cases include ambiguity, permission denial, invalid targets, duplicate actions
|
||||
- [x] Success criteria are measurable and technology-agnostic
|
||||
- [x] Assumptions and dependencies are identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] Scope includes all requested command domains (Git, migration/backup, analysis/docs, status)
|
||||
- [x] Long-running operation feedback with `task_id` is explicitly required
|
||||
- [x] Tracking through existing Task Drawer/reports is explicitly required
|
||||
- [x] Specification is ready for `/speckit.plan` and `/speckit.tasks`
|
||||
|
||||
## Notes
|
||||
|
||||
- Validation iteration: 1
|
||||
- Result: PASS
|
||||
- No blocking issues found.
|
||||
@@ -0,0 +1,270 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Assistant Chat API
|
||||
version: 1.0.0
|
||||
description: API contract for LLM chat assistant command orchestration.
|
||||
|
||||
servers:
|
||||
- url: /api
|
||||
|
||||
paths:
|
||||
/assistant/messages:
|
||||
post:
|
||||
summary: Send user message to assistant
|
||||
description: Parses command, applies safety checks, and returns assistant response state.
|
||||
operationId: sendAssistantMessage
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssistantMessageRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Assistant response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssistantMessageResponse'
|
||||
'400':
|
||||
description: Validation error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'403':
|
||||
description: Forbidden
|
||||
|
||||
/assistant/confirmations/{confirmation_id}/confirm:
|
||||
post:
|
||||
summary: Confirm dangerous operation
|
||||
description: Confirms one pending risky command and executes it once.
|
||||
operationId: confirmAssistantOperation
|
||||
parameters:
|
||||
- in: path
|
||||
name: confirmation_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Confirmation handled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssistantMessageResponse'
|
||||
'400':
|
||||
description: Invalid or expired confirmation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'403':
|
||||
description: Forbidden
|
||||
'404':
|
||||
description: Confirmation not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/assistant/confirmations/{confirmation_id}/cancel:
|
||||
post:
|
||||
summary: Cancel dangerous operation
|
||||
description: Cancels pending confirmation and prevents execution.
|
||||
operationId: cancelAssistantOperation
|
||||
parameters:
|
||||
- in: path
|
||||
name: confirmation_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Confirmation cancelled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssistantMessageResponse'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'403':
|
||||
description: Forbidden
|
||||
'404':
|
||||
description: Confirmation not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/assistant/history:
|
||||
get:
|
||||
summary: Get assistant conversation history
|
||||
description: Returns paginated history for current user.
|
||||
operationId: getAssistantHistory
|
||||
parameters:
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- in: query
|
||||
name: page_size
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
responses:
|
||||
'200':
|
||||
description: Conversation history page
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssistantHistoryPage'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
components:
|
||||
schemas:
|
||||
AssistantResponseState:
|
||||
type: string
|
||||
enum:
|
||||
- started
|
||||
- success
|
||||
- failed
|
||||
- denied
|
||||
- needs_confirmation
|
||||
- needs_clarification
|
||||
|
||||
RiskLevel:
|
||||
type: string
|
||||
enum:
|
||||
- safe
|
||||
- guarded
|
||||
- dangerous
|
||||
|
||||
CommandIntent:
|
||||
type: object
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
operation:
|
||||
type: string
|
||||
entities:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
confidence:
|
||||
type: number
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
risk_level:
|
||||
$ref: '#/components/schemas/RiskLevel'
|
||||
required: [domain, operation, confidence, risk_level]
|
||||
|
||||
AssistantMessageRequest:
|
||||
type: object
|
||||
properties:
|
||||
conversation_id:
|
||||
type: string
|
||||
nullable: true
|
||||
message:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 4000
|
||||
required: [message]
|
||||
|
||||
AssistantAction:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [confirm, cancel, open_task, open_reports, rephrase]
|
||||
label:
|
||||
type: string
|
||||
target:
|
||||
type: string
|
||||
nullable: true
|
||||
required: [type, label]
|
||||
|
||||
AssistantMessageResponse:
|
||||
type: object
|
||||
properties:
|
||||
conversation_id:
|
||||
type: string
|
||||
response_id:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/AssistantResponseState'
|
||||
text:
|
||||
type: string
|
||||
intent:
|
||||
$ref: '#/components/schemas/CommandIntent'
|
||||
confirmation_id:
|
||||
type: string
|
||||
nullable: true
|
||||
task_id:
|
||||
type: string
|
||||
nullable: true
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AssistantAction'
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required: [conversation_id, response_id, state, text, created_at]
|
||||
|
||||
AssistantMessage:
|
||||
type: object
|
||||
properties:
|
||||
message_id:
|
||||
type: string
|
||||
conversation_id:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
enum: [user, assistant]
|
||||
text:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/AssistantResponseState'
|
||||
task_id:
|
||||
type: string
|
||||
nullable: true
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required: [message_id, conversation_id, role, text, created_at]
|
||||
|
||||
AssistantHistoryPage:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AssistantMessage'
|
||||
total:
|
||||
type: integer
|
||||
minimum: 0
|
||||
page:
|
||||
type: integer
|
||||
minimum: 1
|
||||
page_size:
|
||||
type: integer
|
||||
minimum: 1
|
||||
has_next:
|
||||
type: boolean
|
||||
required: [items, total, page, page_size, has_next]
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
detail:
|
||||
type: string
|
||||
required: [detail]
|
||||
156
specs/021-llm-project-assistant/contracts/modules.md
Normal file
156
specs/021-llm-project-assistant/contracts/modules.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Module Contracts: LLM Chat Assistant for Project Operations
|
||||
|
||||
## Backend Assistant Orchestrator Module
|
||||
|
||||
# [DEF:AssistantOrchestratorModule:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: [assistant, intent, dispatch, safety]
|
||||
# @PURPOSE: Parse chat commands, decide execution path, and orchestrate safe dispatch to existing operations.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: DEPENDS_ON -> [DEF:AssistantIntentParserModule]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:AssistantSecurityGuardModule]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:AssistantExecutionAdapterModule]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:AssistantAuditLogModule]
|
||||
# @INVARIANT: No operation is dispatched before permission and risk checks complete.
|
||||
# @PRE: Authenticated user context and non-empty user message are provided.
|
||||
# @POST: Returns one of deterministic states: needs_clarification, denied, needs_confirmation, started, success, failed.
|
||||
# @POST: Long-running operation responses include task_id when task is created.
|
||||
# [/DEF:AssistantOrchestratorModule]
|
||||
|
||||
---
|
||||
|
||||
## Backend Assistant Intent Parser Module
|
||||
|
||||
# [DEF:AssistantIntentParserModule:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: [nlp, intent, entities, confidence]
|
||||
# @PURPOSE: Convert RU/EN natural-language operation requests into normalized intent objects.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: CALLED_BY -> [DEF:AssistantOrchestratorModule]
|
||||
# @INVARIANT: Parsed intent always includes confidence score and risk_level.
|
||||
# @PRE: Input text is available and sanitized.
|
||||
# @POST: Returns normalized intent or unknown intent with low confidence.
|
||||
# [/DEF:AssistantIntentParserModule]
|
||||
|
||||
---
|
||||
|
||||
## Backend Assistant Security Guard Module
|
||||
|
||||
# [DEF:AssistantSecurityGuardModule:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: [rbac, confirmation, risk]
|
||||
# @PURPOSE: Apply RBAC and dangerous-operation confirmation gates before execution.
|
||||
# @LAYER: Security
|
||||
# @RELATION: CALLED_BY -> [DEF:AssistantOrchestratorModule]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:MultiUserAuthModule]
|
||||
# @INVARIANT: Dangerous operations never execute without explicit confirmation token.
|
||||
# @PRE: Normalized intent and authenticated user context are available.
|
||||
# @POST: Returns one of {denied, needs_confirmation, allowed}.
|
||||
# [/DEF:AssistantSecurityGuardModule]
|
||||
|
||||
---
|
||||
|
||||
## Backend Assistant Execution Adapter Module
|
||||
|
||||
# [DEF:AssistantExecutionAdapterModule:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: [adapter, task_manager, integrations]
|
||||
# @PURPOSE: Map validated execution requests to existing Git/migration/backup/LLM/task status APIs.
|
||||
# @LAYER: Integration
|
||||
# @RELATION: CALLED_BY -> [DEF:AssistantOrchestratorModule]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:TasksRouter:Module]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:backend.src.api.routes.git:Module]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:backend.src.api.routes.migration:Module]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:backend/src/api/routes/llm.py:Module]
|
||||
# @INVARIANT: Adapter reuses existing execution paths and does not duplicate domain logic.
|
||||
# @PRE: ExecutionRequest is validated and authorized.
|
||||
# @POST: Returns operation result or started task_id for async flows.
|
||||
# [/DEF:AssistantExecutionAdapterModule]
|
||||
|
||||
---
|
||||
|
||||
## Backend Assistant Audit Log Module
|
||||
|
||||
# [DEF:AssistantAuditLogModule:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: [audit, traceability, observability]
|
||||
# @PURPOSE: Persist assistant command processing decisions and outcomes.
|
||||
# @LAYER: Infra
|
||||
# @RELATION: CALLED_BY -> [DEF:AssistantOrchestratorModule]
|
||||
# @INVARIANT: Every processed command emits one audit record.
|
||||
# @PRE: Command processing context is available.
|
||||
# @POST: Structured audit entry persisted with decision/outcome/task_id (if present).
|
||||
# [/DEF:AssistantAuditLogModule]
|
||||
|
||||
---
|
||||
|
||||
## Backend Assistant API Module
|
||||
|
||||
# [DEF:AssistantApiContract:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: [api, assistant, chat, confirm]
|
||||
# @PURPOSE: Provide chat message, confirmation, and history endpoints for assistant workflows.
|
||||
# @LAYER: Interface
|
||||
# @RELATION: DEPENDS_ON -> [DEF:AssistantOrchestratorModule]
|
||||
# @INVARIANT: API responses are deterministic and machine-readable for frontend state handling.
|
||||
# @PRE: User authenticated.
|
||||
# @POST: Message endpoint returns assistant response state + metadata.
|
||||
# @POST: Confirm endpoint executes pending dangerous command exactly once.
|
||||
# @POST: Cancel endpoint prevents execution of pending dangerous command.
|
||||
# [/DEF:AssistantApiContract]
|
||||
|
||||
---
|
||||
|
||||
## Frontend Assistant Chat Panel Contract
|
||||
|
||||
<!-- [DEF:AssistantChatPanel:Component] -->
|
||||
/**
|
||||
* @TIER: CRITICAL
|
||||
* @SEMANTICS: [ui, chat, commands, feedback]
|
||||
* @PURPOSE: Render in-app assistant conversation and operational command interactions.
|
||||
* @LAYER: UI
|
||||
* @RELATION: DEPENDS_ON -> [DEF:AssistantApiClient]
|
||||
* @RELATION: USES -> [DEF:TaskDrawerStore]
|
||||
* @INVARIANT: Every assistant response is rendered with explicit state badge.
|
||||
* @PRE: User is authenticated and assistant panel is accessible.
|
||||
* @POST: User can send command, receive response, and confirm/cancel risky operations.
|
||||
* @UX_STATE: Idle -> Prompt and examples are visible.
|
||||
* @UX_STATE: Parsing -> Temporary loading message shown.
|
||||
* @UX_STATE: NeedsConfirmation -> Confirmation card with Confirm/Cancel actions displayed.
|
||||
* @UX_STATE: Started -> task_id chip and tracking hint displayed.
|
||||
* @UX_STATE: Error -> Error card with retry/rephrase guidance displayed.
|
||||
* @UX_RECOVERY: User can rephrase ambiguous command or retry after error.
|
||||
*/
|
||||
<!-- [/DEF:AssistantChatPanel] -->
|
||||
|
||||
---
|
||||
|
||||
## Frontend Assistant API Client Contract
|
||||
|
||||
# [DEF:AssistantApiClient:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: [frontend, api_client, assistant]
|
||||
# @PURPOSE: Wrap assistant API requests through existing frontend API helpers.
|
||||
# @LAYER: Infra
|
||||
# @RELATION: CALLS -> [DEF:AssistantApiContract]
|
||||
# @INVARIANT: No direct native fetch bypassing project API wrapper conventions.
|
||||
# @PRE: Valid auth context/token exists.
|
||||
# @POST: Returns typed assistant response payload or structured error object.
|
||||
# [/DEF:AssistantApiClient]
|
||||
|
||||
---
|
||||
|
||||
## Contract Usage Simulation (Key Scenario)
|
||||
|
||||
Scenario traced: User requests production deploy in chat.
|
||||
|
||||
1. `AssistantChatPanel` sends message to `AssistantApiClient`.
|
||||
2. `AssistantApiContract` calls `AssistantOrchestratorModule`.
|
||||
3. `AssistantIntentParserModule` extracts `domain=git`, `operation=deploy`, `target_env=production`, `risk_level=dangerous`.
|
||||
4. `AssistantSecurityGuardModule` returns `needs_confirmation` and issues token.
|
||||
5. UI shows confirmation card.
|
||||
6. User confirms.
|
||||
7. Confirm endpoint calls orchestrator with token.
|
||||
8. `AssistantExecutionAdapterModule` dispatches existing deploy flow and returns `task_id`.
|
||||
9. `AssistantAuditLogModule` records both confirmation request and final dispatch result.
|
||||
10. UI shows `started` state with tracking hint (Task Drawer/reports).
|
||||
156
specs/021-llm-project-assistant/data-model.md
Normal file
156
specs/021-llm-project-assistant/data-model.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Data Model: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Feature**: [`021-llm-project-assistant`](specs/021-llm-project-assistant)
|
||||
**Spec**: [`spec.md`](specs/021-llm-project-assistant/spec.md)
|
||||
**Research**: [`research.md`](specs/021-llm-project-assistant/research.md)
|
||||
|
||||
## 1. Entity: AssistantMessage
|
||||
|
||||
Represents one chat message in assistant conversation.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| message_id | string | Yes | Unique message identifier. |
|
||||
| conversation_id | string | Yes | Logical chat session/thread identifier. |
|
||||
| role | enum | Yes | `user`, `assistant`, `system`. |
|
||||
| text | string | Yes | Message content. |
|
||||
| created_at | datetime | Yes | Message timestamp. |
|
||||
| metadata | object | No | Additional structured metadata (state badge, task link, etc.). |
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- `text` must be non-empty.
|
||||
- `role` must be one of allowed values.
|
||||
- `conversation_id` must be bound to authenticated user scope.
|
||||
|
||||
---
|
||||
|
||||
## 2. Entity: CommandIntent
|
||||
|
||||
Represents parsed intent from user message.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| intent_id | string | Yes | Parsed intent identifier. |
|
||||
| domain | enum | Yes | `git`, `migration`, `backup`, `llm`, `status`, `report`, `unknown`. |
|
||||
| operation | string | Yes | Normalized operation key (e.g., `create_branch`, `execute_migration`). |
|
||||
| entities | object | No | Parsed parameters (`dashboard_id`, `env`, `task_id`, etc.). |
|
||||
| confidence | number | Yes | Confidence score `0.0..1.0`. |
|
||||
| risk_level | enum | Yes | `safe`, `guarded`, `dangerous`. |
|
||||
| requires_confirmation | boolean | Yes | Indicates confirmation gate requirement. |
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- `confidence` must be in `[0.0, 1.0]`.
|
||||
- `requires_confirmation=true` when `risk_level=dangerous`.
|
||||
- Missing mandatory entities triggers `needs_clarification` decision.
|
||||
|
||||
---
|
||||
|
||||
## 3. Entity: ExecutionRequest
|
||||
|
||||
Validated request ready for dispatch to existing backend action.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| execution_id | string | Yes | Execution request identifier. |
|
||||
| actor_user_id | string | Yes | Authenticated user initiating request. |
|
||||
| intent_id | string | Yes | Source `CommandIntent` reference. |
|
||||
| dispatch_target | string | Yes | Internal route/service/plugin target identifier. |
|
||||
| payload | object | Yes | Validated action payload. |
|
||||
| status | enum | Yes | `pending`, `dispatched`, `failed`, `denied`, `cancelled`. |
|
||||
| task_id | string | No | Linked async task id if created. |
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- Dispatch target must map to known execution backend.
|
||||
- Permission checks must pass before `dispatched`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Entity: ConfirmationToken
|
||||
|
||||
One-time confirmation artifact for dangerous operations.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| confirmation_id | string | Yes | Unique confirmation token id. |
|
||||
| actor_user_id | string | Yes | User who must confirm/cancel. |
|
||||
| intent_snapshot | object | Yes | Immutable normalized intent snapshot. |
|
||||
| expires_at | datetime | Yes | Token expiry timestamp. |
|
||||
| state | enum | Yes | `pending`, `confirmed`, `cancelled`, `expired`, `consumed`. |
|
||||
| created_at | datetime | Yes | Creation time. |
|
||||
| consumed_at | datetime | No | Time of finalization. |
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- Only token owner can confirm/cancel.
|
||||
- `confirmed` token transitions to `consumed` after one dispatch.
|
||||
- Expired token cannot transition to `confirmed`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Entity: AssistantExecutionLog
|
||||
|
||||
Structured audit record of assistant decision and outcome.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| log_id | string | Yes | Unique audit id. |
|
||||
| user_id | string | Yes | Actor user id. |
|
||||
| conversation_id | string | Yes | Conversation reference. |
|
||||
| raw_message | string | Yes | Original user message. |
|
||||
| normalized_intent | object | No | Parsed intent snapshot. |
|
||||
| decision | enum | Yes | `executed`, `needs_confirmation`, `needs_clarification`, `denied`, `failed`. |
|
||||
| outcome | enum | Yes | `success`, `error`, `started`, `cancelled`, `n/a`. |
|
||||
| task_id | string | No | Linked async task id. |
|
||||
| created_at | datetime | Yes | Audit timestamp. |
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- Audit entry required for every command-processing attempt.
|
||||
- `task_id` required when `outcome=started` for async operations.
|
||||
|
||||
---
|
||||
|
||||
## 6. Relationships
|
||||
|
||||
- `AssistantMessage` (user role) -> may produce one `CommandIntent`.
|
||||
- `CommandIntent` -> may produce one `ExecutionRequest` or `ConfirmationToken`.
|
||||
- `ConfirmationToken` (confirmed) -> produces one `ExecutionRequest`.
|
||||
- `ExecutionRequest` -> may link to one async `task_id` in existing task manager.
|
||||
- Every processed command -> one `AssistantExecutionLog`.
|
||||
|
||||
---
|
||||
|
||||
## 7. State Flows
|
||||
|
||||
### Command Decision Flow
|
||||
|
||||
`received` -> `parsed` -> (`needs_clarification` | `denied` | `needs_confirmation` | `dispatch`)
|
||||
|
||||
### Confirmation Flow
|
||||
|
||||
`pending` -> (`confirmed` -> `consumed`) | `cancelled` | `expired`
|
||||
|
||||
### Execution Flow
|
||||
|
||||
`pending` -> (`dispatched` -> `started/success`) | `failed` | `denied` | `cancelled`
|
||||
|
||||
---
|
||||
|
||||
## 8. Scale Assumptions
|
||||
|
||||
- Conversation history is moderate per user and can be paginated.
|
||||
- Command audit retention aligns with existing operational retention policies.
|
||||
- Assistant parse step must remain lightweight and avoid blocking task execution system.
|
||||
107
specs/021-llm-project-assistant/plan.md
Normal file
107
specs/021-llm-project-assistant/plan.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Implementation Plan: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Branch**: `021-llm-project-assistant` | **Date**: 2026-02-23 | **Spec**: [`/home/busya/dev/ss-tools/specs/021-llm-project-assistant/spec.md`](specs/021-llm-project-assistant/spec.md)
|
||||
**Input**: Feature specification from [`/specs/021-llm-project-assistant/spec.md`](specs/021-llm-project-assistant/spec.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Implement an embedded LLM chat assistant that accepts natural-language commands and orchestrates existing `ss-tools` operations across Git, migration/backup, LLM analysis/documentation, and task status flows. The design reuses current backend routes/plugins and task manager, adds intent parsing + safe dispatch layer, enforces existing RBAC checks, and introduces explicit confirmation workflow for risky operations (especially production deploy).
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.9+ (backend), Node.js 18+ (frontend)
|
||||
**Primary Dependencies**: FastAPI, existing TaskManager, existing plugin routes/services, SvelteKit chat UI components, configured LLM providers
|
||||
**Storage**: Existing DB for auth/tasks/config + new assistant command audit persistence (same backend DB stack)
|
||||
**Testing**: pytest (backend), Vitest (frontend), API contract tests for assistant endpoints
|
||||
**Target Platform**: Linux backend + browser SPA frontend
|
||||
**Project Type**: Web application (backend + frontend)
|
||||
**Performance Goals**: Parse/dispatch response under 2s p95 (excluding async task runtime); confirmation round-trip under 1s p95
|
||||
**Constraints**: Must not bypass existing permissions; dangerous ops require explicit confirmation; must reuse existing task progress surfaces (Task Drawer/reports)
|
||||
**Scale/Scope**: Multi-command operational assistant used by authenticated operators; daily command volume expected in hundreds to low thousands
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Gate | Status | Notes |
|
||||
|---|---|---|
|
||||
| Semantic Protocol Compliance ([`constitution.md`](.ai/standards/constitution.md)) | PASS | Contracts will define assistant modules with DEF tags and PRE/POST semantics. |
|
||||
| Plugin/Service Boundaries | PASS | Assistant dispatch layer will call existing routes/services/plugins only; no direct bypass logic. |
|
||||
| Security & RBAC | PASS | Command dispatch requires existing `has_permission` checks and user context propagation. |
|
||||
| Async Task Architecture | PASS | Long-running operations stay task-based; assistant returns `task_id` immediately. |
|
||||
| UX Consistency | PASS | Chat states map to explicit UX states (`needs_confirmation`, `started`, `failed`, etc.). |
|
||||
| Auditability | PASS | Assistant command audit log added as mandatory trail for execution decisions. |
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/021-llm-project-assistant/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── ux_reference.md
|
||||
├── contracts/
|
||||
│ ├── modules.md
|
||||
│ └── assistant-api.openapi.yaml
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── api/
|
||||
│ │ └── routes/
|
||||
│ ├── services/
|
||||
│ ├── models/
|
||||
│ └── core/
|
||||
└── tests/
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── components/
|
||||
│ │ ├── stores/
|
||||
│ │ └── api/
|
||||
│ └── routes/
|
||||
└── tests/
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep current backend/frontend split and implement assistant as a new bounded feature slice (assistant API route + intent/dispatch service + chat UI components) that integrates into existing auth/task/report ecosystems.
|
||||
|
||||
## Phase 0: Research Focus
|
||||
|
||||
1. Intent parsing strategy for RU/EN operational commands with deterministic fallback behavior.
|
||||
2. Risk classification strategy for commands requiring explicit confirmation (e.g., prod deploy, prod migration).
|
||||
3. Integration mapping from intents to existing operational endpoints/plugins.
|
||||
4. Confirmation token lifecycle and idempotency guarantees.
|
||||
5. Audit logging schema and retention alignment with task/audit expectations.
|
||||
|
||||
## Phase 1: Design & Contracts Plan
|
||||
|
||||
1. Define canonical assistant interaction model (`message -> intent -> decision -> dispatch/result`).
|
||||
2. Produce [`data-model.md`](specs/021-llm-project-assistant/data-model.md) for message, intent, confirmation, and audit entities.
|
||||
3. Produce module contracts in [`contracts/modules.md`](specs/021-llm-project-assistant/contracts/modules.md) with PRE/POST + UX states.
|
||||
4. Produce API contract in [`contracts/assistant-api.openapi.yaml`](specs/021-llm-project-assistant/contracts/assistant-api.openapi.yaml).
|
||||
5. Produce [`quickstart.md`](specs/021-llm-project-assistant/quickstart.md) with validation flow.
|
||||
6. Decompose to execution tasks in [`tasks.md`](specs/021-llm-project-assistant/tasks.md) by user stories.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
No constitution violations identified.
|
||||
|
||||
## Test Data Reference
|
||||
|
||||
| Component | TIER | Fixture Name | Location |
|
||||
|---|---|---|---|
|
||||
| Assistant intent parsing | CRITICAL | assistant_command_git_branch | [`spec.md`](specs/021-llm-project-assistant/spec.md) |
|
||||
| Dangerous operation confirmation | CRITICAL | assistant_command_prod_deploy_confirmation | [`spec.md`](specs/021-llm-project-assistant/spec.md) |
|
||||
| Task status query resolution | STANDARD | assistant_status_query | [`spec.md`](specs/021-llm-project-assistant/spec.md) |
|
||||
|
||||
**Note**: Tester implementation should materialize these fixtures in backend/frontend test suites during `/speckit.tasks` execution.
|
||||
92
specs/021-llm-project-assistant/quickstart.md
Normal file
92
specs/021-llm-project-assistant/quickstart.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Quickstart: LLM Chat Assistant for Project Operations
|
||||
|
||||
## Purpose
|
||||
|
||||
Implement and validate assistant-driven project operations defined in:
|
||||
- Spec: [`spec.md`](specs/021-llm-project-assistant/spec.md)
|
||||
- UX reference: [`ux_reference.md`](specs/021-llm-project-assistant/ux_reference.md)
|
||||
- Plan: [`plan.md`](specs/021-llm-project-assistant/plan.md)
|
||||
- Data model: [`data-model.md`](specs/021-llm-project-assistant/data-model.md)
|
||||
- Contracts: [`contracts/modules.md`](specs/021-llm-project-assistant/contracts/modules.md), [`contracts/assistant-api.openapi.yaml`](specs/021-llm-project-assistant/contracts/assistant-api.openapi.yaml)
|
||||
|
||||
## 1) Backend implementation flow
|
||||
|
||||
1. Add assistant route module (`/api/assistant/*`).
|
||||
2. Implement intent parser service (hybrid deterministic + LLM fallback).
|
||||
3. Implement security guard and confirmation token workflow.
|
||||
4. Implement execution adapter mapping intents to existing Git/migration/backup/LLM/tasks operations.
|
||||
5. Add structured assistant audit logging.
|
||||
|
||||
## 2) Frontend implementation flow
|
||||
|
||||
1. Add assistant chat panel component and open/close controls in main layout.
|
||||
2. Add assistant API client using existing frontend API wrapper.
|
||||
3. Render assistant response states (`needs_confirmation`, `started`, `denied`, etc.).
|
||||
4. Add confirm/cancel actions and task tracking chips linking to Task Drawer/reports.
|
||||
5. Add i18n keys for RU/EN assistant labels and status texts.
|
||||
|
||||
## 3) Safety checks (must pass)
|
||||
|
||||
- Unauthorized command execution is denied.
|
||||
- Dangerous operations require confirmation and do not auto-run.
|
||||
- Expired/cancelled confirmation never executes.
|
||||
- Confirm endpoint is idempotent (single execution).
|
||||
|
||||
## 4) UX conformance checks (must pass)
|
||||
|
||||
- Chat interaction available in app shell.
|
||||
- Immediate "started" response with `task_id` for long jobs.
|
||||
- Clear clarification prompt on ambiguous commands.
|
||||
- Clear error text and recovery guidance on failed dispatch.
|
||||
|
||||
## 5) Contract checks (must pass)
|
||||
|
||||
- Assistant endpoints conform to [`assistant-api.openapi.yaml`](specs/021-llm-project-assistant/contracts/assistant-api.openapi.yaml).
|
||||
- Response states always included and valid enum values.
|
||||
- Confirmation flow returns deterministic machine-readable output.
|
||||
|
||||
## 6) Suggested validation commands
|
||||
|
||||
Backend tests:
|
||||
```bash
|
||||
cd backend && .venv/bin/python3 -m pytest
|
||||
```
|
||||
|
||||
Frontend tests:
|
||||
```bash
|
||||
cd frontend && npm test
|
||||
```
|
||||
|
||||
Targeted assistant tests (when implemented):
|
||||
```bash
|
||||
cd backend && .venv/bin/python3 -m pytest tests -k assistant
|
||||
cd frontend && npm test -- assistant
|
||||
```
|
||||
|
||||
## 7) Done criteria for planning handoff
|
||||
|
||||
- All planning artifacts for `021` exist and are mutually consistent.
|
||||
- Risk confirmation and RBAC enforcement are explicit in spec and contracts.
|
||||
- Task-tracking integration through existing Task Drawer/reports is reflected in contracts and tasks.
|
||||
- Ready for implementation via `tasks.md`.
|
||||
|
||||
## 8) Validation Log (2026-02-23)
|
||||
|
||||
### Frontend
|
||||
|
||||
- `cd frontend && npx vitest run src/lib/components/assistant/__tests__/assistant_chat.integration.test.js src/lib/components/assistant/__tests__/assistant_confirmation.integration.test.js src/lib/stores/__tests__/assistantChat.test.js`
|
||||
- Result: **PASS** (`11 passed`)
|
||||
- `cd frontend && npm run build`
|
||||
- Result: **PASS** (build successful; project has pre-existing non-blocking a11y warnings outside assistant scope)
|
||||
|
||||
### Backend
|
||||
|
||||
- `python3 -m py_compile backend/src/api/routes/assistant.py backend/src/models/assistant.py backend/src/api/routes/__tests__/test_assistant_api.py backend/src/api/routes/__tests__/test_assistant_authz.py`
|
||||
- Result: **PASS**
|
||||
- `cd backend && .venv/bin/python -m pytest -q src/api/routes/__tests__/test_assistant_api.py src/api/routes/__tests__/test_assistant_authz.py`
|
||||
- Result: **BLOCKED in current env** (runtime hang during request execution after heavy app bootstrap; collection succeeds)
|
||||
|
||||
### Semantics
|
||||
|
||||
- `python3 generate_semantic_map.py`
|
||||
- Result: **PASS** (updated semantic artifacts: `.ai/PROJECT_MAP.md`, `.ai/MODULE_MAP.md`, `semantics/semantic_map.json`)
|
||||
97
specs/021-llm-project-assistant/research.md
Normal file
97
specs/021-llm-project-assistant/research.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Phase 0 Research: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Feature**: [`021-llm-project-assistant`](specs/021-llm-project-assistant)
|
||||
**Input Spec**: [`spec.md`](specs/021-llm-project-assistant/spec.md)
|
||||
**Related UX**: [`ux_reference.md`](specs/021-llm-project-assistant/ux_reference.md)
|
||||
|
||||
## 1) Intent parsing for operational commands (RU/EN)
|
||||
|
||||
### Decision
|
||||
Use hybrid parsing:
|
||||
1. Lightweight deterministic recognizers for high-signal commands/entities (IDs, env names, keywords).
|
||||
2. LLM interpretation for ambiguous/free-form phrasing.
|
||||
3. Confidence threshold gate (`needs_clarification` when below threshold).
|
||||
|
||||
### Rationale
|
||||
Deterministic layer reduces hallucination risk for critical operations; LLM layer preserves natural-language flexibility.
|
||||
|
||||
### Alternatives considered
|
||||
- **Pure LLM parser only**: rejected due to unsafe variance for critical ops.
|
||||
- **Pure regex/rules only**: rejected due to low language flexibility and high maintenance.
|
||||
|
||||
---
|
||||
|
||||
## 2) Risk classification and confirmation policy
|
||||
|
||||
### Decision
|
||||
Introduce explicit risk levels per command template:
|
||||
- `safe`: read/status operations.
|
||||
- `guarded`: state-changing non-production operations.
|
||||
- `dangerous`: production deploy/migration and similarly impactful actions.
|
||||
|
||||
`dangerous` always requires explicit confirmation token before execution.
|
||||
|
||||
### Rationale
|
||||
This maps to user acceptance criteria and prevents accidental production-impact operations.
|
||||
|
||||
### Alternatives considered
|
||||
- **Prompt-only warning without hard confirmation gate**: rejected as insufficiently safe.
|
||||
- **Confirmation for every command**: rejected due to severe UX friction.
|
||||
|
||||
---
|
||||
|
||||
## 3) Execution integration strategy
|
||||
|
||||
### Decision
|
||||
Assistant dispatch calls existing internal services/endpoints instead of duplicating business logic.
|
||||
|
||||
Target mappings:
|
||||
- Git -> existing git routes/service methods.
|
||||
- Migration/backup/LLM analysis -> task creation with existing plugin IDs.
|
||||
- Status/report -> existing tasks/reports APIs.
|
||||
|
||||
### Rationale
|
||||
Reusing existing execution paths preserves permission checks and reduces regression risk.
|
||||
|
||||
### Alternatives considered
|
||||
- **New parallel execution engine**: rejected (duplicated logic, bypass risk).
|
||||
|
||||
---
|
||||
|
||||
## 4) Confirmation token lifecycle
|
||||
|
||||
### Decision
|
||||
Confirmation token model with one-time usage + TTL + user binding.
|
||||
- Token includes normalized command snapshot + risk metadata.
|
||||
- Confirm executes exactly once.
|
||||
- Expired/cancelled token cannot be reused.
|
||||
|
||||
### Rationale
|
||||
Prevents duplicate destructive execution and supports explicit audit trail.
|
||||
|
||||
### Alternatives considered
|
||||
- **Session flag confirmation without token**: rejected due to weak idempotency guarantees.
|
||||
|
||||
---
|
||||
|
||||
## 5) Auditability and observability model
|
||||
|
||||
### Decision
|
||||
Log each assistant interaction as structured audit entry:
|
||||
- actor, raw message, parsed intent, confidence, risk level, decision, dispatch target, outcome, linked `task_id` if any.
|
||||
|
||||
### Rationale
|
||||
Required for post-incident analysis and security traceability.
|
||||
|
||||
### Alternatives considered
|
||||
- **Log only successful executions**: rejected (misses denied/failed attempts and ambiguity events).
|
||||
|
||||
---
|
||||
|
||||
## Consolidated Research Outcomes for Planning
|
||||
|
||||
- Hybrid parser with confidence gating is selected.
|
||||
- Risk-classified confirmation flow is mandatory for dangerous operations.
|
||||
- Existing APIs/plugins are the only execution backends.
|
||||
- One-time confirmation token with TTL is required for safety and idempotency.
|
||||
- Structured assistant audit logging is required for operational trust.
|
||||
187
specs/021-llm-project-assistant/spec.md
Normal file
187
specs/021-llm-project-assistant/spec.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Feature Specification: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Feature Branch**: `021-llm-project-assistant`
|
||||
**Reference UX**: `ux_reference.md` (See specific folder)
|
||||
**Created**: 2026-02-23
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Создать чат-ассистента на базе LLM в веб-интерфейсе, чтобы управлять Git, миграциями/бэкапами, LLM-анализом и статусами задач через команды на естественном языке."
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Use chat instead of manual UI navigation (Priority: P1)
|
||||
|
||||
As an operator, I can open an in-app chat and submit natural-language commands so I can trigger key actions without navigating multiple pages.
|
||||
|
||||
**Why this priority**: Chat entrypoint is the core product value. Without it, no assistant workflow exists.
|
||||
|
||||
**Independent Test**: Open assistant panel, send a command like "создай ветку feature-abc для дашборда 42", and verify that assistant returns a structured actionable response.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an authenticated user on any main page, **When** they open the assistant and send a message, **Then** the message appears in chat history and receives a response.
|
||||
2. **Given** assistant is available, **When** the user asks for supported actions, **Then** assistant returns clear examples for Git, migration/backup, analysis, and task status queries.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Execute core operations via natural language commands (Priority: P1)
|
||||
|
||||
As an operator, I can ask assistant to execute core system operations (Git, migration/backup, LLM analysis/docs, status/report checks) so I can run workflows faster.
|
||||
|
||||
**Why this priority**: Command execution across core modules defines feature completeness.
|
||||
|
||||
**Independent Test**: Send one command per capability group and verify that the corresponding backend task/API action is initiated with valid parameters.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** user command targets Git operation, **When** assistant parses intent and entities, **Then** system starts corresponding Git operation (branch/commit/deploy) or returns explicit validation error.
|
||||
2. **Given** user command targets migration or backup, **When** assistant executes command, **Then** system starts async task and returns created `task_id` immediately.
|
||||
3. **Given** user command targets LLM validation/documentation, **When** assistant executes command, **Then** system triggers the relevant LLM task/plugin and returns launch confirmation with `task_id`.
|
||||
4. **Given** user asks task/report status, **When** assistant handles request, **Then** it returns latest known status and linkable reference to existing task tracking UI.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Safe execution with RBAC and explicit confirmations (Priority: P1)
|
||||
|
||||
As a system administrator, I need assistant actions to respect existing permissions and require explicit user confirmation for risky operations.
|
||||
|
||||
**Why this priority**: Security and production safety are mandatory gate criteria.
|
||||
|
||||
**Independent Test**: Run restricted command with unauthorized user (expect deny), and run production deploy command (expect confirm-before-execute flow).
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** user lacks permission for requested operation, **When** assistant tries to execute command, **Then** execution is denied and assistant returns permission error without side effects.
|
||||
2. **Given** command is classified as dangerous (e.g., deploy to production), **When** assistant detects it, **Then** assistant requires explicit user confirmation before creating task.
|
||||
3. **Given** confirmation is requested, **When** user cancels or confirmation expires, **Then** operation is not executed.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Reliable feedback and progress tracking (Priority: P2)
|
||||
|
||||
As an operator, I need immediate operation feedback, clear errors, and traceable progress for long-running tasks.
|
||||
|
||||
**Why this priority**: Strong feedback loop is required for operational trust and usability.
|
||||
|
||||
**Independent Test**: Launch long migration via chat and verify immediate "started" message with `task_id`, then check progress in Task Drawer and reports page.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** assistant starts long operation, **When** execution begins, **Then** assistant responds immediately with initiation status and `task_id`.
|
||||
2. **Given** operation succeeds or fails, **When** task result becomes available, **Then** assistant can return outcome summary (success/error) on user request.
|
||||
3. **Given** assistant cannot parse command confidently, **When** ambiguity is detected, **Then** assistant asks clarification question and does not execute action.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- User command matches multiple possible operations (ambiguous intent).
|
||||
- User references non-existent dashboard/environment/task IDs.
|
||||
- Duplicate command submission (double-send) for destructive operations.
|
||||
- Confirmation token expires before user confirms.
|
||||
- Provider/LLM unavailable during command interpretation.
|
||||
- Task creation succeeds but downstream execution fails immediately.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST provide an in-application chat interface for assistant interaction.
|
||||
- **FR-002**: System MUST preserve chat context per user session for at least the active session duration.
|
||||
- **FR-003**: Assistant MUST support natural-language command parsing for these domains: Git, migration/backup, LLM analysis/documentation, task/report status.
|
||||
- **FR-004**: Assistant MUST map parsed commands to existing internal operations/APIs and MUST NOT bypass current service boundaries.
|
||||
- **FR-005**: Assistant MUST support Git commands for at least: branch creation, commit initiation, deploy initiation.
|
||||
- **FR-006**: Assistant MUST support migration and backup launch commands and return created `task_id`.
|
||||
- **FR-007**: Assistant MUST support LLM validation/documentation launch commands and return created `task_id`.
|
||||
- **FR-008**: Assistant MUST support status queries for existing tasks by `task_id` and by recent user scope.
|
||||
- **FR-009**: Assistant MUST enforce existing RBAC/permission checks before any operation execution.
|
||||
- **FR-010**: Assistant MUST classify risky operations and require explicit confirmation before execution.
|
||||
- **FR-011**: Confirmation flow MUST include timeout/expiry semantics and explicit cancel path.
|
||||
- **FR-012**: Assistant MUST provide immediate response for long-running operations containing `task_id` and a short tracking hint.
|
||||
- **FR-013**: Assistant responses MUST include operation result state: `success`, `failed`, `started`, `needs_confirmation`, `needs_clarification`, or `denied`.
|
||||
- **FR-014**: Assistant MUST surface actionable error details without exposing secrets.
|
||||
- **FR-015**: Assistant MUST log every attempted assistant command with actor, intent, parameters snapshot, and outcome for auditability.
|
||||
- **FR-016**: Assistant MUST prevent duplicate execution for the same pending confirmation token.
|
||||
- **FR-017**: Assistant MUST support multilingual command input at minimum for Russian and English operational phrasing.
|
||||
- **FR-018**: Assistant MUST degrade safely when intent confidence is below threshold by requesting clarification instead of executing.
|
||||
- **FR-019**: Assistant MUST link users to existing progress surfaces (Task Drawer and reports page) for task tracking.
|
||||
- **FR-020**: Assistant MUST support retrieval of last N executed assistant commands for operational traceability.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **AssistantMessage**: One chat message (user or assistant) with timestamp, role, text, and metadata.
|
||||
- **CommandIntent**: Parsed intent structure containing domain, operation, entities, confidence, and risk level.
|
||||
- **ExecutionRequest**: Validated command payload mapped to a concrete backend action.
|
||||
- **ConfirmationToken**: Pending confirmation record for risky operations with TTL and one-time usage.
|
||||
- **AssistantExecutionLog**: Audit trail entry for command attempts and outcomes.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: At least 90% of predefined core commands (Git/migration/backup/LLM/status) are correctly interpreted in acceptance test set.
|
||||
- **SC-002**: 100% of risky operations (production deploy and equivalent) require explicit confirmation before execution.
|
||||
- **SC-003**: 100% of assistant-started long-running operations return a `task_id` in the first response.
|
||||
- **SC-004**: Permission bypass rate is 0% in security tests (unauthorized commands never execute).
|
||||
- **SC-005**: At least 95% of assistant responses return within 2 seconds for parse/dispatch stage (excluding downstream task runtime).
|
||||
- **SC-006**: At least 90% of operators can launch a target operation from chat faster than through manual multi-page navigation in usability checks.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Existing APIs/plugins for Git, migration, backup, LLM actions, and task status remain authoritative execution backends.
|
||||
- Existing RBAC permissions (`plugin:*`, `tasks:*`, `admin:*`) remain the access model.
|
||||
- Task Drawer and reports page remain current progress/result surfaces and will be reused.
|
||||
- LLM assistant orchestration can use configured provider stack without introducing a separate auth model.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Existing routes/services for Git (`/api/git/...`), migration (`/api/execute`), task APIs (`/api/tasks/...`), and LLM provider/task flows.
|
||||
- Existing authentication and authorization components from multi-user auth implementation.
|
||||
- Existing task manager persistence/logging for async execution tracking.
|
||||
|
||||
---
|
||||
|
||||
## Test Data Fixtures *(recommended for CRITICAL components)*
|
||||
|
||||
### Fixtures
|
||||
|
||||
```yaml
|
||||
assistant_command_git_branch:
|
||||
description: "Valid RU command for branch creation"
|
||||
data:
|
||||
message: "сделай ветку feature/revenue-v2 для дашборда 42"
|
||||
expected:
|
||||
domain: "git"
|
||||
operation: "create_branch"
|
||||
entities:
|
||||
dashboard_id: 42
|
||||
branch_name: "feature/revenue-v2"
|
||||
|
||||
assistant_command_migration_start:
|
||||
description: "Valid migration launch command"
|
||||
data:
|
||||
message: "запусти миграцию с dev на prod для дашборда 42"
|
||||
expected:
|
||||
domain: "migration"
|
||||
operation: "execute"
|
||||
requires_confirmation: true
|
||||
|
||||
assistant_command_prod_deploy_confirmation:
|
||||
description: "Risky production deploy requires confirmation"
|
||||
data:
|
||||
message: "задеплой дашборд 42 в production"
|
||||
expected:
|
||||
state: "needs_confirmation"
|
||||
confirmation_required: true
|
||||
|
||||
assistant_status_query:
|
||||
description: "Task status lookup"
|
||||
data:
|
||||
message: "проверь статус задачи task-123"
|
||||
expected:
|
||||
domain: "status"
|
||||
operation: "get_task"
|
||||
entities:
|
||||
task_id: "task-123"
|
||||
```
|
||||
200
specs/021-llm-project-assistant/tasks.md
Normal file
200
specs/021-llm-project-assistant/tasks.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Tasks: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Input**: Design documents from [`/specs/021-llm-project-assistant/`](specs/021-llm-project-assistant)
|
||||
**Prerequisites**: [`plan.md`](specs/021-llm-project-assistant/plan.md), [`spec.md`](specs/021-llm-project-assistant/spec.md), [`ux_reference.md`](specs/021-llm-project-assistant/ux_reference.md), [`research.md`](specs/021-llm-project-assistant/research.md), [`data-model.md`](specs/021-llm-project-assistant/data-model.md), [`contracts/`](specs/021-llm-project-assistant/contracts)
|
||||
|
||||
**Tests**: Include backend contract/integration tests and frontend integration/state tests.
|
||||
|
||||
**Organization**: Tasks are grouped by user story for independent implementation and validation.
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Prepare assistant feature scaffolding and fixtures.
|
||||
|
||||
- [ ] T001 Create assistant feature folders in `backend/src/services/assistant/`, `frontend/src/lib/components/assistant/`, `frontend/src/lib/api/assistant.js`
|
||||
- [ ] T002 [P] Add backend fixtures for assistant parsing/confirmation in `backend/tests/fixtures/assistant/fixtures_assistant.json`
|
||||
- [ ] T003 [P] Add frontend fixtures for assistant response states in `frontend/src/lib/components/assistant/__tests__/fixtures/assistant.fixtures.js`
|
||||
- [x] T004 [P] Add i18n placeholders for assistant UI in `frontend/src/lib/i18n/locales/en.json` and `frontend/src/lib/i18n/locales/ru.json`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Build core assistant architecture required by all stories.
|
||||
|
||||
- [x] T005 Implement assistant domain models (`AssistantMessage`, `CommandIntent`, `ConfirmationToken`, `AssistantExecutionLog`) in `backend/src/models/assistant.py`
|
||||
- [ ] T006 [P] Implement intent parser service with confidence output in `backend/src/services/assistant/intent_parser.py`
|
||||
- [ ] T007 Implement RBAC + risk guard service in `backend/src/services/assistant/security_guard.py`
|
||||
- [ ] T008 Implement execution adapter to existing operations in `backend/src/services/assistant/execution_adapter.py`
|
||||
- [ ] T009 Implement assistant orchestrator service in `backend/src/services/assistant/orchestrator.py`
|
||||
- [ ] T010 [P] Implement assistant audit logging service/repository in `backend/src/services/assistant/audit_service.py`
|
||||
- [x] T011 Create assistant API route module in `backend/src/api/routes/assistant.py` (CRITICAL: message/confirm/cancel/history endpoints)
|
||||
- [x] T012 Wire assistant router into `backend/src/api/routes/__init__.py` and `backend/src/app.py`
|
||||
- [x] T013 Create frontend assistant API client in `frontend/src/lib/api/assistant.js`
|
||||
- [ ] T014 Verify contracts alignment between `spec.md` and `contracts/modules.md`
|
||||
|
||||
**Checkpoint**: Foundation complete; user stories can begin.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Use chat instead of manual UI navigation (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Add in-app assistant chat interface with working request/response loop.
|
||||
|
||||
**Independent Test**: Open assistant panel and complete one command-response interaction.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [x] T015 [P] [US1] Add backend API contract tests for `POST /api/assistant/messages` in `backend/src/api/routes/__tests__/test_assistant_api.py`
|
||||
- [x] T016 [P] [US1] Add frontend integration test for chat panel interaction in `frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [x] T017 [US1] Implement assistant chat panel component in `frontend/src/lib/components/assistant/AssistantChatPanel.svelte`
|
||||
- [x] T018 [US1] Add assistant launch trigger to top navigation in `frontend/src/lib/components/layout/TopNavbar.svelte`
|
||||
- [x] T019 [US1] Add chat state store in `frontend/src/lib/stores/assistantChat.js`
|
||||
- [x] T020 [US1] Implement message submission and rendering pipeline in `AssistantChatPanel.svelte` with explicit state badges
|
||||
- [x] T021 [US1] Add command examples/help section in `AssistantChatPanel.svelte`
|
||||
- [x] T022 [US1] Validate UX states from [`ux_reference.md`](specs/021-llm-project-assistant/ux_reference.md)
|
||||
|
||||
**Checkpoint**: US1 independently functional and demo-ready.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Execute core operations via natural-language commands (Priority: P1)
|
||||
|
||||
**Goal**: Support command parsing and dispatch for Git/migration/backup/LLM/status operations.
|
||||
|
||||
**Independent Test**: One command per domain executes correct backend path or returns valid error.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [ ] T023 [P] [US2] Add parser tests for RU/EN command variants in `backend/src/services/assistant/__tests__/test_intent_parser.py`
|
||||
- [ ] T024 [P] [US2] Add execution adapter integration tests in `backend/src/services/assistant/__tests__/test_execution_adapter.py`
|
||||
- [ ] T025 [P] [US2] Add frontend state tests for command result rendering in `frontend/src/lib/components/assistant/__tests__/assistant_states.test.js`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T026 [US2] Implement Git command mappings in `execution_adapter.py` (create branch, commit, deploy)
|
||||
- [ ] T027 [US2] Implement migration/backup command mappings in `execution_adapter.py` via existing task creation
|
||||
- [ ] T028 [US2] Implement LLM validation/documentation command mappings in `execution_adapter.py`
|
||||
- [ ] T029 [US2] Implement task/report status query mappings in `execution_adapter.py`
|
||||
- [ ] T030 [US2] Implement confidence gate and clarification responses in `orchestrator.py`
|
||||
- [x] T031 [US2] Render started/success/failed response cards with `task_id` chips in `AssistantChatPanel.svelte`
|
||||
- [x] T032 [US2] Add quick action from `task_id` chip to Task Drawer in `frontend/src/lib/stores/taskDrawer.js` integration points
|
||||
|
||||
**Checkpoint**: US2 independently functional across requested command domains.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Safe execution with RBAC and explicit confirmations (Priority: P1)
|
||||
|
||||
**Goal**: Enforce permissions and confirmation gates for dangerous operations.
|
||||
|
||||
**Independent Test**: Unauthorized command denied; production deploy requires confirm and executes only after confirm.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [x] T033 [P] [US3] Add backend authorization tests in `backend/src/api/routes/__tests__/test_assistant_authz.py`
|
||||
- [ ] T034 [P] [US3] Add backend confirmation lifecycle tests in `backend/src/services/assistant/__tests__/test_confirmation_flow.py`
|
||||
- [x] T035 [P] [US3] Add frontend confirm/cancel UX tests in `frontend/src/lib/components/assistant/__tests__/assistant_confirmation.integration.test.js`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T036 [US3] Implement risk policy matrix in `backend/src/services/assistant/risk_policy.py`
|
||||
- [ ] T037 [US3] Implement confirmation token storage/service in `backend/src/services/assistant/confirmation_service.py`
|
||||
- [x] T038 [US3] Implement confirm/cancel endpoints in `backend/src/api/routes/assistant.py`
|
||||
- [ ] T039 [US3] Enforce one-time token consumption and TTL checks in `confirmation_service.py`
|
||||
- [x] T040 [US3] Add confirmation UI card with explicit operation summary in `AssistantChatPanel.svelte`
|
||||
- [x] T041 [US3] Surface `denied` and `needs_confirmation` assistant states consistently in UI
|
||||
|
||||
**Checkpoint**: US3 independently functional with enforced safety controls.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - Reliable feedback and progress tracking (Priority: P2)
|
||||
|
||||
**Goal**: Provide immediate task start feedback and robust status/report follow-up.
|
||||
|
||||
**Independent Test**: Long operation returns `task_id` immediately and can be tracked via Task Drawer/reports.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [ ] T042 [P] [US4] Add backend tests for started response payload and task linkage in `backend/src/api/routes/__tests__/test_assistant_task_feedback.py`
|
||||
- [ ] T043 [P] [US4] Add frontend integration tests for tracking links/actions in `frontend/src/lib/components/assistant/__tests__/assistant_tracking.integration.test.js`
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T044 [US4] Add response formatter service for consistent assistant states in `backend/src/services/assistant/response_formatter.py`
|
||||
- [x] T045 [US4] Implement history endpoint pagination in `backend/src/api/routes/assistant.py`
|
||||
- [ ] T046 [US4] Implement history loading and pagination in `frontend/src/lib/stores/assistantChat.js`
|
||||
- [x] T047 [US4] Add open-reports quick action for completed/failed tasks in `AssistantChatPanel.svelte`
|
||||
- [ ] T048 [US4] Add structured error mapping and user guidance messages in `response_formatter.py` and frontend render logic
|
||||
|
||||
**Checkpoint**: US4 independently functional with complete feedback loop.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Final quality, compliance, and operational readiness.
|
||||
|
||||
- [ ] T049 [P] Add OpenAPI conformance tests against `specs/021-llm-project-assistant/contracts/assistant-api.openapi.yaml` in `backend/src/api/routes/__tests__/test_assistant_openapi_conformance.py`
|
||||
- [ ] T050 [P] Add audit log persistence tests in `backend/src/services/assistant/__tests__/test_audit_service.py`
|
||||
- [ ] T051 [P] Add frontend performance tests for chat rendering with long history in `frontend/src/lib/components/assistant/__tests__/assistant_performance.test.js`
|
||||
- [ ] T052 Update operational docs in `docs/settings.md` and `README.md` for assistant usage and safety confirmations
|
||||
- [x] T053 Run quickstart validation and record results in `specs/021-llm-project-assistant/quickstart.md`
|
||||
- [ ] T054 Run semantic map generation (`python3 generate_semantic_map.py`) and resolve critical issues in assistant modules
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- Phase 1 has no dependencies.
|
||||
- Phase 2 depends on Phase 1 and blocks all user stories.
|
||||
- Phases 3-6 depend on Phase 2.
|
||||
- Phase 7 depends on completion of selected user stories.
|
||||
|
||||
### User Story Dependency Graph
|
||||
|
||||
- **US1 (P1)**: MVP chat entrypoint.
|
||||
- **US2 (P1)**: Command execution capabilities.
|
||||
- **US3 (P1)**: Safety and security controls.
|
||||
- **US4 (P2)**: Feedback and tracking depth.
|
||||
|
||||
Graph: `US1 -> {US2, US3} -> US4`
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- Setup fixture/i18n tasks: T002, T003, T004.
|
||||
- Foundational tasks: T006/T010 can run in parallel after T005 baseline.
|
||||
- Story tests in each phase marked `[P]` can run in parallel.
|
||||
- US2 domain mappings (T026-T029) can be split across developers.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (Recommended)
|
||||
|
||||
1. Complete Phase 1 + Phase 2.
|
||||
2. Complete Phase 3 (US1).
|
||||
3. Complete minimal subset of US2 for one domain (e.g., migration + status) and validate.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Deliver US1 chat shell.
|
||||
2. Add US2 command domains iteratively.
|
||||
3. Add US3 confirmation and RBAC hardening.
|
||||
4. Add US4 history/tracking refinements.
|
||||
5. Finish Phase 7 compliance and docs.
|
||||
|
||||
### UX Preservation Rule
|
||||
|
||||
No task should degrade clarity or safety defined in [`ux_reference.md`](specs/021-llm-project-assistant/ux_reference.md).
|
||||
84
specs/021-llm-project-assistant/ux_reference.md
Normal file
84
specs/021-llm-project-assistant/ux_reference.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# UX Reference: LLM Chat Assistant for Project Operations
|
||||
|
||||
**Feature Branch**: `021-llm-project-assistant`
|
||||
**Created**: 2026-02-23
|
||||
**Status**: Draft
|
||||
|
||||
## 1. User Persona & Context
|
||||
|
||||
* **Who is the user?**: Platform operator, dashboard engineer, or admin managing Superset operations.
|
||||
* **What is their goal?**: Execute routine and critical project operations quickly via natural language with safe controls.
|
||||
* **Context**: User is already in `ss-tools` web interface and wants to avoid jumping between Git, migration, backup, and task pages.
|
||||
|
||||
## 2. The "Happy Path" Narrative
|
||||
|
||||
The user opens assistant chat from the main interface and enters a natural command to run an operation. Assistant immediately recognizes intent, validates permissions, and either executes directly or asks for confirmation if operation is risky. For long-running actions, assistant responds instantly with `task_id` and where to track progress. User can ask follow-up status questions in the same chat and receives concise operational answers.
|
||||
|
||||
## 3. Interface Mockups
|
||||
|
||||
### UI Layout & Flow (if applicable)
|
||||
|
||||
**Screen/Component**: Assistant Chat Panel
|
||||
|
||||
* **Layout**:
|
||||
- Right-side drawer or modal panel with message history.
|
||||
- Input area at bottom with send action and command examples shortcut.
|
||||
- Message cards with role (User/Assistant), timestamp, state badge.
|
||||
* **Key Elements**:
|
||||
* **Chat Input**: Supports free-text commands in RU/EN.
|
||||
* **State Badge**: `started`, `success`, `failed`, `needs_confirmation`, `denied`.
|
||||
* **Confirmation Card**: Explicit confirm/cancel buttons for risky actions.
|
||||
* **Task Link Chip**: `task_id` chip with quick open in Task Drawer.
|
||||
* **Suggestions Block**: "Try command" examples for supported domains.
|
||||
* **States**:
|
||||
* **Default**: Empty or recent history shown, examples visible.
|
||||
* **Parsing**: Inline "Analyzing command..." loading state.
|
||||
* **Needs Confirmation**: Warning card with operation summary and target environment.
|
||||
* **Started**: Immediate message with `task_id` and tracking hint.
|
||||
* **Error**: Error card with reason and recovery suggestion.
|
||||
|
||||
### Interaction Examples
|
||||
|
||||
```text
|
||||
User: запусти миграцию с dev на prod для дашборда 42
|
||||
Assistant: Операция рискованная (target=production). Подтвердите запуск.
|
||||
[Confirm] [Cancel]
|
||||
|
||||
User: confirm
|
||||
Assistant: Миграция запущена. task_id=task-8f13. Прогресс: Task Drawer / Reports.
|
||||
```
|
||||
|
||||
## 4. The "Error" Experience
|
||||
|
||||
**Philosophy**: Never silently fail. Show what happened, why, and what to do next.
|
||||
|
||||
### Scenario A: Ambiguous Command
|
||||
|
||||
* **User Action**: "сделай задачу для дашборда 42"
|
||||
* **System Response**:
|
||||
* Assistant does not execute immediately.
|
||||
* Returns clarification question with options (Git branch / migration / backup / analysis).
|
||||
* **Recovery**: User selects one option or reformulates command.
|
||||
|
||||
### Scenario B: Permission Denied
|
||||
|
||||
* **User Action**: Non-admin requests production deploy.
|
||||
* **System Response**:
|
||||
* Assistant returns `denied` state with minimal permission reason.
|
||||
* No task is created.
|
||||
* **Recovery**: User requests access or uses allowed operation.
|
||||
|
||||
### Scenario C: Confirmation Timeout
|
||||
|
||||
* **System Response**: "Confirmation expired. Operation was not executed."
|
||||
* **Recovery**: User reissues command.
|
||||
|
||||
### Scenario D: Downstream Failure After Start
|
||||
|
||||
* **System Response**: Assistant returns `task_id` and indicates task moved to failed state with short error summary.
|
||||
* **Recovery**: Open task logs/reports using provided link/action.
|
||||
|
||||
## 5. Tone & Voice
|
||||
|
||||
* **Style**: Operational, concise, explicit.
|
||||
* **Terminology**: Use "command", "task", "confirmation", "status", "task_id"; avoid vague wording.
|
||||
Reference in New Issue
Block a user