pi-codemcp 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/extensions/index.ts +67 -77
- package/package.json +1 -1
- package/sidecar/chains.py +17 -44
- package/sidecar/cli.py +27 -7
- package/sidecar/executor.py +207 -35
- package/sidecar/gateway.py +505 -141
- package/sidecar/mcp_config.py +27 -2
- package/sidecar/refinement_cache.py +154 -0
- package/sidecar/sandbox_api.py +44 -0
- package/sidecar/stats.py +801 -194
- package/sidecar/tool_catalog.py +1 -9
- package/src/chains.ts +63 -34
- package/src/config.ts +8 -39
- package/src/mcp-client.ts +2 -1
- package/src/modal.ts +87 -248
- package/src/prompts.ts +3 -3
- package/src/tools.ts +37 -17
package/sidecar/executor.py
CHANGED
|
@@ -24,6 +24,15 @@ from pydantic import (
|
|
|
24
24
|
from pydantic_core import to_json
|
|
25
25
|
|
|
26
26
|
from .json_types import JSON_VALUE_ADAPTER, JsonObject, JsonValue
|
|
27
|
+
from .refinement_cache import RetainedResult
|
|
28
|
+
from .sandbox_api import (
|
|
29
|
+
EXPECT_INTEGER_NAME,
|
|
30
|
+
EXPECT_LIST_NAME,
|
|
31
|
+
EXPECT_OBJECT_NAME,
|
|
32
|
+
EXPECT_STRING_NAME,
|
|
33
|
+
INSPECT_JSON_NAME,
|
|
34
|
+
SANDBOX_FUNCTION_EXTERNALS,
|
|
35
|
+
)
|
|
27
36
|
from .tool_catalog import referenced_calls, schema_path_summary
|
|
28
37
|
|
|
29
38
|
if TYPE_CHECKING:
|
|
@@ -36,10 +45,24 @@ INSPECT_SAMPLE_LIMIT = 3
|
|
|
36
45
|
INSPECT_DEPTH_LIMIT = 6
|
|
37
46
|
INSPECT_COLLECTION_LIMIT = 10
|
|
38
47
|
INSPECT_STRING_LIMIT = 200
|
|
39
|
-
INSPECT_KEY_LIMIT =
|
|
48
|
+
INSPECT_KEY_LIMIT = 80
|
|
40
49
|
INSPECT_BYTE_LIMIT = 8 * 1024
|
|
41
50
|
CHAIN_INPUT_EXTERNAL = "__codemcp_saved_chain_input"
|
|
42
|
-
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
type FailureStage = Literal["preflight", "runtime", "timeout", "cancelled", "result"]
|
|
54
|
+
type FailureKind = Literal[
|
|
55
|
+
"preflight",
|
|
56
|
+
"result",
|
|
57
|
+
"result_reference",
|
|
58
|
+
"sandbox_runtime",
|
|
59
|
+
"sandbox_timeout",
|
|
60
|
+
"upstream",
|
|
61
|
+
"upstream_transport",
|
|
62
|
+
"upstream_timeout",
|
|
63
|
+
"cancelled",
|
|
64
|
+
"internal",
|
|
65
|
+
]
|
|
43
66
|
|
|
44
67
|
|
|
45
68
|
class ExecutionMetrics(BaseModel):
|
|
@@ -51,6 +74,17 @@ class ExecutionMetrics(BaseModel):
|
|
|
51
74
|
result_bytes: int = Field(default=0, ge=0)
|
|
52
75
|
|
|
53
76
|
|
|
77
|
+
class ExecutionFailureInfo(BaseModel):
|
|
78
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
79
|
+
|
|
80
|
+
kind: FailureKind
|
|
81
|
+
server: str | None = None
|
|
82
|
+
tool: str | None = None
|
|
83
|
+
retryable: bool
|
|
84
|
+
status: int | None = Field(default=None, ge=100, le=599)
|
|
85
|
+
message: str = Field(min_length=1)
|
|
86
|
+
|
|
87
|
+
|
|
54
88
|
class ExecutionContext:
|
|
55
89
|
def __init__(
|
|
56
90
|
self,
|
|
@@ -66,6 +100,7 @@ class ExecutionContext:
|
|
|
66
100
|
self.calls_made = 0
|
|
67
101
|
self.chain_calls = 0
|
|
68
102
|
self.metrics = ExecutionMetrics()
|
|
103
|
+
self.failure: ExecutionFailureInfo | None = None
|
|
69
104
|
self.chain_stack: ContextVar[tuple[str, ...]] = ContextVar(
|
|
70
105
|
"codemcp_chain_stack",
|
|
71
106
|
default=(),
|
|
@@ -82,9 +117,7 @@ class ExecutionContext:
|
|
|
82
117
|
ToolCall = Callable[[str, JsonObject], Awaitable[JsonValue]]
|
|
83
118
|
ContextToolCall = Callable[[str, JsonObject, ExecutionContext], Awaitable[JsonValue]]
|
|
84
119
|
ExternalFunction = Callable[..., Awaitable[JsonValue]]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
type FailureStage = Literal["preflight", "runtime", "timeout", "cancelled", "result"]
|
|
120
|
+
ResultRetainer = Callable[[JsonValue], RetainedResult | None]
|
|
88
121
|
|
|
89
122
|
|
|
90
123
|
class ExecutionResponse(BaseModel):
|
|
@@ -94,7 +127,10 @@ class ExecutionResponse(BaseModel):
|
|
|
94
127
|
failure_stage: FailureStage | None = None
|
|
95
128
|
result: JsonValue = None
|
|
96
129
|
error: str | None = None
|
|
130
|
+
failure: ExecutionFailureInfo | None = None
|
|
97
131
|
shape: JsonObject | None = None
|
|
132
|
+
result_ref: str | None = None
|
|
133
|
+
expires_in_seconds: int | None = Field(default=None, ge=1)
|
|
98
134
|
calls_made: int = Field(default=0, ge=0)
|
|
99
135
|
chain_calls: int = Field(default=0, ge=0)
|
|
100
136
|
metrics: ExecutionMetrics = Field(default_factory=ExecutionMetrics, exclude=True)
|
|
@@ -115,33 +151,51 @@ class ExecutionResponse(BaseModel):
|
|
|
115
151
|
)
|
|
116
152
|
|
|
117
153
|
@classmethod
|
|
118
|
-
def
|
|
154
|
+
def failed(
|
|
119
155
|
cls,
|
|
120
156
|
*,
|
|
121
157
|
failure_stage: FailureStage,
|
|
122
158
|
error: str,
|
|
159
|
+
failure: ExecutionFailureInfo,
|
|
123
160
|
calls_made: int = 0,
|
|
124
161
|
chain_calls: int = 0,
|
|
125
162
|
shape: JsonObject | None = None,
|
|
163
|
+
result_ref: str | None = None,
|
|
164
|
+
expires_in_seconds: int | None = None,
|
|
126
165
|
) -> Self:
|
|
127
166
|
return cls(
|
|
128
167
|
ok=False,
|
|
129
168
|
failure_stage=failure_stage,
|
|
130
169
|
error=error,
|
|
170
|
+
failure=failure,
|
|
131
171
|
calls_made=calls_made,
|
|
132
172
|
chain_calls=chain_calls,
|
|
133
173
|
shape=shape,
|
|
174
|
+
result_ref=result_ref,
|
|
175
|
+
expires_in_seconds=expires_in_seconds,
|
|
134
176
|
)
|
|
135
177
|
|
|
136
178
|
@model_validator(mode="after")
|
|
137
179
|
def validate_state(self) -> Self:
|
|
138
180
|
if self.ok:
|
|
139
|
-
|
|
181
|
+
failure_details = (
|
|
182
|
+
self.failure_stage,
|
|
183
|
+
self.error,
|
|
184
|
+
self.failure,
|
|
185
|
+
self.shape,
|
|
186
|
+
self.result_ref,
|
|
187
|
+
self.expires_in_seconds,
|
|
188
|
+
)
|
|
189
|
+
if any(detail is not None for detail in failure_details):
|
|
140
190
|
raise ValueError("successful execution cannot contain failure details")
|
|
141
|
-
elif self.failure_stage is None or self.error is None:
|
|
142
|
-
raise ValueError("failed execution requires a failure stage and
|
|
191
|
+
elif self.failure_stage is None or self.error is None or self.failure is None:
|
|
192
|
+
raise ValueError("failed execution requires a failure stage, error, and details")
|
|
143
193
|
elif self.result is not None:
|
|
144
194
|
raise ValueError("failed execution cannot contain a result")
|
|
195
|
+
elif (self.result_ref is None) != (self.expires_in_seconds is None):
|
|
196
|
+
raise ValueError("result reference and expiry must be provided together")
|
|
197
|
+
elif self.result_ref is not None and self.failure_stage != "result":
|
|
198
|
+
raise ValueError("only a result failure can contain a result reference")
|
|
145
199
|
return self
|
|
146
200
|
|
|
147
201
|
@model_serializer(mode="plain")
|
|
@@ -153,14 +207,21 @@ class ExecutionResponse(BaseModel):
|
|
|
153
207
|
"calls_made": self.calls_made,
|
|
154
208
|
}
|
|
155
209
|
else:
|
|
210
|
+
failure = self.failure
|
|
211
|
+
if failure is None:
|
|
212
|
+
raise RuntimeError("failed execution is missing structured details")
|
|
156
213
|
response = {
|
|
157
214
|
"ok": False,
|
|
158
215
|
"failure_stage": self.failure_stage,
|
|
159
216
|
"error": self.error,
|
|
217
|
+
"failure": failure.model_dump(mode="json"),
|
|
160
218
|
"calls_made": self.calls_made,
|
|
161
219
|
}
|
|
162
220
|
if self.shape is not None:
|
|
163
221
|
response["shape"] = self.shape
|
|
222
|
+
if self.result_ref is not None:
|
|
223
|
+
response["result_ref"] = self.result_ref
|
|
224
|
+
response["expires_in_seconds"] = self.expires_in_seconds
|
|
164
225
|
if self.chain_calls > 0:
|
|
165
226
|
response["chain_calls"] = self.chain_calls
|
|
166
227
|
timings: JsonObject = {
|
|
@@ -200,7 +261,14 @@ class MontyExecutor:
|
|
|
200
261
|
def update_catalog(self, catalog: ToolCatalog) -> None:
|
|
201
262
|
self.catalog = catalog
|
|
202
263
|
|
|
203
|
-
async def execute(
|
|
264
|
+
async def execute(
|
|
265
|
+
self,
|
|
266
|
+
code: str,
|
|
267
|
+
call_tool: ToolCall,
|
|
268
|
+
*,
|
|
269
|
+
input_value: JsonValue = None,
|
|
270
|
+
retain_result: ResultRetainer | None = None,
|
|
271
|
+
) -> ExecutionResponse:
|
|
204
272
|
async def adapted(
|
|
205
273
|
name: str,
|
|
206
274
|
arguments: JsonObject,
|
|
@@ -208,22 +276,39 @@ class MontyExecutor:
|
|
|
208
276
|
) -> JsonValue:
|
|
209
277
|
return await call_tool(name, arguments)
|
|
210
278
|
|
|
211
|
-
return await self.execute_graph(
|
|
279
|
+
return await self.execute_graph(
|
|
280
|
+
code,
|
|
281
|
+
adapted,
|
|
282
|
+
input_value=input_value,
|
|
283
|
+
retain_result=retain_result,
|
|
284
|
+
)
|
|
212
285
|
|
|
213
286
|
async def execute_graph(
|
|
214
287
|
self,
|
|
215
288
|
code: str,
|
|
216
289
|
call_tool: ContextToolCall,
|
|
290
|
+
*,
|
|
291
|
+
input_value: JsonValue = None,
|
|
292
|
+
retain_result: ResultRetainer | None = None,
|
|
217
293
|
) -> ExecutionResponse:
|
|
218
294
|
async with self._execution_lock:
|
|
219
295
|
context = self._new_context(self.catalog, call_tool)
|
|
220
|
-
return await self._execute_program(
|
|
296
|
+
return await self._execute_program(
|
|
297
|
+
code,
|
|
298
|
+
context,
|
|
299
|
+
input_value=input_value,
|
|
300
|
+
input_type="JsonValue" if input_value is not None else None,
|
|
301
|
+
enforce_result_limit=True,
|
|
302
|
+
retain_result=retain_result,
|
|
303
|
+
)
|
|
221
304
|
|
|
222
305
|
async def execute_saved_chain(
|
|
223
306
|
self,
|
|
224
307
|
chain: SavedChainManifest,
|
|
225
308
|
arguments: JsonObject,
|
|
226
309
|
call_tool: ContextToolCall,
|
|
310
|
+
*,
|
|
311
|
+
retain_result: ResultRetainer | None = None,
|
|
227
312
|
) -> ExecutionResponse:
|
|
228
313
|
async with self._execution_lock:
|
|
229
314
|
catalog = self.catalog
|
|
@@ -231,9 +316,15 @@ class MontyExecutor:
|
|
|
231
316
|
try:
|
|
232
317
|
validated = catalog.validate_arguments(spec.name, arguments)
|
|
233
318
|
except (TypeError, ValidationError, ValueError) as error:
|
|
234
|
-
|
|
319
|
+
message = f"{chain.call}: invalid arguments: {error}"
|
|
320
|
+
return ExecutionResponse.failed(
|
|
235
321
|
failure_stage="preflight",
|
|
236
|
-
error=
|
|
322
|
+
error=message,
|
|
323
|
+
failure=ExecutionFailureInfo(
|
|
324
|
+
kind="preflight",
|
|
325
|
+
retryable=False,
|
|
326
|
+
message=message,
|
|
327
|
+
),
|
|
237
328
|
)
|
|
238
329
|
context = self._new_context(catalog, call_tool)
|
|
239
330
|
context.chain_stack.set((chain.name,))
|
|
@@ -245,6 +336,7 @@ class MontyExecutor:
|
|
|
245
336
|
output_type=spec.output_type_name,
|
|
246
337
|
output_spec_name=spec.name,
|
|
247
338
|
enforce_result_limit=True,
|
|
339
|
+
retain_result=retain_result,
|
|
248
340
|
)
|
|
249
341
|
|
|
250
342
|
async def execute_nested_chain(
|
|
@@ -318,11 +410,12 @@ class MontyExecutor:
|
|
|
318
410
|
code: str,
|
|
319
411
|
context: ExecutionContext,
|
|
320
412
|
*,
|
|
321
|
-
input_value:
|
|
413
|
+
input_value: JsonValue = None,
|
|
322
414
|
input_type: str | None = None,
|
|
323
415
|
output_type: str | None = None,
|
|
324
416
|
output_spec_name: str | None = None,
|
|
325
417
|
enforce_result_limit: bool,
|
|
418
|
+
retain_result: ResultRetainer | None = None,
|
|
326
419
|
) -> ExecutionResponse:
|
|
327
420
|
if not code.strip():
|
|
328
421
|
return self._failure(context, "preflight", "Execution code must not be empty")
|
|
@@ -333,6 +426,7 @@ class MontyExecutor:
|
|
|
333
426
|
input_type=input_type,
|
|
334
427
|
output_type=output_type,
|
|
335
428
|
typed=True,
|
|
429
|
+
has_input=has_input,
|
|
336
430
|
)
|
|
337
431
|
referenced = referenced_calls(code, context.catalog.facade_calls)
|
|
338
432
|
type_stubs = context.catalog.type_stubs_for(
|
|
@@ -403,8 +497,19 @@ class MontyExecutor:
|
|
|
403
497
|
if remaining <= 0:
|
|
404
498
|
raise TimeoutError
|
|
405
499
|
timeout = min(context.settings.tool_timeout_seconds, remaining)
|
|
406
|
-
|
|
407
|
-
|
|
500
|
+
try:
|
|
501
|
+
async with asyncio.timeout(timeout):
|
|
502
|
+
return await context.call_tool(name, validated, context)
|
|
503
|
+
except TimeoutError:
|
|
504
|
+
message = f"{spec.call} timed out after {timeout:g}s"
|
|
505
|
+
context.failure = ExecutionFailureInfo(
|
|
506
|
+
kind="upstream_timeout",
|
|
507
|
+
server=spec.server,
|
|
508
|
+
tool=spec.backend_name,
|
|
509
|
+
retryable=True,
|
|
510
|
+
message=message,
|
|
511
|
+
)
|
|
512
|
+
raise
|
|
408
513
|
|
|
409
514
|
external_functions: dict[str, ExternalFunction] = {}
|
|
410
515
|
|
|
@@ -430,7 +535,37 @@ class MontyExecutor:
|
|
|
430
535
|
byte_limit=_inspection_byte_limit(context.settings.result_byte_limit),
|
|
431
536
|
)
|
|
432
537
|
|
|
433
|
-
|
|
538
|
+
async def expect_object_external(value: JsonValue) -> JsonValue:
|
|
539
|
+
await asyncio.sleep(0)
|
|
540
|
+
if not isinstance(value, dict):
|
|
541
|
+
raise TypeError(f"expect_object expected object, got {_shape_label(value)}")
|
|
542
|
+
return value
|
|
543
|
+
|
|
544
|
+
async def expect_list_external(value: JsonValue) -> JsonValue:
|
|
545
|
+
await asyncio.sleep(0)
|
|
546
|
+
if not isinstance(value, list):
|
|
547
|
+
raise TypeError(f"expect_list expected array, got {_shape_label(value)}")
|
|
548
|
+
return value
|
|
549
|
+
|
|
550
|
+
async def expect_string_external(value: JsonValue) -> JsonValue:
|
|
551
|
+
await asyncio.sleep(0)
|
|
552
|
+
if not isinstance(value, str):
|
|
553
|
+
raise TypeError(f"expect_string expected string, got {_shape_label(value)}")
|
|
554
|
+
return value
|
|
555
|
+
|
|
556
|
+
async def expect_integer_external(value: JsonValue) -> JsonValue:
|
|
557
|
+
await asyncio.sleep(0)
|
|
558
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
559
|
+
raise TypeError(f"expect_integer expected integer, got {_shape_label(value)}")
|
|
560
|
+
return value
|
|
561
|
+
|
|
562
|
+
external_functions[SANDBOX_FUNCTION_EXTERNALS[INSPECT_JSON_NAME]] = inspect_json_external
|
|
563
|
+
external_functions[SANDBOX_FUNCTION_EXTERNALS[EXPECT_OBJECT_NAME]] = expect_object_external
|
|
564
|
+
external_functions[SANDBOX_FUNCTION_EXTERNALS[EXPECT_LIST_NAME]] = expect_list_external
|
|
565
|
+
external_functions[SANDBOX_FUNCTION_EXTERNALS[EXPECT_STRING_NAME]] = expect_string_external
|
|
566
|
+
external_functions[SANDBOX_FUNCTION_EXTERNALS[EXPECT_INTEGER_NAME]] = (
|
|
567
|
+
expect_integer_external
|
|
568
|
+
)
|
|
434
569
|
for spec in context.catalog.tools.values():
|
|
435
570
|
|
|
436
571
|
async def sdk_method(
|
|
@@ -491,7 +626,10 @@ class MontyExecutor:
|
|
|
491
626
|
lowered = message.lower()
|
|
492
627
|
stage: Literal["runtime", "timeout"] = (
|
|
493
628
|
"timeout"
|
|
494
|
-
if
|
|
629
|
+
if (context.failure is not None and context.failure.kind == "upstream_timeout")
|
|
630
|
+
or "duration" in lowered
|
|
631
|
+
or "timed out" in lowered
|
|
632
|
+
or "timeout" in lowered
|
|
495
633
|
else "runtime"
|
|
496
634
|
)
|
|
497
635
|
return self._failure(context, stage, message)
|
|
@@ -513,11 +651,18 @@ class MontyExecutor:
|
|
|
513
651
|
context.metrics.serialization_ms += _elapsed_ms(serialization_started)
|
|
514
652
|
context.metrics.result_bytes = result_bytes
|
|
515
653
|
if enforce_result_limit and result_bytes >= context.settings.result_byte_limit:
|
|
516
|
-
|
|
654
|
+
retained = retain_result(result) if retain_result is not None else None
|
|
655
|
+
message = (
|
|
656
|
+
f"Returned value is {result_bytes} bytes; reduce it below "
|
|
657
|
+
f"{context.settings.result_byte_limit} bytes"
|
|
658
|
+
)
|
|
659
|
+
response = ExecutionResponse.failed(
|
|
517
660
|
failure_stage="result",
|
|
518
|
-
error=
|
|
519
|
-
|
|
520
|
-
|
|
661
|
+
error=message,
|
|
662
|
+
failure=ExecutionFailureInfo(
|
|
663
|
+
kind="result",
|
|
664
|
+
retryable=False,
|
|
665
|
+
message=message,
|
|
521
666
|
),
|
|
522
667
|
shape=_inspect_json(
|
|
523
668
|
result,
|
|
@@ -527,6 +672,8 @@ class MontyExecutor:
|
|
|
527
672
|
),
|
|
528
673
|
calls_made=context.calls_made,
|
|
529
674
|
chain_calls=context.chain_calls,
|
|
675
|
+
result_ref=retained.reference if retained is not None else None,
|
|
676
|
+
expires_in_seconds=(retained.expires_in_seconds if retained is not None else None),
|
|
530
677
|
)
|
|
531
678
|
response.metrics = context.metrics.model_copy()
|
|
532
679
|
return response
|
|
@@ -584,9 +731,11 @@ class MontyExecutor:
|
|
|
584
731
|
stage: Literal["preflight", "runtime", "timeout", "cancelled", "result"],
|
|
585
732
|
error: str,
|
|
586
733
|
) -> ExecutionResponse:
|
|
587
|
-
|
|
734
|
+
failure = context.failure or _execution_failure_info(stage, error)
|
|
735
|
+
response = ExecutionResponse.failed(
|
|
588
736
|
failure_stage=stage,
|
|
589
737
|
error=error,
|
|
738
|
+
failure=failure,
|
|
590
739
|
calls_made=context.calls_made,
|
|
591
740
|
chain_calls=context.chain_calls,
|
|
592
741
|
)
|
|
@@ -594,6 +743,27 @@ class MontyExecutor:
|
|
|
594
743
|
return response
|
|
595
744
|
|
|
596
745
|
|
|
746
|
+
def _execution_failure_info(
|
|
747
|
+
stage: FailureStage,
|
|
748
|
+
message: str,
|
|
749
|
+
) -> ExecutionFailureInfo:
|
|
750
|
+
if stage == "preflight":
|
|
751
|
+
kind: FailureKind = "preflight"
|
|
752
|
+
elif stage == "result":
|
|
753
|
+
kind = "result"
|
|
754
|
+
elif stage == "timeout":
|
|
755
|
+
kind = "sandbox_timeout"
|
|
756
|
+
elif stage == "cancelled":
|
|
757
|
+
kind = "cancelled"
|
|
758
|
+
else:
|
|
759
|
+
kind = "sandbox_runtime"
|
|
760
|
+
return ExecutionFailureInfo(
|
|
761
|
+
kind=kind,
|
|
762
|
+
retryable=False,
|
|
763
|
+
message=message,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
597
767
|
def _saved_chain_result_error(
|
|
598
768
|
error: TypeError | ValidationError | ValueError,
|
|
599
769
|
output_schema: JsonObject,
|
|
@@ -623,7 +793,7 @@ def _elapsed_ms(started: float) -> float:
|
|
|
623
793
|
|
|
624
794
|
|
|
625
795
|
def _inspection_byte_limit(result_byte_limit: int) -> int:
|
|
626
|
-
return max(256, min(INSPECT_BYTE_LIMIT, result_byte_limit *
|
|
796
|
+
return max(256, min(INSPECT_BYTE_LIMIT, result_byte_limit * 2 // 3))
|
|
627
797
|
|
|
628
798
|
|
|
629
799
|
def _inspect_json(
|
|
@@ -867,10 +1037,10 @@ def _wrap_code(
|
|
|
867
1037
|
normalized = textwrap.dedent(code).strip("\n")
|
|
868
1038
|
uses_input = input_type is not None if has_input is None else has_input
|
|
869
1039
|
if typed and uses_input:
|
|
870
|
-
if input_type is None
|
|
871
|
-
raise ValueError("Typed
|
|
1040
|
+
if input_type is None:
|
|
1041
|
+
raise ValueError("Typed input code requires an input type")
|
|
872
1042
|
signature = f"input: {input_type}"
|
|
873
|
-
return_annotation = f" -> {output_type}"
|
|
1043
|
+
return_annotation = f" -> {output_type}" if output_type is not None else ""
|
|
874
1044
|
elif uses_input:
|
|
875
1045
|
signature = "input"
|
|
876
1046
|
return_annotation = ""
|
|
@@ -898,13 +1068,15 @@ def _rewrite_sdk_calls(code: str, catalog: ToolCatalog) -> str:
|
|
|
898
1068
|
def visit_Call(self, node: ast.Call) -> ast.AST:
|
|
899
1069
|
self.generic_visit(node)
|
|
900
1070
|
function = node.func
|
|
901
|
-
if isinstance(function, ast.Name)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1071
|
+
if isinstance(function, ast.Name):
|
|
1072
|
+
external_name = SANDBOX_FUNCTION_EXTERNALS.get(function.id)
|
|
1073
|
+
if external_name is not None:
|
|
1074
|
+
external_call = ast.Call(
|
|
1075
|
+
func=ast.Name(id=external_name, ctx=ast.Load()),
|
|
1076
|
+
args=node.args,
|
|
1077
|
+
keywords=node.keywords,
|
|
1078
|
+
)
|
|
1079
|
+
return ast.copy_location(ast.Await(value=external_call), node)
|
|
908
1080
|
if not isinstance(function, ast.Attribute):
|
|
909
1081
|
return node
|
|
910
1082
|
if not isinstance(function.value, ast.Name):
|