Skip to content

Commit 0bd1184

Browse files
authored
Merge pull request #186 from XSpoonAi/feature-1120
neofs adds support for uploading images and examples
2 parents 9a9e4d9 + 514d842 commit 0bd1184

File tree

2 files changed

+86
-13
lines changed

2 files changed

+86
-13
lines changed

examples/neofs-agent-demo.py

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"""
2222

2323
import asyncio
24+
import base64
25+
import os
2426
from typing import List
2527
from dotenv import load_dotenv
2628

@@ -160,7 +162,7 @@ class NeoFSAgentDemo:
160162
"test_scenarios": {
161163
"existing_containers": [
162164
{
163-
"id": "xxxxxx",
165+
"id": "GuZWKoBFg8GYCWdgHxZwVdBfdHTvNPJhvY1EHFQTCoXV",
164166
"name": "demo-public-storage",
165167
"type": "public",
166168
"acl": "public-read-write"
@@ -249,6 +251,8 @@ def load_test_data(self):
249251

250252
def create_agent(self, name: str, tools: List, description: str) -> ToolCallAgent:
251253
"""Create a specialized agent with specific tools"""
254+
# Capture variables for closure to avoid issues with lambda
255+
agent_tools_list = tools
252256

253257
class NeoFSSpecializedAgent(ToolCallAgent):
254258
agent_name: str = name
@@ -295,13 +299,14 @@ class NeoFSSpecializedAgent(ToolCallAgent):
295299
Explain each step clearly.
296300
"""
297301
max_steps: int = 20
298-
available_tools: ToolManager = Field(default_factory=lambda: ToolManager(tools))
302+
available_tools: ToolManager = Field(default_factory=lambda: ToolManager(agent_tools_list))
299303

300304
agent = NeoFSSpecializedAgent(
301305
llm=ChatBot(
302306
llm_provider="openrouter",
303307
model_name="openai/gpt-4o"
304-
)
308+
),
309+
available_tools=ToolManager(agent_tools_list)
305310
)
306311
return agent
307312

@@ -483,7 +488,7 @@ async def demo_public_container_workflow(self):
483488
self.print_section_header("3. PUBLIC Container Complete Workflow")
484489

485490
# Use PUBLIC container ID
486-
public_container_id = "xxxxxxxxx"
491+
public_container_id = "xxxxxxxxxxxxxxx"
487492

488493
# Use persistent agent to remember object_id
489494
agent = self.agents['object']
@@ -537,7 +542,7 @@ async def demo_eacl_container_workflow(self):
537542
self.print_section_header("4. eACL Container Complete Workflow")
538543

539544
# Use existing eACL container
540-
eacl_container_id = "xxxxxxxxx"
545+
eacl_container_id = "xxxxxxxxxxxxxxx"
541546

542547
# Use persistent agent
543548
agent = self.agents['access']
@@ -662,8 +667,8 @@ async def demo_advanced_scenarios(self):
662667
self.print_section_header("6. Advanced Object Operations")
663668

664669
# User-provided container IDs
665-
public_container_id = "xxxxxxxxx"
666-
eacl_container_id = "xxxxxxxxx"
670+
public_container_id = "xxxxxxxxxxxxxxx"
671+
eacl_container_id = "xxxxxxxxxxxxxxx"
667672

668673
# Use persistent agent
669674
agent = self.agents['object']
@@ -863,6 +868,54 @@ async def demo_advanced_scenarios(self):
863868
No bearer token needed for delete operation.""")
864869
print(f"✅ Response: {response11}")
865870

871+
async def demo_upload_to_specific_container(self, image_path: str = None, file_name: str = None, attributes: dict = None):
872+
"""Upload image to a specific public container using agent
873+
874+
Args:
875+
image_path: Path to image file. If None, uses sample image data.
876+
file_name: File name. If None, uses default or basename of image_path.
877+
attributes: File attributes dict. If None, uses default.
878+
"""
879+
self.print_section_header("7. Upload Image to Specific Public Container")
880+
881+
# Fixed container ID
882+
# Original: "754iyTDY8xUtZJZfheSYLUn7jvCkxr79RcbjMt81QykC" (does not exist - confirmed by tests)
883+
# Using one of your public containers instead for testing:
884+
TARGET_CONTAINER_ID = "xxxxxxxxxxxxxxx" # agent-demo-public-1760869880 (ACL: 1fbfbfff = public-read-write)
885+
886+
# Use object storage agent
887+
agent = self.agents['object']
888+
889+
# Prepare image data
890+
if image_path and os.path.exists(image_path):
891+
# Read image file and encode to base64
892+
with open(image_path, 'rb') as f:
893+
image_bytes = f.read()
894+
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
895+
if file_name is None:
896+
file_name = os.path.basename(image_path)
897+
ext = os.path.splitext(file_name)[1][1:].lower()
898+
content_type = f"image/{ext}" if ext in ['png', 'jpg', 'jpeg', 'gif', 'webp'] else "image/png"
899+
else:
900+
# Use sample image (1x1 pixel red PNG)
901+
image_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
902+
file_name = file_name or "demo-image.png"
903+
content_type = "image/png"
904+
905+
print(f"\n{'-'*60}")
906+
print(f" Agent: {agent.agent_name}")
907+
print(f" Scenario: Upload Image to Public Container")
908+
print(f" Container ID: {TARGET_CONTAINER_ID}")
909+
print(f" Image: {file_name}")
910+
print(f" Content Type: {content_type}")
911+
print(f"{'-'*60}")
912+
913+
# Simplified natural language prompt - let agent handle details
914+
response = await agent.run(f"""Upload an image file named "{file_name}" ({content_type}) to container {TARGET_CONTAINER_ID}.
915+
The image data is: {image_base64}.
916+
This is a public container.""")
917+
print(f"✅ Response: {response}")
918+
866919
async def run_comprehensive_demo(self):
867920
"""Run the complete agent-based demonstration"""
868921
print("🚀 NeoFS Storage - AI Agent Comprehensive Demonstration")
@@ -883,12 +936,19 @@ async def run_comprehensive_demo(self):
883936
print(f"✅ Created {len(self.agents)} specialized agents")
884937

885938
# Run comprehensive demonstrations
886-
# await self.demo_network_status()
887-
# await self.demo_container_operations()
888-
# await self.demo_public_container_workflow()
889-
# await self.demo_eacl_container_workflow()
890-
# await self.demo_access_control()
939+
await self.demo_network_status()
940+
await self.demo_container_operations()
941+
await self.demo_public_container_workflow()
942+
await self.demo_eacl_container_workflow()
943+
await self.demo_access_control()
891944
await self.demo_advanced_scenarios()
945+
# Test with sample image
946+
await self.demo_upload_to_specific_container()
947+
948+
# Test with real image file if exists
949+
test_image_path = "examples/test_image.png"
950+
if os.path.exists(test_image_path):
951+
await self.demo_upload_to_specific_container(image_path=test_image_path)
892952

893953
# Final summary
894954
self.print_section_header("Demo Completed Successfully")

spoon_ai/tools/neofs_tools.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,21 @@ async def execute(
230230
import json
231231

232232
try:
233+
import base64
233234
client = get_shared_neofs_client()
234-
content_bytes = content.encode('utf-8')
235+
236+
# Handle content encoding:
237+
# - If content is base64-encoded (images, binary files), decode it
238+
# - If content is plain text (not base64), encode as UTF-8
239+
# This ensures backward compatibility: plain text files work as-is,
240+
# while base64-encoded content (like images) is properly decoded
241+
try:
242+
# Attempt base64 decode - if it succeeds, use decoded bytes
243+
content_bytes = base64.b64decode(content, validate=True)
244+
except Exception:
245+
# If base64 decode fails, treat as plain text and encode as UTF-8
246+
# This handles regular text files without requiring base64 encoding
247+
content_bytes = content.encode('utf-8')
235248

236249
# Parse attributes from JSON string if provided, otherwise use dict
237250
if attributes_json:

0 commit comments

Comments
 (0)