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,723 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-agent-runtime
|
|
3
|
+
Version: 0.3.6
|
|
4
|
+
Summary: Production-grade AI agent runtime for Django - framework and model agnostic
|
|
5
|
+
Author: Chris Barry
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/makemore/django-agent-runtime
|
|
8
|
+
Project-URL: Documentation, https://github.com/makemore/django-agent-runtime#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/makemore/django-agent-runtime
|
|
10
|
+
Project-URL: Issues, https://github.com/makemore/django-agent-runtime/issues
|
|
11
|
+
Keywords: django,ai,agent,llm,runtime,async
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.0
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: Django>=4.2
|
|
28
|
+
Requires-Dist: djangorestframework>=3.14
|
|
29
|
+
Requires-Dist: agent-runtime-core>=0.4.0
|
|
30
|
+
Provides-Extra: redis
|
|
31
|
+
Requires-Dist: redis>=4.5.0; extra == "redis"
|
|
32
|
+
Provides-Extra: openai
|
|
33
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
34
|
+
Provides-Extra: anthropic
|
|
35
|
+
Requires-Dist: anthropic>=0.18.0; extra == "anthropic"
|
|
36
|
+
Provides-Extra: litellm
|
|
37
|
+
Requires-Dist: litellm>=1.0.0; extra == "litellm"
|
|
38
|
+
Provides-Extra: langfuse
|
|
39
|
+
Requires-Dist: langfuse>=2.0.0; extra == "langfuse"
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Requires-Dist: django-agent-runtime[anthropic,langfuse,litellm,openai,redis]; extra == "all"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
47
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
48
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
49
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
# django-agent-runtime
|
|
53
|
+
|
|
54
|
+
[](https://badge.fury.io/py/django-agent-runtime)
|
|
55
|
+
[](https://www.python.org/downloads/)
|
|
56
|
+
[](https://www.djangoproject.com/)
|
|
57
|
+
[](https://opensource.org/licenses/MIT)
|
|
58
|
+
|
|
59
|
+
A production-ready Django app for AI agent execution. Provides everything you need to run AI agents in production: database models, REST API, real-time streaming, background workers, and more.
|
|
60
|
+
|
|
61
|
+
## Recent Updates
|
|
62
|
+
|
|
63
|
+
| Version | Date | Changes |
|
|
64
|
+
|---------|------|---------|
|
|
65
|
+
| **0.3.6** | 2025-01-13 | Auto-reload for `runagent` in DEBUG mode (like Django's runserver) |
|
|
66
|
+
| **0.3.5** | 2025-01-13 | Added Recent Updates changelog to README |
|
|
67
|
+
| **0.3.4** | 2025-01-13 | Documentation updates for message history |
|
|
68
|
+
| **0.3.3** | 2025-01-13 | Added `conversation.get_message_history()` for retrieving full message sequences |
|
|
69
|
+
| **0.3.2** | 2025-01-13 | Event visibility system - filter events by `internal`/`debug`/`user` levels |
|
|
70
|
+
| **0.3.1** | 2025-01-12 | Anonymous session support for unauthenticated users |
|
|
71
|
+
| **0.3.0** | 2025-01-11 | ViewSet refactor - base classes for custom auth/permissions |
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
- 🔌 **Framework Agnostic** - Works with LangGraph, CrewAI, OpenAI Agents, or custom loops
|
|
76
|
+
- 🤖 **Model Agnostic** - OpenAI, Anthropic, or any provider via LiteLLM
|
|
77
|
+
- ⚡ **Production-Grade Concurrency** - Multi-process + async workers with `./manage.py runagent`
|
|
78
|
+
- 📊 **PostgreSQL Queue** - Reliable, lease-based job queue with automatic retries
|
|
79
|
+
- 🔄 **Real-Time Streaming** - Server-Sent Events (SSE) for live UI updates
|
|
80
|
+
- 🛡️ **Resilient** - Retries, cancellation, timeouts, and heartbeats built-in
|
|
81
|
+
- 📈 **Observable** - Optional Langfuse integration for tracing
|
|
82
|
+
- 🧩 **Installable** - Drop-in Django app, ready in minutes
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install django-agent-runtime
|
|
88
|
+
|
|
89
|
+
# With LLM providers
|
|
90
|
+
pip install django-agent-runtime[openai]
|
|
91
|
+
pip install django-agent-runtime[anthropic]
|
|
92
|
+
|
|
93
|
+
# With Redis support (recommended for production)
|
|
94
|
+
pip install django-agent-runtime[redis]
|
|
95
|
+
|
|
96
|
+
# Everything
|
|
97
|
+
pip install django-agent-runtime[all]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Quick Start
|
|
101
|
+
|
|
102
|
+
### 1. Add to Django Settings
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# settings.py
|
|
106
|
+
INSTALLED_APPS = [
|
|
107
|
+
...
|
|
108
|
+
'rest_framework',
|
|
109
|
+
'django_agent_runtime',
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
DJANGO_AGENT_RUNTIME = {
|
|
113
|
+
# Queue & Events
|
|
114
|
+
'QUEUE_BACKEND': 'postgres', # or 'redis_streams'
|
|
115
|
+
'EVENT_BUS_BACKEND': 'db', # or 'redis'
|
|
116
|
+
|
|
117
|
+
# LLM Configuration
|
|
118
|
+
'MODEL_PROVIDER': 'openai', # or 'anthropic', 'litellm'
|
|
119
|
+
'DEFAULT_MODEL': 'gpt-4o',
|
|
120
|
+
|
|
121
|
+
# Timeouts
|
|
122
|
+
'LEASE_TTL_SECONDS': 30,
|
|
123
|
+
'RUN_TIMEOUT_SECONDS': 900,
|
|
124
|
+
|
|
125
|
+
# Agent Discovery
|
|
126
|
+
'RUNTIME_REGISTRY': [
|
|
127
|
+
'myapp.agents:register_agents',
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 2. Run Migrations
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python manage.py migrate django_agent_runtime
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Set Up API ViewSets and URLs
|
|
139
|
+
|
|
140
|
+
Create your own ViewSets by inheriting from the base classes and configure authentication:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# myapp/api/views.py
|
|
144
|
+
from django_agent_runtime.api.views import BaseAgentRunViewSet, BaseAgentConversationViewSet
|
|
145
|
+
from rest_framework.permissions import IsAuthenticated
|
|
146
|
+
|
|
147
|
+
class AgentRunViewSet(BaseAgentRunViewSet):
|
|
148
|
+
permission_classes = [IsAuthenticated]
|
|
149
|
+
|
|
150
|
+
class AgentConversationViewSet(BaseAgentConversationViewSet):
|
|
151
|
+
permission_classes = [IsAuthenticated]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Then wire up your URLs:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# myapp/api/urls.py
|
|
158
|
+
from django.urls import path, include
|
|
159
|
+
from rest_framework.routers import DefaultRouter
|
|
160
|
+
from django_agent_runtime.api.views import sync_event_stream, async_event_stream
|
|
161
|
+
from .views import AgentRunViewSet, AgentConversationViewSet
|
|
162
|
+
|
|
163
|
+
router = DefaultRouter()
|
|
164
|
+
router.register(r"conversations", AgentConversationViewSet, basename="conversation")
|
|
165
|
+
router.register(r"runs", AgentRunViewSet, basename="run")
|
|
166
|
+
|
|
167
|
+
urlpatterns = [
|
|
168
|
+
path("", include(router.urls)),
|
|
169
|
+
path("runs/<str:run_id>/events/", sync_event_stream, name="run-events"),
|
|
170
|
+
path("runs/<str:run_id>/events/stream/", async_event_stream, name="run-stream"),
|
|
171
|
+
]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Include in your main urls.py:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# urls.py
|
|
178
|
+
from django.urls import path, include
|
|
179
|
+
|
|
180
|
+
urlpatterns = [
|
|
181
|
+
...
|
|
182
|
+
path('api/agents/', include('myapp.api.urls')),
|
|
183
|
+
]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 4. Create an Agent
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# myapp/agents.py
|
|
190
|
+
from django_agent_runtime.runtime.interfaces import (
|
|
191
|
+
AgentRuntime,
|
|
192
|
+
RunContext,
|
|
193
|
+
RunResult,
|
|
194
|
+
EventType,
|
|
195
|
+
)
|
|
196
|
+
from django_agent_runtime.runtime.registry import register_runtime
|
|
197
|
+
from django_agent_runtime.runtime.llm import get_llm_client
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ChatAgent(AgentRuntime):
|
|
201
|
+
"""A simple conversational agent."""
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def key(self) -> str:
|
|
205
|
+
return "chat-agent"
|
|
206
|
+
|
|
207
|
+
async def run(self, ctx: RunContext) -> RunResult:
|
|
208
|
+
# Get the LLM client
|
|
209
|
+
llm = get_llm_client()
|
|
210
|
+
|
|
211
|
+
# Generate a response
|
|
212
|
+
response = await llm.generate(ctx.input_messages)
|
|
213
|
+
|
|
214
|
+
# Emit event for real-time streaming
|
|
215
|
+
await ctx.emit(EventType.ASSISTANT_MESSAGE, {
|
|
216
|
+
"content": response.message["content"],
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return RunResult(
|
|
220
|
+
final_output={"response": response.message["content"]},
|
|
221
|
+
final_messages=[response.message],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def register_agents():
|
|
226
|
+
"""Called by django-agent-runtime on startup."""
|
|
227
|
+
register_runtime(ChatAgent())
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 5. Start Workers
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Start agent workers (4 processes, 20 concurrent runs each)
|
|
234
|
+
python manage.py runagent --processes 4 --concurrency 20
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## API Endpoints
|
|
238
|
+
|
|
239
|
+
### Create a Run
|
|
240
|
+
|
|
241
|
+
```http
|
|
242
|
+
POST /api/agents/runs/
|
|
243
|
+
Content-Type: application/json
|
|
244
|
+
Authorization: Token <your-token>
|
|
245
|
+
|
|
246
|
+
{
|
|
247
|
+
"agent_key": "chat-agent",
|
|
248
|
+
"messages": [
|
|
249
|
+
{"role": "user", "content": "Hello! How are you?"}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Response:**
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
258
|
+
"agent_key": "chat-agent",
|
|
259
|
+
"status": "queued",
|
|
260
|
+
"created_at": "2024-01-15T10:30:00Z"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Stream Events (SSE)
|
|
265
|
+
|
|
266
|
+
```http
|
|
267
|
+
GET /api/agents/runs/{id}/events/
|
|
268
|
+
Accept: text/event-stream
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Event Stream:**
|
|
272
|
+
```
|
|
273
|
+
event: run.started
|
|
274
|
+
data: {"run_id": "550e8400...", "ts": "2024-01-15T10:30:01Z"}
|
|
275
|
+
|
|
276
|
+
event: assistant.message
|
|
277
|
+
data: {"content": "Hello! I'm doing well, thank you for asking!"}
|
|
278
|
+
|
|
279
|
+
event: run.succeeded
|
|
280
|
+
data: {"run_id": "550e8400...", "output": {...}}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Get Run Status
|
|
284
|
+
|
|
285
|
+
```http
|
|
286
|
+
GET /api/agents/runs/{id}/
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Cancel a Run
|
|
290
|
+
|
|
291
|
+
```http
|
|
292
|
+
POST /api/agents/runs/{id}/cancel/
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### List Conversations
|
|
296
|
+
|
|
297
|
+
```http
|
|
298
|
+
GET /api/agents/conversations/
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Architecture
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
305
|
+
│ Django API │────▶│ PostgreSQL │────▶│ Workers │
|
|
306
|
+
│ (REST/SSE) │ │ Queue │ │ (runagent) │
|
|
307
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
308
|
+
│ │
|
|
309
|
+
│ ▼
|
|
310
|
+
│ ┌─────────────────┐
|
|
311
|
+
│ │ Your Agent │
|
|
312
|
+
│ │ (AgentRuntime)│
|
|
313
|
+
│ └─────────────────┘
|
|
314
|
+
│ │
|
|
315
|
+
▼ ▼
|
|
316
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
317
|
+
│ Frontend │◀────────────────────────────│ Event Bus │
|
|
318
|
+
│ (SSE Client) │ Real-time │ (DB/Redis) │
|
|
319
|
+
└─────────────────┘ └─────────────────┘
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Models
|
|
323
|
+
|
|
324
|
+
### Conversation
|
|
325
|
+
|
|
326
|
+
Groups related agent runs together:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from django_agent_runtime.models import AgentConversation
|
|
330
|
+
|
|
331
|
+
conversation = AgentConversation.objects.create(
|
|
332
|
+
user=request.user,
|
|
333
|
+
agent_key="chat-agent",
|
|
334
|
+
title="My Chat",
|
|
335
|
+
metadata={"source": "web"},
|
|
336
|
+
)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Message History
|
|
340
|
+
|
|
341
|
+
Get the full message history across all runs in a conversation:
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
# Get all messages (user, assistant, tool calls, tool results)
|
|
345
|
+
messages = conversation.get_message_history()
|
|
346
|
+
|
|
347
|
+
# Include messages from failed runs
|
|
348
|
+
messages = conversation.get_message_history(include_failed_runs=True)
|
|
349
|
+
|
|
350
|
+
# Get just the last assistant message
|
|
351
|
+
last_msg = conversation.get_last_assistant_message()
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Returns messages in the framework-neutral format:
|
|
355
|
+
```python
|
|
356
|
+
[
|
|
357
|
+
{"role": "user", "content": "What's the weather?"},
|
|
358
|
+
{"role": "assistant", "content": None, "tool_calls": [...]},
|
|
359
|
+
{"role": "tool", "content": "72°F sunny", "tool_call_id": "call_123"},
|
|
360
|
+
{"role": "assistant", "content": "The weather is 72°F and sunny."},
|
|
361
|
+
]
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### AgentRun
|
|
365
|
+
|
|
366
|
+
Represents a single agent execution:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from django_agent_runtime.models import AgentRun
|
|
370
|
+
|
|
371
|
+
run = AgentRun.objects.create(
|
|
372
|
+
conversation=conversation,
|
|
373
|
+
agent_key="chat-agent",
|
|
374
|
+
input={"messages": [...]},
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# After completion, output contains final_messages
|
|
378
|
+
messages = run.output.get("final_messages", [])
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### AgentEvent
|
|
382
|
+
|
|
383
|
+
Stores events emitted during runs:
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from django_agent_runtime.models import AgentEvent
|
|
387
|
+
|
|
388
|
+
events = AgentEvent.objects.filter(run=run).order_by('seq')
|
|
389
|
+
for event in events:
|
|
390
|
+
print(f"{event.event_type}: {event.payload}")
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Building Agents with Tools
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from django_agent_runtime.runtime.interfaces import (
|
|
397
|
+
AgentRuntime, RunContext, RunResult, EventType,
|
|
398
|
+
Tool, ToolRegistry,
|
|
399
|
+
)
|
|
400
|
+
from django_agent_runtime.runtime.llm import get_llm_client
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def get_weather(location: str) -> str:
|
|
404
|
+
"""Get current weather for a location."""
|
|
405
|
+
# Your weather API call here
|
|
406
|
+
return f"Sunny, 72°F in {location}"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def search_database(query: str) -> list:
|
|
410
|
+
"""Search the database for relevant information."""
|
|
411
|
+
# Your database search here
|
|
412
|
+
return [{"title": "Result 1", "content": "..."}]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class ToolAgent(AgentRuntime):
|
|
416
|
+
@property
|
|
417
|
+
def key(self) -> str:
|
|
418
|
+
return "tool-agent"
|
|
419
|
+
|
|
420
|
+
def __init__(self):
|
|
421
|
+
self.tools = ToolRegistry()
|
|
422
|
+
self.tools.register(Tool.from_function(get_weather))
|
|
423
|
+
self.tools.register(Tool.from_function(search_database))
|
|
424
|
+
|
|
425
|
+
async def run(self, ctx: RunContext) -> RunResult:
|
|
426
|
+
llm = get_llm_client()
|
|
427
|
+
messages = list(ctx.input_messages)
|
|
428
|
+
|
|
429
|
+
while True:
|
|
430
|
+
response = await llm.generate(
|
|
431
|
+
messages,
|
|
432
|
+
tools=self.tools.to_openai_format(),
|
|
433
|
+
)
|
|
434
|
+
messages.append(response.message)
|
|
435
|
+
|
|
436
|
+
if not response.tool_calls:
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
for tool_call in response.tool_calls:
|
|
440
|
+
# Emit tool call event
|
|
441
|
+
await ctx.emit(EventType.TOOL_CALL, {
|
|
442
|
+
"tool": tool_call["function"]["name"],
|
|
443
|
+
"arguments": tool_call["function"]["arguments"],
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
# Execute tool
|
|
447
|
+
result = await self.tools.execute(
|
|
448
|
+
tool_call["function"]["name"],
|
|
449
|
+
tool_call["function"]["arguments"],
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Emit result event
|
|
453
|
+
await ctx.emit(EventType.TOOL_RESULT, {
|
|
454
|
+
"tool_call_id": tool_call["id"],
|
|
455
|
+
"result": result,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
messages.append({
|
|
459
|
+
"role": "tool",
|
|
460
|
+
"tool_call_id": tool_call["id"],
|
|
461
|
+
"content": str(result),
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
return RunResult(
|
|
465
|
+
final_output={"response": response.message["content"]},
|
|
466
|
+
final_messages=messages,
|
|
467
|
+
)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Anonymous Sessions
|
|
471
|
+
|
|
472
|
+
django-agent-runtime supports anonymous sessions for unauthenticated users who have a session token. This is useful for public-facing chat interfaces.
|
|
473
|
+
|
|
474
|
+
### Setup
|
|
475
|
+
|
|
476
|
+
1. **Configure the anonymous session model** in your settings:
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
DJANGO_AGENT_RUNTIME = {
|
|
480
|
+
# ... other settings ...
|
|
481
|
+
'ANONYMOUS_SESSION_MODEL': 'accounts.AnonymousSession',
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
2. **Create your anonymous session model** with required fields:
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
# accounts/models.py
|
|
489
|
+
import uuid
|
|
490
|
+
from django.db import models
|
|
491
|
+
from django.utils import timezone
|
|
492
|
+
|
|
493
|
+
class AnonymousSession(models.Model):
|
|
494
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
495
|
+
token = models.CharField(max_length=64, unique=True, db_index=True)
|
|
496
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
497
|
+
expires_at = models.DateTimeField()
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def is_expired(self):
|
|
501
|
+
return timezone.now() > self.expires_at
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
3. **Set up authentication** in your ViewSets:
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
from rest_framework.authentication import TokenAuthentication
|
|
508
|
+
from django_agent_runtime.api.views import BaseAgentRunViewSet, BaseAgentConversationViewSet
|
|
509
|
+
from django_agent_runtime.api.permissions import (
|
|
510
|
+
AnonymousSessionAuthentication,
|
|
511
|
+
IsAuthenticatedOrAnonymousSession,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
class AgentConversationViewSet(BaseAgentConversationViewSet):
|
|
515
|
+
authentication_classes = [TokenAuthentication, AnonymousSessionAuthentication]
|
|
516
|
+
permission_classes = [IsAuthenticatedOrAnonymousSession]
|
|
517
|
+
|
|
518
|
+
class AgentRunViewSet(BaseAgentRunViewSet):
|
|
519
|
+
authentication_classes = [TokenAuthentication, AnonymousSessionAuthentication]
|
|
520
|
+
permission_classes = [IsAuthenticatedOrAnonymousSession]
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Client Usage
|
|
524
|
+
|
|
525
|
+
Pass the session token via the `X-Anonymous-Token` header:
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
curl -X POST https://api.example.com/agent/runs/ \
|
|
529
|
+
-H "X-Anonymous-Token: your-session-token" \
|
|
530
|
+
-H "Content-Type: application/json" \
|
|
531
|
+
-d '{"agent_key": "chat-agent", "messages": [{"role": "user", "content": "Hello!"}]}'
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
For SSE streaming (where headers can't be set), use a query parameter:
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
const eventSource = new EventSource(
|
|
538
|
+
`/api/agents/runs/${runId}/events/?anonymous_token=your-session-token`
|
|
539
|
+
);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Event Visibility
|
|
543
|
+
|
|
544
|
+
Events have visibility levels that control what's shown to users in the UI:
|
|
545
|
+
|
|
546
|
+
| Level | Description |
|
|
547
|
+
|-------|-------------|
|
|
548
|
+
| `internal` | Never shown to UI (heartbeats, checkpoints) |
|
|
549
|
+
| `debug` | Shown only in debug mode (tool calls, tool results) |
|
|
550
|
+
| `user` | Always shown to users (messages, errors) |
|
|
551
|
+
|
|
552
|
+
### Configuration
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
DJANGO_AGENT_RUNTIME = {
|
|
556
|
+
'EVENT_VISIBILITY': {
|
|
557
|
+
'run.started': 'internal',
|
|
558
|
+
'run.failed': 'user',
|
|
559
|
+
'assistant.message': 'user',
|
|
560
|
+
'tool.call': 'debug',
|
|
561
|
+
'tool.result': 'debug',
|
|
562
|
+
'state.checkpoint': 'internal',
|
|
563
|
+
'error': 'user',
|
|
564
|
+
},
|
|
565
|
+
'DEBUG_MODE': False, # When True, 'debug' events become visible
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### SSE Filtering
|
|
570
|
+
|
|
571
|
+
The SSE endpoint filters events by visibility:
|
|
572
|
+
|
|
573
|
+
```javascript
|
|
574
|
+
// Only user-visible events (default)
|
|
575
|
+
new EventSource(`/api/agents/runs/${runId}/events/`);
|
|
576
|
+
|
|
577
|
+
// Include debug events
|
|
578
|
+
new EventSource(`/api/agents/runs/${runId}/events/?include_debug=true`);
|
|
579
|
+
|
|
580
|
+
// Include all events (for debugging)
|
|
581
|
+
new EventSource(`/api/agents/runs/${runId}/events/?include_all=true`);
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Helper Methods
|
|
585
|
+
|
|
586
|
+
Agent runtimes can use convenience methods:
|
|
587
|
+
|
|
588
|
+
```python
|
|
589
|
+
async def run(self, ctx: RunContext) -> RunResult:
|
|
590
|
+
# Emit a message always shown to users
|
|
591
|
+
await ctx.emit_user_message("Processing your request...")
|
|
592
|
+
|
|
593
|
+
# Emit an error shown to users
|
|
594
|
+
await ctx.emit_error("Something went wrong", {"code": "ERR_001"})
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## Configuration Reference
|
|
598
|
+
|
|
599
|
+
| Setting | Type | Default | Description |
|
|
600
|
+
|---------|------|---------|-------------|
|
|
601
|
+
| `QUEUE_BACKEND` | str | `"postgres"` | Queue backend: `postgres`, `redis_streams` |
|
|
602
|
+
| `EVENT_BUS_BACKEND` | str | `"db"` | Event bus: `db`, `redis` |
|
|
603
|
+
| `REDIS_URL` | str | `None` | Redis connection URL |
|
|
604
|
+
| `MODEL_PROVIDER` | str | `"openai"` | LLM provider: `openai`, `anthropic`, `litellm` |
|
|
605
|
+
| `DEFAULT_MODEL` | str | `"gpt-4o"` | Default model name |
|
|
606
|
+
| `LEASE_TTL_SECONDS` | int | `30` | Worker lease duration |
|
|
607
|
+
| `RUN_TIMEOUT_SECONDS` | int | `900` | Maximum run duration |
|
|
608
|
+
| `MAX_RETRIES` | int | `3` | Retry attempts on failure |
|
|
609
|
+
| `RUNTIME_REGISTRY` | list | `[]` | Agent registration functions |
|
|
610
|
+
| `ANONYMOUS_SESSION_MODEL` | str | `None` | Path to anonymous session model |
|
|
611
|
+
| `EVENT_VISIBILITY` | dict | See above | Event visibility configuration |
|
|
612
|
+
| `DEBUG_MODE` | bool | `False` | Show debug-level events in UI |
|
|
613
|
+
| `LANGFUSE_ENABLED` | bool | `False` | Enable Langfuse tracing |
|
|
614
|
+
|
|
615
|
+
## Event Types
|
|
616
|
+
|
|
617
|
+
| Event | Visibility | Description |
|
|
618
|
+
|-------|------------|-------------|
|
|
619
|
+
| `run.started` | internal | Run execution began |
|
|
620
|
+
| `run.succeeded` | internal | Run completed successfully |
|
|
621
|
+
| `run.failed` | user | Run failed with error |
|
|
622
|
+
| `run.cancelled` | user | Run was cancelled |
|
|
623
|
+
| `run.timed_out` | user | Run exceeded timeout |
|
|
624
|
+
| `run.heartbeat` | internal | Worker heartbeat |
|
|
625
|
+
| `tool.call` | debug | Tool was invoked |
|
|
626
|
+
| `tool.result` | debug | Tool returned result |
|
|
627
|
+
| `assistant.message` | user | LLM generated message |
|
|
628
|
+
| `assistant.delta` | user | Token streaming delta |
|
|
629
|
+
| `state.checkpoint` | internal | State checkpoint saved |
|
|
630
|
+
| `error` | user | Runtime error (distinct from run.failed) |
|
|
631
|
+
|
|
632
|
+
## Management Commands
|
|
633
|
+
|
|
634
|
+
### runagent
|
|
635
|
+
|
|
636
|
+
Start agent workers:
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
# Basic usage
|
|
640
|
+
python manage.py runagent
|
|
641
|
+
|
|
642
|
+
# With options
|
|
643
|
+
python manage.py runagent \
|
|
644
|
+
--processes 4 \
|
|
645
|
+
--concurrency 20 \
|
|
646
|
+
--agent-keys chat-agent,tool-agent \
|
|
647
|
+
--queue-poll-interval 1.0
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
#### Auto-Reload (Development)
|
|
651
|
+
|
|
652
|
+
In `DEBUG=True` mode, `runagent` automatically reloads when Python files change—just like Django's `runserver`:
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
# Auto-reload enabled by default in DEBUG mode
|
|
656
|
+
python manage.py runagent
|
|
657
|
+
|
|
658
|
+
# Disable auto-reload
|
|
659
|
+
python manage.py runagent --noreload
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**Note:** Auto-reload only works in single-process mode. Multi-process mode (`--processes > 1`) automatically disables auto-reload.
|
|
663
|
+
|
|
664
|
+
## Frontend Integration
|
|
665
|
+
|
|
666
|
+
### JavaScript SSE Client
|
|
667
|
+
|
|
668
|
+
```javascript
|
|
669
|
+
const eventSource = new EventSource('/api/agents/runs/550e8400.../events/');
|
|
670
|
+
|
|
671
|
+
eventSource.addEventListener('assistant.message', (event) => {
|
|
672
|
+
const data = JSON.parse(event.data);
|
|
673
|
+
appendMessage(data.content);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
eventSource.addEventListener('run.succeeded', (event) => {
|
|
677
|
+
eventSource.close();
|
|
678
|
+
showComplete();
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
eventSource.addEventListener('run.failed', (event) => {
|
|
682
|
+
const data = JSON.parse(event.data);
|
|
683
|
+
showError(data.error);
|
|
684
|
+
eventSource.close();
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### React Hook Example
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
function useAgentRun(runId: string) {
|
|
692
|
+
const [events, setEvents] = useState<AgentEvent[]>([]);
|
|
693
|
+
const [status, setStatus] = useState<'running' | 'complete' | 'error'>('running');
|
|
694
|
+
|
|
695
|
+
useEffect(() => {
|
|
696
|
+
const es = new EventSource(`/api/agents/runs/${runId}/events/`);
|
|
697
|
+
|
|
698
|
+
es.onmessage = (event) => {
|
|
699
|
+
const data = JSON.parse(event.data);
|
|
700
|
+
setEvents(prev => [...prev, data]);
|
|
701
|
+
|
|
702
|
+
if (data.type === 'run.succeeded') setStatus('complete');
|
|
703
|
+
if (data.type === 'run.failed') setStatus('error');
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
return () => es.close();
|
|
707
|
+
}, [runId]);
|
|
708
|
+
|
|
709
|
+
return { events, status };
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
## Related Packages
|
|
714
|
+
|
|
715
|
+
- [agent-runtime-core](https://pypi.org/project/agent-runtime-core/) - The framework-agnostic core library (used internally)
|
|
716
|
+
|
|
717
|
+
## Contributing
|
|
718
|
+
|
|
719
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
720
|
+
|
|
721
|
+
## License
|
|
722
|
+
|
|
723
|
+
MIT License - see [LICENSE](LICENSE) for details.
|