1use std::{collections::BTreeMap, path::Path, sync::Arc};
2
3use async_trait::async_trait;
4use serde_json::Value;
5
6use crate::{
7 CodeModeExecuteRequest, ExecutionContext, GatewayExecuteRequest, GatewayMcpOutcome,
8 GatewayMcpRoundTrip, GatewayPromptRoute, GatewayReloadRequest, GatewayResourceRoute,
9 GatewayRouteScope, GatewayToolRoute, OpenApiExecuteRequest,
10};
11use soma_provider_adapters::python::materializer::PythonEnvironmentUpdateReport;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PortError {
16 pub code: String,
18 pub message: String,
20 pub retryable: bool,
22 pub remediation: String,
24}
25
26impl PortError {
27 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
30 Self {
31 code: code.into(),
32 message: message.into(),
33 retryable: false,
34 remediation: "Check the engine configuration and retry.".to_owned(),
35 }
36 }
37}
38
39#[async_trait]
42pub trait GatewayPort: Send + Sync {
43 async fn status(&self, context: &ExecutionContext) -> Result<Value, PortError>;
45 async fn reload(
47 &self,
48 request: GatewayReloadRequest,
49 context: &ExecutionContext,
50 ) -> Result<Value, PortError>;
51 async fn execute(
53 &self,
54 request: GatewayExecuteRequest,
55 context: &ExecutionContext,
56 ) -> Result<Value, PortError>;
57
58 async fn list_mcp_tools(
60 &self,
61 scope: Option<&GatewayRouteScope>,
62 context: &ExecutionContext,
63 ) -> Result<Vec<GatewayToolRoute>, PortError>;
64
65 async fn call_mcp_tool(
67 &self,
68 name: &str,
69 params: Value,
70 scope: Option<&GatewayRouteScope>,
71 context: &ExecutionContext,
72 ) -> Result<Option<Value>, PortError>;
73
74 async fn call_mcp_tool_once(
76 &self,
77 name: &str,
78 params: Value,
79 round_trip: GatewayMcpRoundTrip,
80 scope: Option<&GatewayRouteScope>,
81 context: &ExecutionContext,
82 ) -> Result<Option<GatewayMcpOutcome>, PortError> {
83 let _ = round_trip;
84 self.call_mcp_tool(name, params, scope, context)
85 .await
86 .map(|result| {
87 result.map(|value| {
88 GatewayMcpOutcome::Complete(serde_json::json!({
89 "resultType": "complete",
90 "content": [],
91 "structuredContent": value
92 }))
93 })
94 })
95 }
96
97 async fn get_mcp_task(
99 &self,
100 task_id: &str,
101 context: &ExecutionContext,
102 ) -> Result<Value, PortError> {
103 let _ = (task_id, context);
104 Err(PortError::new(
105 "tasks_unsupported",
106 "the configured gateway does not support MCP tasks",
107 ))
108 }
109
110 async fn update_mcp_task(
112 &self,
113 task_id: &str,
114 input_responses: BTreeMap<String, Value>,
115 context: &ExecutionContext,
116 ) -> Result<(), PortError> {
117 let _ = (task_id, input_responses, context);
118 Err(PortError::new(
119 "tasks_unsupported",
120 "the configured gateway does not support MCP tasks",
121 ))
122 }
123
124 async fn cancel_mcp_task(
126 &self,
127 task_id: &str,
128 context: &ExecutionContext,
129 ) -> Result<(), PortError> {
130 let _ = (task_id, context);
131 Err(PortError::new(
132 "tasks_unsupported",
133 "the configured gateway does not support MCP tasks",
134 ))
135 }
136
137 async fn list_mcp_resources(
139 &self,
140 scope: Option<&GatewayRouteScope>,
141 context: &ExecutionContext,
142 ) -> Result<Vec<GatewayResourceRoute>, PortError>;
143
144 async fn read_mcp_resource(
146 &self,
147 uri: &str,
148 scope: Option<&GatewayRouteScope>,
149 context: &ExecutionContext,
150 ) -> Result<Option<Value>, PortError>;
151
152 async fn read_mcp_resource_once(
154 &self,
155 uri: &str,
156 round_trip: GatewayMcpRoundTrip,
157 scope: Option<&GatewayRouteScope>,
158 context: &ExecutionContext,
159 ) -> Result<Option<GatewayMcpOutcome>, PortError> {
160 let _ = round_trip;
161 self.read_mcp_resource(uri, scope, context)
162 .await
163 .map(|result| result.map(GatewayMcpOutcome::Complete))
164 }
165
166 async fn list_mcp_prompts(
168 &self,
169 scope: Option<&GatewayRouteScope>,
170 context: &ExecutionContext,
171 ) -> Result<Vec<GatewayPromptRoute>, PortError>;
172
173 async fn get_mcp_prompt(
175 &self,
176 name: &str,
177 arguments: Option<serde_json::Map<String, Value>>,
178 scope: Option<&GatewayRouteScope>,
179 context: &ExecutionContext,
180 ) -> Result<Option<Value>, PortError>;
181 async fn get_mcp_prompt_once(
183 &self,
184 name: &str,
185 arguments: Option<serde_json::Map<String, Value>>,
186 round_trip: GatewayMcpRoundTrip,
187 scope: Option<&GatewayRouteScope>,
188 context: &ExecutionContext,
189 ) -> Result<Option<GatewayMcpOutcome>, PortError> {
190 let _ = round_trip;
191 self.get_mcp_prompt(name, arguments, scope, context)
192 .await
193 .map(|result| result.map(GatewayMcpOutcome::Complete))
194 }
195}
196
197#[async_trait]
200pub trait CodeModePort: Send + Sync {
201 async fn execute(
203 &self,
204 request: CodeModeExecuteRequest,
205 context: &ExecutionContext,
206 ) -> Result<Value, PortError>;
207}
208
209#[async_trait]
211pub trait OpenApiPort: Send + Sync {
212 async fn execute(
214 &self,
215 request: OpenApiExecuteRequest,
216 context: &ExecutionContext,
217 ) -> Result<Value, PortError>;
218}
219
220pub struct PythonEnvironmentUpdateCandidate(PythonEnvironmentUpdateReport);
222
223impl PythonEnvironmentUpdateCandidate {
224 #[must_use]
227 pub fn from_report(report: PythonEnvironmentUpdateReport) -> Self {
228 Self(report)
229 }
230
231 pub(crate) fn into_report(self) -> PythonEnvironmentUpdateReport {
232 self.0
233 }
234}
235
236#[async_trait]
238pub trait PythonEnvironmentPort: Send + Sync {
239 async fn status(&self) -> Result<Value, PortError>;
241 async fn prune(
243 &self,
244 stale_before_unix_seconds: u64,
245 max_entries: usize,
246 apply: bool,
247 ) -> Result<Value, PortError>;
248 async fn repair(&self, provider_path: &Path) -> Result<Value, PortError>;
250 async fn update(
252 &self,
253 provider_path: &Path,
254 ) -> Result<PythonEnvironmentUpdateCandidate, PortError>;
255}
256
257pub struct ApplicationPorts {
259 pub gateway: Arc<dyn GatewayPort>,
261 pub codemode: Arc<dyn CodeModePort>,
263 pub openapi: Arc<dyn OpenApiPort>,
265 pub python_environment: Arc<dyn PythonEnvironmentPort>,
267}
268
269impl ApplicationPorts {
270 pub fn unavailable() -> Self {
272 let port = Arc::new(UnavailableEnginePort);
273 Self {
274 gateway: port.clone(),
275 codemode: port.clone(),
276 openapi: port.clone(),
277 python_environment: port,
278 }
279 }
280
281 pub fn with_gateway(mut self, gateway: Arc<dyn GatewayPort>) -> Self {
283 self.gateway = gateway;
284 self
285 }
286
287 pub fn with_codemode(mut self, codemode: Arc<dyn CodeModePort>) -> Self {
289 self.codemode = codemode;
290 self
291 }
292
293 pub fn with_openapi(mut self, openapi: Arc<dyn OpenApiPort>) -> Self {
295 self.openapi = openapi;
296 self
297 }
298
299 pub fn with_python_environment(
301 mut self,
302 python_environment: Arc<dyn PythonEnvironmentPort>,
303 ) -> Self {
304 self.python_environment = python_environment;
305 self
306 }
307}
308
309struct UnavailableEnginePort;
310
311impl UnavailableEnginePort {
312 fn error(engine: &str) -> PortError {
313 PortError::new(
314 "engine_unavailable",
315 format!("{engine} is not configured for this application instance"),
316 )
317 }
318}
319
320#[async_trait]
321impl GatewayPort for UnavailableEnginePort {
322 async fn status(&self, _context: &ExecutionContext) -> Result<Value, PortError> {
323 Err(Self::error("gateway"))
324 }
325
326 async fn reload(
327 &self,
328 _request: GatewayReloadRequest,
329 _context: &ExecutionContext,
330 ) -> Result<Value, PortError> {
331 Err(Self::error("gateway"))
332 }
333
334 async fn execute(
335 &self,
336 _request: GatewayExecuteRequest,
337 _context: &ExecutionContext,
338 ) -> Result<Value, PortError> {
339 Err(Self::error("gateway"))
340 }
341
342 async fn list_mcp_tools(
343 &self,
344 _scope: Option<&GatewayRouteScope>,
345 _context: &ExecutionContext,
346 ) -> Result<Vec<GatewayToolRoute>, PortError> {
347 Err(Self::error("gateway"))
348 }
349
350 async fn call_mcp_tool(
351 &self,
352 _name: &str,
353 _params: Value,
354 _scope: Option<&GatewayRouteScope>,
355 _context: &ExecutionContext,
356 ) -> Result<Option<Value>, PortError> {
357 Err(Self::error("gateway"))
358 }
359
360 async fn list_mcp_resources(
361 &self,
362 _scope: Option<&GatewayRouteScope>,
363 _context: &ExecutionContext,
364 ) -> Result<Vec<GatewayResourceRoute>, PortError> {
365 Err(Self::error("gateway"))
366 }
367
368 async fn read_mcp_resource(
369 &self,
370 _uri: &str,
371 _scope: Option<&GatewayRouteScope>,
372 _context: &ExecutionContext,
373 ) -> Result<Option<Value>, PortError> {
374 Err(Self::error("gateway"))
375 }
376
377 async fn list_mcp_prompts(
378 &self,
379 _scope: Option<&GatewayRouteScope>,
380 _context: &ExecutionContext,
381 ) -> Result<Vec<GatewayPromptRoute>, PortError> {
382 Err(Self::error("gateway"))
383 }
384
385 async fn get_mcp_prompt(
386 &self,
387 _name: &str,
388 _arguments: Option<serde_json::Map<String, Value>>,
389 _scope: Option<&GatewayRouteScope>,
390 _context: &ExecutionContext,
391 ) -> Result<Option<Value>, PortError> {
392 Err(Self::error("gateway"))
393 }
394}
395
396#[async_trait]
397impl CodeModePort for UnavailableEnginePort {
398 async fn execute(
399 &self,
400 _request: CodeModeExecuteRequest,
401 _context: &ExecutionContext,
402 ) -> Result<Value, PortError> {
403 Err(Self::error("Code Mode"))
404 }
405}
406
407#[async_trait]
408impl OpenApiPort for UnavailableEnginePort {
409 async fn execute(
410 &self,
411 _request: OpenApiExecuteRequest,
412 _context: &ExecutionContext,
413 ) -> Result<Value, PortError> {
414 Err(Self::error("OpenAPI"))
415 }
416}
417
418#[async_trait]
419impl PythonEnvironmentPort for UnavailableEnginePort {
420 async fn status(&self) -> Result<Value, PortError> {
421 Err(Self::error("Python environment lifecycle"))
422 }
423
424 async fn prune(
425 &self,
426 _stale_before_unix_seconds: u64,
427 _max_entries: usize,
428 _apply: bool,
429 ) -> Result<Value, PortError> {
430 Err(Self::error("Python environment lifecycle"))
431 }
432
433 async fn repair(&self, _provider_path: &Path) -> Result<Value, PortError> {
434 Err(Self::error("Python environment lifecycle"))
435 }
436
437 async fn update(
438 &self,
439 _provider_path: &Path,
440 ) -> Result<PythonEnvironmentUpdateCandidate, PortError> {
441 Err(Self::error("Python environment lifecycle"))
442 }
443}