django-agent-studio 0.1.9__py3-none-any.whl → 0.2.1__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.
@@ -41,6 +41,7 @@ You have access to tools that allow you to:
41
41
  8. **Switch between agents and systems** in the UI
42
42
  9. **Configure agent memory** (enable/disable the remember tool)
43
43
  10. **Manage agent specifications** (human-readable behavior descriptions)
44
+ 11. **Configure file uploads** (allowed types, size limits, OCR/vision providers)
44
45
 
45
46
  ## IMPORTANT: Tool Usage
46
47
 
@@ -97,15 +98,70 @@ You can control the builder UI to switch between different agents and systems:
97
98
  When the user asks to work on a different agent or system, use these tools to switch context.
98
99
  The UI will automatically update to show the selected agent/system.
99
100
 
101
+ ## System Management
102
+
103
+ You can create and manage multi-agent systems directly:
104
+ - Use `create_system` to create a new system with an entry agent
105
+ - Use `add_agent_to_system` to add agents to an existing system
106
+ - Use `remove_agent_from_system` to remove agents from a system
107
+ - Use `update_system_config` to modify system settings (name, description, entry agent, shared knowledge)
108
+ - Use `delete_system` to delete a system (agents are NOT deleted)
109
+
110
+ **Creating a System:**
111
+ ```
112
+ create_system({{
113
+ "name": "Customer Support",
114
+ "entry_agent_slug": "support-triage",
115
+ "description": "Handles customer inquiries",
116
+ "auto_discover": true // Automatically adds all sub-agents
117
+ }})
118
+ ```
119
+
120
+ **Shared Knowledge:**
121
+ Systems can have shared knowledge that applies to ALL agents in the system. Use `update_system_config` with `shared_knowledge`:
122
+ ```
123
+ update_system_config({{
124
+ "system_slug": "customer-support",
125
+ "shared_knowledge": [
126
+ {{
127
+ "key": "company-info",
128
+ "title": "Company Information",
129
+ "content": "We are Acme Corp...",
130
+ "inject_as": "system", // Prepend to system prompt
131
+ "priority": 0,
132
+ "enabled": true
133
+ }}
134
+ ]
135
+ }})
136
+ ```
137
+
138
+ **inject_as options:**
139
+ - `system`: Prepend to every agent's system prompt
140
+ - `context`: Add as conversation context
141
+ - `knowledge`: Make searchable via RAG
142
+
100
143
  ## Agent Memory
101
144
 
102
145
  Agents have a built-in memory system that allows them to remember facts about users across conversations:
103
146
  - Memory is **enabled by default** for all agents
104
- - When enabled, the agent has a `remember` tool it can use to store key-value facts
105
- - Memories are scoped per-user and per-conversation
147
+ - When enabled, the agent has `remember`, `recall`, and `forget` tools
106
148
  - Memory only works for **authenticated users** (not anonymous visitors)
107
- - Use `get_memory_status` to check if memory is enabled
108
- - Use `set_memory_enabled` to enable or disable memory
149
+ - Use `get_memory_status` to check current memory configuration
150
+ - Use `set_memory_enabled` to quickly enable or disable memory
151
+ - Use `configure_memory` for advanced memory settings
152
+
153
+ **Memory Scopes:**
154
+ - `conversation` - Memories only for this chat session
155
+ - `user` - Memories persist across all conversations for this user (default)
156
+ - `system` - Memories shared with other agents in the system
157
+
158
+ **Advanced Configuration (via `configure_memory`):**
159
+ - `default_scope` - Default scope for new memories (default: "user")
160
+ - `allowed_scopes` - Which scopes the agent can use (default: all)
161
+ - `auto_recall` - Auto-load memories at conversation start (default: true)
162
+ - `max_memories_in_prompt` - Limit memories in system prompt (default: 50)
163
+ - `include_system_memories` - Include memories from other agents (default: true)
164
+ - `retention_days` - Auto-delete memories after N days (default: null = forever)
109
165
 
110
166
  **When to disable memory:**
111
167
  - Public-facing agents where you don't want user data stored
@@ -139,6 +195,28 @@ Every agent can have a **spec** - a human-readable description of its intended b
139
195
 
140
196
  **Best practice:** When building an agent, start by writing or reviewing the spec, then craft the system prompt to implement that spec. This ensures alignment between intended and actual behavior.
141
197
 
198
+ ## File Upload Configuration
199
+
200
+ Agents can accept file uploads from users. Use `update_file_config` to configure:
201
+ - **enabled**: Turn file uploads on/off for this agent
202
+ - **max_file_size_mb**: Maximum file size (default: 100MB)
203
+ - **allowed_types**: MIME type patterns like `image/*`, `application/pdf`, `text/*`
204
+ - **ocr_provider**: Extract text from images/PDFs using:
205
+ - `tesseract` (local, free)
206
+ - `google_vision` (Google Cloud Vision API)
207
+ - `aws_textract` (AWS Textract)
208
+ - `azure_di` (Azure Document Intelligence)
209
+ - **vision_provider**: AI image understanding using:
210
+ - `openai` (GPT-4 Vision)
211
+ - `anthropic` (Claude Vision)
212
+ - `gemini` (Google Gemini)
213
+ - **enable_thumbnails**: Generate preview thumbnails for images
214
+
215
+ **Example configurations:**
216
+ - Document processing agent: Enable PDF, DOCX, use `tesseract` for OCR
217
+ - Image analysis agent: Enable `image/*`, use `openai` vision provider
218
+ - General assistant: Enable common types, no OCR/vision needed
219
+
142
220
  ## Spec Document System (Advanced)
143
221
 
144
222
  For organizations managing multiple agents, there's a **Spec Document System** that provides:
@@ -210,14 +288,41 @@ Be conversational and helpful. Guide users through the process step by step.
210
288
 
211
289
  **Be conservative with ambiguous requests.** When in doubt, ask for clarification rather than taking action.
212
290
 
213
- ## IMPORTANT: Always End with a Summary
291
+ ## IMPORTANT: Always Provide Clear Summaries
214
292
 
215
- 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:
216
- - "I've created the 'Customer Support' agent with a helpful assistant prompt and added the search_orders tool."
293
+ ### After Individual Actions
294
+ After completing any actions (creating agents, updating prompts, adding tools, etc.), briefly mention what you did:
217
295
  - "I've updated the system prompt to make the agent respond in pirate speak."
218
- - "I've added 3 sub-agent tools to the Orchestrator: billing_specialist, tech_support, and returns_handler."
296
+ - "I've added the search_orders tool to the agent."
297
+
298
+ ### After Completing a Task or Request
299
+ When you've finished fulfilling a user's request (especially multi-step tasks), ALWAYS provide a comprehensive **Task Completion Summary** that includes:
219
300
 
220
- This helps users understand what changes were made without having to expand the tool results.
301
+ 1. **What was accomplished**: A clear statement of what you achieved
302
+ 2. **Changes made**: List the specific modifications (agents created, prompts updated, tools added, etc.)
303
+ 3. **Current state**: Brief description of how the agent/system is now configured
304
+ 4. **Next steps** (if applicable): Suggestions for testing or further improvements
305
+
306
+ **Example Task Completion Summary:**
307
+ ```
308
+ ## ✅ Task Complete
309
+
310
+ I've set up your Customer Support system with the following:
311
+
312
+ **Created:**
313
+ - Customer Support Bot (customer-support) - Main entry point
314
+ - Billing Specialist (billing-specialist) - Handles payment questions
315
+ - Tech Support (tech-support) - Handles technical issues
316
+
317
+ **Configured:**
318
+ - Added sub-agent routing to the main bot
319
+ - Each specialist has domain-specific system prompts
320
+ - All agents share access to the knowledge base
321
+
322
+ **Ready to test:** Try asking the Customer Support Bot about a billing issue - it should route to the Billing Specialist.
323
+ ```
324
+
325
+ This summary format helps users understand exactly what was built and how to use it.
221
326
 
222
327
  Current agent being edited: {agent_context}
223
328
  """
@@ -362,6 +467,48 @@ BUILDER_TOOLS = [
362
467
  },
363
468
  },
364
469
  },
470
+ {
471
+ "type": "function",
472
+ "function": {
473
+ "name": "configure_memory",
474
+ "description": "Configure advanced memory settings for the agent. Memory allows agents to remember facts about users across conversations with different scopes: 'conversation' (this chat only), 'user' (persists across chats), 'system' (shared with other agents).",
475
+ "parameters": {
476
+ "type": "object",
477
+ "properties": {
478
+ "enabled": {
479
+ "type": "boolean",
480
+ "description": "Whether memory is enabled. Default: true",
481
+ },
482
+ "default_scope": {
483
+ "type": "string",
484
+ "enum": ["conversation", "user", "system"],
485
+ "description": "Default scope for new memories. 'conversation' = this chat only, 'user' = persists across chats, 'system' = shared with other agents. Default: 'user'",
486
+ },
487
+ "allowed_scopes": {
488
+ "type": "array",
489
+ "items": {"type": "string", "enum": ["conversation", "user", "system"]},
490
+ "description": "Which scopes the agent can use. Default: all scopes",
491
+ },
492
+ "auto_recall": {
493
+ "type": "boolean",
494
+ "description": "Whether to automatically recall memories at the start of conversations. Default: true",
495
+ },
496
+ "max_memories_in_prompt": {
497
+ "type": "integer",
498
+ "description": "Maximum number of memories to include in the system prompt. Default: 50",
499
+ },
500
+ "include_system_memories": {
501
+ "type": "boolean",
502
+ "description": "Whether to include system-scoped memories from other agents. Default: true",
503
+ },
504
+ "retention_days": {
505
+ "type": "integer",
506
+ "description": "Number of days to retain memories. Null = forever. Default: null",
507
+ },
508
+ },
509
+ },
510
+ },
511
+ },
365
512
  # Spec Document tools
366
513
  {
367
514
  "type": "function",
@@ -697,6 +844,45 @@ BUILDER_TOOLS = [
697
844
  },
698
845
  },
699
846
  },
847
+ {
848
+ "type": "function",
849
+ "function": {
850
+ "name": "update_file_config",
851
+ "description": "Update the file upload and processing configuration for the agent. Controls what files users can upload and how they are processed.",
852
+ "parameters": {
853
+ "type": "object",
854
+ "properties": {
855
+ "enabled": {
856
+ "type": "boolean",
857
+ "description": "Enable or disable file uploads for this agent",
858
+ },
859
+ "max_file_size_mb": {
860
+ "type": "integer",
861
+ "description": "Maximum file size in megabytes. Default: 100",
862
+ },
863
+ "allowed_types": {
864
+ "type": "array",
865
+ "items": {"type": "string"},
866
+ "description": "List of allowed MIME types or patterns (e.g., 'image/*', 'application/pdf', 'text/*'). Default: ['image/*', 'application/pdf', 'text/*']",
867
+ },
868
+ "ocr_provider": {
869
+ "type": "string",
870
+ "enum": ["tesseract", "google_vision", "aws_textract", "azure_di", None],
871
+ "description": "OCR provider for text extraction from images/PDFs. Options: tesseract (local), google_vision, aws_textract, azure_di. Set to null to disable OCR.",
872
+ },
873
+ "vision_provider": {
874
+ "type": "string",
875
+ "enum": ["openai", "anthropic", "gemini", None],
876
+ "description": "AI vision provider for image understanding. Options: openai (GPT-4V), anthropic (Claude Vision), gemini. Set to null to disable vision.",
877
+ },
878
+ "enable_thumbnails": {
879
+ "type": "boolean",
880
+ "description": "Generate thumbnails for uploaded images. Default: true",
881
+ },
882
+ },
883
+ },
884
+ },
885
+ },
700
886
  {
701
887
  "type": "function",
702
888
  "function": {
@@ -1105,6 +1291,166 @@ BUILDER_TOOLS = [
1105
1291
  },
1106
1292
  },
1107
1293
  },
1294
+ # ==========================================================================
1295
+ # System Management Tools - Create, modify, and delete multi-agent systems
1296
+ # ==========================================================================
1297
+ {
1298
+ "type": "function",
1299
+ "function": {
1300
+ "name": "create_system",
1301
+ "description": "Create a new multi-agent system. A system groups related agents that work together, with one agent as the entry point that handles initial requests.",
1302
+ "parameters": {
1303
+ "type": "object",
1304
+ "properties": {
1305
+ "name": {
1306
+ "type": "string",
1307
+ "description": "Human-readable name for the system (e.g., 'Customer Support')",
1308
+ },
1309
+ "slug": {
1310
+ "type": "string",
1311
+ "description": "Unique identifier slug (e.g., 'customer-support'). If not provided, will be generated from name.",
1312
+ },
1313
+ "description": {
1314
+ "type": "string",
1315
+ "description": "Description of what this system does",
1316
+ },
1317
+ "entry_agent_slug": {
1318
+ "type": "string",
1319
+ "description": "Slug of the agent that handles initial requests (entry point)",
1320
+ },
1321
+ "auto_discover": {
1322
+ "type": "boolean",
1323
+ "description": "If true, automatically discover and add all sub-agents reachable from the entry agent. Default: true",
1324
+ },
1325
+ },
1326
+ "required": ["name", "entry_agent_slug"],
1327
+ },
1328
+ },
1329
+ },
1330
+ {
1331
+ "type": "function",
1332
+ "function": {
1333
+ "name": "add_agent_to_system",
1334
+ "description": "Add an agent to a multi-agent system as a member.",
1335
+ "parameters": {
1336
+ "type": "object",
1337
+ "properties": {
1338
+ "system_slug": {
1339
+ "type": "string",
1340
+ "description": "Slug of the system to add the agent to",
1341
+ },
1342
+ "agent_slug": {
1343
+ "type": "string",
1344
+ "description": "Slug of the agent to add",
1345
+ },
1346
+ "role": {
1347
+ "type": "string",
1348
+ "enum": ["specialist", "utility", "supervisor"],
1349
+ "description": "Role of the agent in the system. Default: 'specialist'",
1350
+ },
1351
+ "notes": {
1352
+ "type": "string",
1353
+ "description": "Optional notes about this agent's role in the system",
1354
+ },
1355
+ },
1356
+ "required": ["system_slug", "agent_slug"],
1357
+ },
1358
+ },
1359
+ },
1360
+ {
1361
+ "type": "function",
1362
+ "function": {
1363
+ "name": "remove_agent_from_system",
1364
+ "description": "Remove an agent from a multi-agent system. Cannot remove the entry point agent.",
1365
+ "parameters": {
1366
+ "type": "object",
1367
+ "properties": {
1368
+ "system_slug": {
1369
+ "type": "string",
1370
+ "description": "Slug of the system to remove the agent from",
1371
+ },
1372
+ "agent_slug": {
1373
+ "type": "string",
1374
+ "description": "Slug of the agent to remove",
1375
+ },
1376
+ },
1377
+ "required": ["system_slug", "agent_slug"],
1378
+ },
1379
+ },
1380
+ },
1381
+ {
1382
+ "type": "function",
1383
+ "function": {
1384
+ "name": "update_system_config",
1385
+ "description": "Update a multi-agent system's configuration including name, description, entry agent, and shared knowledge.",
1386
+ "parameters": {
1387
+ "type": "object",
1388
+ "properties": {
1389
+ "system_slug": {
1390
+ "type": "string",
1391
+ "description": "Slug of the system to update",
1392
+ },
1393
+ "name": {
1394
+ "type": "string",
1395
+ "description": "New name for the system",
1396
+ },
1397
+ "description": {
1398
+ "type": "string",
1399
+ "description": "New description for the system",
1400
+ },
1401
+ "entry_agent_slug": {
1402
+ "type": "string",
1403
+ "description": "Slug of the new entry point agent (must be a member of the system)",
1404
+ },
1405
+ "is_active": {
1406
+ "type": "boolean",
1407
+ "description": "Whether the system is active",
1408
+ },
1409
+ "shared_knowledge": {
1410
+ "type": "array",
1411
+ "description": "Shared knowledge items for all agents in the system. Each item has: key, title, content, inject_as ('system'|'context'|'knowledge'), priority, enabled",
1412
+ "items": {
1413
+ "type": "object",
1414
+ "properties": {
1415
+ "key": {"type": "string", "description": "Unique key for this knowledge item"},
1416
+ "title": {"type": "string", "description": "Title/name of the knowledge"},
1417
+ "content": {"type": "string", "description": "The knowledge content"},
1418
+ "inject_as": {
1419
+ "type": "string",
1420
+ "enum": ["system", "context", "knowledge"],
1421
+ "description": "How to inject: 'system' (prepend to system prompt), 'context' (add as context), 'knowledge' (RAG searchable)",
1422
+ },
1423
+ "priority": {"type": "integer", "description": "Priority order (lower = higher priority)"},
1424
+ "enabled": {"type": "boolean", "description": "Whether this knowledge is active"},
1425
+ },
1426
+ },
1427
+ },
1428
+ },
1429
+ "required": ["system_slug"],
1430
+ },
1431
+ },
1432
+ },
1433
+ {
1434
+ "type": "function",
1435
+ "function": {
1436
+ "name": "delete_system",
1437
+ "description": "Delete a multi-agent system. This removes the system and all its member associations, but does NOT delete the agents themselves.",
1438
+ "parameters": {
1439
+ "type": "object",
1440
+ "properties": {
1441
+ "system_slug": {
1442
+ "type": "string",
1443
+ "description": "Slug of the system to delete",
1444
+ },
1445
+ "confirm": {
1446
+ "type": "boolean",
1447
+ "description": "Must be true to confirm deletion",
1448
+ },
1449
+ },
1450
+ "required": ["system_slug", "confirm"],
1451
+ },
1452
+ },
1453
+ },
1108
1454
  ]
1109
1455
 
1110
1456
 
@@ -1124,12 +1470,16 @@ class BuilderAgentRuntime(AgentRuntime):
1124
1470
  """Execute the builder agent with agentic loop."""
1125
1471
  # Get the agent being edited from context
1126
1472
  # Check both metadata (from frontend) and params (from API)
1127
- agent_id = ctx.metadata.get("agent_id") or ctx.params.get("agent_id")
1473
+ initial_agent_id = ctx.metadata.get("agent_id") or ctx.params.get("agent_id")
1128
1474
  agent_context = "No agent selected. Ask the user what kind of agent they want to create."
1129
1475
 
1130
- if agent_id:
1476
+ # Use a mutable container so switch_to_agent can update the current agent
1477
+ # This allows the builder to work on different agents during a single run
1478
+ current_agent = {"id": initial_agent_id}
1479
+
1480
+ if initial_agent_id:
1131
1481
  try:
1132
- agent = await sync_to_async(AgentDefinition.objects.get)(id=agent_id)
1482
+ agent = await sync_to_async(AgentDefinition.objects.get)(id=initial_agent_id)
1133
1483
  config = await sync_to_async(agent.get_effective_config)()
1134
1484
  agent_context = f"""
1135
1485
  Agent: {agent.name} ({agent.slug})
@@ -1154,11 +1504,13 @@ Knowledge: {len(config.get('knowledge', []))} sources
1154
1504
  llm = get_llm_client_for_model(model)
1155
1505
 
1156
1506
  # Create tool executor function for the agentic loop
1507
+ # Uses current_agent dict so switch_to_agent can update which agent we're working on
1157
1508
  async def execute_tool(tool_name: str, tool_args: dict) -> dict:
1158
- return await self._execute_tool(agent_id, tool_name, tool_args, ctx)
1509
+ return await self._execute_tool(current_agent, tool_name, tool_args, ctx)
1159
1510
 
1160
1511
  # Use the shared agentic loop
1161
1512
  # Note: agentic_loop emits ASSISTANT_MESSAGE for the final response
1513
+ # ensure_final_response=True ensures a summary is generated if tools were used
1162
1514
  result = await run_agentic_loop(
1163
1515
  llm=llm,
1164
1516
  messages=messages,
@@ -1168,6 +1520,7 @@ Knowledge: {len(config.get('knowledge', []))} sources
1168
1520
  model=model,
1169
1521
  max_iterations=10,
1170
1522
  temperature=0.7,
1523
+ ensure_final_response=True,
1171
1524
  )
1172
1525
 
1173
1526
  return RunResult(
@@ -1177,17 +1530,28 @@ Knowledge: {len(config.get('knowledge', []))} sources
1177
1530
 
1178
1531
  async def _execute_tool(
1179
1532
  self,
1180
- agent_id: Optional[str],
1533
+ current_agent: dict,
1181
1534
  tool_name: str,
1182
1535
  args: dict,
1183
1536
  ctx: RunContext,
1184
1537
  ) -> dict:
1185
- """Execute a builder tool."""
1538
+ """Execute a builder tool.
1539
+
1540
+ Args:
1541
+ current_agent: Mutable dict with 'id' key tracking the current agent.
1542
+ This allows switch_to_agent to update which agent we're working on.
1543
+ tool_name: Name of the tool to execute
1544
+ args: Tool arguments
1545
+ ctx: Run context
1546
+ """
1186
1547
  # Import here to avoid circular imports
1187
1548
  from django_agent_runtime.models import AgentKnowledge, DynamicTool
1188
1549
  from django_agent_runtime.dynamic_tools.scanner import ProjectScanner
1189
1550
  from django_agent_runtime.dynamic_tools.generator import ToolGenerator
1190
1551
 
1552
+ agent_id = current_agent.get("id")
1553
+ logger.info(f"_execute_tool: {tool_name} with current_agent={current_agent}")
1554
+
1191
1555
  # Tools that don't require an agent
1192
1556
  if tool_name == "scan_project_for_tools":
1193
1557
  return await self._scan_project_for_tools(args, ctx)
@@ -1199,11 +1563,21 @@ Knowledge: {len(config.get('knowledge', []))} sources
1199
1563
  return await self._get_function_details(args, ctx)
1200
1564
 
1201
1565
  if tool_name == "create_agent":
1202
- return await self._create_agent(args, ctx)
1566
+ result = await self._create_agent(args, ctx)
1567
+ # If agent was created successfully, update current_agent to point to it
1568
+ if result.get("success") and result.get("agent_id"):
1569
+ current_agent["id"] = result["agent_id"]
1570
+ logger.info(f"Updated current_agent to newly created agent: {result['agent_id']}")
1571
+ return result
1203
1572
 
1204
1573
  # UI Control tools - these emit special events to control the frontend
1205
1574
  if tool_name == "switch_to_agent":
1206
- return await self._switch_to_agent(args, ctx)
1575
+ result = await self._switch_to_agent(args, ctx)
1576
+ # If switch was successful, update current_agent to the new agent
1577
+ if result.get("success") and result.get("agent_id"):
1578
+ current_agent["id"] = result["agent_id"]
1579
+ logger.info(f"Updated current_agent to switched agent: {result['agent_id']}")
1580
+ return result
1207
1581
 
1208
1582
  if tool_name == "switch_to_system":
1209
1583
  return await self._switch_to_system(args, ctx)
@@ -1217,6 +1591,22 @@ Knowledge: {len(config.get('knowledge', []))} sources
1217
1591
  if tool_name == "get_system_details":
1218
1592
  return await self._get_system_details(args, ctx)
1219
1593
 
1594
+ # System management tools
1595
+ if tool_name == "create_system":
1596
+ return await self._create_system(args, ctx)
1597
+
1598
+ if tool_name == "add_agent_to_system":
1599
+ return await self._add_agent_to_system(args, ctx)
1600
+
1601
+ if tool_name == "remove_agent_from_system":
1602
+ return await self._remove_agent_from_system(args, ctx)
1603
+
1604
+ if tool_name == "update_system_config":
1605
+ return await self._update_system_config(args, ctx)
1606
+
1607
+ if tool_name == "delete_system":
1608
+ return await self._delete_system(args, ctx)
1609
+
1220
1610
  # Tools that require an agent
1221
1611
  if not agent_id:
1222
1612
  return {"error": "No agent selected. Please create an agent first or use create_agent tool."}
@@ -1251,20 +1641,56 @@ Knowledge: {len(config.get('knowledge', []))} sources
1251
1641
  return {"success": True, "message": "Agent name updated"}
1252
1642
 
1253
1643
  elif tool_name == "get_agent_spec":
1644
+ # Get spec from linked SpecDocument
1645
+ from django_agent_runtime.models import SpecDocument
1646
+ spec_doc = await sync_to_async(
1647
+ lambda: SpecDocument.objects.filter(linked_agent=agent).first()
1648
+ )()
1649
+ spec_content = spec_doc.content if spec_doc else ""
1254
1650
  return {
1255
- "spec": agent.spec or "",
1256
- "has_spec": bool(agent.spec),
1651
+ "spec": spec_content,
1652
+ "has_spec": bool(spec_content),
1653
+ "spec_document_id": str(spec_doc.id) if spec_doc else None,
1654
+ "spec_document_title": spec_doc.title if spec_doc else None,
1257
1655
  "message": "The agent spec describes intended behavior in plain English, separate from the technical system prompt.",
1258
1656
  }
1259
1657
 
1260
1658
  elif tool_name == "update_agent_spec":
1261
- agent.spec = args["spec"]
1262
- await sync_to_async(agent.save)()
1659
+ # Update or create linked SpecDocument
1660
+ from django_agent_runtime.models import SpecDocument
1661
+ logger.info(f"update_agent_spec called for agent {agent.id} ({agent.slug})")
1662
+
1663
+ spec_content = args["spec"]
1664
+
1665
+ # Find or create the spec document for this agent
1666
+ def update_or_create_spec():
1667
+ spec_doc = SpecDocument.objects.filter(linked_agent=agent).first()
1668
+ if spec_doc:
1669
+ # Update existing document
1670
+ spec_doc.content = spec_content
1671
+ spec_doc.save() # This auto-creates a version
1672
+ return spec_doc, False
1673
+ else:
1674
+ # Create new document
1675
+ spec_doc = SpecDocument.objects.create(
1676
+ title=f"{agent.name} Specification",
1677
+ content=spec_content,
1678
+ linked_agent=agent,
1679
+ owner=agent.owner,
1680
+ )
1681
+ return spec_doc, True
1682
+
1683
+ spec_doc, created = await sync_to_async(update_or_create_spec)()
1684
+ logger.info(f"{'Created' if created else 'Updated'} spec document {spec_doc.id} for agent {agent.id}")
1685
+
1263
1686
  await create_revision(agent, comment="Updated agent specification")
1264
1687
  return {
1265
1688
  "success": True,
1266
- "message": "Agent specification updated",
1267
- "spec_preview": agent.spec[:200] + "..." if len(agent.spec) > 200 else agent.spec,
1689
+ "message": f"Agent specification {'created' if created else 'updated'} for '{agent.name}' ({agent.slug})",
1690
+ "agent_id": str(agent.id),
1691
+ "agent_slug": agent.slug,
1692
+ "spec_document_id": str(spec_doc.id),
1693
+ "spec_preview": spec_content[:200] + "..." if len(spec_content) > 200 else spec_content,
1268
1694
  }
1269
1695
 
1270
1696
  elif tool_name == "update_model_settings":
@@ -1301,10 +1727,70 @@ Knowledge: {len(config.get('knowledge', []))} sources
1301
1727
  if version:
1302
1728
  extra = version.extra_config or {}
1303
1729
  enabled = extra.get("memory_enabled", True) # Default is True
1730
+ default_scope = extra.get("memory_default_scope", "user")
1731
+ allowed_scopes = extra.get("memory_allowed_scopes", ["conversation", "user", "system"])
1732
+ auto_recall = extra.get("memory_auto_recall", True)
1733
+ max_in_prompt = extra.get("memory_max_in_prompt", 50)
1734
+ include_system = extra.get("memory_include_system", True)
1735
+ retention_days = extra.get("memory_retention_days", None)
1304
1736
  return {
1305
1737
  "memory_enabled": enabled,
1738
+ "default_scope": default_scope,
1739
+ "allowed_scopes": allowed_scopes,
1740
+ "auto_recall": auto_recall,
1741
+ "max_memories_in_prompt": max_in_prompt,
1742
+ "include_system_memories": include_system,
1743
+ "retention_days": retention_days,
1306
1744
  "message": f"Memory is {'enabled' if enabled else 'disabled'} for this agent",
1307
- "note": "When enabled, the agent has a 'remember' tool to store facts about users. Memory only works for authenticated users.",
1745
+ "note": "When enabled, the agent has 'remember', 'recall', and 'forget' tools. Memory scopes: 'conversation' (this chat), 'user' (persists), 'system' (shared).",
1746
+ }
1747
+ return {"error": "No active version found"}
1748
+
1749
+ elif tool_name == "configure_memory":
1750
+ if version:
1751
+ if version.extra_config is None:
1752
+ version.extra_config = {}
1753
+
1754
+ changes = []
1755
+ if "enabled" in args:
1756
+ version.extra_config["memory_enabled"] = args["enabled"]
1757
+ changes.append(f"enabled={args['enabled']}")
1758
+ if "default_scope" in args:
1759
+ version.extra_config["memory_default_scope"] = args["default_scope"]
1760
+ changes.append(f"default_scope={args['default_scope']}")
1761
+ if "allowed_scopes" in args:
1762
+ version.extra_config["memory_allowed_scopes"] = args["allowed_scopes"]
1763
+ changes.append(f"allowed_scopes={args['allowed_scopes']}")
1764
+ if "auto_recall" in args:
1765
+ version.extra_config["memory_auto_recall"] = args["auto_recall"]
1766
+ changes.append(f"auto_recall={args['auto_recall']}")
1767
+ if "max_memories_in_prompt" in args:
1768
+ version.extra_config["memory_max_in_prompt"] = args["max_memories_in_prompt"]
1769
+ changes.append(f"max_memories={args['max_memories_in_prompt']}")
1770
+ if "include_system_memories" in args:
1771
+ version.extra_config["memory_include_system"] = args["include_system_memories"]
1772
+ changes.append(f"include_system={args['include_system_memories']}")
1773
+ if "retention_days" in args:
1774
+ version.extra_config["memory_retention_days"] = args["retention_days"]
1775
+ changes.append(f"retention_days={args['retention_days']}")
1776
+
1777
+ await sync_to_async(version.save)()
1778
+ await create_revision(agent, comment=f"Memory config: {', '.join(changes)}")
1779
+
1780
+ # Return current config
1781
+ extra = version.extra_config
1782
+ return {
1783
+ "success": True,
1784
+ "message": f"Memory configuration updated: {', '.join(changes)}",
1785
+ "config": {
1786
+ "enabled": extra.get("memory_enabled", True),
1787
+ "default_scope": extra.get("memory_default_scope", "user"),
1788
+ "allowed_scopes": extra.get("memory_allowed_scopes", ["conversation", "user", "system"]),
1789
+ "auto_recall": extra.get("memory_auto_recall", True),
1790
+ "max_memories_in_prompt": extra.get("memory_max_in_prompt", 50),
1791
+ "include_system_memories": extra.get("memory_include_system", True),
1792
+ "retention_days": extra.get("memory_retention_days", None),
1793
+ },
1308
1794
  }
1309
1795
  return {"error": "No active version found"}
1310
1796
 
@@ -1374,6 +1860,9 @@ Knowledge: {len(config.get('knowledge', []))} sources
1374
1860
  elif tool_name == "update_rag_config":
1375
1861
  return await self._update_rag_config(agent, args, ctx)
1376
1862
 
1863
+ elif tool_name == "update_file_config":
1864
+ return await self._update_file_config(agent, args, ctx)
1865
+
1377
1866
  elif tool_name == "add_tool_from_function":
1378
1867
  return await self._add_tool_from_function(agent, args, ctx)
1379
1868
 
@@ -1900,6 +2389,47 @@ Knowledge: {len(config.get('knowledge', []))} sources
1900
2389
  logger.exception("Error updating RAG config")
1901
2390
  return {"error": str(e)}
1902
2391
 
2392
+ async def _update_file_config(self, agent, args: dict, ctx: RunContext) -> dict:
2393
+ """Update the file upload and processing configuration for the agent."""
2394
+ try:
2395
+ # Get current config with defaults
2396
+ current_config = agent.file_config or {
2397
+ "enabled": False,
2398
+ "max_file_size_mb": 100,
2399
+ "allowed_types": ["image/*", "application/pdf", "text/*"],
2400
+ "ocr_provider": None,
2401
+ "vision_provider": None,
2402
+ "enable_thumbnails": True,
2403
+ }
2404
+
2405
+ # Update with provided values
2406
+ if "enabled" in args:
2407
+ current_config["enabled"] = args["enabled"]
2408
+ if "max_file_size_mb" in args:
2409
+ current_config["max_file_size_mb"] = args["max_file_size_mb"]
2410
+ if "allowed_types" in args:
2411
+ current_config["allowed_types"] = args["allowed_types"]
2412
+ if "ocr_provider" in args:
2413
+ current_config["ocr_provider"] = args["ocr_provider"]
2414
+ if "vision_provider" in args:
2415
+ current_config["vision_provider"] = args["vision_provider"]
2416
+ if "enable_thumbnails" in args:
2417
+ current_config["enable_thumbnails"] = args["enable_thumbnails"]
2418
+
2419
+ # Save
2420
+ agent.file_config = current_config
2421
+ await sync_to_async(agent.save)(update_fields=["file_config"])
2422
+ await create_revision(agent, comment="Updated file upload configuration")
2423
+
2424
+ return {
2425
+ "success": True,
2426
+ "message": "File upload configuration updated",
2427
+ "config": current_config,
2428
+ }
2429
+ except Exception as e:
2430
+ logger.exception("Error updating file config")
2431
+ return {"error": str(e)}
2432
+
1903
2433
  # ==========================================================================
1904
2434
  # Multi-Agent / Sub-Agent Tools
1905
2435
  # ==========================================================================
@@ -2348,6 +2878,276 @@ Knowledge: {len(config.get('knowledge', []))} sources
2348
2878
  logger.exception("Error getting system details")
2349
2879
  return {"error": str(e)}
2350
2880
 
2881
+ # ==================== System Management Methods ====================
2882
+
2883
+ async def _create_system(self, args: dict, ctx: RunContext) -> dict:
2884
+ """Create a new multi-agent system."""
2885
+ from django_agent_runtime.models import AgentSystem
2886
+ from django_agent_runtime.services.multi_agent import create_system_from_entry_agent
2887
+
2888
+ name = args.get("name")
2889
+ entry_agent_slug = args.get("entry_agent_slug")
2890
+
2891
+ if not name or not entry_agent_slug:
2892
+ return {"error": "name and entry_agent_slug are required"}
2893
+
2894
+ # Generate slug if not provided
2895
+ slug = args.get("slug")
2896
+ if not slug:
2897
+ slug = slugify(name)
2898
+
2899
+ description = args.get("description", "")
2900
+ auto_discover = args.get("auto_discover", True)
2901
+
2902
+ try:
2903
+ # Check if slug already exists
2904
+ if await sync_to_async(AgentSystem.objects.filter(slug=slug).exists)():
2905
+ return {"error": f"System with slug '{slug}' already exists"}
2906
+
2907
+ # Get the entry agent
2908
+ try:
2909
+ entry_agent = await sync_to_async(AgentDefinition.objects.get)(slug=entry_agent_slug)
2910
+ except AgentDefinition.DoesNotExist:
2911
+ return {"error": f"Entry agent not found: {entry_agent_slug}"}
2912
+
2913
+ # Get owner from context if available
2914
+ owner = getattr(ctx, 'user', None)
2915
+
2916
+ # Create the system
2917
+ system = await sync_to_async(create_system_from_entry_agent)(
2918
+ slug=slug,
2919
+ name=name,
2920
+ entry_agent=entry_agent,
2921
+ description=description,
2922
+ owner=owner,
2923
+ auto_discover=auto_discover,
2924
+ )
2925
+
2926
+ # Get member count
2927
+ member_count = await sync_to_async(system.members.count)()
2928
+
2929
+ return {
2930
+ "success": True,
2931
+ "message": f"Created system '{name}' with {member_count} member(s)",
2932
+ "system_id": str(system.id),
2933
+ "system_slug": system.slug,
2934
+ "member_count": member_count,
2935
+ "entry_agent_slug": entry_agent.slug,
2936
+ }
2937
+ except Exception as e:
2938
+ logger.exception("Error creating system")
2939
+ return {"error": str(e)}
2940
+
2941
+ async def _add_agent_to_system(self, args: dict, ctx: RunContext) -> dict:
2942
+ """Add an agent to a multi-agent system."""
2943
+ from django_agent_runtime.models import AgentSystem, AgentSystemMember
2944
+ from django_agent_runtime.services.multi_agent import add_agent_to_system
2945
+
2946
+ system_slug = args.get("system_slug")
2947
+ agent_slug = args.get("agent_slug")
2948
+
2949
+ if not system_slug or not agent_slug:
2950
+ return {"error": "system_slug and agent_slug are required"}
2951
+
2952
+ role = args.get("role", "specialist")
2953
+ notes = args.get("notes", "")
2954
+
2955
+ try:
2956
+ # Get the system
2957
+ try:
2958
+ system = await sync_to_async(AgentSystem.objects.get)(slug=system_slug)
2959
+ except AgentSystem.DoesNotExist:
2960
+ return {"error": f"System not found: {system_slug}"}
2961
+
2962
+ # Get the agent
2963
+ try:
2964
+ agent = await sync_to_async(AgentDefinition.objects.get)(slug=agent_slug)
2965
+ except AgentDefinition.DoesNotExist:
2966
+ return {"error": f"Agent not found: {agent_slug}"}
2967
+
2968
+ # Check if agent is already a member
2969
+ existing = await sync_to_async(
2970
+ system.members.filter(agent=agent).exists
2971
+ )()
2972
+ if existing:
2973
+ return {"error": f"Agent '{agent_slug}' is already a member of system '{system_slug}'"}
2974
+
2975
+ # Map role string to enum
2976
+ role_map = {
2977
+ "specialist": AgentSystemMember.Role.SPECIALIST,
2978
+ "utility": AgentSystemMember.Role.UTILITY,
2979
+ "supervisor": AgentSystemMember.Role.SUPERVISOR,
2980
+ "entry_point": AgentSystemMember.Role.ENTRY_POINT,
2981
+ }
2982
+ role_enum = role_map.get(role, AgentSystemMember.Role.SPECIALIST)
2983
+
2984
+ # Add the agent
2985
+ member = await sync_to_async(add_agent_to_system)(
2986
+ system=system,
2987
+ agent=agent,
2988
+ role=role_enum,
2989
+ notes=notes,
2990
+ )
2991
+
2992
+ return {
2993
+ "success": True,
2994
+ "message": f"Added agent '{agent_slug}' to system '{system_slug}' as {role}",
2995
+ "member_id": str(member.id),
2996
+ "agent_slug": agent_slug,
2997
+ "role": role,
2998
+ }
2999
+ except Exception as e:
3000
+ logger.exception("Error adding agent to system")
3001
+ return {"error": str(e)}
3002
+
3003
+ async def _remove_agent_from_system(self, args: dict, ctx: RunContext) -> dict:
3004
+ """Remove an agent from a multi-agent system."""
3005
+ from django_agent_runtime.models import AgentSystem
3006
+
3007
+ system_slug = args.get("system_slug")
3008
+ agent_slug = args.get("agent_slug")
3009
+
3010
+ if not system_slug or not agent_slug:
3011
+ return {"error": "system_slug and agent_slug are required"}
3012
+
3013
+ try:
3014
+ # Get the system
3015
+ try:
3016
+ system = await sync_to_async(AgentSystem.objects.get)(slug=system_slug)
3017
+ except AgentSystem.DoesNotExist:
3018
+ return {"error": f"System not found: {system_slug}"}
3019
+
3020
+ # Get the agent
3021
+ try:
3022
+ agent = await sync_to_async(AgentDefinition.objects.get)(slug=agent_slug)
3023
+ except AgentDefinition.DoesNotExist:
3024
+ return {"error": f"Agent not found: {agent_slug}"}
3025
+
3026
+ # Check if this is the entry agent
3027
+ entry_agent = await sync_to_async(lambda: system.entry_agent)()
3028
+ if entry_agent and entry_agent.slug == agent_slug:
3029
+ return {"error": f"Cannot remove entry agent '{agent_slug}'. Change the entry agent first."}
3030
+
3031
+ # Find and delete the membership
3032
+ member = await sync_to_async(
3033
+ system.members.filter(agent=agent).first
3034
+ )()
3035
+ if not member:
3036
+ return {"error": f"Agent '{agent_slug}' is not a member of system '{system_slug}'"}
3037
+
3038
+ await sync_to_async(member.delete)()
3039
+
3040
+ return {
3041
+ "success": True,
3042
+ "message": f"Removed agent '{agent_slug}' from system '{system_slug}'",
3043
+ }
3044
+ except Exception as e:
3045
+ logger.exception("Error removing agent from system")
3046
+ return {"error": str(e)}
3047
+
3048
+ async def _update_system_config(self, args: dict, ctx: RunContext) -> dict:
3049
+ """Update a multi-agent system's configuration."""
3050
+ from django_agent_runtime.models import AgentSystem
3051
+
3052
+ system_slug = args.get("system_slug")
3053
+ if not system_slug:
3054
+ return {"error": "system_slug is required"}
3055
+
3056
+ try:
3057
+ # Get the system
3058
+ try:
3059
+ system = await sync_to_async(AgentSystem.objects.get)(slug=system_slug)
3060
+ except AgentSystem.DoesNotExist:
3061
+ return {"error": f"System not found: {system_slug}"}
3062
+
3063
+ changes = []
3064
+
3065
+ # Update name
3066
+ if "name" in args:
3067
+ system.name = args["name"]
3068
+ changes.append(f"name='{args['name']}'")
3069
+
3070
+ # Update description
3071
+ if "description" in args:
3072
+ system.description = args["description"]
3073
+ changes.append("description updated")
3074
+
3075
+ # Update is_active
3076
+ if "is_active" in args:
3077
+ system.is_active = args["is_active"]
3078
+ changes.append(f"is_active={args['is_active']}")
3079
+
3080
+ # Update entry agent
3081
+ if "entry_agent_slug" in args:
3082
+ new_entry_slug = args["entry_agent_slug"]
3083
+ try:
3084
+ new_entry = await sync_to_async(AgentDefinition.objects.get)(slug=new_entry_slug)
3085
+ except AgentDefinition.DoesNotExist:
3086
+ return {"error": f"Entry agent not found: {new_entry_slug}"}
3087
+
3088
+ # Verify the new entry agent is a member
3089
+ is_member = await sync_to_async(
3090
+ system.members.filter(agent=new_entry).exists
3091
+ )()
3092
+ if not is_member:
3093
+ return {"error": f"Agent '{new_entry_slug}' must be a member of the system before becoming entry agent"}
3094
+
3095
+ system.entry_agent = new_entry
3096
+ changes.append(f"entry_agent='{new_entry_slug}'")
3097
+
3098
+ # Update shared knowledge
3099
+ if "shared_knowledge" in args:
3100
+ system.shared_knowledge = args["shared_knowledge"]
3101
+ changes.append(f"shared_knowledge ({len(args['shared_knowledge'])} items)")
3102
+
3103
+ if not changes:
3104
+ return {"message": "No changes specified"}
3105
+
3106
+ await sync_to_async(system.save)()
3107
+
3108
+ return {
3109
+ "success": True,
3110
+ "message": f"Updated system '{system_slug}': {', '.join(changes)}",
3111
+ "changes": changes,
3112
+ }
3113
+ except Exception as e:
3114
+ logger.exception("Error updating system config")
3115
+ return {"error": str(e)}
3116
+
3117
+ async def _delete_system(self, args: dict, ctx: RunContext) -> dict:
3118
+ """Delete a multi-agent system."""
3119
+ from django_agent_runtime.models import AgentSystem
3120
+
3121
+ system_slug = args.get("system_slug")
3122
+ confirm = args.get("confirm", False)
3123
+
3124
+ if not system_slug:
3125
+ return {"error": "system_slug is required"}
3126
+
3127
+ if not confirm:
3128
+ return {"error": "Must set confirm=true to delete a system"}
3129
+
3130
+ try:
3131
+ # Get the system
3132
+ try:
3133
+ system = await sync_to_async(AgentSystem.objects.get)(slug=system_slug)
3134
+ except AgentSystem.DoesNotExist:
3135
+ return {"error": f"System not found: {system_slug}"}
3136
+
3137
+ system_name = system.name
3138
+ member_count = await sync_to_async(system.members.count)()
3139
+
3140
+ # Delete the system (cascades to members, versions, snapshots)
3141
+ await sync_to_async(system.delete)()
3142
+
3143
+ return {
3144
+ "success": True,
3145
+ "message": f"Deleted system '{system_name}' ({system_slug}) with {member_count} member(s). Agents were NOT deleted.",
3146
+ }
3147
+ except Exception as e:
3148
+ logger.exception("Error deleting system")
3149
+ return {"error": str(e)}
3150
+
2351
3151
  # ==================== Spec Document Methods ====================
2352
3152
 
2353
3153
  async def _list_spec_documents(self, agent, args: dict, ctx: RunContext) -> dict: