Coverage for sources/agentsmgr/context.py: 14%
73 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-26 02:00 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-26 02:00 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
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#============================================================================#
21''' Template rendering context normalization and tool mapping.
23 Provides context transformation for template rendering, including
24 hyphen-to-underscore normalization and coder-specific tool mapping.
25'''
28from . import __
29from . import exceptions as _exceptions
32ToolSpecification: __.typx.TypeAlias = (
33 str | dict[ str, __.typx.Any ] )
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}
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}
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.
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 }
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.
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 [ ]
105def _map_tools_claude(
106 tool_specs: __.cabc.Sequence[ ToolSpecification ]
107) -> list[ str ]:
108 ''' Maps tool specifications to Claude-specific syntax.
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'
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 )
135def _map_semantic_tool_claude( tool_name: str ) -> str:
136 ''' Maps semantic tool name to Claude tool name.
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 ]
146def _map_shell_tool_claude( spec: dict[ str, __.typx.Any ] ) -> str:
147 ''' Maps shell command specification to Claude Bash tool syntax.
149 Format: { tool = 'shell', arguments = 'git status' }
150 → 'Bash(git status)'
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})"
163def _map_mcp_tool_claude( spec: dict[ str, __.typx.Any ] ) -> str:
164 ''' Maps MCP tool specification to Claude MCP tool syntax.
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}"
175def _map_tools_qwen(
176 tool_specs: __.cabc.Sequence[ ToolSpecification ]
177) -> list[ str ]:
178 ''' Maps tool specifications to Qwen-specific syntax.
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'
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 )
206def _map_semantic_tool_qwen( tool_name: str ) -> str:
207 ''' Maps semantic tool name to Qwen tool name.
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 ]
220def _map_shell_tool_qwen( spec: dict[ str, __.typx.Any ] ) -> str:
221 ''' Maps shell command specification to Qwen run_shell_command syntax.
223 Qwen uses prefix matching for shell commands - no wildcard needed.
224 Format: { tool = 'shell', arguments = 'git status' }
225 → 'run_shell_command(git status)'
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})"
236def _map_mcp_tool_qwen( spec: dict[ str, __.typx.Any ] ) -> str:
237 ''' Maps MCP tool specification to Qwen MCP tool syntax.
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}"