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.
@@ -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
+ })