Coverage for src / agent / commands / manifest.py: 96%
52 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
1# Copyright 2025-2026 Microsoft Corporation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""Slash command manifest schema and parsing.
17This module defines Pydantic models for slash command manifests and provides
18utilities for extracting and parsing YAML front matter.
20The command file format follows this structure:
21```yaml
22---
23name: clone
24description: Clone OSDU repositories by category
25args: "<category>"
26required_args: ["arg0"]
27examples:
28 - "/clone core"
29---
31Clone OSDU repositories from the **{{arg0}}** category...
32```
33"""
35import re
36from typing import Any
38import yaml
39from pydantic import BaseModel, Field, field_validator
42class SlashCommandError(Exception):
43 """Base exception for slash command errors."""
45 pass
48class SlashCommandManifest(BaseModel):
49 """Pydantic model for slash command YAML front matter.
51 Required fields:
52 name: Command name (lowercase alphanumeric + hyphens, max 32 chars)
53 description: Brief description (max 200 chars)
55 Optional fields:
56 args: Argument pattern (e.g., "<category>", "[options]")
57 required_args: List of required template variables (e.g., ["arg0"])
58 examples: Usage examples
60 The prompt_template field is extracted from markdown content below the
61 YAML front matter and is not part of the YAML itself.
63 Example:
64 >>> manifest = SlashCommandManifest(
65 ... name="clone",
66 ... description="Clone OSDU repositories by category",
67 ... args="<category>",
68 ... required_args=["arg0"],
69 ... examples=["/clone core"]
70 ... )
71 """
73 # Required fields
74 name: str = Field(..., min_length=1, max_length=32)
75 description: str = Field(..., min_length=1, max_length=200)
77 # Optional fields
78 args: str | None = Field(default=None, description="Argument pattern, e.g., '<category>'")
79 required_args: list[str] = Field(
80 default_factory=list, description="List of required template variables, e.g., ['arg0']"
81 )
82 examples: list[str] = Field(default_factory=list, description="Usage examples")
84 # Markdown prompt template (extracted separately, not in YAML)
85 prompt_template: str = ""
87 @field_validator("name")
88 @classmethod
89 def validate_name(cls, v: str) -> str:
90 """Validate command name: lowercase alphanumeric + hyphens only."""
91 if not re.match(r"^[a-z][a-z0-9-]*$", v):
92 raise ValueError(
93 "Command name must start with lowercase letter, "
94 "contain only lowercase letters, numbers, and hyphens"
95 )
96 return v
98 @field_validator("required_args")
99 @classmethod
100 def validate_required_args(cls, v: list[str]) -> list[str]:
101 """Validate required args format (must be valid identifiers)."""
102 for arg in v:
103 if not re.match(r"^[a-z_][a-z0-9_]*$", arg):
104 raise ValueError(
105 f"Invalid required_arg '{arg}': must be lowercase identifier "
106 "(letters, numbers, underscores)"
107 )
108 return v
111def extract_yaml_frontmatter(content: str) -> tuple[dict[str, Any], str]:
112 """Extract YAML front matter from command file content.
114 Command file format:
115 ```
116 ---
117 name: command-name
118 description: Brief description
119 ---
121 # Markdown prompt template...
122 ```
124 Args:
125 content: Full command file content
127 Returns:
128 Tuple of (yaml_data, markdown_content)
130 Raises:
131 SlashCommandError: If YAML front matter is missing or malformed
132 """
133 # Match YAML front matter between --- markers
134 pattern = r"^---\s*\n(.*?)\n---\s*\n(.*)"
135 match = re.match(pattern, content, re.DOTALL)
137 if not match:
138 raise SlashCommandError(
139 "Command file must start with YAML front matter delimited by '---' markers"
140 )
142 yaml_content = match.group(1)
143 markdown_content = match.group(2).strip()
145 try:
146 yaml_data = yaml.safe_load(yaml_content)
147 if not isinstance(yaml_data, dict):
148 raise SlashCommandError("YAML front matter must be a dictionary")
149 except yaml.YAMLError as e:
150 raise SlashCommandError(f"Invalid YAML front matter: {e}")
152 return yaml_data, markdown_content
155def parse_command_manifest(content: str, filename: str = "") -> SlashCommandManifest:
156 """Parse slash command manifest from file content.
158 Args:
159 content: Full command file content (with YAML frontmatter)
160 filename: Optional filename for error messages
162 Returns:
163 Parsed SlashCommandManifest with YAML data and prompt template
165 Raises:
166 SlashCommandError: If content is malformed or invalid
167 """
168 try:
169 yaml_data, prompt_template = extract_yaml_frontmatter(content)
170 except SlashCommandError:
171 raise
172 except Exception as e:
173 raise SlashCommandError(f"Failed to parse command file {filename}: {e}")
175 # Add prompt template to the data for model creation
176 yaml_data["prompt_template"] = prompt_template
178 try:
179 return SlashCommandManifest(**yaml_data)
180 except Exception as e:
181 raise SlashCommandError(f"Invalid command manifest {filename}: {e}")