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
331 lines
16 KiB
Python
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
|