Coverage for sources / agentsmgr / context.py: 14%

73 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-30 00:03 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Template rendering context normalization and tool mapping. 

22 

23 Provides context transformation for template rendering, including 

24 hyphen-to-underscore normalization and coder-specific tool mapping. 

25''' 

26 

27 

28from . import __ 

29from . import exceptions as _exceptions 

30 

31 

32ToolSpecification: __.typx.TypeAlias = ( 

33 str | dict[ str, __.typx.Any ] ) 

34 

35 

36_SEMANTIC_TOOLS_CLAUDE: dict[ str, str ] = { 

37 'read': 'Read', 

38 'edit': 'Edit', 

39 'multi-edit': 'MultiEdit', 

40 'write': 'Write', 

41 'list-directory': 'LS', 

42 'glob': 'Glob', 

43 'grep': 'Grep', 

44 'todo-write': 'TodoWrite', 

45 'web-fetch': 'WebFetch', 

46 'web-search': 'WebSearch', 

47} 

48 

49_SEMANTIC_TOOLS_QWEN: dict[ str, str ] = { 

50 'read': 'read_file', 

51 'edit': 'edit', 

52 'multi-edit': 'edit', 

53 'write': 'write_file', 

54 'list-directory': 'list_directory', 

55 'glob': 'glob', 

56 'grep': 'search_file_content', 

57 'todo-write': 'todo_write', 

58 'web-fetch': 'web_fetch', 

59 'web-search': 'web_search', 

60} 

61 

62 

63def normalize_render_context( 

64 context_data: __.cabc.Mapping[ str, __.typx.Any ], 

65 coder_config: __.cabc.Mapping[ str, __.typx.Any ], 

66) -> dict[ str, __.typx.Any ]: 

67 ''' Normalizes template rendering context with tool mapping. 

68 

69 Transforms hyphenated keys to underscored keys, wraps configurations 

70 in SimpleNamespace objects for dot-notation access, and maps 

71 allowed-tools specifications to coder-specific syntax. 

72 ''' 

73 coder_name = coder_config.get( 'name', 'unknown' ) 

74 normalized_context = { 

75 key.replace( '-', '_' ): value 

76 for key, value in context_data.items( ) } 

77 if 'allowed_tools' in normalized_context: 

78 raw_tools = normalized_context[ 'allowed_tools' ] 

79 normalized_context[ 'allowed_tools' ] = ( 

80 _map_tools_for_coder( raw_tools, coder_name ) ) 

81 context_namespace = __.types.SimpleNamespace( **normalized_context ) 

82 coder_namespace = __.types.SimpleNamespace( **coder_config ) 

83 return { 

84 'context': context_namespace, 

85 'coder': coder_namespace, 

86 } 

87 

88 

89def _map_tools_for_coder( 

90 tool_specs: __.cabc.Sequence[ ToolSpecification ], 

91 coder_name: str, 

92) -> list[ str ]: 

93 ''' Maps tool specifications to coder-specific syntax. 

94 

95 Dispatches to coder-specific mapping function based on coder name. 

96 Returns empty list if coder is not supported. 

97 ''' 

98 if coder_name == 'claude': 

99 return _map_tools_claude( tool_specs ) 

100 if coder_name == 'qwen': 

101 return _map_tools_qwen( tool_specs ) 

102 return [ ] 

103 

104 

105def _map_tools_claude( 

106 tool_specs: __.cabc.Sequence[ ToolSpecification ] 

107) -> list[ str ]: 

108 ''' Maps tool specifications to Claude-specific syntax. 

109 

110 Handles three specification types: 

111 - String literals (semantic names): 'read' → 'Read' 

112 - Shell commands: { tool = 'shell', arguments, ... } → 'Bash(...)' 

113 - MCP tools: { server, tool } → 'mcp__server__tool' 

114 

115 Returns tools sorted alphabetically for consistent output. 

116 ''' 

117 mapped: list[ str ] = [ ] 

118 for spec in tool_specs: 

119 if isinstance( spec, str ): 

120 mapped.append( _map_semantic_tool_claude( spec ) ) 

121 elif isinstance( spec, dict ): 

122 if 'server' in spec: 

123 mapped.append( _map_mcp_tool_claude( spec ) ) 

124 elif spec.get( 'tool' ) == 'shell': 

125 mapped.append( _map_shell_tool_claude( spec ) ) 

126 else: 

127 raise _exceptions.ToolSpecificationInvalidity( str( spec ) ) 

128 else: 

129 raise _exceptions.ToolSpecificationTypeInvalidity( 

130 type( spec ).__name__ 

131 ) 

132 return sorted( mapped ) 

133 

134 

135def _map_semantic_tool_claude( tool_name: str ) -> str: 

136 ''' Maps semantic tool name to Claude tool name. 

137 

138 Uses lookup table for known semantic names. 

139 Raises ToolSpecificationInvalidity for unknown tools. 

140 ''' 

141 if tool_name not in _SEMANTIC_TOOLS_CLAUDE: 

142 raise _exceptions.ToolSpecificationInvalidity( tool_name ) 

143 return _SEMANTIC_TOOLS_CLAUDE[ tool_name ] 

144 

145 

146def _map_shell_tool_claude( spec: dict[ str, __.typx.Any ] ) -> str: 

147 ''' Maps shell command specification to Claude Bash tool syntax. 

148 

149 Format: { tool = 'shell', arguments = 'git status' } 

150 → 'Bash(git status)' 

151 

152 With wildcard: { tool = 'shell', arguments = 'git pull', 

153 allow-extra-arguments = true } 

154 → 'Bash(git pull:*)' 

155 ''' 

156 arguments = spec.get( 'arguments', '' ) 

157 allow_extra = spec.get( 'allow-extra-arguments', False ) 

158 if allow_extra: 

159 return f"Bash({arguments}:*)" 

160 return f"Bash({arguments})" 

161 

162 

163def _map_mcp_tool_claude( spec: dict[ str, __.typx.Any ] ) -> str: 

164 ''' Maps MCP tool specification to Claude MCP tool syntax. 

165 

166 Format: { server = 'librovore', tool = 'query-inventory' } 

167 → 'mcp__librovore__query_inventory' 

168 ''' 

169 server = spec.get( 'server', '' ) 

170 tool = spec.get( 'tool', '' ) 

171 tool_normalized = tool.replace( '-', '_' ) 

172 return f"mcp__{server}__{tool_normalized}" 

173 

174 

175def _map_tools_qwen( 

176 tool_specs: __.cabc.Sequence[ ToolSpecification ] 

177) -> list[ str ]: 

178 ''' Maps tool specifications to Qwen-specific syntax. 

179 

180 Handles three specification types: 

181 - String literals (semantic names): 'read' → 'read_file' 

182 - Shell commands: { tool = 'shell', arguments, ... } → 

183 'run_shell_command(...)' in coreTools (prefix matching) 

184 - MCP tools: { server, tool } → 'mcp__server__tool' 

185 

186 Returns tools sorted alphabetically for consistent output. 

187 ''' 

188 mapped: list[ str ] = [ ] 

189 for spec in tool_specs: 

190 if isinstance( spec, str ): 

191 mapped.append( _map_semantic_tool_qwen( spec ) ) 

192 elif isinstance( spec, dict ): 

193 if 'server' in spec: 

194 mapped.append( _map_mcp_tool_qwen( spec ) ) 

195 elif spec.get( 'tool' ) == 'shell': 

196 mapped.append( _map_shell_tool_qwen( spec ) ) 

197 else: 

198 raise _exceptions.ToolSpecificationInvalidity( str( spec ) ) 

199 else: 

200 raise _exceptions.ToolSpecificationTypeInvalidity( 

201 type( spec ).__name__ 

202 ) 

203 return sorted( mapped ) 

204 

205 

206def _map_semantic_tool_qwen( tool_name: str ) -> str: 

207 ''' Maps semantic tool name to Qwen tool name. 

208 

209 Uses lookup table for known semantic names. 

210 Raises ToolSpecificationInvalidity for unknown tools. 

211 ''' 

212 # TODO: Verify semantic tool mappings are correct for Qwen coder. 

213 # The structure is identical to Claude version, but tool names differ. 

214 # Keep implementations separate since each coder has its own tool names. 

215 if tool_name not in _SEMANTIC_TOOLS_QWEN: 

216 raise _exceptions.ToolSpecificationInvalidity( tool_name ) 

217 return _SEMANTIC_TOOLS_QWEN[ tool_name ] 

218 

219 

220def _map_shell_tool_qwen( spec: dict[ str, __.typx.Any ] ) -> str: 

221 ''' Maps shell command specification to Qwen run_shell_command syntax. 

222 

223 Qwen uses prefix matching for shell commands - no wildcard needed. 

224 Format: { tool = 'shell', arguments = 'git status' } 

225 → 'run_shell_command(git status)' 

226 

227 allow-extra-arguments is implicit in Qwen's prefix matching. 

228 Format: { tool = 'shell', arguments = 'git pull', 

229 allow-extra-arguments = true } 

230 → 'run_shell_command(git pull)' (same as without extra-arguments) 

231 ''' 

232 arguments = spec.get( 'arguments', '' ) 

233 return f"run_shell_command({arguments})" 

234 

235 

236def _map_mcp_tool_qwen( spec: dict[ str, __.typx.Any ] ) -> str: 

237 ''' Maps MCP tool specification to Qwen MCP tool syntax. 

238 

239 Format: { server = 'librovore', tool = 'query-inventory' } 

240 → 'mcp__librovore__query_inventory' 

241 ''' 

242 # TODO: Verify this implementation is correct for Qwen coder. 

243 # Once verified, consider whether consolidation with Claude version 

244 # is appropriate or if they should remain separate. 

245 server = spec.get( 'server', '' ) 

246 tool = spec.get( 'tool', '' ) 

247 tool_normalized = tool.replace( '-', '_' ) 

248 return f"mcp__{server}__{tool_normalized}"