django-cfg 1.1.81__py3-none-any.whl → 1.2.0__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.
Files changed (246) hide show
  1. django_cfg/__init__.py +20 -448
  2. django_cfg/apps/accounts/README.md +3 -3
  3. django_cfg/apps/accounts/admin/__init__.py +0 -2
  4. django_cfg/apps/accounts/admin/activity.py +2 -9
  5. django_cfg/apps/accounts/admin/filters.py +0 -42
  6. django_cfg/apps/accounts/admin/inlines.py +8 -8
  7. django_cfg/apps/accounts/admin/otp.py +5 -5
  8. django_cfg/apps/accounts/admin/registration_source.py +1 -8
  9. django_cfg/apps/accounts/admin/user.py +12 -20
  10. django_cfg/apps/accounts/managers/user_manager.py +2 -129
  11. django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
  12. django_cfg/apps/accounts/models.py +3 -123
  13. django_cfg/apps/accounts/serializers/otp.py +40 -44
  14. django_cfg/apps/accounts/serializers/profile.py +0 -2
  15. django_cfg/apps/accounts/services/otp_service.py +98 -186
  16. django_cfg/apps/accounts/signals.py +25 -15
  17. django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
  18. django_cfg/apps/accounts/views/otp.py +35 -36
  19. django_cfg/apps/agents/README.md +129 -0
  20. django_cfg/apps/agents/__init__.py +68 -0
  21. django_cfg/apps/agents/admin/__init__.py +17 -0
  22. django_cfg/apps/agents/admin/execution_admin.py +460 -0
  23. django_cfg/apps/agents/admin/registry_admin.py +360 -0
  24. django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
  25. django_cfg/apps/agents/apps.py +29 -0
  26. django_cfg/apps/agents/core/__init__.py +20 -0
  27. django_cfg/apps/agents/core/agent.py +281 -0
  28. django_cfg/apps/agents/core/dependencies.py +154 -0
  29. django_cfg/apps/agents/core/exceptions.py +66 -0
  30. django_cfg/apps/agents/core/models.py +106 -0
  31. django_cfg/apps/agents/core/orchestrator.py +391 -0
  32. django_cfg/apps/agents/examples/__init__.py +3 -0
  33. django_cfg/apps/agents/examples/simple_example.py +161 -0
  34. django_cfg/apps/agents/integration/__init__.py +14 -0
  35. django_cfg/apps/agents/integration/middleware.py +80 -0
  36. django_cfg/apps/agents/integration/registry.py +345 -0
  37. django_cfg/apps/agents/integration/signals.py +50 -0
  38. django_cfg/apps/agents/management/__init__.py +3 -0
  39. django_cfg/apps/agents/management/commands/__init__.py +3 -0
  40. django_cfg/apps/agents/management/commands/create_agent.py +365 -0
  41. django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
  42. django_cfg/apps/agents/managers/__init__.py +23 -0
  43. django_cfg/apps/agents/managers/execution.py +236 -0
  44. django_cfg/apps/agents/managers/registry.py +254 -0
  45. django_cfg/apps/agents/managers/toolsets.py +496 -0
  46. django_cfg/apps/agents/migrations/0001_initial.py +286 -0
  47. django_cfg/apps/agents/migrations/__init__.py +5 -0
  48. django_cfg/apps/agents/models/__init__.py +15 -0
  49. django_cfg/apps/agents/models/execution.py +215 -0
  50. django_cfg/apps/agents/models/registry.py +220 -0
  51. django_cfg/apps/agents/models/toolsets.py +305 -0
  52. django_cfg/apps/agents/patterns/__init__.py +24 -0
  53. django_cfg/apps/agents/patterns/content_agents.py +234 -0
  54. django_cfg/apps/agents/toolsets/__init__.py +15 -0
  55. django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
  56. django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
  57. django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
  58. django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
  59. django_cfg/apps/agents/urls.py +46 -0
  60. django_cfg/apps/knowbase/README.md +150 -0
  61. django_cfg/apps/knowbase/__init__.py +27 -0
  62. django_cfg/apps/knowbase/admin/__init__.py +23 -0
  63. django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
  64. django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
  65. django_cfg/apps/knowbase/admin/document_admin.py +650 -0
  66. django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
  67. django_cfg/apps/knowbase/apps.py +81 -0
  68. django_cfg/apps/knowbase/config/README.md +176 -0
  69. django_cfg/apps/knowbase/config/__init__.py +51 -0
  70. django_cfg/apps/knowbase/config/constance_fields.py +186 -0
  71. django_cfg/apps/knowbase/config/constance_settings.py +200 -0
  72. django_cfg/apps/knowbase/config/settings.py +444 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +79 -17
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/templates/guide.md +266 -0
  198. django_cfg/modules/django_twilio/twilio_service.py +2 -2
  199. django_cfg/modules/django_unfold/__init__.py +69 -0
  200. django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
  201. django_cfg/modules/django_unfold/dashboard.py +278 -0
  202. django_cfg/modules/django_unfold/icons/README.md +145 -0
  203. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  204. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  205. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  206. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  207. django_cfg/modules/django_unfold/models/config.py +601 -0
  208. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  209. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  210. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  211. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  212. django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
  213. django_cfg/modules/django_unfold/utils.py +140 -0
  214. django_cfg/registry/__init__.py +23 -0
  215. django_cfg/registry/core.py +61 -0
  216. django_cfg/registry/exceptions.py +11 -0
  217. django_cfg/registry/modules.py +12 -0
  218. django_cfg/registry/services.py +26 -0
  219. django_cfg/registry/third_party.py +52 -0
  220. django_cfg/routing/__init__.py +19 -0
  221. django_cfg/routing/callbacks.py +198 -0
  222. django_cfg/routing/routers.py +48 -0
  223. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  224. django_cfg/templatetags/__init__.py +0 -0
  225. django_cfg/templatetags/django_cfg.py +33 -0
  226. django_cfg/urls.py +33 -0
  227. django_cfg/utils/path_resolution.py +1 -1
  228. django_cfg/utils/smart_defaults.py +7 -61
  229. django_cfg/utils/toolkit.py +663 -0
  230. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
  231. django_cfg-1.2.0.dist-info/RECORD +441 -0
  232. django_cfg/apps/tasks/@docs/README.md +0 -195
  233. django_cfg/archive/django_sample.zip +0 -0
  234. django_cfg/models/unfold.py +0 -271
  235. django_cfg/modules/unfold/__init__.py +0 -29
  236. django_cfg/modules/unfold/dashboard.py +0 -318
  237. django_cfg/pyproject.toml +0 -370
  238. django_cfg/routers.py +0 -83
  239. django_cfg-1.1.81.dist-info/RECORD +0 -278
  240. /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
  241. /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
  242. /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
  243. /django_cfg/{version_check.py → utils/version_check.py} +0 -0
  244. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,220 @@
1
+ """
2
+ Core Django toolset with common Django operations.
3
+ """
4
+
5
+ import logging
6
+ from typing import Dict, Any, List, Optional
7
+ from pydantic_ai.toolsets import AbstractToolset
8
+ from pydantic_ai import RunContext
9
+
10
+ from ..core.dependencies import DjangoDeps
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class DjangoToolset(AbstractToolset[DjangoDeps]):
16
+ """
17
+ Core Django toolset providing common Django operations.
18
+
19
+ Includes tools for:
20
+ - User management
21
+ - Session handling
22
+ - Settings access
23
+ - Logging
24
+ """
25
+
26
+ @property
27
+ def id(self) -> str:
28
+ return "django_core"
29
+
30
+ async def get_user_info(self, ctx: RunContext[DjangoDeps]) -> Dict[str, Any]:
31
+ """Get current user information."""
32
+ user = ctx.deps.user
33
+
34
+ return {
35
+ 'id': user.id,
36
+ 'username': user.username,
37
+ 'email': user.email,
38
+ 'first_name': user.first_name,
39
+ 'last_name': user.last_name,
40
+ 'is_active': user.is_active,
41
+ 'is_staff': user.is_staff,
42
+ 'is_superuser': user.is_superuser,
43
+ 'date_joined': user.date_joined.isoformat() if user.date_joined else None,
44
+ 'last_login': user.last_login.isoformat() if user.last_login else None,
45
+ }
46
+
47
+ async def get_user_permissions(self, ctx: RunContext[DjangoDeps]) -> List[str]:
48
+ """Get user permissions."""
49
+ user = ctx.deps.user
50
+
51
+ # Get all permissions
52
+ permissions = []
53
+
54
+ # Direct user permissions
55
+ user_perms = await user.user_permissions.aall()
56
+ async for perm in user_perms:
57
+ permissions.append(f"{perm.content_type.app_label}.{perm.codename}")
58
+
59
+ # Group permissions
60
+ groups = await user.groups.aall()
61
+ async for group in groups:
62
+ group_perms = await group.permissions.aall()
63
+ async for perm in group_perms:
64
+ perm_str = f"{perm.content_type.app_label}.{perm.codename}"
65
+ if perm_str not in permissions:
66
+ permissions.append(perm_str)
67
+
68
+ return sorted(permissions)
69
+
70
+ async def check_permission(self, ctx: RunContext[DjangoDeps], permission: str) -> bool:
71
+ """Check if user has specific permission."""
72
+ user = ctx.deps.user
73
+
74
+ # Handle superuser
75
+ if user.is_superuser:
76
+ return True
77
+
78
+ # Check permission
79
+ return user.has_perm(permission)
80
+
81
+ async def get_session_data(self, ctx: RunContext[DjangoDeps], key: Optional[str] = None) -> Any:
82
+ """Get session data."""
83
+ session_data = ctx.deps.session_data
84
+
85
+ if key:
86
+ return session_data.get(key)
87
+
88
+ return session_data
89
+
90
+ async def log_message(
91
+ self,
92
+ ctx: RunContext[DjangoDeps],
93
+ message: str,
94
+ level: str = "info",
95
+ extra_data: Optional[Dict[str, Any]] = None
96
+ ) -> bool:
97
+ """Log message with user context."""
98
+ user = ctx.deps.user
99
+
100
+ # Prepare log data
101
+ log_data = {
102
+ 'user_id': user.id,
103
+ 'username': user.username,
104
+ 'message': message,
105
+ }
106
+
107
+ if extra_data:
108
+ log_data.update(extra_data)
109
+
110
+ # Log based on level
111
+ if level.lower() == 'debug':
112
+ logger.debug(message, extra=log_data)
113
+ elif level.lower() == 'info':
114
+ logger.info(message, extra=log_data)
115
+ elif level.lower() == 'warning':
116
+ logger.warning(message, extra=log_data)
117
+ elif level.lower() == 'error':
118
+ logger.error(message, extra=log_data)
119
+ elif level.lower() == 'critical':
120
+ logger.critical(message, extra=log_data)
121
+ else:
122
+ logger.info(message, extra=log_data)
123
+
124
+ return True
125
+
126
+ async def get_django_setting(self, ctx: RunContext[DjangoDeps], setting_name: str) -> Any:
127
+ """Get Django setting value (safe settings only)."""
128
+ from django.conf import settings
129
+
130
+ # Whitelist of safe settings to expose
131
+ safe_settings = {
132
+ 'DEBUG',
133
+ 'TIME_ZONE',
134
+ 'LANGUAGE_CODE',
135
+ 'USE_TZ',
136
+ 'USE_I18N',
137
+ 'MEDIA_URL',
138
+ 'STATIC_URL',
139
+ 'DEFAULT_AUTO_FIELD',
140
+ }
141
+
142
+ if setting_name not in safe_settings:
143
+ raise ValueError(f"Setting '{setting_name}' is not in the safe settings list")
144
+
145
+ return getattr(settings, setting_name, None)
146
+
147
+ async def get_app_config(self, ctx: RunContext[DjangoDeps], app_label: str) -> Dict[str, Any]:
148
+ """Get Django app configuration."""
149
+ from django.apps import apps
150
+
151
+ try:
152
+ app_config = apps.get_app_config(app_label)
153
+
154
+ return {
155
+ 'name': app_config.name,
156
+ 'label': app_config.label,
157
+ 'verbose_name': app_config.verbose_name,
158
+ 'path': str(app_config.path),
159
+ 'models_module': app_config.models_module.__name__ if app_config.models_module else None,
160
+ }
161
+ except Exception as e:
162
+ logger.error(f"Failed to get app config for '{app_label}': {e}")
163
+ return {}
164
+
165
+ async def format_datetime(
166
+ self,
167
+ ctx: RunContext[DjangoDeps],
168
+ datetime_str: str,
169
+ format_str: str = "%Y-%m-%d %H:%M:%S"
170
+ ) -> str:
171
+ """Format datetime string using Django's timezone handling."""
172
+ from django.utils import timezone
173
+ from django.utils.dateparse import parse_datetime
174
+
175
+ try:
176
+ # Parse datetime
177
+ dt = parse_datetime(datetime_str)
178
+ if not dt:
179
+ return datetime_str
180
+
181
+ # Convert to user's timezone if available
182
+ if hasattr(ctx.deps, 'request') and ctx.deps.request:
183
+ # Use request timezone if available
184
+ dt = timezone.localtime(dt)
185
+
186
+ return dt.strftime(format_str)
187
+ except Exception as e:
188
+ logger.error(f"Failed to format datetime '{datetime_str}': {e}")
189
+ return datetime_str
190
+
191
+ async def get_model_info(self, ctx: RunContext[DjangoDeps], app_label: str, model_name: str) -> Dict[str, Any]:
192
+ """Get Django model information."""
193
+ from django.apps import apps
194
+
195
+ try:
196
+ model = apps.get_model(app_label, model_name)
197
+
198
+ # Get field information
199
+ fields = []
200
+ for field in model._meta.fields:
201
+ fields.append({
202
+ 'name': field.name,
203
+ 'type': field.__class__.__name__,
204
+ 'null': field.null,
205
+ 'blank': field.blank,
206
+ 'help_text': field.help_text,
207
+ })
208
+
209
+ return {
210
+ 'app_label': model._meta.app_label,
211
+ 'model_name': model._meta.model_name,
212
+ 'verbose_name': str(model._meta.verbose_name),
213
+ 'verbose_name_plural': str(model._meta.verbose_name_plural),
214
+ 'db_table': model._meta.db_table,
215
+ 'fields': fields,
216
+ 'field_count': len(fields),
217
+ }
218
+ except Exception as e:
219
+ logger.error(f"Failed to get model info for '{app_label}.{model_name}': {e}")
220
+ return {}
@@ -0,0 +1,324 @@
1
+ """
2
+ Django file toolset for file operations.
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Dict, Any, List, Optional, Union
9
+ from pydantic_ai.toolsets import AbstractToolset
10
+ from pydantic_ai import RunContext
11
+ from django.conf import settings
12
+ from django.core.files.storage import default_storage
13
+ from django.core.files.base import ContentFile
14
+
15
+ from ..core.dependencies import DjangoDeps
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class FileToolset(AbstractToolset[DjangoDeps]):
21
+ """
22
+ Django file toolset for safe file operations.
23
+
24
+ Provides tools for:
25
+ - File reading/writing (within allowed directories)
26
+ - Media file handling
27
+ - Static file operations
28
+ - File metadata
29
+ """
30
+
31
+ def __init__(self, allowed_paths: Optional[List[str]] = None, max_file_size: int = 10 * 1024 * 1024):
32
+ """
33
+ Initialize file toolset.
34
+
35
+ Args:
36
+ allowed_paths: List of allowed directory paths (relative to MEDIA_ROOT)
37
+ max_file_size: Maximum file size in bytes (default: 10MB)
38
+ """
39
+ self.allowed_paths = allowed_paths or ['orchestrator', 'temp']
40
+ self.max_file_size = max_file_size
41
+
42
+ @property
43
+ def id(self) -> str:
44
+ return "django_files"
45
+
46
+ def _check_path_access(self, file_path: str) -> bool:
47
+ """Check if file path is within allowed directories."""
48
+ path = Path(file_path)
49
+
50
+ # Normalize path and check if it's within allowed paths
51
+ try:
52
+ # Remove any parent directory traversal
53
+ normalized_path = path.resolve()
54
+
55
+ # Check against allowed paths
56
+ for allowed_path in self.allowed_paths:
57
+ allowed_full_path = Path(settings.MEDIA_ROOT) / allowed_path
58
+ try:
59
+ normalized_path.relative_to(allowed_full_path.resolve())
60
+ return True
61
+ except ValueError:
62
+ continue
63
+
64
+ return False
65
+ except Exception:
66
+ return False
67
+
68
+ def _get_safe_path(self, file_path: str, user_id: int) -> str:
69
+ """Get safe file path with user scoping."""
70
+ # Add user scoping to prevent access to other users' files
71
+ safe_path = f"orchestrator/user_{user_id}/{file_path}"
72
+
73
+ # Normalize and validate
74
+ normalized = os.path.normpath(safe_path)
75
+
76
+ # Ensure no directory traversal
77
+ if '..' in normalized or normalized.startswith('/'):
78
+ raise ValueError("Invalid file path")
79
+
80
+ return normalized
81
+
82
+ async def read_file(
83
+ self,
84
+ ctx: RunContext[DjangoDeps],
85
+ file_path: str,
86
+ encoding: str = 'utf-8'
87
+ ) -> Optional[str]:
88
+ """Read text file content."""
89
+ user_id = ctx.deps.user.id
90
+ safe_path = self._get_safe_path(file_path, user_id)
91
+
92
+ try:
93
+ if default_storage.exists(safe_path):
94
+ with default_storage.open(safe_path, 'r') as f:
95
+ content = f.read()
96
+
97
+ logger.debug(f"Read file: {safe_path} ({len(content)} chars)")
98
+ return content
99
+ else:
100
+ logger.warning(f"File not found: {safe_path}")
101
+ return None
102
+ except Exception as e:
103
+ logger.error(f"Failed to read file '{safe_path}': {e}")
104
+ return None
105
+
106
+ async def write_file(
107
+ self,
108
+ ctx: RunContext[DjangoDeps],
109
+ file_path: str,
110
+ content: str,
111
+ encoding: str = 'utf-8'
112
+ ) -> bool:
113
+ """Write text content to file."""
114
+ user_id = ctx.deps.user.id
115
+ safe_path = self._get_safe_path(file_path, user_id)
116
+
117
+ # Check file size
118
+ content_bytes = content.encode(encoding)
119
+ if len(content_bytes) > self.max_file_size:
120
+ logger.error(f"File too large: {len(content_bytes)} bytes > {self.max_file_size}")
121
+ return False
122
+
123
+ try:
124
+ # Create directory if needed
125
+ dir_path = os.path.dirname(safe_path)
126
+ if dir_path and not default_storage.exists(dir_path):
127
+ # Create directory structure
128
+ parts = dir_path.split('/')
129
+ current_path = ''
130
+ for part in parts:
131
+ current_path = os.path.join(current_path, part) if current_path else part
132
+ if not default_storage.exists(current_path):
133
+ default_storage.save(f"{current_path}/.keep", ContentFile(b''))
134
+
135
+ # Write file
136
+ default_storage.save(safe_path, ContentFile(content_bytes))
137
+
138
+ logger.debug(f"Wrote file: {safe_path} ({len(content_bytes)} bytes)")
139
+ return True
140
+ except Exception as e:
141
+ logger.error(f"Failed to write file '{safe_path}': {e}")
142
+ return False
143
+
144
+ async def delete_file(self, ctx: RunContext[DjangoDeps], file_path: str) -> bool:
145
+ """Delete file."""
146
+ user_id = ctx.deps.user.id
147
+ safe_path = self._get_safe_path(file_path, user_id)
148
+
149
+ try:
150
+ if default_storage.exists(safe_path):
151
+ default_storage.delete(safe_path)
152
+ logger.debug(f"Deleted file: {safe_path}")
153
+ return True
154
+ else:
155
+ logger.warning(f"File not found for deletion: {safe_path}")
156
+ return False
157
+ except Exception as e:
158
+ logger.error(f"Failed to delete file '{safe_path}': {e}")
159
+ return False
160
+
161
+ async def list_files(
162
+ self,
163
+ ctx: RunContext[DjangoDeps],
164
+ directory_path: str = "",
165
+ pattern: Optional[str] = None
166
+ ) -> List[Dict[str, Any]]:
167
+ """List files in directory."""
168
+ user_id = ctx.deps.user.id
169
+ safe_path = self._get_safe_path(directory_path, user_id)
170
+
171
+ try:
172
+ if not default_storage.exists(safe_path):
173
+ return []
174
+
175
+ # List directory contents
176
+ directories, files = default_storage.listdir(safe_path)
177
+
178
+ results = []
179
+
180
+ # Add directories
181
+ for directory in directories:
182
+ dir_path = os.path.join(safe_path, directory)
183
+ results.append({
184
+ 'name': directory,
185
+ 'type': 'directory',
186
+ 'path': dir_path,
187
+ 'size': None,
188
+ 'modified': None,
189
+ })
190
+
191
+ # Add files
192
+ for file in files:
193
+ file_path = os.path.join(safe_path, file)
194
+
195
+ # Apply pattern filter if specified
196
+ if pattern and pattern not in file:
197
+ continue
198
+
199
+ try:
200
+ size = default_storage.size(file_path)
201
+ modified = default_storage.get_modified_time(file_path)
202
+
203
+ results.append({
204
+ 'name': file,
205
+ 'type': 'file',
206
+ 'path': file_path,
207
+ 'size': size,
208
+ 'modified': modified.isoformat() if modified else None,
209
+ })
210
+ except Exception as e:
211
+ logger.warning(f"Could not get file info for '{file_path}': {e}")
212
+ results.append({
213
+ 'name': file,
214
+ 'type': 'file',
215
+ 'path': file_path,
216
+ 'size': None,
217
+ 'modified': None,
218
+ })
219
+
220
+ return results
221
+ except Exception as e:
222
+ logger.error(f"Failed to list files in '{safe_path}': {e}")
223
+ return []
224
+
225
+ async def get_file_info(
226
+ self,
227
+ ctx: RunContext[DjangoDeps],
228
+ file_path: str
229
+ ) -> Optional[Dict[str, Any]]:
230
+ """Get file metadata."""
231
+ user_id = ctx.deps.user.id
232
+ safe_path = self._get_safe_path(file_path, user_id)
233
+
234
+ try:
235
+ if not default_storage.exists(safe_path):
236
+ return None
237
+
238
+ size = default_storage.size(safe_path)
239
+ modified = default_storage.get_modified_time(safe_path)
240
+ url = default_storage.url(safe_path) if hasattr(default_storage, 'url') else None
241
+
242
+ return {
243
+ 'path': safe_path,
244
+ 'size': size,
245
+ 'modified': modified.isoformat() if modified else None,
246
+ 'url': url,
247
+ 'exists': True,
248
+ }
249
+ except Exception as e:
250
+ logger.error(f"Failed to get file info for '{safe_path}': {e}")
251
+ return None
252
+
253
+ async def copy_file(
254
+ self,
255
+ ctx: RunContext[DjangoDeps],
256
+ source_path: str,
257
+ destination_path: str
258
+ ) -> bool:
259
+ """Copy file to new location."""
260
+ user_id = ctx.deps.user.id
261
+ safe_source = self._get_safe_path(source_path, user_id)
262
+ safe_dest = self._get_safe_path(destination_path, user_id)
263
+
264
+ try:
265
+ if not default_storage.exists(safe_source):
266
+ logger.error(f"Source file not found: {safe_source}")
267
+ return False
268
+
269
+ # Read source file
270
+ with default_storage.open(safe_source, 'rb') as source_file:
271
+ content = source_file.read()
272
+
273
+ # Check size limit
274
+ if len(content) > self.max_file_size:
275
+ logger.error(f"File too large to copy: {len(content)} bytes")
276
+ return False
277
+
278
+ # Write to destination
279
+ default_storage.save(safe_dest, ContentFile(content))
280
+
281
+ logger.debug(f"Copied file: {safe_source} -> {safe_dest}")
282
+ return True
283
+ except Exception as e:
284
+ logger.error(f"Failed to copy file '{safe_source}' to '{safe_dest}': {e}")
285
+ return False
286
+
287
+ async def move_file(
288
+ self,
289
+ ctx: RunContext[DjangoDeps],
290
+ source_path: str,
291
+ destination_path: str
292
+ ) -> bool:
293
+ """Move file to new location."""
294
+ # Copy then delete
295
+ if await self.copy_file(ctx, source_path, destination_path):
296
+ return await self.delete_file(ctx, source_path)
297
+ return False
298
+
299
+ async def get_storage_info(self, ctx: RunContext[DjangoDeps]) -> Dict[str, Any]:
300
+ """Get storage backend information."""
301
+ user_id = ctx.deps.user.id
302
+ user_dir = self._get_safe_path("", user_id)
303
+
304
+ info = {
305
+ 'storage_backend': default_storage.__class__.__name__,
306
+ 'user_directory': user_dir,
307
+ 'max_file_size': self.max_file_size,
308
+ 'allowed_paths': self.allowed_paths,
309
+ }
310
+
311
+ try:
312
+ # Try to get user directory size
313
+ files = await self.list_files(ctx, "")
314
+ total_size = sum(f.get('size', 0) or 0 for f in files if f['type'] == 'file')
315
+ file_count = sum(1 for f in files if f['type'] == 'file')
316
+
317
+ info.update({
318
+ 'user_files_count': file_count,
319
+ 'user_total_size': total_size,
320
+ })
321
+ except Exception as e:
322
+ logger.warning(f"Could not get storage stats: {e}")
323
+
324
+ return info