Enhance ImagesViewSet and Images component with site and sector filtering
- Added site_id and sector_id query parameter support in ImagesViewSet for filtering content and task-linked images. - Implemented event listeners in the Images component to refresh data on site and sector changes. - Updated image prompt handling to allow undefined values.
This commit is contained in:
232
backend/igny8_core/api/tests/test_ai_framework.py
Normal file
232
backend/igny8_core/api/tests/test_ai_framework.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for AI framework
|
||||||
|
Tests get_model_config() and AICore.run_ai_request() functions
|
||||||
|
"""
|
||||||
|
from django.test import TestCase
|
||||||
|
from igny8_core.auth.models import Account, User, Plan
|
||||||
|
from igny8_core.modules.system.models import IntegrationSettings
|
||||||
|
from igny8_core.ai.settings import get_model_config
|
||||||
|
from igny8_core.ai.ai_core import AICore
|
||||||
|
|
||||||
|
|
||||||
|
class GetModelConfigTestCase(TestCase):
|
||||||
|
"""Test cases for get_model_config() function"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test data"""
|
||||||
|
# Create plan first
|
||||||
|
self.plan = Plan.objects.create(
|
||||||
|
name="Test Plan",
|
||||||
|
slug="test-plan",
|
||||||
|
price=0,
|
||||||
|
credits_per_month=1000
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create user first (Account needs owner)
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username='testuser',
|
||||||
|
email='test@test.com',
|
||||||
|
password='testpass123',
|
||||||
|
role='owner'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create account with owner
|
||||||
|
self.account = Account.objects.create(
|
||||||
|
name='Test Account',
|
||||||
|
slug='test-account',
|
||||||
|
plan=self.plan,
|
||||||
|
owner=self.user,
|
||||||
|
status='active'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update user to have account
|
||||||
|
self.user.account = self.account
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
def test_get_model_config_with_valid_settings(self):
|
||||||
|
"""Test get_model_config() with valid IntegrationSettings"""
|
||||||
|
IntegrationSettings.objects.create(
|
||||||
|
integration_type='openai',
|
||||||
|
account=self.account,
|
||||||
|
is_active=True,
|
||||||
|
config={
|
||||||
|
'model': 'gpt-4o',
|
||||||
|
'max_tokens': 4000,
|
||||||
|
'temperature': 0.7,
|
||||||
|
'apiKey': 'test-key'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
config = get_model_config('auto_cluster', self.account)
|
||||||
|
|
||||||
|
self.assertEqual(config['model'], 'gpt-4o')
|
||||||
|
self.assertEqual(config['max_tokens'], 4000)
|
||||||
|
self.assertEqual(config['temperature'], 0.7)
|
||||||
|
self.assertIn('response_format', config)
|
||||||
|
|
||||||
|
def test_get_model_config_without_account(self):
|
||||||
|
"""Test get_model_config() without account - should raise ValueError"""
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
get_model_config('auto_cluster', None)
|
||||||
|
|
||||||
|
self.assertIn('Account is required', str(context.exception))
|
||||||
|
|
||||||
|
def test_get_model_config_without_integration_settings(self):
|
||||||
|
"""Test get_model_config() without IntegrationSettings - should raise ValueError"""
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
get_model_config('auto_cluster', self.account)
|
||||||
|
|
||||||
|
self.assertIn('OpenAI IntegrationSettings not configured', str(context.exception))
|
||||||
|
self.assertIn(str(self.account.id), str(context.exception))
|
||||||
|
|
||||||
|
def test_get_model_config_without_model_in_config(self):
|
||||||
|
"""Test get_model_config() without model in config - should raise ValueError"""
|
||||||
|
IntegrationSettings.objects.create(
|
||||||
|
integration_type='openai',
|
||||||
|
account=self.account,
|
||||||
|
is_active=True,
|
||||||
|
config={
|
||||||
|
'max_tokens': 4000,
|
||||||
|
'temperature': 0.7,
|
||||||
|
'apiKey': 'test-key'
|
||||||
|
# No 'model' key
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
get_model_config('auto_cluster', self.account)
|
||||||
|
|
||||||
|
self.assertIn('Model not configured in IntegrationSettings', str(context.exception))
|
||||||
|
self.assertIn(str(self.account.id), str(context.exception))
|
||||||
|
|
||||||
|
def test_get_model_config_with_inactive_settings(self):
|
||||||
|
"""Test get_model_config() with inactive IntegrationSettings - should raise ValueError"""
|
||||||
|
IntegrationSettings.objects.create(
|
||||||
|
integration_type='openai',
|
||||||
|
account=self.account,
|
||||||
|
is_active=False,
|
||||||
|
config={
|
||||||
|
'model': 'gpt-4o',
|
||||||
|
'max_tokens': 4000,
|
||||||
|
'temperature': 0.7
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
get_model_config('auto_cluster', self.account)
|
||||||
|
|
||||||
|
self.assertIn('OpenAI IntegrationSettings not configured', str(context.exception))
|
||||||
|
|
||||||
|
def test_get_model_config_with_function_alias(self):
|
||||||
|
"""Test get_model_config() with function alias"""
|
||||||
|
IntegrationSettings.objects.create(
|
||||||
|
integration_type='openai',
|
||||||
|
account=self.account,
|
||||||
|
is_active=True,
|
||||||
|
config={
|
||||||
|
'model': 'gpt-4o-mini',
|
||||||
|
'max_tokens': 2000,
|
||||||
|
'temperature': 0.5
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with alias
|
||||||
|
config1 = get_model_config('cluster_keywords', self.account)
|
||||||
|
config2 = get_model_config('auto_cluster', self.account)
|
||||||
|
|
||||||
|
# Both should return the same config
|
||||||
|
self.assertEqual(config1['model'], config2['model'])
|
||||||
|
self.assertEqual(config1['model'], 'gpt-4o-mini')
|
||||||
|
|
||||||
|
def test_get_model_config_json_mode_models(self):
|
||||||
|
"""Test get_model_config() sets response_format for JSON mode models"""
|
||||||
|
json_models = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo-preview']
|
||||||
|
|
||||||
|
for model in json_models:
|
||||||
|
IntegrationSettings.objects.filter(account=self.account).delete()
|
||||||
|
IntegrationSettings.objects.create(
|
||||||
|
integration_type='openai',
|
||||||
|
account=self.account,
|
||||||
|
is_active=True,
|
||||||
|
config={
|
||||||
|
'model': model,
|
||||||
|
'max_tokens': 4000,
|
||||||
|
'temperature': 0.7
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
config = get_model_config('auto_cluster', self.account)
|
||||||
|
self.assertIn('response_format', config)
|
||||||
|
self.assertEqual(config['response_format'], {'type': 'json_object'})
|
||||||
|
|
||||||
|
|
||||||
|
class AICoreTestCase(TestCase):
|
||||||
|
"""Test cases for AICore.run_ai_request() function"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test data"""
|
||||||
|
# Create plan first
|
||||||
|
self.plan = Plan.objects.create(
|
||||||
|
name="Test Plan",
|
||||||
|
slug="test-plan",
|
||||||
|
price=0,
|
||||||
|
credits_per_month=1000
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create user first (Account needs owner)
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username='testuser',
|
||||||
|
email='test@test.com',
|
||||||
|
password='testpass123',
|
||||||
|
role='owner'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create account with owner
|
||||||
|
self.account = Account.objects.create(
|
||||||
|
name='Test Account',
|
||||||
|
slug='test-account',
|
||||||
|
plan=self.plan,
|
||||||
|
owner=self.user,
|
||||||
|
status='active'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update user to have account
|
||||||
|
self.user.account = self.account
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
self.ai_core = AICore(account=self.account)
|
||||||
|
|
||||||
|
def test_run_ai_request_without_model(self):
|
||||||
|
"""Test run_ai_request() without model - should return error dict"""
|
||||||
|
result = self.ai_core.run_ai_request(
|
||||||
|
prompt="Test prompt",
|
||||||
|
model=None,
|
||||||
|
function_name='test_function'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('error', result)
|
||||||
|
self.assertIn('Model is required', result['error'])
|
||||||
|
self.assertEqual(result['content'], None)
|
||||||
|
self.assertEqual(result['total_tokens'], 0)
|
||||||
|
|
||||||
|
def test_run_ai_request_with_empty_model(self):
|
||||||
|
"""Test run_ai_request() with empty model string - should return error dict"""
|
||||||
|
result = self.ai_core.run_ai_request(
|
||||||
|
prompt="Test prompt",
|
||||||
|
model="",
|
||||||
|
function_name='test_function'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('error', result)
|
||||||
|
self.assertIn('Model is required', result['error'])
|
||||||
|
self.assertEqual(result['content'], None)
|
||||||
|
self.assertEqual(result['total_tokens'], 0)
|
||||||
|
|
||||||
|
def test_get_model_deprecated(self):
|
||||||
|
"""Test get_model() method is deprecated and raises ValueError"""
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.ai_core.get_model('openai')
|
||||||
|
|
||||||
|
self.assertIn('deprecated', str(context.exception).lower())
|
||||||
|
self.assertIn('run_ai_request', str(context.exception))
|
||||||
|
|
||||||
@@ -663,17 +663,47 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
|||||||
|
|
||||||
account = getattr(request, 'account', None)
|
account = getattr(request, 'account', None)
|
||||||
|
|
||||||
|
# Get site_id and sector_id from query parameters
|
||||||
|
site_id = request.query_params.get('site_id')
|
||||||
|
sector_id = request.query_params.get('sector_id')
|
||||||
|
|
||||||
# Get all content that has images (either directly or via task)
|
# Get all content that has images (either directly or via task)
|
||||||
# First, get content with direct image links
|
# First, get content with direct image links
|
||||||
queryset = Content.objects.filter(images__isnull=False)
|
queryset = Content.objects.filter(images__isnull=False)
|
||||||
if account:
|
if account:
|
||||||
queryset = queryset.filter(account=account)
|
queryset = queryset.filter(account=account)
|
||||||
|
|
||||||
|
# Apply site/sector filtering if provided
|
||||||
|
if site_id:
|
||||||
|
try:
|
||||||
|
queryset = queryset.filter(site_id=int(site_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if sector_id:
|
||||||
|
try:
|
||||||
|
queryset = queryset.filter(sector_id=int(sector_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
# Also get content from images linked via task
|
# Also get content from images linked via task
|
||||||
task_linked_images = Images.objects.filter(task__isnull=False, content__isnull=True)
|
task_linked_images = Images.objects.filter(task__isnull=False, content__isnull=True)
|
||||||
if account:
|
if account:
|
||||||
task_linked_images = task_linked_images.filter(account=account)
|
task_linked_images = task_linked_images.filter(account=account)
|
||||||
|
|
||||||
|
# Apply site/sector filtering to task-linked images
|
||||||
|
if site_id:
|
||||||
|
try:
|
||||||
|
task_linked_images = task_linked_images.filter(site_id=int(site_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if sector_id:
|
||||||
|
try:
|
||||||
|
task_linked_images = task_linked_images.filter(sector_id=int(sector_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
# Get content IDs from task-linked images
|
# Get content IDs from task-linked images
|
||||||
task_content_ids = set()
|
task_content_ids = set()
|
||||||
for image in task_linked_images:
|
for image in task_linked_images:
|
||||||
@@ -694,6 +724,7 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
|||||||
for content_id in content_ids:
|
for content_id in content_ids:
|
||||||
try:
|
try:
|
||||||
content = Content.objects.get(id=content_id)
|
content = Content.objects.get(id=content_id)
|
||||||
|
|
||||||
# Get images linked directly to content OR via task
|
# Get images linked directly to content OR via task
|
||||||
content_images = Images.objects.filter(
|
content_images = Images.objects.filter(
|
||||||
Q(content=content) | Q(task=content.task)
|
Q(content=content) | Q(task=content.task)
|
||||||
|
|||||||
@@ -153,6 +153,24 @@ export default function Images() {
|
|||||||
loadImages();
|
loadImages();
|
||||||
}, [loadImages]);
|
}, [loadImages]);
|
||||||
|
|
||||||
|
// Listen for site and sector changes and refresh data
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSiteChange = () => {
|
||||||
|
loadImages();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSectorChange = () => {
|
||||||
|
loadImages();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('siteChanged', handleSiteChange);
|
||||||
|
window.addEventListener('sectorChanged', handleSectorChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('siteChanged', handleSiteChange);
|
||||||
|
window.removeEventListener('sectorChanged', handleSectorChange);
|
||||||
|
};
|
||||||
|
}, [loadImages]);
|
||||||
|
|
||||||
// Debounced search
|
// Debounced search
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -258,7 +276,7 @@ export default function Images() {
|
|||||||
type: 'in_article',
|
type: 'in_article',
|
||||||
position: img.position || idx + 1,
|
position: img.position || idx + 1,
|
||||||
contentTitle: contentImages.content_title || `Content #${contentId}`,
|
contentTitle: contentImages.content_title || `Content #${contentId}`,
|
||||||
prompt: img.prompt,
|
prompt: img.prompt || undefined,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user