django-agent-studio 0.1.5__py3-none-any.whl → 0.1.7__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.
@@ -40,6 +40,7 @@ You have access to tools that allow you to:
40
40
  7. **Create multi-agent systems** by adding sub-agent tools
41
41
  8. **Switch between agents and systems** in the UI
42
42
  9. **Configure agent memory** (enable/disable the remember tool)
43
+ 10. **Manage agent specifications** (human-readable behavior descriptions)
43
44
 
44
45
  ## IMPORTANT: Tool Usage
45
46
 
@@ -116,6 +117,59 @@ Agents have a built-in memory system that allows them to remember facts about us
116
117
  - Support agents that benefit from knowing user history
117
118
  - Any agent where personalization improves the experience
118
119
 
120
+ ## Agent Specification (Spec)
121
+
122
+ Every agent can have a **spec** - a human-readable description of its intended behavior, separate from the technical system prompt.
123
+
124
+ **Why use a spec?**
125
+ - **Human oversight**: Non-technical stakeholders can review and approve agent behavior
126
+ - **Documentation**: Clear record of what the agent should and shouldn't do
127
+ - **Builder context**: You can reference the spec when crafting the system prompt
128
+
129
+ **What to include in a spec:**
130
+ - Purpose: What is this agent for?
131
+ - Capabilities: What can it do?
132
+ - Constraints: What should it NOT do?
133
+ - Tone/personality: How should it communicate?
134
+ - Edge cases: How should it handle unusual situations?
135
+
136
+ **Simple spec tools (per-agent):**
137
+ - `get_agent_spec` - View the current agent's spec
138
+ - `update_agent_spec` - Update the current agent's spec
139
+
140
+ **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
+
142
+ ## Spec Document System (Advanced)
143
+
144
+ For organizations managing multiple agents, there's a **Spec Document System** that provides:
145
+ - **Document tree structure**: Organize specs hierarchically (e.g., "Company Agents" → "Support" → "Billing Agent")
146
+ - **Agent linking**: Link any document to an agent - changes sync automatically to the agent's spec
147
+ - **Version history**: Every change creates a new version with full history and rollback
148
+ - **Unified view**: Render the entire document tree as a single markdown document for human review
149
+
150
+ **Spec Document Tools:**
151
+ - `list_spec_documents` - List all spec documents
152
+ - `get_spec_document` - Get a document with its content
153
+ - `create_spec_document` - Create a new document (root or child)
154
+ - `update_spec_document` - Update content (auto-versions)
155
+ - `link_spec_to_agent` - Link a document to an agent
156
+ - `unlink_spec_from_agent` - Remove the link
157
+ - `get_spec_document_history` - View version history
158
+ - `restore_spec_document_version` - Rollback to a previous version
159
+ - `delete_spec_document` - Delete a document (and children)
160
+ - `render_full_spec` - Render all documents as one markdown file
161
+
162
+ **Example structure:**
163
+ ```
164
+ Company AI Agents (root document)
165
+ ├── Customer Support
166
+ │ ├── Billing Support → linked to billing-agent
167
+ │ └── Technical Support → linked to tech-agent
168
+ ├── Internal Tools
169
+ │ └── HR Assistant → linked to hr-agent
170
+ └── Guidelines (shared context for all agents)
171
+ ```
172
+
119
173
  When helping users:
120
174
  - Ask clarifying questions to understand what they want their agent to do
121
175
  - Suggest appropriate system prompts based on the agent's purpose
@@ -232,6 +286,34 @@ BUILDER_TOOLS = [
232
286
  },
233
287
  },
234
288
  },
289
+ {
290
+ "type": "function",
291
+ "function": {
292
+ "name": "get_agent_spec",
293
+ "description": "Get the agent's specification - a human-readable description of the agent's intended behavior, capabilities, and constraints. The spec is separate from the technical system prompt and is meant for human oversight and documentation.",
294
+ "parameters": {
295
+ "type": "object",
296
+ "properties": {},
297
+ },
298
+ },
299
+ },
300
+ {
301
+ "type": "function",
302
+ "function": {
303
+ "name": "update_agent_spec",
304
+ "description": "Update the agent's specification. The spec should describe in plain English: what the agent does, what it should and shouldn't do, its personality/tone, and any important constraints. This is for human oversight - non-technical stakeholders can review and edit this.",
305
+ "parameters": {
306
+ "type": "object",
307
+ "properties": {
308
+ "spec": {
309
+ "type": "string",
310
+ "description": "The agent specification in plain English. Describe the agent's purpose, capabilities, constraints, and expected behavior.",
311
+ },
312
+ },
313
+ "required": ["spec"],
314
+ },
315
+ },
316
+ },
235
317
  {
236
318
  "type": "function",
237
319
  "function": {
@@ -280,6 +362,231 @@ BUILDER_TOOLS = [
280
362
  },
281
363
  },
282
364
  },
365
+ # Spec Document tools
366
+ {
367
+ "type": "function",
368
+ "function": {
369
+ "name": "list_spec_documents",
370
+ "description": "List all spec documents, optionally filtered. Returns the document tree structure.",
371
+ "parameters": {
372
+ "type": "object",
373
+ "properties": {
374
+ "root_only": {
375
+ "type": "boolean",
376
+ "description": "If true, only return root documents (no parent). Default: false",
377
+ },
378
+ "linked_only": {
379
+ "type": "boolean",
380
+ "description": "If true, only return documents linked to agents. Default: false",
381
+ },
382
+ },
383
+ },
384
+ },
385
+ },
386
+ {
387
+ "type": "function",
388
+ "function": {
389
+ "name": "get_spec_document",
390
+ "description": "Get a spec document by ID, including its content and metadata.",
391
+ "parameters": {
392
+ "type": "object",
393
+ "properties": {
394
+ "document_id": {
395
+ "type": "string",
396
+ "description": "The UUID of the document to retrieve",
397
+ },
398
+ "include_children": {
399
+ "type": "boolean",
400
+ "description": "If true, include all descendant documents. Default: false",
401
+ },
402
+ "render_as_markdown": {
403
+ "type": "boolean",
404
+ "description": "If true, render the document tree as a single markdown document. Default: false",
405
+ },
406
+ },
407
+ "required": ["document_id"],
408
+ },
409
+ },
410
+ },
411
+ {
412
+ "type": "function",
413
+ "function": {
414
+ "name": "create_spec_document",
415
+ "description": "Create a new spec document. Can be a root document or a child of an existing document.",
416
+ "parameters": {
417
+ "type": "object",
418
+ "properties": {
419
+ "title": {
420
+ "type": "string",
421
+ "description": "Document title",
422
+ },
423
+ "content": {
424
+ "type": "string",
425
+ "description": "Markdown content of the document",
426
+ },
427
+ "parent_id": {
428
+ "type": "string",
429
+ "description": "Optional: UUID of parent document. If not provided, creates a root document.",
430
+ },
431
+ "linked_agent_id": {
432
+ "type": "string",
433
+ "description": "Optional: UUID of agent to link this document to. The document content will sync to the agent's spec.",
434
+ },
435
+ "order": {
436
+ "type": "integer",
437
+ "description": "Optional: Order among siblings. Default: 0",
438
+ },
439
+ },
440
+ "required": ["title"],
441
+ },
442
+ },
443
+ },
444
+ {
445
+ "type": "function",
446
+ "function": {
447
+ "name": "update_spec_document",
448
+ "description": "Update a spec document's content or metadata. Creates a new version automatically.",
449
+ "parameters": {
450
+ "type": "object",
451
+ "properties": {
452
+ "document_id": {
453
+ "type": "string",
454
+ "description": "The UUID of the document to update",
455
+ },
456
+ "title": {
457
+ "type": "string",
458
+ "description": "New title (optional)",
459
+ },
460
+ "content": {
461
+ "type": "string",
462
+ "description": "New markdown content (optional)",
463
+ },
464
+ "order": {
465
+ "type": "integer",
466
+ "description": "New order among siblings (optional)",
467
+ },
468
+ },
469
+ "required": ["document_id"],
470
+ },
471
+ },
472
+ },
473
+ {
474
+ "type": "function",
475
+ "function": {
476
+ "name": "link_spec_to_agent",
477
+ "description": "Link a spec document to an agent. The document's content will sync to the agent's spec field.",
478
+ "parameters": {
479
+ "type": "object",
480
+ "properties": {
481
+ "document_id": {
482
+ "type": "string",
483
+ "description": "The UUID of the document",
484
+ },
485
+ "agent_id": {
486
+ "type": "string",
487
+ "description": "The UUID of the agent to link to. Use 'current' for the agent being edited.",
488
+ },
489
+ },
490
+ "required": ["document_id", "agent_id"],
491
+ },
492
+ },
493
+ },
494
+ {
495
+ "type": "function",
496
+ "function": {
497
+ "name": "unlink_spec_from_agent",
498
+ "description": "Remove the link between a spec document and its agent.",
499
+ "parameters": {
500
+ "type": "object",
501
+ "properties": {
502
+ "document_id": {
503
+ "type": "string",
504
+ "description": "The UUID of the document to unlink",
505
+ },
506
+ },
507
+ "required": ["document_id"],
508
+ },
509
+ },
510
+ },
511
+ {
512
+ "type": "function",
513
+ "function": {
514
+ "name": "get_spec_document_history",
515
+ "description": "Get the version history of a spec document.",
516
+ "parameters": {
517
+ "type": "object",
518
+ "properties": {
519
+ "document_id": {
520
+ "type": "string",
521
+ "description": "The UUID of the document",
522
+ },
523
+ "limit": {
524
+ "type": "integer",
525
+ "description": "Maximum number of versions to return. Default: 10",
526
+ },
527
+ },
528
+ "required": ["document_id"],
529
+ },
530
+ },
531
+ },
532
+ {
533
+ "type": "function",
534
+ "function": {
535
+ "name": "restore_spec_document_version",
536
+ "description": "Restore a spec document to a previous version. Creates a new version with the restored content.",
537
+ "parameters": {
538
+ "type": "object",
539
+ "properties": {
540
+ "document_id": {
541
+ "type": "string",
542
+ "description": "The UUID of the document",
543
+ },
544
+ "version_number": {
545
+ "type": "integer",
546
+ "description": "The version number to restore to",
547
+ },
548
+ },
549
+ "required": ["document_id", "version_number"],
550
+ },
551
+ },
552
+ },
553
+ {
554
+ "type": "function",
555
+ "function": {
556
+ "name": "delete_spec_document",
557
+ "description": "Delete a spec document. If it has children, they will also be deleted.",
558
+ "parameters": {
559
+ "type": "object",
560
+ "properties": {
561
+ "document_id": {
562
+ "type": "string",
563
+ "description": "The UUID of the document to delete",
564
+ },
565
+ "confirm": {
566
+ "type": "boolean",
567
+ "description": "Must be true to confirm deletion",
568
+ },
569
+ },
570
+ "required": ["document_id", "confirm"],
571
+ },
572
+ },
573
+ },
574
+ {
575
+ "type": "function",
576
+ "function": {
577
+ "name": "render_full_spec",
578
+ "description": "Render all spec documents (or a subtree) as a single unified markdown document for human review.",
579
+ "parameters": {
580
+ "type": "object",
581
+ "properties": {
582
+ "root_document_id": {
583
+ "type": "string",
584
+ "description": "Optional: Start from this document. If not provided, renders all root documents.",
585
+ },
586
+ },
587
+ },
588
+ },
589
+ },
283
590
  {
284
591
  "type": "function",
285
592
  "function": {
@@ -943,6 +1250,23 @@ Knowledge: {len(config.get('knowledge', []))} sources
943
1250
  await create_revision(agent, comment=f"Renamed from '{old_name}' to '{agent.name}'")
944
1251
  return {"success": True, "message": "Agent name updated"}
945
1252
 
1253
+ elif tool_name == "get_agent_spec":
1254
+ return {
1255
+ "spec": agent.spec or "",
1256
+ "has_spec": bool(agent.spec),
1257
+ "message": "The agent spec describes intended behavior in plain English, separate from the technical system prompt.",
1258
+ }
1259
+
1260
+ elif tool_name == "update_agent_spec":
1261
+ agent.spec = args["spec"]
1262
+ await sync_to_async(agent.save)()
1263
+ await create_revision(agent, comment="Updated agent specification")
1264
+ return {
1265
+ "success": True,
1266
+ "message": "Agent specification updated",
1267
+ "spec_preview": agent.spec[:200] + "..." if len(agent.spec) > 200 else agent.spec,
1268
+ }
1269
+
946
1270
  elif tool_name == "update_model_settings":
947
1271
  if version:
948
1272
  changes = []
@@ -984,6 +1308,37 @@ Knowledge: {len(config.get('knowledge', []))} sources
984
1308
  }
985
1309
  return {"error": "No active version found"}
986
1310
 
1311
+ # Spec Document tools
1312
+ elif tool_name == "list_spec_documents":
1313
+ return await self._list_spec_documents(agent, args, ctx)
1314
+
1315
+ elif tool_name == "get_spec_document":
1316
+ return await self._get_spec_document(agent, args, ctx)
1317
+
1318
+ elif tool_name == "create_spec_document":
1319
+ return await self._create_spec_document(agent, args, ctx)
1320
+
1321
+ elif tool_name == "update_spec_document":
1322
+ return await self._update_spec_document(agent, args, ctx)
1323
+
1324
+ elif tool_name == "link_spec_to_agent":
1325
+ return await self._link_spec_to_agent(agent, args, ctx)
1326
+
1327
+ elif tool_name == "unlink_spec_from_agent":
1328
+ return await self._unlink_spec_from_agent(agent, args, ctx)
1329
+
1330
+ elif tool_name == "get_spec_document_history":
1331
+ return await self._get_spec_document_history(agent, args, ctx)
1332
+
1333
+ elif tool_name == "restore_spec_document_version":
1334
+ return await self._restore_spec_document_version(agent, args, ctx)
1335
+
1336
+ elif tool_name == "delete_spec_document":
1337
+ return await self._delete_spec_document(agent, args, ctx)
1338
+
1339
+ elif tool_name == "render_full_spec":
1340
+ return await self._render_full_spec(agent, args, ctx)
1341
+
987
1342
  elif tool_name == "add_knowledge":
988
1343
  inclusion_mode = args.get("inclusion_mode", "always")
989
1344
  knowledge = await sync_to_async(AgentKnowledge.objects.create)(
@@ -1992,3 +2347,385 @@ Knowledge: {len(config.get('knowledge', []))} sources
1992
2347
  except Exception as e:
1993
2348
  logger.exception("Error getting system details")
1994
2349
  return {"error": str(e)}
2350
+
2351
+ # ==================== Spec Document Methods ====================
2352
+
2353
+ async def _list_spec_documents(self, agent, args: dict, ctx: RunContext) -> dict:
2354
+ """List spec documents with optional filtering."""
2355
+ from django_agent_runtime.models import SpecDocument
2356
+
2357
+ try:
2358
+ root_only = args.get("root_only", False)
2359
+ linked_only = args.get("linked_only", False)
2360
+
2361
+ if root_only:
2362
+ docs = await sync_to_async(list)(
2363
+ SpecDocument.objects.filter(parent__isnull=True)
2364
+ .select_related("linked_agent")
2365
+ .order_by("order", "title")
2366
+ )
2367
+ else:
2368
+ docs = await sync_to_async(list)(
2369
+ SpecDocument.objects.all()
2370
+ .select_related("linked_agent", "parent")
2371
+ .order_by("parent_id", "order", "title")
2372
+ )
2373
+
2374
+ if linked_only:
2375
+ docs = [d for d in docs if d.linked_agent_id]
2376
+
2377
+ def format_doc(doc):
2378
+ result = {
2379
+ "id": str(doc.id),
2380
+ "title": doc.title,
2381
+ "parent_id": str(doc.parent_id) if doc.parent_id else None,
2382
+ "order": doc.order,
2383
+ "current_version": doc.current_version,
2384
+ "has_content": bool(doc.content),
2385
+ "content_preview": doc.content[:100] + "..." if len(doc.content) > 100 else doc.content,
2386
+ }
2387
+ if doc.linked_agent:
2388
+ result["linked_agent"] = {
2389
+ "id": str(doc.linked_agent.id),
2390
+ "slug": doc.linked_agent.slug,
2391
+ "name": doc.linked_agent.name,
2392
+ }
2393
+ return result
2394
+
2395
+ return {
2396
+ "documents": [format_doc(d) for d in docs],
2397
+ "count": len(docs),
2398
+ }
2399
+ except Exception as e:
2400
+ logger.exception("Error listing spec documents")
2401
+ return {"error": str(e)}
2402
+
2403
+ async def _get_spec_document(self, agent, args: dict, ctx: RunContext) -> dict:
2404
+ """Get a spec document by ID."""
2405
+ from django_agent_runtime.models import SpecDocument
2406
+
2407
+ try:
2408
+ doc_id = args["document_id"]
2409
+ include_children = args.get("include_children", False)
2410
+ render_markdown = args.get("render_as_markdown", False)
2411
+
2412
+ doc = await sync_to_async(
2413
+ SpecDocument.objects.select_related("linked_agent", "parent").get
2414
+ )(id=doc_id)
2415
+
2416
+ result = {
2417
+ "id": str(doc.id),
2418
+ "title": doc.title,
2419
+ "content": doc.content,
2420
+ "parent_id": str(doc.parent_id) if doc.parent_id else None,
2421
+ "order": doc.order,
2422
+ "current_version": doc.current_version,
2423
+ "full_path": await sync_to_async(doc.get_full_path)(),
2424
+ "created_at": doc.created_at.isoformat(),
2425
+ "updated_at": doc.updated_at.isoformat(),
2426
+ }
2427
+
2428
+ if doc.linked_agent:
2429
+ result["linked_agent"] = {
2430
+ "id": str(doc.linked_agent.id),
2431
+ "slug": doc.linked_agent.slug,
2432
+ "name": doc.linked_agent.name,
2433
+ }
2434
+
2435
+ if include_children:
2436
+ descendants = await sync_to_async(doc.get_descendants)()
2437
+ result["children"] = [
2438
+ {
2439
+ "id": str(d.id),
2440
+ "title": d.title,
2441
+ "parent_id": str(d.parent_id) if d.parent_id else None,
2442
+ "has_content": bool(d.content),
2443
+ "linked_agent_slug": d.linked_agent.slug if d.linked_agent else None,
2444
+ }
2445
+ for d in descendants
2446
+ ]
2447
+
2448
+ if render_markdown:
2449
+ result["rendered_markdown"] = await sync_to_async(doc.render_tree_as_markdown)()
2450
+
2451
+ return result
2452
+ except SpecDocument.DoesNotExist:
2453
+ return {"error": f"Document not found: {args.get('document_id')}"}
2454
+ except Exception as e:
2455
+ logger.exception("Error getting spec document")
2456
+ return {"error": str(e)}
2457
+
2458
+ async def _create_spec_document(self, agent, args: dict, ctx: RunContext) -> dict:
2459
+ """Create a new spec document."""
2460
+ from django_agent_runtime.models import SpecDocument, AgentDefinition
2461
+
2462
+ try:
2463
+ title = args["title"]
2464
+ content = args.get("content", "")
2465
+ parent_id = args.get("parent_id")
2466
+ linked_agent_id = args.get("linked_agent_id")
2467
+ order = args.get("order", 0)
2468
+
2469
+ # Get parent if specified
2470
+ parent = None
2471
+ if parent_id:
2472
+ parent = await sync_to_async(SpecDocument.objects.get)(id=parent_id)
2473
+
2474
+ # Get linked agent if specified
2475
+ linked_agent = None
2476
+ if linked_agent_id:
2477
+ linked_agent = await sync_to_async(AgentDefinition.objects.get)(id=linked_agent_id)
2478
+
2479
+ # Create document
2480
+ doc = await sync_to_async(SpecDocument.objects.create)(
2481
+ title=title,
2482
+ content=content,
2483
+ parent=parent,
2484
+ linked_agent=linked_agent,
2485
+ order=order,
2486
+ owner=ctx.user if ctx.user and ctx.user.is_authenticated else None,
2487
+ )
2488
+
2489
+ return {
2490
+ "success": True,
2491
+ "message": f"Created spec document: {title}",
2492
+ "document_id": str(doc.id),
2493
+ "version": doc.current_version,
2494
+ "linked_agent": linked_agent.slug if linked_agent else None,
2495
+ }
2496
+ except SpecDocument.DoesNotExist:
2497
+ return {"error": f"Parent document not found: {parent_id}"}
2498
+ except AgentDefinition.DoesNotExist:
2499
+ return {"error": f"Agent not found: {linked_agent_id}"}
2500
+ except Exception as e:
2501
+ logger.exception("Error creating spec document")
2502
+ return {"error": str(e)}
2503
+
2504
+ async def _update_spec_document(self, agent, args: dict, ctx: RunContext) -> dict:
2505
+ """Update a spec document."""
2506
+ from django_agent_runtime.models import SpecDocument
2507
+
2508
+ try:
2509
+ doc_id = args["document_id"]
2510
+ doc = await sync_to_async(SpecDocument.objects.get)(id=doc_id)
2511
+
2512
+ changes = []
2513
+ if "title" in args:
2514
+ doc.title = args["title"]
2515
+ changes.append("title")
2516
+ if "content" in args:
2517
+ doc.content = args["content"]
2518
+ changes.append("content")
2519
+ if "order" in args:
2520
+ doc.order = args["order"]
2521
+ changes.append("order")
2522
+
2523
+ if changes:
2524
+ await sync_to_async(doc.save)()
2525
+ return {
2526
+ "success": True,
2527
+ "message": f"Updated spec document: {', '.join(changes)}",
2528
+ "document_id": str(doc.id),
2529
+ "new_version": doc.current_version,
2530
+ }
2531
+ else:
2532
+ return {"message": "No changes specified"}
2533
+ except SpecDocument.DoesNotExist:
2534
+ return {"error": f"Document not found: {args.get('document_id')}"}
2535
+ except Exception as e:
2536
+ logger.exception("Error updating spec document")
2537
+ return {"error": str(e)}
2538
+
2539
+ async def _link_spec_to_agent(self, agent, args: dict, ctx: RunContext) -> dict:
2540
+ """Link a spec document to an agent."""
2541
+ from django_agent_runtime.models import SpecDocument, AgentDefinition
2542
+
2543
+ try:
2544
+ doc_id = args["document_id"]
2545
+ agent_id = args["agent_id"]
2546
+
2547
+ doc = await sync_to_async(SpecDocument.objects.get)(id=doc_id)
2548
+
2549
+ # Handle 'current' as the agent being edited
2550
+ if agent_id == "current":
2551
+ target_agent = agent
2552
+ else:
2553
+ target_agent = await sync_to_async(AgentDefinition.objects.get)(id=agent_id)
2554
+
2555
+ doc.linked_agent = target_agent
2556
+ await sync_to_async(doc.save)()
2557
+
2558
+ return {
2559
+ "success": True,
2560
+ "message": f"Linked document '{doc.title}' to agent '{target_agent.name}'",
2561
+ "document_id": str(doc.id),
2562
+ "agent_id": str(target_agent.id),
2563
+ "agent_slug": target_agent.slug,
2564
+ }
2565
+ except SpecDocument.DoesNotExist:
2566
+ return {"error": f"Document not found: {args.get('document_id')}"}
2567
+ except AgentDefinition.DoesNotExist:
2568
+ return {"error": f"Agent not found: {args.get('agent_id')}"}
2569
+ except Exception as e:
2570
+ logger.exception("Error linking spec to agent")
2571
+ return {"error": str(e)}
2572
+
2573
+ async def _unlink_spec_from_agent(self, agent, args: dict, ctx: RunContext) -> dict:
2574
+ """Unlink a spec document from its agent."""
2575
+ from django_agent_runtime.models import SpecDocument
2576
+
2577
+ try:
2578
+ doc_id = args["document_id"]
2579
+ doc = await sync_to_async(
2580
+ SpecDocument.objects.select_related("linked_agent").get
2581
+ )(id=doc_id)
2582
+
2583
+ if not doc.linked_agent:
2584
+ return {"message": "Document is not linked to any agent"}
2585
+
2586
+ old_agent_name = doc.linked_agent.name
2587
+ doc.linked_agent = None
2588
+ await sync_to_async(doc.save)()
2589
+
2590
+ return {
2591
+ "success": True,
2592
+ "message": f"Unlinked document '{doc.title}' from agent '{old_agent_name}'",
2593
+ "document_id": str(doc.id),
2594
+ }
2595
+ except SpecDocument.DoesNotExist:
2596
+ return {"error": f"Document not found: {args.get('document_id')}"}
2597
+ except Exception as e:
2598
+ logger.exception("Error unlinking spec from agent")
2599
+ return {"error": str(e)}
2600
+
2601
+ async def _get_spec_document_history(self, agent, args: dict, ctx: RunContext) -> dict:
2602
+ """Get version history of a spec document."""
2603
+ from django_agent_runtime.models import SpecDocument, SpecDocumentVersion
2604
+
2605
+ try:
2606
+ doc_id = args["document_id"]
2607
+ limit = args.get("limit", 10)
2608
+
2609
+ doc = await sync_to_async(SpecDocument.objects.get)(id=doc_id)
2610
+ versions = await sync_to_async(list)(
2611
+ doc.versions.all()[:limit]
2612
+ )
2613
+
2614
+ return {
2615
+ "document_id": str(doc.id),
2616
+ "document_title": doc.title,
2617
+ "current_version": doc.current_version,
2618
+ "versions": [
2619
+ {
2620
+ "version_number": v.version_number,
2621
+ "title": v.title,
2622
+ "content_preview": v.content[:100] + "..." if len(v.content) > 100 else v.content,
2623
+ "created_at": v.created_at.isoformat(),
2624
+ "change_summary": v.change_summary or "",
2625
+ }
2626
+ for v in versions
2627
+ ],
2628
+ }
2629
+ except SpecDocument.DoesNotExist:
2630
+ return {"error": f"Document not found: {args.get('document_id')}"}
2631
+ except Exception as e:
2632
+ logger.exception("Error getting spec document history")
2633
+ return {"error": str(e)}
2634
+
2635
+ async def _restore_spec_document_version(self, agent, args: dict, ctx: RunContext) -> dict:
2636
+ """Restore a spec document to a previous version."""
2637
+ from django_agent_runtime.models import SpecDocument, SpecDocumentVersion
2638
+
2639
+ try:
2640
+ doc_id = args["document_id"]
2641
+ version_number = args["version_number"]
2642
+
2643
+ doc = await sync_to_async(SpecDocument.objects.get)(id=doc_id)
2644
+ version = await sync_to_async(doc.versions.get)(version_number=version_number)
2645
+
2646
+ # Restore creates a new version
2647
+ await sync_to_async(version.restore)()
2648
+
2649
+ # Refresh doc to get new version number
2650
+ await sync_to_async(doc.refresh_from_db)()
2651
+
2652
+ return {
2653
+ "success": True,
2654
+ "message": f"Restored document to version {version_number}",
2655
+ "document_id": str(doc.id),
2656
+ "restored_from_version": version_number,
2657
+ "new_version": doc.current_version,
2658
+ }
2659
+ except SpecDocument.DoesNotExist:
2660
+ return {"error": f"Document not found: {args.get('document_id')}"}
2661
+ except SpecDocumentVersion.DoesNotExist:
2662
+ return {"error": f"Version not found: {args.get('version_number')}"}
2663
+ except Exception as e:
2664
+ logger.exception("Error restoring spec document version")
2665
+ return {"error": str(e)}
2666
+
2667
+ async def _delete_spec_document(self, agent, args: dict, ctx: RunContext) -> dict:
2668
+ """Delete a spec document."""
2669
+ from django_agent_runtime.models import SpecDocument
2670
+
2671
+ try:
2672
+ doc_id = args["document_id"]
2673
+ confirm = args.get("confirm", False)
2674
+
2675
+ if not confirm:
2676
+ return {"error": "Must set confirm=true to delete a document"}
2677
+
2678
+ doc = await sync_to_async(SpecDocument.objects.get)(id=doc_id)
2679
+ title = doc.title
2680
+
2681
+ # Count children that will be deleted
2682
+ children_count = await sync_to_async(doc.children.count)()
2683
+
2684
+ await sync_to_async(doc.delete)()
2685
+
2686
+ return {
2687
+ "success": True,
2688
+ "message": f"Deleted document '{title}'" + (f" and {children_count} children" if children_count else ""),
2689
+ "deleted_children": children_count,
2690
+ }
2691
+ except SpecDocument.DoesNotExist:
2692
+ return {"error": f"Document not found: {args.get('document_id')}"}
2693
+ except Exception as e:
2694
+ logger.exception("Error deleting spec document")
2695
+ return {"error": str(e)}
2696
+
2697
+ async def _render_full_spec(self, agent, args: dict, ctx: RunContext) -> dict:
2698
+ """Render all spec documents as a single markdown document."""
2699
+ from django_agent_runtime.models import SpecDocument
2700
+
2701
+ try:
2702
+ root_id = args.get("root_document_id")
2703
+
2704
+ if root_id:
2705
+ # Render from specific root
2706
+ doc = await sync_to_async(SpecDocument.objects.get)(id=root_id)
2707
+ markdown = await sync_to_async(doc.render_tree_as_markdown)()
2708
+ return {
2709
+ "markdown": markdown,
2710
+ "root_document": doc.title,
2711
+ }
2712
+ else:
2713
+ # Render all root documents
2714
+ roots = await sync_to_async(list)(
2715
+ SpecDocument.objects.filter(parent__isnull=True).order_by("order", "title")
2716
+ )
2717
+
2718
+ parts = []
2719
+ for root in roots:
2720
+ parts.append(await sync_to_async(root.render_tree_as_markdown)())
2721
+
2722
+ return {
2723
+ "markdown": "\n\n---\n\n".join(parts),
2724
+ "root_count": len(roots),
2725
+ "roots": [{"id": str(r.id), "title": r.title} for r in roots],
2726
+ }
2727
+ except SpecDocument.DoesNotExist:
2728
+ return {"error": f"Document not found: {args.get('root_document_id')}"}
2729
+ except Exception as e:
2730
+ logger.exception("Error rendering full spec")
2731
+ return {"error": str(e)}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-agent-studio
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Visual agent builder and management studio for Django - build custom GPTs with a two-pane interface
5
5
  Author: Chris Barry
6
6
  License-Expression: MIT
@@ -3,7 +3,7 @@ django_agent_studio/apps.py,sha256=L89QWn4XOvPBs2z6qHAhaE4uZQpasJntYD75aSd2p_k,6
3
3
  django_agent_studio/urls.py,sha256=O_rrobaxpmg8gR0JmeUzQ0TI3E8mGdcz27p4OJlcI8s,743
4
4
  django_agent_studio/views.py,sha256=bQxSIeL9-D4MzKwWkeHEFXrzo1CD911qicrt_A8t_6M,3153
5
5
  django_agent_studio/agents/__init__.py,sha256=VYL_ato0DtggIo4BGRkyiz9cm1ARPXhhTQFzoG__NVM,800
6
- django_agent_studio/agents/builder.py,sha256=Kpn16CK_5JuPJT02UO0VsTPhNusY2v4LrWA2Dcvs7WM,80594
6
+ django_agent_studio/agents/builder.py,sha256=LwREdCELIYh5hMoDBQVGtpNZ5k7yMUcYsaqONQ79mEg,110635
7
7
  django_agent_studio/agents/dynamic.py,sha256=KhmdAHkYUdfuuBgsKeY-Sf9sjDA-QiBVWHpw0NAgPX0,13939
8
8
  django_agent_studio/api/__init__.py,sha256=vtBwuvBENyFFhFqCWyFsI6cYu4N9ZGqSMmHIRhr9a_U,45
9
9
  django_agent_studio/api/permissions.py,sha256=MutmA8TxZb4ZwGfeEoolK-QI04Gbcxs7DPNzkXe_Bss,5302
@@ -26,7 +26,7 @@ django_agent_studio/templates/django_agent_studio/base.html,sha256=rpHmr7CAWGwRk
26
26
  django_agent_studio/templates/django_agent_studio/builder.html,sha256=L2KX-RLVpnC2mmYsIl_aSeJIc6b63knTYVrDg8aaU1g,61425
27
27
  django_agent_studio/templates/django_agent_studio/home.html,sha256=pgwKPjiQe9UxYYLGSsKT-6erEPDUdW9MZ5D-MOjIxK4,4385
28
28
  django_agent_studio/templates/django_agent_studio/test.html,sha256=h9aTtTD1eYcgG-n410qMSryHdpXyKny0hjiKYAoGsic,3779
29
- django_agent_studio-0.1.5.dist-info/METADATA,sha256=_2hWCRBbGlZNCwlNNzIxeaT2jvjPiOhQI9krgm-zoOw,11294
30
- django_agent_studio-0.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
31
- django_agent_studio-0.1.5.dist-info/top_level.txt,sha256=O1kqZzXPOsJlqnPSAcB2fH5WpJNY8ZNfHEJzX9_SZ0A,20
32
- django_agent_studio-0.1.5.dist-info/RECORD,,
29
+ django_agent_studio-0.1.7.dist-info/METADATA,sha256=w8VyEVJK5oLHDLnxoRj1gDe34TH8Q9i4vbv37UNomAo,11294
30
+ django_agent_studio-0.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
31
+ django_agent_studio-0.1.7.dist-info/top_level.txt,sha256=O1kqZzXPOsJlqnPSAcB2fH5WpJNY8ZNfHEJzX9_SZ0A,20
32
+ django_agent_studio-0.1.7.dist-info/RECORD,,