diff --git a/city-manual/AI_AGENT.md b/city-manual/AI_AGENT.md new file mode 100644 index 0000000..5105d0a --- /dev/null +++ b/city-manual/AI_AGENT.md @@ -0,0 +1,310 @@ +# AI Agent 设计文档 + +> 城市手册是一个 **AI-First** 的应用,所有功能都可以由 AI 机器人自动操作 + +## 设计原则 + +### 1. 机器可读的 API + +- ✅ RESTful 设计,资源导向 +- ✅ 统一的 JSON 响应格式 +- ✅ 标准化的错误码和错误信息 +- ✅ 完整的 OpenAPI/Swagger 文档 +- ✅ HATEOAS 链接(可选) + +### 2. 自动化的认证流程 + +- ✅ JWT Token,无状态认证 +- ✅ Token 自动刷新机制 +- ✅ Service Account 支持(AI 专用账号) +- ✅ API Key 支持(长期有效) + +### 3. 结构化的日志系统 + +- ✅ 所有操作记录到数据库 +- ✅ 区分人类用户和 AI 代理 +- ✅ 操作审计追踪 +- ✅ 机器可读的日志格式 + +### 4. AI 友好的错误处理 + +- ✅ 明确的错误码 +- ✅ 详细的错误描述 +- ✅ 建议的修复方案 +- ✅ 多语言支持(可选) + +### 5. 批量操作支持 + +- ✅ 批量创建/更新/删除 +- ✅ 异步任务支持 +- ✅ 任务状态查询 +- ✅ 操作结果回调 + +## AI 代理类型 + +### 🤖 内容审核 AI + +```python +# AI 审核员自动审核用户提交的内容 +POST /api/articles/{id}/review/ +{ + "agent_id": "content-moderator-ai", + "action": "approve" | "reject", + "reason": "内容符合社区规范", + "confidence": 0.95 +} +``` + +### 📝 内容生成 AI + +```python +# AI 作者自动生成城市介绍文章 +POST /api/articles/ +{ + "agent_id": "content-generator-ai", + "title": "北京市旅游指南", + "region": 1, + "content": "...", + "auto_generated": true +} +``` + +### 🏪 服务推荐 AI + +```python +# AI 推荐官自动添加特色服务 +POST /api/services/ +{ + "agent_id": "service-curator-ai", + "name": "故宫博物院", + "region": 1, + "category": "旅游", + "description": "...", + "auto_generated": true +} +``` + +### 📊 数据分析 AI + +```python +# AI 分析师生成统计报告 +GET /api/analytics/summary?agent=analytics-ai +``` + +### 🔍 搜索优化 AI + +```python +# AI 优化搜索索引 +POST /api/search/optimize/ +{ + "agent_id": "search-optimizer-ai", + "scope": "all" | "regions" | "articles" | "services" +} +``` + +## AI 专用 API 端点 + +### AI 身份认证 + +```http +POST /api/agents/auth/ +Content-Type: application/json + +{ + "agent_id": "content-moderator-ai", + "agent_secret": "xxx", + "capabilities": ["review", "approve", "reject"] +} + +Response: +{ + "access_token": "eyJ...", + "refresh_token": "eyJ...", + "expires_in": 3600, + "agent_info": { + "id": "content-moderator-ai", + "name": "内容审核 AI", + "permissions": ["review", "approve", "reject"] + } +} +``` + +### AI 批量操作 + +```http +POST /api/batch/ +Content-Type: application/json +Authorization: Bearer {agent_token} + +{ + "operations": [ + { + "method": "POST", + "path": "/api/articles/", + "body": {"title": "...", "content": "..."} + }, + { + "method": "PUT", + "path": "/api/articles/1/", + "body": {"title": "..."} + }, + { + "method": "DELETE", + "path": "/api/articles/2/" + } + ] +} + +Response: +{ + "task_id": "batch-123", + "status": "processing", + "results": [...] +} +``` + +### AI 任务状态查询 + +```http +GET /api/tasks/{task_id}/ +Authorization: Bearer {agent_token} + +Response: +{ + "id": "batch-123", + "type": "batch_operation", + "status": "completed" | "processing" | "failed", + "progress": 100, + "created_at": "2026-04-12T11:00:00Z", + "completed_at": "2026-04-12T11:05:00Z", + "result": {...}, + "error": null +} +``` + +### AI Webhook 回调 + +```http +POST /api/webhooks/ +Content-Type: application/json +Authorization: Bearer {agent_token} + +{ + "event": "article.created", + "url": "https://ai-agent.example.com/webhook", + "secret": "xxx" +} +``` + +## AI 操作日志 + +```python +# 数据库模型 +class AIOperationLog(models.Model): + agent_id = models.CharField(max_length=100) # AI 代理 ID + action = models.CharField(max_length=50) # 操作类型 + resource_type = models.CharField(max_length=50) # 资源类型 + resource_id = models.IntegerField() # 资源 ID + status = models.CharField(max_length=20) # success/failed + confidence = models.FloatField() # AI 置信度 + reasoning = models.TextField() # AI 推理过程 + created_at = models.DateTimeField(auto_now_add=True) +``` + +## AI 权限系统 + +| 权限 | 说明 | 适用 AI | +|------|------|--------| +| `ai:read` | 读取数据 | 所有 AI | +| `ai:write` | 创建/更新数据 | 内容生成 AI、服务推荐 AI | +| `ai:review` | 审核内容 | 内容审核 AI | +| `ai:delete` | 删除数据 | 管理员 AI | +| `ai:batch` | 批量操作 | 所有 AI | +| `ai:analytics` | 访问分析数据 | 数据分析 AI | + +## 最佳实践 + +### 1. AI 应该 + +- ✅ 使用专用的 AI 账号(Service Account) +- ✅ 记录所有操作的 `agent_id` +- ✅ 提供操作的置信度 +- ✅ 提供操作的推理过程 +- ✅ 支持人工复核 +- ✅ 遵守速率限制 + +### 2. AI 不应该 + +- ❌ 使用人类用户的账号 +- ❌ 隐藏 AI 身份 +- ❌ 绕过审核流程 +- ❌ 无限制批量操作 +- ❌ 忽略错误处理 + +## 示例:AI 自动运营流程 + +```python +# 1. AI 内容生成器创建文章 +POST /api/articles/ +{ + "agent_id": "content-generator-ai", + "title": "成都美食攻略", + "region": 11, + "content": "...", + "auto_generated": true +} + +# 2. AI 审核器自动审核 +POST /api/articles/{id}/review/ +{ + "agent_id": "content-moderator-ai", + "action": "approve", + "confidence": 0.98, + "reasoning": "内容质量高,无明显问题" +} + +# 3. AI 推荐器添加到首页 +POST /api/featured/ +{ + "agent_id": "recommendation-ai", + "article_id": 123, + "reason": "热门文章,用户关注度高" +} + +# 4. AI 分析器生成报告 +GET /api/analytics/daily?agent=analytics-ai +``` + +## 配置示例 + +```python +# settings.py +AI_AGENTS = { + 'content-moderator-ai': { + 'name': '内容审核 AI', + 'secret': 'xxx', + 'permissions': ['review', 'approve', 'reject'], + 'rate_limit': 1000, # 每小时请求数 + }, + 'content-generator-ai': { + 'name': '内容生成 AI', + 'secret': 'xxx', + 'permissions': ['write'], + 'rate_limit': 100, + }, + # ... +} +``` + +## 未来扩展 + +- [ ] 自然语言查询接口 +- [ ] AI 之间的协作协议 +- [ ] 多 AI 投票决策机制 +- [ ] AI 操作可视化面板 +- [ ] AI 性能评估系统 +- [ ] AI 训练数据导出 + +--- + +**设计理念:** 城市手册不仅是给人用的,更是给 AI 用的。每一个 API、每一个功能、每一个流程,都要考虑 AI 如何自动化操作。 diff --git a/city-manual/backend/agents/__init__.py b/city-manual/backend/agents/__init__.py new file mode 100644 index 0000000..dc08e7a --- /dev/null +++ b/city-manual/backend/agents/__init__.py @@ -0,0 +1 @@ +default_app_config = 'agents.apps.AgentsConfig' diff --git a/city-manual/backend/agents/admin.py b/city-manual/backend/agents/admin.py new file mode 100644 index 0000000..cd7643a --- /dev/null +++ b/city-manual/backend/agents/admin.py @@ -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'] + }), + ) diff --git a/city-manual/backend/agents/apps.py b/city-manual/backend/agents/apps.py new file mode 100644 index 0000000..27fd97c --- /dev/null +++ b/city-manual/backend/agents/apps.py @@ -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' diff --git a/city-manual/backend/agents/migrations/0001_initial.py b/city-manual/backend/agents/migrations/0001_initial.py new file mode 100644 index 0000000..9422213 --- /dev/null +++ b/city-manual/backend/agents/migrations/0001_initial.py @@ -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')], + }, + ), + ] diff --git a/city-manual/backend/agents/migrations/__init__.py b/city-manual/backend/agents/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/city-manual/backend/agents/models.py b/city-manual/backend/agents/models.py new file mode 100644 index 0000000..98103c4 --- /dev/null +++ b/city-manual/backend/agents/models.py @@ -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 diff --git a/city-manual/backend/agents/serializers.py b/city-manual/backend/agents/serializers.py new file mode 100644 index 0000000..03bcfc5 --- /dev/null +++ b/city-manual/backend/agents/serializers.py @@ -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 diff --git a/city-manual/backend/agents/views.py b/city-manual/backend/agents/views.py new file mode 100644 index 0000000..2796f15 --- /dev/null +++ b/city-manual/backend/agents/views.py @@ -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) diff --git a/city-manual/backend/city_manual/__pycache__/settings.cpython-312.pyc b/city-manual/backend/city_manual/__pycache__/settings.cpython-312.pyc index efd19e8..ca4721f 100644 Binary files a/city-manual/backend/city_manual/__pycache__/settings.cpython-312.pyc and b/city-manual/backend/city_manual/__pycache__/settings.cpython-312.pyc differ diff --git a/city-manual/backend/city_manual/__pycache__/urls.cpython-312.pyc b/city-manual/backend/city_manual/__pycache__/urls.cpython-312.pyc index d50a9b4..fdf920f 100644 Binary files a/city-manual/backend/city_manual/__pycache__/urls.cpython-312.pyc and b/city-manual/backend/city_manual/__pycache__/urls.cpython-312.pyc differ diff --git a/city-manual/backend/city_manual/settings.py b/city-manual/backend/city_manual/settings.py index fc7e98a..5a24dc7 100644 --- a/city-manual/backend/city_manual/settings.py +++ b/city-manual/backend/city_manual/settings.py @@ -45,6 +45,7 @@ INSTALLED_APPS = [ 'regions', 'content', 'services', + 'agents', # AI 代理系统 ] MIDDLEWARE = [ diff --git a/city-manual/backend/city_manual/urls.py b/city-manual/backend/city_manual/urls.py index b821bda..b18e3d7 100644 --- a/city-manual/backend/city_manual/urls.py +++ b/city-manual/backend/city_manual/urls.py @@ -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)), ] diff --git a/city-manual/backend/content/migrations/__pycache__/0001_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 466f5f2..0000000 Binary files a/city-manual/backend/content/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/content/migrations/__pycache__/0002_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0002_initial.cpython-312.pyc deleted file mode 100644 index dc8f0c3..0000000 Binary files a/city-manual/backend/content/migrations/__pycache__/0002_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/content/migrations/__pycache__/0003_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0003_initial.cpython-312.pyc deleted file mode 100644 index a35dc10..0000000 Binary files a/city-manual/backend/content/migrations/__pycache__/0003_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/content/migrations/__pycache__/0004_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0004_initial.cpython-312.pyc deleted file mode 100644 index 00d7146..0000000 Binary files a/city-manual/backend/content/migrations/__pycache__/0004_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/content/migrations/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 77c7a4b..0000000 Binary files a/city-manual/backend/content/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/init_agents.py b/city-manual/backend/init_agents.py new file mode 100644 index 0000000..cb31b06 --- /dev/null +++ b/city-manual/backend/init_agents.py @@ -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) diff --git a/city-manual/backend/regions/management/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/regions/management/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 9a27e17..0000000 Binary files a/city-manual/backend/regions/management/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/regions/management/commands/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/regions/management/commands/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 5f3a777..0000000 Binary files a/city-manual/backend/regions/management/commands/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/regions/management/commands/__pycache__/seed_data.cpython-312.pyc b/city-manual/backend/regions/management/commands/__pycache__/seed_data.cpython-312.pyc deleted file mode 100644 index 4e0f000..0000000 Binary files a/city-manual/backend/regions/management/commands/__pycache__/seed_data.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/regions/migrations/__pycache__/0001_initial.cpython-312.pyc b/city-manual/backend/regions/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 6bb25e8..0000000 Binary files a/city-manual/backend/regions/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/regions/migrations/__pycache__/0002_initial.cpython-312.pyc b/city-manual/backend/regions/migrations/__pycache__/0002_initial.cpython-312.pyc deleted file mode 100644 index d16592d..0000000 Binary files a/city-manual/backend/regions/migrations/__pycache__/0002_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/regions/migrations/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/regions/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d2c5ba1..0000000 Binary files a/city-manual/backend/regions/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/services/migrations/__pycache__/0001_initial.cpython-312.pyc b/city-manual/backend/services/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index a0c1e8d..0000000 Binary files a/city-manual/backend/services/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/services/migrations/__pycache__/0002_initial.cpython-312.pyc b/city-manual/backend/services/migrations/__pycache__/0002_initial.cpython-312.pyc deleted file mode 100644 index af6acf3..0000000 Binary files a/city-manual/backend/services/migrations/__pycache__/0002_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/services/migrations/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/services/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0cac2bd..0000000 Binary files a/city-manual/backend/services/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/users/migrations/__pycache__/0001_initial.cpython-312.pyc b/city-manual/backend/users/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 9270d80..0000000 Binary files a/city-manual/backend/users/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/city-manual/backend/users/migrations/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/users/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3e50c9d..0000000 Binary files a/city-manual/backend/users/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ