django-agent-studio 0.1.0__py3-none-any.whl → 0.1.4__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.
- django_agent_studio/agents/__init__.py +29 -0
- django_agent_studio/agents/builder.py +1918 -0
- django_agent_studio/agents/dynamic.py +376 -0
- django_agent_studio/migrations/0001_initial.py +63 -0
- django_agent_studio/migrations/__init__.py +0 -0
- django_agent_studio/models/__init__.py +18 -0
- django_agent_studio/models/permissions.py +191 -0
- django_agent_studio/services/__init__.py +14 -0
- django_agent_studio/services/permissions.py +228 -0
- django_agent_studio/static/agent-frontend/chat-widget.css +48 -0
- django_agent_studio/static/agent-frontend/chat-widget.js +119 -100
- django_agent_studio/templates/django_agent_studio/base.html +3 -2
- django_agent_studio/templates/django_agent_studio/builder.html +69 -10
- {django_agent_studio-0.1.0.dist-info → django_agent_studio-0.1.4.dist-info}/METADATA +1 -1
- {django_agent_studio-0.1.0.dist-info → django_agent_studio-0.1.4.dist-info}/RECORD +17 -8
- {django_agent_studio-0.1.0.dist-info → django_agent_studio-0.1.4.dist-info}/WHEEL +1 -1
- {django_agent_studio-0.1.0.dist-info → django_agent_studio-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DynamicAgentRuntime - An agent that loads its configuration from the database.
|
|
3
|
+
|
|
4
|
+
This allows agents to be defined and modified via the AgentDefinition model
|
|
5
|
+
without requiring code changes.
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- RAG (Retrieval Augmented Generation) for knowledge sources with inclusion_mode='rag'
|
|
9
|
+
- Conversation-scoped memory via the 'remember' tool
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from agent_runtime_core.registry import AgentRuntime
|
|
18
|
+
from agent_runtime_core.interfaces import RunContext, RunResult, EventType
|
|
19
|
+
from agent_runtime_core.agentic_loop import run_agentic_loop
|
|
20
|
+
from django_agent_runtime.runtime.llm import get_llm_client_for_model, DEFAULT_MODEL
|
|
21
|
+
from django_agent_runtime.models import AgentDefinition
|
|
22
|
+
from django_agent_runtime.dynamic_tools.executor import DynamicToolExecutor
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# Memory Tool Definition
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
MEMORY_TOOL_SCHEMA = {
|
|
32
|
+
"type": "function",
|
|
33
|
+
"function": {
|
|
34
|
+
"name": "remember",
|
|
35
|
+
"description": (
|
|
36
|
+
"Store information to remember for this conversation. Use this to remember "
|
|
37
|
+
"important facts about the user, their preferences, project details, or anything "
|
|
38
|
+
"that would be useful to recall in future messages within this conversation. "
|
|
39
|
+
"Examples: user's name, their goals, preferences, important context."
|
|
40
|
+
),
|
|
41
|
+
"parameters": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"key": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": (
|
|
47
|
+
"A short, descriptive key for what you're remembering "
|
|
48
|
+
"(e.g., 'user_name', 'project_goal', 'preferred_language')"
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
"value": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "The information to remember",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"required": ["key", "value"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DynamicAgentRuntime(AgentRuntime):
|
|
63
|
+
"""
|
|
64
|
+
An agent runtime that loads its configuration from an AgentDefinition.
|
|
65
|
+
|
|
66
|
+
This allows agents to be created and modified via the database/API
|
|
67
|
+
without requiring code changes or deployments.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, agent_definition: AgentDefinition):
|
|
71
|
+
self._definition = agent_definition
|
|
72
|
+
self._config: Optional[dict] = None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def key(self) -> str:
|
|
76
|
+
"""Return the agent's slug as its key."""
|
|
77
|
+
return self._definition.slug
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def config(self) -> dict:
|
|
81
|
+
"""Get the effective configuration (cached)."""
|
|
82
|
+
if self._config is None:
|
|
83
|
+
self._config = self._definition.get_effective_config()
|
|
84
|
+
return self._config
|
|
85
|
+
|
|
86
|
+
def refresh_config(self):
|
|
87
|
+
"""Refresh the configuration from the database."""
|
|
88
|
+
self._definition.refresh_from_db()
|
|
89
|
+
self._config = None
|
|
90
|
+
|
|
91
|
+
async def run(self, ctx: RunContext) -> RunResult:
|
|
92
|
+
"""Execute the agent with the dynamic configuration and agentic loop."""
|
|
93
|
+
config = self.config
|
|
94
|
+
|
|
95
|
+
# Build the messages list
|
|
96
|
+
messages = []
|
|
97
|
+
|
|
98
|
+
# Add system prompt
|
|
99
|
+
system_prompt = config.get("system_prompt", "")
|
|
100
|
+
|
|
101
|
+
# Add knowledge that should always be included
|
|
102
|
+
knowledge_context = self._build_knowledge_context(config)
|
|
103
|
+
if knowledge_context:
|
|
104
|
+
system_prompt = f"{system_prompt}\n\n{knowledge_context}"
|
|
105
|
+
|
|
106
|
+
# Add RAG-retrieved knowledge based on user's query
|
|
107
|
+
rag_context = await self._retrieve_rag_knowledge(config, ctx)
|
|
108
|
+
if rag_context:
|
|
109
|
+
system_prompt = f"{system_prompt}\n\n{rag_context}"
|
|
110
|
+
|
|
111
|
+
# Add conversation memories (if we have a conversation_id and user)
|
|
112
|
+
memory_store = await self._get_memory_store(ctx)
|
|
113
|
+
if memory_store:
|
|
114
|
+
memory_context = await self._recall_memories(memory_store)
|
|
115
|
+
if memory_context:
|
|
116
|
+
system_prompt = f"{system_prompt}\n\n{memory_context}"
|
|
117
|
+
|
|
118
|
+
if system_prompt:
|
|
119
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
120
|
+
|
|
121
|
+
# Add conversation history
|
|
122
|
+
messages.extend(ctx.input_messages)
|
|
123
|
+
|
|
124
|
+
# Build tool schemas - include memory tool
|
|
125
|
+
tools = self._build_tool_schemas(config)
|
|
126
|
+
tools.append(MEMORY_TOOL_SCHEMA) # Add remember tool to all designed agents
|
|
127
|
+
|
|
128
|
+
tool_map = self._build_tool_map(config) # Maps tool name to execution info
|
|
129
|
+
|
|
130
|
+
# Get model: params override > agent config > default
|
|
131
|
+
model = ctx.params.get("model") or config.get("model") or DEFAULT_MODEL
|
|
132
|
+
model_settings = config.get("model_settings", {})
|
|
133
|
+
|
|
134
|
+
# Get LLM client for the model (auto-detects provider)
|
|
135
|
+
llm = get_llm_client_for_model(model)
|
|
136
|
+
|
|
137
|
+
# Initialize tool executor for dynamic tools
|
|
138
|
+
tool_executor = DynamicToolExecutor()
|
|
139
|
+
|
|
140
|
+
# Create tool executor function for the agentic loop
|
|
141
|
+
async def execute_tool(tool_name: str, tool_args: dict) -> str:
|
|
142
|
+
# Handle the built-in remember tool
|
|
143
|
+
if tool_name == "remember":
|
|
144
|
+
return await self._execute_remember_tool(tool_args, memory_store)
|
|
145
|
+
|
|
146
|
+
return await self._execute_tool(
|
|
147
|
+
tool_name, tool_args, tool_map, tool_executor, ctx
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Use the shared agentic loop
|
|
152
|
+
result = await run_agentic_loop(
|
|
153
|
+
llm=llm,
|
|
154
|
+
messages=messages,
|
|
155
|
+
tools=tools if tools else None,
|
|
156
|
+
execute_tool=execute_tool,
|
|
157
|
+
ctx=ctx,
|
|
158
|
+
model=model,
|
|
159
|
+
max_iterations=15,
|
|
160
|
+
**model_settings,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Emit the final assistant message
|
|
164
|
+
if result.final_content:
|
|
165
|
+
await ctx.emit(EventType.ASSISTANT_MESSAGE, {"content": result.final_content})
|
|
166
|
+
|
|
167
|
+
return RunResult(
|
|
168
|
+
final_output={"response": result.final_content},
|
|
169
|
+
final_messages=result.messages,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.exception(f"Error in DynamicAgentRuntime for {self.key}")
|
|
174
|
+
await ctx.emit(EventType.RUN_FAILED, {"error": str(e)})
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
async def _get_memory_store(self, ctx: RunContext) -> Optional["ConversationMemoryStore"]:
|
|
178
|
+
"""
|
|
179
|
+
Get the memory store for this conversation, if available.
|
|
180
|
+
|
|
181
|
+
Returns None if we don't have the required context (user, conversation_id).
|
|
182
|
+
"""
|
|
183
|
+
from django_agent_runtime.persistence.stores import ConversationMemoryStore
|
|
184
|
+
|
|
185
|
+
# Need both user and conversation_id for memory
|
|
186
|
+
user_id = ctx.metadata.get("user_id")
|
|
187
|
+
conversation_id = ctx.conversation_id
|
|
188
|
+
|
|
189
|
+
if not user_id or not conversation_id:
|
|
190
|
+
logger.debug(
|
|
191
|
+
f"Memory not available: user_id={user_id}, conversation_id={conversation_id}"
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Get the user object
|
|
197
|
+
from django.contrib.auth import get_user_model
|
|
198
|
+
from asgiref.sync import sync_to_async
|
|
199
|
+
|
|
200
|
+
User = get_user_model()
|
|
201
|
+
user = await sync_to_async(User.objects.get)(id=user_id)
|
|
202
|
+
|
|
203
|
+
return ConversationMemoryStore(user=user, conversation_id=conversation_id)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.warning(f"Failed to create memory store: {e}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
async def _recall_memories(self, memory_store: "ConversationMemoryStore") -> str:
|
|
209
|
+
"""
|
|
210
|
+
Recall all memories for this conversation and format for the prompt.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
memories = await memory_store.recall_all()
|
|
214
|
+
if memories:
|
|
215
|
+
logger.info(f"Recalled {len(memories)} memories for conversation")
|
|
216
|
+
return memory_store.format_for_prompt(memories)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.warning(f"Failed to recall memories: {e}")
|
|
219
|
+
return ""
|
|
220
|
+
|
|
221
|
+
async def _execute_remember_tool(
|
|
222
|
+
self,
|
|
223
|
+
args: dict,
|
|
224
|
+
memory_store: Optional["ConversationMemoryStore"],
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Execute the remember tool to store a memory."""
|
|
227
|
+
if not memory_store:
|
|
228
|
+
return json.dumps({
|
|
229
|
+
"error": "Memory not available for this conversation",
|
|
230
|
+
"hint": "Memory requires a logged-in user and conversation context",
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
key = args.get("key", "").strip()
|
|
234
|
+
value = args.get("value", "").strip()
|
|
235
|
+
|
|
236
|
+
if not key:
|
|
237
|
+
return json.dumps({"error": "Missing required parameter: key"})
|
|
238
|
+
if not value:
|
|
239
|
+
return json.dumps({"error": "Missing required parameter: value"})
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
await memory_store.remember(key, value, source="agent")
|
|
243
|
+
logger.info(f"Stored memory: {key}")
|
|
244
|
+
return json.dumps({
|
|
245
|
+
"success": True,
|
|
246
|
+
"message": f"Remembered: {key}",
|
|
247
|
+
})
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.exception(f"Failed to store memory: {key}")
|
|
250
|
+
return json.dumps({"error": str(e)})
|
|
251
|
+
|
|
252
|
+
def _build_knowledge_context(self, config: dict) -> str:
|
|
253
|
+
"""Build context string from always-included knowledge sources."""
|
|
254
|
+
parts = []
|
|
255
|
+
for knowledge in config.get("knowledge", []):
|
|
256
|
+
if knowledge.get("inclusion_mode") == "always":
|
|
257
|
+
content = knowledge.get("content")
|
|
258
|
+
if content:
|
|
259
|
+
name = knowledge.get("name", "Knowledge")
|
|
260
|
+
parts.append(f"## {name}\n{content}")
|
|
261
|
+
return "\n\n".join(parts)
|
|
262
|
+
|
|
263
|
+
async def _retrieve_rag_knowledge(self, config: dict, ctx: RunContext) -> str:
|
|
264
|
+
"""
|
|
265
|
+
Retrieve relevant knowledge using RAG for knowledge sources with inclusion_mode='rag'.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
config: The agent's effective configuration
|
|
269
|
+
ctx: The run context containing user messages
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Formatted string of retrieved knowledge, or empty string if no RAG knowledge
|
|
273
|
+
"""
|
|
274
|
+
# Check if there are any RAG knowledge sources
|
|
275
|
+
rag_knowledge = [
|
|
276
|
+
k for k in config.get("knowledge", [])
|
|
277
|
+
if k.get("inclusion_mode") == "rag" and k.get("rag", {}).get("status") == "indexed"
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
if not rag_knowledge:
|
|
281
|
+
return ""
|
|
282
|
+
|
|
283
|
+
# Get the user's query from the last user message
|
|
284
|
+
user_query = ""
|
|
285
|
+
for msg in reversed(ctx.input_messages):
|
|
286
|
+
if msg.get("role") == "user":
|
|
287
|
+
user_query = msg.get("content", "")
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if not user_query:
|
|
291
|
+
return ""
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
from django_agent_runtime.rag import KnowledgeRetriever
|
|
295
|
+
|
|
296
|
+
retriever = KnowledgeRetriever()
|
|
297
|
+
rag_config = config.get("rag_config", {})
|
|
298
|
+
|
|
299
|
+
# Retrieve relevant knowledge
|
|
300
|
+
context = await retriever.retrieve_for_agent(
|
|
301
|
+
agent_id=str(self._definition.id),
|
|
302
|
+
query=user_query,
|
|
303
|
+
rag_config=rag_config,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return context
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.warning(f"Error retrieving RAG knowledge: {e}")
|
|
310
|
+
return ""
|
|
311
|
+
|
|
312
|
+
def _build_tool_schemas(self, config: dict) -> list:
|
|
313
|
+
"""Build OpenAI-format tool schemas from config."""
|
|
314
|
+
schemas = []
|
|
315
|
+
for tool in config.get("tools", []):
|
|
316
|
+
# Skip the _meta field when building schema
|
|
317
|
+
schema = {
|
|
318
|
+
"type": tool.get("type", "function"),
|
|
319
|
+
"function": tool.get("function", {}),
|
|
320
|
+
}
|
|
321
|
+
schemas.append(schema)
|
|
322
|
+
return schemas
|
|
323
|
+
|
|
324
|
+
def _build_tool_map(self, config: dict) -> dict:
|
|
325
|
+
"""Build a map of tool name to execution info."""
|
|
326
|
+
tool_map = {}
|
|
327
|
+
for tool in config.get("tools", []):
|
|
328
|
+
func_info = tool.get("function", {})
|
|
329
|
+
tool_name = func_info.get("name")
|
|
330
|
+
if tool_name:
|
|
331
|
+
# Get execution metadata from _meta field
|
|
332
|
+
meta = tool.get("_meta", {})
|
|
333
|
+
tool_map[tool_name] = {
|
|
334
|
+
"function_path": meta.get("function_path"),
|
|
335
|
+
"tool_id": meta.get("tool_id"),
|
|
336
|
+
"is_dynamic": meta.get("is_dynamic", False),
|
|
337
|
+
}
|
|
338
|
+
return tool_map
|
|
339
|
+
|
|
340
|
+
async def _execute_tool(
|
|
341
|
+
self,
|
|
342
|
+
tool_name: str,
|
|
343
|
+
tool_args: dict,
|
|
344
|
+
tool_map: dict,
|
|
345
|
+
executor: "DynamicToolExecutor",
|
|
346
|
+
ctx: RunContext,
|
|
347
|
+
) -> str:
|
|
348
|
+
"""Execute a tool and return its result."""
|
|
349
|
+
tool_info = tool_map.get(tool_name, {})
|
|
350
|
+
function_path = tool_info.get("function_path")
|
|
351
|
+
|
|
352
|
+
if not function_path:
|
|
353
|
+
return json.dumps({"error": f"Tool '{tool_name}' has no function_path configured"})
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Get user_id from context metadata if available
|
|
357
|
+
user_id = ctx.metadata.get("user_id")
|
|
358
|
+
|
|
359
|
+
# Execute the tool
|
|
360
|
+
result = await executor.execute(
|
|
361
|
+
function_path=function_path,
|
|
362
|
+
arguments=tool_args,
|
|
363
|
+
agent_run_id=ctx.run_id,
|
|
364
|
+
user_id=user_id,
|
|
365
|
+
tool_id=tool_info.get("tool_id"),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Convert result to string if needed
|
|
369
|
+
if isinstance(result, str):
|
|
370
|
+
return result
|
|
371
|
+
return json.dumps(result)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.exception(f"Error executing tool {tool_name}")
|
|
375
|
+
return json.dumps({"error": str(e)})
|
|
376
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generated by Django 5.2.10 on 2026-01-23 14:27
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import uuid
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
('django_agent_runtime', '0007_discoveredfunction_dynamictool_dynamictoolexecution'),
|
|
15
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='ToolApprovalRequest',
|
|
21
|
+
fields=[
|
|
22
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
23
|
+
('proposed_name', models.CharField(max_length=100)),
|
|
24
|
+
('proposed_description', models.TextField()),
|
|
25
|
+
('proposed_is_safe', models.BooleanField(default=False)),
|
|
26
|
+
('proposed_requires_confirmation', models.BooleanField(default=True)),
|
|
27
|
+
('proposed_timeout_seconds', models.PositiveIntegerField(default=30)),
|
|
28
|
+
('request_reason', models.TextField(help_text='Why this tool is needed')),
|
|
29
|
+
('status', models.CharField(choices=[('pending', 'Pending Review'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('cancelled', 'Cancelled by Requester')], default='pending', max_length=20)),
|
|
30
|
+
('reviewed_at', models.DateTimeField(blank=True, null=True)),
|
|
31
|
+
('review_notes', models.TextField(blank=True)),
|
|
32
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
33
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
34
|
+
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tool_approval_requests', to='django_agent_runtime.agentdefinition')),
|
|
35
|
+
('created_tool', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approval_request', to='django_agent_runtime.dynamictool')),
|
|
36
|
+
('discovered_function', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approval_requests', to='django_agent_runtime.discoveredfunction')),
|
|
37
|
+
('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tool_approval_requests', to=settings.AUTH_USER_MODEL)),
|
|
38
|
+
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tool_approval_reviews', to=settings.AUTH_USER_MODEL)),
|
|
39
|
+
],
|
|
40
|
+
options={
|
|
41
|
+
'verbose_name': 'Tool Approval Request',
|
|
42
|
+
'verbose_name_plural': 'Tool Approval Requests',
|
|
43
|
+
'ordering': ['-created_at'],
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
migrations.CreateModel(
|
|
47
|
+
name='UserDynamicToolAccess',
|
|
48
|
+
fields=[
|
|
49
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
50
|
+
('access_level', models.CharField(choices=[('none', 'No Access'), ('viewer', 'Viewer (read-only)'), ('scanner', 'Scanner (can scan project)'), ('requester', 'Requester (needs approval)'), ('creator', 'Creator (can create tools)'), ('admin', 'Admin (full access)')], default='none', max_length=20)),
|
|
51
|
+
('granted_at', models.DateTimeField(auto_now_add=True)),
|
|
52
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
53
|
+
('notes', models.TextField(blank=True, help_text='Reason for granting this access level')),
|
|
54
|
+
('granted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dynamic_tool_access_grants_given', to=settings.AUTH_USER_MODEL)),
|
|
55
|
+
('restricted_to_agents', models.ManyToManyField(blank=True, help_text='If set, access only applies to these agents', related_name='dynamic_tool_access_grants', to='django_agent_runtime.agentdefinition')),
|
|
56
|
+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dynamic_tool_access', to=settings.AUTH_USER_MODEL)),
|
|
57
|
+
],
|
|
58
|
+
options={
|
|
59
|
+
'verbose_name': 'User Dynamic Tool Access',
|
|
60
|
+
'verbose_name_plural': 'User Dynamic Tool Access',
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models for Django Agent Studio.
|
|
3
|
+
|
|
4
|
+
Includes permission and approval workflow models for dynamic tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django_agent_studio.models.permissions import (
|
|
8
|
+
DynamicToolAccessLevel,
|
|
9
|
+
UserDynamicToolAccess,
|
|
10
|
+
ToolApprovalRequest,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'DynamicToolAccessLevel',
|
|
15
|
+
'UserDynamicToolAccess',
|
|
16
|
+
'ToolApprovalRequest',
|
|
17
|
+
]
|
|
18
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permission models for Dynamic Tool access control.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Access levels for dynamic tool operations
|
|
6
|
+
- Per-user access assignments
|
|
7
|
+
- Approval workflow for tool creation requests
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.db import models
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DynamicToolAccessLevel(models.TextChoices):
|
|
16
|
+
"""
|
|
17
|
+
Access levels for dynamic tool operations.
|
|
18
|
+
|
|
19
|
+
Ordered from least to most privileged:
|
|
20
|
+
- NONE: No access to dynamic tools
|
|
21
|
+
- VIEWER: Can view discovered functions and existing tools
|
|
22
|
+
- SCANNER: Can scan project for functions
|
|
23
|
+
- REQUESTER: Can request tool creation (needs admin approval)
|
|
24
|
+
- CREATOR: Can create tools directly (no approval needed)
|
|
25
|
+
- ADMIN: Full access including approving requests and managing permissions
|
|
26
|
+
"""
|
|
27
|
+
NONE = 'none', 'No Access'
|
|
28
|
+
VIEWER = 'viewer', 'Viewer (read-only)'
|
|
29
|
+
SCANNER = 'scanner', 'Scanner (can scan project)'
|
|
30
|
+
REQUESTER = 'requester', 'Requester (needs approval)'
|
|
31
|
+
CREATOR = 'creator', 'Creator (can create tools)'
|
|
32
|
+
ADMIN = 'admin', 'Admin (full access)'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Define the hierarchy for permission checks
|
|
36
|
+
ACCESS_LEVEL_HIERARCHY = {
|
|
37
|
+
DynamicToolAccessLevel.NONE: 0,
|
|
38
|
+
DynamicToolAccessLevel.VIEWER: 1,
|
|
39
|
+
DynamicToolAccessLevel.SCANNER: 2,
|
|
40
|
+
DynamicToolAccessLevel.REQUESTER: 3,
|
|
41
|
+
DynamicToolAccessLevel.CREATOR: 4,
|
|
42
|
+
DynamicToolAccessLevel.ADMIN: 5,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UserDynamicToolAccess(models.Model):
|
|
47
|
+
"""
|
|
48
|
+
Per-user access level for dynamic tools.
|
|
49
|
+
|
|
50
|
+
If a user doesn't have a record, they default to NONE access
|
|
51
|
+
unless they're a superuser (who always have ADMIN access).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
55
|
+
|
|
56
|
+
user = models.OneToOneField(
|
|
57
|
+
settings.AUTH_USER_MODEL,
|
|
58
|
+
on_delete=models.CASCADE,
|
|
59
|
+
related_name='dynamic_tool_access',
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
access_level = models.CharField(
|
|
63
|
+
max_length=20,
|
|
64
|
+
choices=DynamicToolAccessLevel.choices,
|
|
65
|
+
default=DynamicToolAccessLevel.NONE,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Optional: restrict to specific agents
|
|
69
|
+
# If empty, access applies to all agents user can access
|
|
70
|
+
restricted_to_agents = models.ManyToManyField(
|
|
71
|
+
'django_agent_runtime.AgentDefinition',
|
|
72
|
+
blank=True,
|
|
73
|
+
related_name='dynamic_tool_access_grants',
|
|
74
|
+
help_text='If set, access only applies to these agents',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Audit fields
|
|
78
|
+
granted_by = models.ForeignKey(
|
|
79
|
+
settings.AUTH_USER_MODEL,
|
|
80
|
+
on_delete=models.SET_NULL,
|
|
81
|
+
null=True,
|
|
82
|
+
blank=True,
|
|
83
|
+
related_name='dynamic_tool_access_grants_given',
|
|
84
|
+
)
|
|
85
|
+
granted_at = models.DateTimeField(auto_now_add=True)
|
|
86
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
87
|
+
|
|
88
|
+
notes = models.TextField(
|
|
89
|
+
blank=True,
|
|
90
|
+
help_text='Reason for granting this access level',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
class Meta:
|
|
94
|
+
verbose_name = 'User Dynamic Tool Access'
|
|
95
|
+
verbose_name_plural = 'User Dynamic Tool Access'
|
|
96
|
+
|
|
97
|
+
def __str__(self):
|
|
98
|
+
return f"{self.user} - {self.get_access_level_display()}"
|
|
99
|
+
|
|
100
|
+
def has_level(self, required_level: str) -> bool:
|
|
101
|
+
"""Check if user has at least the required access level."""
|
|
102
|
+
user_rank = ACCESS_LEVEL_HIERARCHY.get(self.access_level, 0)
|
|
103
|
+
required_rank = ACCESS_LEVEL_HIERARCHY.get(required_level, 0)
|
|
104
|
+
return user_rank >= required_rank
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ToolApprovalRequest(models.Model):
|
|
108
|
+
"""
|
|
109
|
+
Request for tool creation that requires admin approval.
|
|
110
|
+
|
|
111
|
+
Created when a user with REQUESTER access level wants to create a tool.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
class Status(models.TextChoices):
|
|
115
|
+
PENDING = 'pending', 'Pending Review'
|
|
116
|
+
APPROVED = 'approved', 'Approved'
|
|
117
|
+
REJECTED = 'rejected', 'Rejected'
|
|
118
|
+
CANCELLED = 'cancelled', 'Cancelled by Requester'
|
|
119
|
+
|
|
120
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
121
|
+
|
|
122
|
+
# The agent this tool would be added to
|
|
123
|
+
agent = models.ForeignKey(
|
|
124
|
+
'django_agent_runtime.AgentDefinition',
|
|
125
|
+
on_delete=models.CASCADE,
|
|
126
|
+
related_name='tool_approval_requests',
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# The discovered function to convert to a tool
|
|
130
|
+
discovered_function = models.ForeignKey(
|
|
131
|
+
'django_agent_runtime.DiscoveredFunction',
|
|
132
|
+
on_delete=models.CASCADE,
|
|
133
|
+
related_name='approval_requests',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Requester's proposed configuration
|
|
137
|
+
proposed_name = models.CharField(max_length=100)
|
|
138
|
+
proposed_description = models.TextField()
|
|
139
|
+
proposed_is_safe = models.BooleanField(default=False)
|
|
140
|
+
proposed_requires_confirmation = models.BooleanField(default=True)
|
|
141
|
+
proposed_timeout_seconds = models.PositiveIntegerField(default=30)
|
|
142
|
+
|
|
143
|
+
# Request metadata
|
|
144
|
+
requester = models.ForeignKey(
|
|
145
|
+
settings.AUTH_USER_MODEL,
|
|
146
|
+
on_delete=models.CASCADE,
|
|
147
|
+
related_name='tool_approval_requests',
|
|
148
|
+
)
|
|
149
|
+
request_reason = models.TextField(
|
|
150
|
+
help_text='Why this tool is needed',
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Status
|
|
154
|
+
status = models.CharField(
|
|
155
|
+
max_length=20,
|
|
156
|
+
choices=Status.choices,
|
|
157
|
+
default=Status.PENDING,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Review metadata
|
|
161
|
+
reviewed_by = models.ForeignKey(
|
|
162
|
+
settings.AUTH_USER_MODEL,
|
|
163
|
+
on_delete=models.SET_NULL,
|
|
164
|
+
null=True,
|
|
165
|
+
blank=True,
|
|
166
|
+
related_name='tool_approval_reviews',
|
|
167
|
+
)
|
|
168
|
+
reviewed_at = models.DateTimeField(null=True, blank=True)
|
|
169
|
+
review_notes = models.TextField(blank=True)
|
|
170
|
+
|
|
171
|
+
# If approved, link to the created tool
|
|
172
|
+
created_tool = models.ForeignKey(
|
|
173
|
+
'django_agent_runtime.DynamicTool',
|
|
174
|
+
on_delete=models.SET_NULL,
|
|
175
|
+
null=True,
|
|
176
|
+
blank=True,
|
|
177
|
+
related_name='approval_request',
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Timestamps
|
|
181
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
182
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
183
|
+
|
|
184
|
+
class Meta:
|
|
185
|
+
verbose_name = 'Tool Approval Request'
|
|
186
|
+
verbose_name_plural = 'Tool Approval Requests'
|
|
187
|
+
ordering = ['-created_at']
|
|
188
|
+
|
|
189
|
+
def __str__(self):
|
|
190
|
+
return f"{self.proposed_name} - {self.get_status_display()}"
|
|
191
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Services for Django Agent Studio.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django_agent_studio.services.permissions import (
|
|
6
|
+
DynamicToolPermissionService,
|
|
7
|
+
get_permission_service,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'DynamicToolPermissionService',
|
|
12
|
+
'get_permission_service',
|
|
13
|
+
]
|
|
14
|
+
|