vibesop
VibeSOP - Modern Python Edition.
A battle-tested, multi-platform workflow SOP for AI-assisted development.
This project is a complete rewrite of the Ruby version, leveraging:
- Python 3.12+ type system
- Pydantic v2 for runtime validation
- Modern async/await patterns
- Type-safe LLM clients
1"""VibeSOP - Modern Python Edition. 2 3A battle-tested, multi-platform workflow SOP for AI-assisted development. 4 5This project is a complete rewrite of the Ruby version, leveraging: 6- Python 3.12+ type system 7- Pydantic v2 for runtime validation 8- Modern async/await patterns 9- Type-safe LLM clients 10""" 11 12from vibesop._version import __version__ 13 14__author__ = "nehcuh" 15__license__ = "MIT" 16 17# Core public API 18from vibesop.core.models import ( 19 RoutingLayer, 20 RoutingRequest, 21 RoutingResult, 22 SkillRegistry, 23 SkillRoute, 24) 25 26__all__ = [ 27 "RoutingLayer", 28 "RoutingRequest", 29 "RoutingResult", 30 "SkillRegistry", 31 "SkillRoute", 32 "__version__", 33]
class
RoutingLayer(enum.StrEnum):
16class RoutingLayer(StrEnum): 17 """Routing layers in priority order (Layer 0 → Layer 9).""" 18 19 EXPLICIT = "explicit" # Layer 0 20 SCENARIO = "scenario" # Layer 1 21 AI_TRIAGE = "ai_triage" # Layer 2 22 KEYWORD = "keyword" # Layer 3 23 TFIDF = "tfidf" # Layer 4 24 EMBEDDING = "embedding" # Layer 5 25 LEVENSHTEIN = "levenshtein" # Layer 6 26 CUSTOM = "custom" # Layer 7 27 NO_MATCH = "no_match" # Layer 8 28 FALLBACK_LLM = "fallback_llm" # Layer 9 29 30 @property 31 def layer_number(self) -> int: 32 """Return numeric layer index for backward compatibility.""" 33 mapping = { 34 RoutingLayer.EXPLICIT: 0, 35 RoutingLayer.SCENARIO: 1, 36 RoutingLayer.AI_TRIAGE: 2, 37 RoutingLayer.KEYWORD: 3, 38 RoutingLayer.TFIDF: 4, 39 RoutingLayer.EMBEDDING: 5, 40 RoutingLayer.LEVENSHTEIN: 6, 41 RoutingLayer.CUSTOM: 7, 42 RoutingLayer.NO_MATCH: 8, 43 RoutingLayer.FALLBACK_LLM: 9, 44 } 45 return mapping[self]
Routing layers in priority order (Layer 0 → Layer 9).
EXPLICIT =
<RoutingLayer.EXPLICIT: 'explicit'>
SCENARIO =
<RoutingLayer.SCENARIO: 'scenario'>
AI_TRIAGE =
<RoutingLayer.AI_TRIAGE: 'ai_triage'>
KEYWORD =
<RoutingLayer.KEYWORD: 'keyword'>
TFIDF =
<RoutingLayer.TFIDF: 'tfidf'>
EMBEDDING =
<RoutingLayer.EMBEDDING: 'embedding'>
LEVENSHTEIN =
<RoutingLayer.LEVENSHTEIN: 'levenshtein'>
CUSTOM =
<RoutingLayer.CUSTOM: 'custom'>
NO_MATCH =
<RoutingLayer.NO_MATCH: 'no_match'>
FALLBACK_LLM =
<RoutingLayer.FALLBACK_LLM: 'fallback_llm'>
layer_number: int
30 @property 31 def layer_number(self) -> int: 32 """Return numeric layer index for backward compatibility.""" 33 mapping = { 34 RoutingLayer.EXPLICIT: 0, 35 RoutingLayer.SCENARIO: 1, 36 RoutingLayer.AI_TRIAGE: 2, 37 RoutingLayer.KEYWORD: 3, 38 RoutingLayer.TFIDF: 4, 39 RoutingLayer.EMBEDDING: 5, 40 RoutingLayer.LEVENSHTEIN: 6, 41 RoutingLayer.CUSTOM: 7, 42 RoutingLayer.NO_MATCH: 8, 43 RoutingLayer.FALLBACK_LLM: 9, 44 } 45 return mapping[self]
Return numeric layer index for backward compatibility.
class
RoutingRequest(pydantic.main.BaseModel):
126class RoutingRequest(BaseModel): 127 """Request for skill routing. 128 129 Attributes: 130 query: User's natural language query 131 context: Additional context (file type, error count, etc.) 132 """ 133 134 query: str = Field(..., min_length=1, description="User query") 135 context: dict[str, str | int] = Field( 136 default_factory=dict, 137 description="Routing context", 138 )
Request for skill routing.
Attributes:
- query: User's natural language query
- context: Additional context (file type, error count, etc.)
class
RoutingResult(pydantic.main.BaseModel):
210class RoutingResult(BaseModel): 211 """Result of skill routing operation. 212 213 Attributes: 214 primary: Best matching skill (None if no match) 215 alternatives: List of alternative matches 216 routing_path: Which layers were consulted 217 layer_details: Per-layer diagnostic details for transparency 218 query: The original query 219 duration_ms: How long routing took 220 """ 221 222 model_config = {"arbitrary_types_allowed": True} 223 224 primary: SkillRoute | None = Field( 225 default=None, 226 description="Primary skill match", 227 ) 228 alternatives: list[SkillRoute] = Field( 229 default_factory=list, 230 description="Alternative skill matches", 231 ) 232 routing_path: list[RoutingLayer] = Field( 233 default_factory=list, 234 description="Layers consulted during routing", 235 ) 236 layer_details: list[LayerDetail] = Field( 237 default_factory=list, 238 description="Per-layer diagnostic details for transparency", 239 ) 240 query: str = Field(default="", description="Original query") 241 duration_ms: float = Field(default=0.0, description="Routing duration in ms") 242 243 @property 244 def has_match(self) -> bool: 245 """Whether a match was found (excluding fallback).""" 246 return self.primary is not None and self.primary.layer != RoutingLayer.FALLBACK_LLM 247 248 def to_dict(self) -> dict[str, Any]: 249 """Convert to dictionary for serialization.""" 250 return { 251 "primary": self.primary.to_dict() if self.primary else None, 252 "alternatives": [a.to_dict() for a in self.alternatives], 253 "routing_path": [layer.value for layer in self.routing_path], 254 "layer_details": [d.to_dict() for d in self.layer_details], 255 "query": self.query, 256 "duration_ms": self.duration_ms, 257 "has_match": self.has_match, 258 }
Result of skill routing operation.
Attributes:
- primary: Best matching skill (None if no match)
- alternatives: List of alternative matches
- routing_path: Which layers were consulted
- layer_details: Per-layer diagnostic details for transparency
- query: The original query
- duration_ms: How long routing took
layer_details: list[vibesop.core.models.LayerDetail] =
PydanticUndefined
Per-layer diagnostic details for transparency
has_match: bool
243 @property 244 def has_match(self) -> bool: 245 """Whether a match was found (excluding fallback).""" 246 return self.primary is not None and self.primary.layer != RoutingLayer.FALLBACK_LLM
Whether a match was found (excluding fallback).
def
to_dict(self) -> dict[str, typing.Any]:
248 def to_dict(self) -> dict[str, Any]: 249 """Convert to dictionary for serialization.""" 250 return { 251 "primary": self.primary.to_dict() if self.primary else None, 252 "alternatives": [a.to_dict() for a in self.alternatives], 253 "routing_path": [layer.value for layer in self.routing_path], 254 "layer_details": [d.to_dict() for d in self.layer_details], 255 "query": self.query, 256 "duration_ms": self.duration_ms, 257 "has_match": self.has_match, 258 }
Convert to dictionary for serialization.
class
SkillRegistry(pydantic.main.BaseModel):
580class SkillRegistry(BaseModel): 581 """Registry of all available skills. 582 583 Attributes: 584 skills: Map of skill_id to SkillDefinition 585 version: Registry version 586 """ 587 588 skills: dict[str, SkillDefinition] = Field( 589 default_factory=dict, 590 description="Available skills", 591 ) 592 version: str = Field(default="1.0.0", description="Registry version")
Registry of all available skills.
Attributes:
- skills: Map of skill_id to SkillDefinition
- version: Registry version
class
SkillRoute(pydantic.main.BaseModel):
78class SkillRoute(BaseModel): 79 """Result of skill routing operation. 80 81 Attributes: 82 skill_id: Unique skill identifier (e.g., 'gstack/review') 83 confidence: Routing confidence (0.0 to 1.0) 84 layer: Which routing layer made this decision 85 source: Skill pack source (e.g., 'builtin', 'gstack', 'external') 86 metadata: Additional routing metadata 87 """ 88 89 model_config = {"arbitrary_types_allowed": True} 90 91 skill_id: str = Field(..., min_length=1, description="Skill identifier") 92 confidence: float = Field( 93 default=0.0, 94 ge=0.0, 95 le=1.0, 96 description="Routing confidence score", 97 ) 98 layer: RoutingLayer = Field( 99 ..., 100 description="Routing layer that produced this match", 101 ) 102 source: str = Field(default="builtin", description="Skill pack source") 103 description: str = Field( 104 default="", 105 description="Skill description for display in CLI", 106 ) 107 metadata: dict[str, Any] = Field( 108 default_factory=dict, 109 description="Additional routing metadata", 110 ) 111 112 # Note: min_length=1 on the Field already enforces non-empty skill_id 113 # No additional field_validator needed 114 115 def to_dict(self) -> dict[str, Any]: 116 """Convert to dictionary for serialization.""" 117 return { 118 "skill_id": self.skill_id, 119 "confidence": self.confidence, 120 "layer": self.layer.value, 121 "source": self.source, 122 "metadata": self.metadata, 123 }
Result of skill routing operation.
Attributes:
- skill_id: Unique skill identifier (e.g., 'gstack/review')
- confidence: Routing confidence (0.0 to 1.0)
- layer: Which routing layer made this decision
- source: Skill pack source (e.g., 'builtin', 'gstack', 'external')
- metadata: Additional routing metadata
def
to_dict(self) -> dict[str, typing.Any]:
115 def to_dict(self) -> dict[str, Any]: 116 """Convert to dictionary for serialization.""" 117 return { 118 "skill_id": self.skill_id, 119 "confidence": self.confidence, 120 "layer": self.layer.value, 121 "source": self.source, 122 "metadata": self.metadata, 123 }
Convert to dictionary for serialization.
__version__ =
'5.4.0'