django-agent-studio 0.1.0__py3-none-any.whl → 0.1.5__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,1994 @@
1
+ """
2
+ BuilderAgentRuntime - The agent that helps users build and customize other agents.
3
+
4
+ This is the "right pane" agent in the studio interface that guides users
5
+ through creating and modifying their custom agents.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from typing import Optional
11
+
12
+ from asgiref.sync import sync_to_async
13
+ from agent_runtime_core.registry import AgentRuntime
14
+ from agent_runtime_core.interfaces import RunContext, RunResult, EventType
15
+ from agent_runtime_core.agentic_loop import run_agentic_loop
16
+ from django_agent_runtime.runtime.llm import get_llm_client_for_model, DEFAULT_MODEL
17
+ from django_agent_runtime.models import AgentDefinition, AgentVersion, AgentRevision
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ async def create_revision(agent: AgentDefinition, comment: str = "", user=None) -> AgentRevision:
23
+ """
24
+ Create a new revision for an agent after a change.
25
+
26
+ This should be called after any modification to the agent's configuration,
27
+ tools, or knowledge sources.
28
+ """
29
+ return await sync_to_async(AgentRevision.create_from_agent)(agent, comment=comment, user=user)
30
+
31
+ BUILDER_SYSTEM_PROMPT = """You are an AI Agent Builder assistant. Your role is to help users create and customize their own AI agents (similar to custom GPTs).
32
+
33
+ You have access to tools that allow you to:
34
+ 1. Create new agents
35
+ 2. Update agent configurations (system prompts, models, settings)
36
+ 3. Add/remove/modify tools for agents
37
+ 4. Add/remove/modify knowledge sources for agents
38
+ 5. View the current agent configuration
39
+ 6. **Discover and add dynamic tools** from the Django project codebase
40
+ 7. **Create multi-agent systems** by adding sub-agent tools
41
+ 8. **Switch between agents and systems** in the UI
42
+ 9. **Configure agent memory** (enable/disable the remember tool)
43
+
44
+ ## IMPORTANT: Tool Usage
45
+
46
+ When calling tools, you MUST provide all required parameters. For example:
47
+ - `update_system_prompt` requires the `system_prompt` parameter with the FULL new prompt text
48
+ - `create_agent` requires the `name` parameter
49
+
50
+ Example of correct tool usage:
51
+ ```
52
+ update_system_prompt({{"system_prompt": "You are a helpful pirate assistant. Respond to all questions in pirate speak, using 'Arrr', 'matey', 'ye', etc."}})
53
+ ```
54
+
55
+ ## Dynamic Tool Discovery
56
+
57
+ You can scan the Django project to discover functions that can be turned into tools for agents:
58
+ - Use `scan_project_for_tools` to discover available functions in the codebase
59
+ - Use `list_discovered_functions` to browse and filter discovered functions
60
+ - Use `get_function_details` to see full details about a specific function
61
+ - Use `add_tool_from_function` to add a discovered function as a tool
62
+ - Use `list_agent_tools` to see all tools currently assigned to the agent
63
+ - Use `remove_tool` to remove a tool from the agent
64
+
65
+ ## Multi-Agent Systems (Sub-Agents)
66
+
67
+ You can create multi-agent systems where one agent delegates to specialized sub-agents:
68
+ - Use `list_available_agents` to see agents that can be used as sub-agents
69
+ - Use `add_sub_agent_tool` to add another agent as a sub-agent tool
70
+ - Use `list_sub_agent_tools` to see current sub-agent tools
71
+ - Use `update_sub_agent_tool` to modify a sub-agent tool's configuration
72
+ - Use `remove_sub_agent_tool` to remove a sub-agent tool
73
+
74
+ **Example multi-agent setup:**
75
+ A "Customer Support Triage" agent might have sub-agent tools for:
76
+ - `billing_specialist`: Handles billing questions, refunds, invoices
77
+ - `technical_specialist`: Handles technical issues, bugs, how-to questions
78
+
79
+ When adding sub-agent tools:
80
+ 1. First create the specialist agents with appropriate system prompts
81
+ 2. Then add them as sub-agent tools to the triage/coordinator agent
82
+ 3. Choose the right context_mode:
83
+ - `message_only` (default): Just pass the task message - best for focused delegation
84
+ - `summary`: Pass a brief context summary - good for context-aware responses
85
+ - `full`: Pass entire conversation - use sparingly, can be verbose
86
+
87
+ ## UI Control - Switching Agents and Systems
88
+
89
+ You can control the builder UI to switch between different agents and systems:
90
+ - Use `list_all_agents` to see all available agents
91
+ - Use `switch_to_agent` to switch the UI to edit a different agent
92
+ - Use `list_all_systems` to see all multi-agent systems
93
+ - Use `switch_to_system` to select a system in the UI
94
+ - Use `get_system_details` to see detailed info about a system
95
+
96
+ When the user asks to work on a different agent or system, use these tools to switch context.
97
+ The UI will automatically update to show the selected agent/system.
98
+
99
+ ## Agent Memory
100
+
101
+ Agents have a built-in memory system that allows them to remember facts about users across conversations:
102
+ - Memory is **enabled by default** for all agents
103
+ - When enabled, the agent has a `remember` tool it can use to store key-value facts
104
+ - Memories are scoped per-user and per-conversation
105
+ - Memory only works for **authenticated users** (not anonymous visitors)
106
+ - Use `get_memory_status` to check if memory is enabled
107
+ - Use `set_memory_enabled` to enable or disable memory
108
+
109
+ **When to disable memory:**
110
+ - Public-facing agents where you don't want user data stored
111
+ - Simple Q&A agents that don't need personalization
112
+ - Agents handling sensitive information that shouldn't be persisted
113
+
114
+ **When to keep memory enabled (default):**
115
+ - Personal assistants that should remember user preferences
116
+ - Support agents that benefit from knowing user history
117
+ - Any agent where personalization improves the experience
118
+
119
+ When helping users:
120
+ - Ask clarifying questions to understand what they want their agent to do
121
+ - Suggest appropriate system prompts based on the agent's purpose
122
+ - Recommend tools that would be useful for the agent's tasks
123
+ - Help them add relevant knowledge/context
124
+ - Explain the impact of different model choices and settings
125
+ - **Proactively suggest scanning for tools** when users describe functionality that might exist in their codebase
126
+ - **Suggest multi-agent patterns** when the user describes complex workflows that could benefit from specialized agents
127
+ - **Offer to switch agents** when the user mentions wanting to work on a different agent
128
+
129
+ Be conversational and helpful. Guide users through the process step by step.
130
+
131
+ ## CRITICAL: Distinguish Instructions from Examples
132
+
133
+ **Before taking action, carefully determine whether the user is:**
134
+ 1. **Giving you instructions** to modify agents
135
+ 2. **Showing you example output** from an agent conversation
136
+ 3. **Describing a problem** they observed and want to discuss
137
+
138
+ **Signs that text is EXAMPLE OUTPUT (not instructions):**
139
+ - Quoted conversation transcripts
140
+ - Text that reads like an agent responding to a user
141
+ - Phrases like "this was the output", "the agent said", "here's what happened"
142
+ - Multiple back-and-forth exchanges between user/assistant roles
143
+
144
+ **When you see example output or conversation transcripts:**
145
+ - Do NOT immediately start modifying agents based on the content
146
+ - ASK the user what they want you to do with this information
147
+ - Example: "I see you've shared a conversation from the S'Ai system. What would you like me to help with? Are you asking me to modify how these agents behave, or are you describing an issue you'd like to discuss?"
148
+
149
+ ## CRITICAL: Confirm Before Bulk Changes
150
+
151
+ **Before making changes to multiple agents:**
152
+ - Summarize what you're about to do
153
+ - List which agents will be affected
154
+ - Ask for explicit confirmation
155
+ - Example: "I understand you want to update 5 agents to present as a unified system. This will modify: stage-assessor, state-assessor, epistemic-validator, integration-planner, and guardrail-monitor. Should I proceed?"
156
+
157
+ **Be conservative with ambiguous requests.** When in doubt, ask for clarification rather than taking action.
158
+
159
+ ## IMPORTANT: Always End with a Summary
160
+
161
+ After completing any actions (creating agents, updating prompts, adding tools, etc.), ALWAYS end your response with a brief summary of what you did. For example:
162
+ - "I've created the 'Customer Support' agent with a helpful assistant prompt and added the search_orders tool."
163
+ - "I've updated the system prompt to make the agent respond in pirate speak."
164
+ - "I've added 3 sub-agent tools to the Orchestrator: billing_specialist, tech_support, and returns_handler."
165
+
166
+ This helps users understand what changes were made without having to expand the tool results.
167
+
168
+ Current agent being edited: {agent_context}
169
+ """
170
+
171
+ BUILDER_TOOLS = [
172
+ {
173
+ "type": "function",
174
+ "function": {
175
+ "name": "create_agent",
176
+ "description": "Create a new agent with the given name and description. Use this when no agent is selected or when the user wants to create a new agent.",
177
+ "parameters": {
178
+ "type": "object",
179
+ "properties": {
180
+ "name": {
181
+ "type": "string",
182
+ "description": "The name for the new agent (e.g., 'Tony', 'Customer Support Bot')",
183
+ },
184
+ "description": {
185
+ "type": "string",
186
+ "description": "A brief description of what the agent does",
187
+ },
188
+ "system_prompt": {
189
+ "type": "string",
190
+ "description": "Optional initial system prompt. Can be updated later.",
191
+ },
192
+ },
193
+ "required": ["name"],
194
+ },
195
+ },
196
+ },
197
+ {
198
+ "type": "function",
199
+ "function": {
200
+ "name": "update_system_prompt",
201
+ "description": "Update the agent's system prompt. This defines the agent's personality, role, and behavior.",
202
+ "parameters": {
203
+ "type": "object",
204
+ "properties": {
205
+ "system_prompt": {
206
+ "type": "string",
207
+ "description": "The new system prompt for the agent",
208
+ },
209
+ },
210
+ "required": ["system_prompt"],
211
+ },
212
+ },
213
+ },
214
+ {
215
+ "type": "function",
216
+ "function": {
217
+ "name": "update_agent_name",
218
+ "description": "Update the agent's name and description.",
219
+ "parameters": {
220
+ "type": "object",
221
+ "properties": {
222
+ "name": {
223
+ "type": "string",
224
+ "description": "The new name for the agent",
225
+ },
226
+ "description": {
227
+ "type": "string",
228
+ "description": "A brief description of what the agent does",
229
+ },
230
+ },
231
+ "required": ["name"],
232
+ },
233
+ },
234
+ },
235
+ {
236
+ "type": "function",
237
+ "function": {
238
+ "name": "update_model_settings",
239
+ "description": "Update the agent's model and settings (temperature, etc).",
240
+ "parameters": {
241
+ "type": "object",
242
+ "properties": {
243
+ "model": {
244
+ "type": "string",
245
+ "description": "The LLM model to use (e.g., 'gpt-4o', 'gpt-4o-mini', 'claude-3-opus')",
246
+ },
247
+ "temperature": {
248
+ "type": "number",
249
+ "description": "Temperature setting (0-2). Lower = more focused, higher = more creative",
250
+ },
251
+ },
252
+ },
253
+ },
254
+ },
255
+ {
256
+ "type": "function",
257
+ "function": {
258
+ "name": "set_memory_enabled",
259
+ "description": "Enable or disable conversation memory for the agent. When enabled (default), the agent can remember facts about users across messages using the 'remember' tool. Memory only works for authenticated users.",
260
+ "parameters": {
261
+ "type": "object",
262
+ "properties": {
263
+ "enabled": {
264
+ "type": "boolean",
265
+ "description": "Whether to enable memory. True = agent can remember facts, False = no memory.",
266
+ },
267
+ },
268
+ "required": ["enabled"],
269
+ },
270
+ },
271
+ },
272
+ {
273
+ "type": "function",
274
+ "function": {
275
+ "name": "get_memory_status",
276
+ "description": "Check if memory is enabled for the current agent.",
277
+ "parameters": {
278
+ "type": "object",
279
+ "properties": {},
280
+ },
281
+ },
282
+ },
283
+ {
284
+ "type": "function",
285
+ "function": {
286
+ "name": "add_knowledge",
287
+ "description": "Add a knowledge source to the agent. Use inclusion_mode='always' for small context that should always be included, or 'rag' for larger documents that should be searched.",
288
+ "parameters": {
289
+ "type": "object",
290
+ "properties": {
291
+ "name": {
292
+ "type": "string",
293
+ "description": "Name/title for this knowledge source",
294
+ },
295
+ "content": {
296
+ "type": "string",
297
+ "description": "The knowledge content (text)",
298
+ },
299
+ "inclusion_mode": {
300
+ "type": "string",
301
+ "enum": ["always", "rag"],
302
+ "description": "How to include this knowledge: 'always' (in every prompt) or 'rag' (retrieved based on relevance). Default: 'always'",
303
+ },
304
+ },
305
+ "required": ["name", "content"],
306
+ },
307
+ },
308
+ },
309
+ {
310
+ "type": "function",
311
+ "function": {
312
+ "name": "index_knowledge",
313
+ "description": "Index RAG knowledge sources for similarity search. Call this after adding knowledge with inclusion_mode='rag'.",
314
+ "parameters": {
315
+ "type": "object",
316
+ "properties": {
317
+ "knowledge_id": {
318
+ "type": "string",
319
+ "description": "Optional: specific knowledge ID to index. If not provided, indexes all pending RAG knowledge.",
320
+ },
321
+ "force": {
322
+ "type": "boolean",
323
+ "description": "Force re-indexing even if already indexed. Default: false",
324
+ },
325
+ },
326
+ },
327
+ },
328
+ },
329
+ {
330
+ "type": "function",
331
+ "function": {
332
+ "name": "get_rag_status",
333
+ "description": "Get the indexing status of RAG knowledge sources for the agent.",
334
+ "parameters": {
335
+ "type": "object",
336
+ "properties": {},
337
+ },
338
+ },
339
+ },
340
+ {
341
+ "type": "function",
342
+ "function": {
343
+ "name": "preview_rag_search",
344
+ "description": "Preview what knowledge would be retrieved for a given query. Useful for testing RAG configuration.",
345
+ "parameters": {
346
+ "type": "object",
347
+ "properties": {
348
+ "query": {
349
+ "type": "string",
350
+ "description": "The search query to test",
351
+ },
352
+ "top_k": {
353
+ "type": "integer",
354
+ "description": "Number of results to return. Default: 5",
355
+ },
356
+ },
357
+ "required": ["query"],
358
+ },
359
+ },
360
+ },
361
+ {
362
+ "type": "function",
363
+ "function": {
364
+ "name": "update_rag_config",
365
+ "description": "Update the RAG configuration for the agent (top_k, similarity_threshold, etc).",
366
+ "parameters": {
367
+ "type": "object",
368
+ "properties": {
369
+ "enabled": {
370
+ "type": "boolean",
371
+ "description": "Enable or disable RAG for this agent",
372
+ },
373
+ "top_k": {
374
+ "type": "integer",
375
+ "description": "Number of chunks to retrieve. Default: 5",
376
+ },
377
+ "similarity_threshold": {
378
+ "type": "number",
379
+ "description": "Minimum similarity score (0-1). Default: 0.7",
380
+ },
381
+ "chunk_size": {
382
+ "type": "integer",
383
+ "description": "Size of text chunks in characters. Default: 500",
384
+ },
385
+ "chunk_overlap": {
386
+ "type": "integer",
387
+ "description": "Overlap between chunks. Default: 50",
388
+ },
389
+ },
390
+ },
391
+ },
392
+ },
393
+ {
394
+ "type": "function",
395
+ "function": {
396
+ "name": "get_current_config",
397
+ "description": "Get the current configuration of the agent being edited.",
398
+ "parameters": {
399
+ "type": "object",
400
+ "properties": {},
401
+ },
402
+ },
403
+ },
404
+ # Dynamic Tool Inspector tools
405
+ {
406
+ "type": "function",
407
+ "function": {
408
+ "name": "scan_project_for_tools",
409
+ "description": "Scan the Django project to discover functions that can be used as tools. Returns a list of discovered functions with their signatures and descriptions.",
410
+ "parameters": {
411
+ "type": "object",
412
+ "properties": {
413
+ "app_filter": {
414
+ "type": "array",
415
+ "items": {"type": "string"},
416
+ "description": "Optional list of app names to scan. If not provided, scans all project apps.",
417
+ },
418
+ "include_private": {
419
+ "type": "boolean",
420
+ "description": "Whether to include private functions (starting with _). Default: false",
421
+ },
422
+ "directory": {
423
+ "type": "string",
424
+ "description": "Optional specific directory to scan instead of Django apps.",
425
+ },
426
+ },
427
+ },
428
+ },
429
+ },
430
+ {
431
+ "type": "function",
432
+ "function": {
433
+ "name": "list_discovered_functions",
434
+ "description": "List functions that were discovered from the most recent scan. Can filter by type, module, or side effects.",
435
+ "parameters": {
436
+ "type": "object",
437
+ "properties": {
438
+ "function_type": {
439
+ "type": "string",
440
+ "enum": ["function", "method", "view", "model_method", "manager_method", "utility"],
441
+ "description": "Filter by function type",
442
+ },
443
+ "module_filter": {
444
+ "type": "string",
445
+ "description": "Filter by module path (partial match)",
446
+ },
447
+ "safe_only": {
448
+ "type": "boolean",
449
+ "description": "Only show functions without side effects",
450
+ },
451
+ "limit": {
452
+ "type": "integer",
453
+ "description": "Maximum number of results to return. Default: 50",
454
+ },
455
+ },
456
+ },
457
+ },
458
+ },
459
+ {
460
+ "type": "function",
461
+ "function": {
462
+ "name": "add_tool_from_function",
463
+ "description": "Add a discovered function as a dynamic tool to the agent. The function will be callable by the agent at runtime.",
464
+ "parameters": {
465
+ "type": "object",
466
+ "properties": {
467
+ "function_path": {
468
+ "type": "string",
469
+ "description": "Full import path to the function (e.g., 'myapp.utils.calculate_tax')",
470
+ },
471
+ "tool_name": {
472
+ "type": "string",
473
+ "description": "Optional custom name for the tool. If not provided, derives from function name.",
474
+ },
475
+ "description": {
476
+ "type": "string",
477
+ "description": "Optional custom description. If not provided, uses function docstring.",
478
+ },
479
+ "requires_confirmation": {
480
+ "type": "boolean",
481
+ "description": "Whether to require user confirmation before execution. Default: true for functions with side effects.",
482
+ },
483
+ },
484
+ "required": ["function_path"],
485
+ },
486
+ },
487
+ },
488
+ {
489
+ "type": "function",
490
+ "function": {
491
+ "name": "list_agent_tools",
492
+ "description": "List all tools currently assigned to the agent, including both static and dynamic tools.",
493
+ "parameters": {
494
+ "type": "object",
495
+ "properties": {
496
+ "include_inactive": {
497
+ "type": "boolean",
498
+ "description": "Whether to include inactive tools. Default: false",
499
+ },
500
+ },
501
+ },
502
+ },
503
+ },
504
+ {
505
+ "type": "function",
506
+ "function": {
507
+ "name": "remove_tool",
508
+ "description": "Remove a tool from the agent.",
509
+ "parameters": {
510
+ "type": "object",
511
+ "properties": {
512
+ "tool_name": {
513
+ "type": "string",
514
+ "description": "Name of the tool to remove",
515
+ },
516
+ "tool_id": {
517
+ "type": "string",
518
+ "description": "UUID of the tool to remove (alternative to tool_name)",
519
+ },
520
+ },
521
+ },
522
+ },
523
+ },
524
+ {
525
+ "type": "function",
526
+ "function": {
527
+ "name": "get_function_details",
528
+ "description": "Get detailed information about a specific discovered function, including its full signature, docstring, and parameters.",
529
+ "parameters": {
530
+ "type": "object",
531
+ "properties": {
532
+ "function_path": {
533
+ "type": "string",
534
+ "description": "Full import path to the function (e.g., 'myapp.utils.calculate_tax')",
535
+ },
536
+ },
537
+ "required": ["function_path"],
538
+ },
539
+ },
540
+ },
541
+ {
542
+ "type": "function",
543
+ "function": {
544
+ "name": "list_revisions",
545
+ "description": "List all revisions (version history) for the agent. Shows when changes were made and what changed.",
546
+ "parameters": {
547
+ "type": "object",
548
+ "properties": {
549
+ "limit": {
550
+ "type": "integer",
551
+ "description": "Maximum number of revisions to return. Default: 20",
552
+ },
553
+ },
554
+ },
555
+ },
556
+ },
557
+ {
558
+ "type": "function",
559
+ "function": {
560
+ "name": "get_revision",
561
+ "description": "Get the full configuration snapshot from a specific revision.",
562
+ "parameters": {
563
+ "type": "object",
564
+ "properties": {
565
+ "revision_number": {
566
+ "type": "integer",
567
+ "description": "The revision number to retrieve",
568
+ },
569
+ },
570
+ "required": ["revision_number"],
571
+ },
572
+ },
573
+ },
574
+ {
575
+ "type": "function",
576
+ "function": {
577
+ "name": "restore_revision",
578
+ "description": "Restore the agent to a previous revision. This creates a new revision with the old configuration.",
579
+ "parameters": {
580
+ "type": "object",
581
+ "properties": {
582
+ "revision_number": {
583
+ "type": "integer",
584
+ "description": "The revision number to restore",
585
+ },
586
+ },
587
+ "required": ["revision_number"],
588
+ },
589
+ },
590
+ },
591
+ # ==========================================================================
592
+ # Multi-Agent / Sub-Agent Tools
593
+ # ==========================================================================
594
+ {
595
+ "type": "function",
596
+ "function": {
597
+ "name": "list_available_agents",
598
+ "description": "List all available agents that can be used as sub-agents. Returns agents that the current agent can delegate to.",
599
+ "parameters": {
600
+ "type": "object",
601
+ "properties": {
602
+ "include_inactive": {
603
+ "type": "boolean",
604
+ "description": "Whether to include inactive agents. Default: false",
605
+ },
606
+ "search": {
607
+ "type": "string",
608
+ "description": "Optional search term to filter agents by name or description",
609
+ },
610
+ },
611
+ },
612
+ },
613
+ },
614
+ {
615
+ "type": "function",
616
+ "function": {
617
+ "name": "add_sub_agent_tool",
618
+ "description": "Add another agent as a sub-agent tool. This allows the current agent to delegate tasks to the sub-agent. Requires CREATOR or ADMIN permission level.",
619
+ "parameters": {
620
+ "type": "object",
621
+ "properties": {
622
+ "sub_agent_slug": {
623
+ "type": "string",
624
+ "description": "The slug of the agent to add as a sub-agent (e.g., 'billing-specialist')",
625
+ },
626
+ "tool_name": {
627
+ "type": "string",
628
+ "description": "The name for this sub-agent tool (e.g., 'billing_specialist'). Should be snake_case.",
629
+ },
630
+ "description": {
631
+ "type": "string",
632
+ "description": "Description of when to use this sub-agent (e.g., 'Consult for billing and payment questions')",
633
+ },
634
+ "context_mode": {
635
+ "type": "string",
636
+ "enum": ["message_only", "summary", "full"],
637
+ "description": "How much context to pass to the sub-agent. 'message_only' (default): just the task message. 'summary': brief context summary. 'full': entire conversation history.",
638
+ },
639
+ },
640
+ "required": ["sub_agent_slug", "tool_name", "description"],
641
+ },
642
+ },
643
+ },
644
+ {
645
+ "type": "function",
646
+ "function": {
647
+ "name": "list_sub_agent_tools",
648
+ "description": "List all sub-agent tools configured for the current agent.",
649
+ "parameters": {
650
+ "type": "object",
651
+ "properties": {},
652
+ },
653
+ },
654
+ },
655
+ {
656
+ "type": "function",
657
+ "function": {
658
+ "name": "remove_sub_agent_tool",
659
+ "description": "Remove a sub-agent tool from the current agent. Requires CREATOR or ADMIN permission level.",
660
+ "parameters": {
661
+ "type": "object",
662
+ "properties": {
663
+ "tool_name": {
664
+ "type": "string",
665
+ "description": "The name of the sub-agent tool to remove",
666
+ },
667
+ },
668
+ "required": ["tool_name"],
669
+ },
670
+ },
671
+ },
672
+ {
673
+ "type": "function",
674
+ "function": {
675
+ "name": "update_sub_agent_tool",
676
+ "description": "Update a sub-agent tool's configuration (description, context_mode). Requires CREATOR or ADMIN permission level.",
677
+ "parameters": {
678
+ "type": "object",
679
+ "properties": {
680
+ "tool_name": {
681
+ "type": "string",
682
+ "description": "The name of the sub-agent tool to update",
683
+ },
684
+ "description": {
685
+ "type": "string",
686
+ "description": "New description for the sub-agent tool",
687
+ },
688
+ "context_mode": {
689
+ "type": "string",
690
+ "enum": ["message_only", "summary", "full"],
691
+ "description": "New context mode for the sub-agent",
692
+ },
693
+ },
694
+ "required": ["tool_name"],
695
+ },
696
+ },
697
+ },
698
+ # ==========================================================================
699
+ # UI Control Tools - Allow builder to switch agents/systems in the UI
700
+ # ==========================================================================
701
+ {
702
+ "type": "function",
703
+ "function": {
704
+ "name": "switch_to_agent",
705
+ "description": "Switch the UI to edit a different agent. This changes which agent is being edited in the builder interface.",
706
+ "parameters": {
707
+ "type": "object",
708
+ "properties": {
709
+ "agent_slug": {
710
+ "type": "string",
711
+ "description": "The slug of the agent to switch to (e.g., 'billing-specialist')",
712
+ },
713
+ "agent_id": {
714
+ "type": "string",
715
+ "description": "The UUID of the agent to switch to (alternative to slug)",
716
+ },
717
+ },
718
+ },
719
+ },
720
+ },
721
+ {
722
+ "type": "function",
723
+ "function": {
724
+ "name": "switch_to_system",
725
+ "description": "Switch the UI to work with a multi-agent system. This selects a system in the builder interface.",
726
+ "parameters": {
727
+ "type": "object",
728
+ "properties": {
729
+ "system_slug": {
730
+ "type": "string",
731
+ "description": "The slug of the system to switch to (e.g., 'customer-support')",
732
+ },
733
+ "system_id": {
734
+ "type": "string",
735
+ "description": "The UUID of the system to switch to (alternative to slug)",
736
+ },
737
+ },
738
+ },
739
+ },
740
+ },
741
+ {
742
+ "type": "function",
743
+ "function": {
744
+ "name": "list_all_agents",
745
+ "description": "List all available agents in the system. Use this to find agents to switch to or to see what agents exist.",
746
+ "parameters": {
747
+ "type": "object",
748
+ "properties": {
749
+ "include_inactive": {
750
+ "type": "boolean",
751
+ "description": "Whether to include inactive agents. Default: false",
752
+ },
753
+ "search": {
754
+ "type": "string",
755
+ "description": "Optional search term to filter agents by name or description",
756
+ },
757
+ },
758
+ },
759
+ },
760
+ },
761
+ {
762
+ "type": "function",
763
+ "function": {
764
+ "name": "list_all_systems",
765
+ "description": "List all available multi-agent systems. Use this to find systems to switch to or to see what systems exist.",
766
+ "parameters": {
767
+ "type": "object",
768
+ "properties": {
769
+ "include_inactive": {
770
+ "type": "boolean",
771
+ "description": "Whether to include inactive systems. Default: false",
772
+ },
773
+ "search": {
774
+ "type": "string",
775
+ "description": "Optional search term to filter systems by name or description",
776
+ },
777
+ },
778
+ },
779
+ },
780
+ },
781
+ {
782
+ "type": "function",
783
+ "function": {
784
+ "name": "get_system_details",
785
+ "description": "Get detailed information about a multi-agent system, including its members and configuration.",
786
+ "parameters": {
787
+ "type": "object",
788
+ "properties": {
789
+ "system_slug": {
790
+ "type": "string",
791
+ "description": "The slug of the system to get details for",
792
+ },
793
+ "system_id": {
794
+ "type": "string",
795
+ "description": "The UUID of the system (alternative to slug)",
796
+ },
797
+ },
798
+ },
799
+ },
800
+ },
801
+ ]
802
+
803
+
804
+ class BuilderAgentRuntime(AgentRuntime):
805
+ """
806
+ The agent builder assistant that helps users create custom agents.
807
+
808
+ This agent has tools to modify AgentDefinition objects and guides
809
+ users through the agent creation process.
810
+ """
811
+
812
+ @property
813
+ def key(self) -> str:
814
+ return "agent-builder"
815
+
816
+ async def run(self, ctx: RunContext) -> RunResult:
817
+ """Execute the builder agent with agentic loop."""
818
+ # Get the agent being edited from context
819
+ # Check both metadata (from frontend) and params (from API)
820
+ agent_id = ctx.metadata.get("agent_id") or ctx.params.get("agent_id")
821
+ agent_context = "No agent selected. Ask the user what kind of agent they want to create."
822
+
823
+ if agent_id:
824
+ try:
825
+ agent = await sync_to_async(AgentDefinition.objects.get)(id=agent_id)
826
+ config = await sync_to_async(agent.get_effective_config)()
827
+ agent_context = f"""
828
+ Agent: {agent.name} ({agent.slug})
829
+ Description: {agent.description or 'Not set'}
830
+ Model: {config.get('model', 'gpt-4o')}
831
+ System Prompt: {config.get('system_prompt', 'Not set')[:500]}...
832
+ Tools: {len(config.get('tools', []))} configured
833
+ Knowledge: {len(config.get('knowledge', []))} sources
834
+ """
835
+ except AgentDefinition.DoesNotExist:
836
+ agent_context = "Agent not found. A new agent will be created."
837
+
838
+ # Build messages
839
+ system_prompt = BUILDER_SYSTEM_PROMPT.format(agent_context=agent_context)
840
+ messages = [{"role": "system", "content": system_prompt}]
841
+ messages.extend(ctx.input_messages)
842
+
843
+ # Get model from params (allows per-request override) or use default
844
+ model = ctx.params.get("model", DEFAULT_MODEL)
845
+
846
+ # Get LLM client for the specified model (auto-detects provider)
847
+ llm = get_llm_client_for_model(model)
848
+
849
+ # Create tool executor function for the agentic loop
850
+ async def execute_tool(tool_name: str, tool_args: dict) -> dict:
851
+ return await self._execute_tool(agent_id, tool_name, tool_args, ctx)
852
+
853
+ # Use the shared agentic loop
854
+ # Note: agentic_loop emits ASSISTANT_MESSAGE for the final response
855
+ result = await run_agentic_loop(
856
+ llm=llm,
857
+ messages=messages,
858
+ tools=BUILDER_TOOLS,
859
+ execute_tool=execute_tool,
860
+ ctx=ctx,
861
+ model=model,
862
+ max_iterations=10,
863
+ temperature=0.7,
864
+ )
865
+
866
+ return RunResult(
867
+ final_output={"response": result.final_content},
868
+ final_messages=result.messages,
869
+ )
870
+
871
+ async def _execute_tool(
872
+ self,
873
+ agent_id: Optional[str],
874
+ tool_name: str,
875
+ args: dict,
876
+ ctx: RunContext,
877
+ ) -> dict:
878
+ """Execute a builder tool."""
879
+ # Import here to avoid circular imports
880
+ from django_agent_runtime.models import AgentKnowledge, DynamicTool
881
+ from django_agent_runtime.dynamic_tools.scanner import ProjectScanner
882
+ from django_agent_runtime.dynamic_tools.generator import ToolGenerator
883
+
884
+ # Tools that don't require an agent
885
+ if tool_name == "scan_project_for_tools":
886
+ return await self._scan_project_for_tools(args, ctx)
887
+
888
+ if tool_name == "list_discovered_functions":
889
+ return await self._list_discovered_functions(args, ctx)
890
+
891
+ if tool_name == "get_function_details":
892
+ return await self._get_function_details(args, ctx)
893
+
894
+ if tool_name == "create_agent":
895
+ return await self._create_agent(args, ctx)
896
+
897
+ # UI Control tools - these emit special events to control the frontend
898
+ if tool_name == "switch_to_agent":
899
+ return await self._switch_to_agent(args, ctx)
900
+
901
+ if tool_name == "switch_to_system":
902
+ return await self._switch_to_system(args, ctx)
903
+
904
+ if tool_name == "list_all_agents":
905
+ return await self._list_all_agents(args, ctx)
906
+
907
+ if tool_name == "list_all_systems":
908
+ return await self._list_all_systems(args, ctx)
909
+
910
+ if tool_name == "get_system_details":
911
+ return await self._get_system_details(args, ctx)
912
+
913
+ # Tools that require an agent
914
+ if not agent_id:
915
+ return {"error": "No agent selected. Please create an agent first or use create_agent tool."}
916
+
917
+ try:
918
+ agent = await sync_to_async(AgentDefinition.objects.get)(id=agent_id)
919
+ version = await sync_to_async(agent.versions.filter(is_active=True).first)()
920
+
921
+ if tool_name == "get_current_config":
922
+ return await sync_to_async(agent.get_effective_config)()
923
+
924
+ elif tool_name == "update_system_prompt":
925
+ if "system_prompt" not in args:
926
+ return {
927
+ "error": "Missing required parameter: system_prompt",
928
+ "hint": "You must provide the full new system_prompt text. Example: update_system_prompt({\"system_prompt\": \"You are a helpful pirate assistant...\"})"
929
+ }
930
+ if version:
931
+ version.system_prompt = args["system_prompt"]
932
+ await sync_to_async(version.save)()
933
+ await create_revision(agent, comment="Updated system prompt")
934
+ return {"success": True, "message": "System prompt updated"}
935
+ return {"error": "No active version found"}
936
+
937
+ elif tool_name == "update_agent_name":
938
+ old_name = agent.name
939
+ agent.name = args["name"]
940
+ if "description" in args:
941
+ agent.description = args["description"]
942
+ await sync_to_async(agent.save)()
943
+ await create_revision(agent, comment=f"Renamed from '{old_name}' to '{agent.name}'")
944
+ return {"success": True, "message": "Agent name updated"}
945
+
946
+ elif tool_name == "update_model_settings":
947
+ if version:
948
+ changes = []
949
+ if "model" in args:
950
+ version.model = args["model"]
951
+ changes.append(f"model={args['model']}")
952
+ if "temperature" in args:
953
+ version.model_settings["temperature"] = args["temperature"]
954
+ changes.append(f"temperature={args['temperature']}")
955
+ await sync_to_async(version.save)()
956
+ await create_revision(agent, comment=f"Updated model settings: {', '.join(changes)}")
957
+ return {"success": True, "message": "Model settings updated"}
958
+ return {"error": "No active version found"}
959
+
960
+ elif tool_name == "set_memory_enabled":
961
+ if version:
962
+ enabled = args.get("enabled", True)
963
+ if version.extra_config is None:
964
+ version.extra_config = {}
965
+ version.extra_config["memory_enabled"] = enabled
966
+ await sync_to_async(version.save)()
967
+ status = "enabled" if enabled else "disabled"
968
+ await create_revision(agent, comment=f"Memory {status}")
969
+ return {
970
+ "success": True,
971
+ "message": f"Memory {status} for this agent",
972
+ "memory_enabled": enabled,
973
+ }
974
+ return {"error": "No active version found"}
975
+
976
+ elif tool_name == "get_memory_status":
977
+ if version:
978
+ extra = version.extra_config or {}
979
+ enabled = extra.get("memory_enabled", True) # Default is True
980
+ return {
981
+ "memory_enabled": enabled,
982
+ "message": f"Memory is {'enabled' if enabled else 'disabled'} for this agent",
983
+ "note": "When enabled, the agent has a 'remember' tool to store facts about users. Memory only works for authenticated users.",
984
+ }
985
+ return {"error": "No active version found"}
986
+
987
+ elif tool_name == "add_knowledge":
988
+ inclusion_mode = args.get("inclusion_mode", "always")
989
+ knowledge = await sync_to_async(AgentKnowledge.objects.create)(
990
+ agent=agent,
991
+ name=args["name"],
992
+ knowledge_type="text",
993
+ content=args["content"],
994
+ inclusion_mode=inclusion_mode,
995
+ )
996
+ await create_revision(agent, comment=f"Added knowledge: {args['name']} ({inclusion_mode})")
997
+
998
+ result = {
999
+ "success": True,
1000
+ "message": f"Knowledge '{args['name']}' added with mode '{inclusion_mode}'",
1001
+ "knowledge_id": str(knowledge.id),
1002
+ }
1003
+
1004
+ # If RAG mode, remind to index
1005
+ if inclusion_mode == "rag":
1006
+ result["note"] = "Use index_knowledge to index this knowledge for RAG search."
1007
+
1008
+ return result
1009
+
1010
+ elif tool_name == "index_knowledge":
1011
+ return await self._index_knowledge(agent, args, ctx)
1012
+
1013
+ elif tool_name == "get_rag_status":
1014
+ return await self._get_rag_status(agent, args, ctx)
1015
+
1016
+ elif tool_name == "preview_rag_search":
1017
+ return await self._preview_rag_search(agent, args, ctx)
1018
+
1019
+ elif tool_name == "update_rag_config":
1020
+ return await self._update_rag_config(agent, args, ctx)
1021
+
1022
+ elif tool_name == "add_tool_from_function":
1023
+ return await self._add_tool_from_function(agent, args, ctx)
1024
+
1025
+ elif tool_name == "list_agent_tools":
1026
+ return await self._list_agent_tools(agent, args, ctx)
1027
+
1028
+ elif tool_name == "remove_tool":
1029
+ return await self._remove_tool(agent, args, ctx)
1030
+
1031
+ elif tool_name == "list_revisions":
1032
+ return await self._list_revisions(agent, args, ctx)
1033
+
1034
+ elif tool_name == "get_revision":
1035
+ return await self._get_revision(agent, args, ctx)
1036
+
1037
+ elif tool_name == "restore_revision":
1038
+ return await self._restore_revision(agent, args, ctx)
1039
+
1040
+ # Multi-agent / Sub-agent tools
1041
+ elif tool_name == "list_available_agents":
1042
+ return await self._list_available_agents(agent, args, ctx)
1043
+
1044
+ elif tool_name == "add_sub_agent_tool":
1045
+ return await self._add_sub_agent_tool(agent, args, ctx)
1046
+
1047
+ elif tool_name == "list_sub_agent_tools":
1048
+ return await self._list_sub_agent_tools(agent, args, ctx)
1049
+
1050
+ elif tool_name == "remove_sub_agent_tool":
1051
+ return await self._remove_sub_agent_tool(agent, args, ctx)
1052
+
1053
+ elif tool_name == "update_sub_agent_tool":
1054
+ return await self._update_sub_agent_tool(agent, args, ctx)
1055
+
1056
+ else:
1057
+ return {"error": f"Unknown tool: {tool_name}"}
1058
+
1059
+ except AgentDefinition.DoesNotExist:
1060
+ return {"error": "Agent not found"}
1061
+ except Exception as e:
1062
+ logger.exception(f"Error executing builder tool {tool_name}")
1063
+ return {"error": str(e)}
1064
+
1065
+ async def _create_agent(self, args: dict, ctx: RunContext) -> dict:
1066
+ """Create a new agent with the given name and description."""
1067
+ from django.utils.text import slugify
1068
+
1069
+ name = args.get("name", "New Agent")
1070
+ description = args.get("description", "")
1071
+ system_prompt = args.get("system_prompt", "You are a helpful assistant.")
1072
+
1073
+ # Generate a unique slug
1074
+ base_slug = slugify(name)
1075
+ slug = base_slug
1076
+ counter = 1
1077
+ while await sync_to_async(AgentDefinition.objects.filter(slug=slug).exists)():
1078
+ slug = f"{base_slug}-{counter}"
1079
+ counter += 1
1080
+
1081
+ try:
1082
+ # Create the agent
1083
+ agent = await sync_to_async(AgentDefinition.objects.create)(
1084
+ name=name,
1085
+ slug=slug,
1086
+ description=description,
1087
+ is_active=True,
1088
+ )
1089
+
1090
+ # Create the initial version
1091
+ await sync_to_async(AgentVersion.objects.create)(
1092
+ agent=agent,
1093
+ version="1.0",
1094
+ system_prompt=system_prompt,
1095
+ model="gpt-4o",
1096
+ is_active=True,
1097
+ is_draft=False,
1098
+ )
1099
+
1100
+ # Create initial revision
1101
+ await create_revision(agent, comment="Initial creation")
1102
+
1103
+ return {
1104
+ "success": True,
1105
+ "agent_id": str(agent.id),
1106
+ "slug": agent.slug,
1107
+ "message": f"Created agent '{name}' (slug: {slug}). You can now configure it with tools and knowledge.",
1108
+ }
1109
+ except Exception as e:
1110
+ logger.exception("Error creating agent")
1111
+ return {"error": str(e)}
1112
+
1113
+ async def _scan_project_for_tools(self, args: dict, ctx: RunContext) -> dict:
1114
+ """Scan the Django project for functions that can be used as tools."""
1115
+ from django_agent_runtime.dynamic_tools.scanner import ProjectScanner
1116
+
1117
+ try:
1118
+ scanner = ProjectScanner(
1119
+ include_private=args.get("include_private", False),
1120
+ include_tests=False,
1121
+ app_filter=args.get("app_filter"),
1122
+ )
1123
+
1124
+ # Scan either a specific directory or all Django apps
1125
+ if args.get("directory"):
1126
+ functions = scanner.scan_directory(args["directory"])
1127
+ else:
1128
+ functions = scanner.scan()
1129
+
1130
+ # Store in context for later use
1131
+ ctx.state["discovered_functions"] = [f.to_dict() for f in functions]
1132
+
1133
+ # Return summary
1134
+ by_type = {}
1135
+ for f in functions:
1136
+ ftype = f.function_type
1137
+ by_type[ftype] = by_type.get(ftype, 0) + 1
1138
+
1139
+ return {
1140
+ "success": True,
1141
+ "total_discovered": len(functions),
1142
+ "by_type": by_type,
1143
+ "message": f"Discovered {len(functions)} functions. Use list_discovered_functions to see details.",
1144
+ }
1145
+ except Exception as e:
1146
+ logger.exception("Error scanning project")
1147
+ return {"error": str(e)}
1148
+
1149
+ async def _list_discovered_functions(self, args: dict, ctx: RunContext) -> dict:
1150
+ """List discovered functions from the most recent scan."""
1151
+ functions = ctx.state.get("discovered_functions", [])
1152
+
1153
+ if not functions:
1154
+ return {
1155
+ "error": "No functions discovered yet. Run scan_project_for_tools first."
1156
+ }
1157
+
1158
+ # Apply filters
1159
+ filtered = functions
1160
+
1161
+ if args.get("function_type"):
1162
+ filtered = [f for f in filtered if f["function_type"] == args["function_type"]]
1163
+
1164
+ if args.get("module_filter"):
1165
+ module_filter = args["module_filter"].lower()
1166
+ filtered = [f for f in filtered if module_filter in f["module_path"].lower()]
1167
+
1168
+ if args.get("safe_only"):
1169
+ filtered = [f for f in filtered if not f["has_side_effects"]]
1170
+
1171
+ # Apply limit
1172
+ limit = args.get("limit", 50)
1173
+ filtered = filtered[:limit]
1174
+
1175
+ # Return simplified view
1176
+ results = []
1177
+ for f in filtered:
1178
+ results.append({
1179
+ "name": f["name"],
1180
+ "function_path": f["function_path"],
1181
+ "function_type": f["function_type"],
1182
+ "signature": f["signature"],
1183
+ "has_side_effects": f["has_side_effects"],
1184
+ "docstring_preview": (f["docstring"][:100] + "...") if len(f.get("docstring", "")) > 100 else f.get("docstring", ""),
1185
+ })
1186
+
1187
+ return {
1188
+ "total": len(functions),
1189
+ "filtered": len(results),
1190
+ "functions": results,
1191
+ }
1192
+
1193
+ async def _get_function_details(self, args: dict, ctx: RunContext) -> dict:
1194
+ """Get detailed information about a specific function."""
1195
+ functions = ctx.state.get("discovered_functions", [])
1196
+ function_path = args.get("function_path", "")
1197
+
1198
+ for f in functions:
1199
+ if f["function_path"] == function_path:
1200
+ return {
1201
+ "found": True,
1202
+ "function": f,
1203
+ }
1204
+
1205
+ return {
1206
+ "found": False,
1207
+ "error": f"Function '{function_path}' not found in discovered functions. Run scan_project_for_tools first.",
1208
+ }
1209
+
1210
+ async def _add_tool_from_function(self, agent, args: dict, ctx: RunContext) -> dict:
1211
+ """Add a discovered function as a dynamic tool to the agent."""
1212
+ from django_agent_runtime.models import DynamicTool
1213
+ from django_agent_runtime.dynamic_tools.generator import ToolGenerator
1214
+
1215
+ function_path = args.get("function_path", "")
1216
+ functions = ctx.state.get("discovered_functions", [])
1217
+
1218
+ # Find the function in discovered functions
1219
+ func_info = None
1220
+ for f in functions:
1221
+ if f["function_path"] == function_path:
1222
+ func_info = f
1223
+ break
1224
+
1225
+ if not func_info:
1226
+ return {
1227
+ "error": f"Function '{function_path}' not found. Run scan_project_for_tools first."
1228
+ }
1229
+
1230
+ # Generate tool schema
1231
+ generator = ToolGenerator()
1232
+
1233
+ # Determine tool name
1234
+ tool_name = args.get("tool_name")
1235
+ if not tool_name:
1236
+ tool_name = func_info["name"]
1237
+ if func_info.get("class_name"):
1238
+ tool_name = f"{func_info['class_name'].lower()}_{tool_name}"
1239
+
1240
+ # Determine description
1241
+ description = args.get("description")
1242
+ if not description:
1243
+ description = func_info.get("docstring", "").split("\n\n")[0].strip()
1244
+ if not description:
1245
+ description = f"Execute {func_info['name'].replace('_', ' ')}"
1246
+
1247
+ # Determine requires_confirmation
1248
+ requires_confirmation = args.get("requires_confirmation")
1249
+ if requires_confirmation is None:
1250
+ requires_confirmation = func_info.get("has_side_effects", True)
1251
+
1252
+ # Build parameters schema from function parameters
1253
+ properties = {}
1254
+ required = []
1255
+ for param in func_info.get("parameters", []):
1256
+ param_name = param.get("name", "")
1257
+ if param_name.startswith("*"):
1258
+ continue
1259
+
1260
+ param_type = "string"
1261
+ annotation = param.get("annotation", "")
1262
+ if annotation:
1263
+ type_map = {"str": "string", "int": "integer", "float": "number", "bool": "boolean"}
1264
+ param_type = type_map.get(annotation.split("[")[0], "string")
1265
+
1266
+ properties[param_name] = {"type": param_type}
1267
+ if not param.get("has_default", False):
1268
+ required.append(param_name)
1269
+
1270
+ parameters_schema = {"type": "object", "properties": properties}
1271
+ if required:
1272
+ parameters_schema["required"] = required
1273
+
1274
+ # Check if tool already exists
1275
+ existing = await sync_to_async(DynamicTool.objects.filter(agent=agent, name=tool_name).first)()
1276
+ if existing:
1277
+ return {
1278
+ "error": f"Tool '{tool_name}' already exists for this agent. Remove it first or use a different name."
1279
+ }
1280
+
1281
+ # Create the dynamic tool
1282
+ tool = await sync_to_async(DynamicTool.objects.create)(
1283
+ agent=agent,
1284
+ name=tool_name,
1285
+ description=description,
1286
+ function_path=function_path,
1287
+ source_file=func_info.get("file_path", ""),
1288
+ source_line=func_info.get("line_number"),
1289
+ parameters_schema=parameters_schema,
1290
+ is_safe=not func_info.get("has_side_effects", True),
1291
+ requires_confirmation=requires_confirmation,
1292
+ )
1293
+
1294
+ # Create revision
1295
+ await create_revision(agent, comment=f"Added tool: {tool_name}")
1296
+
1297
+ return {
1298
+ "success": True,
1299
+ "message": f"Tool '{tool_name}' added successfully",
1300
+ "tool_id": str(tool.id),
1301
+ "tool_name": tool_name,
1302
+ "function_path": function_path,
1303
+ }
1304
+
1305
+ async def _list_agent_tools(self, agent, args: dict, ctx: RunContext) -> dict:
1306
+ """List all tools assigned to the agent."""
1307
+ from django_agent_runtime.models import AgentTool, DynamicTool
1308
+
1309
+ include_inactive = args.get("include_inactive", False)
1310
+
1311
+ # Get static tools (AgentTool)
1312
+ static_query = agent.tools.all()
1313
+ if not include_inactive:
1314
+ static_query = static_query.filter(is_active=True)
1315
+
1316
+ static_tools = []
1317
+ for tool in await sync_to_async(list)(static_query):
1318
+ static_tools.append({
1319
+ "id": str(tool.id),
1320
+ "name": tool.name,
1321
+ "type": "static",
1322
+ "tool_type": tool.tool_type,
1323
+ "description": tool.description,
1324
+ "is_active": tool.is_active,
1325
+ })
1326
+
1327
+ # Get dynamic tools (DynamicTool)
1328
+ dynamic_query = agent.dynamic_tools.all()
1329
+ if not include_inactive:
1330
+ dynamic_query = dynamic_query.filter(is_active=True)
1331
+
1332
+ dynamic_tools = []
1333
+ for tool in await sync_to_async(list)(dynamic_query):
1334
+ dynamic_tools.append({
1335
+ "id": str(tool.id),
1336
+ "name": tool.name,
1337
+ "type": "dynamic",
1338
+ "function_path": tool.function_path,
1339
+ "description": tool.description,
1340
+ "is_active": tool.is_active,
1341
+ "is_safe": tool.is_safe,
1342
+ "requires_confirmation": tool.requires_confirmation,
1343
+ })
1344
+
1345
+ return {
1346
+ "static_tools": static_tools,
1347
+ "dynamic_tools": dynamic_tools,
1348
+ "total_static": len(static_tools),
1349
+ "total_dynamic": len(dynamic_tools),
1350
+ }
1351
+
1352
+ async def _remove_tool(self, agent, args: dict, ctx: RunContext) -> dict:
1353
+ """Remove a tool from the agent."""
1354
+ from django_agent_runtime.models import AgentTool, DynamicTool
1355
+
1356
+ tool_name = args.get("tool_name")
1357
+ tool_id = args.get("tool_id")
1358
+
1359
+ if not tool_name and not tool_id:
1360
+ return {"error": "Either tool_name or tool_id must be provided"}
1361
+
1362
+ # Try to find and remove static tool
1363
+ static_query = agent.tools.all()
1364
+ if tool_id:
1365
+ static_query = static_query.filter(id=tool_id)
1366
+ elif tool_name:
1367
+ static_query = static_query.filter(name=tool_name)
1368
+
1369
+ static_tool = await sync_to_async(static_query.first)()
1370
+ if static_tool:
1371
+ name = static_tool.name
1372
+ await sync_to_async(static_tool.delete)()
1373
+ # Create revision
1374
+ await create_revision(agent, comment=f"Removed tool: {name}")
1375
+ return {"success": True, "message": f"Static tool '{name}' removed"}
1376
+
1377
+ # Try to find and remove dynamic tool
1378
+ dynamic_query = agent.dynamic_tools.all()
1379
+ if tool_id:
1380
+ dynamic_query = dynamic_query.filter(id=tool_id)
1381
+ elif tool_name:
1382
+ dynamic_query = dynamic_query.filter(name=tool_name)
1383
+
1384
+ dynamic_tool = await sync_to_async(dynamic_query.first)()
1385
+ if dynamic_tool:
1386
+ name = dynamic_tool.name
1387
+ await sync_to_async(dynamic_tool.delete)()
1388
+ # Create revision
1389
+ await create_revision(agent, comment=f"Removed tool: {name}")
1390
+ return {"success": True, "message": f"Dynamic tool '{name}' removed"}
1391
+
1392
+ return {"error": f"Tool not found: {tool_name or tool_id}"}
1393
+
1394
+ async def _list_revisions(self, agent, args: dict, ctx: RunContext) -> dict:
1395
+ """List all revisions for the agent."""
1396
+ limit = args.get("limit", 20)
1397
+
1398
+ revisions = await sync_to_async(list)(
1399
+ agent.revisions.all()[:limit]
1400
+ )
1401
+
1402
+ result = []
1403
+ for rev in revisions:
1404
+ result.append({
1405
+ "revision_number": rev.revision_number,
1406
+ "comment": rev.comment,
1407
+ "created_at": rev.created_at.isoformat(),
1408
+ "created_by": str(rev.created_by) if rev.created_by else None,
1409
+ })
1410
+
1411
+ return {
1412
+ "revisions": result,
1413
+ "total": len(result),
1414
+ }
1415
+
1416
+ async def _get_revision(self, agent, args: dict, ctx: RunContext) -> dict:
1417
+ """Get a specific revision's content."""
1418
+ revision_number = args.get("revision_number")
1419
+
1420
+ if not revision_number:
1421
+ return {"error": "revision_number is required"}
1422
+
1423
+ revision = await sync_to_async(
1424
+ agent.revisions.filter(revision_number=revision_number).first
1425
+ )()
1426
+
1427
+ if not revision:
1428
+ return {"error": f"Revision {revision_number} not found"}
1429
+
1430
+ return {
1431
+ "revision_number": revision.revision_number,
1432
+ "comment": revision.comment,
1433
+ "created_at": revision.created_at.isoformat(),
1434
+ "content": revision.content,
1435
+ }
1436
+
1437
+ async def _restore_revision(self, agent, args: dict, ctx: RunContext) -> dict:
1438
+ """Restore the agent to a previous revision."""
1439
+ revision_number = args.get("revision_number")
1440
+
1441
+ if not revision_number:
1442
+ return {"error": "revision_number is required"}
1443
+
1444
+ revision = await sync_to_async(
1445
+ agent.revisions.filter(revision_number=revision_number).first
1446
+ )()
1447
+
1448
+ if not revision:
1449
+ return {"error": f"Revision {revision_number} not found"}
1450
+
1451
+ # Restore creates a new revision with the old content
1452
+ new_revision = await sync_to_async(revision.restore)()
1453
+
1454
+ return {
1455
+ "success": True,
1456
+ "message": f"Restored to revision {revision_number}",
1457
+ "new_revision_number": new_revision.revision_number,
1458
+ }
1459
+
1460
+ # ==========================================================================
1461
+ # RAG Knowledge Management Tools
1462
+ # ==========================================================================
1463
+
1464
+ async def _index_knowledge(self, agent, args: dict, ctx: RunContext) -> dict:
1465
+ """Index RAG knowledge sources for similarity search."""
1466
+ from django_agent_runtime.rag import KnowledgeIndexer
1467
+
1468
+ try:
1469
+ indexer = KnowledgeIndexer()
1470
+ knowledge_id = args.get("knowledge_id")
1471
+ force = args.get("force", False)
1472
+
1473
+ if knowledge_id:
1474
+ # Index specific knowledge
1475
+ result = await indexer.index_knowledge(knowledge_id, force=force)
1476
+ else:
1477
+ # Index all pending RAG knowledge for this agent
1478
+ result = await indexer.index_agent_knowledge(str(agent.id), force=force)
1479
+
1480
+ return result
1481
+
1482
+ except Exception as e:
1483
+ logger.exception("Error indexing knowledge")
1484
+ return {"error": str(e)}
1485
+
1486
+ async def _get_rag_status(self, agent, args: dict, ctx: RunContext) -> dict:
1487
+ """Get the indexing status of RAG knowledge sources."""
1488
+ from django_agent_runtime.rag import KnowledgeIndexer
1489
+
1490
+ try:
1491
+ indexer = KnowledgeIndexer()
1492
+ return await indexer.get_indexing_status(str(agent.id))
1493
+ except Exception as e:
1494
+ logger.exception("Error getting RAG status")
1495
+ return {"error": str(e)}
1496
+
1497
+ async def _preview_rag_search(self, agent, args: dict, ctx: RunContext) -> dict:
1498
+ """Preview RAG search results for a query."""
1499
+ from django_agent_runtime.rag import KnowledgeRetriever
1500
+
1501
+ query = args.get("query")
1502
+ if not query:
1503
+ return {"error": "query is required"}
1504
+
1505
+ try:
1506
+ retriever = KnowledgeRetriever()
1507
+ return await retriever.preview_search(
1508
+ agent_id=str(agent.id),
1509
+ query=query,
1510
+ top_k=args.get("top_k", 5),
1511
+ )
1512
+ except Exception as e:
1513
+ logger.exception("Error previewing RAG search")
1514
+ return {"error": str(e)}
1515
+
1516
+ async def _update_rag_config(self, agent, args: dict, ctx: RunContext) -> dict:
1517
+ """Update the RAG configuration for the agent."""
1518
+ try:
1519
+ # Get current config
1520
+ current_config = agent.rag_config or {}
1521
+
1522
+ # Update with provided values
1523
+ if "enabled" in args:
1524
+ current_config["enabled"] = args["enabled"]
1525
+ if "top_k" in args:
1526
+ current_config["top_k"] = args["top_k"]
1527
+ if "similarity_threshold" in args:
1528
+ current_config["similarity_threshold"] = args["similarity_threshold"]
1529
+ if "chunk_size" in args:
1530
+ current_config["chunk_size"] = args["chunk_size"]
1531
+ if "chunk_overlap" in args:
1532
+ current_config["chunk_overlap"] = args["chunk_overlap"]
1533
+
1534
+ # Save
1535
+ agent.rag_config = current_config
1536
+ await sync_to_async(agent.save)(update_fields=["rag_config"])
1537
+ await create_revision(agent, comment="Updated RAG configuration")
1538
+
1539
+ return {
1540
+ "success": True,
1541
+ "message": "RAG configuration updated",
1542
+ "config": current_config,
1543
+ }
1544
+ except Exception as e:
1545
+ logger.exception("Error updating RAG config")
1546
+ return {"error": str(e)}
1547
+
1548
+ # ==========================================================================
1549
+ # Multi-Agent / Sub-Agent Tools
1550
+ # ==========================================================================
1551
+
1552
+ async def _list_available_agents(self, current_agent, args: dict, ctx: RunContext) -> dict:
1553
+ """List all agents that can be used as sub-agents."""
1554
+ include_inactive = args.get("include_inactive", False)
1555
+ search = args.get("search", "")
1556
+
1557
+ try:
1558
+ # Get all agents except the current one (can't be sub-agent of itself)
1559
+ query = AgentDefinition.objects.exclude(id=current_agent.id)
1560
+
1561
+ if not include_inactive:
1562
+ query = query.filter(is_active=True)
1563
+
1564
+ if search:
1565
+ from django.db.models import Q
1566
+ query = query.filter(
1567
+ Q(name__icontains=search) |
1568
+ Q(description__icontains=search) |
1569
+ Q(slug__icontains=search)
1570
+ )
1571
+
1572
+ agents = await sync_to_async(list)(query.order_by("name")[:50])
1573
+
1574
+ results = []
1575
+ for agent in agents:
1576
+ results.append({
1577
+ "slug": agent.slug,
1578
+ "name": agent.name,
1579
+ "description": agent.description or "",
1580
+ "is_active": agent.is_active,
1581
+ })
1582
+
1583
+ return {
1584
+ "agents": results,
1585
+ "total": len(results),
1586
+ "message": f"Found {len(results)} available agents",
1587
+ }
1588
+ except Exception as e:
1589
+ logger.exception("Error listing available agents")
1590
+ return {"error": str(e)}
1591
+
1592
+ async def _add_sub_agent_tool(self, agent, args: dict, ctx: RunContext) -> dict:
1593
+ """Add a sub-agent tool to the agent."""
1594
+ from django_agent_runtime.models import SubAgentTool
1595
+ from django_agent_studio.services.permissions import get_permission_service
1596
+
1597
+ sub_agent_slug = args.get("sub_agent_slug")
1598
+ tool_name = args.get("tool_name")
1599
+ description = args.get("description")
1600
+ context_mode = args.get("context_mode", "message_only")
1601
+
1602
+ if not all([sub_agent_slug, tool_name, description]):
1603
+ return {"error": "sub_agent_slug, tool_name, and description are required"}
1604
+
1605
+ # Check permissions
1606
+ user = ctx.metadata.get("user")
1607
+ if user:
1608
+ permission_service = get_permission_service()
1609
+ if not permission_service.can_create_tool(user, agent):
1610
+ return {
1611
+ "error": "Permission denied: You need CREATOR or ADMIN access level to add sub-agent tools.",
1612
+ "hint": "Contact an administrator to request elevated permissions.",
1613
+ "current_level": permission_service.get_user_access_level(user, agent),
1614
+ }
1615
+
1616
+ try:
1617
+ # Find the sub-agent
1618
+ sub_agent = await sync_to_async(AgentDefinition.objects.get)(slug=sub_agent_slug)
1619
+
1620
+ # Check for circular reference
1621
+ if sub_agent.id == agent.id:
1622
+ return {"error": "An agent cannot be a sub-agent of itself"}
1623
+
1624
+ # Check if tool name already exists
1625
+ existing = await sync_to_async(
1626
+ SubAgentTool.objects.filter(parent_agent=agent, name=tool_name).exists
1627
+ )()
1628
+ if existing:
1629
+ return {
1630
+ "error": f"A sub-agent tool named '{tool_name}' already exists",
1631
+ "hint": "Use a different name or remove the existing tool first",
1632
+ }
1633
+
1634
+ # Create the sub-agent tool
1635
+ sub_agent_tool = await sync_to_async(SubAgentTool.objects.create)(
1636
+ parent_agent=agent,
1637
+ sub_agent=sub_agent,
1638
+ name=tool_name,
1639
+ description=description,
1640
+ context_mode=context_mode,
1641
+ )
1642
+
1643
+ # Create revision
1644
+ await create_revision(agent, comment=f"Added sub-agent tool: {tool_name} -> {sub_agent.name}")
1645
+
1646
+ return {
1647
+ "success": True,
1648
+ "message": f"Added sub-agent tool '{tool_name}' pointing to '{sub_agent.name}'",
1649
+ "tool_id": str(sub_agent_tool.id),
1650
+ "tool_name": tool_name,
1651
+ "sub_agent_slug": sub_agent_slug,
1652
+ "context_mode": context_mode,
1653
+ }
1654
+
1655
+ except AgentDefinition.DoesNotExist:
1656
+ return {
1657
+ "error": f"Sub-agent '{sub_agent_slug}' not found",
1658
+ "hint": "Use list_available_agents to see available agents",
1659
+ }
1660
+ except Exception as e:
1661
+ logger.exception("Error adding sub-agent tool")
1662
+ return {"error": str(e)}
1663
+
1664
+ async def _list_sub_agent_tools(self, agent, args: dict, ctx: RunContext) -> dict:
1665
+ """List all sub-agent tools for the agent."""
1666
+ from django_agent_runtime.models import SubAgentTool
1667
+
1668
+ try:
1669
+ tools = await sync_to_async(list)(
1670
+ agent.sub_agent_tools.select_related("sub_agent").all()
1671
+ )
1672
+
1673
+ results = []
1674
+ for tool in tools:
1675
+ results.append({
1676
+ "id": str(tool.id),
1677
+ "name": tool.name,
1678
+ "description": tool.description,
1679
+ "sub_agent_slug": tool.sub_agent.slug,
1680
+ "sub_agent_name": tool.sub_agent.name,
1681
+ "context_mode": tool.context_mode,
1682
+ "is_active": tool.is_active,
1683
+ })
1684
+
1685
+ return {
1686
+ "sub_agent_tools": results,
1687
+ "total": len(results),
1688
+ }
1689
+ except Exception as e:
1690
+ logger.exception("Error listing sub-agent tools")
1691
+ return {"error": str(e)}
1692
+
1693
+ async def _remove_sub_agent_tool(self, agent, args: dict, ctx: RunContext) -> dict:
1694
+ """Remove a sub-agent tool from the agent."""
1695
+ from django_agent_runtime.models import SubAgentTool
1696
+ from django_agent_studio.services.permissions import get_permission_service
1697
+
1698
+ tool_name = args.get("tool_name")
1699
+ if not tool_name:
1700
+ return {"error": "tool_name is required"}
1701
+
1702
+ # Check permissions
1703
+ user = ctx.metadata.get("user")
1704
+ if user:
1705
+ permission_service = get_permission_service()
1706
+ if not permission_service.can_create_tool(user, agent):
1707
+ return {
1708
+ "error": "Permission denied: You need CREATOR or ADMIN access level to remove sub-agent tools.",
1709
+ "hint": "Contact an administrator to request elevated permissions.",
1710
+ "current_level": permission_service.get_user_access_level(user, agent),
1711
+ }
1712
+
1713
+ try:
1714
+ tool = await sync_to_async(
1715
+ agent.sub_agent_tools.filter(name=tool_name).first
1716
+ )()
1717
+
1718
+ if not tool:
1719
+ return {
1720
+ "error": f"Sub-agent tool '{tool_name}' not found",
1721
+ "hint": "Use list_sub_agent_tools to see existing tools",
1722
+ }
1723
+
1724
+ sub_agent_name = await sync_to_async(lambda: tool.sub_agent.name)()
1725
+ await sync_to_async(tool.delete)()
1726
+
1727
+ # Create revision
1728
+ await create_revision(agent, comment=f"Removed sub-agent tool: {tool_name}")
1729
+
1730
+ return {
1731
+ "success": True,
1732
+ "message": f"Removed sub-agent tool '{tool_name}' (was pointing to '{sub_agent_name}')",
1733
+ }
1734
+ except Exception as e:
1735
+ logger.exception("Error removing sub-agent tool")
1736
+ return {"error": str(e)}
1737
+
1738
+ async def _update_sub_agent_tool(self, agent, args: dict, ctx: RunContext) -> dict:
1739
+ """Update a sub-agent tool's configuration."""
1740
+ from django_agent_runtime.models import SubAgentTool
1741
+ from django_agent_studio.services.permissions import get_permission_service
1742
+
1743
+ tool_name = args.get("tool_name")
1744
+ if not tool_name:
1745
+ return {"error": "tool_name is required"}
1746
+
1747
+ # Check permissions
1748
+ user = ctx.metadata.get("user")
1749
+ if user:
1750
+ permission_service = get_permission_service()
1751
+ if not permission_service.can_create_tool(user, agent):
1752
+ return {
1753
+ "error": "Permission denied: You need CREATOR or ADMIN access level to update sub-agent tools.",
1754
+ "hint": "Contact an administrator to request elevated permissions.",
1755
+ "current_level": permission_service.get_user_access_level(user, agent),
1756
+ }
1757
+
1758
+ try:
1759
+ tool = await sync_to_async(
1760
+ agent.sub_agent_tools.filter(name=tool_name).first
1761
+ )()
1762
+
1763
+ if not tool:
1764
+ return {
1765
+ "error": f"Sub-agent tool '{tool_name}' not found",
1766
+ "hint": "Use list_sub_agent_tools to see existing tools",
1767
+ }
1768
+
1769
+ changes = []
1770
+ if "description" in args:
1771
+ tool.description = args["description"]
1772
+ changes.append("description")
1773
+ if "context_mode" in args:
1774
+ tool.context_mode = args["context_mode"]
1775
+ changes.append(f"context_mode={args['context_mode']}")
1776
+
1777
+ if not changes:
1778
+ return {"error": "No changes specified. Provide description or context_mode."}
1779
+
1780
+ await sync_to_async(tool.save)()
1781
+
1782
+ # Create revision
1783
+ await create_revision(agent, comment=f"Updated sub-agent tool {tool_name}: {', '.join(changes)}")
1784
+
1785
+ return {
1786
+ "success": True,
1787
+ "message": f"Updated sub-agent tool '{tool_name}'",
1788
+ "changes": changes,
1789
+ }
1790
+ except Exception as e:
1791
+ logger.exception("Error updating sub-agent tool")
1792
+ return {"error": str(e)}
1793
+
1794
+ # ==========================================================================
1795
+ # UI Control Tools - Allow builder to switch agents/systems in the UI
1796
+ # ==========================================================================
1797
+
1798
+ async def _switch_to_agent(self, args: dict, ctx: RunContext) -> dict:
1799
+ """Switch the UI to edit a different agent."""
1800
+ agent_slug = args.get("agent_slug")
1801
+ agent_id = args.get("agent_id")
1802
+
1803
+ if not agent_slug and not agent_id:
1804
+ return {"error": "Either agent_slug or agent_id must be provided"}
1805
+
1806
+ try:
1807
+ if agent_id:
1808
+ agent = await sync_to_async(AgentDefinition.objects.get)(id=agent_id)
1809
+ else:
1810
+ agent = await sync_to_async(AgentDefinition.objects.get)(slug=agent_slug)
1811
+
1812
+ # Emit a special UI control event that the frontend can listen for
1813
+ await ctx.emit("ui.control", {
1814
+ "type": "ui_control",
1815
+ "action": "switch_agent",
1816
+ "agent_id": str(agent.id),
1817
+ "agent_slug": agent.slug,
1818
+ "agent_name": agent.name,
1819
+ })
1820
+
1821
+ return {
1822
+ "success": True,
1823
+ "message": f"Switched to agent '{agent.name}' ({agent.slug})",
1824
+ "agent_id": str(agent.id),
1825
+ "agent_slug": agent.slug,
1826
+ "agent_name": agent.name,
1827
+ }
1828
+ except AgentDefinition.DoesNotExist:
1829
+ return {"error": f"Agent not found: {agent_slug or agent_id}"}
1830
+ except Exception as e:
1831
+ logger.exception("Error switching agent")
1832
+ return {"error": str(e)}
1833
+
1834
+ async def _switch_to_system(self, args: dict, ctx: RunContext) -> dict:
1835
+ """Switch the UI to work with a multi-agent system."""
1836
+ from django_agent_runtime.models import AgentSystem
1837
+
1838
+ system_slug = args.get("system_slug")
1839
+ system_id = args.get("system_id")
1840
+
1841
+ if not system_slug and not system_id:
1842
+ return {"error": "Either system_slug or system_id must be provided"}
1843
+
1844
+ try:
1845
+ if system_id:
1846
+ system = await sync_to_async(AgentSystem.objects.get)(id=system_id)
1847
+ else:
1848
+ system = await sync_to_async(AgentSystem.objects.get)(slug=system_slug)
1849
+
1850
+ # Emit a special UI control event
1851
+ await ctx.emit("ui.control", {
1852
+ "type": "ui_control",
1853
+ "action": "switch_system",
1854
+ "system_id": str(system.id),
1855
+ "system_slug": system.slug,
1856
+ "system_name": system.name,
1857
+ })
1858
+
1859
+ return {
1860
+ "success": True,
1861
+ "message": f"Switched to system '{system.name}' ({system.slug})",
1862
+ "system_id": str(system.id),
1863
+ "system_slug": system.slug,
1864
+ "system_name": system.name,
1865
+ }
1866
+ except AgentSystem.DoesNotExist:
1867
+ return {"error": f"System not found: {system_slug or system_id}"}
1868
+ except Exception as e:
1869
+ logger.exception("Error switching system")
1870
+ return {"error": str(e)}
1871
+
1872
+ async def _list_all_agents(self, args: dict, ctx: RunContext) -> dict:
1873
+ """List all available agents in the system."""
1874
+ include_inactive = args.get("include_inactive", False)
1875
+ search = args.get("search", "")
1876
+
1877
+ try:
1878
+ query = AgentDefinition.objects.all()
1879
+ if not include_inactive:
1880
+ query = query.filter(is_active=True)
1881
+ if search:
1882
+ query = query.filter(name__icontains=search) | query.filter(description__icontains=search)
1883
+
1884
+ agents = await sync_to_async(list)(query.order_by("name")[:50])
1885
+
1886
+ result = []
1887
+ for agent in agents:
1888
+ result.append({
1889
+ "id": str(agent.id),
1890
+ "slug": agent.slug,
1891
+ "name": agent.name,
1892
+ "description": agent.description or "",
1893
+ "is_active": agent.is_active,
1894
+ "icon": agent.icon or "🤖",
1895
+ })
1896
+
1897
+ return {
1898
+ "agents": result,
1899
+ "total": len(result),
1900
+ }
1901
+ except Exception as e:
1902
+ logger.exception("Error listing agents")
1903
+ return {"error": str(e)}
1904
+
1905
+ async def _list_all_systems(self, args: dict, ctx: RunContext) -> dict:
1906
+ """List all available multi-agent systems."""
1907
+ from django_agent_runtime.models import AgentSystem
1908
+
1909
+ include_inactive = args.get("include_inactive", False)
1910
+ search = args.get("search", "")
1911
+
1912
+ try:
1913
+ query = AgentSystem.objects.all()
1914
+ if not include_inactive:
1915
+ query = query.filter(is_active=True)
1916
+ if search:
1917
+ query = query.filter(name__icontains=search) | query.filter(description__icontains=search)
1918
+
1919
+ systems = await sync_to_async(list)(query.order_by("name")[:50])
1920
+
1921
+ result = []
1922
+ for system in systems:
1923
+ member_count = await sync_to_async(system.members.count)()
1924
+ entry_agent = await sync_to_async(lambda: system.entry_agent)()
1925
+ result.append({
1926
+ "id": str(system.id),
1927
+ "slug": system.slug,
1928
+ "name": system.name,
1929
+ "description": system.description or "",
1930
+ "is_active": system.is_active,
1931
+ "member_count": member_count,
1932
+ "entry_agent_slug": entry_agent.slug if entry_agent else None,
1933
+ })
1934
+
1935
+ return {
1936
+ "systems": result,
1937
+ "total": len(result),
1938
+ }
1939
+ except Exception as e:
1940
+ logger.exception("Error listing systems")
1941
+ return {"error": str(e)}
1942
+
1943
+ async def _get_system_details(self, args: dict, ctx: RunContext) -> dict:
1944
+ """Get detailed information about a multi-agent system."""
1945
+ from django_agent_runtime.models import AgentSystem
1946
+
1947
+ system_slug = args.get("system_slug")
1948
+ system_id = args.get("system_id")
1949
+
1950
+ if not system_slug and not system_id:
1951
+ return {"error": "Either system_slug or system_id must be provided"}
1952
+
1953
+ try:
1954
+ if system_id:
1955
+ system = await sync_to_async(AgentSystem.objects.get)(id=system_id)
1956
+ else:
1957
+ system = await sync_to_async(AgentSystem.objects.get)(slug=system_slug)
1958
+
1959
+ # Get members
1960
+ members = await sync_to_async(list)(system.members.select_related("agent").all())
1961
+ member_list = []
1962
+ for member in members:
1963
+ agent = await sync_to_async(lambda m=member: m.agent)()
1964
+ member_list.append({
1965
+ "id": str(member.id),
1966
+ "agent_id": str(agent.id),
1967
+ "agent_slug": agent.slug,
1968
+ "agent_name": agent.name,
1969
+ "role": member.role or "",
1970
+ "is_entry_point": member.is_entry_point,
1971
+ })
1972
+
1973
+ # Get entry agent
1974
+ entry_agent = await sync_to_async(lambda: system.entry_agent)()
1975
+
1976
+ return {
1977
+ "id": str(system.id),
1978
+ "slug": system.slug,
1979
+ "name": system.name,
1980
+ "description": system.description or "",
1981
+ "is_active": system.is_active,
1982
+ "entry_agent": {
1983
+ "id": str(entry_agent.id),
1984
+ "slug": entry_agent.slug,
1985
+ "name": entry_agent.name,
1986
+ } if entry_agent else None,
1987
+ "members": member_list,
1988
+ "member_count": len(member_list),
1989
+ }
1990
+ except AgentSystem.DoesNotExist:
1991
+ return {"error": f"System not found: {system_slug or system_id}"}
1992
+ except Exception as e:
1993
+ logger.exception("Error getting system details")
1994
+ return {"error": str(e)}