django-agent-studio 0.1.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.
- django_agent_studio/__init__.py +12 -0
- django_agent_studio/api/__init__.py +4 -0
- django_agent_studio/api/permissions.py +160 -0
- django_agent_studio/api/serializers.py +606 -0
- django_agent_studio/api/urls.py +245 -0
- django_agent_studio/api/views.py +1548 -0
- django_agent_studio/apps.py +24 -0
- django_agent_studio/management/__init__.py +2 -0
- django_agent_studio/management/commands/__init__.py +2 -0
- django_agent_studio/static/agent-frontend/chat-widget-markdown.js +110 -0
- django_agent_studio/static/agent-frontend/chat-widget.css +1401 -0
- django_agent_studio/static/agent-frontend/chat-widget.js +319 -0
- django_agent_studio/templates/django_agent_studio/agent_list.html +101 -0
- django_agent_studio/templates/django_agent_studio/base.html +137 -0
- django_agent_studio/templates/django_agent_studio/builder.html +1443 -0
- django_agent_studio/templates/django_agent_studio/home.html +97 -0
- django_agent_studio/templates/django_agent_studio/test.html +126 -0
- django_agent_studio/urls.py +25 -0
- django_agent_studio/views.py +100 -0
- django_agent_studio-0.1.0.dist-info/METADATA +416 -0
- django_agent_studio-0.1.0.dist-info/RECORD +23 -0
- django_agent_studio-0.1.0.dist-info/WHEEL +5 -0
- django_agent_studio-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1548 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API views for django_agent_studio.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.http import Http404
|
|
7
|
+
from django.shortcuts import get_object_or_404
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
from rest_framework import generics, status
|
|
10
|
+
from rest_framework.permissions import IsAuthenticated
|
|
11
|
+
from rest_framework.response import Response
|
|
12
|
+
from rest_framework.views import APIView
|
|
13
|
+
|
|
14
|
+
from django_agent_runtime.models import (
|
|
15
|
+
AgentDefinition,
|
|
16
|
+
AgentVersion,
|
|
17
|
+
AgentTool,
|
|
18
|
+
AgentKnowledge,
|
|
19
|
+
DiscoveredFunction,
|
|
20
|
+
DynamicTool,
|
|
21
|
+
DynamicToolExecution,
|
|
22
|
+
# Multi-agent system models
|
|
23
|
+
AgentSystem,
|
|
24
|
+
AgentSystemMember,
|
|
25
|
+
AgentSystemVersion,
|
|
26
|
+
AgentSystemSnapshot,
|
|
27
|
+
)
|
|
28
|
+
from django_agent_studio.api.serializers import (
|
|
29
|
+
AgentDefinitionListSerializer,
|
|
30
|
+
AgentDefinitionDetailSerializer,
|
|
31
|
+
AgentVersionSerializer,
|
|
32
|
+
AgentToolSerializer,
|
|
33
|
+
AgentKnowledgeSerializer,
|
|
34
|
+
DiscoveredFunctionSerializer,
|
|
35
|
+
DynamicToolSerializer,
|
|
36
|
+
DynamicToolExecutionSerializer,
|
|
37
|
+
ProjectScanRequestSerializer,
|
|
38
|
+
GenerateToolsRequestSerializer,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Helper functions for consistent access control
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
def get_agent_for_user(user, agent_id):
|
|
47
|
+
"""
|
|
48
|
+
Get an agent if the user has access.
|
|
49
|
+
|
|
50
|
+
- Superusers can access any agent
|
|
51
|
+
- Regular users can only access their own agents
|
|
52
|
+
"""
|
|
53
|
+
if user.is_superuser:
|
|
54
|
+
return get_object_or_404(AgentDefinition, id=agent_id)
|
|
55
|
+
return get_object_or_404(AgentDefinition, id=agent_id, owner=user)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_agent_queryset_for_user(user):
|
|
59
|
+
"""
|
|
60
|
+
Get queryset of agents accessible to the user.
|
|
61
|
+
|
|
62
|
+
- Superusers can see all agents
|
|
63
|
+
- Regular users see only their own agents
|
|
64
|
+
"""
|
|
65
|
+
if user.is_superuser:
|
|
66
|
+
return AgentDefinition.objects.all()
|
|
67
|
+
return AgentDefinition.objects.filter(owner=user)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_system_for_user(user, system_id):
|
|
71
|
+
"""
|
|
72
|
+
Get a system if the user has access.
|
|
73
|
+
|
|
74
|
+
- Superusers can access any system
|
|
75
|
+
- Regular users can only access their own systems
|
|
76
|
+
"""
|
|
77
|
+
if user.is_superuser:
|
|
78
|
+
return get_object_or_404(AgentSystem, id=system_id)
|
|
79
|
+
return get_object_or_404(AgentSystem, id=system_id, owner=user)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_system_queryset_for_user(user):
|
|
83
|
+
"""
|
|
84
|
+
Get queryset of systems accessible to the user.
|
|
85
|
+
|
|
86
|
+
- Superusers can see all systems
|
|
87
|
+
- Regular users see only their own systems
|
|
88
|
+
"""
|
|
89
|
+
if user.is_superuser:
|
|
90
|
+
return AgentSystem.objects.all()
|
|
91
|
+
return AgentSystem.objects.filter(owner=user)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AgentDefinitionListCreateView(generics.ListCreateAPIView):
|
|
95
|
+
"""List and create agent definitions."""
|
|
96
|
+
|
|
97
|
+
permission_classes = [IsAuthenticated]
|
|
98
|
+
serializer_class = AgentDefinitionListSerializer
|
|
99
|
+
|
|
100
|
+
def get_queryset(self):
|
|
101
|
+
return get_agent_queryset_for_user(self.request.user)
|
|
102
|
+
|
|
103
|
+
def perform_create(self, serializer):
|
|
104
|
+
agent = serializer.save(owner=self.request.user)
|
|
105
|
+
# Create initial draft version
|
|
106
|
+
AgentVersion.objects.create(
|
|
107
|
+
agent=agent,
|
|
108
|
+
version="draft",
|
|
109
|
+
is_draft=True,
|
|
110
|
+
is_active=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AgentDefinitionDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
115
|
+
"""Retrieve, update, or delete an agent definition."""
|
|
116
|
+
|
|
117
|
+
permission_classes = [IsAuthenticated]
|
|
118
|
+
serializer_class = AgentDefinitionDetailSerializer
|
|
119
|
+
|
|
120
|
+
def get_queryset(self):
|
|
121
|
+
return get_agent_queryset_for_user(self.request.user)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AgentVersionListCreateView(generics.ListCreateAPIView):
|
|
125
|
+
"""List and create versions for an agent."""
|
|
126
|
+
|
|
127
|
+
permission_classes = [IsAuthenticated]
|
|
128
|
+
serializer_class = AgentVersionSerializer
|
|
129
|
+
|
|
130
|
+
def get_queryset(self):
|
|
131
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
132
|
+
return agent.versions.all()
|
|
133
|
+
|
|
134
|
+
def perform_create(self, serializer):
|
|
135
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
136
|
+
serializer.save(agent=agent)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AgentVersionDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
140
|
+
"""Retrieve, update, or delete an agent version."""
|
|
141
|
+
|
|
142
|
+
permission_classes = [IsAuthenticated]
|
|
143
|
+
serializer_class = AgentVersionSerializer
|
|
144
|
+
|
|
145
|
+
def get_queryset(self):
|
|
146
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
147
|
+
return agent.versions.all()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AgentVersionActivateView(APIView):
|
|
151
|
+
"""Activate a specific version of an agent."""
|
|
152
|
+
|
|
153
|
+
permission_classes = [IsAuthenticated]
|
|
154
|
+
|
|
155
|
+
def post(self, request, agent_id, pk):
|
|
156
|
+
agent = get_agent_for_user(request.user, agent_id)
|
|
157
|
+
version = get_object_or_404(agent.versions, id=pk)
|
|
158
|
+
|
|
159
|
+
# Deactivate all other versions
|
|
160
|
+
agent.versions.update(is_active=False)
|
|
161
|
+
|
|
162
|
+
# Activate this version
|
|
163
|
+
version.is_active = True
|
|
164
|
+
version.is_draft = False
|
|
165
|
+
version.published_at = timezone.now()
|
|
166
|
+
version.save()
|
|
167
|
+
|
|
168
|
+
return Response(AgentVersionSerializer(version).data)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class AgentToolListCreateView(generics.ListCreateAPIView):
|
|
172
|
+
"""List and create tools for an agent."""
|
|
173
|
+
|
|
174
|
+
permission_classes = [IsAuthenticated]
|
|
175
|
+
serializer_class = AgentToolSerializer
|
|
176
|
+
|
|
177
|
+
def get_queryset(self):
|
|
178
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
179
|
+
return agent.tools.all()
|
|
180
|
+
|
|
181
|
+
def perform_create(self, serializer):
|
|
182
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
183
|
+
serializer.save(agent=agent)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class AgentToolDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
187
|
+
"""Retrieve, update, or delete an agent tool."""
|
|
188
|
+
|
|
189
|
+
permission_classes = [IsAuthenticated]
|
|
190
|
+
serializer_class = AgentToolSerializer
|
|
191
|
+
|
|
192
|
+
def get_queryset(self):
|
|
193
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
194
|
+
return agent.tools.all()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AgentKnowledgeListCreateView(generics.ListCreateAPIView):
|
|
198
|
+
"""List and create knowledge sources for an agent."""
|
|
199
|
+
|
|
200
|
+
permission_classes = [IsAuthenticated]
|
|
201
|
+
serializer_class = AgentKnowledgeSerializer
|
|
202
|
+
|
|
203
|
+
def get_queryset(self):
|
|
204
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
205
|
+
return agent.knowledge_sources.all()
|
|
206
|
+
|
|
207
|
+
def perform_create(self, serializer):
|
|
208
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
209
|
+
serializer.save(agent=agent)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class AgentKnowledgeDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
213
|
+
"""Retrieve, update, or delete an agent knowledge source."""
|
|
214
|
+
|
|
215
|
+
permission_classes = [IsAuthenticated]
|
|
216
|
+
serializer_class = AgentKnowledgeSerializer
|
|
217
|
+
|
|
218
|
+
def get_queryset(self):
|
|
219
|
+
agent = get_agent_for_user(self.request.user, self.kwargs["agent_id"])
|
|
220
|
+
return agent.knowledge_sources.all()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TemplateAgentListView(generics.ListAPIView):
|
|
224
|
+
"""List public template agents."""
|
|
225
|
+
|
|
226
|
+
permission_classes = [IsAuthenticated]
|
|
227
|
+
serializer_class = AgentDefinitionListSerializer
|
|
228
|
+
|
|
229
|
+
def get_queryset(self):
|
|
230
|
+
return AgentDefinition.objects.filter(
|
|
231
|
+
is_template=True,
|
|
232
|
+
is_public=True,
|
|
233
|
+
is_active=True,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class AgentForkView(APIView):
|
|
238
|
+
"""Fork an agent (create a copy with the current user as owner)."""
|
|
239
|
+
|
|
240
|
+
permission_classes = [IsAuthenticated]
|
|
241
|
+
|
|
242
|
+
def post(self, request, pk):
|
|
243
|
+
# Get the source agent (must be public template, owned by user, or superuser)
|
|
244
|
+
if request.user.is_superuser:
|
|
245
|
+
source = get_object_or_404(AgentDefinition, id=pk)
|
|
246
|
+
else:
|
|
247
|
+
source = get_object_or_404(
|
|
248
|
+
AgentDefinition.objects.filter(
|
|
249
|
+
models.Q(owner=request.user) |
|
|
250
|
+
models.Q(is_template=True, is_public=True)
|
|
251
|
+
),
|
|
252
|
+
id=pk,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Create a copy
|
|
256
|
+
new_agent = AgentDefinition.objects.create(
|
|
257
|
+
name=f"{source.name} (Copy)",
|
|
258
|
+
slug=f"{source.slug}-copy-{timezone.now().strftime('%Y%m%d%H%M%S')}",
|
|
259
|
+
description=source.description,
|
|
260
|
+
icon=source.icon,
|
|
261
|
+
parent=source if source.is_template else source.parent,
|
|
262
|
+
owner=request.user,
|
|
263
|
+
is_public=False,
|
|
264
|
+
is_template=False,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Copy the active version
|
|
268
|
+
source_version = source.versions.filter(is_active=True).first()
|
|
269
|
+
if source_version:
|
|
270
|
+
AgentVersion.objects.create(
|
|
271
|
+
agent=new_agent,
|
|
272
|
+
version="draft",
|
|
273
|
+
system_prompt=source_version.system_prompt,
|
|
274
|
+
model=source_version.model,
|
|
275
|
+
model_settings=source_version.model_settings,
|
|
276
|
+
extra_config=source_version.extra_config,
|
|
277
|
+
is_draft=True,
|
|
278
|
+
is_active=True,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Copy tools
|
|
282
|
+
for tool in source.tools.filter(is_active=True):
|
|
283
|
+
AgentTool.objects.create(
|
|
284
|
+
agent=new_agent,
|
|
285
|
+
name=tool.name,
|
|
286
|
+
tool_type=tool.tool_type,
|
|
287
|
+
description=tool.description,
|
|
288
|
+
parameters_schema=tool.parameters_schema,
|
|
289
|
+
builtin_ref=tool.builtin_ref,
|
|
290
|
+
subagent=tool.subagent,
|
|
291
|
+
config=tool.config,
|
|
292
|
+
order=tool.order,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Copy knowledge (except files - those need to be re-uploaded)
|
|
296
|
+
for knowledge in source.knowledge_sources.filter(is_active=True):
|
|
297
|
+
AgentKnowledge.objects.create(
|
|
298
|
+
agent=new_agent,
|
|
299
|
+
name=knowledge.name,
|
|
300
|
+
knowledge_type=knowledge.knowledge_type,
|
|
301
|
+
content=knowledge.content,
|
|
302
|
+
url=knowledge.url,
|
|
303
|
+
dynamic_config=knowledge.dynamic_config,
|
|
304
|
+
inclusion_mode=knowledge.inclusion_mode,
|
|
305
|
+
order=knowledge.order,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return Response(
|
|
309
|
+
AgentDefinitionDetailSerializer(new_agent).data,
|
|
310
|
+
status=status.HTTP_201_CREATED,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# =============================================================================
|
|
315
|
+
# Dynamic Tool Discovery Views
|
|
316
|
+
# =============================================================================
|
|
317
|
+
|
|
318
|
+
from django_agent_studio.api.permissions import (
|
|
319
|
+
CanViewDynamicTools,
|
|
320
|
+
CanScanProject,
|
|
321
|
+
CanRequestTool,
|
|
322
|
+
CanCreateTool,
|
|
323
|
+
CanApproveTool,
|
|
324
|
+
CanManagePermissions,
|
|
325
|
+
DynamicToolObjectPermission,
|
|
326
|
+
IsOwnerOrHasDynamicToolAccess,
|
|
327
|
+
)
|
|
328
|
+
from django_agent_studio.services.permissions import get_permission_service
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class ProjectScanView(APIView):
|
|
332
|
+
"""
|
|
333
|
+
Scan the Django project to discover functions.
|
|
334
|
+
|
|
335
|
+
POST /api/agents/{id}/scan-project/
|
|
336
|
+
|
|
337
|
+
Requires: Scanner access level or agent ownership
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
permission_classes = [IsAuthenticated, CanScanProject | IsOwnerOrHasDynamicToolAccess]
|
|
341
|
+
|
|
342
|
+
def post(self, request, agent_id):
|
|
343
|
+
agent = get_object_or_404(AgentDefinition, id=agent_id)
|
|
344
|
+
|
|
345
|
+
serializer = ProjectScanRequestSerializer(data=request.data)
|
|
346
|
+
serializer.is_valid(raise_exception=True)
|
|
347
|
+
|
|
348
|
+
from django_agent_runtime.dynamic_tools import ProjectScanner
|
|
349
|
+
|
|
350
|
+
# Create scanner with options
|
|
351
|
+
scanner = ProjectScanner(
|
|
352
|
+
include_private=serializer.validated_data.get('include_private', False),
|
|
353
|
+
include_tests=serializer.validated_data.get('include_tests', False),
|
|
354
|
+
app_filter=serializer.validated_data.get('app_filter'),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Scan project or specific directory
|
|
358
|
+
directory = serializer.validated_data.get('directory')
|
|
359
|
+
if directory:
|
|
360
|
+
functions = scanner.scan_directory(directory)
|
|
361
|
+
else:
|
|
362
|
+
functions = scanner.scan()
|
|
363
|
+
|
|
364
|
+
# Store discovered functions
|
|
365
|
+
scan_session = scanner.scan_session
|
|
366
|
+
created_functions = []
|
|
367
|
+
|
|
368
|
+
for func_info in functions:
|
|
369
|
+
discovered, created = DiscoveredFunction.objects.update_or_create(
|
|
370
|
+
function_path=func_info.function_path,
|
|
371
|
+
scan_session=scan_session,
|
|
372
|
+
defaults={
|
|
373
|
+
'name': func_info.name,
|
|
374
|
+
'module_path': func_info.module_path,
|
|
375
|
+
'function_type': func_info.function_type,
|
|
376
|
+
'class_name': func_info.class_name,
|
|
377
|
+
'file_path': func_info.file_path,
|
|
378
|
+
'line_number': func_info.line_number,
|
|
379
|
+
'signature': func_info.signature,
|
|
380
|
+
'docstring': func_info.docstring,
|
|
381
|
+
'parameters': func_info.parameters,
|
|
382
|
+
'return_type': func_info.return_type,
|
|
383
|
+
'is_async': func_info.is_async,
|
|
384
|
+
'has_side_effects': func_info.has_side_effects,
|
|
385
|
+
'is_private': func_info.is_private,
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
created_functions.append(discovered)
|
|
389
|
+
|
|
390
|
+
return Response({
|
|
391
|
+
'scan_session': scan_session,
|
|
392
|
+
'functions_discovered': len(created_functions),
|
|
393
|
+
'functions': DiscoveredFunctionSerializer(created_functions, many=True).data,
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class DiscoveredFunctionListView(generics.ListAPIView):
|
|
398
|
+
"""
|
|
399
|
+
List discovered functions from project scans.
|
|
400
|
+
|
|
401
|
+
GET /api/discovered-functions/
|
|
402
|
+
|
|
403
|
+
Requires: Viewer access level
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
permission_classes = [IsAuthenticated, CanViewDynamicTools]
|
|
407
|
+
serializer_class = DiscoveredFunctionSerializer
|
|
408
|
+
|
|
409
|
+
def get_queryset(self):
|
|
410
|
+
queryset = DiscoveredFunction.objects.all()
|
|
411
|
+
|
|
412
|
+
# Filter by scan session
|
|
413
|
+
scan_session = self.request.query_params.get('scan_session')
|
|
414
|
+
if scan_session:
|
|
415
|
+
queryset = queryset.filter(scan_session=scan_session)
|
|
416
|
+
|
|
417
|
+
# Filter by function type
|
|
418
|
+
function_type = self.request.query_params.get('function_type')
|
|
419
|
+
if function_type:
|
|
420
|
+
queryset = queryset.filter(function_type=function_type)
|
|
421
|
+
|
|
422
|
+
# Filter by selected status
|
|
423
|
+
is_selected = self.request.query_params.get('is_selected')
|
|
424
|
+
if is_selected is not None:
|
|
425
|
+
queryset = queryset.filter(is_selected=is_selected.lower() == 'true')
|
|
426
|
+
|
|
427
|
+
return queryset
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class GenerateToolsView(APIView):
|
|
431
|
+
"""
|
|
432
|
+
Generate dynamic tools from discovered functions.
|
|
433
|
+
|
|
434
|
+
POST /api/agents/{id}/generate-tools/
|
|
435
|
+
|
|
436
|
+
Requires:
|
|
437
|
+
- Creator access: creates tools directly
|
|
438
|
+
- Requester access: creates approval requests instead
|
|
439
|
+
- Owner: creates tools directly
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
permission_classes = [IsAuthenticated, CanRequestTool | IsOwnerOrHasDynamicToolAccess]
|
|
443
|
+
|
|
444
|
+
def post(self, request, agent_id):
|
|
445
|
+
agent = get_object_or_404(AgentDefinition, id=agent_id)
|
|
446
|
+
|
|
447
|
+
# Check if user can create directly or needs approval
|
|
448
|
+
perm_service = get_permission_service()
|
|
449
|
+
can_create_directly = (
|
|
450
|
+
agent.owner == request.user or
|
|
451
|
+
perm_service.can_create_tool(request.user, agent)
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
serializer = GenerateToolsRequestSerializer(data=request.data)
|
|
455
|
+
serializer.is_valid(raise_exception=True)
|
|
456
|
+
|
|
457
|
+
from django_agent_runtime.dynamic_tools import ToolGenerator
|
|
458
|
+
|
|
459
|
+
# Get discovered functions
|
|
460
|
+
function_ids = serializer.validated_data['function_ids']
|
|
461
|
+
functions = DiscoveredFunction.objects.filter(id__in=function_ids)
|
|
462
|
+
|
|
463
|
+
if not functions.exists():
|
|
464
|
+
return Response(
|
|
465
|
+
{'error': 'No functions found with the provided IDs'},
|
|
466
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Create generator
|
|
470
|
+
generator = ToolGenerator(
|
|
471
|
+
default_requires_confirmation=serializer.validated_data.get(
|
|
472
|
+
'requires_confirmation', True
|
|
473
|
+
),
|
|
474
|
+
name_prefix=serializer.validated_data.get('name_prefix', ''),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if can_create_directly:
|
|
478
|
+
# Create tools directly
|
|
479
|
+
return self._create_tools_directly(
|
|
480
|
+
request, agent, functions, generator, serializer
|
|
481
|
+
)
|
|
482
|
+
else:
|
|
483
|
+
# Create approval requests
|
|
484
|
+
return self._create_approval_requests(
|
|
485
|
+
request, agent, functions, generator, serializer
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _create_tools_directly(self, request, agent, functions, generator, serializer):
|
|
489
|
+
"""Create tools directly (for owners and creators)."""
|
|
490
|
+
from django_agent_runtime.dynamic_tools.scanner import FunctionInfo
|
|
491
|
+
|
|
492
|
+
created_tools = []
|
|
493
|
+
for discovered in functions:
|
|
494
|
+
func_info = FunctionInfo(
|
|
495
|
+
name=discovered.name,
|
|
496
|
+
module_path=discovered.module_path,
|
|
497
|
+
function_path=discovered.function_path,
|
|
498
|
+
function_type=discovered.function_type,
|
|
499
|
+
file_path=discovered.file_path,
|
|
500
|
+
line_number=discovered.line_number,
|
|
501
|
+
signature=discovered.signature,
|
|
502
|
+
docstring=discovered.docstring,
|
|
503
|
+
class_name=discovered.class_name,
|
|
504
|
+
parameters=discovered.parameters,
|
|
505
|
+
return_type=discovered.return_type,
|
|
506
|
+
is_async=discovered.is_async,
|
|
507
|
+
has_side_effects=discovered.has_side_effects,
|
|
508
|
+
is_private=discovered.is_private,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
schema = generator.generate(func_info)
|
|
512
|
+
|
|
513
|
+
tool, created = DynamicTool.objects.update_or_create(
|
|
514
|
+
agent=agent,
|
|
515
|
+
function_path=schema.function_path,
|
|
516
|
+
defaults={
|
|
517
|
+
'name': schema.name,
|
|
518
|
+
'description': schema.description,
|
|
519
|
+
'parameters_schema': schema.parameters_schema,
|
|
520
|
+
'source_file': schema.source_file,
|
|
521
|
+
'source_line': schema.source_line,
|
|
522
|
+
'is_safe': schema.is_safe,
|
|
523
|
+
'requires_confirmation': schema.requires_confirmation,
|
|
524
|
+
'discovered_function': discovered,
|
|
525
|
+
'created_by': request.user,
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
discovered.is_selected = True
|
|
530
|
+
discovered.save(update_fields=['is_selected'])
|
|
531
|
+
created_tools.append(tool)
|
|
532
|
+
|
|
533
|
+
return Response({
|
|
534
|
+
'tools_created': len(created_tools),
|
|
535
|
+
'tools': DynamicToolSerializer(created_tools, many=True).data,
|
|
536
|
+
}, status=status.HTTP_201_CREATED)
|
|
537
|
+
|
|
538
|
+
def _create_approval_requests(self, request, agent, functions, generator, serializer):
|
|
539
|
+
"""Create approval requests (for requesters)."""
|
|
540
|
+
from django_agent_runtime.dynamic_tools.scanner import FunctionInfo
|
|
541
|
+
from django_agent_studio.models import ToolApprovalRequest
|
|
542
|
+
from django_agent_studio.api.serializers import ToolApprovalRequestSerializer
|
|
543
|
+
|
|
544
|
+
created_requests = []
|
|
545
|
+
for discovered in functions:
|
|
546
|
+
func_info = FunctionInfo(
|
|
547
|
+
name=discovered.name,
|
|
548
|
+
module_path=discovered.module_path,
|
|
549
|
+
function_path=discovered.function_path,
|
|
550
|
+
function_type=discovered.function_type,
|
|
551
|
+
file_path=discovered.file_path,
|
|
552
|
+
line_number=discovered.line_number,
|
|
553
|
+
signature=discovered.signature,
|
|
554
|
+
docstring=discovered.docstring,
|
|
555
|
+
class_name=discovered.class_name,
|
|
556
|
+
parameters=discovered.parameters,
|
|
557
|
+
return_type=discovered.return_type,
|
|
558
|
+
is_async=discovered.is_async,
|
|
559
|
+
has_side_effects=discovered.has_side_effects,
|
|
560
|
+
is_private=discovered.is_private,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
schema = generator.generate(func_info)
|
|
564
|
+
|
|
565
|
+
approval_request = ToolApprovalRequest.objects.create(
|
|
566
|
+
agent=agent,
|
|
567
|
+
discovered_function=discovered,
|
|
568
|
+
proposed_name=schema.name,
|
|
569
|
+
proposed_description=schema.description,
|
|
570
|
+
proposed_is_safe=schema.is_safe,
|
|
571
|
+
proposed_requires_confirmation=schema.requires_confirmation,
|
|
572
|
+
proposed_timeout_seconds=30,
|
|
573
|
+
requester=request.user,
|
|
574
|
+
request_reason=serializer.validated_data.get('request_reason', ''),
|
|
575
|
+
)
|
|
576
|
+
created_requests.append(approval_request)
|
|
577
|
+
|
|
578
|
+
return Response({
|
|
579
|
+
'approval_required': True,
|
|
580
|
+
'requests_created': len(created_requests),
|
|
581
|
+
'requests': ToolApprovalRequestSerializer(created_requests, many=True).data,
|
|
582
|
+
}, status=status.HTTP_202_ACCEPTED)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class DynamicToolListView(generics.ListAPIView):
|
|
586
|
+
"""
|
|
587
|
+
List dynamic tools for an agent.
|
|
588
|
+
|
|
589
|
+
GET /api/agents/{id}/dynamic-tools/
|
|
590
|
+
|
|
591
|
+
Requires: Viewer access or agent ownership
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
permission_classes = [IsAuthenticated, CanViewDynamicTools | IsOwnerOrHasDynamicToolAccess]
|
|
595
|
+
serializer_class = DynamicToolSerializer
|
|
596
|
+
|
|
597
|
+
def get_queryset(self):
|
|
598
|
+
agent = get_object_or_404(AgentDefinition, id=self.kwargs["agent_id"])
|
|
599
|
+
return agent.dynamic_tools.all()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class DynamicToolDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
603
|
+
"""
|
|
604
|
+
Retrieve, update, or delete a dynamic tool.
|
|
605
|
+
|
|
606
|
+
GET/PUT/PATCH/DELETE /api/agents/{id}/dynamic-tools/{tool_id}/
|
|
607
|
+
|
|
608
|
+
Requires:
|
|
609
|
+
- GET: Viewer access or ownership
|
|
610
|
+
- PUT/PATCH: Creator access or ownership
|
|
611
|
+
- DELETE: Admin access or ownership
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
permission_classes = [IsAuthenticated, DynamicToolObjectPermission | IsOwnerOrHasDynamicToolAccess]
|
|
615
|
+
serializer_class = DynamicToolSerializer
|
|
616
|
+
|
|
617
|
+
def get_queryset(self):
|
|
618
|
+
agent = get_object_or_404(AgentDefinition, id=self.kwargs["agent_id"])
|
|
619
|
+
return agent.dynamic_tools.all()
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class DynamicToolToggleView(APIView):
|
|
623
|
+
"""
|
|
624
|
+
Toggle a dynamic tool's active status.
|
|
625
|
+
|
|
626
|
+
POST /api/agents/{id}/dynamic-tools/{tool_id}/toggle/
|
|
627
|
+
|
|
628
|
+
Requires: Creator access or ownership
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
permission_classes = [IsAuthenticated, CanCreateTool | IsOwnerOrHasDynamicToolAccess]
|
|
632
|
+
|
|
633
|
+
def post(self, request, agent_id, tool_id):
|
|
634
|
+
agent = get_object_or_404(AgentDefinition, id=agent_id)
|
|
635
|
+
tool = get_object_or_404(agent.dynamic_tools, id=tool_id)
|
|
636
|
+
|
|
637
|
+
tool.is_active = not tool.is_active
|
|
638
|
+
tool.save(update_fields=['is_active'])
|
|
639
|
+
|
|
640
|
+
return Response(DynamicToolSerializer(tool).data)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class DynamicToolExecutionListView(generics.ListAPIView):
|
|
644
|
+
"""
|
|
645
|
+
List executions for a dynamic tool.
|
|
646
|
+
|
|
647
|
+
GET /api/agents/{id}/dynamic-tools/{tool_id}/executions/
|
|
648
|
+
|
|
649
|
+
Requires: Viewer access or ownership
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
permission_classes = [IsAuthenticated, CanViewDynamicTools | IsOwnerOrHasDynamicToolAccess]
|
|
653
|
+
serializer_class = DynamicToolExecutionSerializer
|
|
654
|
+
|
|
655
|
+
def get_queryset(self):
|
|
656
|
+
agent = get_object_or_404(AgentDefinition, id=self.kwargs["agent_id"])
|
|
657
|
+
tool = get_object_or_404(agent.dynamic_tools, id=self.kwargs["tool_id"])
|
|
658
|
+
return tool.executions.all()
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# =============================================================================
|
|
662
|
+
# Approval Workflow Views
|
|
663
|
+
# =============================================================================
|
|
664
|
+
|
|
665
|
+
from django_agent_studio.models import ToolApprovalRequest, UserDynamicToolAccess
|
|
666
|
+
from django_agent_studio.api.serializers import (
|
|
667
|
+
ToolApprovalRequestSerializer,
|
|
668
|
+
ToolApprovalReviewSerializer,
|
|
669
|
+
UserDynamicToolAccessSerializer,
|
|
670
|
+
GrantAccessSerializer,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class ToolApprovalRequestListView(generics.ListAPIView):
|
|
675
|
+
"""
|
|
676
|
+
List tool approval requests.
|
|
677
|
+
|
|
678
|
+
GET /api/tool-approval-requests/
|
|
679
|
+
|
|
680
|
+
- Admins see all pending requests
|
|
681
|
+
- Requesters see their own requests
|
|
682
|
+
"""
|
|
683
|
+
|
|
684
|
+
permission_classes = [IsAuthenticated, CanRequestTool]
|
|
685
|
+
serializer_class = ToolApprovalRequestSerializer
|
|
686
|
+
|
|
687
|
+
def get_queryset(self):
|
|
688
|
+
perm_service = get_permission_service()
|
|
689
|
+
|
|
690
|
+
# Admins see all, others see only their own
|
|
691
|
+
if perm_service.can_approve(self.request.user):
|
|
692
|
+
queryset = ToolApprovalRequest.objects.all()
|
|
693
|
+
else:
|
|
694
|
+
queryset = ToolApprovalRequest.objects.filter(requester=self.request.user)
|
|
695
|
+
|
|
696
|
+
# Filter by status
|
|
697
|
+
status_filter = self.request.query_params.get('status')
|
|
698
|
+
if status_filter:
|
|
699
|
+
queryset = queryset.filter(status=status_filter)
|
|
700
|
+
|
|
701
|
+
# Filter by agent
|
|
702
|
+
agent_id = self.request.query_params.get('agent_id')
|
|
703
|
+
if agent_id:
|
|
704
|
+
queryset = queryset.filter(agent_id=agent_id)
|
|
705
|
+
|
|
706
|
+
return queryset
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class ToolApprovalRequestDetailView(generics.RetrieveAPIView):
|
|
710
|
+
"""
|
|
711
|
+
Retrieve a tool approval request.
|
|
712
|
+
|
|
713
|
+
GET /api/tool-approval-requests/{id}/
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
permission_classes = [IsAuthenticated, CanRequestTool]
|
|
717
|
+
serializer_class = ToolApprovalRequestSerializer
|
|
718
|
+
|
|
719
|
+
def get_queryset(self):
|
|
720
|
+
perm_service = get_permission_service()
|
|
721
|
+
|
|
722
|
+
if perm_service.can_approve(self.request.user):
|
|
723
|
+
return ToolApprovalRequest.objects.all()
|
|
724
|
+
return ToolApprovalRequest.objects.filter(requester=self.request.user)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class ToolApprovalReviewView(APIView):
|
|
728
|
+
"""
|
|
729
|
+
Review (approve/reject) a tool approval request.
|
|
730
|
+
|
|
731
|
+
POST /api/tool-approval-requests/{id}/review/
|
|
732
|
+
|
|
733
|
+
Requires: Admin access
|
|
734
|
+
"""
|
|
735
|
+
|
|
736
|
+
permission_classes = [IsAuthenticated, CanApproveTool]
|
|
737
|
+
|
|
738
|
+
def post(self, request, pk):
|
|
739
|
+
approval_request = get_object_or_404(ToolApprovalRequest, id=pk)
|
|
740
|
+
|
|
741
|
+
if approval_request.status != ToolApprovalRequest.Status.PENDING:
|
|
742
|
+
return Response(
|
|
743
|
+
{'error': f'Request is already {approval_request.get_status_display()}'},
|
|
744
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
serializer = ToolApprovalReviewSerializer(data=request.data)
|
|
748
|
+
serializer.is_valid(raise_exception=True)
|
|
749
|
+
|
|
750
|
+
action = serializer.validated_data['action']
|
|
751
|
+
review_notes = serializer.validated_data.get('review_notes', '')
|
|
752
|
+
|
|
753
|
+
approval_request.reviewed_by = request.user
|
|
754
|
+
approval_request.reviewed_at = timezone.now()
|
|
755
|
+
approval_request.review_notes = review_notes
|
|
756
|
+
|
|
757
|
+
if action == 'approve':
|
|
758
|
+
return self._approve_request(approval_request, serializer.validated_data)
|
|
759
|
+
else:
|
|
760
|
+
return self._reject_request(approval_request)
|
|
761
|
+
|
|
762
|
+
def _approve_request(self, approval_request, validated_data):
|
|
763
|
+
"""Approve the request and create the tool."""
|
|
764
|
+
# Use overrides if provided, otherwise use proposed values
|
|
765
|
+
name = validated_data.get('override_name', approval_request.proposed_name)
|
|
766
|
+
description = validated_data.get(
|
|
767
|
+
'override_description', approval_request.proposed_description
|
|
768
|
+
)
|
|
769
|
+
is_safe = validated_data.get('override_is_safe', approval_request.proposed_is_safe)
|
|
770
|
+
requires_confirmation = validated_data.get(
|
|
771
|
+
'override_requires_confirmation',
|
|
772
|
+
approval_request.proposed_requires_confirmation
|
|
773
|
+
)
|
|
774
|
+
timeout = validated_data.get(
|
|
775
|
+
'override_timeout_seconds',
|
|
776
|
+
approval_request.proposed_timeout_seconds
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Create the tool
|
|
780
|
+
tool = DynamicTool.objects.create(
|
|
781
|
+
agent=approval_request.agent,
|
|
782
|
+
name=name,
|
|
783
|
+
description=description,
|
|
784
|
+
function_path=approval_request.discovered_function.function_path,
|
|
785
|
+
source_file=approval_request.discovered_function.file_path,
|
|
786
|
+
source_line=approval_request.discovered_function.line_number,
|
|
787
|
+
parameters_schema=self._build_parameters_schema(
|
|
788
|
+
approval_request.discovered_function
|
|
789
|
+
),
|
|
790
|
+
is_safe=is_safe,
|
|
791
|
+
requires_confirmation=requires_confirmation,
|
|
792
|
+
timeout_seconds=timeout,
|
|
793
|
+
discovered_function=approval_request.discovered_function,
|
|
794
|
+
created_by=approval_request.requester,
|
|
795
|
+
is_verified=True, # Admin-approved tools are verified
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# Update request
|
|
799
|
+
approval_request.status = ToolApprovalRequest.Status.APPROVED
|
|
800
|
+
approval_request.created_tool = tool
|
|
801
|
+
approval_request.save()
|
|
802
|
+
|
|
803
|
+
# Mark function as selected
|
|
804
|
+
approval_request.discovered_function.is_selected = True
|
|
805
|
+
approval_request.discovered_function.save(update_fields=['is_selected'])
|
|
806
|
+
|
|
807
|
+
return Response({
|
|
808
|
+
'status': 'approved',
|
|
809
|
+
'tool': DynamicToolSerializer(tool).data,
|
|
810
|
+
'request': ToolApprovalRequestSerializer(approval_request).data,
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
def _reject_request(self, approval_request):
|
|
814
|
+
"""Reject the request."""
|
|
815
|
+
approval_request.status = ToolApprovalRequest.Status.REJECTED
|
|
816
|
+
approval_request.save()
|
|
817
|
+
|
|
818
|
+
return Response({
|
|
819
|
+
'status': 'rejected',
|
|
820
|
+
'request': ToolApprovalRequestSerializer(approval_request).data,
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
def _build_parameters_schema(self, discovered_function):
|
|
824
|
+
"""Build JSON Schema from discovered function parameters."""
|
|
825
|
+
from django_agent_runtime.dynamic_tools import ToolGenerator
|
|
826
|
+
from django_agent_runtime.dynamic_tools.scanner import FunctionInfo
|
|
827
|
+
|
|
828
|
+
func_info = FunctionInfo(
|
|
829
|
+
name=discovered_function.name,
|
|
830
|
+
module_path=discovered_function.module_path,
|
|
831
|
+
function_path=discovered_function.function_path,
|
|
832
|
+
function_type=discovered_function.function_type,
|
|
833
|
+
file_path=discovered_function.file_path,
|
|
834
|
+
line_number=discovered_function.line_number,
|
|
835
|
+
signature=discovered_function.signature,
|
|
836
|
+
docstring=discovered_function.docstring,
|
|
837
|
+
class_name=discovered_function.class_name,
|
|
838
|
+
parameters=discovered_function.parameters,
|
|
839
|
+
return_type=discovered_function.return_type,
|
|
840
|
+
is_async=discovered_function.is_async,
|
|
841
|
+
has_side_effects=discovered_function.has_side_effects,
|
|
842
|
+
is_private=discovered_function.is_private,
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
generator = ToolGenerator()
|
|
846
|
+
schema = generator.generate(func_info)
|
|
847
|
+
return schema.parameters_schema
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
class ToolApprovalCancelView(APIView):
|
|
851
|
+
"""
|
|
852
|
+
Cancel a pending tool approval request.
|
|
853
|
+
|
|
854
|
+
POST /api/tool-approval-requests/{id}/cancel/
|
|
855
|
+
|
|
856
|
+
Only the requester can cancel their own request.
|
|
857
|
+
"""
|
|
858
|
+
|
|
859
|
+
permission_classes = [IsAuthenticated]
|
|
860
|
+
|
|
861
|
+
def post(self, request, pk):
|
|
862
|
+
approval_request = get_object_or_404(
|
|
863
|
+
ToolApprovalRequest,
|
|
864
|
+
id=pk,
|
|
865
|
+
requester=request.user,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
if approval_request.status != ToolApprovalRequest.Status.PENDING:
|
|
869
|
+
return Response(
|
|
870
|
+
{'error': f'Request is already {approval_request.get_status_display()}'},
|
|
871
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
approval_request.status = ToolApprovalRequest.Status.CANCELLED
|
|
875
|
+
approval_request.save()
|
|
876
|
+
|
|
877
|
+
return Response({
|
|
878
|
+
'status': 'cancelled',
|
|
879
|
+
'request': ToolApprovalRequestSerializer(approval_request).data,
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
# =============================================================================
|
|
885
|
+
# Permission Management Views
|
|
886
|
+
# =============================================================================
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
class UserAccessListView(generics.ListAPIView):
|
|
890
|
+
"""
|
|
891
|
+
List all user access grants.
|
|
892
|
+
|
|
893
|
+
GET /api/dynamic-tool-access/
|
|
894
|
+
|
|
895
|
+
Requires: Admin access
|
|
896
|
+
"""
|
|
897
|
+
|
|
898
|
+
permission_classes = [IsAuthenticated, CanManagePermissions]
|
|
899
|
+
serializer_class = UserDynamicToolAccessSerializer
|
|
900
|
+
queryset = UserDynamicToolAccess.objects.all()
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
class UserAccessDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
904
|
+
"""
|
|
905
|
+
Retrieve, update, or delete a user's access.
|
|
906
|
+
|
|
907
|
+
GET/PUT/PATCH/DELETE /api/dynamic-tool-access/{id}/
|
|
908
|
+
|
|
909
|
+
Requires: Admin access
|
|
910
|
+
"""
|
|
911
|
+
|
|
912
|
+
permission_classes = [IsAuthenticated, CanManagePermissions]
|
|
913
|
+
serializer_class = UserDynamicToolAccessSerializer
|
|
914
|
+
queryset = UserDynamicToolAccess.objects.all()
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
class GrantAccessView(APIView):
|
|
918
|
+
"""
|
|
919
|
+
Grant dynamic tool access to a user.
|
|
920
|
+
|
|
921
|
+
POST /api/dynamic-tool-access/grant/
|
|
922
|
+
|
|
923
|
+
Requires: Admin access
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
permission_classes = [IsAuthenticated, CanManagePermissions]
|
|
927
|
+
|
|
928
|
+
def post(self, request):
|
|
929
|
+
serializer = GrantAccessSerializer(data=request.data)
|
|
930
|
+
serializer.is_valid(raise_exception=True)
|
|
931
|
+
|
|
932
|
+
from django.contrib.auth import get_user_model
|
|
933
|
+
User = get_user_model()
|
|
934
|
+
|
|
935
|
+
try:
|
|
936
|
+
user = User.objects.get(id=serializer.validated_data['user_id'])
|
|
937
|
+
except User.DoesNotExist:
|
|
938
|
+
return Response(
|
|
939
|
+
{'error': 'User not found'},
|
|
940
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
# Get agents if restricted
|
|
944
|
+
agents = None
|
|
945
|
+
agent_ids = serializer.validated_data.get('restricted_to_agent_ids')
|
|
946
|
+
if agent_ids:
|
|
947
|
+
agents = AgentDefinition.objects.filter(id__in=agent_ids)
|
|
948
|
+
|
|
949
|
+
perm_service = get_permission_service()
|
|
950
|
+
access = perm_service.grant_access(
|
|
951
|
+
user=user,
|
|
952
|
+
access_level=serializer.validated_data['access_level'],
|
|
953
|
+
granted_by=request.user,
|
|
954
|
+
agents=agents,
|
|
955
|
+
notes=serializer.validated_data.get('notes', ''),
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
return Response(
|
|
959
|
+
UserDynamicToolAccessSerializer(access).data,
|
|
960
|
+
status=status.HTTP_201_CREATED,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
class RevokeAccessView(APIView):
|
|
965
|
+
"""
|
|
966
|
+
Revoke dynamic tool access from a user.
|
|
967
|
+
|
|
968
|
+
POST /api/dynamic-tool-access/revoke/
|
|
969
|
+
|
|
970
|
+
Requires: Admin access
|
|
971
|
+
"""
|
|
972
|
+
|
|
973
|
+
permission_classes = [IsAuthenticated, CanManagePermissions]
|
|
974
|
+
|
|
975
|
+
def post(self, request):
|
|
976
|
+
user_id = request.data.get('user_id')
|
|
977
|
+
if not user_id:
|
|
978
|
+
return Response(
|
|
979
|
+
{'error': 'user_id is required'},
|
|
980
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
from django.contrib.auth import get_user_model
|
|
984
|
+
User = get_user_model()
|
|
985
|
+
|
|
986
|
+
try:
|
|
987
|
+
user = User.objects.get(id=user_id)
|
|
988
|
+
except User.DoesNotExist:
|
|
989
|
+
return Response(
|
|
990
|
+
{'error': 'User not found'},
|
|
991
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
perm_service = get_permission_service()
|
|
995
|
+
revoked = perm_service.revoke_access(user)
|
|
996
|
+
|
|
997
|
+
if revoked:
|
|
998
|
+
return Response({'status': 'revoked'})
|
|
999
|
+
return Response(
|
|
1000
|
+
{'status': 'no_access', 'message': 'User had no access to revoke'},
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
class MyAccessView(APIView):
|
|
1005
|
+
"""
|
|
1006
|
+
Get the current user's dynamic tool access level.
|
|
1007
|
+
|
|
1008
|
+
GET /api/dynamic-tool-access/me/
|
|
1009
|
+
"""
|
|
1010
|
+
|
|
1011
|
+
permission_classes = [IsAuthenticated]
|
|
1012
|
+
|
|
1013
|
+
def get(self, request):
|
|
1014
|
+
perm_service = get_permission_service()
|
|
1015
|
+
access_level = perm_service.get_user_access_level(request.user)
|
|
1016
|
+
|
|
1017
|
+
# Get the access record if it exists
|
|
1018
|
+
try:
|
|
1019
|
+
access = UserDynamicToolAccess.objects.get(user=request.user)
|
|
1020
|
+
return Response({
|
|
1021
|
+
'access_level': access_level,
|
|
1022
|
+
'access_level_display': access.get_access_level_display(),
|
|
1023
|
+
'restricted_to_agents': [
|
|
1024
|
+
{'id': str(a.id), 'name': a.name}
|
|
1025
|
+
for a in access.restricted_to_agents.all()
|
|
1026
|
+
],
|
|
1027
|
+
'granted_at': access.granted_at,
|
|
1028
|
+
})
|
|
1029
|
+
except UserDynamicToolAccess.DoesNotExist:
|
|
1030
|
+
# User has no explicit access (defaults to NONE unless superuser)
|
|
1031
|
+
from django_agent_studio.models.permissions import DynamicToolAccessLevel
|
|
1032
|
+
return Response({
|
|
1033
|
+
'access_level': access_level,
|
|
1034
|
+
'access_level_display': (
|
|
1035
|
+
'Admin (superuser)' if request.user.is_superuser
|
|
1036
|
+
else DynamicToolAccessLevel(access_level).label
|
|
1037
|
+
),
|
|
1038
|
+
'restricted_to_agents': [],
|
|
1039
|
+
'granted_at': None,
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
class ModelsListView(APIView):
|
|
1044
|
+
"""List available LLM models for the model selector."""
|
|
1045
|
+
|
|
1046
|
+
permission_classes = [IsAuthenticated]
|
|
1047
|
+
|
|
1048
|
+
def get(self, request):
|
|
1049
|
+
from django_agent_runtime.runtime.llm import list_models_for_ui, DEFAULT_MODEL
|
|
1050
|
+
|
|
1051
|
+
models = list_models_for_ui()
|
|
1052
|
+
return Response({
|
|
1053
|
+
"models": models,
|
|
1054
|
+
"default": DEFAULT_MODEL,
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
# =============================================================================
|
|
1059
|
+
# Agent Schema Editor Views
|
|
1060
|
+
# =============================================================================
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
class AgentFullSchemaView(APIView):
|
|
1064
|
+
"""
|
|
1065
|
+
Get or update the complete agent schema for debugging/editing.
|
|
1066
|
+
|
|
1067
|
+
GET /api/agents/{id}/full-schema/
|
|
1068
|
+
Returns the complete agent configuration including all tools, knowledge,
|
|
1069
|
+
dynamic tools, discovered functions, RAG config, etc.
|
|
1070
|
+
|
|
1071
|
+
PUT /api/agents/{id}/full-schema/
|
|
1072
|
+
Updates the agent configuration from a full schema JSON.
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
permission_classes = [IsAuthenticated]
|
|
1076
|
+
|
|
1077
|
+
def get(self, request, pk):
|
|
1078
|
+
"""Get the complete agent schema."""
|
|
1079
|
+
agent = get_agent_for_user(request.user, pk)
|
|
1080
|
+
|
|
1081
|
+
# Get active version
|
|
1082
|
+
active_version = agent.versions.filter(is_active=True).first()
|
|
1083
|
+
|
|
1084
|
+
# Build comprehensive schema
|
|
1085
|
+
schema = {
|
|
1086
|
+
# Metadata
|
|
1087
|
+
"id": str(agent.id),
|
|
1088
|
+
"slug": agent.slug,
|
|
1089
|
+
"name": agent.name,
|
|
1090
|
+
"description": agent.description,
|
|
1091
|
+
"icon": agent.icon,
|
|
1092
|
+
"is_public": agent.is_public,
|
|
1093
|
+
"is_template": agent.is_template,
|
|
1094
|
+
"is_active": agent.is_active,
|
|
1095
|
+
"created_at": agent.created_at.isoformat() if agent.created_at else None,
|
|
1096
|
+
"updated_at": agent.updated_at.isoformat() if agent.updated_at else None,
|
|
1097
|
+
|
|
1098
|
+
# Parent agent (for inheritance)
|
|
1099
|
+
"parent": {
|
|
1100
|
+
"id": str(agent.parent.id),
|
|
1101
|
+
"slug": agent.parent.slug,
|
|
1102
|
+
"name": agent.parent.name,
|
|
1103
|
+
} if agent.parent else None,
|
|
1104
|
+
|
|
1105
|
+
# Active version configuration
|
|
1106
|
+
"version": {
|
|
1107
|
+
"id": str(active_version.id) if active_version else None,
|
|
1108
|
+
"version": active_version.version if active_version else "draft",
|
|
1109
|
+
"system_prompt": active_version.system_prompt if active_version else "",
|
|
1110
|
+
"model": active_version.model if active_version else "gpt-4o",
|
|
1111
|
+
"model_settings": active_version.model_settings if active_version else {},
|
|
1112
|
+
"extra_config": active_version.extra_config if active_version else {},
|
|
1113
|
+
"is_draft": active_version.is_draft if active_version else True,
|
|
1114
|
+
"is_active": active_version.is_active if active_version else False,
|
|
1115
|
+
"notes": active_version.notes if active_version else "",
|
|
1116
|
+
},
|
|
1117
|
+
|
|
1118
|
+
# RAG configuration
|
|
1119
|
+
"rag_config": agent.rag_config or {
|
|
1120
|
+
"enabled": False,
|
|
1121
|
+
"top_k": 5,
|
|
1122
|
+
"similarity_threshold": 0.7,
|
|
1123
|
+
"chunk_size": 500,
|
|
1124
|
+
"chunk_overlap": 50,
|
|
1125
|
+
"embedding_model": "text-embedding-3-small",
|
|
1126
|
+
},
|
|
1127
|
+
|
|
1128
|
+
# Static tools (AgentTool)
|
|
1129
|
+
"tools": [
|
|
1130
|
+
{
|
|
1131
|
+
"id": str(tool.id),
|
|
1132
|
+
"name": tool.name,
|
|
1133
|
+
"tool_type": tool.tool_type,
|
|
1134
|
+
"description": tool.description,
|
|
1135
|
+
"parameters_schema": tool.parameters_schema,
|
|
1136
|
+
"builtin_ref": tool.builtin_ref,
|
|
1137
|
+
"subagent_id": str(tool.subagent_id) if tool.subagent_id else None,
|
|
1138
|
+
"config": tool.config,
|
|
1139
|
+
"is_active": tool.is_active,
|
|
1140
|
+
"order": tool.order,
|
|
1141
|
+
}
|
|
1142
|
+
for tool in agent.tools.all().order_by('order', 'name')
|
|
1143
|
+
],
|
|
1144
|
+
|
|
1145
|
+
# Dynamic tools (from function discovery)
|
|
1146
|
+
"dynamic_tools": [
|
|
1147
|
+
{
|
|
1148
|
+
"id": str(dt.id),
|
|
1149
|
+
"name": dt.name,
|
|
1150
|
+
"description": dt.description,
|
|
1151
|
+
"function_path": dt.function_path,
|
|
1152
|
+
"source_file": dt.source_file,
|
|
1153
|
+
"source_line": dt.source_line,
|
|
1154
|
+
"parameters_schema": dt.parameters_schema,
|
|
1155
|
+
"execution_mode": dt.execution_mode,
|
|
1156
|
+
"timeout_seconds": dt.timeout_seconds,
|
|
1157
|
+
"is_safe": dt.is_safe,
|
|
1158
|
+
"requires_confirmation": dt.requires_confirmation,
|
|
1159
|
+
"allowed_for_auto_execution": dt.allowed_for_auto_execution,
|
|
1160
|
+
"allowed_imports": dt.allowed_imports,
|
|
1161
|
+
"blocked_imports": dt.blocked_imports,
|
|
1162
|
+
"is_active": dt.is_active,
|
|
1163
|
+
"is_verified": dt.is_verified,
|
|
1164
|
+
"version": dt.version,
|
|
1165
|
+
}
|
|
1166
|
+
for dt in agent.dynamic_tools.all().order_by('name')
|
|
1167
|
+
],
|
|
1168
|
+
|
|
1169
|
+
# Knowledge sources
|
|
1170
|
+
"knowledge": [
|
|
1171
|
+
{
|
|
1172
|
+
"id": str(k.id),
|
|
1173
|
+
"name": k.name,
|
|
1174
|
+
"knowledge_type": k.knowledge_type,
|
|
1175
|
+
"content": k.content if k.knowledge_type == 'text' else None,
|
|
1176
|
+
"url": k.url if k.knowledge_type == 'url' else None,
|
|
1177
|
+
"file": k.file.url if k.file else None,
|
|
1178
|
+
"dynamic_config": k.dynamic_config if k.knowledge_type == 'dynamic' else None,
|
|
1179
|
+
"inclusion_mode": k.inclusion_mode,
|
|
1180
|
+
"is_active": k.is_active,
|
|
1181
|
+
"order": k.order,
|
|
1182
|
+
# RAG-specific fields
|
|
1183
|
+
"embedding_status": k.embedding_status,
|
|
1184
|
+
"chunk_count": k.chunk_count,
|
|
1185
|
+
"indexed_at": k.indexed_at.isoformat() if k.indexed_at else None,
|
|
1186
|
+
"rag_config": k.rag_config,
|
|
1187
|
+
}
|
|
1188
|
+
for k in agent.knowledge_sources.all().order_by('order', 'name')
|
|
1189
|
+
],
|
|
1190
|
+
|
|
1191
|
+
# Available discovered functions (not yet converted to tools)
|
|
1192
|
+
"available_functions": [
|
|
1193
|
+
{
|
|
1194
|
+
"id": str(f.id),
|
|
1195
|
+
"name": f.name,
|
|
1196
|
+
"module_path": f.module_path,
|
|
1197
|
+
"function_path": f.function_path,
|
|
1198
|
+
"function_type": f.function_type,
|
|
1199
|
+
"class_name": f.class_name,
|
|
1200
|
+
"file_path": f.file_path,
|
|
1201
|
+
"line_number": f.line_number,
|
|
1202
|
+
"signature": f.signature,
|
|
1203
|
+
"docstring": f.docstring,
|
|
1204
|
+
"parameters": f.parameters,
|
|
1205
|
+
"return_type": f.return_type,
|
|
1206
|
+
"is_async": f.is_async,
|
|
1207
|
+
"has_side_effects": f.has_side_effects,
|
|
1208
|
+
"is_private": f.is_private,
|
|
1209
|
+
"is_selected": f.is_selected,
|
|
1210
|
+
}
|
|
1211
|
+
for f in DiscoveredFunction.objects.filter(is_selected=False).order_by('module_path', 'name')[:100]
|
|
1212
|
+
],
|
|
1213
|
+
|
|
1214
|
+
# Effective config (merged with parent)
|
|
1215
|
+
"effective_config": agent.get_effective_config(),
|
|
1216
|
+
|
|
1217
|
+
# Portable config format (for export)
|
|
1218
|
+
"portable_config": agent.to_config_dict(),
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return Response(schema)
|
|
1222
|
+
|
|
1223
|
+
def put(self, request, pk):
|
|
1224
|
+
"""Update the agent configuration from a full schema."""
|
|
1225
|
+
agent = get_agent_for_user(request.user, pk)
|
|
1226
|
+
|
|
1227
|
+
data = request.data
|
|
1228
|
+
|
|
1229
|
+
# Update agent metadata
|
|
1230
|
+
if 'name' in data:
|
|
1231
|
+
agent.name = data['name']
|
|
1232
|
+
if 'description' in data:
|
|
1233
|
+
agent.description = data['description']
|
|
1234
|
+
if 'icon' in data:
|
|
1235
|
+
agent.icon = data['icon']
|
|
1236
|
+
if 'is_public' in data:
|
|
1237
|
+
agent.is_public = data['is_public']
|
|
1238
|
+
if 'is_template' in data:
|
|
1239
|
+
agent.is_template = data['is_template']
|
|
1240
|
+
if 'is_active' in data:
|
|
1241
|
+
agent.is_active = data['is_active']
|
|
1242
|
+
if 'rag_config' in data:
|
|
1243
|
+
agent.rag_config = data['rag_config']
|
|
1244
|
+
|
|
1245
|
+
agent.save()
|
|
1246
|
+
|
|
1247
|
+
# Update active version
|
|
1248
|
+
if 'version' in data:
|
|
1249
|
+
version_data = data['version']
|
|
1250
|
+
active_version = agent.versions.filter(is_active=True).first()
|
|
1251
|
+
|
|
1252
|
+
if active_version:
|
|
1253
|
+
if 'system_prompt' in version_data:
|
|
1254
|
+
active_version.system_prompt = version_data['system_prompt']
|
|
1255
|
+
if 'model' in version_data:
|
|
1256
|
+
active_version.model = version_data['model']
|
|
1257
|
+
if 'model_settings' in version_data:
|
|
1258
|
+
active_version.model_settings = version_data['model_settings']
|
|
1259
|
+
if 'extra_config' in version_data:
|
|
1260
|
+
active_version.extra_config = version_data['extra_config']
|
|
1261
|
+
if 'notes' in version_data:
|
|
1262
|
+
active_version.notes = version_data['notes']
|
|
1263
|
+
active_version.save()
|
|
1264
|
+
|
|
1265
|
+
# Update tools
|
|
1266
|
+
if 'tools' in data:
|
|
1267
|
+
for tool_data in data['tools']:
|
|
1268
|
+
if 'id' in tool_data:
|
|
1269
|
+
try:
|
|
1270
|
+
tool = agent.tools.get(id=tool_data['id'])
|
|
1271
|
+
for field in ['name', 'description', 'tool_type', 'parameters_schema',
|
|
1272
|
+
'builtin_ref', 'config', 'is_active', 'order']:
|
|
1273
|
+
if field in tool_data:
|
|
1274
|
+
setattr(tool, field, tool_data[field])
|
|
1275
|
+
tool.save()
|
|
1276
|
+
except AgentTool.DoesNotExist:
|
|
1277
|
+
pass
|
|
1278
|
+
else:
|
|
1279
|
+
# Create new tool
|
|
1280
|
+
AgentTool.objects.create(
|
|
1281
|
+
agent=agent,
|
|
1282
|
+
name=tool_data.get('name', 'new_tool'),
|
|
1283
|
+
tool_type=tool_data.get('tool_type', 'function'),
|
|
1284
|
+
description=tool_data.get('description', ''),
|
|
1285
|
+
parameters_schema=tool_data.get('parameters_schema', {}),
|
|
1286
|
+
builtin_ref=tool_data.get('builtin_ref', ''),
|
|
1287
|
+
config=tool_data.get('config', {}),
|
|
1288
|
+
is_active=tool_data.get('is_active', True),
|
|
1289
|
+
order=tool_data.get('order', 0),
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
# Update dynamic tools
|
|
1293
|
+
if 'dynamic_tools' in data:
|
|
1294
|
+
for dt_data in data['dynamic_tools']:
|
|
1295
|
+
if 'id' in dt_data:
|
|
1296
|
+
try:
|
|
1297
|
+
dt = agent.dynamic_tools.get(id=dt_data['id'])
|
|
1298
|
+
for field in ['name', 'description', 'function_path', 'parameters_schema',
|
|
1299
|
+
'execution_mode', 'timeout_seconds', 'is_safe',
|
|
1300
|
+
'requires_confirmation', 'allowed_for_auto_execution',
|
|
1301
|
+
'allowed_imports', 'blocked_imports', 'is_active', 'is_verified']:
|
|
1302
|
+
if field in dt_data:
|
|
1303
|
+
setattr(dt, field, dt_data[field])
|
|
1304
|
+
dt.save()
|
|
1305
|
+
except DynamicTool.DoesNotExist:
|
|
1306
|
+
pass
|
|
1307
|
+
|
|
1308
|
+
# Update knowledge sources
|
|
1309
|
+
if 'knowledge' in data:
|
|
1310
|
+
for k_data in data['knowledge']:
|
|
1311
|
+
if 'id' in k_data:
|
|
1312
|
+
try:
|
|
1313
|
+
k = agent.knowledge_sources.get(id=k_data['id'])
|
|
1314
|
+
for field in ['name', 'knowledge_type', 'content', 'url',
|
|
1315
|
+
'dynamic_config', 'inclusion_mode', 'is_active',
|
|
1316
|
+
'order', 'rag_config']:
|
|
1317
|
+
if field in k_data:
|
|
1318
|
+
setattr(k, field, k_data[field])
|
|
1319
|
+
k.save()
|
|
1320
|
+
except AgentKnowledge.DoesNotExist:
|
|
1321
|
+
pass
|
|
1322
|
+
|
|
1323
|
+
# Create a revision to track this change
|
|
1324
|
+
from django_agent_runtime.models import AgentRevision
|
|
1325
|
+
AgentRevision.create_from_agent(
|
|
1326
|
+
agent,
|
|
1327
|
+
comment=f"Updated via schema editor by {request.user.email}",
|
|
1328
|
+
user=request.user,
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
return Response({
|
|
1332
|
+
"status": "updated",
|
|
1333
|
+
"message": "Agent schema updated successfully",
|
|
1334
|
+
})
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
# =============================================================================
|
|
1338
|
+
# Multi-Agent System Views
|
|
1339
|
+
# =============================================================================
|
|
1340
|
+
|
|
1341
|
+
from django_agent_studio.api.serializers import (
|
|
1342
|
+
AgentSystemListSerializer,
|
|
1343
|
+
AgentSystemDetailSerializer,
|
|
1344
|
+
AgentSystemCreateSerializer,
|
|
1345
|
+
AgentSystemMemberSerializer,
|
|
1346
|
+
AgentSystemVersionSerializer,
|
|
1347
|
+
AddMemberSerializer,
|
|
1348
|
+
PublishVersionSerializer,
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
class AgentSystemListCreateView(generics.ListCreateAPIView):
|
|
1353
|
+
"""List and create agent systems."""
|
|
1354
|
+
|
|
1355
|
+
permission_classes = [IsAuthenticated]
|
|
1356
|
+
|
|
1357
|
+
def get_serializer_class(self):
|
|
1358
|
+
if self.request.method == 'POST':
|
|
1359
|
+
return AgentSystemCreateSerializer
|
|
1360
|
+
return AgentSystemListSerializer
|
|
1361
|
+
|
|
1362
|
+
def get_queryset(self):
|
|
1363
|
+
return get_system_queryset_for_user(self.request.user)
|
|
1364
|
+
|
|
1365
|
+
def create(self, request, *args, **kwargs):
|
|
1366
|
+
serializer = self.get_serializer(data=request.data)
|
|
1367
|
+
serializer.is_valid(raise_exception=True)
|
|
1368
|
+
|
|
1369
|
+
# Get the entry agent
|
|
1370
|
+
entry_agent = get_agent_for_user(request.user, serializer.validated_data['entry_agent_id'])
|
|
1371
|
+
|
|
1372
|
+
# Use the service to create the system
|
|
1373
|
+
from django_agent_runtime.services.multi_agent import create_system_from_entry_agent
|
|
1374
|
+
|
|
1375
|
+
system = create_system_from_entry_agent(
|
|
1376
|
+
slug=serializer.validated_data['slug'],
|
|
1377
|
+
name=serializer.validated_data['name'],
|
|
1378
|
+
entry_agent=entry_agent,
|
|
1379
|
+
description=serializer.validated_data.get('description', ''),
|
|
1380
|
+
owner=request.user,
|
|
1381
|
+
auto_discover=serializer.validated_data.get('auto_discover', True),
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
return Response(
|
|
1385
|
+
AgentSystemDetailSerializer(system).data,
|
|
1386
|
+
status=status.HTTP_201_CREATED,
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
class AgentSystemDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
1391
|
+
"""Retrieve, update, or delete an agent system."""
|
|
1392
|
+
|
|
1393
|
+
permission_classes = [IsAuthenticated]
|
|
1394
|
+
serializer_class = AgentSystemDetailSerializer
|
|
1395
|
+
|
|
1396
|
+
def get_queryset(self):
|
|
1397
|
+
return get_system_queryset_for_user(self.request.user)
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
class AgentSystemMemberListCreateView(generics.ListCreateAPIView):
|
|
1401
|
+
"""List and add members to a system."""
|
|
1402
|
+
|
|
1403
|
+
permission_classes = [IsAuthenticated]
|
|
1404
|
+
|
|
1405
|
+
def get_serializer_class(self):
|
|
1406
|
+
if self.request.method == 'POST':
|
|
1407
|
+
return AddMemberSerializer
|
|
1408
|
+
return AgentSystemMemberSerializer
|
|
1409
|
+
|
|
1410
|
+
def get_queryset(self):
|
|
1411
|
+
system = get_system_for_user(self.request.user, self.kwargs['system_id'])
|
|
1412
|
+
return system.members.select_related('agent').all()
|
|
1413
|
+
|
|
1414
|
+
def create(self, request, *args, **kwargs):
|
|
1415
|
+
system = get_system_for_user(request.user, self.kwargs['system_id'])
|
|
1416
|
+
|
|
1417
|
+
serializer = self.get_serializer(data=request.data)
|
|
1418
|
+
serializer.is_valid(raise_exception=True)
|
|
1419
|
+
|
|
1420
|
+
agent = get_agent_for_user(request.user, serializer.validated_data['agent_id'])
|
|
1421
|
+
|
|
1422
|
+
from django_agent_runtime.services.multi_agent import add_agent_to_system
|
|
1423
|
+
|
|
1424
|
+
member = add_agent_to_system(
|
|
1425
|
+
system=system,
|
|
1426
|
+
agent=agent,
|
|
1427
|
+
role=serializer.validated_data.get('role', AgentSystemMember.Role.SPECIALIST),
|
|
1428
|
+
notes=serializer.validated_data.get('notes', ''),
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
return Response(
|
|
1432
|
+
AgentSystemMemberSerializer(member).data,
|
|
1433
|
+
status=status.HTTP_201_CREATED,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
class AgentSystemMemberDetailView(generics.RetrieveUpdateDestroyAPIView):
|
|
1438
|
+
"""Retrieve, update, or delete a system member."""
|
|
1439
|
+
|
|
1440
|
+
permission_classes = [IsAuthenticated]
|
|
1441
|
+
serializer_class = AgentSystemMemberSerializer
|
|
1442
|
+
|
|
1443
|
+
def get_queryset(self):
|
|
1444
|
+
system = get_system_for_user(self.request.user, self.kwargs['system_id'])
|
|
1445
|
+
return system.members.all()
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
class AgentSystemVersionListView(generics.ListAPIView):
|
|
1449
|
+
"""List versions of a system."""
|
|
1450
|
+
|
|
1451
|
+
permission_classes = [IsAuthenticated]
|
|
1452
|
+
serializer_class = AgentSystemVersionSerializer
|
|
1453
|
+
|
|
1454
|
+
def get_queryset(self):
|
|
1455
|
+
system = get_system_for_user(self.request.user, self.kwargs['system_id'])
|
|
1456
|
+
return system.versions.all()
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
class AgentSystemPublishView(APIView):
|
|
1460
|
+
"""Publish a new version of a system."""
|
|
1461
|
+
|
|
1462
|
+
permission_classes = [IsAuthenticated]
|
|
1463
|
+
|
|
1464
|
+
def post(self, request, system_id):
|
|
1465
|
+
system = get_system_for_user(request.user, system_id)
|
|
1466
|
+
|
|
1467
|
+
serializer = PublishVersionSerializer(data=request.data)
|
|
1468
|
+
serializer.is_valid(raise_exception=True)
|
|
1469
|
+
|
|
1470
|
+
from django_agent_runtime.services.multi_agent import publish_system_version
|
|
1471
|
+
|
|
1472
|
+
version = publish_system_version(
|
|
1473
|
+
system=system,
|
|
1474
|
+
version=serializer.validated_data['version'],
|
|
1475
|
+
notes=serializer.validated_data.get('notes', ''),
|
|
1476
|
+
user=request.user,
|
|
1477
|
+
make_active=serializer.validated_data.get('make_active', False),
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
return Response(
|
|
1481
|
+
AgentSystemVersionSerializer(version).data,
|
|
1482
|
+
status=status.HTTP_201_CREATED,
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
class AgentSystemDeployView(APIView):
|
|
1487
|
+
"""Deploy (activate) a system version."""
|
|
1488
|
+
|
|
1489
|
+
permission_classes = [IsAuthenticated]
|
|
1490
|
+
|
|
1491
|
+
def post(self, request, system_id, version_id):
|
|
1492
|
+
system = get_system_for_user(request.user, system_id)
|
|
1493
|
+
version = get_object_or_404(system.versions, id=version_id)
|
|
1494
|
+
|
|
1495
|
+
from django_agent_runtime.services.multi_agent import deploy_system_version
|
|
1496
|
+
|
|
1497
|
+
deploy_system_version(version)
|
|
1498
|
+
|
|
1499
|
+
return Response(AgentSystemVersionSerializer(version).data)
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
class AgentSystemExportView(APIView):
|
|
1503
|
+
"""Export a system version as portable JSON."""
|
|
1504
|
+
|
|
1505
|
+
permission_classes = [IsAuthenticated]
|
|
1506
|
+
|
|
1507
|
+
def get(self, request, system_id, version_id):
|
|
1508
|
+
system = get_system_for_user(request.user, system_id)
|
|
1509
|
+
version = get_object_or_404(system.versions, id=version_id)
|
|
1510
|
+
|
|
1511
|
+
embed_agents = request.query_params.get('embed', 'true').lower() == 'true'
|
|
1512
|
+
|
|
1513
|
+
from django_agent_runtime.services.multi_agent import export_system_version
|
|
1514
|
+
|
|
1515
|
+
config = export_system_version(version, embed_agents=embed_agents)
|
|
1516
|
+
|
|
1517
|
+
return Response(config)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
class AgentSystemDiscoverView(APIView):
|
|
1521
|
+
"""Discover all agents reachable from an entry agent."""
|
|
1522
|
+
|
|
1523
|
+
permission_classes = [IsAuthenticated]
|
|
1524
|
+
|
|
1525
|
+
def get(self, request, agent_id):
|
|
1526
|
+
agent = get_agent_for_user(request.user, agent_id)
|
|
1527
|
+
|
|
1528
|
+
from django_agent_runtime.services.multi_agent import discover_system_agents
|
|
1529
|
+
|
|
1530
|
+
agents = discover_system_agents(agent)
|
|
1531
|
+
|
|
1532
|
+
return Response({
|
|
1533
|
+
'entry_agent': {
|
|
1534
|
+
'id': str(agent.id),
|
|
1535
|
+
'slug': agent.slug,
|
|
1536
|
+
'name': agent.name,
|
|
1537
|
+
},
|
|
1538
|
+
'discovered_agents': [
|
|
1539
|
+
{
|
|
1540
|
+
'id': str(a.id),
|
|
1541
|
+
'slug': a.slug,
|
|
1542
|
+
'name': a.name,
|
|
1543
|
+
'description': a.description,
|
|
1544
|
+
}
|
|
1545
|
+
for a in agents
|
|
1546
|
+
],
|
|
1547
|
+
'total_count': len(agents),
|
|
1548
|
+
})
|