F: Implement backend for prompts
This commit is contained in:
parent
1fda7c79c3
commit
e4411c2c78
0
rsconcept/backend/apps/prompt/__init__.py
Normal file
0
rsconcept/backend/apps/prompt/__init__.py
Normal file
12
rsconcept/backend/apps/prompt/admin.py
Normal file
12
rsconcept/backend/apps/prompt/admin.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
''' Admin view: Prompts for AI helper. '''
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.PromptTemplate)
|
||||||
|
class PromptTemplateAdmin(admin.ModelAdmin):
|
||||||
|
''' Admin model: PromptTemplate. '''
|
||||||
|
list_display = ('id', 'label', 'owner', 'is_shared')
|
||||||
|
list_filter = ('is_shared', 'owner')
|
||||||
|
search_fields = ('label', 'description', 'text')
|
8
rsconcept/backend/apps/prompt/apps.py
Normal file
8
rsconcept/backend/apps/prompt/apps.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
''' Application: Prompts for AI helper. '''
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PromptConfig(AppConfig):
|
||||||
|
''' Application config. '''
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.prompt'
|
28
rsconcept/backend/apps/prompt/migrations/0001_initial.py
Normal file
28
rsconcept/backend/apps/prompt/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-07-13 13:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PromptTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('is_shared', models.BooleanField(default=False, verbose_name='Общий доступ')),
|
||||||
|
('label', models.CharField(max_length=255, verbose_name='Название')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||||
|
('text', models.TextField(blank=True, verbose_name='Содержание')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prompt_templates', to=settings.AUTH_USER_MODEL, verbose_name='Владелец')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
46
rsconcept/backend/apps/prompt/models/PromptTemplate.py
Normal file
46
rsconcept/backend/apps/prompt/models/PromptTemplate.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
''' Model: PromptTemplate for AI prompt storage and sharing. '''
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplate(models.Model):
|
||||||
|
'''Represents an AI prompt template, which can be user-owned or shared globally.'''
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
verbose_name='Владелец',
|
||||||
|
to=User,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='prompt_templates'
|
||||||
|
)
|
||||||
|
is_shared = models.BooleanField(
|
||||||
|
verbose_name='Общий доступ',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
label = models.CharField(
|
||||||
|
verbose_name='Название',
|
||||||
|
max_length=255
|
||||||
|
)
|
||||||
|
description = models.TextField(
|
||||||
|
verbose_name='Описание',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
text = models.TextField(
|
||||||
|
verbose_name='Содержание',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def can_set_shared(self, user: User) -> bool:
|
||||||
|
'''Return True if the user can set is_shared=True (admin/staff only).'''
|
||||||
|
return user.is_superuser or user.is_staff
|
||||||
|
|
||||||
|
def can_access(self, user: User) -> bool:
|
||||||
|
'''Return True if the user can access this template (shared or owner).'''
|
||||||
|
if self.is_shared:
|
||||||
|
return True
|
||||||
|
return self.owner == user
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.label}'
|
3
rsconcept/backend/apps/prompt/models/__init__.py
Normal file
3
rsconcept/backend/apps/prompt/models/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
''' Django: Models for AI Prompts. '''
|
||||||
|
|
||||||
|
from .PromptTemplate import PromptTemplate
|
2
rsconcept/backend/apps/prompt/serializers/__init__.py
Normal file
2
rsconcept/backend/apps/prompt/serializers/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
''' Serializers for persistent data manipulation (AI Prompts). '''
|
||||||
|
from .data_access import PromptTemplateSerializer
|
39
rsconcept/backend/apps/prompt/serializers/data_access.py
Normal file
39
rsconcept/backend/apps/prompt/serializers/data_access.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
''' Serializers for prompt template data access. '''
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from shared import messages as msg
|
||||||
|
|
||||||
|
from ..models import PromptTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplateSerializer(serializers.ModelSerializer):
|
||||||
|
'''Serializer for PromptTemplate, enforcing permissions and ownership logic.'''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = PromptTemplate
|
||||||
|
fields = ['id', 'owner', 'is_shared', 'label', 'description', 'text']
|
||||||
|
read_only_fields = ['id', 'owner']
|
||||||
|
|
||||||
|
def validate_label(self, value):
|
||||||
|
user = self.context['request'].user
|
||||||
|
if PromptTemplate.objects.filter(owner=user, label=value).exists():
|
||||||
|
raise serializers.ValidationError(msg.promptLabelTaken(value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_is_shared(self, value):
|
||||||
|
user = self.context['request'].user
|
||||||
|
if value and not (user.is_superuser or user.is_staff):
|
||||||
|
raise serializers.ValidationError(msg.promptSharedPermissionDenied())
|
||||||
|
return value
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['owner'] = self.context['request'].user
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
user = self.context['request'].user
|
||||||
|
if 'is_shared' in validated_data:
|
||||||
|
if validated_data['is_shared'] and not (user.is_superuser or user.is_staff):
|
||||||
|
raise serializers.ValidationError(msg.promptSharedPermissionDenied())
|
||||||
|
return super().update(instance, validated_data)
|
2
rsconcept/backend/apps/prompt/tests/__init__.py
Normal file
2
rsconcept/backend/apps/prompt/tests/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
''' Tests. '''
|
||||||
|
from .t_prompts import *
|
113
rsconcept/backend/apps/prompt/tests/t_prompts.py
Normal file
113
rsconcept/backend/apps/prompt/tests/t_prompts.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
''' Testing API: Prompts. '''
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
from ..models import PromptTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class TestPromptTemplateViewSet(EndpointTester):
|
||||||
|
''' Testing PromptTemplate viewset. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.admin = self.user2
|
||||||
|
self.admin.is_superuser = True
|
||||||
|
self.admin.save()
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/', method='post')
|
||||||
|
def test_create_prompt(self):
|
||||||
|
data = {
|
||||||
|
'label': 'Test',
|
||||||
|
'description': 'desc',
|
||||||
|
'text': 'prompt text',
|
||||||
|
'is_shared': False
|
||||||
|
}
|
||||||
|
response = self.executeCreated(data=data)
|
||||||
|
self.assertEqual(response.data['label'], 'Test')
|
||||||
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/', method='post')
|
||||||
|
def test_create_shared_prompt_by_admin(self):
|
||||||
|
self.client.force_authenticate(user=self.admin)
|
||||||
|
data = {
|
||||||
|
'label': 'Shared',
|
||||||
|
'description': 'desc',
|
||||||
|
'text': 'prompt text',
|
||||||
|
'is_shared': True
|
||||||
|
}
|
||||||
|
response = self.executeCreated(data=data)
|
||||||
|
self.assertTrue(response.data['is_shared'])
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/', method='post')
|
||||||
|
def test_create_shared_prompt_by_user_forbidden(self):
|
||||||
|
data = {
|
||||||
|
'label': 'Shared',
|
||||||
|
'description': 'desc',
|
||||||
|
'text': 'prompt text',
|
||||||
|
'is_shared': True
|
||||||
|
}
|
||||||
|
response = self.executeBadData(data=data)
|
||||||
|
self.assertIn('is_shared', response.data)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
||||||
|
def test_update_prompt_owner(self):
|
||||||
|
prompt = PromptTemplate.objects.create(owner=self.user, label='ToUpdate', description='', text='t')
|
||||||
|
response = self.executeOK(data={'label': 'Updated'}, item=prompt.id)
|
||||||
|
self.assertEqual(response.data['label'], 'Updated')
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
||||||
|
def test_update_prompt_not_owner_forbidden(self):
|
||||||
|
prompt = PromptTemplate.objects.create(owner=self.admin, label='Other', description='', text='t')
|
||||||
|
response = self.executeForbidden(data={'label': 'Updated'}, item=prompt.id)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/{item}/', method='delete')
|
||||||
|
def test_delete_prompt_owner(self):
|
||||||
|
prompt = PromptTemplate.objects.create(owner=self.user, label='ToDelete', description='', text='t')
|
||||||
|
self.executeNoContent(item=prompt.id)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/{item}/', method='delete')
|
||||||
|
def test_delete_prompt_not_owner_forbidden(self):
|
||||||
|
prompt = PromptTemplate.objects.create(owner=self.admin, label='Other2', description='', text='t')
|
||||||
|
self.executeForbidden(item=prompt.id)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/available/', method='get')
|
||||||
|
def test_available_endpoint(self):
|
||||||
|
PromptTemplate.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
label='Mine',
|
||||||
|
description='',
|
||||||
|
text='t'
|
||||||
|
)
|
||||||
|
PromptTemplate.objects.create(
|
||||||
|
owner=self.admin,
|
||||||
|
label='Shared',
|
||||||
|
description='',
|
||||||
|
text='t',
|
||||||
|
is_shared=True
|
||||||
|
)
|
||||||
|
response = self.executeOK()
|
||||||
|
labels = [item['label'] for item in response.data]
|
||||||
|
self.assertIn('Mine', labels)
|
||||||
|
self.assertIn('Shared', labels)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/prompts/{item}/', method='patch')
|
||||||
|
def test_permissions_on_shared(self):
|
||||||
|
prompt = PromptTemplate.objects.create(
|
||||||
|
owner=self.admin,
|
||||||
|
label='Shared',
|
||||||
|
description='',
|
||||||
|
text='t',
|
||||||
|
is_shared=True
|
||||||
|
)
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
response = self.executeForbidden(data={'label': 'Nope'}, item=prompt.id)
|
9
rsconcept/backend/apps/prompt/urls.py
Normal file
9
rsconcept/backend/apps/prompt/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
''' Routing: Prompts for AI helper. '''
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from .views import PromptTemplateViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register('prompts', PromptTemplateViewSet, 'prompt-template')
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
2
rsconcept/backend/apps/prompt/views/__init__.py
Normal file
2
rsconcept/backend/apps/prompt/views/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
''' REST API: Endpoint processors for AI Prompts. '''
|
||||||
|
from .prompts import PromptTemplateViewSet
|
52
rsconcept/backend/apps/prompt/views/prompts.py
Normal file
52
rsconcept/backend/apps/prompt/views/prompts.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
''' Views: PromptTemplate endpoints for AI prompt management. '''
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework import permissions, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from ..models import PromptTemplate
|
||||||
|
from ..serializers import PromptTemplateSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class IsOwnerOrAdmin(permissions.BasePermission):
|
||||||
|
'''Permission: Only owner or admin can modify, anyone can view shared.'''
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if request.method in permissions.SAFE_METHODS:
|
||||||
|
return obj.is_shared or obj.owner == request.user
|
||||||
|
return obj.owner == request.user or request.user.is_staff or request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=['Prompts'])
|
||||||
|
class PromptTemplateViewSet(viewsets.ModelViewSet):
|
||||||
|
'''ViewSet: CRUD and listing for PromptTemplate, with sharing logic.'''
|
||||||
|
queryset = PromptTemplate.objects.all()
|
||||||
|
serializer_class = PromptTemplateSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if self.action == 'available':
|
||||||
|
return PromptTemplate.objects.none()
|
||||||
|
return PromptTemplate.objects.filter(models.Q(owner=user) | models.Q(is_shared=True)).distinct()
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
obj = PromptTemplate.objects.get(pk=self.kwargs['pk'])
|
||||||
|
self.check_object_permissions(self.request, obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(owner=self.request.user)
|
||||||
|
|
||||||
|
@extend_schema(summary='List user-owned and shared prompt templates')
|
||||||
|
@action(detail=False, methods=['get'], url_path='available')
|
||||||
|
def available(self, request):
|
||||||
|
'''Return user-owned and shared prompt templates.'''
|
||||||
|
user = request.user
|
||||||
|
owned = PromptTemplate.objects.filter(owner=user)
|
||||||
|
shared = PromptTemplate.objects.filter(is_shared=True)
|
||||||
|
templates = (owned | shared).distinct()
|
||||||
|
serializer = self.get_serializer(templates, many=True)
|
||||||
|
return Response(serializer.data)
|
|
@ -4,11 +4,9 @@ from django.contrib import admin
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Constituenta)
|
||||||
class ConstituentaAdmin(admin.ModelAdmin):
|
class ConstituentaAdmin(admin.ModelAdmin):
|
||||||
''' Admin model: Constituenta. '''
|
''' Admin model: Constituenta. '''
|
||||||
ordering = ['schema', 'order']
|
ordering = ['schema', 'order']
|
||||||
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved']
|
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved']
|
||||||
search_fields = ['term_resolved', 'definition_resolved']
|
search_fields = ['term_resolved', 'definition_resolved']
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(models.Constituenta, ConstituentaAdmin)
|
|
||||||
|
|
|
@ -76,6 +76,7 @@ INSTALLED_APPS = [
|
||||||
'apps.library',
|
'apps.library',
|
||||||
'apps.rsform',
|
'apps.rsform',
|
||||||
'apps.oss',
|
'apps.oss',
|
||||||
|
'apps.prompt',
|
||||||
|
|
||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
'drf_spectacular_sidecar',
|
'drf_spectacular_sidecar',
|
||||||
|
|
|
@ -11,6 +11,7 @@ urlpatterns = [
|
||||||
path('api/', include('apps.library.urls')),
|
path('api/', include('apps.library.urls')),
|
||||||
path('api/', include('apps.rsform.urls')),
|
path('api/', include('apps.rsform.urls')),
|
||||||
path('api/', include('apps.oss.urls')),
|
path('api/', include('apps.oss.urls')),
|
||||||
|
path('api/', include('apps.prompt.urls')),
|
||||||
path('users/', include('apps.users.urls')),
|
path('users/', include('apps.users.urls')),
|
||||||
path('schema', SpectacularAPIView.as_view(), name='schema'),
|
path('schema', SpectacularAPIView.as_view(), name='schema'),
|
||||||
path('redoc', SpectacularRedocView.as_view()),
|
path('redoc', SpectacularRedocView.as_view()),
|
||||||
|
|
|
@ -144,3 +144,19 @@ def passwordsNotMatch():
|
||||||
|
|
||||||
def emailAlreadyTaken():
|
def emailAlreadyTaken():
|
||||||
return 'Пользователь с данным email уже существует'
|
return 'Пользователь с данным email уже существует'
|
||||||
|
|
||||||
|
|
||||||
|
def promptLabelTaken(label: str):
|
||||||
|
return f'Шаблон с меткой "{label}" уже существует у пользователя.'
|
||||||
|
|
||||||
|
|
||||||
|
def promptNotOwner():
|
||||||
|
return 'Вы не являетесь владельцем этого шаблона.'
|
||||||
|
|
||||||
|
|
||||||
|
def promptSharedPermissionDenied():
|
||||||
|
return 'Только администратор может сделать шаблон общедоступным.'
|
||||||
|
|
||||||
|
|
||||||
|
def promptNotFound():
|
||||||
|
return 'Шаблон не найден.'
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
export interface IPromptTemplate {
|
export interface IPromptTemplate {
|
||||||
id: number;
|
id: number;
|
||||||
owner: number | null;
|
owner: number | null;
|
||||||
|
is_shared: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========= SCHEMAS ========
|
||||||
|
|
|
@ -13,12 +13,14 @@ const mockPrompts: IPromptTemplate[] = [
|
||||||
id: 1,
|
id: 1,
|
||||||
owner: null,
|
owner: null,
|
||||||
label: 'Greeting',
|
label: 'Greeting',
|
||||||
|
is_shared: true,
|
||||||
description: 'A simple greeting prompt.',
|
description: 'A simple greeting prompt.',
|
||||||
text: 'Hello, ${name}! How can I assist you today?'
|
text: 'Hello, ${name}! How can I assist you today?'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
owner: null,
|
owner: null,
|
||||||
|
is_shared: true,
|
||||||
label: 'Summary',
|
label: 'Summary',
|
||||||
description: 'Summarize the following text.',
|
description: 'Summarize the following text.',
|
||||||
text: 'Please summarize the following: ${text}'
|
text: 'Please summarize the following: ${text}'
|
||||||
|
|
13
rsconcept/frontend/src/features/ai/labels.ts
Normal file
13
rsconcept/frontend/src/features/ai/labels.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { PromptVariableType } from './models/prompting';
|
||||||
|
|
||||||
|
const describePromptVariableRecord: Record<PromptVariableType, string> = {
|
||||||
|
[PromptVariableType.BLOCK]: 'Текущий блок операционной схемы',
|
||||||
|
[PromptVariableType.OSS]: 'Текущая операционная схема',
|
||||||
|
[PromptVariableType.SCHEMA]: 'Текущая концептуальный схема',
|
||||||
|
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Retrieves description for {@link PromptVariableType}. */
|
||||||
|
export function describePromptVariable(itemType: PromptVariableType): string {
|
||||||
|
return describePromptVariableRecord[itemType] ?? `UNKNOWN VARIABLE TYPE: ${itemType}`;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { extractPromptVariables } from './prompting-api';
|
||||||
|
|
||||||
|
describe('extractPromptVariables', () => {
|
||||||
|
it('extracts a single variable', () => {
|
||||||
|
expect(extractPromptVariables('Hello {{name}}!')).toEqual(['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts multiple variables', () => {
|
||||||
|
expect(extractPromptVariables('Hi {{firstName}}, your ID is {{user.id}}.')).toEqual(['firstName', 'user.id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts variables with hyphens and dots', () => {
|
||||||
|
expect(extractPromptVariables('Welcome {{user-name}} and {{user.name}}!')).toEqual(['user-name', 'user.name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array if no variables', () => {
|
||||||
|
expect(extractPromptVariables('No variables here!')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid variable patterns', () => {
|
||||||
|
expect(extractPromptVariables('Hello {name}, {{name!}}, {{123}}, {{user_name}}')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts repeated variables', () => {
|
||||||
|
expect(extractPromptVariables('Repeat: {{foo}}, again: {{foo}}')).toEqual(['foo', 'foo']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with adjacent variables', () => {
|
||||||
|
expect(extractPromptVariables('{{a}}{{b}}{{c}}')).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array for empty string', () => {
|
||||||
|
expect(extractPromptVariables('')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts variables at string boundaries', () => {
|
||||||
|
expect(extractPromptVariables('{{start}} middle {{end}}')).toEqual(['start', 'end']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
/** Extracts a list of variables (as string[]) from a target string.
|
||||||
|
* Note: Variables are wrapped in {{...}} and can include a-zA-Z, hyphen, and dot inside curly braces.
|
||||||
|
* */
|
||||||
|
export function extractPromptVariables(target: string): string[] {
|
||||||
|
const regex = /\{\{([a-zA-Z.-]+)\}\}/g;
|
||||||
|
const result: string[] = [];
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = regex.exec(target)) !== null) {
|
||||||
|
result.push(match[1]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -1,29 +1,29 @@
|
||||||
/** Represents prompt variable type. */
|
/** Represents prompt variable type. */
|
||||||
export const PromptVariableType = {
|
export const PromptVariableType = {
|
||||||
BLOCK: 'block',
|
BLOCK: 'block',
|
||||||
BLOCK_TITLE: 'block.title',
|
// BLOCK_TITLE: 'block.title',
|
||||||
BLOCK_DESCRIPTION: 'block.description',
|
// BLOCK_DESCRIPTION: 'block.description',
|
||||||
BLOCK_CONTENTS: 'block.contents',
|
// BLOCK_CONTENTS: 'block.contents',
|
||||||
|
|
||||||
OSS: 'oss',
|
OSS: 'oss',
|
||||||
OSS_CONTENTS: 'oss.contents',
|
// OSS_CONTENTS: 'oss.contents',
|
||||||
OSS_ALIAS: 'oss.alias',
|
// OSS_ALIAS: 'oss.alias',
|
||||||
OSS_TITLE: 'oss.title',
|
// OSS_TITLE: 'oss.title',
|
||||||
OSS_DESCRIPTION: 'oss.description',
|
// OSS_DESCRIPTION: 'oss.description',
|
||||||
|
|
||||||
SCHEMA: 'schema',
|
SCHEMA: 'schema',
|
||||||
SCHEMA_ALIAS: 'schema.alias',
|
// SCHEMA_ALIAS: 'schema.alias',
|
||||||
SCHEMA_TITLE: 'schema.title',
|
// SCHEMA_TITLE: 'schema.title',
|
||||||
SCHEMA_DESCRIPTION: 'schema.description',
|
// SCHEMA_DESCRIPTION: 'schema.description',
|
||||||
SCHEMA_THESAURUS: 'schema.thesaurus',
|
// SCHEMA_THESAURUS: 'schema.thesaurus',
|
||||||
SCHEMA_GRAPH: 'schema.graph',
|
// SCHEMA_GRAPH: 'schema.graph',
|
||||||
SCHEMA_TYPE_GRAPH: 'schema.type-graph',
|
// SCHEMA_TYPE_GRAPH: 'schema.type-graph',
|
||||||
|
|
||||||
CONSTITUENTA: 'constituent',
|
CONSTITUENTA: 'constituenta'
|
||||||
CONSTITUENTA_ALIAS: 'constituent.alias',
|
// CONSTITUENTA_ALIAS: 'constituent.alias',
|
||||||
CONSTITUENTA_CONVENTION: 'constituent.convention',
|
// CONSTITUENTA_CONVENTION: 'constituent.convention',
|
||||||
CONSTITUENTA_DEFINITION: 'constituent.definition',
|
// CONSTITUENTA_DEFINITION: 'constituent.definition',
|
||||||
CONSTITUENTA_DEFINITION_FORMAL: 'constituent.definition-formal',
|
// CONSTITUENTA_DEFINITION_FORMAL: 'constituent.definition-formal',
|
||||||
CONSTITUENTA_EXPRESSION_TREE: 'constituent.expression-tree'
|
// CONSTITUENTA_EXPRESSION_TREE: 'constituent.expression-tree'
|
||||||
} as const;
|
} as const;
|
||||||
export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType];
|
export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType];
|
||||||
|
|
64
rsconcept/frontend/src/features/ai/stores/ai-context.ts
Normal file
64
rsconcept/frontend/src/features/ai/stores/ai-context.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { type IBlock, type IOperationSchema } from '@/features/oss/models/oss';
|
||||||
|
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||||
|
|
||||||
|
import { PromptVariableType } from '../models/prompting';
|
||||||
|
|
||||||
|
interface AIContextStore {
|
||||||
|
currentOSS: IOperationSchema | null;
|
||||||
|
setCurrentOSS: (value: IOperationSchema | null) => void;
|
||||||
|
|
||||||
|
currentSchema: IRSForm | null;
|
||||||
|
setCurrentSchema: (value: IRSForm | null) => void;
|
||||||
|
|
||||||
|
currentBlock: IBlock | null;
|
||||||
|
setCurrentBlock: (value: IBlock | null) => void;
|
||||||
|
|
||||||
|
currentConstituenta: IConstituenta | null;
|
||||||
|
setCurrentConstituenta: (value: IConstituenta | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAIStore = create<AIContextStore>()(set => ({
|
||||||
|
currentOSS: null,
|
||||||
|
setCurrentOSS: value => set({ currentOSS: value }),
|
||||||
|
|
||||||
|
currentSchema: null,
|
||||||
|
setCurrentSchema: value => set({ currentSchema: value }),
|
||||||
|
|
||||||
|
currentBlock: null,
|
||||||
|
setCurrentBlock: value => set({ currentBlock: value }),
|
||||||
|
|
||||||
|
currentConstituenta: null,
|
||||||
|
setCurrentConstituenta: value => set({ currentConstituenta: value })
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** Returns a selector function for Zustand based on variable type */
|
||||||
|
export function makeVariableSelector(variableType: PromptVariableType) {
|
||||||
|
switch (variableType) {
|
||||||
|
case PromptVariableType.OSS:
|
||||||
|
return (state: AIContextStore) => ({ currentOSS: state.currentOSS });
|
||||||
|
case PromptVariableType.SCHEMA:
|
||||||
|
return (state: AIContextStore) => ({ currentSchema: state.currentSchema });
|
||||||
|
case PromptVariableType.BLOCK:
|
||||||
|
return (state: AIContextStore) => ({ currentBlock: state.currentBlock });
|
||||||
|
case PromptVariableType.CONSTITUENTA:
|
||||||
|
return (state: AIContextStore) => ({ currentConstituenta: state.currentConstituenta });
|
||||||
|
default:
|
||||||
|
return () => ({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluates a prompt variable */
|
||||||
|
export function evaluatePromptVariable(variableType: PromptVariableType, context: Partial<AIContextStore>): string {
|
||||||
|
switch (variableType) {
|
||||||
|
case PromptVariableType.OSS:
|
||||||
|
return context.currentOSS?.title ?? '';
|
||||||
|
case PromptVariableType.SCHEMA:
|
||||||
|
return context.currentSchema?.title ?? '';
|
||||||
|
case PromptVariableType.BLOCK:
|
||||||
|
return context.currentBlock?.title ?? '';
|
||||||
|
case PromptVariableType.CONSTITUENTA:
|
||||||
|
return context.currentConstituenta?.alias ?? '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { PromptVariableType } from '../models/prompting';
|
||||||
|
|
||||||
|
import { useAIStore } from './ai-context';
|
||||||
|
|
||||||
|
export function useAvailableVariables(): PromptVariableType[] {
|
||||||
|
const hasCurrentOSS = useAIStore(state => !!state.currentOSS);
|
||||||
|
const hasCurrentSchema = useAIStore(state => !!state.currentSchema);
|
||||||
|
const hasCurrentBlock = useAIStore(state => !!state.currentBlock);
|
||||||
|
const hasCurrentConstituenta = useAIStore(state => !!state.currentConstituenta);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(hasCurrentOSS ? [PromptVariableType.OSS] : []),
|
||||||
|
...(hasCurrentSchema ? [PromptVariableType.SCHEMA] : []),
|
||||||
|
...(hasCurrentBlock ? [PromptVariableType.BLOCK] : []),
|
||||||
|
...(hasCurrentConstituenta ? [PromptVariableType.CONSTITUENTA] : [])
|
||||||
|
];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user