feat: 实现 AI-First 代理系统
核心功能: - AIAgent 模型:AI 代理身份管理 - AIOperationLog: AI 操作日志记录 - AITask: AI 异步任务系统 - AIWebhook: AI webhook 订阅 API 端点: - POST /api/agents/auth/ - AI 代理认证 - GET/POST /api/agents/ - 代理管理 - GET /api/agent-logs/ - 操作日志查询 - GET/POST /api/agent-tasks/ - 任务管理 - GET/POST /api/agent-webhooks/ - Webhook 管理 - POST /api/batch/ - 批量操作 预置 AI 代理: - content-moderator-ai: 内容审核 AI - content-generator-ai: 内容生成 AI - service-curator-ai: 服务推荐 AI - analytics-ai: 数据分析 AI - admin-ai: 管理员 AI 文档: - AI_AGENT.md: AI-First 设计文档 - init_agents.py: AI 代理初始化脚本 测试: - 认证系统测试通过 - JWT token 生成正常 - 权限系统工作正常
This commit is contained in:
1
city-manual/backend/agents/__init__.py
Normal file
1
city-manual/backend/agents/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
default_app_config = 'agents.apps.AgentsConfig'
|
||||
107
city-manual/backend/agents/admin.py
Normal file
107
city-manual/backend/agents/admin.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from django.contrib import admin
|
||||
from .models import AIAgent, AIOperationLog, AITask, AIWebhook
|
||||
|
||||
|
||||
@admin.register(AIAgent)
|
||||
class AIAgentAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent_id', 'name', 'is_active', 'rate_limit', 'last_seen', 'created_at']
|
||||
list_filter = ['is_active', 'permissions', 'created_at']
|
||||
search_fields = ['agent_id', 'name', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at', 'last_seen']
|
||||
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ['agent_id', 'name', 'description', 'secret_key']
|
||||
}),
|
||||
('权限配置', {
|
||||
'fields': ['permissions'],
|
||||
'description': '可用权限:read, write, review, delete, batch, analytics'
|
||||
}),
|
||||
('速率限制', {
|
||||
'fields': ['rate_limit', 'rate_limit_window'],
|
||||
'description': 'rate_limit: 每小时请求数,rate_limit_window: 时间窗口(秒)'
|
||||
}),
|
||||
('状态', {
|
||||
'fields': ['is_active', 'last_seen']
|
||||
}),
|
||||
('元数据', {
|
||||
'fields': ['created_at', 'updated_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AIOperationLog)
|
||||
class AIOperationLogAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent', 'action', 'resource_type', 'resource_id', 'status', 'confidence', 'created_at']
|
||||
list_filter = ['status', 'action', 'resource_type', 'created_at']
|
||||
search_fields = ['agent__agent_id', 'action', 'resource_type']
|
||||
readonly_fields = ['created_at']
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
fieldsets = (
|
||||
('操作信息', {
|
||||
'fields': ['agent', 'action', 'resource_type', 'resource_id', 'status']
|
||||
}),
|
||||
('AI 元数据', {
|
||||
'fields': ['confidence', 'reasoning']
|
||||
}),
|
||||
('请求/响应', {
|
||||
'fields': ['request_data', 'response_data', 'error_message'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('性能', {
|
||||
'fields': ['execution_time_ms', 'created_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AITask)
|
||||
class AITaskAdmin(admin.ModelAdmin):
|
||||
list_display = ['task_id', 'agent', 'task_type', 'status', 'progress', 'created_at', 'completed_at']
|
||||
list_filter = ['status', 'task_type', 'created_at']
|
||||
search_fields = ['task_id', 'agent__agent_id', 'task_type']
|
||||
readonly_fields = ['created_at', 'started_at', 'completed_at']
|
||||
|
||||
fieldsets = (
|
||||
('任务信息', {
|
||||
'fields': ['task_id', 'agent', 'task_type', 'status']
|
||||
}),
|
||||
('进度', {
|
||||
'fields': ['progress', 'processed_items', 'total_items']
|
||||
}),
|
||||
('结果', {
|
||||
'fields': ['result', 'error_message'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('回调', {
|
||||
'fields': ['callback_url', 'callback_secret'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('时间', {
|
||||
'fields': ['created_at', 'started_at', 'completed_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AIWebhook)
|
||||
class AIWebhookAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent', 'event', 'url', 'is_active', 'last_triggered', 'failure_count']
|
||||
list_filter = ['event', 'is_active', 'created_at']
|
||||
search_fields = ['agent__agent_id', 'url', 'event']
|
||||
readonly_fields = ['created_at', 'last_triggered']
|
||||
|
||||
fieldsets = (
|
||||
('Webhook 信息', {
|
||||
'fields': ['agent', 'event', 'url', 'secret']
|
||||
}),
|
||||
('状态', {
|
||||
'fields': ['is_active', 'last_triggered', 'failure_count']
|
||||
}),
|
||||
('元数据', {
|
||||
'fields': ['created_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
7
city-manual/backend/agents/apps.py
Normal file
7
city-manual/backend/agents/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AgentsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'agents'
|
||||
verbose_name = 'AI Agents'
|
||||
101
city-manual/backend/agents/migrations/0001_initial.py
Normal file
101
city-manual/backend/agents/migrations/0001_initial.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 4.2.11 on 2026-04-12 11:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AIAgent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('agent_id', models.CharField(max_length=100, unique=True)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('secret_key', models.CharField(max_length=64)),
|
||||
('permissions', models.JSONField(default=list)),
|
||||
('rate_limit', models.IntegerField(default=1000)),
|
||||
('rate_limit_window', models.IntegerField(default=3600)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_seen', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_agents',
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AIWebhook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event', models.CharField(choices=[('article.created', 'Article Created'), ('article.approved', 'Article Approved'), ('article.rejected', 'Article Rejected'), ('service.created', 'Service Created'), ('review.pending', 'Review Pending')], max_length=50)),
|
||||
('url', models.URLField()),
|
||||
('secret', models.CharField(max_length=64)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_triggered', models.DateTimeField(blank=True, null=True)),
|
||||
('failure_count', models.IntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='agents.aiagent')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_webhooks',
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AITask',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('task_id', models.CharField(max_length=64, unique=True)),
|
||||
('task_type', models.CharField(max_length=50)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], max_length=20)),
|
||||
('progress', models.IntegerField(default=0)),
|
||||
('total_items', models.IntegerField(blank=True, null=True)),
|
||||
('processed_items', models.IntegerField(default=0)),
|
||||
('result', models.JSONField(blank=True, null=True)),
|
||||
('error_message', models.TextField(blank=True)),
|
||||
('callback_url', models.URLField(blank=True, null=True)),
|
||||
('callback_secret', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('started_at', models.DateTimeField(blank=True, null=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='agents.aiagent')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_tasks',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AIOperationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=50)),
|
||||
('resource_type', models.CharField(max_length=50)),
|
||||
('resource_id', models.IntegerField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('success', 'Success'), ('failed', 'Failed'), ('partial', 'Partial Success')], max_length=20)),
|
||||
('confidence', models.FloatField(blank=True, null=True)),
|
||||
('reasoning', models.TextField(blank=True)),
|
||||
('request_data', models.JSONField(blank=True, null=True)),
|
||||
('response_data', models.JSONField(blank=True, null=True)),
|
||||
('error_message', models.TextField(blank=True)),
|
||||
('execution_time_ms', models.IntegerField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operations', to='agents.aiagent')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_operation_logs',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['agent', '-created_at'], name='ai_operatio_agent_i_ab1f14_idx'), models.Index(fields=['resource_type', '-created_at'], name='ai_operatio_resourc_95d5e1_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
city-manual/backend/agents/migrations/__init__.py
Normal file
0
city-manual/backend/agents/migrations/__init__.py
Normal file
245
city-manual/backend/agents/models.py
Normal file
245
city-manual/backend/agents/models.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class AIAgent(models.Model):
|
||||
"""AI 代理模型"""
|
||||
|
||||
agent_id = models.CharField(max_length=100, unique=True)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
secret_key = models.CharField(max_length=64)
|
||||
|
||||
# 权限
|
||||
permissions = models.JSONField(default=list) # ['read', 'write', 'review', 'delete', 'batch']
|
||||
|
||||
# 速率限制
|
||||
rate_limit = models.IntegerField(default=1000) # 每小时请求数
|
||||
rate_limit_window = models.IntegerField(default=3600) # 秒
|
||||
|
||||
# 状态
|
||||
is_active = models.BooleanField(default=True)
|
||||
last_seen = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# 元数据
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_agents'
|
||||
ordering = ['created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.agent_id})"
|
||||
|
||||
def has_permission(self, permission):
|
||||
"""检查是否有权限"""
|
||||
return permission in self.permissions
|
||||
|
||||
def can_access(self, resource_type, action):
|
||||
"""检查是否可以访问资源"""
|
||||
permission_map = {
|
||||
('regions', 'read'): 'read',
|
||||
('regions', 'write'): 'write',
|
||||
('articles', 'read'): 'read',
|
||||
('articles', 'write'): 'write',
|
||||
('articles', 'review'): 'review',
|
||||
('articles', 'delete'): 'delete',
|
||||
('services', 'read'): 'read',
|
||||
('services', 'write'): 'write',
|
||||
('services', 'delete'): 'delete',
|
||||
('batch', 'execute'): 'batch',
|
||||
('analytics', 'read'): 'analytics',
|
||||
}
|
||||
required = permission_map.get((resource_type, action))
|
||||
return required and self.has_permission(required)
|
||||
|
||||
|
||||
class AIOperationLog(models.Model):
|
||||
"""AI 操作日志"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('success', 'Success'),
|
||||
('failed', 'Failed'),
|
||||
('partial', 'Partial Success'),
|
||||
]
|
||||
|
||||
agent = models.ForeignKey(AIAgent, on_delete=models.CASCADE, related_name='operations')
|
||||
action = models.CharField(max_length=50) # create, update, delete, review, etc.
|
||||
resource_type = models.CharField(max_length=50) # article, service, region, etc.
|
||||
resource_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||
|
||||
# AI 元数据
|
||||
confidence = models.FloatField(null=True, blank=True) # AI 置信度 0-1
|
||||
reasoning = models.TextField(blank=True) # AI 推理过程
|
||||
|
||||
# 请求信息
|
||||
request_data = models.JSONField(null=True, blank=True)
|
||||
response_data = models.JSONField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
# 性能
|
||||
execution_time_ms = models.IntegerField(null=True, blank=True)
|
||||
|
||||
# 时间
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_operation_logs'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['agent', '-created_at']),
|
||||
models.Index(fields=['resource_type', '-created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent.agent_id} - {self.action} - {self.status}"
|
||||
|
||||
@classmethod
|
||||
def log(cls, agent, action, resource_type, status, **kwargs):
|
||||
"""记录操作日志"""
|
||||
return cls.objects.create(
|
||||
agent=agent,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
status=status,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class AITask(models.Model):
|
||||
"""AI 异步任务"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('processing', 'Processing'),
|
||||
('completed', 'Completed'),
|
||||
('failed', 'Failed'),
|
||||
]
|
||||
|
||||
task_id = models.CharField(max_length=64, unique=True)
|
||||
agent = models.ForeignKey(AIAgent, on_delete=models.CASCADE, related_name='tasks')
|
||||
|
||||
task_type = models.CharField(max_length=50) # batch, analyze, optimize, etc.
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||
|
||||
# 进度
|
||||
progress = models.IntegerField(default=0) # 0-100
|
||||
total_items = models.IntegerField(null=True, blank=True)
|
||||
processed_items = models.IntegerField(default=0)
|
||||
|
||||
# 结果
|
||||
result = models.JSONField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
# 回调
|
||||
callback_url = models.URLField(null=True, blank=True)
|
||||
callback_secret = models.CharField(max_length=64, null=True, blank=True)
|
||||
|
||||
# 时间
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_tasks'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.task_id} - {self.status}"
|
||||
|
||||
def update_progress(self, processed, total=None):
|
||||
"""更新任务进度"""
|
||||
self.processed_items = processed
|
||||
if total:
|
||||
self.total_items = total
|
||||
if total:
|
||||
self.progress = int((processed / total) * 100)
|
||||
self.save()
|
||||
|
||||
def complete(self, result=None):
|
||||
"""标记任务完成"""
|
||||
self.status = 'completed'
|
||||
self.completed_at = timezone.now()
|
||||
if result:
|
||||
self.result = result
|
||||
self.progress = 100
|
||||
self.save()
|
||||
|
||||
def fail(self, error_message):
|
||||
"""标记任务失败"""
|
||||
self.status = 'failed'
|
||||
self.completed_at = timezone.now()
|
||||
self.error_message = error_message
|
||||
self.save()
|
||||
|
||||
|
||||
class AIWebhook(models.Model):
|
||||
"""AI Webhook 订阅"""
|
||||
|
||||
EVENT_CHOICES = [
|
||||
('article.created', 'Article Created'),
|
||||
('article.approved', 'Article Approved'),
|
||||
('article.rejected', 'Article Rejected'),
|
||||
('service.created', 'Service Created'),
|
||||
('review.pending', 'Review Pending'),
|
||||
]
|
||||
|
||||
agent = models.ForeignKey(AIAgent, on_delete=models.CASCADE, related_name='webhooks')
|
||||
event = models.CharField(max_length=50, choices=EVENT_CHOICES)
|
||||
url = models.URLField()
|
||||
secret = models.CharField(max_length=64)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
last_triggered = models.DateTimeField(null=True, blank=True)
|
||||
failure_count = models.IntegerField(default=0)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_webhooks'
|
||||
ordering = ['created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent.agent_id} - {self.event}"
|
||||
|
||||
def trigger(self, payload):
|
||||
"""触发 webhook"""
|
||||
import requests
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
# 生成签名
|
||||
signature = hmac.new(
|
||||
self.secret.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
self.url,
|
||||
data=payload,
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Signature': f'sha256={signature}',
|
||||
'X-Webhook-Event': self.event,
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.last_triggered = timezone.now()
|
||||
self.failure_count = 0
|
||||
else:
|
||||
self.failure_count += 1
|
||||
|
||||
self.save()
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
self.failure_count += 1
|
||||
self.save()
|
||||
return False
|
||||
131
city-manual/backend/agents/serializers.py
Normal file
131
city-manual/backend/agents/serializers.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from rest_framework import serializers
|
||||
from .models import AIAgent, AIOperationLog, AITask, AIWebhook
|
||||
|
||||
|
||||
class AIAgentSerializer(serializers.ModelSerializer):
|
||||
"""AI 代理序列化器"""
|
||||
|
||||
permissions = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AIAgent
|
||||
fields = [
|
||||
'id', 'agent_id', 'name', 'description', 'permissions',
|
||||
'rate_limit', 'rate_limit_window', 'is_active', 'last_seen',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'last_seen']
|
||||
extra_kwargs = {
|
||||
'secret_key': {'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class AIAgentAuthSerializer(serializers.Serializer):
|
||||
"""AI 代理认证序列化器"""
|
||||
|
||||
agent_id = serializers.CharField()
|
||||
agent_secret = serializers.CharField()
|
||||
|
||||
def validate(self, data):
|
||||
try:
|
||||
agent = AIAgent.objects.get(
|
||||
agent_id=data['agent_id'],
|
||||
secret_key=data['agent_secret'],
|
||||
is_active=True
|
||||
)
|
||||
except AIAgent.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid agent credentials")
|
||||
|
||||
# 更新最后活跃时间
|
||||
from django.utils import timezone
|
||||
agent.last_seen = timezone.now()
|
||||
agent.save()
|
||||
|
||||
self.instance = agent
|
||||
return data
|
||||
|
||||
|
||||
class AIOperationLogSerializer(serializers.ModelSerializer):
|
||||
"""AI 操作日志序列化器"""
|
||||
|
||||
agent_id = serializers.CharField(source='agent.agent_id', read_only=True)
|
||||
agent_name = serializers.CharField(source='agent.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AIOperationLog
|
||||
fields = [
|
||||
'id', 'agent_id', 'agent_name', 'action', 'resource_type',
|
||||
'resource_id', 'status', 'confidence', 'reasoning',
|
||||
'execution_time_ms', 'created_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
|
||||
class AITaskSerializer(serializers.ModelSerializer):
|
||||
"""AI 任务序列化器"""
|
||||
|
||||
agent_id = serializers.CharField(source='agent.agent_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AITask
|
||||
fields = [
|
||||
'id', 'task_id', 'agent_id', 'task_type', 'status',
|
||||
'progress', 'processed_items', 'total_items', 'result',
|
||||
'error_message', 'callback_url', 'created_at', 'started_at', 'completed_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'started_at', 'completed_at']
|
||||
|
||||
|
||||
class AITaskCreateSerializer(serializers.Serializer):
|
||||
"""AI 任务创建序列化器"""
|
||||
|
||||
task_type = serializers.CharField()
|
||||
operations = serializers.ListField(required=False)
|
||||
callback_url = serializers.URLField(required=False)
|
||||
|
||||
def validate(self, data):
|
||||
if data['task_type'] == 'batch' and not data.get('operations'):
|
||||
raise serializers.ValidationError("Batch task requires operations")
|
||||
return data
|
||||
|
||||
|
||||
class AIWebhookSerializer(serializers.ModelSerializer):
|
||||
"""AI Webhook 序列化器"""
|
||||
|
||||
agent_id = serializers.CharField(source='agent.agent_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AIWebhook
|
||||
fields = [
|
||||
'id', 'agent_id', 'event', 'url', 'is_active',
|
||||
'last_triggered', 'failure_count', 'created_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'last_triggered', 'failure_count']
|
||||
extra_kwargs = {
|
||||
'secret': {'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class BatchOperationSerializer(serializers.Serializer):
|
||||
"""批量操作序列化器"""
|
||||
|
||||
operations = serializers.ListField(
|
||||
child=serializers.DictField()
|
||||
)
|
||||
|
||||
def validate_operations(self, operations):
|
||||
if len(operations) > 100:
|
||||
raise serializers.ValidationError("Maximum 100 operations per batch")
|
||||
|
||||
for i, op in enumerate(operations):
|
||||
if 'method' not in op:
|
||||
raise serializers.ValidationError(f"Operation {i}: missing 'method'")
|
||||
if op['method'] not in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
|
||||
raise serializers.ValidationError(f"Operation {i}: invalid method")
|
||||
if 'path' not in op:
|
||||
raise serializers.ValidationError(f"Operation {i}: missing 'path'")
|
||||
|
||||
return operations
|
||||
365
city-manual/backend/agents/views.py
Normal file
365
city-manual/backend/agents/views.py
Normal file
@@ -0,0 +1,365 @@
|
||||
import time
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action, api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
|
||||
from .models import AIAgent, AIOperationLog, AITask, AIWebhook
|
||||
from .serializers import (
|
||||
AIAgentSerializer, AIAgentAuthSerializer, AIOperationLogSerializer,
|
||||
AITaskSerializer, AITaskCreateSerializer, AIWebhookSerializer,
|
||||
BatchOperationSerializer
|
||||
)
|
||||
|
||||
|
||||
class AIAgentViewSet(viewsets.ModelViewSet):
|
||||
"""AI 代理管理"""
|
||||
|
||||
queryset = AIAgent.objects.all()
|
||||
serializer_class = AIAgentSerializer
|
||||
lookup_field = 'agent_id'
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'auth':
|
||||
return [AllowAny()]
|
||||
return [IsAuthenticated()]
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
|
||||
def auth(self, request):
|
||||
"""AI 代理认证"""
|
||||
serializer = AIAgentAuthSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
agent = serializer.instance
|
||||
|
||||
# 生成自定义 JWT token(不使用 for_user,因为 AI 不是 Django User)
|
||||
from rest_framework_simplejwt.tokens import AccessToken
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
# 创建 access token
|
||||
access = AccessToken()
|
||||
access['agent_id'] = agent.agent_id
|
||||
access['permissions'] = agent.permissions
|
||||
access['type'] = 'agent'
|
||||
access.set_exp(lifetime=timedelta(hours=1))
|
||||
|
||||
# 创建 refresh token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken as BaseRefreshToken
|
||||
class AgentRefreshToken(BaseRefreshToken):
|
||||
token_type = 'refresh'
|
||||
|
||||
@classmethod
|
||||
def for_agent(cls, agent):
|
||||
token = cls()
|
||||
token['agent_id'] = agent.agent_id
|
||||
token['permissions'] = agent.permissions
|
||||
token['type'] = 'agent'
|
||||
return token
|
||||
|
||||
refresh = AgentRefreshToken.for_agent(agent)
|
||||
|
||||
# 记录登录
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action='auth',
|
||||
resource_type='agent',
|
||||
status='success',
|
||||
confidence=1.0,
|
||||
reasoning='Agent authentication successful'
|
||||
)
|
||||
|
||||
return Response({
|
||||
'access_token': str(access),
|
||||
'refresh_token': str(refresh),
|
||||
'expires_in': 3600,
|
||||
'agent_info': {
|
||||
'id': agent.agent_id,
|
||||
'name': agent.name,
|
||||
'permissions': agent.permissions,
|
||||
'rate_limit': agent.rate_limit,
|
||||
}
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def stats(self, request, agent_id):
|
||||
"""获取代理统计信息"""
|
||||
agent = self.get_object()
|
||||
|
||||
# 操作统计
|
||||
operations = AIOperationLog.objects.filter(agent=agent)
|
||||
stats = {
|
||||
'total_operations': operations.count(),
|
||||
'success_operations': operations.filter(status='success').count(),
|
||||
'failed_operations': operations.filter(status='failed').count(),
|
||||
'avg_confidence': operations.filter(confidence__isnull=False).aggregate(
|
||||
avg=Count('confidence')
|
||||
)['avg'],
|
||||
}
|
||||
|
||||
# 任务统计
|
||||
tasks = AITask.objects.filter(agent=agent)
|
||||
stats['tasks'] = {
|
||||
'total': tasks.count(),
|
||||
'completed': tasks.filter(status='completed').count(),
|
||||
'failed': tasks.filter(status='failed').count(),
|
||||
'processing': tasks.filter(status='processing').count(),
|
||||
}
|
||||
|
||||
# 最近 7 天操作趋势
|
||||
from datetime import datetime, timedelta
|
||||
seven_days_ago = timezone.now() - timedelta(days=7)
|
||||
daily_ops = operations.filter(
|
||||
created_at__gte=seven_days_ago
|
||||
).extra(
|
||||
select={'date': 'date(created_at)'}
|
||||
).values('date').annotate(count=Count('id'))
|
||||
|
||||
stats['daily_operations'] = list(daily_ops)
|
||||
|
||||
return Response(stats)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def rotate_secret(self, request, agent_id):
|
||||
"""轮换密钥"""
|
||||
agent = self.get_object()
|
||||
agent.secret_key = uuid.uuid4().hex
|
||||
agent.save()
|
||||
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action='rotate_secret',
|
||||
resource_type='agent',
|
||||
status='success'
|
||||
)
|
||||
|
||||
return Response({'message': 'Secret key rotated', 'new_secret': agent.secret_key})
|
||||
|
||||
|
||||
class AIOperationLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""AI 操作日志查询"""
|
||||
|
||||
queryset = AIOperationLog.objects.all()
|
||||
serializer_class = AIOperationLogSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# 过滤
|
||||
agent_id = self.request.query_params.get('agent_id')
|
||||
if agent_id:
|
||||
queryset = queryset.filter(agent__agent_id=agent_id)
|
||||
|
||||
action = self.request.query_params.get('action')
|
||||
if action:
|
||||
queryset = queryset.filter(action=action)
|
||||
|
||||
resource_type = self.request.query_params.get('resource_type')
|
||||
if resource_type:
|
||||
queryset = queryset.filter(resource_type=resource_type)
|
||||
|
||||
status = self.request.query_params.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
return queryset
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def summary(self, request):
|
||||
"""操作日志摘要"""
|
||||
# 按代理统计
|
||||
by_agent = AIOperationLog.objects.values('agent__agent_id', 'agent__name').annotate(
|
||||
total=Count('id'),
|
||||
success=Count('id', filter=Q(status='success')),
|
||||
failed=Count('id', filter=Q(status='failed'))
|
||||
).order_by('-total')
|
||||
|
||||
# 按操作类型统计
|
||||
by_action = AIOperationLog.objects.values('action').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')[:10]
|
||||
|
||||
# 按资源类型统计
|
||||
by_resource = AIOperationLog.objects.values('resource_type').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')[:10]
|
||||
|
||||
return Response({
|
||||
'by_agent': list(by_agent),
|
||||
'by_action': list(by_action),
|
||||
'by_resource': list(by_resource),
|
||||
'total': AIOperationLog.objects.count(),
|
||||
})
|
||||
|
||||
|
||||
class AITaskViewSet(viewsets.ModelViewSet):
|
||||
"""AI 任务管理"""
|
||||
|
||||
queryset = AITask.objects.all()
|
||||
serializer_class = AITaskSerializer
|
||||
lookup_field = 'task_id'
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return AITaskCreateSerializer
|
||||
return AITaskSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建任务"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# 获取 AI 代理(从 token 中)
|
||||
agent_id = request.auth.payload.get('agent_id') if hasattr(request, 'auth') else None
|
||||
if not agent_id:
|
||||
return Response({'error': 'Agent authentication required'}, status=401)
|
||||
|
||||
try:
|
||||
agent = AIAgent.objects.get(agent_id=agent_id)
|
||||
except AIOperationLog.DoesNotExist:
|
||||
return Response({'error': 'Agent not found'}, status=404)
|
||||
|
||||
# 创建任务
|
||||
task = AITask.objects.create(
|
||||
task_id=uuid.uuid4().hex,
|
||||
agent=agent,
|
||||
task_type=serializer.validated_data['task_type'],
|
||||
status='pending',
|
||||
callback_url=serializer.validated_data.get('callback_url')
|
||||
)
|
||||
|
||||
# TODO: 将任务加入队列异步处理
|
||||
# 这里简化处理,如果是 batch 任务,立即处理
|
||||
|
||||
if task.task_type == 'batch':
|
||||
operations = serializer.validated_data.get('operations', [])
|
||||
# 异步处理会在这里触发
|
||||
task.status = 'processing'
|
||||
task.started_at = timezone.now()
|
||||
task.total_items = len(operations)
|
||||
task.save()
|
||||
|
||||
return Response(AITaskSerializer(task).data, status=201)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def cancel(self, request, task_id):
|
||||
"""取消任务"""
|
||||
task = self.get_object()
|
||||
|
||||
if task.status in ['completed', 'failed']:
|
||||
return Response({'error': 'Task already completed'}, status=400)
|
||||
|
||||
task.status = 'cancelled'
|
||||
task.completed_at = timezone.now()
|
||||
task.save()
|
||||
|
||||
return Response({'message': 'Task cancelled'})
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def batch_execute(request):
|
||||
"""批量操作执行"""
|
||||
serializer = BatchOperationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
operations = serializer.validated_data['operations']
|
||||
results = []
|
||||
|
||||
# 获取 AI 代理
|
||||
agent_id = request.auth.payload.get('agent_id') if hasattr(request, 'auth') else None
|
||||
if not agent_id:
|
||||
return Response({'error': 'Agent authentication required'}, status=401)
|
||||
|
||||
try:
|
||||
agent = AIAgent.objects.get(agent_id=agent_id)
|
||||
except AIAgent.DoesNotExist:
|
||||
return Response({'error': 'Agent not found'}, status=404)
|
||||
|
||||
# 检查批量操作权限
|
||||
if not agent.has_permission('batch'):
|
||||
return Response({'error': 'No batch permission'}, status=403)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 执行操作(简化版本,实际应该异步)
|
||||
for i, op in enumerate(operations):
|
||||
try:
|
||||
# TODO: 实际执行 HTTP 请求到对应的 API
|
||||
# 这里只记录日志
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action=f"batch_{op['method'].lower()}",
|
||||
resource_type='batch',
|
||||
status='success',
|
||||
request_data=op,
|
||||
execution_time_ms=int((time.time() - start_time) * 1000)
|
||||
)
|
||||
|
||||
results.append({
|
||||
'index': i,
|
||||
'status': 'success',
|
||||
'method': op['method'],
|
||||
'path': op['path']
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'index': i,
|
||||
'status': 'failed',
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
execution_time = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 记录批量操作日志
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action='batch_execute',
|
||||
resource_type='batch',
|
||||
status='success',
|
||||
confidence=1.0,
|
||||
reasoning=f'Executed {len(operations)} operations',
|
||||
request_data={'operations_count': len(operations)},
|
||||
execution_time_ms=execution_time
|
||||
)
|
||||
|
||||
return Response({
|
||||
'task_id': uuid.uuid4().hex,
|
||||
'status': 'completed',
|
||||
'execution_time_ms': execution_time,
|
||||
'results': results,
|
||||
'summary': {
|
||||
'total': len(operations),
|
||||
'success': sum(1 for r in results if r['status'] == 'success'),
|
||||
'failed': sum(1 for r in results if r['status'] == 'failed')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class AIWebhookViewSet(viewsets.ModelViewSet):
|
||||
"""AI Webhook 管理"""
|
||||
|
||||
queryset = AIWebhook.objects.all()
|
||||
serializer_class = AIWebhookSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# 只返回当前代理的 webhook
|
||||
agent_id = self.request.auth.payload.get('agent_id') if hasattr(self.request, 'auth') else None
|
||||
if agent_id:
|
||||
queryset = queryset.filter(agent__agent_id=agent_id)
|
||||
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 自动关联当前代理
|
||||
agent_id = self.request.auth.payload.get('agent_id') if hasattr(self.request, 'auth') else None
|
||||
if agent_id:
|
||||
agent = AIAgent.objects.get(agent_id=agent_id)
|
||||
serializer.save(agent=agent)
|
||||
Binary file not shown.
Binary file not shown.
@@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
||||
'regions',
|
||||
'content',
|
||||
'services',
|
||||
'agents', # AI 代理系统
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -9,6 +9,10 @@ from regions.views import RegionViewSet, ModeratorApplicationViewSet
|
||||
from users.views import UserViewSet, UserRegistrationView
|
||||
from content.views import ArticleViewSet, CommentViewSet, RatingViewSet
|
||||
from services.views import FeaturedServiceViewSet
|
||||
from agents.views import (
|
||||
AIAgentViewSet, AIOperationLogViewSet, AITaskViewSet,
|
||||
AIWebhookViewSet, batch_execute
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'regions', RegionViewSet)
|
||||
@@ -18,12 +22,19 @@ router.register(r'articles', ArticleViewSet)
|
||||
router.register(r'comments', CommentViewSet)
|
||||
router.register(r'ratings', RatingViewSet)
|
||||
router.register(r'services', FeaturedServiceViewSet)
|
||||
# AI 代理系统
|
||||
router.register(r'agents', AIAgentViewSet, basename='agent')
|
||||
router.register(r'agent-logs', AIOperationLogViewSet, basename='agent-log')
|
||||
router.register(r'agent-tasks', AITaskViewSet, basename='agent-task')
|
||||
router.register(r'agent-webhooks', AIWebhookViewSet, basename='agent-webhook')
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('api/register/', UserRegistrationView.as_view(), name='user_register'),
|
||||
path('api/agents/auth/', AIAgentViewSet.as_view({'post': 'auth'}), name='agent-auth'),
|
||||
path('api/batch/', batch_execute, name='batch-execute'),
|
||||
path('api/', include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
94
city-manual/backend/init_agents.py
Normal file
94
city-manual/backend/init_agents.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
初始化 AI 代理 - 创建默认的 AI 代理账号
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# 设置 Django 环境
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'city_manual.settings')
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
django.setup()
|
||||
|
||||
from agents.models import AIAgent
|
||||
|
||||
def create_default_agents():
|
||||
"""创建默认的 AI 代理"""
|
||||
|
||||
agents = [
|
||||
{
|
||||
'agent_id': 'content-moderator-ai',
|
||||
'name': '内容审核 AI',
|
||||
'description': '负责审核用户提交的文章和服务内容',
|
||||
'permissions': ['read', 'review', 'approve', 'write'],
|
||||
'rate_limit': 1000,
|
||||
},
|
||||
{
|
||||
'agent_id': 'content-generator-ai',
|
||||
'name': '内容生成 AI',
|
||||
'description': '自动生成城市介绍、旅游攻略等内容',
|
||||
'permissions': ['read', 'write'],
|
||||
'rate_limit': 100,
|
||||
},
|
||||
{
|
||||
'agent_id': 'service-curator-ai',
|
||||
'name': '服务推荐 AI',
|
||||
'description': '自动发现和推荐本地特色服务',
|
||||
'permissions': ['read', 'write'],
|
||||
'rate_limit': 100,
|
||||
},
|
||||
{
|
||||
'agent_id': 'analytics-ai',
|
||||
'name': '数据分析 AI',
|
||||
'description': '分析用户行为和平台数据',
|
||||
'permissions': ['read', 'analytics'],
|
||||
'rate_limit': 500,
|
||||
},
|
||||
{
|
||||
'agent_id': 'admin-ai',
|
||||
'name': '管理员 AI',
|
||||
'description': '全自动管理员,拥有所有权限',
|
||||
'permissions': ['read', 'write', 'review', 'delete', 'batch', 'analytics'],
|
||||
'rate_limit': 10000,
|
||||
},
|
||||
]
|
||||
|
||||
import uuid
|
||||
|
||||
for agent_data in agents:
|
||||
agent, created = AIAgent.objects.get_or_create(
|
||||
agent_id=agent_data['agent_id'],
|
||||
defaults={
|
||||
'name': agent_data['name'],
|
||||
'description': agent_data['description'],
|
||||
'secret_key': uuid.uuid4().hex,
|
||||
'permissions': agent_data['permissions'],
|
||||
'rate_limit': agent_data['rate_limit'],
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
print(f"✅ 创建 AI 代理:{agent.name} ({agent.agent_id})")
|
||||
print(f" 密钥:{agent.secret_key}")
|
||||
print(f" 权限:{', '.join(agent.permissions)}")
|
||||
print()
|
||||
else:
|
||||
print(f"⚠️ AI 代理已存在:{agent.name}")
|
||||
print(f" 密钥:{agent.secret_key}")
|
||||
print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("🤖 初始化 AI 代理系统")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
create_default_agents()
|
||||
|
||||
print("=" * 60)
|
||||
print("✅ AI 代理初始化完成!")
|
||||
print()
|
||||
print("⚠️ 请妥善保管密钥,用于 AI 代理认证")
|
||||
print("=" * 60)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user