Skip to content

Commit f89439d

Browse files
authored
Merge pull request #211 from moonpyt/main
Add an optional bearer_token argument to the delete operation and Fixed InterruptError propagation
2 parents 39a7dbe + 7a657a4 commit f89439d

File tree

4 files changed

+82
-16
lines changed

4 files changed

+82
-16
lines changed

spoon_ai/graph/engine.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ async def __call__(self, state: State, config: Optional[Dict[str, Any]] = None)
8686
else:
8787
return {"result": result}
8888

89+
except InterruptError:
90+
# Let InterruptError propagate to be handled in invoke()
91+
raise
8992
except Exception as e:
9093
logger.error(f"Node {self.name} execution failed: {e}")
9194
raise NodeExecutionError(f"Node '{self.name}' failed", node_name=self.name, original_error=e, state=state) from e
@@ -757,8 +760,44 @@ async def invoke(self, initial_state: Optional[Dict[str, Any]] = None, config: O
757760
config = config or {}
758761
thread_id = config.get("configurable", {}).get("thread_id", str(uuid.uuid4()))
759762

760-
# Handle resume from checkpoint
761-
if self._resume_thread_id:
763+
# Handle Command object (for resume/goto/update)
764+
if isinstance(initial_state, Command):
765+
cmd = initial_state
766+
# Handle resume: load checkpoint and merge updates
767+
if cmd.resume is not None:
768+
checkpoint = self.graph.checkpointer.get_checkpoint(thread_id)
769+
if checkpoint:
770+
state = checkpoint.values.copy()
771+
# Merge resume data into state
772+
if isinstance(cmd.resume, dict):
773+
state.update(cmd.resume)
774+
current_node = checkpoint.next[0] if checkpoint.next else self.graph._entry_point
775+
iteration = checkpoint.metadata.get("iteration", 0) + 1
776+
else:
777+
raise GraphExecutionError(f"No checkpoint found for thread_id '{thread_id}' to resume from")
778+
# Handle goto: jump to specific node
779+
elif cmd.goto:
780+
state = self._initialize_state(cmd.update or {})
781+
current_node = cmd.goto
782+
iteration = 0
783+
# Handle update: update state and continue
784+
elif cmd.update:
785+
checkpoint = self.graph.checkpointer.get_checkpoint(thread_id)
786+
if checkpoint:
787+
state = checkpoint.values.copy()
788+
state.update(cmd.update)
789+
current_node = checkpoint.next[0] if checkpoint.next else self.graph._entry_point
790+
iteration = checkpoint.metadata.get("iteration", 0) + 1
791+
else:
792+
state = self._initialize_state(cmd.update)
793+
current_node = self.graph._entry_point
794+
iteration = 0
795+
else:
796+
state = self._initialize_state({})
797+
current_node = self.graph._entry_point
798+
iteration = 0
799+
# Handle resume from checkpoint (legacy)
800+
elif self._resume_thread_id:
762801
thread_id = self._resume_thread_id
763802
checkpoint = self.graph.checkpointer.get_checkpoint(thread_id, self._resume_checkpoint_id)
764803
if checkpoint:
@@ -871,6 +910,9 @@ async def _execute_node(self, node_name: str, state: State, config: Optional[Dic
871910
except Exception:
872911
pass
873912
return result if isinstance(result, dict) else {"result": result}
913+
except InterruptError:
914+
# Let InterruptError propagate to be handled in invoke()
915+
raise
874916
except Exception as e:
875917
logger.error(f"Node {node_name} execution failed: {e}")
876918
try:

spoon_ai/neofs/client.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,27 @@ def get_object_header_by_attribute(
345345
return self._request('HEAD', f'/v1/objects/{container_id}/by_attribute/{encoded_key}/{encoded_val}', headers=headers, params=params if params else None)
346346

347347
# 8. Object Delete
348-
def delete_object(self, container_id: str, object_id: str) -> SuccessResponse:
349-
params = generate_simple_signature_params(self.private_key_wif)
350-
response = self._request('DELETE', f'/v1/objects/{container_id}/{object_id}', params=params)
348+
def delete_object(
349+
self,
350+
container_id: str,
351+
object_id: str,
352+
*,
353+
bearer_token: Optional[str] = None
354+
) -> SuccessResponse:
355+
"""Delete object. Bearer token is optional for public containers, required for eACL containers with DENY DELETE rule."""
356+
headers = {}
357+
params = {}
358+
359+
if bearer_token:
360+
signature_value, signature_key = sign_bearer_token(bearer_token, self.private_key_wif, wallet_connect=False)
361+
headers['Authorization'] = f'Bearer {bearer_token}'
362+
headers['X-Bearer-Signature'] = signature_value
363+
headers['X-Bearer-Signature-Key'] = signature_key
364+
params = generate_simple_signature_params(self.private_key_wif, payload_parts=(signature_value.encode(),))
365+
else:
366+
params = generate_simple_signature_params(self.private_key_wif)
367+
368+
response = self._request('DELETE', f'/v1/objects/{container_id}/{object_id}', headers=headers, params=params)
351369
return SuccessResponse(**response.json())
352370

353371
# 9. Object Search

spoon_ai/tools/mcp_tool.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,15 @@ def _create_transport_from_config(self, config: dict):
111111
elif command == "uvx":
112112
if not args:
113113
raise ValueError("No package specified for uvx transport")
114-
package = args[0]
115-
return UvxStdioTransport(package=package, args=args[1:], env_vars=env)
114+
tool_name = args[0]
115+
tool_args = args[1:] if len(args) > 1 else None
116+
return UvxStdioTransport(tool_name=tool_name, tool_args=tool_args, env_vars=env)
116117
elif command in ["python", "python3"]:
117-
return PythonStdioTransport(args=args, env=merged_env)
118+
if not args:
119+
raise ValueError("No script path specified for python transport")
120+
script_path = args[0]
121+
script_args = args[1:] if len(args) > 1 else None
122+
return PythonStdioTransport(script_path=script_path, args=script_args, env=merged_env)
118123
else:
119124
full_command = [command] + args
120125
return StdioTransport(command=full_command[0], args=full_command[1:], env=merged_env)

spoon_ai/tools/neofs_tools.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -578,20 +578,21 @@ class DeleteObjectTool(BaseTool):
578578
"""Delete an object"""
579579

580580
name: str = "delete_neofs_object"
581-
description: str = "Delete an object from container."
581+
description: str = "Delete an object from container. Bearer token is optional for public containers, required for eACL containers with DENY DELETE rule."
582582
parameters: dict = {
583583
"type": "object",
584584
"properties": {
585585
"container_id": {"type": "string", "description": "Container ID"},
586-
"object_id": {"type": "string", "description": "Object ID"}
586+
"object_id": {"type": "string", "description": "Object ID"},
587+
"bearer_token": {"type": "string", "description": "Bearer token (optional for public containers, required for eACL containers with DENY DELETE rule)"}
587588
},
588589
"required": ["container_id", "object_id"]
589590
}
590591

591-
async def execute(self, container_id: str, object_id: str, **kwargs) -> str:
592+
async def execute(self, container_id: str, object_id: str, bearer_token: str = None, **kwargs) -> str:
592593
try:
593594
client = get_shared_neofs_client()
594-
result = client.delete_object(container_id, object_id)
595+
result = client.delete_object(container_id, object_id, bearer_token=bearer_token)
595596

596597
return f"""✅ Object deleted!
597598
Object ID: {object_id}
@@ -802,18 +803,18 @@ class DeleteContainerTool(BaseTool):
802803
"""Delete container"""
803804

804805
name: str = "delete_neofs_container"
805-
description: str = "Delete container. Requires bearer token."
806+
description: str = "Delete container. Bearer token is optional but recommended - can use private key signature if not provided."
806807
parameters: dict = {
807808
"type": "object",
808809
"properties": {
809810
"container_id": {"type": "string", "description": "Container ID"},
810-
"bearer_token": {"type": "string", "description": "Bearer token (required)"},
811+
"bearer_token": {"type": "string", "description": "Bearer token (optional but recommended for security)"},
811812
"wallet_connect": {"type": "boolean", "description": "Use wallet_connect mode"}
812813
},
813-
"required": ["container_id", "bearer_token"]
814+
"required": ["container_id"]
814815
}
815816

816-
async def execute(self, container_id: str, bearer_token: str, wallet_connect: bool = True, **kwargs) -> str:
817+
async def execute(self, container_id: str, bearer_token: str = None, wallet_connect: bool = True, **kwargs) -> str:
817818
try:
818819
client = get_shared_neofs_client()
819820
client.delete_container(container_id, bearer_token=bearer_token, wallet_connect=wallet_connect)

0 commit comments

Comments
 (0)