django-agent-runtime 0.3.6__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_runtime/__init__.py +25 -0
- django_agent_runtime/admin.py +155 -0
- django_agent_runtime/api/__init__.py +26 -0
- django_agent_runtime/api/permissions.py +109 -0
- django_agent_runtime/api/serializers.py +114 -0
- django_agent_runtime/api/views.py +472 -0
- django_agent_runtime/apps.py +26 -0
- django_agent_runtime/conf.py +241 -0
- django_agent_runtime/examples/__init__.py +10 -0
- django_agent_runtime/examples/langgraph_adapter.py +164 -0
- django_agent_runtime/examples/langgraph_tools.py +179 -0
- django_agent_runtime/examples/simple_chat.py +69 -0
- django_agent_runtime/examples/tool_agent.py +157 -0
- django_agent_runtime/management/__init__.py +2 -0
- django_agent_runtime/management/commands/__init__.py +2 -0
- django_agent_runtime/management/commands/runagent.py +419 -0
- django_agent_runtime/migrations/0001_initial.py +117 -0
- django_agent_runtime/migrations/0002_persistence_models.py +129 -0
- django_agent_runtime/migrations/0003_persistenceconversation_active_branch_id_and_more.py +212 -0
- django_agent_runtime/migrations/0004_add_anonymous_session_id.py +18 -0
- django_agent_runtime/migrations/__init__.py +2 -0
- django_agent_runtime/models/__init__.py +54 -0
- django_agent_runtime/models/base.py +450 -0
- django_agent_runtime/models/concrete.py +146 -0
- django_agent_runtime/persistence/__init__.py +60 -0
- django_agent_runtime/persistence/helpers.py +148 -0
- django_agent_runtime/persistence/models.py +506 -0
- django_agent_runtime/persistence/stores.py +1191 -0
- django_agent_runtime/runtime/__init__.py +23 -0
- django_agent_runtime/runtime/events/__init__.py +65 -0
- django_agent_runtime/runtime/events/base.py +135 -0
- django_agent_runtime/runtime/events/db.py +129 -0
- django_agent_runtime/runtime/events/redis.py +228 -0
- django_agent_runtime/runtime/events/sync.py +140 -0
- django_agent_runtime/runtime/interfaces.py +475 -0
- django_agent_runtime/runtime/llm/__init__.py +91 -0
- django_agent_runtime/runtime/llm/anthropic.py +249 -0
- django_agent_runtime/runtime/llm/litellm_adapter.py +173 -0
- django_agent_runtime/runtime/llm/openai.py +230 -0
- django_agent_runtime/runtime/queue/__init__.py +75 -0
- django_agent_runtime/runtime/queue/base.py +158 -0
- django_agent_runtime/runtime/queue/postgres.py +248 -0
- django_agent_runtime/runtime/queue/redis_streams.py +336 -0
- django_agent_runtime/runtime/queue/sync.py +277 -0
- django_agent_runtime/runtime/registry.py +186 -0
- django_agent_runtime/runtime/runner.py +540 -0
- django_agent_runtime/runtime/tracing/__init__.py +48 -0
- django_agent_runtime/runtime/tracing/langfuse.py +117 -0
- django_agent_runtime/runtime/tracing/noop.py +36 -0
- django_agent_runtime/urls.py +39 -0
- django_agent_runtime-0.3.6.dist-info/METADATA +723 -0
- django_agent_runtime-0.3.6.dist-info/RECORD +55 -0
- django_agent_runtime-0.3.6.dist-info/WHEEL +5 -0
- django_agent_runtime-0.3.6.dist-info/licenses/LICENSE +22 -0
- django_agent_runtime-0.3.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base models for the Agent Runtime.
|
|
3
|
+
|
|
4
|
+
These can be extended by host projects for customization.
|
|
5
|
+
Use Pattern A (concrete models) by default, Pattern B (swappable) for advanced use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RunStatus(models.TextChoices):
|
|
14
|
+
"""Status choices for agent runs."""
|
|
15
|
+
|
|
16
|
+
QUEUED = "queued", "Queued"
|
|
17
|
+
RUNNING = "running", "Running"
|
|
18
|
+
SUCCEEDED = "succeeded", "Succeeded"
|
|
19
|
+
FAILED = "failed", "Failed"
|
|
20
|
+
CANCELLED = "cancelled", "Cancelled"
|
|
21
|
+
TIMED_OUT = "timed_out", "Timed Out"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AbstractAgentConversation(models.Model):
|
|
25
|
+
"""
|
|
26
|
+
Abstract model for grouping related agent runs.
|
|
27
|
+
|
|
28
|
+
A conversation represents a multi-turn interaction with an agent.
|
|
29
|
+
Supports both authenticated users and anonymous sessions.
|
|
30
|
+
|
|
31
|
+
Anonymous Session Support:
|
|
32
|
+
The abstract model stores anonymous_session_id as a UUID field.
|
|
33
|
+
This allows the runtime to work without requiring a specific session model.
|
|
34
|
+
|
|
35
|
+
To enable anonymous sessions:
|
|
36
|
+
1. Set ANONYMOUS_SESSION_MODEL in DJANGO_AGENT_RUNTIME settings
|
|
37
|
+
2. The model must have a 'token' field and optionally 'is_expired' property
|
|
38
|
+
|
|
39
|
+
For a proper FK relationship, create a custom conversation model::
|
|
40
|
+
|
|
41
|
+
class MyAgentConversation(AbstractAgentConversation):
|
|
42
|
+
anonymous_session = models.ForeignKey(
|
|
43
|
+
"myapp.AnonymousSession",
|
|
44
|
+
on_delete=models.SET_NULL,
|
|
45
|
+
null=True, blank=True,
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
50
|
+
|
|
51
|
+
# Optional user association (nullable for system-initiated conversations)
|
|
52
|
+
user = models.ForeignKey(
|
|
53
|
+
settings.AUTH_USER_MODEL,
|
|
54
|
+
on_delete=models.SET_NULL,
|
|
55
|
+
null=True,
|
|
56
|
+
blank=True,
|
|
57
|
+
related_name="agent_conversations",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Optional anonymous session association (stores session ID as UUID)
|
|
61
|
+
# This allows anonymous sessions without requiring a specific model FK
|
|
62
|
+
anonymous_session_id = models.UUIDField(
|
|
63
|
+
null=True,
|
|
64
|
+
blank=True,
|
|
65
|
+
db_index=True,
|
|
66
|
+
help_text="UUID of the anonymous session (if using anonymous sessions)",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Agent identification
|
|
70
|
+
agent_key = models.CharField(
|
|
71
|
+
max_length=100,
|
|
72
|
+
db_index=True,
|
|
73
|
+
help_text="Identifier for the agent runtime to use",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Conversation state
|
|
77
|
+
title = models.CharField(max_length=255, blank=True)
|
|
78
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
79
|
+
|
|
80
|
+
# Timestamps
|
|
81
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
82
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
83
|
+
|
|
84
|
+
class Meta:
|
|
85
|
+
abstract = True
|
|
86
|
+
ordering = ["-created_at"]
|
|
87
|
+
verbose_name = "Agent Conversation"
|
|
88
|
+
verbose_name_plural = "Agent Conversations"
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return f"{self.agent_key} - {self.id}"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def owner(self):
|
|
95
|
+
"""Return the owner (User or AnonymousSession) of this conversation."""
|
|
96
|
+
if self.user:
|
|
97
|
+
return self.user
|
|
98
|
+
# Try to get anonymous_session FK if it exists (custom model)
|
|
99
|
+
if hasattr(self, 'anonymous_session') and self.anonymous_session:
|
|
100
|
+
return self.anonymous_session
|
|
101
|
+
# Fall back to resolving from anonymous_session_id
|
|
102
|
+
return self.get_anonymous_session()
|
|
103
|
+
|
|
104
|
+
def get_anonymous_session(self):
|
|
105
|
+
"""
|
|
106
|
+
Get the anonymous session object if configured and available.
|
|
107
|
+
|
|
108
|
+
Returns the session object or None if:
|
|
109
|
+
- No anonymous_session_id is set
|
|
110
|
+
- ANONYMOUS_SESSION_MODEL is not configured
|
|
111
|
+
- Session doesn't exist or is expired
|
|
112
|
+
"""
|
|
113
|
+
if not self.anonymous_session_id:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# Check if we have a direct FK (custom model)
|
|
117
|
+
if hasattr(self, 'anonymous_session'):
|
|
118
|
+
return self.anonymous_session
|
|
119
|
+
|
|
120
|
+
# Resolve from configured model
|
|
121
|
+
from django_agent_runtime.conf import runtime_settings
|
|
122
|
+
|
|
123
|
+
settings_obj = runtime_settings()
|
|
124
|
+
model_path = settings_obj.ANONYMOUS_SESSION_MODEL
|
|
125
|
+
|
|
126
|
+
if not model_path:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
from django.apps import apps
|
|
131
|
+
app_label, model_name = model_path.rsplit('.', 1)
|
|
132
|
+
AnonymousSession = apps.get_model(app_label, model_name)
|
|
133
|
+
session = AnonymousSession.objects.get(id=self.anonymous_session_id)
|
|
134
|
+
|
|
135
|
+
# Check if expired
|
|
136
|
+
if hasattr(session, 'is_expired') and session.is_expired:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
return session
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def get_message_history(self, include_failed_runs: bool = False) -> list[dict]:
|
|
144
|
+
"""
|
|
145
|
+
Get the full message history across all runs in this conversation.
|
|
146
|
+
|
|
147
|
+
Returns messages in chronological order, including:
|
|
148
|
+
- Input messages from each run
|
|
149
|
+
- Assistant responses (including tool calls)
|
|
150
|
+
- Tool results
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
include_failed_runs: If True, include messages from failed runs.
|
|
154
|
+
Default is False (only successful runs).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of Message dicts in the framework-neutral format:
|
|
158
|
+
[
|
|
159
|
+
{"role": "user", "content": "..."},
|
|
160
|
+
{"role": "assistant", "content": "...", "tool_calls": [...]},
|
|
161
|
+
{"role": "tool", "content": "...", "tool_call_id": "..."},
|
|
162
|
+
...
|
|
163
|
+
]
|
|
164
|
+
"""
|
|
165
|
+
from django_agent_runtime.models.base import RunStatus
|
|
166
|
+
|
|
167
|
+
# Get runs in chronological order
|
|
168
|
+
runs_qs = self.runs.order_by("created_at")
|
|
169
|
+
|
|
170
|
+
if not include_failed_runs:
|
|
171
|
+
runs_qs = runs_qs.filter(status=RunStatus.SUCCEEDED)
|
|
172
|
+
|
|
173
|
+
messages = []
|
|
174
|
+
seen_message_hashes = set() # Avoid duplicates from overlapping input
|
|
175
|
+
|
|
176
|
+
for run in runs_qs:
|
|
177
|
+
# Get input messages (user messages that started this run)
|
|
178
|
+
input_data = run.input or {}
|
|
179
|
+
input_messages = input_data.get("messages", [])
|
|
180
|
+
|
|
181
|
+
# Add input messages (avoiding duplicates)
|
|
182
|
+
for msg in input_messages:
|
|
183
|
+
# Create a hash to detect duplicates
|
|
184
|
+
msg_hash = _message_hash(msg)
|
|
185
|
+
if msg_hash not in seen_message_hashes:
|
|
186
|
+
messages.append(_normalize_message(msg))
|
|
187
|
+
seen_message_hashes.add(msg_hash)
|
|
188
|
+
|
|
189
|
+
# Get output messages (assistant responses, tool calls, etc.)
|
|
190
|
+
output_data = run.output or {}
|
|
191
|
+
output_messages = output_data.get("final_messages", [])
|
|
192
|
+
|
|
193
|
+
for msg in output_messages:
|
|
194
|
+
msg_hash = _message_hash(msg)
|
|
195
|
+
if msg_hash not in seen_message_hashes:
|
|
196
|
+
messages.append(_normalize_message(msg))
|
|
197
|
+
seen_message_hashes.add(msg_hash)
|
|
198
|
+
|
|
199
|
+
return messages
|
|
200
|
+
|
|
201
|
+
def get_last_assistant_message(self) -> dict | None:
|
|
202
|
+
"""
|
|
203
|
+
Get the most recent assistant message from the conversation.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The last assistant message dict, or None if no assistant messages exist.
|
|
207
|
+
"""
|
|
208
|
+
messages = self.get_message_history()
|
|
209
|
+
for msg in reversed(messages):
|
|
210
|
+
if msg.get("role") == "assistant":
|
|
211
|
+
return msg
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _message_hash(msg: dict) -> str:
|
|
216
|
+
"""Create a hash for deduplication of messages."""
|
|
217
|
+
import hashlib
|
|
218
|
+
import json
|
|
219
|
+
|
|
220
|
+
# Use role + content + tool_call_id for uniqueness
|
|
221
|
+
key_parts = [
|
|
222
|
+
msg.get("role", ""),
|
|
223
|
+
str(msg.get("content", "")),
|
|
224
|
+
msg.get("tool_call_id", ""),
|
|
225
|
+
]
|
|
226
|
+
# Include tool_calls if present
|
|
227
|
+
if msg.get("tool_calls"):
|
|
228
|
+
key_parts.append(json.dumps(msg["tool_calls"], sort_keys=True))
|
|
229
|
+
|
|
230
|
+
key = "|".join(key_parts)
|
|
231
|
+
return hashlib.md5(key.encode()).hexdigest()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _normalize_message(msg: dict) -> dict:
|
|
235
|
+
"""
|
|
236
|
+
Normalize a message to the framework-neutral Message format.
|
|
237
|
+
|
|
238
|
+
Ensures consistent structure regardless of how it was stored.
|
|
239
|
+
"""
|
|
240
|
+
normalized = {
|
|
241
|
+
"role": msg.get("role", "user"),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Handle content (can be string, dict, or list)
|
|
245
|
+
content = msg.get("content")
|
|
246
|
+
if content is not None:
|
|
247
|
+
normalized["content"] = content
|
|
248
|
+
|
|
249
|
+
# Optional fields - only include if present
|
|
250
|
+
if msg.get("name"):
|
|
251
|
+
normalized["name"] = msg["name"]
|
|
252
|
+
|
|
253
|
+
if msg.get("tool_call_id"):
|
|
254
|
+
normalized["tool_call_id"] = msg["tool_call_id"]
|
|
255
|
+
|
|
256
|
+
if msg.get("tool_calls"):
|
|
257
|
+
normalized["tool_calls"] = msg["tool_calls"]
|
|
258
|
+
|
|
259
|
+
if msg.get("metadata"):
|
|
260
|
+
normalized["metadata"] = msg["metadata"]
|
|
261
|
+
|
|
262
|
+
return normalized
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class AbstractAgentRun(models.Model):
|
|
266
|
+
"""
|
|
267
|
+
Abstract model for a single agent execution.
|
|
268
|
+
|
|
269
|
+
This is the core model - tracks status, input/output, retries, and leasing.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
273
|
+
|
|
274
|
+
# Relationship to conversation (optional)
|
|
275
|
+
# Note: concrete model defines the FK to avoid circular imports
|
|
276
|
+
# conversation = models.ForeignKey(...)
|
|
277
|
+
|
|
278
|
+
# Agent identification
|
|
279
|
+
agent_key = models.CharField(
|
|
280
|
+
max_length=100,
|
|
281
|
+
db_index=True,
|
|
282
|
+
help_text="Identifier for the agent runtime to use",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Status tracking
|
|
286
|
+
status = models.CharField(
|
|
287
|
+
max_length=20,
|
|
288
|
+
choices=RunStatus.choices,
|
|
289
|
+
default=RunStatus.QUEUED,
|
|
290
|
+
db_index=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Input/Output (the canonical schema)
|
|
294
|
+
input = models.JSONField(
|
|
295
|
+
default=dict,
|
|
296
|
+
help_text='{"messages": [...], "params": {...}}',
|
|
297
|
+
)
|
|
298
|
+
output = models.JSONField(
|
|
299
|
+
default=dict,
|
|
300
|
+
blank=True,
|
|
301
|
+
help_text="Final output from the agent",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Error tracking
|
|
305
|
+
error = models.JSONField(
|
|
306
|
+
default=dict,
|
|
307
|
+
blank=True,
|
|
308
|
+
help_text='{"type": "", "message": "", "stack": "", "retriable": true}',
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Retry configuration
|
|
312
|
+
attempt = models.PositiveIntegerField(default=1)
|
|
313
|
+
max_attempts = models.PositiveIntegerField(default=3)
|
|
314
|
+
|
|
315
|
+
# Lease management (for distributed workers)
|
|
316
|
+
lease_owner = models.CharField(
|
|
317
|
+
max_length=100,
|
|
318
|
+
blank=True,
|
|
319
|
+
db_index=True,
|
|
320
|
+
help_text="Worker ID that owns this run",
|
|
321
|
+
)
|
|
322
|
+
lease_expires_at = models.DateTimeField(
|
|
323
|
+
null=True,
|
|
324
|
+
blank=True,
|
|
325
|
+
db_index=True,
|
|
326
|
+
help_text="When the lease expires",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Idempotency
|
|
330
|
+
idempotency_key = models.CharField(
|
|
331
|
+
max_length=255,
|
|
332
|
+
null=True,
|
|
333
|
+
blank=True,
|
|
334
|
+
unique=True,
|
|
335
|
+
help_text="Client-provided key for idempotent requests",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Cancellation
|
|
339
|
+
cancel_requested_at = models.DateTimeField(
|
|
340
|
+
null=True,
|
|
341
|
+
blank=True,
|
|
342
|
+
help_text="When cancellation was requested",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Timestamps
|
|
346
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
347
|
+
started_at = models.DateTimeField(null=True, blank=True)
|
|
348
|
+
finished_at = models.DateTimeField(null=True, blank=True)
|
|
349
|
+
|
|
350
|
+
# Extensibility
|
|
351
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
352
|
+
|
|
353
|
+
class Meta:
|
|
354
|
+
abstract = True
|
|
355
|
+
ordering = ["-created_at"]
|
|
356
|
+
verbose_name = "Agent Run"
|
|
357
|
+
verbose_name_plural = "Agent Runs"
|
|
358
|
+
indexes = [
|
|
359
|
+
models.Index(fields=["status", "lease_expires_at"]),
|
|
360
|
+
models.Index(fields=["agent_key", "status"]),
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
def __str__(self):
|
|
364
|
+
return f"{self.agent_key} - {self.status} - {self.id}"
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def is_terminal(self) -> bool:
|
|
368
|
+
"""Check if the run is in a terminal state."""
|
|
369
|
+
return self.status in {
|
|
370
|
+
RunStatus.SUCCEEDED,
|
|
371
|
+
RunStatus.FAILED,
|
|
372
|
+
RunStatus.CANCELLED,
|
|
373
|
+
RunStatus.TIMED_OUT,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class AbstractAgentEvent(models.Model):
|
|
378
|
+
"""
|
|
379
|
+
Abstract model for agent events (append-only log).
|
|
380
|
+
|
|
381
|
+
Events are the communication channel between workers and UI.
|
|
382
|
+
Strictly increasing seq per run, exactly one terminal event.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
386
|
+
|
|
387
|
+
# Relationship to run (concrete model defines FK)
|
|
388
|
+
# run = models.ForeignKey(...)
|
|
389
|
+
|
|
390
|
+
# Event ordering
|
|
391
|
+
seq = models.PositiveIntegerField(
|
|
392
|
+
db_index=True,
|
|
393
|
+
help_text="Strictly increasing sequence number per run",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Event data
|
|
397
|
+
event_type = models.CharField(
|
|
398
|
+
max_length=50,
|
|
399
|
+
db_index=True,
|
|
400
|
+
help_text="Event type (e.g., run.started, assistant.message)",
|
|
401
|
+
)
|
|
402
|
+
payload = models.JSONField(default=dict)
|
|
403
|
+
|
|
404
|
+
# Timestamp
|
|
405
|
+
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
406
|
+
|
|
407
|
+
class Meta:
|
|
408
|
+
abstract = True
|
|
409
|
+
ordering = ["seq"]
|
|
410
|
+
verbose_name = "Agent Event"
|
|
411
|
+
verbose_name_plural = "Agent Events"
|
|
412
|
+
|
|
413
|
+
def __str__(self):
|
|
414
|
+
return f"{self.event_type} (seq={self.seq})"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class AbstractAgentCheckpoint(models.Model):
|
|
418
|
+
"""
|
|
419
|
+
Abstract model for state checkpoints.
|
|
420
|
+
|
|
421
|
+
Checkpoints allow recovery from failures mid-run.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
425
|
+
|
|
426
|
+
# Relationship to run (concrete model defines FK)
|
|
427
|
+
# run = models.ForeignKey(...)
|
|
428
|
+
|
|
429
|
+
# Checkpoint data
|
|
430
|
+
state = models.JSONField(
|
|
431
|
+
help_text="Serialized agent state for recovery",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Ordering
|
|
435
|
+
seq = models.PositiveIntegerField(
|
|
436
|
+
help_text="Checkpoint sequence number",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Timestamp
|
|
440
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
441
|
+
|
|
442
|
+
class Meta:
|
|
443
|
+
abstract = True
|
|
444
|
+
ordering = ["-seq"]
|
|
445
|
+
verbose_name = "Agent Checkpoint"
|
|
446
|
+
verbose_name_plural = "Agent Checkpoints"
|
|
447
|
+
|
|
448
|
+
def __str__(self):
|
|
449
|
+
return f"Checkpoint {self.seq}"
|
|
450
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concrete model implementations for the Agent Runtime.
|
|
3
|
+
|
|
4
|
+
These are the default models used when no custom models are configured.
|
|
5
|
+
Host projects can use these directly or create their own by extending the abstract models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.db import models
|
|
9
|
+
|
|
10
|
+
from django_agent_runtime.models.base import (
|
|
11
|
+
AbstractAgentConversation,
|
|
12
|
+
AbstractAgentRun,
|
|
13
|
+
AbstractAgentEvent,
|
|
14
|
+
AbstractAgentCheckpoint,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentConversation(AbstractAgentConversation):
|
|
19
|
+
"""
|
|
20
|
+
Default concrete implementation of AgentConversation.
|
|
21
|
+
|
|
22
|
+
Groups related agent runs into a conversation.
|
|
23
|
+
|
|
24
|
+
Anonymous Session Support:
|
|
25
|
+
This model uses the anonymous_session_id UUID field from the abstract model.
|
|
26
|
+
When ANONYMOUS_SESSION_MODEL is configured, the get_anonymous_session() method
|
|
27
|
+
will resolve the session object.
|
|
28
|
+
|
|
29
|
+
The anonymous_session property provides convenient access to the session object.
|
|
30
|
+
|
|
31
|
+
For a proper FK relationship with database-level integrity, create your own model::
|
|
32
|
+
|
|
33
|
+
from django.db import models
|
|
34
|
+
from django_agent_runtime.models.base import AbstractAgentConversation
|
|
35
|
+
|
|
36
|
+
class MyAgentConversation(AbstractAgentConversation):
|
|
37
|
+
anonymous_session = models.ForeignKey(
|
|
38
|
+
"myapp.MySession",
|
|
39
|
+
on_delete=models.SET_NULL,
|
|
40
|
+
null=True,
|
|
41
|
+
blank=True,
|
|
42
|
+
related_name="agent_conversations",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
class Meta(AbstractAgentConversation.Meta):
|
|
46
|
+
abstract = False
|
|
47
|
+
|
|
48
|
+
Then configure in settings::
|
|
49
|
+
|
|
50
|
+
DJANGO_AGENT_RUNTIME = {
|
|
51
|
+
'CONVERSATION_MODEL': 'myapp.MyAgentConversation',
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
class Meta(AbstractAgentConversation.Meta):
|
|
56
|
+
abstract = False
|
|
57
|
+
db_table = "agent_runtime_conversation"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def anonymous_session(self):
|
|
61
|
+
"""
|
|
62
|
+
Get the anonymous session object.
|
|
63
|
+
|
|
64
|
+
This property resolves the session from anonymous_session_id using
|
|
65
|
+
the configured ANONYMOUS_SESSION_MODEL.
|
|
66
|
+
|
|
67
|
+
Returns None if:
|
|
68
|
+
- No anonymous_session_id is set
|
|
69
|
+
- ANONYMOUS_SESSION_MODEL is not configured
|
|
70
|
+
- Session doesn't exist or is expired
|
|
71
|
+
"""
|
|
72
|
+
return self.get_anonymous_session()
|
|
73
|
+
|
|
74
|
+
@anonymous_session.setter
|
|
75
|
+
def anonymous_session(self, session):
|
|
76
|
+
"""
|
|
77
|
+
Set the anonymous session.
|
|
78
|
+
|
|
79
|
+
Accepts either a session object (with an 'id' attribute) or a UUID.
|
|
80
|
+
"""
|
|
81
|
+
if session is None:
|
|
82
|
+
self.anonymous_session_id = None
|
|
83
|
+
elif hasattr(session, 'id'):
|
|
84
|
+
self.anonymous_session_id = session.id
|
|
85
|
+
else:
|
|
86
|
+
# Assume it's a UUID
|
|
87
|
+
self.anonymous_session_id = session
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AgentRun(AbstractAgentRun):
|
|
91
|
+
"""
|
|
92
|
+
Default concrete implementation of AgentRun.
|
|
93
|
+
|
|
94
|
+
Tracks individual agent executions with full lifecycle management.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
conversation = models.ForeignKey(
|
|
98
|
+
AgentConversation,
|
|
99
|
+
on_delete=models.CASCADE,
|
|
100
|
+
null=True,
|
|
101
|
+
blank=True,
|
|
102
|
+
related_name="runs",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
class Meta(AbstractAgentRun.Meta):
|
|
106
|
+
abstract = False
|
|
107
|
+
db_table = "agent_runtime_run"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AgentEvent(AbstractAgentEvent):
|
|
111
|
+
"""
|
|
112
|
+
Default concrete implementation of AgentEvent.
|
|
113
|
+
|
|
114
|
+
Append-only event log for streaming to UI.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
run = models.ForeignKey(
|
|
118
|
+
AgentRun,
|
|
119
|
+
on_delete=models.CASCADE,
|
|
120
|
+
related_name="events",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
class Meta(AbstractAgentEvent.Meta):
|
|
124
|
+
abstract = False
|
|
125
|
+
db_table = "agent_runtime_event"
|
|
126
|
+
unique_together = [("run", "seq")]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class AgentCheckpoint(AbstractAgentCheckpoint):
|
|
130
|
+
"""
|
|
131
|
+
Default concrete implementation of AgentCheckpoint.
|
|
132
|
+
|
|
133
|
+
State snapshots for recovery from failures.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
run = models.ForeignKey(
|
|
137
|
+
AgentRun,
|
|
138
|
+
on_delete=models.CASCADE,
|
|
139
|
+
related_name="checkpoints",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
class Meta(AbstractAgentCheckpoint.Meta):
|
|
143
|
+
abstract = False
|
|
144
|
+
db_table = "agent_runtime_checkpoint"
|
|
145
|
+
unique_together = [("run", "seq")]
|
|
146
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django persistence layer for agent-runtime-core.
|
|
3
|
+
|
|
4
|
+
This module provides Django-backed implementations of the persistence stores
|
|
5
|
+
defined in agent_runtime_core.persistence.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from django_agent_runtime.persistence import (
|
|
9
|
+
DjangoMemoryStore,
|
|
10
|
+
DjangoConversationStore,
|
|
11
|
+
DjangoTaskStore,
|
|
12
|
+
DjangoPreferencesStore,
|
|
13
|
+
DjangoKnowledgeStore,
|
|
14
|
+
DjangoAuditStore,
|
|
15
|
+
get_persistence_manager,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# In a view or middleware
|
|
19
|
+
manager = get_persistence_manager(request.user)
|
|
20
|
+
|
|
21
|
+
# Or configure manually
|
|
22
|
+
from agent_runtime_core.persistence import PersistenceConfig, PersistenceManager
|
|
23
|
+
|
|
24
|
+
config = PersistenceConfig(
|
|
25
|
+
memory_store=DjangoMemoryStore(user=request.user),
|
|
26
|
+
conversation_store=DjangoConversationStore(user=request.user),
|
|
27
|
+
task_store=DjangoTaskStore(user=request.user),
|
|
28
|
+
preferences_store=DjangoPreferencesStore(user=request.user),
|
|
29
|
+
knowledge_store=DjangoKnowledgeStore(user=request.user),
|
|
30
|
+
audit_store=DjangoAuditStore(user=request.user),
|
|
31
|
+
)
|
|
32
|
+
manager = PersistenceManager(config)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from django_agent_runtime.persistence.stores import (
|
|
36
|
+
DjangoMemoryStore,
|
|
37
|
+
DjangoConversationStore,
|
|
38
|
+
DjangoTaskStore,
|
|
39
|
+
DjangoPreferencesStore,
|
|
40
|
+
DjangoKnowledgeStore,
|
|
41
|
+
DjangoAuditStore,
|
|
42
|
+
)
|
|
43
|
+
from django_agent_runtime.persistence.helpers import (
|
|
44
|
+
get_persistence_manager,
|
|
45
|
+
get_persistence_config,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Store implementations
|
|
50
|
+
"DjangoMemoryStore",
|
|
51
|
+
"DjangoConversationStore",
|
|
52
|
+
"DjangoTaskStore",
|
|
53
|
+
"DjangoPreferencesStore",
|
|
54
|
+
"DjangoKnowledgeStore",
|
|
55
|
+
"DjangoAuditStore",
|
|
56
|
+
# Helpers
|
|
57
|
+
"get_persistence_manager",
|
|
58
|
+
"get_persistence_config",
|
|
59
|
+
]
|
|
60
|
+
|