django-agent-studio 0.1.9__py3-none-any.whl → 0.2.0__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.
@@ -25,18 +25,19 @@ logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
27
  # =============================================================================
28
- # Memory Tool Definition
28
+ # Memory Tool Definitions
29
29
  # =============================================================================
30
30
 
31
+ # Conversation-scoped memory (ephemeral, lost when conversation ends)
31
32
  MEMORY_TOOL_SCHEMA = {
32
33
  "type": "function",
33
34
  "function": {
34
35
  "name": "remember",
35
36
  "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."
37
+ "Store information to remember. Use this to remember important facts about "
38
+ "the user, their preferences, project details, or anything useful to recall. "
39
+ "Use semantic dot-notation keys like 'user.name', 'user.preferences.theme', "
40
+ "'project.goal'. By default, memories persist across conversations for this user."
40
41
  ),
41
42
  "parameters": {
42
43
  "type": "object",
@@ -44,20 +45,90 @@ MEMORY_TOOL_SCHEMA = {
44
45
  "key": {
45
46
  "type": "string",
46
47
  "description": (
47
- "A short, descriptive key for what you're remembering "
48
- "(e.g., 'user_name', 'project_goal', 'preferred_language')"
48
+ "A semantic key using dot-notation for what you're remembering "
49
+ "(e.g., 'user.name', 'user.preferences.language', 'project.goal')"
49
50
  ),
50
51
  },
51
52
  "value": {
52
53
  "type": "string",
53
54
  "description": "The information to remember",
54
55
  },
56
+ "scope": {
57
+ "type": "string",
58
+ "enum": ["conversation", "user", "system"],
59
+ "description": (
60
+ "Memory scope: 'conversation' (this chat only), "
61
+ "'user' (persists across chats, default), "
62
+ "'system' (shared with other agents)"
63
+ ),
64
+ },
55
65
  },
56
66
  "required": ["key", "value"],
57
67
  },
58
68
  },
59
69
  }
60
70
 
71
+ # Tool to recall memories
72
+ RECALL_TOOL_SCHEMA = {
73
+ "type": "function",
74
+ "function": {
75
+ "name": "recall",
76
+ "description": (
77
+ "Recall stored memories. Use this to retrieve information you've previously "
78
+ "remembered about the user or project. You can recall a specific key or list "
79
+ "all memories matching a prefix."
80
+ ),
81
+ "parameters": {
82
+ "type": "object",
83
+ "properties": {
84
+ "key": {
85
+ "type": "string",
86
+ "description": (
87
+ "The specific key to recall (e.g., 'user.name') or a prefix "
88
+ "to list all matching memories (e.g., 'user.preferences')"
89
+ ),
90
+ },
91
+ "scope": {
92
+ "type": "string",
93
+ "enum": ["conversation", "user", "system", "all"],
94
+ "description": (
95
+ "Which scope to search: 'conversation', 'user', 'system', "
96
+ "or 'all' (default) to search all scopes"
97
+ ),
98
+ },
99
+ },
100
+ "required": ["key"],
101
+ },
102
+ },
103
+ }
104
+
105
+ # Tool to forget memories
106
+ FORGET_TOOL_SCHEMA = {
107
+ "type": "function",
108
+ "function": {
109
+ "name": "forget",
110
+ "description": (
111
+ "Forget/delete a stored memory. Use this when the user asks you to forget "
112
+ "something or when information is no longer relevant."
113
+ ),
114
+ "parameters": {
115
+ "type": "object",
116
+ "properties": {
117
+ "key": {
118
+ "type": "string",
119
+ "description": "The key of the memory to forget (e.g., 'user.name')",
120
+ },
121
+ "scope": {
122
+ "type": "string",
123
+ "enum": ["conversation", "user", "system"],
124
+ "description": "The scope of the memory to forget",
125
+ },
126
+ },
127
+ "required": ["key"],
128
+ },
129
+ },
130
+ }
131
+
61
132
 
62
133
  class DynamicAgentRuntime(AgentRuntime):
63
134
  """
@@ -78,19 +149,26 @@ class DynamicAgentRuntime(AgentRuntime):
78
149
 
79
150
  @property
80
151
  def config(self) -> dict:
81
- """Get the effective configuration (cached)."""
152
+ """Get the effective configuration (cached). Use get_config_async() in async contexts."""
82
153
  if self._config is None:
83
154
  self._config = self._definition.get_effective_config()
84
155
  return self._config
85
-
156
+
157
+ async def get_config_async(self) -> dict:
158
+ """Get the effective configuration in an async-safe way."""
159
+ from asgiref.sync import sync_to_async
160
+ if self._config is None:
161
+ self._config = await sync_to_async(self._definition.get_effective_config)()
162
+ return self._config
163
+
86
164
  def refresh_config(self):
87
165
  """Refresh the configuration from the database."""
88
166
  self._definition.refresh_from_db()
89
167
  self._config = None
90
-
168
+
91
169
  async def run(self, ctx: RunContext) -> RunResult:
92
170
  """Execute the agent with the dynamic configuration and agentic loop."""
93
- config = self.config
171
+ config = await self.get_config_async()
94
172
 
95
173
  # Check if memory is enabled (default: True)
96
174
  extra_config = config.get("extra", {})
@@ -99,8 +177,23 @@ class DynamicAgentRuntime(AgentRuntime):
99
177
  # Build the messages list
100
178
  messages = []
101
179
 
102
- # Add system prompt
103
- system_prompt = config.get("system_prompt", "")
180
+ # Start with system-level shared knowledge (if agent is part of a system)
181
+ system_context = await self._get_system_context()
182
+ system_prefix = ""
183
+ if system_context:
184
+ system_prefix = system_context.get_system_prompt_prefix()
185
+ logger.debug(f"Injecting system context from '{system_context.system_name}'")
186
+
187
+ # Add agent's system prompt
188
+ agent_prompt = config.get("system_prompt", "")
189
+
190
+ # Combine system prefix with agent prompt
191
+ if system_prefix and agent_prompt:
192
+ system_prompt = f"{system_prefix}\n\n---\n\n{agent_prompt}"
193
+ elif system_prefix:
194
+ system_prompt = system_prefix
195
+ else:
196
+ system_prompt = agent_prompt
104
197
 
105
198
  # Add knowledge that should always be included
106
199
  knowledge_context = self._build_knowledge_context(config)
@@ -117,7 +210,7 @@ class DynamicAgentRuntime(AgentRuntime):
117
210
  if memory_enabled:
118
211
  memory_store = await self._get_memory_store(ctx)
119
212
  if memory_store:
120
- memory_context = await self._recall_memories(memory_store)
213
+ memory_context = await self._recall_memories(memory_store, ctx)
121
214
  if memory_context:
122
215
  system_prompt = f"{system_prompt}\n\n{memory_context}"
123
216
 
@@ -127,10 +220,12 @@ class DynamicAgentRuntime(AgentRuntime):
127
220
  # Add conversation history
128
221
  messages.extend(ctx.input_messages)
129
222
 
130
- # Build tool schemas - include memory tool only if memory is enabled
223
+ # Build tool schemas - include memory tools if memory is enabled
131
224
  tools = self._build_tool_schemas(config)
132
225
  if memory_enabled:
133
226
  tools.append(MEMORY_TOOL_SCHEMA)
227
+ tools.append(RECALL_TOOL_SCHEMA)
228
+ tools.append(FORGET_TOOL_SCHEMA)
134
229
 
135
230
  tool_map = self._build_tool_map(config) # Maps tool name to execution info
136
231
 
@@ -146,9 +241,13 @@ class DynamicAgentRuntime(AgentRuntime):
146
241
 
147
242
  # Create tool executor function for the agentic loop
148
243
  async def execute_tool(tool_name: str, tool_args: dict) -> str:
149
- # Handle the built-in remember tool
244
+ # Handle the built-in memory tools
150
245
  if tool_name == "remember":
151
- return await self._execute_remember_tool(tool_args, memory_store)
246
+ return await self._execute_remember_tool(tool_args, memory_store, ctx)
247
+ if tool_name == "recall":
248
+ return await self._execute_recall_tool(tool_args, memory_store, ctx)
249
+ if tool_name == "forget":
250
+ return await self._execute_forget_tool(tool_args, memory_store, ctx)
152
251
 
153
252
  return await self._execute_tool(
154
253
  tool_name, tool_args, tool_map, tool_executor, ctx
@@ -181,22 +280,50 @@ class DynamicAgentRuntime(AgentRuntime):
181
280
  await ctx.emit(EventType.RUN_FAILED, {"error": str(e)})
182
281
  raise
183
282
 
184
- async def _get_memory_store(self, ctx: RunContext) -> Optional["ConversationMemoryStore"]:
283
+ async def _get_system_context(self):
185
284
  """
186
- Get the memory store for this conversation, if available.
285
+ Get the SystemContext if this agent is part of an AgentSystem.
187
286
 
188
- Returns None if we don't have the required context (user, conversation_id).
287
+ Returns:
288
+ SystemContext with shared knowledge, or None if agent is not in a system
189
289
  """
190
- from django_agent_runtime.persistence.stores import ConversationMemoryStore
290
+ from asgiref.sync import sync_to_async
291
+ from django_agent_runtime.models import AgentSystemMember
191
292
 
192
- # Need both user and conversation_id for memory
293
+ try:
294
+ # Check if this agent is a member of any system
295
+ membership = await sync_to_async(
296
+ lambda: AgentSystemMember.objects.filter(
297
+ agent=self._definition
298
+ ).select_related('system').first()
299
+ )()
300
+
301
+ if membership and membership.system:
302
+ # Get the SystemContext from the system
303
+ system_context = await sync_to_async(
304
+ membership.system.get_system_context
305
+ )()
306
+ return system_context
307
+
308
+ except Exception as e:
309
+ logger.warning(f"Failed to get system context: {e}")
310
+
311
+ return None
312
+
313
+ async def _get_memory_store(self, ctx: RunContext) -> Optional["DjangoSharedMemoryStore"]:
314
+ """
315
+ Get the shared memory store for this user, if available.
316
+
317
+ Returns None if we don't have the required context (authenticated user).
318
+ Privacy enforcement: Only authenticated users can have persistent memories.
319
+ """
320
+ from django_agent_runtime.persistence.stores import DjangoSharedMemoryStore
321
+
322
+ # Need authenticated user for memory (privacy enforcement)
193
323
  user_id = ctx.metadata.get("user_id")
194
- conversation_id = ctx.conversation_id
195
324
 
196
- if not user_id or not conversation_id:
197
- logger.debug(
198
- f"Memory not available: user_id={user_id}, conversation_id={conversation_id}"
199
- )
325
+ if not user_id:
326
+ logger.debug("Memory not available: no authenticated user")
200
327
  return None
201
328
 
202
329
  try:
@@ -207,54 +334,237 @@ class DynamicAgentRuntime(AgentRuntime):
207
334
  User = get_user_model()
208
335
  user = await sync_to_async(User.objects.get)(id=user_id)
209
336
 
210
- return ConversationMemoryStore(user=user, conversation_id=conversation_id)
337
+ return DjangoSharedMemoryStore(user=user)
211
338
  except Exception as e:
212
339
  logger.warning(f"Failed to create memory store: {e}")
213
340
  return None
214
341
 
215
- async def _recall_memories(self, memory_store: "ConversationMemoryStore") -> str:
342
+ async def _recall_memories(
343
+ self,
344
+ memory_store: "DjangoSharedMemoryStore",
345
+ ctx: RunContext,
346
+ ) -> str:
216
347
  """
217
- Recall all memories for this conversation and format for the prompt.
348
+ Recall all memories for this user and format for the prompt.
349
+ Includes user-scoped and system-scoped memories.
218
350
  """
219
351
  try:
220
- memories = await memory_store.recall_all()
221
- if memories:
222
- logger.info(f"Recalled {len(memories)} memories for conversation")
223
- return memory_store.format_for_prompt(memories)
352
+ # Get user-scoped memories
353
+ user_memories = await memory_store.list(scope="user", limit=50)
354
+
355
+ # Get system-scoped memories (shared across agents)
356
+ system_memories = await memory_store.list(scope="system", limit=50)
357
+
358
+ # Get conversation-scoped memories
359
+ conversation_memories = []
360
+ if ctx.conversation_id:
361
+ conversation_memories = await memory_store.list(
362
+ scope="conversation",
363
+ conversation_id=ctx.conversation_id,
364
+ limit=50,
365
+ )
366
+
367
+ all_memories = user_memories + system_memories + conversation_memories
368
+
369
+ if all_memories:
370
+ logger.info(f"Recalled {len(all_memories)} memories for user")
371
+ return self._format_memories_for_prompt(all_memories)
224
372
  except Exception as e:
225
373
  logger.warning(f"Failed to recall memories: {e}")
226
374
  return ""
227
375
 
376
+ def _format_memories_for_prompt(self, memories: list) -> str:
377
+ """Format memories for inclusion in a system prompt."""
378
+ if not memories:
379
+ return ""
380
+
381
+ lines = ["# Remembered Information", ""]
382
+
383
+ # Group by scope
384
+ by_scope = {"user": [], "system": [], "conversation": []}
385
+ for mem in memories:
386
+ scope = mem.scope if hasattr(mem, 'scope') else "user"
387
+ if scope in by_scope:
388
+ by_scope[scope].append(mem)
389
+
390
+ if by_scope["user"]:
391
+ lines.append("## About This User")
392
+ for mem in by_scope["user"]:
393
+ display_key = mem.key.replace(".", " > ").replace("_", " ").title()
394
+ lines.append(f"- **{display_key}**: {mem.value}")
395
+ lines.append("")
396
+
397
+ if by_scope["system"]:
398
+ lines.append("## Shared Knowledge")
399
+ for mem in by_scope["system"]:
400
+ display_key = mem.key.replace(".", " > ").replace("_", " ").title()
401
+ lines.append(f"- **{display_key}**: {mem.value}")
402
+ lines.append("")
403
+
404
+ if by_scope["conversation"]:
405
+ lines.append("## This Conversation")
406
+ for mem in by_scope["conversation"]:
407
+ display_key = mem.key.replace(".", " > ").replace("_", " ").title()
408
+ lines.append(f"- **{display_key}**: {mem.value}")
409
+ lines.append("")
410
+
411
+ return "\n".join(lines)
412
+
228
413
  async def _execute_remember_tool(
229
414
  self,
230
415
  args: dict,
231
- memory_store: Optional["ConversationMemoryStore"],
416
+ memory_store: Optional["DjangoSharedMemoryStore"],
417
+ ctx: RunContext,
232
418
  ) -> str:
233
419
  """Execute the remember tool to store a memory."""
234
420
  if not memory_store:
235
421
  return json.dumps({
236
- "error": "Memory not available for this conversation",
237
- "hint": "Memory requires a logged-in user and conversation context",
422
+ "error": "Memory not available",
423
+ "hint": "Memory requires a logged-in user",
238
424
  })
239
425
 
240
426
  key = args.get("key", "").strip()
241
427
  value = args.get("value", "").strip()
428
+ scope = args.get("scope", "user").strip()
242
429
 
243
430
  if not key:
244
431
  return json.dumps({"error": "Missing required parameter: key"})
245
432
  if not value:
246
433
  return json.dumps({"error": "Missing required parameter: value"})
434
+ if scope not in ("conversation", "user", "system"):
435
+ return json.dumps({"error": f"Invalid scope: {scope}"})
247
436
 
248
437
  try:
249
- await memory_store.remember(key, value, source="agent")
250
- logger.info(f"Stored memory: {key}")
438
+ # For conversation scope, need conversation_id
439
+ conversation_id = ctx.conversation_id if scope == "conversation" else None
440
+
441
+ await memory_store.set(
442
+ key=key,
443
+ value=value,
444
+ scope=scope,
445
+ source=f"agent:{self.key}",
446
+ conversation_id=conversation_id,
447
+ )
448
+ logger.info(f"Stored memory: {key} (scope={scope})")
251
449
  return json.dumps({
252
450
  "success": True,
253
- "message": f"Remembered: {key}",
451
+ "message": f"Remembered: {key} (scope: {scope})",
254
452
  })
255
453
  except Exception as e:
256
454
  logger.exception(f"Failed to store memory: {key}")
257
455
  return json.dumps({"error": str(e)})
456
+
457
+ async def _execute_recall_tool(
458
+ self,
459
+ args: dict,
460
+ memory_store: Optional["DjangoSharedMemoryStore"],
461
+ ctx: RunContext,
462
+ ) -> str:
463
+ """Execute the recall tool to retrieve memories."""
464
+ if not memory_store:
465
+ return json.dumps({
466
+ "error": "Memory not available",
467
+ "hint": "Memory requires a logged-in user",
468
+ })
469
+
470
+ key = args.get("key", "").strip()
471
+ scope = args.get("scope", "all").strip()
472
+
473
+ if not key:
474
+ return json.dumps({"error": "Missing required parameter: key"})
475
+
476
+ try:
477
+ results = []
478
+
479
+ # Determine which scopes to search
480
+ scopes_to_search = []
481
+ if scope == "all":
482
+ scopes_to_search = ["user", "system", "conversation"]
483
+ else:
484
+ scopes_to_search = [scope]
485
+
486
+ for s in scopes_to_search:
487
+ conversation_id = ctx.conversation_id if s == "conversation" else None
488
+
489
+ # Try exact match first
490
+ item = await memory_store.get(key, scope=s, conversation_id=conversation_id)
491
+ if item:
492
+ results.append({
493
+ "key": item.key,
494
+ "value": item.value,
495
+ "scope": item.scope,
496
+ "source": item.source,
497
+ })
498
+ else:
499
+ # Try prefix match
500
+ items = await memory_store.list(
501
+ prefix=key,
502
+ scope=s,
503
+ conversation_id=conversation_id,
504
+ limit=20,
505
+ )
506
+ for item in items:
507
+ results.append({
508
+ "key": item.key,
509
+ "value": item.value,
510
+ "scope": item.scope,
511
+ "source": item.source,
512
+ })
513
+
514
+ if results:
515
+ return json.dumps({"memories": results})
516
+ else:
517
+ return json.dumps({"memories": [], "message": f"No memories found for key: {key}"})
518
+
519
+ except Exception as e:
520
+ logger.exception(f"Failed to recall memory: {key}")
521
+ return json.dumps({"error": str(e)})
522
+
523
+ async def _execute_forget_tool(
524
+ self,
525
+ args: dict,
526
+ memory_store: Optional["DjangoSharedMemoryStore"],
527
+ ctx: RunContext,
528
+ ) -> str:
529
+ """Execute the forget tool to delete a memory."""
530
+ if not memory_store:
531
+ return json.dumps({
532
+ "error": "Memory not available",
533
+ "hint": "Memory requires a logged-in user",
534
+ })
535
+
536
+ key = args.get("key", "").strip()
537
+ scope = args.get("scope", "user").strip()
538
+
539
+ if not key:
540
+ return json.dumps({"error": "Missing required parameter: key"})
541
+ if scope not in ("conversation", "user", "system"):
542
+ return json.dumps({"error": f"Invalid scope: {scope}"})
543
+
544
+ try:
545
+ conversation_id = ctx.conversation_id if scope == "conversation" else None
546
+
547
+ deleted = await memory_store.delete(
548
+ key=key,
549
+ scope=scope,
550
+ conversation_id=conversation_id,
551
+ )
552
+
553
+ if deleted:
554
+ logger.info(f"Deleted memory: {key} (scope={scope})")
555
+ return json.dumps({
556
+ "success": True,
557
+ "message": f"Forgot: {key}",
558
+ })
559
+ else:
560
+ return json.dumps({
561
+ "success": False,
562
+ "message": f"No memory found with key: {key}",
563
+ })
564
+
565
+ except Exception as e:
566
+ logger.exception(f"Failed to forget memory: {key}")
567
+ return json.dumps({"error": str(e)})
258
568
 
259
569
  def _build_knowledge_context(self, config: dict) -> str:
260
570
  """Build context string from always-included knowledge sources."""
@@ -293,4 +293,11 @@ urlpatterns = [
293
293
  views.SpecDocumentRenderView.as_view(),
294
294
  name="spec_document_render",
295
295
  ),
296
+
297
+ # Agent-specific spec document (get/update the spec linked to an agent)
298
+ path(
299
+ "agents/<uuid:agent_id>/spec/",
300
+ views.AgentSpecDocumentView.as_view(),
301
+ name="agent_spec_document",
302
+ ),
296
303
  ]