django-agent-studio 0.1.5__py3-none-any.whl → 0.1.9__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.
@@ -242,4 +242,55 @@ urlpatterns = [
242
242
  views.AgentSystemDiscoverView.as_view(),
243
243
  name="discover_system",
244
244
  ),
245
+
246
+ # ==========================================================================
247
+ # Spec Document Endpoints
248
+ # ==========================================================================
249
+
250
+ # List and create spec documents
251
+ path(
252
+ "spec-documents/",
253
+ views.SpecDocumentListCreateView.as_view(),
254
+ name="spec_document_list",
255
+ ),
256
+ path(
257
+ "spec-documents/<uuid:pk>/",
258
+ views.SpecDocumentDetailView.as_view(),
259
+ name="spec_document_detail",
260
+ ),
261
+
262
+ # Link/unlink to agent
263
+ path(
264
+ "spec-documents/<uuid:pk>/link/",
265
+ views.SpecDocumentLinkView.as_view(),
266
+ name="spec_document_link",
267
+ ),
268
+
269
+ # Version history
270
+ path(
271
+ "spec-documents/<uuid:pk>/history/",
272
+ views.SpecDocumentHistoryView.as_view(),
273
+ name="spec_document_history",
274
+ ),
275
+
276
+ # Restore version
277
+ path(
278
+ "spec-documents/<uuid:pk>/restore/",
279
+ views.SpecDocumentRestoreView.as_view(),
280
+ name="spec_document_restore",
281
+ ),
282
+
283
+ # Tree view
284
+ path(
285
+ "spec-documents/tree/",
286
+ views.SpecDocumentTreeView.as_view(),
287
+ name="spec_document_tree",
288
+ ),
289
+
290
+ # Render as markdown
291
+ path(
292
+ "spec-documents/render/",
293
+ views.SpecDocumentRenderView.as_view(),
294
+ name="spec_document_render",
295
+ ),
245
296
  ]
@@ -1546,3 +1546,382 @@ class AgentSystemDiscoverView(APIView):
1546
1546
  ],
1547
1547
  'total_count': len(agents),
1548
1548
  })
1549
+
1550
+
1551
+ # =============================================================================
1552
+ # Spec Document Views
1553
+ # =============================================================================
1554
+
1555
+ class SpecDocumentListCreateView(APIView):
1556
+ """List all spec documents or create a new one."""
1557
+ permission_classes = [IsAuthenticated]
1558
+
1559
+ def get(self, request):
1560
+ from django_agent_runtime.models import SpecDocument
1561
+
1562
+ root_only = request.query_params.get('root_only', 'false').lower() == 'true'
1563
+ linked_only = request.query_params.get('linked_only', 'false').lower() == 'true'
1564
+
1565
+ if root_only:
1566
+ docs = SpecDocument.objects.filter(parent__isnull=True).select_related('linked_agent').order_by('order', 'title')
1567
+ else:
1568
+ docs = SpecDocument.objects.all().select_related('linked_agent', 'parent').order_by('parent_id', 'order', 'title')
1569
+
1570
+ if linked_only:
1571
+ docs = docs.filter(linked_agent__isnull=False)
1572
+
1573
+ result = []
1574
+ for doc in docs:
1575
+ item = {
1576
+ 'id': str(doc.id),
1577
+ 'title': doc.title,
1578
+ 'parent_id': str(doc.parent_id) if doc.parent_id else None,
1579
+ 'order': doc.order,
1580
+ 'current_version': doc.current_version,
1581
+ 'has_content': bool(doc.content),
1582
+ 'content_preview': doc.content[:100] + '...' if len(doc.content) > 100 else doc.content,
1583
+ 'created_at': doc.created_at.isoformat(),
1584
+ 'updated_at': doc.updated_at.isoformat(),
1585
+ }
1586
+ if doc.linked_agent:
1587
+ item['linked_agent'] = {
1588
+ 'id': str(doc.linked_agent.id),
1589
+ 'slug': doc.linked_agent.slug,
1590
+ 'name': doc.linked_agent.name,
1591
+ }
1592
+ result.append(item)
1593
+
1594
+ return Response({'documents': result, 'count': len(result)})
1595
+
1596
+ def post(self, request):
1597
+ from django_agent_runtime.models import SpecDocument
1598
+
1599
+ title = request.data.get('title')
1600
+ if not title:
1601
+ return Response({'error': 'title is required'}, status=status.HTTP_400_BAD_REQUEST)
1602
+
1603
+ content = request.data.get('content', '')
1604
+ parent_id = request.data.get('parent_id')
1605
+ linked_agent_id = request.data.get('linked_agent_id')
1606
+ order = request.data.get('order', 0)
1607
+
1608
+ parent = None
1609
+ if parent_id:
1610
+ try:
1611
+ parent = SpecDocument.objects.get(id=parent_id)
1612
+ except SpecDocument.DoesNotExist:
1613
+ return Response({'error': f'Parent document not found: {parent_id}'}, status=status.HTTP_404_NOT_FOUND)
1614
+
1615
+ linked_agent = None
1616
+ if linked_agent_id:
1617
+ try:
1618
+ linked_agent = AgentDefinition.objects.get(id=linked_agent_id)
1619
+ except AgentDefinition.DoesNotExist:
1620
+ return Response({'error': f'Agent not found: {linked_agent_id}'}, status=status.HTTP_404_NOT_FOUND)
1621
+
1622
+ doc = SpecDocument.objects.create(
1623
+ title=title,
1624
+ content=content,
1625
+ parent=parent,
1626
+ linked_agent=linked_agent,
1627
+ order=order,
1628
+ owner=request.user,
1629
+ )
1630
+
1631
+ return Response({
1632
+ 'id': str(doc.id),
1633
+ 'title': doc.title,
1634
+ 'current_version': doc.current_version,
1635
+ 'message': f'Created spec document: {title}',
1636
+ }, status=status.HTTP_201_CREATED)
1637
+
1638
+
1639
+ class SpecDocumentDetailView(APIView):
1640
+ """Get, update, or delete a spec document."""
1641
+ permission_classes = [IsAuthenticated]
1642
+
1643
+ def get(self, request, pk):
1644
+ from django_agent_runtime.models import SpecDocument
1645
+
1646
+ try:
1647
+ doc = SpecDocument.objects.select_related('linked_agent', 'parent').get(id=pk)
1648
+ except SpecDocument.DoesNotExist:
1649
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1650
+
1651
+ include_children = request.query_params.get('include_children', 'false').lower() == 'true'
1652
+ render_markdown = request.query_params.get('render_markdown', 'false').lower() == 'true'
1653
+
1654
+ result = {
1655
+ 'id': str(doc.id),
1656
+ 'title': doc.title,
1657
+ 'content': doc.content,
1658
+ 'parent_id': str(doc.parent_id) if doc.parent_id else None,
1659
+ 'order': doc.order,
1660
+ 'current_version': doc.current_version,
1661
+ 'full_path': doc.get_full_path(),
1662
+ 'created_at': doc.created_at.isoformat(),
1663
+ 'updated_at': doc.updated_at.isoformat(),
1664
+ }
1665
+
1666
+ if doc.linked_agent:
1667
+ result['linked_agent'] = {
1668
+ 'id': str(doc.linked_agent.id),
1669
+ 'slug': doc.linked_agent.slug,
1670
+ 'name': doc.linked_agent.name,
1671
+ }
1672
+
1673
+ if include_children:
1674
+ descendants = doc.get_descendants()
1675
+ result['children'] = [
1676
+ {
1677
+ 'id': str(d.id),
1678
+ 'title': d.title,
1679
+ 'parent_id': str(d.parent_id) if d.parent_id else None,
1680
+ 'has_content': bool(d.content),
1681
+ 'linked_agent_slug': d.linked_agent.slug if d.linked_agent else None,
1682
+ }
1683
+ for d in descendants
1684
+ ]
1685
+
1686
+ if render_markdown:
1687
+ result['rendered_markdown'] = doc.render_tree_as_markdown()
1688
+
1689
+ return Response(result)
1690
+
1691
+ def put(self, request, pk):
1692
+ from django_agent_runtime.models import SpecDocument
1693
+
1694
+ try:
1695
+ doc = SpecDocument.objects.get(id=pk)
1696
+ except SpecDocument.DoesNotExist:
1697
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1698
+
1699
+ changes = []
1700
+ if 'title' in request.data:
1701
+ doc.title = request.data['title']
1702
+ changes.append('title')
1703
+ if 'content' in request.data:
1704
+ doc.content = request.data['content']
1705
+ changes.append('content')
1706
+ if 'order' in request.data:
1707
+ doc.order = request.data['order']
1708
+ changes.append('order')
1709
+ if 'parent_id' in request.data:
1710
+ parent_id = request.data['parent_id']
1711
+ if parent_id:
1712
+ try:
1713
+ doc.parent = SpecDocument.objects.get(id=parent_id)
1714
+ except SpecDocument.DoesNotExist:
1715
+ return Response({'error': f'Parent not found: {parent_id}'}, status=status.HTTP_404_NOT_FOUND)
1716
+ else:
1717
+ doc.parent = None
1718
+ changes.append('parent')
1719
+
1720
+ if changes:
1721
+ doc.save()
1722
+ return Response({
1723
+ 'id': str(doc.id),
1724
+ 'title': doc.title,
1725
+ 'current_version': doc.current_version,
1726
+ 'changes': changes,
1727
+ 'message': f'Updated: {", ".join(changes)}',
1728
+ })
1729
+ else:
1730
+ return Response({'message': 'No changes specified'})
1731
+
1732
+ def delete(self, request, pk):
1733
+ from django_agent_runtime.models import SpecDocument
1734
+
1735
+ try:
1736
+ doc = SpecDocument.objects.get(id=pk)
1737
+ except SpecDocument.DoesNotExist:
1738
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1739
+
1740
+ title = doc.title
1741
+ children_count = doc.children.count()
1742
+ doc.delete()
1743
+
1744
+ return Response({
1745
+ 'message': f'Deleted document: {title}',
1746
+ 'deleted_children': children_count,
1747
+ })
1748
+
1749
+
1750
+ class SpecDocumentLinkView(APIView):
1751
+ """Link or unlink a spec document to/from an agent."""
1752
+ permission_classes = [IsAuthenticated]
1753
+
1754
+ def post(self, request, pk):
1755
+ from django_agent_runtime.models import SpecDocument
1756
+
1757
+ try:
1758
+ doc = SpecDocument.objects.get(id=pk)
1759
+ except SpecDocument.DoesNotExist:
1760
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1761
+
1762
+ agent_id = request.data.get('agent_id')
1763
+ if not agent_id:
1764
+ return Response({'error': 'agent_id is required'}, status=status.HTTP_400_BAD_REQUEST)
1765
+
1766
+ try:
1767
+ agent = AgentDefinition.objects.get(id=agent_id)
1768
+ except AgentDefinition.DoesNotExist:
1769
+ return Response({'error': f'Agent not found: {agent_id}'}, status=status.HTTP_404_NOT_FOUND)
1770
+
1771
+ doc.linked_agent = agent
1772
+ doc.save()
1773
+
1774
+ return Response({
1775
+ 'message': f"Linked '{doc.title}' to agent '{agent.name}'",
1776
+ 'document_id': str(doc.id),
1777
+ 'agent_id': str(agent.id),
1778
+ 'agent_slug': agent.slug,
1779
+ })
1780
+
1781
+ def delete(self, request, pk):
1782
+ from django_agent_runtime.models import SpecDocument
1783
+
1784
+ try:
1785
+ doc = SpecDocument.objects.select_related('linked_agent').get(id=pk)
1786
+ except SpecDocument.DoesNotExist:
1787
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1788
+
1789
+ if not doc.linked_agent:
1790
+ return Response({'message': 'Document is not linked to any agent'})
1791
+
1792
+ old_agent_name = doc.linked_agent.name
1793
+ doc.linked_agent = None
1794
+ doc.save()
1795
+
1796
+ return Response({
1797
+ 'message': f"Unlinked '{doc.title}' from agent '{old_agent_name}'",
1798
+ 'document_id': str(doc.id),
1799
+ })
1800
+
1801
+
1802
+ class SpecDocumentHistoryView(APIView):
1803
+ """Get version history of a spec document."""
1804
+ permission_classes = [IsAuthenticated]
1805
+
1806
+ def get(self, request, pk):
1807
+ from django_agent_runtime.models import SpecDocument
1808
+
1809
+ try:
1810
+ doc = SpecDocument.objects.get(id=pk)
1811
+ except SpecDocument.DoesNotExist:
1812
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1813
+
1814
+ limit = int(request.query_params.get('limit', 20))
1815
+ versions = doc.versions.all()[:limit]
1816
+
1817
+ return Response({
1818
+ 'document_id': str(doc.id),
1819
+ 'document_title': doc.title,
1820
+ 'current_version': doc.current_version,
1821
+ 'versions': [
1822
+ {
1823
+ 'version_number': v.version_number,
1824
+ 'title': v.title,
1825
+ 'content_preview': v.content[:200] + '...' if len(v.content) > 200 else v.content,
1826
+ 'created_at': v.created_at.isoformat(),
1827
+ 'change_summary': v.change_summary or '',
1828
+ }
1829
+ for v in versions
1830
+ ],
1831
+ })
1832
+
1833
+
1834
+ class SpecDocumentRestoreView(APIView):
1835
+ """Restore a spec document to a previous version."""
1836
+ permission_classes = [IsAuthenticated]
1837
+
1838
+ def post(self, request, pk):
1839
+ from django_agent_runtime.models import SpecDocument, SpecDocumentVersion
1840
+
1841
+ try:
1842
+ doc = SpecDocument.objects.get(id=pk)
1843
+ except SpecDocument.DoesNotExist:
1844
+ return Response({'error': 'Document not found'}, status=status.HTTP_404_NOT_FOUND)
1845
+
1846
+ version_number = request.data.get('version_number')
1847
+ if version_number is None:
1848
+ return Response({'error': 'version_number is required'}, status=status.HTTP_400_BAD_REQUEST)
1849
+
1850
+ try:
1851
+ version = doc.versions.get(version_number=version_number)
1852
+ except SpecDocumentVersion.DoesNotExist:
1853
+ return Response({'error': f'Version not found: {version_number}'}, status=status.HTTP_404_NOT_FOUND)
1854
+
1855
+ version.restore()
1856
+ doc.refresh_from_db()
1857
+
1858
+ return Response({
1859
+ 'message': f'Restored document to version {version_number}',
1860
+ 'document_id': str(doc.id),
1861
+ 'restored_from_version': version_number,
1862
+ 'new_version': doc.current_version,
1863
+ })
1864
+
1865
+
1866
+ class SpecDocumentTreeView(APIView):
1867
+ """Get the full document tree structure."""
1868
+ permission_classes = [IsAuthenticated]
1869
+
1870
+ def get(self, request):
1871
+ from django_agent_runtime.models import SpecDocument
1872
+
1873
+ def build_tree(parent_id=None):
1874
+ if parent_id:
1875
+ docs = SpecDocument.objects.filter(parent_id=parent_id).select_related('linked_agent').order_by('order', 'title')
1876
+ else:
1877
+ docs = SpecDocument.objects.filter(parent__isnull=True).select_related('linked_agent').order_by('order', 'title')
1878
+
1879
+ result = []
1880
+ for doc in docs:
1881
+ node = {
1882
+ 'id': str(doc.id),
1883
+ 'title': doc.title,
1884
+ 'has_content': bool(doc.content),
1885
+ 'order': doc.order,
1886
+ 'linked_agent': {
1887
+ 'id': str(doc.linked_agent.id),
1888
+ 'slug': doc.linked_agent.slug,
1889
+ 'name': doc.linked_agent.name,
1890
+ } if doc.linked_agent else None,
1891
+ 'children': build_tree(doc.id),
1892
+ }
1893
+ result.append(node)
1894
+ return result
1895
+
1896
+ tree = build_tree()
1897
+ return Response({'tree': tree})
1898
+
1899
+
1900
+ class SpecDocumentRenderView(APIView):
1901
+ """Render all spec documents as a single markdown document."""
1902
+ permission_classes = [IsAuthenticated]
1903
+
1904
+ def get(self, request):
1905
+ from django_agent_runtime.models import SpecDocument
1906
+
1907
+ root_id = request.query_params.get('root_id')
1908
+
1909
+ if root_id:
1910
+ try:
1911
+ doc = SpecDocument.objects.get(id=root_id)
1912
+ markdown = doc.render_tree_as_markdown()
1913
+ return Response({
1914
+ 'markdown': markdown,
1915
+ 'root_document': doc.title,
1916
+ })
1917
+ except SpecDocument.DoesNotExist:
1918
+ return Response({'error': f'Document not found: {root_id}'}, status=status.HTTP_404_NOT_FOUND)
1919
+ else:
1920
+ roots = SpecDocument.objects.filter(parent__isnull=True).order_by('order', 'title')
1921
+ parts = [root.render_tree_as_markdown() for root in roots]
1922
+
1923
+ return Response({
1924
+ 'markdown': '\n\n---\n\n'.join(parts),
1925
+ 'root_count': len(parts),
1926
+ 'roots': [{'id': str(r.id), 'title': r.title} for r in roots],
1927
+ })
@@ -118,7 +118,7 @@
118
118
  </div>
119
119
  <div class="flex items-center space-x-4">
120
120
  {% if request.user.is_authenticated %}
121
- <span class="text-sm text-gray-600">{{ request.user.email|default:request.user.username }}</span>
121
+ <span class="text-sm text-gray-600">{{ request.user.email|default:request.user }}</span>
122
122
  {% endif %}
123
123
  </div>
124
124
  </header>