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,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django Agent Runtime - Production-grade AI agent execution for Django.
|
|
3
|
+
|
|
4
|
+
Framework-agnostic • Model-agnostic • Production-grade concurrency
|
|
5
|
+
|
|
6
|
+
This package provides:
|
|
7
|
+
- AgentRun model for tracking agent executions
|
|
8
|
+
- Queue adapters (Postgres, Redis Streams) for job distribution
|
|
9
|
+
- Event bus for real-time streaming to UI
|
|
10
|
+
- Plugin system for custom agent runtimes
|
|
11
|
+
- LLM client abstraction (provider-agnostic)
|
|
12
|
+
- Persistence layer (memory, conversations, tasks, preferences)
|
|
13
|
+
- Optional integrations (LiteLLM, Langfuse)
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
1. Add 'django_agent_runtime' to INSTALLED_APPS
|
|
17
|
+
2. Configure DJANGO_AGENT_RUNTIME settings
|
|
18
|
+
3. Run migrations
|
|
19
|
+
4. Start workers: ./manage.py runagent
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.3.0"
|
|
23
|
+
|
|
24
|
+
default_app_config = "django_agent_runtime.apps.DjangoAgentRuntimeConfig"
|
|
25
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django admin configuration for agent runtime models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib import admin
|
|
6
|
+
from django.utils.html import format_html
|
|
7
|
+
|
|
8
|
+
from django_agent_runtime.models import (
|
|
9
|
+
AgentConversation,
|
|
10
|
+
AgentRun,
|
|
11
|
+
AgentEvent,
|
|
12
|
+
AgentCheckpoint,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@admin.register(AgentConversation)
|
|
17
|
+
class AgentConversationAdmin(admin.ModelAdmin):
|
|
18
|
+
"""Admin for AgentConversation."""
|
|
19
|
+
|
|
20
|
+
list_display = ["id", "agent_key", "user", "title", "created_at"]
|
|
21
|
+
list_filter = ["agent_key", "created_at"]
|
|
22
|
+
search_fields = ["id", "title", "user__email"]
|
|
23
|
+
readonly_fields = ["id", "created_at", "updated_at"]
|
|
24
|
+
raw_id_fields = ["user"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentEventInline(admin.TabularInline):
|
|
28
|
+
"""Inline for viewing events on a run."""
|
|
29
|
+
|
|
30
|
+
model = AgentEvent
|
|
31
|
+
extra = 0
|
|
32
|
+
readonly_fields = ["seq", "event_type", "payload", "timestamp"]
|
|
33
|
+
can_delete = False
|
|
34
|
+
|
|
35
|
+
def has_add_permission(self, request, obj=None):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AgentCheckpointInline(admin.TabularInline):
|
|
40
|
+
"""Inline for viewing checkpoints on a run."""
|
|
41
|
+
|
|
42
|
+
model = AgentCheckpoint
|
|
43
|
+
extra = 0
|
|
44
|
+
readonly_fields = ["seq", "state", "created_at"]
|
|
45
|
+
can_delete = False
|
|
46
|
+
|
|
47
|
+
def has_add_permission(self, request, obj=None):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@admin.register(AgentRun)
|
|
52
|
+
class AgentRunAdmin(admin.ModelAdmin):
|
|
53
|
+
"""Admin for AgentRun."""
|
|
54
|
+
|
|
55
|
+
list_display = [
|
|
56
|
+
"id",
|
|
57
|
+
"agent_key",
|
|
58
|
+
"status_badge",
|
|
59
|
+
"attempt",
|
|
60
|
+
"conversation",
|
|
61
|
+
"created_at",
|
|
62
|
+
"duration",
|
|
63
|
+
]
|
|
64
|
+
list_filter = ["status", "agent_key", "created_at"]
|
|
65
|
+
search_fields = ["id", "agent_key", "idempotency_key"]
|
|
66
|
+
readonly_fields = [
|
|
67
|
+
"id",
|
|
68
|
+
"status",
|
|
69
|
+
"attempt",
|
|
70
|
+
"lease_owner",
|
|
71
|
+
"lease_expires_at",
|
|
72
|
+
"created_at",
|
|
73
|
+
"started_at",
|
|
74
|
+
"finished_at",
|
|
75
|
+
"cancel_requested_at",
|
|
76
|
+
]
|
|
77
|
+
raw_id_fields = ["conversation"]
|
|
78
|
+
inlines = [AgentEventInline, AgentCheckpointInline]
|
|
79
|
+
|
|
80
|
+
fieldsets = (
|
|
81
|
+
(None, {
|
|
82
|
+
"fields": ("id", "agent_key", "conversation", "status")
|
|
83
|
+
}),
|
|
84
|
+
("Input/Output", {
|
|
85
|
+
"fields": ("input", "output", "error"),
|
|
86
|
+
"classes": ("collapse",),
|
|
87
|
+
}),
|
|
88
|
+
("Execution", {
|
|
89
|
+
"fields": (
|
|
90
|
+
"attempt",
|
|
91
|
+
"max_attempts",
|
|
92
|
+
"lease_owner",
|
|
93
|
+
"lease_expires_at",
|
|
94
|
+
"cancel_requested_at",
|
|
95
|
+
),
|
|
96
|
+
}),
|
|
97
|
+
("Timestamps", {
|
|
98
|
+
"fields": ("created_at", "started_at", "finished_at"),
|
|
99
|
+
}),
|
|
100
|
+
("Metadata", {
|
|
101
|
+
"fields": ("idempotency_key", "metadata"),
|
|
102
|
+
"classes": ("collapse",),
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def status_badge(self, obj):
|
|
107
|
+
"""Display status as a colored badge."""
|
|
108
|
+
colors = {
|
|
109
|
+
"queued": "#6c757d",
|
|
110
|
+
"running": "#007bff",
|
|
111
|
+
"succeeded": "#28a745",
|
|
112
|
+
"failed": "#dc3545",
|
|
113
|
+
"cancelled": "#ffc107",
|
|
114
|
+
"timed_out": "#fd7e14",
|
|
115
|
+
}
|
|
116
|
+
color = colors.get(obj.status, "#6c757d")
|
|
117
|
+
return format_html(
|
|
118
|
+
'<span style="background-color: {}; color: white; padding: 2px 8px; '
|
|
119
|
+
'border-radius: 4px; font-size: 11px;">{}</span>',
|
|
120
|
+
color,
|
|
121
|
+
obj.status.upper(),
|
|
122
|
+
)
|
|
123
|
+
status_badge.short_description = "Status"
|
|
124
|
+
|
|
125
|
+
def duration(self, obj):
|
|
126
|
+
"""Calculate run duration."""
|
|
127
|
+
if obj.started_at and obj.finished_at:
|
|
128
|
+
delta = obj.finished_at - obj.started_at
|
|
129
|
+
return f"{delta.total_seconds():.1f}s"
|
|
130
|
+
elif obj.started_at:
|
|
131
|
+
return "Running..."
|
|
132
|
+
return "-"
|
|
133
|
+
duration.short_description = "Duration"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@admin.register(AgentEvent)
|
|
137
|
+
class AgentEventAdmin(admin.ModelAdmin):
|
|
138
|
+
"""Admin for AgentEvent."""
|
|
139
|
+
|
|
140
|
+
list_display = ["id", "run", "seq", "event_type", "timestamp"]
|
|
141
|
+
list_filter = ["event_type", "timestamp"]
|
|
142
|
+
search_fields = ["run__id", "event_type"]
|
|
143
|
+
readonly_fields = ["id", "run", "seq", "event_type", "payload", "timestamp"]
|
|
144
|
+
raw_id_fields = ["run"]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@admin.register(AgentCheckpoint)
|
|
148
|
+
class AgentCheckpointAdmin(admin.ModelAdmin):
|
|
149
|
+
"""Admin for AgentCheckpoint."""
|
|
150
|
+
|
|
151
|
+
list_display = ["id", "run", "seq", "created_at"]
|
|
152
|
+
search_fields = ["run__id"]
|
|
153
|
+
readonly_fields = ["id", "run", "seq", "state", "created_at"]
|
|
154
|
+
raw_id_fields = ["run"]
|
|
155
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API module for django_agent_runtime.
|
|
3
|
+
|
|
4
|
+
Provides base ViewSets for agent runtime API. Inherit from these
|
|
5
|
+
in your project and set your own permission_classes.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from django_agent_runtime.api.views import BaseAgentRunViewSet
|
|
9
|
+
|
|
10
|
+
class AgentRunViewSet(BaseAgentRunViewSet):
|
|
11
|
+
permission_classes = [IsAuthenticated]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from django_agent_runtime.api.views import (
|
|
15
|
+
BaseAgentRunViewSet,
|
|
16
|
+
BaseAgentConversationViewSet,
|
|
17
|
+
sync_event_stream,
|
|
18
|
+
async_event_stream,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BaseAgentRunViewSet",
|
|
23
|
+
"BaseAgentConversationViewSet",
|
|
24
|
+
"sync_event_stream",
|
|
25
|
+
"async_event_stream",
|
|
26
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom permissions and authentication for agent runtime API.
|
|
3
|
+
|
|
4
|
+
Supports both authenticated users and anonymous sessions via X-Anonymous-Token header.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rest_framework import permissions
|
|
8
|
+
from rest_framework.authentication import BaseAuthentication
|
|
9
|
+
|
|
10
|
+
from django_agent_runtime.conf import runtime_settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_anonymous_session_model():
|
|
14
|
+
"""
|
|
15
|
+
Get the anonymous session model if configured.
|
|
16
|
+
|
|
17
|
+
Returns None if anonymous sessions are not configured.
|
|
18
|
+
"""
|
|
19
|
+
settings = runtime_settings()
|
|
20
|
+
model_path = settings.ANONYMOUS_SESSION_MODEL
|
|
21
|
+
|
|
22
|
+
if not model_path:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from django.apps import apps
|
|
27
|
+
app_label, model_name = model_path.rsplit('.', 1)
|
|
28
|
+
return apps.get_model(app_label, model_name)
|
|
29
|
+
except Exception:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AnonymousSessionAuthentication(BaseAuthentication):
|
|
34
|
+
"""
|
|
35
|
+
DRF Authentication class that authenticates via X-Anonymous-Token header.
|
|
36
|
+
|
|
37
|
+
This allows anonymous users to access the agent runtime API by providing
|
|
38
|
+
a valid anonymous session token.
|
|
39
|
+
|
|
40
|
+
To enable, set AGENT_RUNTIME["ANONYMOUS_SESSION_MODEL"] to your session model path,
|
|
41
|
+
e.g., "accounts.AnonymousSession". The model must have:
|
|
42
|
+
- A `token` field
|
|
43
|
+
- An `is_expired` property
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def authenticate(self, request):
|
|
47
|
+
"""
|
|
48
|
+
Authenticate the request using X-Anonymous-Token header.
|
|
49
|
+
|
|
50
|
+
Returns a tuple of (user, auth) where user is None for anonymous sessions
|
|
51
|
+
and auth is the AnonymousSession object.
|
|
52
|
+
"""
|
|
53
|
+
token = request.headers.get('X-Anonymous-Token')
|
|
54
|
+
if not token:
|
|
55
|
+
token = request.query_params.get('anonymous_token')
|
|
56
|
+
|
|
57
|
+
if not token:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
AnonymousSession = _get_anonymous_session_model()
|
|
61
|
+
if not AnonymousSession:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
session = AnonymousSession.objects.get(token=token)
|
|
66
|
+
if hasattr(session, 'is_expired') and session.is_expired:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
# Store the session on the request for later use
|
|
70
|
+
request.anonymous_session = session
|
|
71
|
+
|
|
72
|
+
# Return (None, session) - None user means anonymous
|
|
73
|
+
# The session is the "auth" object
|
|
74
|
+
return (None, session)
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def authenticate_header(self, request):
|
|
79
|
+
"""Return a string to be used as the value of the WWW-Authenticate header."""
|
|
80
|
+
return 'X-Anonymous-Token'
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class IsAuthenticatedOrAnonymousSession(permissions.BasePermission):
|
|
84
|
+
"""
|
|
85
|
+
Permission class that allows access if:
|
|
86
|
+
1. User is authenticated (via Token auth), OR
|
|
87
|
+
2. Request has a valid X-Anonymous-Token header
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def has_permission(self, request, view):
|
|
91
|
+
# Check if user is authenticated
|
|
92
|
+
if request.user and request.user.is_authenticated:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Check if we have an anonymous session (set by AnonymousSessionAuthentication)
|
|
96
|
+
if hasattr(request, 'anonymous_session') and request.anonymous_session:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_anonymous_session(request):
|
|
103
|
+
"""
|
|
104
|
+
Helper function to get the anonymous session from a request.
|
|
105
|
+
|
|
106
|
+
Returns the AnonymousSession object if present, None otherwise.
|
|
107
|
+
"""
|
|
108
|
+
return getattr(request, 'anonymous_session', None)
|
|
109
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DRF serializers for agent runtime API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
|
|
7
|
+
from django_agent_runtime.models import AgentRun, AgentConversation, AgentEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentConversationSerializer(serializers.ModelSerializer):
|
|
11
|
+
"""Serializer for AgentConversation."""
|
|
12
|
+
|
|
13
|
+
class Meta:
|
|
14
|
+
model = AgentConversation
|
|
15
|
+
fields = [
|
|
16
|
+
"id",
|
|
17
|
+
"agent_key",
|
|
18
|
+
"title",
|
|
19
|
+
"metadata",
|
|
20
|
+
"created_at",
|
|
21
|
+
"updated_at",
|
|
22
|
+
]
|
|
23
|
+
read_only_fields = ["id", "created_at", "updated_at"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentRunSerializer(serializers.ModelSerializer):
|
|
27
|
+
"""Serializer for AgentRun."""
|
|
28
|
+
|
|
29
|
+
class Meta:
|
|
30
|
+
model = AgentRun
|
|
31
|
+
fields = [
|
|
32
|
+
"id",
|
|
33
|
+
"conversation_id",
|
|
34
|
+
"agent_key",
|
|
35
|
+
"status",
|
|
36
|
+
"input",
|
|
37
|
+
"output",
|
|
38
|
+
"error",
|
|
39
|
+
"attempt",
|
|
40
|
+
"max_attempts",
|
|
41
|
+
"idempotency_key",
|
|
42
|
+
"created_at",
|
|
43
|
+
"started_at",
|
|
44
|
+
"finished_at",
|
|
45
|
+
"metadata",
|
|
46
|
+
]
|
|
47
|
+
read_only_fields = [
|
|
48
|
+
"id",
|
|
49
|
+
"status",
|
|
50
|
+
"output",
|
|
51
|
+
"error",
|
|
52
|
+
"attempt",
|
|
53
|
+
"created_at",
|
|
54
|
+
"started_at",
|
|
55
|
+
"finished_at",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AgentRunCreateSerializer(serializers.Serializer):
|
|
60
|
+
"""Serializer for creating a new agent run."""
|
|
61
|
+
|
|
62
|
+
agent_key = serializers.CharField(max_length=100)
|
|
63
|
+
conversation_id = serializers.UUIDField(required=False, allow_null=True)
|
|
64
|
+
messages = serializers.ListField(
|
|
65
|
+
child=serializers.DictField(),
|
|
66
|
+
required=True,
|
|
67
|
+
help_text="List of messages in the conversation",
|
|
68
|
+
)
|
|
69
|
+
params = serializers.DictField(
|
|
70
|
+
required=False,
|
|
71
|
+
default=dict,
|
|
72
|
+
help_text="Additional parameters for the agent",
|
|
73
|
+
)
|
|
74
|
+
max_attempts = serializers.IntegerField(
|
|
75
|
+
required=False,
|
|
76
|
+
default=3,
|
|
77
|
+
min_value=1,
|
|
78
|
+
max_value=10,
|
|
79
|
+
)
|
|
80
|
+
idempotency_key = serializers.CharField(
|
|
81
|
+
required=False,
|
|
82
|
+
allow_null=True,
|
|
83
|
+
max_length=255,
|
|
84
|
+
)
|
|
85
|
+
metadata = serializers.DictField(
|
|
86
|
+
required=False,
|
|
87
|
+
default=dict,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AgentEventSerializer(serializers.ModelSerializer):
|
|
92
|
+
"""Serializer for AgentEvent."""
|
|
93
|
+
|
|
94
|
+
class Meta:
|
|
95
|
+
model = AgentEvent
|
|
96
|
+
fields = [
|
|
97
|
+
"id",
|
|
98
|
+
"run_id",
|
|
99
|
+
"seq",
|
|
100
|
+
"event_type",
|
|
101
|
+
"payload",
|
|
102
|
+
"timestamp",
|
|
103
|
+
]
|
|
104
|
+
read_only_fields = fields
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AgentRunDetailSerializer(AgentRunSerializer):
|
|
108
|
+
"""Detailed serializer for AgentRun with events."""
|
|
109
|
+
|
|
110
|
+
events = AgentEventSerializer(many=True, read_only=True)
|
|
111
|
+
|
|
112
|
+
class Meta(AgentRunSerializer.Meta):
|
|
113
|
+
fields = AgentRunSerializer.Meta.fields + ["events"]
|
|
114
|
+
|