mindroot 9.3.0__py3-none-any.whl → 9.5.0__py3-none-any.whl
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.
- mindroot/coreplugins/admin/__init__.py +3 -1
- mindroot/coreplugins/admin/agent_router.py +250 -7
- mindroot/coreplugins/admin/asset_manager.py +164 -0
- mindroot/coreplugins/admin/command_router.py +236 -1
- mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
- mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
- mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
- mindroot/coreplugins/admin/mcp_routes.py +216 -0
- mindroot/coreplugins/admin/mod.py +62 -0
- mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
- mindroot/coreplugins/admin/persona_handler.py +15 -6
- mindroot/coreplugins/admin/persona_router.py +158 -2
- mindroot/coreplugins/admin/plugin_manager.py +63 -0
- mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
- mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
- mindroot/coreplugins/admin/plugin_routes.py +114 -0
- mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
- mindroot/coreplugins/admin/router.py +116 -15
- mindroot/coreplugins/admin/service_models.py +1 -1
- mindroot/coreplugins/admin/settings_router.py +1 -0
- mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
- mindroot/coreplugins/admin/static/css/dark.css +1 -0
- mindroot/coreplugins/admin/static/css/default.css +4 -0
- mindroot/coreplugins/admin/static/js/about-info.js +367 -0
- mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
- mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
- mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
- mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
- mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
- mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
- mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
- mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
- mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
- mindroot/coreplugins/admin/static/js/registry-manager-old.js +385 -0
- mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
- mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
- mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
- mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
- mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
- mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
- mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
- mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
- mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
- mindroot/coreplugins/admin/static/logo.png +0 -0
- mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
- mindroot/coreplugins/agent/Assistant/agent.json +27 -11
- mindroot/coreplugins/agent/agent.py +2 -2
- mindroot/coreplugins/agent/command_parser.py +25 -10
- mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
- mindroot/coreplugins/chat/__init__.py +4 -1
- mindroot/coreplugins/chat/router.py +132 -20
- mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
- mindroot/coreplugins/chat/services.py +31 -1
- mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
- mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
- mindroot/coreplugins/chat/static/css/dark.css +24 -3
- mindroot/coreplugins/chat/static/css/default.css +24 -3
- mindroot/coreplugins/chat/static/css/main.css +1 -0
- mindroot/coreplugins/chat/static/js/action.js +137 -60
- mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
- mindroot/coreplugins/chat/static/js/chat.js +59 -16
- mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
- mindroot/coreplugins/chat/static/js/chatform.js +2 -2
- mindroot/coreplugins/chat/static/site.webmanifest +1 -1
- mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
- mindroot/coreplugins/chat/widget_manager.py +139 -0
- mindroot/coreplugins/chat/widget_routes.py +287 -0
- mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
- mindroot/coreplugins/email/__init__.py +2 -0
- mindroot/coreplugins/email/email_provider.py +2 -2
- mindroot/coreplugins/email/mod.py +100 -0
- mindroot/coreplugins/email/services.py +5 -3
- mindroot/coreplugins/email/smtp_handler.py +9 -3
- mindroot/coreplugins/email/test_email_service.py +75 -0
- mindroot/coreplugins/env_manager/mod.py +61 -25
- mindroot/coreplugins/home/router.py +37 -2
- mindroot/coreplugins/home/static/imgs/logo.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
- mindroot/coreplugins/home/templates/home.jinja2 +15 -6
- mindroot/coreplugins/index/indices/default/index.json +6 -6
- mindroot/coreplugins/jwt_auth/middleware.py +47 -2
- mindroot/coreplugins/jwt_auth/mod.py +40 -17
- mindroot/coreplugins/l8n/__init__.py +6 -0
- mindroot/coreplugins/l8n/debug_loader.py +85 -0
- mindroot/coreplugins/l8n/debug_middleware.py +74 -0
- mindroot/coreplugins/l8n/l8n_constants.py +19 -0
- mindroot/coreplugins/l8n/language_detection.py +183 -0
- mindroot/coreplugins/l8n/middleware.py +151 -0
- mindroot/coreplugins/l8n/mod.py +277 -0
- mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
- mindroot/coreplugins/l8n/test_enhanced.py +298 -0
- mindroot/coreplugins/l8n/test_l8n.py +95 -0
- mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
- mindroot/coreplugins/l8n/test_middleware.py +272 -0
- mindroot/coreplugins/l8n/utils.py +232 -0
- mindroot/coreplugins/mcp_/__init__.py +14 -0
- mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
- mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
- mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
- mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
- mindroot/coreplugins/mcp_/mod.py +367 -0
- mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
- mindroot/coreplugins/mcp_/server_installer.py +79 -0
- mindroot/coreplugins/mcp_/setup.py +26 -0
- mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
- mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
- mindroot/coreplugins/persona/mod.py +12 -7
- mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
- mindroot/coreplugins/subscriptions/__init__.py +1 -0
- mindroot/coreplugins/subscriptions/mod.py +14 -3
- mindroot/coreplugins/subscriptions/router.py +3 -0
- mindroot/coreplugins/user_service/__init__.py +1 -2
- mindroot/coreplugins/user_service/admin_init.py +1 -0
- mindroot/coreplugins/user_service/email_service.py +72 -17
- mindroot/coreplugins/user_service/mod.py +10 -2
- mindroot/coreplugins/user_service/router.py +2 -0
- mindroot/lib/auth/api_key.py +28 -0
- mindroot/lib/cli/plugins.py +94 -0
- mindroot/lib/plugins/default_plugin_manifest.json +20 -0
- mindroot/lib/plugins/installation.py +5 -5
- mindroot/lib/plugins/l8n_static_handler.py +225 -0
- mindroot/lib/plugins/loader.py +33 -3
- mindroot/lib/plugins/loader_with_l8n.py +281 -0
- mindroot/lib/plugins/manifest.py +236 -24
- mindroot/lib/providers/commands.py +3 -1
- mindroot/lib/route_decorators.py +5 -5
- mindroot/lib/templates.py +183 -11
- mindroot/lib/utils/merge_arrays.py +1 -1
- mindroot/migrate.py +39 -20
- mindroot/registry/data_access.py +1 -1
- mindroot/server.py +42 -13
- mindroot/server_missing_normal_args.py +197 -0
- mindroot/server_prev.py +173 -0
- {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
- {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +144 -112
- mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
- mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
- mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
- mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
- mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
- mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
- mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
- mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
- mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
- mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
- mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
- mindroot/coreplugins/index/default.json +0 -76
- mindroot/coreplugins/user_service/file_trigger_service.py +0 -12
- mindroot/coreplugins/user_service/hooks.py +0 -23
- /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
- {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/WHEEL +0 -0
- {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
- {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
|
2
|
+
from fastapi.responses import StreamingResponse
|
|
2
3
|
from pydantic import BaseModel
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
import json
|
|
@@ -7,6 +8,12 @@ from lib.providers.commands import command_manager
|
|
|
7
8
|
from .persona_handler import handle_persona_import, import_persona_from_index
|
|
8
9
|
import traceback
|
|
9
10
|
import hashlib
|
|
11
|
+
import zipfile
|
|
12
|
+
import io
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
from typing import Dict, Any
|
|
16
|
+
from datetime import datetime
|
|
10
17
|
|
|
11
18
|
router = APIRouter()
|
|
12
19
|
|
|
@@ -16,13 +23,88 @@ shared_dir = BASE_DIR / "shared"
|
|
|
16
23
|
local_dir.mkdir(parents=True, exist_ok=True)
|
|
17
24
|
shared_dir.mkdir(parents=True, exist_ok=True)
|
|
18
25
|
|
|
26
|
+
# Cache for agent ownership info
|
|
27
|
+
_agent_ownership_cache = None
|
|
28
|
+
_cache_timestamp = 0
|
|
29
|
+
|
|
19
30
|
class GitHubImportRequest(BaseModel):
|
|
20
31
|
repo_path: str # Format: "owner/repo"
|
|
21
32
|
scope: str
|
|
22
33
|
tag: str = None
|
|
23
34
|
|
|
35
|
+
def scan_agent_ownership() -> Dict[str, Any]:
|
|
36
|
+
"""Scan all agents and build ownership information"""
|
|
37
|
+
ownership_info = {
|
|
38
|
+
'agents': {},
|
|
39
|
+
'scanned_at': datetime.now().isoformat(),
|
|
40
|
+
'total_agents': 0,
|
|
41
|
+
'owned_agents': 0,
|
|
42
|
+
'external_agents': 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Scan both local and shared directories
|
|
46
|
+
for scope in ['local', 'shared']:
|
|
47
|
+
scope_dir = BASE_DIR / scope
|
|
48
|
+
if not scope_dir.exists():
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
for agent_dir in scope_dir.iterdir():
|
|
52
|
+
if not agent_dir.is_dir():
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
agent_json_path = agent_dir / 'agent.json'
|
|
56
|
+
if not agent_json_path.exists():
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(agent_json_path, 'r') as f:
|
|
61
|
+
agent_data = json.load(f)
|
|
62
|
+
|
|
63
|
+
agent_name = agent_data.get('name', agent_dir.name)
|
|
64
|
+
|
|
65
|
+
# Extract ownership information
|
|
66
|
+
creator = agent_data.get('creator')
|
|
67
|
+
owner = agent_data.get('owner')
|
|
68
|
+
registry_owner = agent_data.get('registry_owner')
|
|
69
|
+
created_by = agent_data.get('created_by')
|
|
70
|
+
|
|
71
|
+
# Determine if this agent has external ownership
|
|
72
|
+
has_external_owner = bool(creator or owner or registry_owner or created_by)
|
|
73
|
+
|
|
74
|
+
ownership_info['agents'][f"{scope}/{agent_name}"] = {
|
|
75
|
+
'name': agent_name,
|
|
76
|
+
'scope': scope,
|
|
77
|
+
'creator': creator,
|
|
78
|
+
'owner': owner,
|
|
79
|
+
'registry_owner': registry_owner,
|
|
80
|
+
'created_by': created_by,
|
|
81
|
+
'has_external_owner': has_external_owner,
|
|
82
|
+
'description': agent_data.get('description', ''),
|
|
83
|
+
'version': agent_data.get('version', '1.0.0')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ownership_info['total_agents'] += 1
|
|
87
|
+
if has_external_owner:
|
|
88
|
+
ownership_info['external_agents'] += 1
|
|
89
|
+
else:
|
|
90
|
+
ownership_info['owned_agents'] += 1
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Error reading agent {agent_dir.name}: {e}")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
return ownership_info
|
|
97
|
+
|
|
24
98
|
async def load_persona_data(persona_name: str) -> dict:
|
|
25
99
|
"""Load persona data from local or shared directory"""
|
|
100
|
+
# Handle registry personas: registry/owner/name
|
|
101
|
+
if persona_name.startswith('registry/'):
|
|
102
|
+
persona_path = Path(f'personas/{persona_name}/persona.json')
|
|
103
|
+
if persona_path.exists():
|
|
104
|
+
with open(persona_path, 'r') as f:
|
|
105
|
+
return json.load(f)
|
|
106
|
+
|
|
107
|
+
# Fallback to existing local/shared pattern (UNCHANGED)
|
|
26
108
|
persona_path = Path('personas/local') / persona_name / 'persona.json'
|
|
27
109
|
if not persona_path.exists():
|
|
28
110
|
persona_path = Path('personas/shared') / persona_name / 'persona.json'
|
|
@@ -91,12 +173,55 @@ def list_agents(scope: str):
|
|
|
91
173
|
if scope not in ['local', 'shared']:
|
|
92
174
|
raise HTTPException(status_code=400, detail='Invalid scope')
|
|
93
175
|
scope_dir = BASE_DIR / scope
|
|
94
|
-
agents = [
|
|
95
|
-
|
|
176
|
+
agents = []
|
|
177
|
+
for p in scope_dir.iterdir():
|
|
178
|
+
if p.is_dir():
|
|
179
|
+
agent_json_path = p / 'agent.json'
|
|
180
|
+
if agent_json_path.exists():
|
|
181
|
+
try:
|
|
182
|
+
with open(agent_json_path, 'r') as f:
|
|
183
|
+
agent_data = json.load(f)
|
|
184
|
+
agents.append(agent_data)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
# If we can't read the agent.json, just include the name
|
|
187
|
+
agents.append({'name': p.name})
|
|
188
|
+
return agents
|
|
189
|
+
|
|
190
|
+
@router.get('/agents/ownership-info')
|
|
191
|
+
def get_agent_ownership_info():
|
|
192
|
+
"""Get cached ownership information for all agents"""
|
|
193
|
+
global _agent_ownership_cache, _cache_timestamp
|
|
194
|
+
|
|
195
|
+
# Cache for 5 minutes
|
|
196
|
+
cache_duration = 300 # 5 minutes in seconds
|
|
197
|
+
current_time = time.time()
|
|
198
|
+
|
|
199
|
+
if (_agent_ownership_cache is None or
|
|
200
|
+
current_time - _cache_timestamp > cache_duration):
|
|
201
|
+
|
|
202
|
+
_agent_ownership_cache = scan_agent_ownership()
|
|
203
|
+
_cache_timestamp = current_time
|
|
204
|
+
|
|
205
|
+
return _agent_ownership_cache
|
|
206
|
+
|
|
207
|
+
@router.post('/agents/refresh-ownership-cache')
|
|
208
|
+
def refresh_agent_ownership_cache():
|
|
209
|
+
"""Force refresh of the agent ownership cache"""
|
|
210
|
+
global _agent_ownership_cache, _cache_timestamp
|
|
211
|
+
|
|
212
|
+
_agent_ownership_cache = scan_agent_ownership()
|
|
213
|
+
_cache_timestamp = time.time()
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
'success': True,
|
|
217
|
+
'message': 'Agent ownership cache refreshed',
|
|
218
|
+
'data': _agent_ownership_cache
|
|
219
|
+
}
|
|
96
220
|
|
|
97
221
|
@router.post('/agents/{scope}')
|
|
98
222
|
def create_agent(scope: str, agent: str = Form(...)):
|
|
99
223
|
try:
|
|
224
|
+
print(f"Creating agent in scope: {scope}")
|
|
100
225
|
agent_data = json.loads(agent)
|
|
101
226
|
if scope not in ['local', 'shared']:
|
|
102
227
|
raise HTTPException(status_code=400, detail='Invalid scope')
|
|
@@ -105,6 +230,7 @@ def create_agent(scope: str, agent: str = Form(...)):
|
|
|
105
230
|
if not agent_name:
|
|
106
231
|
raise HTTPException(status_code=400, detail='Agent name is required')
|
|
107
232
|
|
|
233
|
+
print(f"Agent name: {agent_name}")
|
|
108
234
|
if "indexName" in agent_data and agent_data["indexName"] is not None:
|
|
109
235
|
print("Import agent from index: " + agent_data["indexName"])
|
|
110
236
|
import_persona_from_index(agent_data["indexName"], agent_data['persona'])
|
|
@@ -112,8 +238,12 @@ def create_agent(scope: str, agent: str = Form(...)):
|
|
|
112
238
|
if 'persona' in agent_data:
|
|
113
239
|
# This will either return the persona name or handle the import
|
|
114
240
|
# and return the name
|
|
115
|
-
|
|
116
|
-
|
|
241
|
+
# Extract owner information for registry personas
|
|
242
|
+
owner = agent_data.get('registry_owner') or agent_data.get('owner') or agent_data.get('creator')
|
|
243
|
+
persona_scope = 'registry' if owner else scope
|
|
244
|
+
|
|
245
|
+
persona_name = handle_persona_import(agent_data['persona'], persona_scope, owner)
|
|
246
|
+
|
|
117
247
|
agent_data['persona'] = persona_name
|
|
118
248
|
|
|
119
249
|
# Ensure recommended_plugins is present and is a list (also handle legacy required_plugins)
|
|
@@ -136,17 +266,26 @@ def create_agent(scope: str, agent: str = Form(...)):
|
|
|
136
266
|
|
|
137
267
|
agent_path = BASE_DIR / scope / agent_name / 'agent.json'
|
|
138
268
|
if agent_path.exists():
|
|
139
|
-
|
|
269
|
+
# Check if overwrite parameter is provided
|
|
270
|
+
overwrite = agent_data.get('overwrite', False)
|
|
271
|
+
if not overwrite:
|
|
272
|
+
raise HTTPException(status_code=400, detail='Agent already exists')
|
|
273
|
+
else:
|
|
274
|
+
# If overwrite is True, we'll proceed to overwrite the existing agent
|
|
275
|
+
print(f"Overwriting existing agent: {agent_name}")
|
|
140
276
|
|
|
277
|
+
print(f"Creating agent directory: {agent_path.parent}")
|
|
141
278
|
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
print(f"Writing agent file: {agent_path}")
|
|
142
280
|
with open(agent_path, 'w') as f:
|
|
143
281
|
json.dump(agent_data, f, indent=2)
|
|
144
282
|
|
|
283
|
+
print(f"Successfully created agent: {agent_name}")
|
|
145
284
|
return {'status': 'success'}
|
|
146
285
|
except Exception as e:
|
|
147
286
|
trace = traceback.format_exc()
|
|
148
|
-
|
|
149
|
-
|
|
287
|
+
print(f"Error creating agent: {str(e)}\n{trace}")
|
|
288
|
+
raise HTTPException(status_code=500, detail=f'Internal server error: {str(e)}\nTrace: {trace}')
|
|
150
289
|
@router.put('/agents/{scope}/{name}')
|
|
151
290
|
def update_agent(scope: str, name: str, agent: str = Form(...)):
|
|
152
291
|
try:
|
|
@@ -223,3 +362,107 @@ def import_github_agent_endpoint(request: GitHubImportRequest):
|
|
|
223
362
|
raise HTTPException(status_code=400, detail=str(e))
|
|
224
363
|
except Exception as e:
|
|
225
364
|
raise HTTPException(status_code=500, detail=f"Error during GitHub import: {str(e)}")
|
|
365
|
+
|
|
366
|
+
@router.get('/agents/{scope}/{name}/export')
|
|
367
|
+
def export_agent_zip(scope: str, name: str):
|
|
368
|
+
"""Export an agent as a zip file"""
|
|
369
|
+
if scope not in ['local', 'shared']:
|
|
370
|
+
raise HTTPException(status_code=400, detail='Invalid scope')
|
|
371
|
+
|
|
372
|
+
agent_dir = BASE_DIR / scope / name
|
|
373
|
+
if not agent_dir.exists():
|
|
374
|
+
raise HTTPException(status_code=404, detail='Agent not found')
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
# Create zip file in memory
|
|
378
|
+
zip_buffer = io.BytesIO()
|
|
379
|
+
|
|
380
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
381
|
+
# Add all files from the agent directory
|
|
382
|
+
for file_path in agent_dir.rglob('*'):
|
|
383
|
+
if file_path.is_file():
|
|
384
|
+
# Use relative path within the zip
|
|
385
|
+
arcname = file_path.relative_to(agent_dir)
|
|
386
|
+
zip_file.write(file_path, arcname)
|
|
387
|
+
|
|
388
|
+
zip_buffer.seek(0)
|
|
389
|
+
|
|
390
|
+
# Return as streaming response
|
|
391
|
+
return StreamingResponse(
|
|
392
|
+
io.BytesIO(zip_buffer.read()),
|
|
393
|
+
media_type='application/zip',
|
|
394
|
+
headers={'Content-Disposition': f'attachment; filename="{name}_agent.zip"'}
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
raise HTTPException(status_code=500, detail=f"Error creating zip: {str(e)}")
|
|
399
|
+
|
|
400
|
+
@router.post('/agents/{scope}/import-zip')
|
|
401
|
+
async def import_agent_zip(scope: str, file: UploadFile = File(...)):
|
|
402
|
+
"""Import an agent from a zip file"""
|
|
403
|
+
if scope not in ['local', 'shared']:
|
|
404
|
+
raise HTTPException(status_code=400, detail='Invalid scope')
|
|
405
|
+
|
|
406
|
+
if not file.filename.endswith('.zip'):
|
|
407
|
+
raise HTTPException(status_code=400, detail='File must be a zip file')
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
# Read the uploaded file
|
|
411
|
+
content = await file.read()
|
|
412
|
+
|
|
413
|
+
# Create a temporary directory for extraction
|
|
414
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
415
|
+
temp_path = Path(temp_dir)
|
|
416
|
+
|
|
417
|
+
# Extract zip file
|
|
418
|
+
with zipfile.ZipFile(io.BytesIO(content), 'r') as zip_file:
|
|
419
|
+
zip_file.extractall(temp_path)
|
|
420
|
+
|
|
421
|
+
# Find agent.json file
|
|
422
|
+
agent_json_files = list(temp_path.rglob('agent.json'))
|
|
423
|
+
if not agent_json_files:
|
|
424
|
+
raise HTTPException(status_code=400, detail='No agent.json found in zip file')
|
|
425
|
+
|
|
426
|
+
agent_json_path = agent_json_files[0]
|
|
427
|
+
|
|
428
|
+
# Load agent data
|
|
429
|
+
with open(agent_json_path, 'r') as f:
|
|
430
|
+
agent_data = json.load(f)
|
|
431
|
+
|
|
432
|
+
agent_name = agent_data.get('name')
|
|
433
|
+
if not agent_name:
|
|
434
|
+
raise HTTPException(status_code=400, detail='Agent name not found in agent.json')
|
|
435
|
+
|
|
436
|
+
# Check if agent already exists
|
|
437
|
+
target_dir = BASE_DIR / scope / agent_name
|
|
438
|
+
if target_dir.exists():
|
|
439
|
+
# For zip imports, we could add overwrite support in the future
|
|
440
|
+
# For now, we'll keep the existing behavior but make the error message consistent
|
|
441
|
+
raise HTTPException(
|
|
442
|
+
status_code=400,
|
|
443
|
+
detail=f'Agent {agent_name} already exists. Use the registry manager for updates.'
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Copy extracted files to target directory
|
|
447
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
448
|
+
|
|
449
|
+
# Get the directory containing agent.json
|
|
450
|
+
source_dir = agent_json_path.parent
|
|
451
|
+
|
|
452
|
+
# Copy all files from source to target
|
|
453
|
+
import shutil
|
|
454
|
+
for item in source_dir.rglob('*'):
|
|
455
|
+
if item.is_file():
|
|
456
|
+
relative_path = item.relative_to(source_dir)
|
|
457
|
+
target_file = target_dir / relative_path
|
|
458
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
shutil.copy2(item, target_file)
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
'success': True,
|
|
463
|
+
'message': f'Agent {agent_name} imported successfully',
|
|
464
|
+
'agent_name': agent_name
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
raise HTTPException(status_code=500, detail=f"Error importing zip: {str(e)}")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, Optional, Tuple
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class AssetManager:
|
|
9
|
+
"""Manages deduplicated asset storage for personas"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, base_dir: str = "registry_assets"):
|
|
12
|
+
self.base_dir = Path(base_dir)
|
|
13
|
+
self.assets_dir = self.base_dir / "assets"
|
|
14
|
+
self.metadata_dir = self.base_dir / "metadata"
|
|
15
|
+
|
|
16
|
+
# Create directories
|
|
17
|
+
self.assets_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
self.metadata_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
def calculate_file_hash(self, file_path: Path) -> str:
|
|
21
|
+
"""Calculate SHA256 hash of a file"""
|
|
22
|
+
sha256_hash = hashlib.sha256()
|
|
23
|
+
with open(file_path, "rb") as f:
|
|
24
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
25
|
+
sha256_hash.update(chunk)
|
|
26
|
+
return sha256_hash.hexdigest()
|
|
27
|
+
|
|
28
|
+
def calculate_content_hash(self, content: bytes) -> str:
|
|
29
|
+
"""Calculate SHA256 hash of content bytes"""
|
|
30
|
+
return hashlib.sha256(content).hexdigest()
|
|
31
|
+
|
|
32
|
+
def store_asset(self, source_path: Path, asset_type: str = "image") -> Tuple[str, bool]:
|
|
33
|
+
"""Store an asset and return (hash, was_new)"""
|
|
34
|
+
if not source_path.exists():
|
|
35
|
+
raise FileNotFoundError(f"Source file not found: {source_path}")
|
|
36
|
+
|
|
37
|
+
# Calculate hash
|
|
38
|
+
file_hash = self.calculate_file_hash(source_path)
|
|
39
|
+
asset_path = self.assets_dir / file_hash
|
|
40
|
+
|
|
41
|
+
# Check if asset already exists
|
|
42
|
+
if asset_path.exists():
|
|
43
|
+
# Update reference count
|
|
44
|
+
self._increment_reference_count(file_hash)
|
|
45
|
+
return file_hash, False
|
|
46
|
+
|
|
47
|
+
# Copy file to assets directory
|
|
48
|
+
shutil.copy2(source_path, asset_path)
|
|
49
|
+
|
|
50
|
+
# Create metadata
|
|
51
|
+
metadata = {
|
|
52
|
+
"hash": file_hash,
|
|
53
|
+
"type": asset_type,
|
|
54
|
+
"size": source_path.stat().st_size,
|
|
55
|
+
"original_name": source_path.name,
|
|
56
|
+
"reference_count": 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
metadata_path = self.metadata_dir / f"{file_hash}.json"
|
|
60
|
+
with open(metadata_path, 'w') as f:
|
|
61
|
+
json.dump(metadata, f, indent=2)
|
|
62
|
+
|
|
63
|
+
return file_hash, True
|
|
64
|
+
|
|
65
|
+
def store_content(self, content: bytes, filename: str, asset_type: str = "image") -> Tuple[str, bool]:
|
|
66
|
+
"""Store content bytes and return (hash, was_new)"""
|
|
67
|
+
file_hash = self.calculate_content_hash(content)
|
|
68
|
+
asset_path = self.assets_dir / file_hash
|
|
69
|
+
|
|
70
|
+
# Check if asset already exists
|
|
71
|
+
if asset_path.exists():
|
|
72
|
+
self._increment_reference_count(file_hash)
|
|
73
|
+
return file_hash, False
|
|
74
|
+
|
|
75
|
+
# Write content to assets directory
|
|
76
|
+
with open(asset_path, 'wb') as f:
|
|
77
|
+
f.write(content)
|
|
78
|
+
|
|
79
|
+
# Create metadata
|
|
80
|
+
metadata = {
|
|
81
|
+
"hash": file_hash,
|
|
82
|
+
"type": asset_type,
|
|
83
|
+
"size": len(content),
|
|
84
|
+
"original_name": filename,
|
|
85
|
+
"reference_count": 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
metadata_path = self.metadata_dir / f"{file_hash}.json"
|
|
89
|
+
with open(metadata_path, 'w') as f:
|
|
90
|
+
json.dump(metadata, f, indent=2)
|
|
91
|
+
|
|
92
|
+
return file_hash, True
|
|
93
|
+
|
|
94
|
+
def get_asset_path(self, file_hash: str) -> Optional[Path]:
|
|
95
|
+
"""Get the path to an asset by hash"""
|
|
96
|
+
asset_path = self.assets_dir / file_hash
|
|
97
|
+
return asset_path if asset_path.exists() else None
|
|
98
|
+
|
|
99
|
+
def get_asset_metadata(self, file_hash: str) -> Optional[Dict]:
|
|
100
|
+
"""Get metadata for an asset"""
|
|
101
|
+
metadata_path = self.metadata_dir / f"{file_hash}.json"
|
|
102
|
+
if not metadata_path.exists():
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
with open(metadata_path, 'r') as f:
|
|
106
|
+
return json.load(f)
|
|
107
|
+
|
|
108
|
+
def _increment_reference_count(self, file_hash: str):
|
|
109
|
+
"""Increment reference count for an asset"""
|
|
110
|
+
metadata = self.get_asset_metadata(file_hash)
|
|
111
|
+
if metadata:
|
|
112
|
+
metadata['reference_count'] += 1
|
|
113
|
+
metadata_path = self.metadata_dir / f"{file_hash}.json"
|
|
114
|
+
with open(metadata_path, 'w') as f:
|
|
115
|
+
json.dump(metadata, f, indent=2)
|
|
116
|
+
|
|
117
|
+
def decrement_reference_count(self, file_hash: str) -> bool:
|
|
118
|
+
"""Decrement reference count and delete if zero. Returns True if deleted."""
|
|
119
|
+
metadata = self.get_asset_metadata(file_hash)
|
|
120
|
+
if not metadata:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
metadata['reference_count'] -= 1
|
|
124
|
+
|
|
125
|
+
if metadata['reference_count'] <= 0:
|
|
126
|
+
# Delete asset and metadata
|
|
127
|
+
asset_path = self.assets_dir / file_hash
|
|
128
|
+
metadata_path = self.metadata_dir / f"{file_hash}.json"
|
|
129
|
+
|
|
130
|
+
if asset_path.exists():
|
|
131
|
+
asset_path.unlink()
|
|
132
|
+
if metadata_path.exists():
|
|
133
|
+
metadata_path.unlink()
|
|
134
|
+
|
|
135
|
+
return True
|
|
136
|
+
else:
|
|
137
|
+
# Update metadata
|
|
138
|
+
metadata_path = self.metadata_dir / f"{file_hash}.json"
|
|
139
|
+
with open(metadata_path, 'w') as f:
|
|
140
|
+
json.dump(metadata, f, indent=2)
|
|
141
|
+
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def get_stats(self) -> Dict:
|
|
145
|
+
"""Get storage statistics"""
|
|
146
|
+
total_assets = len(list(self.assets_dir.glob('*')))
|
|
147
|
+
total_size = sum(f.stat().st_size for f in self.assets_dir.glob('*'))
|
|
148
|
+
|
|
149
|
+
# Calculate total references
|
|
150
|
+
total_references = 0
|
|
151
|
+
for metadata_file in self.metadata_dir.glob('*.json'):
|
|
152
|
+
with open(metadata_file, 'r') as f:
|
|
153
|
+
metadata = json.load(f)
|
|
154
|
+
total_references += metadata.get('reference_count', 0)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
'total_assets': total_assets,
|
|
158
|
+
'total_size_bytes': total_size,
|
|
159
|
+
'total_references': total_references,
|
|
160
|
+
'deduplication_ratio': total_references / max(total_assets, 1)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Global asset manager instance
|
|
164
|
+
asset_manager = AssetManager()
|