Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions backend/core/admin/github_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,6 @@ class GitHubRepositoryFileAdmin(admin.ModelAdmin):

def get_queryset(self, request):
return super().get_queryset(request).select_related('repository')


# Register inline admins for comments
@admin.register(GitHubIssueComment)
class GitHubIssueCommentAdmin(admin.ModelAdmin):
"""Admin interface for GitHub issue comments"""
Expand Down
1 change: 0 additions & 1 deletion backend/core/serializers/ai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def __init__(self, *args, **kwargs):
if self.instance is not None:
self.fields['provider_api_key'].required = False
self.fields['provider_api_key'].allow_blank = True
# Make provider read-only for updates instead of removing it
if 'provider' in self.fields:
self.fields['provider'].read_only = True

Expand Down
20 changes: 16 additions & 4 deletions backend/core/serializers/app_integration.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from rest_framework import serializers
from typing import Dict, Any

from core.models.app_integration import AppIntegration
from core.serializers.integration import IntegrationViewSerializer


class AppIntegrationCreateSerializer(serializers.ModelSerializer):
class Meta:
model = AppIntegration
fields = ['application', 'integration']

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
return attrs


class AppIntegrationViewSerializer(serializers.ModelSerializer):
integration = IntegrationViewSerializer(read_only=True)
Expand All @@ -17,11 +23,17 @@ class Meta:
'id',
'integration',
'metadata',
'created_at', 'updated_at'
'created_at',
'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']

def to_representation(self, instance):
def to_representation(self, instance: AppIntegration) -> Dict[str, Any]:
integration_data = IntegrationViewSerializer(instance.integration).data
integration_data["metadata"] = instance.metadata
integration_data["app_integration_uuid"] = str(instance.id)

integration_data.update({
'metadata': instance.metadata or {},
'app_integration_uuid': str(instance.id)
})

return integration_data
1 change: 0 additions & 1 deletion backend/core/serializers/chatroom.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class Meta:
fields = ['uuid', 'name', 'last_message', 'has_unread']

def get_last_message(self, chatroom):
# Widget users (non-dashboard) must not see internal messages in the preview
user_identifier = self.context.get('user_identifier', '')
is_dashboard = user_identifier.startswith('dashboard_')
qs = chatroom.messages.order_by('-created_at')
Expand Down
70 changes: 70 additions & 0 deletions backend/core/services/abstractions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional, Tuple


class DataProcessor(ABC):
@abstractmethod
def process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
pass

@abstractmethod
def validate_data(self, data: Dict[str, Any]) -> bool:
pass


class IngestionService(ABC):
@abstractmethod
def ingest(self, owner: str, repo: str, since: Optional[str] = None) -> None:
pass

@abstractmethod
def get_status(self) -> str:
pass


class AIProviderInterface(ABC):
@abstractmethod
def create_client(self, api_key: str, config: Dict[str, Any]) -> Any:
pass

@abstractmethod
def validate_connection(self, api_key: str, config: Dict[str, Any]) -> Tuple[bool, Any]:
pass

@abstractmethod
def get_models(self) -> List[str]:
pass


class RepositoryManagerInterface(ABC):
@abstractmethod
def get_or_create_repository(self, owner: str, repo: str) -> Any:
pass

@abstractmethod
def update_ingestion_status(self, repository: Any, status: str) -> None:
pass


class ValidationService(ABC):
@abstractmethod
def validate(self, data: Dict[str, Any]) -> Tuple[bool, Any]:
pass

@abstractmethod
def get_validation_errors(self, data: Dict[str, Any]) -> List[str]:
pass


class EmbeddingService(ABC):
@abstractmethod
def create_embeddings(self, text: str) -> List[float]:
pass

@abstractmethod
def create_sparse_embeddings(self, text: str) -> Optional[Dict[str, Any]]:
pass

@abstractmethod
def store_embeddings(self, embeddings: List[Any]) -> bool:
pass
167 changes: 116 additions & 51 deletions backend/core/services/ai_client_service.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from typing import Optional, Tuple, Any
from typing import Optional, Tuple, Any, Dict

from core.models import Application, AIProvider, AppAIProvider
from .factories.ai_provider_factory import AIProviderFactory
from .ai_provider_validator import AIProviderValidator


class AIClientService:

def __init__(self):
self.provider_factory = AIProviderFactory()
self.validator = AIProviderValidator()

def get_client_and_model(
self,
Expand All @@ -17,63 +19,126 @@ def get_client_and_model(
context: str = 'response',
capability: str = 'text'
) -> Tuple[Optional[Any], Optional[str]]:
provider = None
selected_model = model
"""
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,
capability: str
) -> Optional[Dict[str, Any]]:
"""Resolve provider configuration based on input parameters"""
if ai_provider_id:
try:
ai_provider = AIProvider.objects.get(id=ai_provider_id)
provider = self.provider_factory.create_provider(
provider_type=ai_provider.provider,
api_key=ai_provider.provider_api_key,
config=ai_provider.metadata or {}
)
except AIProvider.DoesNotExist:
return None, None
return self._get_provider_by_id(ai_provider_id)
else:
config = self._get_app_provider_config(app, context, capability)
if config:
ai_provider = config.ai_provider
provider = self.provider_factory.create_provider(
provider_type=ai_provider.provider,
api_key=ai_provider.provider_api_key,
config=ai_provider.metadata or {}
)
if not selected_model and config.external_model_id:
selected_model = config.external_model_id
return self._get_app_provider_config(app, context, capability)

if not provider:
return None, None

if not selected_model:
try:
supported_models = provider.get_models()
selected_model = supported_models[0]['name'] if supported_models else 'default'
except Exception:
selected_model = 'default'

return provider, selected_model
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 {
'provider': ai_provider,
'type': ai_provider.provider,
'api_key': ai_provider.provider_api_key,
'config': ai_provider.metadata or {}
}
except AIProvider.DoesNotExist:
return None

def _get_app_provider_config(
self,
app: Application,
context: str,
self,
app: Application,
context: str,
capability: str
) -> Optional[AppAIProvider]:
config = AppAIProvider.objects.filter(
application=app,
) -> 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,
'api_key': config.ai_provider.provider_api_key,
'config': config.ai_provider.metadata or {},
'model': config.external_model_id
}
except Exception:
return None

def _get_app_provider(self, app: Application, context: str, capability: str) -> Optional[AppAIProvider]:
return app.app_ai_providers.filter(
context=context,
capability=capability,
is_active=True,
ai_provider__is_builtin=True
).select_related('ai_provider').first()
capability=capability
).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'],
api_key=provider_config['api_key'],
config=provider_config['config']
)
except Exception:
return None

if not config:
config = AppAIProvider.objects.filter(
application=app,
context=context,
capability=capability,
is_active=True
).select_related('ai_provider').order_by('priority').first()
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
except Exception:
return None

return config
def validate_ai_provider(
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
Loading
Loading