Files
ss-tools/backend/src/api/routes/assistant/_routes.py
busya b5e741077d feat: attention-optimized semantic protocol v2.7
Core changes:
- Add @defgroup/@ingroup to 1791 C2+ contracts (555 files) for HCA 128× pre-training DSA grouping
- Add §0.1 Pre-Training Frequency matrix to semantics-core
- Add §VIII Attention Architecture rules (ATTN_1-4) with MLA/CSA/HCA/DSA mechanics
- Add @defgroup/@ingroup to canonical syntax (§II) and all contract examples

Agent prompts (5 files):
- Add ZERO-STATE RATIONALE with MLA/CSA/HCA/DSA compression mechanics
- Add pre-training note: @RATIONALE/@REJECTED are in-context learned tags
- svelte-coder: add missing #region contract, fix Svelte rule violations
- python-coder/fullstack-coder: honor function contracts from speckit plan
- qa-tester: add attention compliance audit (P3 ATTN_1-4 checks)

Skills (6 files):
- Translate all axiom_config descriptions to English
- Fix doc_dirs to index .opencode/ and .specify/
- Deduplicate 5× complexity_rules → single global_tags catalog
- Reduce semantics-svelte 591→485 lines (remove duplicate code blocks)
- Fix semantics-testing: 'Short IDs' → 'Short hierarchical IDs'
- Fix all examples: flat IDs → hierarchical Domain.Name format
- Fix Svelte examples: replace raw Tailwind + <button> with semantic tokens + /ui

Speckit workflow (commands + templates):
- speckit.plan: add Function-Level Contracts for C3+ with @PRE/@POST/@TEST_EDGE
- speckit.plan: add Attention Compliance Gate (ATTN_1-4 before contract generation)
- speckit.tasks: add function contract inlining format (constraints in task description)
- speckit.specify: load semantics-core for spec density rules
- spec-template: add #region contract, @SEMANTICS grouping, hierarchical IDs
- ux-reference-template: add #region wrapper
- plan-template: add attention gate, @defgroup/@ingroup guidance
- tasks-template: add attention audit + rebuild + orphan check tasks
- constitution.md: translate to English, add Principle VIII (attention-optimized contracts)

Reference modules rewritten (hierarchical IDs + full contracts):
- Auth.Jwt: 6 child contracts with @RATIONALE/@REJECTED/@TEST_EDGE
- Api.Auth: 5 endpoints with @TEST_EDGE + molecular CoT markers
- Migration.Model: @defgroup Migration with 18 @ACTION + 6 @INVARIANT

Scripts:
- add_defgroup_ingroup.py: zero-risk additive @ingroup migration (1791 insertions)
- migrate_hierarchical.py: flat→hierarchical ID dry-run analysis (792 contracts)
- merge_prompts.py: merge all prompts/skills/commands into one review file

Config:
- axiom_config.yaml: 749→395 lines (-47%), English, doc_dirs include prompts
- Fix test_datasets.py import collision (rename → test_datasets_routes.py)
- Fix test_preview.py: SupersetClient→get_superset_client, AsyncMock, logger f-string
2026-06-08 16:30:59 +03:00

331 lines
16 KiB
Python

# #region AssistantRoutes [C:5] [TYPE Module] [SEMANTICS assistant, api, route, chat, execution]
# @defgroup AssistantApi Module group.
# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @RELATION DEPENDS_ON -> [AssistantHistory]
# @RELATION DEPENDS_ON -> [AssistantCommandParser]
# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
# @RELATION DEPENDS_ON -> [AssistantDispatch]
# @RELATION DISPATCHES -> [AssistantAdminRoutes]
# @INVARIANT Risky operations are never executed without valid confirmation token.
from __future__ import annotations
from datetime import UTC, datetime
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from src.core.config_manager import ConfigManager
from src.core.database import get_db
from src.core.logger import belief_scope, logger
from src.core.task_manager import TaskManager
from src.dependencies import (
get_config_manager,
get_current_user,
get_task_manager,
)
from src.schemas.auth import User
from ._command_parser import _parse_command
from ._dataset_review import (
_load_dataset_review_context,
_plan_dataset_review_intent,
)
from ._dispatch import (
_async_confirmation_summary,
_clarification_text_for_intent,
)
from ._tool_registry import dispatch
from ._history import (
_append_history,
_audit,
_load_confirmation_from_db,
_persist_audit,
_persist_confirmation,
_persist_message,
_resolve_or_create_conversation,
_update_confirmation_state,
)
from ._llm_planner import _build_tool_catalog
from ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm
from ._tool_registry import get_safe_ops
from ._schemas import (
CONFIRMATIONS,
AssistantAction,
AssistantMessageRequest,
AssistantMessageResponse,
ConfirmationRecord,
)
router = APIRouter(tags=["Assistant"])
@router.post("/messages", response_model=AssistantMessageResponse)
# #region send_message [C:5] [TYPE Function]
# @ingroup AssistantApi
# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.
# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]
# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]
# @RELATION DEPENDS_ON -> [_parse_command]
# @RELATION DEPENDS_ON -> [dispatch]
# @RELATION DEPENDS_ON -> [_append_history]
# @RELATION DEPENDS_ON -> [_persist_message]
# @RELATION DEPENDS_ON -> [_audit]
# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.
# @PRE Authenticated user is available and message text is non-empty.
# @POST Response state is one of clarification/confirmation/started/success/denied/failed.
# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.
async def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):
with belief_scope('send_message'):
logger.reason('Belief protocol reasoning checkpoint for send_message')
user_id = current_user.id
dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)
conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)
_append_history(user_id, conversation_id, 'user', request.message)
_persist_message(db, user_id, conversation_id, 'user', request.message)
tools_catalog = _build_tool_catalog(current_user, config_manager, db)
intent = None
try:
intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)
except Exception as exc:
logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')
if not intent:
intent = _parse_command(request.message, config_manager)
if dataset_review_context:
dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)
if dataset_review_intent is not None:
intent = dataset_review_intent
confidence = float(intent.get('confidence', 0.0))
if intent.get('domain') == 'unknown' or confidence < 0.6:
# Use LLM-generated clarification question when available (from _plan_intent_with_llm),
# otherwise fall back to deterministic text.
text = (
intent.get('clarification')
or 'Команда неоднозначна. Уточните, что вы хотите сделать.'
)
_append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')
_persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})
audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}
_audit(user_id, audit_payload)
_persist_audit(db, user_id, audit_payload, conversation_id)
logger.reflect('Belief protocol postcondition checkpoint for send_message')
return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.now(UTC))
try:
_authorize_intent(intent, current_user)
operation = intent.get('operation')
if operation not in get_safe_ops():
confirmation_id = str(uuid.uuid4())
confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.now(UTC) + __import__('datetime').timedelta(minutes=5), created_at=datetime.now(UTC))
CONFIRMATIONS[confirmation_id] = confirm
_persist_confirmation(db, confirm)
text = await _async_confirmation_summary(intent, config_manager, db)
_append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)
_persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})
audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}
_audit(user_id, audit_payload)
_persist_audit(db, user_id, audit_payload, conversation_id)
logger.reflect('Belief protocol postcondition checkpoint for send_message')
return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.now(UTC))
text, task_id, actions = await dispatch(intent.get('operation'), intent, current_user, task_manager, config_manager, db)
state = 'started' if task_id else 'success'
_append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)
_persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})
audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}
_audit(user_id, audit_payload)
_persist_audit(db, user_id, audit_payload, conversation_id)
logger.reflect('Belief protocol postcondition checkpoint for send_message')
return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.now(UTC))
except HTTPException as exc:
detail_text = str(exc.detail)
is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())
if exc.status_code == status.HTTP_403_FORBIDDEN:
state = 'denied'
elif is_clarification_error:
state = 'needs_clarification'
else:
state = 'failed'
text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text
_append_history(user_id, conversation_id, 'assistant', text, state=state)
_persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})
audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}
_audit(user_id, audit_payload)
_persist_audit(db, user_id, audit_payload, conversation_id)
logger.reflect('Belief protocol postcondition checkpoint for send_message')
return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')] if state == 'needs_clarification' else [], created_at=datetime.now(UTC))
# #endregion send_message
@router.post(
"/confirmations/{confirmation_id}/confirm", response_model=AssistantMessageResponse
)
# #region confirm_operation [C:2] [TYPE Function]
# @ingroup AssistantApi
# @BRIEF Execute previously requested risky operation after explicit user confirmation.
# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.
# @POST Confirmation state becomes consumed and operation result is persisted in history.
async def confirm_operation(
confirmation_id: str,
current_user: User = Depends(get_current_user),
task_manager: TaskManager = Depends(get_task_manager),
config_manager: ConfigManager = Depends(get_config_manager),
db: Session = Depends(get_db),
):
with belief_scope("assistant.confirm"):
record = CONFIRMATIONS.get(confirmation_id)
if not record:
record = _load_confirmation_from_db(db, confirmation_id)
if record:
CONFIRMATIONS[confirmation_id] = record
else:
raise HTTPException(status_code=404, detail="Confirmation not found")
if record.user_id != current_user.id:
raise HTTPException(
status_code=403, detail="Confirmation does not belong to current user"
)
if record.state != "pending":
raise HTTPException(
status_code=400, detail=f"Confirmation already {record.state}"
)
if datetime.now(UTC) > record.expires_at:
record.state = "expired"
_update_confirmation_state(db, confirmation_id, "expired")
raise HTTPException(status_code=400, detail="Confirmation expired")
intent = record.intent
text, task_id, actions = await dispatch(
intent.get('operation'), intent, current_user, task_manager, config_manager, db
)
record.state = "consumed"
_update_confirmation_state(db, confirmation_id, "consumed")
_append_history(
current_user.id,
record.conversation_id,
"assistant",
text,
state="started" if task_id else "success",
task_id=task_id,
)
_persist_message(
db,
current_user.id,
record.conversation_id,
"assistant",
text,
state="started" if task_id else "success",
task_id=task_id,
metadata={"intent": intent, "confirmation_id": confirmation_id},
)
audit_payload = {
"decision": "confirmed_execute",
"confirmation_id": confirmation_id,
"task_id": task_id,
"intent": intent,
}
_audit(current_user.id, audit_payload)
_persist_audit(db, current_user.id, audit_payload, record.conversation_id)
return AssistantMessageResponse(
conversation_id=record.conversation_id,
response_id=str(uuid.uuid4()),
state="started" if task_id else "success",
text=text,
intent=intent,
task_id=task_id,
actions=actions,
created_at=datetime.now(UTC),
)
# #endregion confirm_operation
@router.post(
"/confirmations/{confirmation_id}/cancel", response_model=AssistantMessageResponse
)
# #region cancel_operation [C:2] [TYPE Function]
# @ingroup AssistantApi
# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.
# @PRE confirmation_id exists, belongs to current user, and is still pending.
# @POST Confirmation becomes cancelled and cannot be executed anymore.
async def cancel_operation(
confirmation_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
with belief_scope("assistant.cancel"):
record = CONFIRMATIONS.get(confirmation_id)
if not record:
record = _load_confirmation_from_db(db, confirmation_id)
if record:
CONFIRMATIONS[confirmation_id] = record
else:
raise HTTPException(status_code=404, detail="Confirmation not found")
if record.user_id != current_user.id:
raise HTTPException(
status_code=403, detail="Confirmation does not belong to current user"
)
if record.state != "pending":
raise HTTPException(
status_code=400, detail=f"Confirmation already {record.state}"
)
record.state = "cancelled"
_update_confirmation_state(db, confirmation_id, "cancelled")
text = "Операция отменена. Выполнение не запускалось."
_append_history(
current_user.id,
record.conversation_id,
"assistant",
text,
state="success",
confirmation_id=confirmation_id,
)
_persist_message(
db,
current_user.id,
record.conversation_id,
"assistant",
text,
state="success",
confirmation_id=confirmation_id,
metadata={"intent": record.intent},
)
audit_payload = {
"decision": "cancelled",
"confirmation_id": confirmation_id,
"intent": record.intent,
}
_audit(current_user.id, audit_payload)
_persist_audit(db, current_user.id, audit_payload, record.conversation_id)
return AssistantMessageResponse(
conversation_id=record.conversation_id,
response_id=str(uuid.uuid4()),
state="success",
text=text,
intent=record.intent,
confirmation_id=confirmation_id,
actions=[],
created_at=datetime.now(UTC),
)
# #endregion cancel_operation
# #endregion AssistantRoutes