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.