7
7
import subprocess
8
8
import time
9
9
from abc import ABC , abstractmethod
10
+ from typing import Any
10
11
11
12
import aiohttp
12
13
@@ -15,39 +16,39 @@ class BackendAdapter(ABC):
15
16
"""Backend program adapter base class"""
16
17
17
18
@abstractmethod
18
- async def execute_tool (self , tool_config , parameters ) :
19
+ async def execute_tool (self , tool_config : dict [ str , Any ], parameters : Any ) -> str :
19
20
"""Execute tool call"""
20
21
pass
21
22
22
23
@abstractmethod
23
- async def start (self ):
24
+ async def start (self ) -> None :
24
25
"""Start backend program"""
25
26
pass
26
27
27
28
@abstractmethod
28
- async def stop (self ):
29
+ async def stop (self ) -> None :
29
30
"""Stop backend program"""
30
31
pass
31
32
32
33
33
34
class CommandLineAdapter (BackendAdapter ):
34
35
"""Command line program adapter"""
35
36
36
- def __init__ (self , config ) :
37
+ def __init__ (self , config : dict [ str , Any ]) -> None :
37
38
self .config = config
38
39
self .command = config ["command" ]
39
40
self .base_args = config .get ("args" , [])
40
41
self .cwd = config .get ("cwd" , "." )
41
42
42
- async def start (self ):
43
+ async def start (self ) -> None :
43
44
"""Command line programs don't need startup"""
44
45
pass
45
46
46
- async def stop (self ):
47
+ async def stop (self ) -> None :
47
48
"""Command line programs don't need shutdown"""
48
49
pass
49
50
50
- async def execute_tool (self , tool_config , parameters ) :
51
+ async def execute_tool (self , tool_config : dict [ str , Any ], parameters : Any ) -> str :
51
52
"""Execute command line tool"""
52
53
cmd_args = []
53
54
args_template = tool_config .get ("args" , [])
@@ -74,18 +75,18 @@ async def execute_tool(self, tool_config, parameters):
74
75
class ServerAdapter (BackendAdapter ):
75
76
"""Server program adapter"""
76
77
77
- def __init__ (self , config ) :
78
+ def __init__ (self , config : dict [ str , Any ]) -> None :
78
79
self .config = config
79
80
self .command = config ["command" ]
80
81
self .args = config .get ("args" , [])
81
82
self .cwd = config .get ("cwd" , "." )
82
83
self .startup_timeout = config .get ("startup_timeout" , 5 )
83
84
self .ready_signal = config .get ("ready_signal" , "" )
84
- self .process = None
85
+ self .process : subprocess . Popen | None = None
85
86
self .ready = False
86
87
self .lock = asyncio .Lock ()
87
88
88
- async def start (self ):
89
+ async def start (self ) -> None :
89
90
"""Start server program"""
90
91
if self .process is not None :
91
92
return
@@ -110,46 +111,60 @@ async def start(self):
110
111
111
112
print ("✅ Server startup complete" )
112
113
113
- async def _wait_for_ready (self ):
114
+ async def _wait_for_ready (self ) -> None :
114
115
"""Wait for server ready signal"""
116
+ if self .process is None :
117
+ raise RuntimeError ("Server process is not started" )
118
+
115
119
start_time = time .time ()
116
120
117
121
while time .time () - start_time < self .startup_timeout :
118
122
if self .process .poll () is not None :
119
- stderr = self .process .stderr .read ()
123
+ if self .process .stderr is not None :
124
+ stderr = self .process .stderr .read ()
125
+ else :
126
+ raise RuntimeError ("Process stderr is not available" )
120
127
raise RuntimeError (f"Server startup failed: { stderr } " )
121
128
122
129
# Non-blocking read output
123
130
try :
124
- line = self .process .stdout .readline ()
125
- if line and self .ready_signal in line :
126
- self .ready = True
127
- return
131
+ if self .process .stdout is not None :
132
+ line = self .process .stdout .readline ()
133
+ if line and self .ready_signal in line :
134
+ self .ready = True
135
+ return
136
+ else :
137
+ raise RuntimeError ("Process stdout is not available" )
128
138
except Exception :
129
139
pass
130
140
131
141
await asyncio .sleep (0.1 )
132
142
133
143
raise TimeoutError (f"Server startup timeout ({ self .startup_timeout } s)" )
134
144
135
- async def stop (self ):
145
+ async def stop (self ) -> None :
136
146
"""Stop server program"""
137
147
if self .process is None :
138
148
return
139
149
140
150
print ("🛑 Stopping server..." )
141
151
142
152
try :
143
- # Send quit command
144
- self .process .stdin .write ("quit\n " )
145
- self .process .stdin .flush ()
153
+ if self .process .stdin is not None :
154
+ # Send quit command
155
+ self .process .stdin .write ("quit\n " )
156
+ self .process .stdin .flush ()
146
157
147
- # Wait for process to end
148
- try :
149
- self .process .wait (timeout = 3 )
150
- except subprocess .TimeoutExpired :
151
- self .process .terminate ()
152
- self .process .wait (timeout = 3 )
158
+ # Wait for process to end
159
+ try :
160
+ self .process .wait (timeout = 3 )
161
+ except subprocess .TimeoutExpired :
162
+ self .process .terminate ()
163
+ self .process .wait (timeout = 3 )
164
+ else :
165
+ raise RuntimeError (
166
+ "Process stdin is not available for sending quit command"
167
+ )
153
168
except Exception :
154
169
if self .process .poll () is None :
155
170
self .process .kill ()
@@ -158,7 +173,7 @@ async def stop(self):
158
173
self .ready = False
159
174
print ("✅ Server stopped" )
160
175
161
- async def execute_tool (self , tool_config , parameters ) :
176
+ async def execute_tool (self , tool_config : dict [ str , Any ], parameters : Any ) -> str :
162
177
"""Execute server tool"""
163
178
if not self .ready or self .process is None :
164
179
await self .start ()
@@ -172,13 +187,26 @@ async def execute_tool(self, tool_config, parameters):
172
187
command = command .replace (f"{{{ param_name } }}" , str (value ))
173
188
174
189
try :
175
- # Send command
176
- self .process .stdin .write (f"{ command } \n " )
177
- self .process .stdin .flush ()
178
-
179
- # Read response
180
- response = self .process .stdout .readline ().strip ()
181
- return response
190
+ if self .process is None :
191
+ raise RuntimeError ("Server process is not running" )
192
+
193
+ if self .process .stdin is not None :
194
+ # Send command
195
+ self .process .stdin .write (f"{ command } \n " )
196
+ self .process .stdin .flush ()
197
+ else :
198
+ raise RuntimeError (
199
+ "Process stdin is not available for sending commands"
200
+ )
201
+
202
+ if self .process .stdout is not None :
203
+ # Read response
204
+ response : str = self .process .stdout .readline ().strip ()
205
+ return response
206
+ else :
207
+ raise RuntimeError (
208
+ "Process stdout is not available for reading response"
209
+ )
182
210
183
211
except Exception as e :
184
212
return f"Error communicating with server: { str (e )} "
@@ -187,28 +215,28 @@ async def execute_tool(self, tool_config, parameters):
187
215
class HttpAdapter (BackendAdapter ):
188
216
"""HTTP API adapter"""
189
217
190
- def __init__ (self , config ) :
218
+ def __init__ (self , config : dict [ str , Any ]) -> None :
191
219
self .config = config
192
220
self .base_url = config ["base_url" ]
193
221
self .timeout = config .get ("timeout" , 10 )
194
222
self .headers = config .get ("headers" , {})
195
- self .session = None
223
+ self .session : aiohttp . ClientSession | None = None
196
224
197
- async def start (self ):
225
+ async def start (self ) -> None :
198
226
"""Start HTTP session"""
199
227
if self .session is None :
200
228
timeout = aiohttp .ClientTimeout (total = self .timeout )
201
229
self .session = aiohttp .ClientSession (timeout = timeout , headers = self .headers )
202
230
print (f"🌐 HTTP session started: { self .base_url } " )
203
231
204
- async def stop (self ):
232
+ async def stop (self ) -> None :
205
233
"""Close HTTP session"""
206
234
if self .session :
207
235
await self .session .close ()
208
236
self .session = None
209
237
print ("🌐 HTTP session closed" )
210
238
211
- async def execute_tool (self , tool_config , parameters ) :
239
+ async def execute_tool (self , tool_config : dict [ str , Any ], parameters : Any ) -> str :
212
240
"""Execute HTTP API call"""
213
241
if self .session is None :
214
242
await self .start ()
@@ -219,6 +247,10 @@ async def execute_tool(self, tool_config, parameters):
219
247
url = f"{ self .base_url } { endpoint } "
220
248
221
249
try :
250
+ result : str
251
+ if self .session is None :
252
+ raise RuntimeError ("HTTP session is not started" )
253
+
222
254
if method == "GET" :
223
255
# GET request uses parameters as query parameters
224
256
async with self .session .get (url , params = parameters ) as response :
@@ -256,7 +288,7 @@ async def execute_tool(self, tool_config, parameters):
256
288
return f"HTTP request failed: { str (e )} "
257
289
258
290
259
- def create_adapter (backend_config ) :
291
+ def create_adapter (backend_config : dict [ str , Any ]) -> BackendAdapter :
260
292
"""Create adapter based on configuration"""
261
293
backend_type = backend_config ["type" ]
262
294
config = backend_config ["config" ]
0 commit comments