diff --git a/backend/core/admin/__init__.py b/backend/core/admin/__init__.py deleted file mode 100644 index 2b33e7e..0000000 --- a/backend/core/admin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .github_admin import * diff --git a/backend/core/admin/github_admin.py b/backend/core/admin/github_admin.py deleted file mode 100644 index d606a58..0000000 --- a/backend/core/admin/github_admin.py +++ /dev/null @@ -1,333 +0,0 @@ -from django.contrib import admin -from django.utils.html import format_html -from django.urls import reverse -from django.utils.safestring import mark_safe - -from core.models.github_data import ( - GitHubRepository, GitHubIssue, GitHubIssueComment, GitHubPullRequest, - GitHubPRComment, GitHubPRFile, GitHubDiscussion, GitHubDiscussionComment, - GitHubWikiPage, GitHubRepositoryFile -) - - -@admin.register(GitHubRepository) -class GitHubRepositoryAdmin(admin.ModelAdmin): - """Admin interface for GitHub repositories""" - list_display = [ - 'full_name', 'app_integration', 'ingestion_status', - 'last_ingested_at', 'is_private', 'created_at' - ] - list_filter = [ - 'ingestion_status', 'is_private', 'created_at', 'app_integration' - ] - search_fields = ['full_name', 'name', 'repo_owner', 'description'] - readonly_fields = ['uuid', 'created_at', 'updated_at'] - ordering = ['-created_at'] - - fieldsets = ( - ('Repository Information', { - 'fields': ('full_name', 'name', 'repo_owner', 'description', 'url') - }), - ('Configuration', { - 'fields': ('is_private', 'default_branch', 'app_integration') - }), - ('Ingestion Status', { - 'fields': ('ingestion_status', 'last_ingested_at') - }), - ('Metadata', { - 'fields': ('uuid', 'created_at', 'updated_at'), - 'classes': ('collapse',) - }) - ) - - def get_queryset(self, request): - return super().get_queryset(request).select_related('app_integration__application') - - -class GitHubIssueCommentInline(admin.TabularInline): - """Inline admin for issue comments""" - model = GitHubIssueComment - extra = 0 - readonly_fields = ['github_id', 'author', 'created_at'] - fields = ['github_id', 'author', 'body_preview', 'created_at'] - - def body_preview(self, obj): - if obj.body: - return obj.body[:100] + '...' if len(obj.body) > 100 else obj.body - return '' - body_preview.short_description = 'Body Preview' - - -@admin.register(GitHubIssue) -class GitHubIssueAdmin(admin.ModelAdmin): - """Admin interface for GitHub issues""" - list_display = [ - 'issue_number', 'title', 'repository', 'state', 'author', - 'created_at', 'comment_count' - ] - list_filter = ['state', 'created_at', 'repository'] - search_fields = ['title', 'body', 'author'] - readonly_fields = ['uuid', 'github_id', 'created_at', 'updated_at'] - ordering = ['-created_at'] - inlines = [GitHubIssueCommentInline] - - fieldsets = ( - ('Issue Information', { - 'fields': ('github_id', 'number', 'title', 'state', 'repository') - }), - ('Content', { - 'fields': ('body', 'author', 'author_association') - }), - ('Metadata', { - 'fields': ( - 'assignees', 'labels', 'milestone', 'locked', - 'created_at', 'updated_at', 'closed_at' - ), - 'classes': ('collapse',) - }), - ('External Links', { - 'fields': ('url',) - }) - ) - - def issue_number(self, obj): - return f"#{obj.number}" - issue_number.short_description = 'Issue #' - - def comment_count(self, obj): - return obj.comments.count() - comment_count.short_description = 'Comments' - - def get_queryset(self, request): - return super().get_queryset(request).select_related('repository') - - -class GitHubPRCommentInline(admin.TabularInline): - """Inline admin for PR comments""" - model = GitHubPRComment - extra = 0 - readonly_fields = ['github_id', 'author', 'created_at'] - fields = ['github_id', 'author', 'body_preview', 'created_at'] - - def body_preview(self, obj): - if obj.body: - return obj.body[:100] + '...' if len(obj.body) > 100 else obj.body - return '' - body_preview.short_description = 'Body Preview' - - -class GitHubPRFileInline(admin.TabularInline): - """Inline admin for PR files""" - model = GitHubPRFile - extra = 0 - readonly_fields = ['filename', 'status', 'additions', 'deletions'] - fields = ['filename', 'status', 'additions', 'deletions', 'changes'] - ordering = ['filename'] - - -@admin.register(GitHubPullRequest) -class GitHubPullRequestAdmin(admin.ModelAdmin): - """Admin interface for GitHub pull requests""" - list_display = [ - 'pr_number', 'title', 'repository', 'state', 'author', - 'merged', 'created_at', 'comment_count', 'file_count' - ] - list_filter = ['state', 'merged', 'created_at', 'repository'] - search_fields = ['title', 'body', 'author'] - readonly_fields = ['uuid', 'github_id', 'created_at', 'updated_at'] - ordering = ['-created_at'] - inlines = [GitHubPRCommentInline, GitHubPRFileInline] - - fieldsets = ( - ('PR Information', { - 'fields': ('github_id', 'number', 'title', 'state', 'repository') - }), - ('Content', { - 'fields': ('body', 'author', 'author_association') - }), - ('Branch Information', { - 'fields': ('head_branch', 'base_branch', 'merged', 'merged_at') - }), - ('Statistics', { - 'fields': ('additions', 'deletions', 'changed_files') - }), - ('Metadata', { - 'fields': ( - 'assignees', 'reviewers', 'labels', 'milestone', - 'created_at', 'updated_at', 'closed_at' - ), - 'classes': ('collapse',) - }), - ('External Links', { - 'fields': ('url',) - }) - ) - - def pr_number(self, obj): - return f"#{obj.number}" - pr_number.short_description = 'PR #' - - def comment_count(self, obj): - return obj.comments.count() - comment_count.short_description = 'Comments' - - def file_count(self, obj): - return obj.files.count() - file_count.short_description = 'Files' - - def get_queryset(self, request): - return super().get_queryset(request).select_related('repository') - - -@admin.register(GitHubDiscussion) -class GitHubDiscussionAdmin(admin.ModelAdmin): - """Admin interface for GitHub discussions""" - list_display = [ - 'discussion_number', 'title', 'repository', 'category_name', - 'author', 'upvote_count', 'created_at', 'comment_count' - ] - list_filter = ['created_at', 'repository'] - search_fields = ['title', 'body', 'author'] - readonly_fields = ['uuid', 'github_id', 'created_at', 'updated_at'] - ordering = ['-created_at'] - - fieldsets = ( - ('Discussion Information', { - 'fields': ('github_id', 'number', 'title', 'repository') - }), - ('Content', { - 'fields': ('body', 'author', 'author_association') - }), - ('Category', { - 'fields': ('category',) - }), - ('Interaction', { - 'fields': ('upvote_count', 'answer_chosen_at', 'answer_chosen_by') - }), - ('Metadata', { - 'fields': ('created_at', 'updated_at', 'last_edited_at'), - 'classes': ('collapse',) - }), - ('External Links', { - 'fields': ('url',) - }) - ) - - def discussion_number(self, obj): - return f"#{obj.number}" - discussion_number.short_description = 'Discussion #' - - def category_name(self, obj): - if obj.category and isinstance(obj.category, dict): - return obj.category.get('name', 'N/A') - return 'N/A' - category_name.short_description = 'Category' - - def comment_count(self, obj): - return obj.comments.count() - comment_count.short_description = 'Comments' - - def get_queryset(self, request): - return super().get_queryset(request).select_related('repository') - - -@admin.register(GitHubWikiPage) -class GitHubWikiPageAdmin(admin.ModelAdmin): - """Admin interface for GitHub wiki pages""" - list_display = ['title', 'repository', 'last_modified', 'created_at'] - list_filter = ['last_modified', 'created_at', 'repository'] - search_fields = ['title', 'content'] - readonly_fields = ['uuid', 'sha', 'created_at', 'updated_at'] - ordering = ['title'] - - fieldsets = ( - ('Wiki Page Information', { - 'fields': ('title', 'repository') - }), - ('Content', { - 'fields': ('content',) - }), - ('Metadata', { - 'fields': ('sha', 'html_url', 'download_url', 'last_modified'), - 'classes': ('collapse',) - }) - ) - - def get_queryset(self, request): - return super().get_queryset(request).select_related('repository') - - -@admin.register(GitHubRepositoryFile) -class GitHubRepositoryFileAdmin(admin.ModelAdmin): - """Admin interface for GitHub repository files""" - list_display = ['name', 'path', 'repository', 'size', 'content_type', 'last_modified'] - list_filter = ['content_type', 'last_modified', 'created_at', 'repository'] - search_fields = ['name', 'path', 'content'] - readonly_fields = ['uuid', 'sha', 'size', 'created_at', 'updated_at'] - ordering = ['path'] - - fieldsets = ( - ('File Information', { - 'fields': ('name', 'path', 'repository') - }), - ('Content', { - 'fields': ('content', 'content_type', 'encoding') - }), - ('Metadata', { - 'fields': ('sha', 'size', 'html_url', 'download_url', 'last_modified'), - 'classes': ('collapse',) - }) - ) - - def get_queryset(self, request): - return super().get_queryset(request).select_related('repository') -@admin.register(GitHubIssueComment) -class GitHubIssueCommentAdmin(admin.ModelAdmin): - """Admin interface for GitHub issue comments""" - list_display = ['github_id', 'issue', 'author', 'created_at'] - list_filter = ['created_at', 'author_association'] - search_fields = ['body', 'author'] - readonly_fields = ['uuid', 'github_id', 'created_at', 'updated_at'] - ordering = ['-created_at'] - - def get_queryset(self, request): - return super().get_queryset(request).select_related('issue__repository') - - -@admin.register(GitHubPRComment) -class GitHubPRCommentAdmin(admin.ModelAdmin): - """Admin interface for GitHub PR comments""" - list_display = ['github_id', 'pull_request', 'author', 'created_at'] - list_filter = ['created_at', 'author_association'] - search_fields = ['body', 'author'] - readonly_fields = ['uuid', 'github_id', 'created_at', 'updated_at'] - ordering = ['-created_at'] - - def get_queryset(self, request): - return super().get_queryset(request).select_related('pull_request__repository') - - -@admin.register(GitHubPRFile) -class GitHubPRFileAdmin(admin.ModelAdmin): - """Admin interface for GitHub PR files""" - list_display = ['filename', 'pull_request', 'status', 'additions', 'deletions'] - list_filter = ['status', 'pull_request__repository'] - search_fields = ['filename', 'patch'] - readonly_fields = ['uuid', 'created_at', 'updated_at'] - ordering = ['filename'] - - def get_queryset(self, request): - return super().get_queryset(request).select_related('pull_request__repository') - - -@admin.register(GitHubDiscussionComment) -class GitHubDiscussionCommentAdmin(admin.ModelAdmin): - """Admin interface for GitHub discussion comments""" - list_display = ['github_id', 'discussion', 'author', 'created_at', 'upvote_count'] - list_filter = ['created_at', 'author_association'] - search_fields = ['body', 'author'] - readonly_fields = ['uuid', 'github_id', 'created_at', 'updated_at'] - ordering = ['-created_at'] - - def get_queryset(self, request): - return super().get_queryset(request).select_related('discussion__repository') diff --git a/backend/core/services/ai_client_service.py b/backend/core/services/ai_client_service.py index 70d1455..063cae6 100644 --- a/backend/core/services/ai_client_service.py +++ b/backend/core/services/ai_client_service.py @@ -19,47 +19,32 @@ def get_client_and_model( context: str = 'response', capability: str = 'text' ) -> Tuple[Optional[Any], Optional[str]]: - """ - Get AI client and model for the given application. - - Args: - app: Application instance - ai_provider_id: Specific AI provider ID (optional) - model: Specific model name (optional) - context: Usage context - capability: Required capability - - Returns: - Tuple of (client_instance, model_name) - """ provider_config = self._resolve_provider_config(app, ai_provider_id, context, capability) - + if not provider_config: return None, None - + client = self._create_client(provider_config) selected_model = model or provider_config.get('model') - + if not selected_model: selected_model = self._get_default_model(client) - + return client, selected_model def _resolve_provider_config( - self, - app: Application, - ai_provider_id: Optional[int], - context: str, + self, + app: Application, + ai_provider_id: Optional[int], + context: str, capability: str ) -> Optional[Dict[str, Any]]: - """Resolve provider configuration based on input parameters""" if ai_provider_id: return self._get_provider_by_id(ai_provider_id) else: return self._get_app_provider_config(app, context, capability) def _get_provider_by_id(self, ai_provider_id: int) -> Optional[Dict[str, Any]]: - """Get provider configuration by ID""" try: ai_provider = AIProvider.objects.get(id=ai_provider_id) return { @@ -72,17 +57,16 @@ def _get_provider_by_id(self, ai_provider_id: int) -> Optional[Dict[str, Any]]: return None def _get_app_provider_config( - self, - app: Application, - context: str, + self, + app: Application, + context: str, capability: str ) -> Optional[Dict[str, Any]]: - """Get provider configuration from application settings""" try: config = self._get_app_provider(app, context, capability) if not config: return None - + return { 'provider': config.ai_provider, 'type': config.ai_provider.provider, @@ -100,7 +84,6 @@ def _get_app_provider(self, app: Application, context: str, capability: str) -> ).first() def _create_client(self, provider_config: Dict[str, Any]) -> Optional[Any]: - """Create AI client instance""" try: return self.provider_factory.create_provider( provider_type=provider_config['type'], @@ -111,7 +94,6 @@ def _create_client(self, provider_config: Dict[str, Any]) -> Optional[Any]: return None def _get_default_model(self, client: Any) -> Optional[str]: - """Get default model from client""" try: supported_models = client.get_models() return supported_models[0] if supported_models else None @@ -119,26 +101,16 @@ def _get_default_model(self, client: Any) -> Optional[str]: return None def validate_ai_provider( - self, - validated_data: Dict[str, Any], + self, + validated_data: Dict[str, Any], instance: AIProvider = None ) -> Tuple[bool, Any]: - """ - Validate AI provider configuration - - Args: - validated_data: Validated data from serializer - instance: Existing instance (for updates) - - Returns: - Tuple of (is_valid, result) - """ main_data, config_data = self.validator.validate_ai_provider_data(validated_data, instance) - + is_valid, provider_models = self.validator.validate_provider_config( provider_type=main_data['provider'], api_key=main_data['provider_api_key'], config=config_data ) - + return is_valid, provider_models if is_valid else None diff --git a/backend/core/services/ai_provider_validator.py b/backend/core/services/ai_provider_validator.py index b15109c..5eef315 100644 --- a/backend/core/services/ai_provider_validator.py +++ b/backend/core/services/ai_provider_validator.py @@ -5,28 +5,16 @@ class AIProviderValidator: - """Validates AI provider configurations""" - + def __init__(self): self.provider_factory = AIProviderFactory() - + def validate_provider_config( - self, - provider_type: str, - api_key: str, + self, + provider_type: str, + api_key: str, config: Dict[str, Any] ) -> Tuple[bool, Any]: - """ - Validate AI provider configuration - - Args: - provider_type: Type of AI provider - api_key: API key for the provider - config: Additional configuration - - Returns: - Tuple of (is_valid, provider_models) - """ try: return self.provider_factory.validate_provider( provider_type=provider_type, @@ -35,25 +23,15 @@ def validate_provider_config( ) except Exception as e: return False, str(e) - + def validate_ai_provider_data( - self, - validated_data: Dict[str, Any], + self, + validated_data: Dict[str, Any], instance: AIProvider = None ) -> Tuple[Dict[str, Any], Dict[str, Any]]: - """ - Prepare and validate AI provider data - - Args: - validated_data: Validated serializer data - instance: Existing AI provider instance (for updates) - - Returns: - Tuple of (main_fields, config_data) - """ main_fields = ['name', 'provider', 'provider_api_key'] config = {} - + if instance: current_data = { 'name': instance.name, @@ -68,12 +46,12 @@ def validate_ai_provider_data( validation_data = update_data else: validation_data = validated_data - + main_data = {} for field, value in validation_data.items(): if field in main_fields: main_data[field] = value else: config[field] = str(value).strip() if value is not None else '' - + return main_data, config diff --git a/backend/core/views/ai_provider.py b/backend/core/views/ai_provider.py index c48cf98..b51ea42 100644 --- a/backend/core/views/ai_provider.py +++ b/backend/core/views/ai_provider.py @@ -11,7 +11,7 @@ class AIProviderViewSet(viewsets.ModelViewSet): - + permission_classes = [permissions.IsAuthenticated] lookup_field = 'uuid' http_method_names = ['get', 'post', 'put', 'patch', 'delete'] @@ -38,27 +38,27 @@ def create(self, request, *args, **kwargs): """Create a new AI provider with validation""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - + try: result = self._create_ai_provider(serializer.validated_data, request.user) return Response(result, status=status.HTTP_201_CREATED) - + except Exception as e: return self._handle_validation_error(e) def _create_ai_provider(self, validated_data, user): """Create AI provider with validation""" is_valid, provider_models = self.ai_service.validate_ai_provider(validated_data) - + if not is_valid: raise ValueError('Failed to validate AI provider connection') - + ai_provider = self.get_serializer().create(validated_data) ai_provider.creator = user ai_provider.save() - + self._store_provider_models(ai_provider, provider_models, user) - + return self._format_creation_response(ai_provider, provider_models) def _store_provider_models(self, ai_provider, provider_models, user): @@ -104,31 +104,31 @@ def update(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) - + try: result = self._update_ai_provider(serializer, instance, request.user) return Response(result) - + except Exception as e: return self._handle_validation_error(e) def _update_ai_provider(self, serializer, instance, user): validated_data = serializer.validated_data - + api_key_to_validate = self._get_api_key_to_validate(validated_data, instance) - + if api_key_to_validate and api_key_to_validate.strip(): is_valid, provider_models = self.ai_service.validate_ai_provider( validated_data, instance ) - + if not is_valid: raise ValueError('Failed to validate AI provider connection') - + self._store_provider_models(instance, provider_models, user) - + updated_instance = serializer.save() - + return self._format_update_response(updated_instance) def _get_api_key_to_validate(self, validated_data, instance): @@ -153,31 +153,31 @@ def _handle_validation_error(self, error): status=status.HTTP_400_BAD_REQUEST ) - @action(detail=True, methods=['post']) - def test_connection(self, request, uuid=None): - """Test AI provider connection""" - instance = self.get_object() - - try: - test_data = { - 'provider': instance.provider, - 'provider_api_key': instance.provider_api_key, - **(instance.metadata or {}) - } - - is_valid, provider_models = self.ai_service.validate_ai_provider(test_data) - - return Response({ - 'is_valid': is_valid, - 'models': provider_models if is_valid else None, - 'message': 'Connection successful' if is_valid else 'Connection failed' - }) - - except Exception as e: - return Response( - { - 'is_valid': False, - 'error': str(e) - }, - status=status.HTTP_400_BAD_REQUEST - ) + @action(detail=False, methods=['get']) + def all_models(self, request): + user = request.user + + ai_providers = self.get_queryset().filter(creator=user) + + result = [] + for ai_provider in ai_providers: + try: + provider_models = AIProviderModels.objects.get(ai_provider=ai_provider) + result.append({ + 'ai_provider': AIProviderSerializer(ai_provider).data, + 'ai_provider_models': { + 'id': provider_models.id, + 'models_data': provider_models.models_data, + 'created_at': provider_models.created_at, + 'updated_at': provider_models.updated_at + } + }) + except AIProviderModels.DoesNotExist: + continue + except Exception as e: + print(f"Error retrieving models for provider {ai_provider.uuid}: {str(e)}") + continue + + return Response({ + 'providers': result + }) diff --git a/backend/core/views/application.py b/backend/core/views/application.py index f7c4539..fd46d05 100644 --- a/backend/core/views/application.py +++ b/backend/core/views/application.py @@ -35,6 +35,9 @@ def create(self, request, *args, **kwargs): create_serializer.is_valid(raise_exception=True) app_instance = create_serializer.save(owner=request.user) + # TODO: may be we need to handle proper log and error messages if default + # TODO: models are not configured yet. + AppModel.configure_defaults(app_instance) parsed_kb_items = parse_kb_from_request(request) @@ -108,7 +111,7 @@ def get(self, request, application_uuid): if not sender_identifier: return Response({'detail': 'sender_identifier is required.'}, status=status.HTTP_400_BAD_REQUEST) - chat_type = request.query_params.get('type') + chat_type = request.query_params.get('type') chatroom_ids = ChatroomParticipant.objects.filter( user_identifier=sender_identifier,