feat(assistant): implement spec 021 chat assistant flow with semantic contracts

This commit is contained in:
2026-02-23 19:37:56 +03:00
parent 83e4875097
commit 18e96a58bc
27 changed files with 4029 additions and 20 deletions

View File

@@ -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]

View 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]

View 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]

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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)

View 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]

View 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]

View 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] -->

View File

@@ -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]

View File

@@ -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]

View File

@@ -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"

View File

@@ -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"
}
}
}
}

View File

@@ -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": "Выберите роль"
}
}
}
}

View 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]

View 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]

View File

@@ -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>

View 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.

View File

@@ -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]

View 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).

View 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.

View 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.

View 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`)

View 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.

View 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"
```

View 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).

View 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.