Skip to content
← How we build

backend/src/skills/base_skill.py

"""BaseSkill contract — business capability layer orchestrating Tools + LLM Service."""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import Any

from models.skills import SkillRequest, SkillResult
from observability import log_event, record_metric
from services.llm_service import LLMService


class BaseSkill(ABC):
    """Abstract Skill: orchestrate Tools and the LLM Service, return a typed ``SkillResult``.

    A Skill never calls another Skill and never reads ``config/*.yaml`` directly —
    it only sees what the Skill Factory injects at construction (skills-design.md §1, §7).
    """

    name: str
    required_tools: list[str] = []

    def __init__(
        self,
        llm_service: LLMService,
        rules_engine_service: Any | None = None,
    ) -> None:
        # rules_engine_service stays loosely typed: RulesEngineService doesn't exist
        # yet (Phase 3, rules-engine-design.md) — every Skill receives None in Phase 1
        # regardless of whether it's designed to eventually receive a real instance.
        self.llm_service = llm_service
        self.rules_engine_service = rules_engine_service

    @abstractmethod
    async def execute(self, request: SkillRequest) -> SkillResult:
        """Execute this Skill's business capability."""

    def _emit_start(
        self,
        *,
        trace_id: str | None = None,
        session_id: str | None = None,
        fields: dict[str, Any] | None = None,
    ) -> None:
        log_event(
            "execute_start",
            component=type(self).__name__,
            trace_id=trace_id,
            fields={"skill": self.name, "session_id": session_id, **(fields or {})},
        )

    def _emit_complete(
        self,
        *,
        success: bool,
        latency_ms: float,
        trace_id: str | None = None,
        session_id: str | None = None,
        error: str | None = None,
        fields: dict[str, Any] | None = None,
    ) -> None:
        event_fields: dict[str, Any] = {
            "skill": self.name,
            "session_id": session_id,
            "success": success,
            "latency_ms": round(latency_ms, 2),
            **(fields or {}),
        }
        if error is not None:
            event_fields["error"] = error
        log_event(
            "execute_complete",
            component=type(self).__name__,
            trace_id=trace_id,
            fields=event_fields,
            level=logging.INFO if success else logging.ERROR,
        )
        record_metric(
            "skill.latency_ms",
            latency_ms,
            tags={"skill": self.name, "success": "true" if success else "false"},
        )
        record_metric(
            "skill.success",
            1.0 if success else 0.0,
            tags={"skill": self.name},
        )

Follow the work.

Occasional updates on SignalFoundry, MarketCompass, and what we are building at CompassFoundry Labs.

No spam. Unsubscribe anytime.