django-agent-studio 0.3.0__tar.gz → 0.3.4__tar.gz
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-0.3.0 → django_agent_studio-0.3.4}/PKG-INFO +4 -1
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/README.md +3 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/agents/dynamic.py +166 -7
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/api/serializers.py +166 -8
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/api/urls.py +35 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/api/views.py +470 -8
- django_agent_studio-0.3.4/branding.py +103 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/django_agent_studio.egg-info/PKG-INFO +4 -1
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/django_agent_studio.egg-info/SOURCES.txt +6 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/pyproject.toml +1 -1
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/static/agent-frontend/chat-widget.css +58 -1
- django_agent_studio-0.3.4/static/agent-frontend/chat-widget.js +607 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/static/django_agent_studio/js/builder.js +58 -58
- django_agent_studio-0.3.4/static/django_agent_studio/js/builder.js.map +1 -0
- django_agent_studio-0.3.4/static/django_agent_studio/js/style.css +1 -0
- django_agent_studio-0.3.4/templates/django_agent_studio/agent_list.html +111 -0
- django_agent_studio-0.3.4/templates/django_agent_studio/base.html +238 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/templates/django_agent_studio/builder.html +3 -3
- django_agent_studio-0.3.4/templates/django_agent_studio/collaborators.html +406 -0
- django_agent_studio-0.3.4/templates/django_agent_studio/home.html +162 -0
- django_agent_studio-0.3.4/templates/django_agent_studio/system_create.html +146 -0
- django_agent_studio-0.3.4/templates/django_agent_studio/system_list.html +112 -0
- django_agent_studio-0.3.4/templates/django_agent_studio/system_test.html +147 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/templates/django_agent_studio/test.html +40 -22
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/urls.py +4 -0
- django_agent_studio-0.3.4/views.py +465 -0
- django_agent_studio-0.3.0/static/agent-frontend/chat-widget.js +0 -591
- django_agent_studio-0.3.0/static/django_agent_studio/js/builder.js.map +0 -1
- django_agent_studio-0.3.0/static/django_agent_studio/js/style.css +0 -1
- django_agent_studio-0.3.0/templates/django_agent_studio/agent_list.html +0 -101
- django_agent_studio-0.3.0/templates/django_agent_studio/base.html +0 -138
- django_agent_studio-0.3.0/templates/django_agent_studio/home.html +0 -135
- django_agent_studio-0.3.0/templates/django_agent_studio/system_list.html +0 -92
- django_agent_studio-0.3.0/templates/django_agent_studio/system_test.html +0 -131
- django_agent_studio-0.3.0/views.py +0 -163
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/LICENSE +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/agents/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/agents/builder.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/api/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/api/permissions.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/apps.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/django_agent_studio.egg-info/dependency_links.txt +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/django_agent_studio.egg-info/requires.txt +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/django_agent_studio.egg-info/top_level.txt +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/management/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/management/commands/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/migrations/0001_initial.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/migrations/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/models/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/models/permissions.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/services/__init__.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/services/permissions.py +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/setup.cfg +0 -0
- {django_agent_studio-0.3.0 → django_agent_studio-0.3.4}/static/agent-frontend/chat-widget-markdown.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-agent-studio
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Visual agent builder and management studio for Django - build custom GPTs with a two-pane interface
|
|
5
5
|
Author: Chris Barry
|
|
6
6
|
License: Business Source License 1.1
|
|
@@ -126,6 +126,9 @@ A visual agent builder and management interface for Django applications. Create,
|
|
|
126
126
|
|
|
127
127
|
| Version | Date | Changes |
|
|
128
128
|
|---------|------|---------|
|
|
129
|
+
| **0.3.3** | 2026-01-30 | **Logout & Scroll Fixes** - Added logout button to header, fixed embedded chat widget scrolling with proper flex layout |
|
|
130
|
+
| **0.3.2** | 2026-01-30 | **Multi-User Access Control** - Collaborator management UI, user search autocomplete, supports both email and username-based User models, system creation from homepage, permission inheritance display |
|
|
131
|
+
| **0.3.1** | 2026-01-30 | **Bug Fixes** - Fixed duplicate message emit in dynamic agents, fixed escapejs encoding in templates, added 800px max-width constraint to system test view |
|
|
129
132
|
| **0.3.0** | 2026-01-29 | **System Listing & Navigation** - Browse and test multi-agent systems from homepage, fixed URL routing for My Systems/My Agents links, system creation now supports optional entry agent |
|
|
130
133
|
| **0.2.0** | 2026-01-28 | **Multi-Agent Systems** - System management UI, shared memory configuration, memory privacy controls, builder tools for managing agent systems |
|
|
131
134
|
| **0.1.9** | 2026-01-27 | Spec documents migration, improved builder agent |
|
|
@@ -6,6 +6,9 @@ A visual agent builder and management interface for Django applications. Create,
|
|
|
6
6
|
|
|
7
7
|
| Version | Date | Changes |
|
|
8
8
|
|---------|------|---------|
|
|
9
|
+
| **0.3.3** | 2026-01-30 | **Logout & Scroll Fixes** - Added logout button to header, fixed embedded chat widget scrolling with proper flex layout |
|
|
10
|
+
| **0.3.2** | 2026-01-30 | **Multi-User Access Control** - Collaborator management UI, user search autocomplete, supports both email and username-based User models, system creation from homepage, permission inheritance display |
|
|
11
|
+
| **0.3.1** | 2026-01-30 | **Bug Fixes** - Fixed duplicate message emit in dynamic agents, fixed escapejs encoding in templates, added 800px max-width constraint to system test view |
|
|
9
12
|
| **0.3.0** | 2026-01-29 | **System Listing & Navigation** - Browse and test multi-agent systems from homepage, fixed URL routing for My Systems/My Agents links, system creation now supports optional entry agent |
|
|
10
13
|
| **0.2.0** | 2026-01-28 | **Multi-Agent Systems** - System management UI, shared memory configuration, memory privacy controls, builder tools for managing agent systems |
|
|
11
14
|
| **0.1.9** | 2026-01-27 | Spec documents migration, improved builder agent |
|
|
@@ -266,9 +266,8 @@ class DynamicAgentRuntime(AgentRuntime):
|
|
|
266
266
|
**model_settings,
|
|
267
267
|
)
|
|
268
268
|
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
await ctx.emit(EventType.ASSISTANT_MESSAGE, {"content": result.final_content})
|
|
269
|
+
# Note: run_agentic_loop already emits ASSISTANT_MESSAGE events,
|
|
270
|
+
# so we don't emit here to avoid duplicate messages in the UI.
|
|
272
271
|
|
|
273
272
|
return RunResult(
|
|
274
273
|
final_output={"response": result.final_content},
|
|
@@ -627,8 +626,13 @@ class DynamicAgentRuntime(AgentRuntime):
|
|
|
627
626
|
return ""
|
|
628
627
|
|
|
629
628
|
def _build_tool_schemas(self, config: dict) -> list:
|
|
630
|
-
"""Build OpenAI-format tool schemas from config.
|
|
629
|
+
"""Build OpenAI-format tool schemas from config.
|
|
630
|
+
|
|
631
|
+
Includes both regular tools and sub-agent tools.
|
|
632
|
+
"""
|
|
631
633
|
schemas = []
|
|
634
|
+
|
|
635
|
+
# Add regular tools
|
|
632
636
|
for tool in config.get("tools", []):
|
|
633
637
|
# Skip the _meta field when building schema
|
|
634
638
|
schema = {
|
|
@@ -636,22 +640,68 @@ class DynamicAgentRuntime(AgentRuntime):
|
|
|
636
640
|
"function": tool.get("function", {}),
|
|
637
641
|
}
|
|
638
642
|
schemas.append(schema)
|
|
643
|
+
|
|
644
|
+
# Add sub-agent tools in OpenAI format
|
|
645
|
+
for sub_tool in config.get("sub_agent_tools", []):
|
|
646
|
+
schema = {
|
|
647
|
+
"type": "function",
|
|
648
|
+
"function": {
|
|
649
|
+
"name": sub_tool.get("name"),
|
|
650
|
+
"description": sub_tool.get("description", ""),
|
|
651
|
+
"parameters": {
|
|
652
|
+
"type": "object",
|
|
653
|
+
"properties": {
|
|
654
|
+
"message": {
|
|
655
|
+
"type": "string",
|
|
656
|
+
"description": f"The message or task to send to the {sub_tool.get('name')} agent",
|
|
657
|
+
},
|
|
658
|
+
"context": {
|
|
659
|
+
"type": "string",
|
|
660
|
+
"description": "Optional additional context to include",
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
"required": ["message"],
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
}
|
|
667
|
+
schemas.append(schema)
|
|
668
|
+
logger.debug(f"Added sub-agent tool schema: {sub_tool.get('name')}")
|
|
669
|
+
|
|
639
670
|
return schemas
|
|
640
671
|
|
|
641
672
|
def _build_tool_map(self, config: dict) -> dict:
|
|
642
|
-
"""Build a map of tool name to execution info.
|
|
673
|
+
"""Build a map of tool name to execution info.
|
|
674
|
+
|
|
675
|
+
Includes both regular tools and sub-agent tools.
|
|
676
|
+
"""
|
|
643
677
|
tool_map = {}
|
|
678
|
+
|
|
679
|
+
# Add regular tools
|
|
644
680
|
for tool in config.get("tools", []):
|
|
645
681
|
func_info = tool.get("function", {})
|
|
646
682
|
tool_name = func_info.get("name")
|
|
647
683
|
if tool_name:
|
|
648
684
|
# Get execution metadata from _meta field
|
|
649
685
|
meta = tool.get("_meta", {})
|
|
686
|
+
# Check both function_path (DynamicTool) and builtin_ref (AgentTool)
|
|
650
687
|
tool_map[tool_name] = {
|
|
651
|
-
"function_path": meta.get("function_path"),
|
|
688
|
+
"function_path": meta.get("function_path") or meta.get("builtin_ref"),
|
|
652
689
|
"tool_id": meta.get("tool_id"),
|
|
653
690
|
"is_dynamic": meta.get("is_dynamic", False),
|
|
691
|
+
"is_sub_agent": False,
|
|
654
692
|
}
|
|
693
|
+
|
|
694
|
+
# Add sub-agent tools
|
|
695
|
+
for sub_tool in config.get("sub_agent_tools", []):
|
|
696
|
+
tool_name = sub_tool.get("name")
|
|
697
|
+
if tool_name:
|
|
698
|
+
tool_map[tool_name] = {
|
|
699
|
+
"is_sub_agent": True,
|
|
700
|
+
"agent_slug": sub_tool.get("agent_slug"),
|
|
701
|
+
"context_mode": sub_tool.get("context_mode", "message_only"),
|
|
702
|
+
}
|
|
703
|
+
logger.debug(f"Added sub-agent tool to map: {tool_name} -> {sub_tool.get('agent_slug')}")
|
|
704
|
+
|
|
655
705
|
return tool_map
|
|
656
706
|
|
|
657
707
|
async def _execute_tool(
|
|
@@ -662,8 +712,22 @@ class DynamicAgentRuntime(AgentRuntime):
|
|
|
662
712
|
executor: "DynamicToolExecutor",
|
|
663
713
|
ctx: RunContext,
|
|
664
714
|
) -> str:
|
|
665
|
-
"""Execute a tool and return its result.
|
|
715
|
+
"""Execute a tool and return its result.
|
|
716
|
+
|
|
717
|
+
Handles both regular tools and sub-agent tools.
|
|
718
|
+
"""
|
|
666
719
|
tool_info = tool_map.get(tool_name, {})
|
|
720
|
+
|
|
721
|
+
# Check if this is a sub-agent tool
|
|
722
|
+
if tool_info.get("is_sub_agent"):
|
|
723
|
+
return await self._execute_sub_agent_tool(
|
|
724
|
+
tool_name=tool_name,
|
|
725
|
+
tool_args=tool_args,
|
|
726
|
+
tool_info=tool_info,
|
|
727
|
+
ctx=ctx,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Regular tool execution
|
|
667
731
|
function_path = tool_info.get("function_path")
|
|
668
732
|
|
|
669
733
|
if not function_path:
|
|
@@ -691,3 +755,98 @@ class DynamicAgentRuntime(AgentRuntime):
|
|
|
691
755
|
logger.exception(f"Error executing tool {tool_name}")
|
|
692
756
|
return json.dumps({"error": str(e)})
|
|
693
757
|
|
|
758
|
+
async def _execute_sub_agent_tool(
|
|
759
|
+
self,
|
|
760
|
+
tool_name: str,
|
|
761
|
+
tool_args: dict,
|
|
762
|
+
tool_info: dict,
|
|
763
|
+
ctx: RunContext,
|
|
764
|
+
) -> str:
|
|
765
|
+
"""Execute a sub-agent tool by invoking the sub-agent.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
tool_name: Name of the sub-agent tool
|
|
769
|
+
tool_args: Arguments passed to the tool (message, context)
|
|
770
|
+
tool_info: Tool metadata including agent_slug and context_mode
|
|
771
|
+
ctx: Parent agent's run context
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
JSON string with the sub-agent's response
|
|
775
|
+
"""
|
|
776
|
+
from agent_runtime_core.multi_agent import (
|
|
777
|
+
AgentTool as AgentToolCore,
|
|
778
|
+
InvocationMode,
|
|
779
|
+
ContextMode,
|
|
780
|
+
invoke_agent,
|
|
781
|
+
)
|
|
782
|
+
from django_agent_runtime.runtime.registry import get_runtime_async
|
|
783
|
+
|
|
784
|
+
sub_agent_slug = tool_info.get("agent_slug")
|
|
785
|
+
context_mode_str = tool_info.get("context_mode", "message_only")
|
|
786
|
+
|
|
787
|
+
# Map context_mode string to enum
|
|
788
|
+
context_mode_map = {
|
|
789
|
+
'message_only': ContextMode.MESSAGE_ONLY,
|
|
790
|
+
'summary': ContextMode.SUMMARY,
|
|
791
|
+
'full': ContextMode.FULL,
|
|
792
|
+
}
|
|
793
|
+
context_mode = context_mode_map.get(context_mode_str, ContextMode.MESSAGE_ONLY)
|
|
794
|
+
|
|
795
|
+
message = tool_args.get("message", "")
|
|
796
|
+
additional_context = tool_args.get("context")
|
|
797
|
+
|
|
798
|
+
if not message:
|
|
799
|
+
return json.dumps({"error": "Missing required parameter: message"})
|
|
800
|
+
|
|
801
|
+
try:
|
|
802
|
+
# Get the sub-agent's runtime
|
|
803
|
+
sub_agent_runtime = await get_runtime_async(sub_agent_slug)
|
|
804
|
+
|
|
805
|
+
# Create an AgentTool wrapper for the sub-agent
|
|
806
|
+
agent_tool = AgentToolCore(
|
|
807
|
+
agent=sub_agent_runtime,
|
|
808
|
+
name=tool_name,
|
|
809
|
+
description=f"Sub-agent: {sub_agent_slug}",
|
|
810
|
+
invocation_mode=InvocationMode.DELEGATE,
|
|
811
|
+
context_mode=context_mode,
|
|
812
|
+
metadata={
|
|
813
|
+
'sub_agent_slug': sub_agent_slug,
|
|
814
|
+
'parent_run_id': str(ctx.run_id) if ctx.run_id else None,
|
|
815
|
+
},
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Get conversation history from parent context
|
|
819
|
+
conversation_history = list(ctx.input_messages)
|
|
820
|
+
|
|
821
|
+
# Invoke the sub-agent
|
|
822
|
+
logger.info(f"Invoking sub-agent '{sub_agent_slug}' via tool '{tool_name}'")
|
|
823
|
+
result = await invoke_agent(
|
|
824
|
+
agent_tool=agent_tool,
|
|
825
|
+
message=message,
|
|
826
|
+
parent_ctx=ctx,
|
|
827
|
+
conversation_history=conversation_history,
|
|
828
|
+
additional_context=additional_context,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
logger.info(f"Sub-agent '{sub_agent_slug}' completed for tool '{tool_name}'")
|
|
832
|
+
|
|
833
|
+
return json.dumps({
|
|
834
|
+
"response": result.response,
|
|
835
|
+
"sub_agent": result.sub_agent_key,
|
|
836
|
+
"handoff": result.handoff,
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
except KeyError as e:
|
|
840
|
+
logger.error(f"Sub-agent not found: {sub_agent_slug} - {e}")
|
|
841
|
+
return json.dumps({
|
|
842
|
+
"error": f"Sub-agent not found: {sub_agent_slug}",
|
|
843
|
+
"tool": tool_name,
|
|
844
|
+
})
|
|
845
|
+
except Exception as e:
|
|
846
|
+
logger.exception(f"Error invoking sub-agent '{sub_agent_slug}'")
|
|
847
|
+
return json.dumps({
|
|
848
|
+
"error": str(e),
|
|
849
|
+
"tool": tool_name,
|
|
850
|
+
"sub_agent": sub_agent_slug,
|
|
851
|
+
})
|
|
852
|
+
|
|
@@ -17,6 +17,10 @@ from django_agent_runtime.models import (
|
|
|
17
17
|
AgentSystemMember,
|
|
18
18
|
AgentSystemVersion,
|
|
19
19
|
AgentSystemSnapshot,
|
|
20
|
+
# Collaborator models
|
|
21
|
+
CollaboratorRole,
|
|
22
|
+
AgentCollaborator,
|
|
23
|
+
SystemCollaborator,
|
|
20
24
|
)
|
|
21
25
|
|
|
22
26
|
|
|
@@ -499,8 +503,8 @@ class AgentSystemVersionSerializer(serializers.ModelSerializer):
|
|
|
499
503
|
class AgentSystemListSerializer(serializers.ModelSerializer):
|
|
500
504
|
"""Serializer for listing AgentSystems."""
|
|
501
505
|
|
|
502
|
-
|
|
503
|
-
|
|
506
|
+
entry_agent = serializers.SerializerMethodField()
|
|
507
|
+
members = AgentSystemMemberSerializer(many=True, read_only=True)
|
|
504
508
|
member_count = serializers.SerializerMethodField()
|
|
505
509
|
active_version = serializers.SerializerMethodField()
|
|
506
510
|
|
|
@@ -512,8 +516,7 @@ class AgentSystemListSerializer(serializers.ModelSerializer):
|
|
|
512
516
|
"name",
|
|
513
517
|
"description",
|
|
514
518
|
"entry_agent",
|
|
515
|
-
"
|
|
516
|
-
"entry_agent_slug",
|
|
519
|
+
"members",
|
|
517
520
|
"is_active",
|
|
518
521
|
"member_count",
|
|
519
522
|
"active_version",
|
|
@@ -522,6 +525,16 @@ class AgentSystemListSerializer(serializers.ModelSerializer):
|
|
|
522
525
|
]
|
|
523
526
|
read_only_fields = ["id", "created_at", "updated_at"]
|
|
524
527
|
|
|
528
|
+
def get_entry_agent(self, obj):
|
|
529
|
+
if obj.entry_agent:
|
|
530
|
+
return {
|
|
531
|
+
"id": str(obj.entry_agent.id),
|
|
532
|
+
"name": obj.entry_agent.name,
|
|
533
|
+
"slug": obj.entry_agent.slug,
|
|
534
|
+
"icon": obj.entry_agent.icon,
|
|
535
|
+
}
|
|
536
|
+
return None
|
|
537
|
+
|
|
525
538
|
def get_member_count(self, obj):
|
|
526
539
|
return obj.members.count()
|
|
527
540
|
|
|
@@ -535,8 +548,6 @@ class AgentSystemListSerializer(serializers.ModelSerializer):
|
|
|
535
548
|
class AgentSystemDetailSerializer(serializers.ModelSerializer):
|
|
536
549
|
"""Serializer for AgentSystem detail view."""
|
|
537
550
|
|
|
538
|
-
entry_agent_name = serializers.CharField(source='entry_agent.name', read_only=True)
|
|
539
|
-
entry_agent_slug = serializers.CharField(source='entry_agent.slug', read_only=True)
|
|
540
551
|
members = AgentSystemMemberSerializer(many=True, read_only=True)
|
|
541
552
|
versions = AgentSystemVersionSerializer(many=True, read_only=True)
|
|
542
553
|
dependency_graph = serializers.SerializerMethodField()
|
|
@@ -549,8 +560,6 @@ class AgentSystemDetailSerializer(serializers.ModelSerializer):
|
|
|
549
560
|
"name",
|
|
550
561
|
"description",
|
|
551
562
|
"entry_agent",
|
|
552
|
-
"entry_agent_name",
|
|
553
|
-
"entry_agent_slug",
|
|
554
563
|
"is_active",
|
|
555
564
|
"members",
|
|
556
565
|
"versions",
|
|
@@ -563,6 +572,20 @@ class AgentSystemDetailSerializer(serializers.ModelSerializer):
|
|
|
563
572
|
def get_dependency_graph(self, obj):
|
|
564
573
|
return obj.get_dependency_graph()
|
|
565
574
|
|
|
575
|
+
def to_representation(self, instance):
|
|
576
|
+
"""Return entry_agent as an object for reading."""
|
|
577
|
+
data = super().to_representation(instance)
|
|
578
|
+
if instance.entry_agent:
|
|
579
|
+
data['entry_agent'] = {
|
|
580
|
+
"id": str(instance.entry_agent.id),
|
|
581
|
+
"name": instance.entry_agent.name,
|
|
582
|
+
"slug": instance.entry_agent.slug,
|
|
583
|
+
"icon": instance.entry_agent.icon,
|
|
584
|
+
}
|
|
585
|
+
else:
|
|
586
|
+
data['entry_agent'] = None
|
|
587
|
+
return data
|
|
588
|
+
|
|
566
589
|
|
|
567
590
|
class AgentSystemCreateSerializer(serializers.Serializer):
|
|
568
591
|
"""Serializer for creating an AgentSystem."""
|
|
@@ -621,3 +644,138 @@ class PublishVersionSerializer(serializers.Serializer):
|
|
|
621
644
|
default=False,
|
|
622
645
|
help_text="Whether to make this the active version immediately",
|
|
623
646
|
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# =============================================================================
|
|
650
|
+
# Collaborator Serializers for Multi-User Access Control
|
|
651
|
+
# =============================================================================
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class AgentCollaboratorSerializer(serializers.ModelSerializer):
|
|
655
|
+
"""Serializer for AgentCollaborator."""
|
|
656
|
+
|
|
657
|
+
user_email = serializers.SerializerMethodField()
|
|
658
|
+
user_name = serializers.SerializerMethodField()
|
|
659
|
+
added_by_email = serializers.SerializerMethodField()
|
|
660
|
+
role_display = serializers.CharField(source='get_role_display', read_only=True)
|
|
661
|
+
can_view = serializers.BooleanField(read_only=True)
|
|
662
|
+
can_edit = serializers.BooleanField(read_only=True)
|
|
663
|
+
can_admin = serializers.BooleanField(read_only=True)
|
|
664
|
+
|
|
665
|
+
class Meta:
|
|
666
|
+
model = AgentCollaborator
|
|
667
|
+
fields = [
|
|
668
|
+
"id",
|
|
669
|
+
"agent",
|
|
670
|
+
"user",
|
|
671
|
+
"user_email",
|
|
672
|
+
"user_name",
|
|
673
|
+
"role",
|
|
674
|
+
"role_display",
|
|
675
|
+
"can_view",
|
|
676
|
+
"can_edit",
|
|
677
|
+
"can_admin",
|
|
678
|
+
"added_by",
|
|
679
|
+
"added_by_email",
|
|
680
|
+
"added_at",
|
|
681
|
+
"updated_at",
|
|
682
|
+
]
|
|
683
|
+
read_only_fields = ["id", "added_by", "added_at", "updated_at"]
|
|
684
|
+
|
|
685
|
+
def get_user_email(self, obj):
|
|
686
|
+
"""Return email or username as identifier."""
|
|
687
|
+
if obj.user:
|
|
688
|
+
return obj.user.email or getattr(obj.user, 'username', None) or str(obj.user)
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
def get_user_name(self, obj):
|
|
692
|
+
"""Return full name, or fall back to email/username."""
|
|
693
|
+
if obj.user:
|
|
694
|
+
if hasattr(obj.user, 'get_full_name'):
|
|
695
|
+
name = obj.user.get_full_name()
|
|
696
|
+
if name:
|
|
697
|
+
return name
|
|
698
|
+
return obj.user.email or getattr(obj.user, 'username', None) or str(obj.user)
|
|
699
|
+
return None
|
|
700
|
+
|
|
701
|
+
def get_added_by_email(self, obj):
|
|
702
|
+
"""Return added_by email or username."""
|
|
703
|
+
if obj.added_by:
|
|
704
|
+
return obj.added_by.email or getattr(obj.added_by, 'username', None) or str(obj.added_by)
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
class SystemCollaboratorSerializer(serializers.ModelSerializer):
|
|
709
|
+
"""Serializer for SystemCollaborator."""
|
|
710
|
+
|
|
711
|
+
user_email = serializers.SerializerMethodField()
|
|
712
|
+
user_name = serializers.SerializerMethodField()
|
|
713
|
+
added_by_email = serializers.SerializerMethodField()
|
|
714
|
+
role_display = serializers.CharField(source='get_role_display', read_only=True)
|
|
715
|
+
can_view = serializers.BooleanField(read_only=True)
|
|
716
|
+
can_edit = serializers.BooleanField(read_only=True)
|
|
717
|
+
can_admin = serializers.BooleanField(read_only=True)
|
|
718
|
+
|
|
719
|
+
class Meta:
|
|
720
|
+
model = SystemCollaborator
|
|
721
|
+
fields = [
|
|
722
|
+
"id",
|
|
723
|
+
"system",
|
|
724
|
+
"user",
|
|
725
|
+
"user_email",
|
|
726
|
+
"user_name",
|
|
727
|
+
"role",
|
|
728
|
+
"role_display",
|
|
729
|
+
"can_view",
|
|
730
|
+
"can_edit",
|
|
731
|
+
"can_admin",
|
|
732
|
+
"added_by",
|
|
733
|
+
"added_by_email",
|
|
734
|
+
"added_at",
|
|
735
|
+
"updated_at",
|
|
736
|
+
]
|
|
737
|
+
read_only_fields = ["id", "added_by", "added_at", "updated_at"]
|
|
738
|
+
|
|
739
|
+
def get_user_email(self, obj):
|
|
740
|
+
"""Return email or username as identifier."""
|
|
741
|
+
if obj.user:
|
|
742
|
+
return obj.user.email or getattr(obj.user, 'username', None) or str(obj.user)
|
|
743
|
+
return None
|
|
744
|
+
|
|
745
|
+
def get_user_name(self, obj):
|
|
746
|
+
"""Return full name, or fall back to email/username."""
|
|
747
|
+
if obj.user:
|
|
748
|
+
if hasattr(obj.user, 'get_full_name'):
|
|
749
|
+
name = obj.user.get_full_name()
|
|
750
|
+
if name:
|
|
751
|
+
return name
|
|
752
|
+
return obj.user.email or getattr(obj.user, 'username', None) or str(obj.user)
|
|
753
|
+
return None
|
|
754
|
+
|
|
755
|
+
def get_added_by_email(self, obj):
|
|
756
|
+
"""Return added_by email or username."""
|
|
757
|
+
if obj.added_by:
|
|
758
|
+
return obj.added_by.email or getattr(obj.added_by, 'username', None) or str(obj.added_by)
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class AddCollaboratorSerializer(serializers.Serializer):
|
|
763
|
+
"""Serializer for adding a collaborator by email or username."""
|
|
764
|
+
|
|
765
|
+
email = serializers.CharField(
|
|
766
|
+
help_text="Email or username of the user to add as collaborator"
|
|
767
|
+
)
|
|
768
|
+
role = serializers.ChoiceField(
|
|
769
|
+
choices=CollaboratorRole.choices,
|
|
770
|
+
default=CollaboratorRole.VIEWER,
|
|
771
|
+
help_text="Role to grant: viewer, editor, or admin",
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class UpdateCollaboratorRoleSerializer(serializers.Serializer):
|
|
776
|
+
"""Serializer for updating a collaborator's role."""
|
|
777
|
+
|
|
778
|
+
role = serializers.ChoiceField(
|
|
779
|
+
choices=CollaboratorRole.choices,
|
|
780
|
+
help_text="New role: viewer, editor, or admin",
|
|
781
|
+
)
|
|
@@ -300,4 +300,39 @@ urlpatterns = [
|
|
|
300
300
|
views.AgentSpecDocumentView.as_view(),
|
|
301
301
|
name="agent_spec_document",
|
|
302
302
|
),
|
|
303
|
+
|
|
304
|
+
# ==========================================================================
|
|
305
|
+
# Collaborator Management Endpoints
|
|
306
|
+
# ==========================================================================
|
|
307
|
+
|
|
308
|
+
# Agent collaborators
|
|
309
|
+
path(
|
|
310
|
+
"agents/<uuid:agent_id>/collaborators/",
|
|
311
|
+
views.AgentCollaboratorListCreateView.as_view(),
|
|
312
|
+
name="agent_collaborator_list",
|
|
313
|
+
),
|
|
314
|
+
path(
|
|
315
|
+
"agents/<uuid:agent_id>/collaborators/<uuid:pk>/",
|
|
316
|
+
views.AgentCollaboratorDetailView.as_view(),
|
|
317
|
+
name="agent_collaborator_detail",
|
|
318
|
+
),
|
|
319
|
+
|
|
320
|
+
# System collaborators
|
|
321
|
+
path(
|
|
322
|
+
"systems/<uuid:system_id>/collaborators/",
|
|
323
|
+
views.SystemCollaboratorListCreateView.as_view(),
|
|
324
|
+
name="system_collaborator_list",
|
|
325
|
+
),
|
|
326
|
+
path(
|
|
327
|
+
"systems/<uuid:system_id>/collaborators/<uuid:pk>/",
|
|
328
|
+
views.SystemCollaboratorDetailView.as_view(),
|
|
329
|
+
name="system_collaborator_detail",
|
|
330
|
+
),
|
|
331
|
+
|
|
332
|
+
# User search for collaborator autocomplete
|
|
333
|
+
path(
|
|
334
|
+
"users/search/",
|
|
335
|
+
views.UserSearchView.as_view(),
|
|
336
|
+
name="user_search",
|
|
337
|
+
),
|
|
303
338
|
]
|