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

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. 

14 

15"""Slash command manifest schema and parsing. 

16 

17This module defines Pydantic models for slash command manifests and provides 

18utilities for extracting and parsing YAML front matter. 

19 

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--- 

30 

31Clone OSDU repositories from the **{{arg0}}** category... 

32``` 

33""" 

34 

35import re 

36from typing import Any 

37 

38import yaml 

39from pydantic import BaseModel, Field, field_validator 

40 

41 

42class SlashCommandError(Exception): 

43 """Base exception for slash command errors.""" 

44 

45 pass 

46 

47 

48class SlashCommandManifest(BaseModel): 

49 """Pydantic model for slash command YAML front matter. 

50 

51 Required fields: 

52 name: Command name (lowercase alphanumeric + hyphens, max 32 chars) 

53 description: Brief description (max 200 chars) 

54 

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 

59 

60 The prompt_template field is extracted from markdown content below the 

61 YAML front matter and is not part of the YAML itself. 

62 

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 """ 

72 

73 # Required fields 

74 name: str = Field(..., min_length=1, max_length=32) 

75 description: str = Field(..., min_length=1, max_length=200) 

76 

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") 

83 

84 # Markdown prompt template (extracted separately, not in YAML) 

85 prompt_template: str = "" 

86 

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 

97 

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 

109 

110 

111def extract_yaml_frontmatter(content: str) -> tuple[dict[str, Any], str]: 

112 """Extract YAML front matter from command file content. 

113 

114 Command file format: 

115 ``` 

116 --- 

117 name: command-name 

118 description: Brief description 

119 --- 

120 

121 # Markdown prompt template... 

122 ``` 

123 

124 Args: 

125 content: Full command file content 

126 

127 Returns: 

128 Tuple of (yaml_data, markdown_content) 

129 

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) 

136 

137 if not match: 

138 raise SlashCommandError( 

139 "Command file must start with YAML front matter delimited by '---' markers" 

140 ) 

141 

142 yaml_content = match.group(1) 

143 markdown_content = match.group(2).strip() 

144 

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}") 

151 

152 return yaml_data, markdown_content 

153 

154 

155def parse_command_manifest(content: str, filename: str = "") -> SlashCommandManifest: 

156 """Parse slash command manifest from file content. 

157 

158 Args: 

159 content: Full command file content (with YAML frontmatter) 

160 filename: Optional filename for error messages 

161 

162 Returns: 

163 Parsed SlashCommandManifest with YAML data and prompt template 

164 

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}") 

174 

175 # Add prompt template to the data for model creation 

176 yaml_data["prompt_template"] = prompt_template 

177 

178 try: 

179 return SlashCommandManifest(**yaml_data) 

180 except Exception as e: 

181 raise SlashCommandError(f"Invalid command manifest {filename}: {e}")