diff --git a/src/api/fixtures/initial.json b/src/api/fixtures/initial.json index fffbb34a..3b0b373b 100644 --- a/src/api/fixtures/initial.json +++ b/src/api/fixtures/initial.json @@ -136,7 +136,7 @@ "salut" ], "project": 1 - } + } }, { "model": "api.intent", @@ -157,7 +157,7 @@ "jamais" ], "project": 1 - } + } }, { "model": "api.intent", @@ -380,7 +380,7 @@ { "model": "api.utter", "pk": 1, - + "fields": { "name": "utter_default", "multiple_alternatives": true, @@ -436,8 +436,8 @@ "pk": 5, "fields": { "name": "utter_tudo_bem", - "multiple_alternatives": false, - "alternatives":[ + "multiple_alternatives": false, + "alternatives":[ ["Tudo bem, obrigada! Em que posso te ajudar?"] ], "project": 1 @@ -449,9 +449,9 @@ "fields": { "name": "utter_continuar_conversa", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ [" E aí, qual nosso próximo assunto?"] - + ], "project": 1 } @@ -462,20 +462,20 @@ "fields": { "name": "utter_contato", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ ["Para entrar em contato por meio do Suporte bastante acessar este link: lappis.rocks"] ], "project": 1 } - }, + }, { "model": "api.utter", "pk": 8, "fields": { "name": "utter_endereco", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ ["O Lappis fica na Universidade de Brasília, campus do Gama"] ], @@ -488,7 +488,7 @@ "fields": { "name": "utter_definicao", "multiple_alternatives": true, - "alternatives":[ + "alternatives":[ ["Eu sou uma assistente virtual da Secretaria Especial da Cultura, meu objetivo é tirar dúvidas sobre a Lei de Incentivo à Cultura."], ["Uma assistente virtual é uma inteligência artificial criada para interagir com as pessoas."], ["Para me comunicar com vocês utilizo algoritmos de aprendizagem profundo (deep learning) e sou toda software livre :P"], @@ -504,7 +504,7 @@ "fields": { "name": "utter_quem_criou_a_tais", "multiple_alternatives": true, - "alternatives":[ + "alternatives":[ ["Fui desenvolvida pelo LAPPIS (Laboratório Avançado de Produção Pesquisa e Inovação em Software) na Universidade de Brasília, um laboratório super inovador e descolado!"], ["Se quiser me conhecer mais, acesse o meu projeto :) https://github.com/lappis-unb/tais"] @@ -518,7 +518,7 @@ "fields": { "name": "utter_elogios", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ ["Obrigada! É sempre bom dar e receber elogios :P"] ], @@ -531,7 +531,7 @@ "fields": { "name": "utter_tem_wpp", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ ["Eu não tenho wpp, eu só posso me comunicar com você aqui pelo portal, aqui é minha casa."] ], @@ -544,7 +544,7 @@ "fields": { "name": "utter_o_que_sei_falar", "multiple_alternatives": true, - "alternatives":[ + "alternatives":[ ["Veja alguns dos assuntos em que posso te ajudar :)"], ["Agora que você já sabe do que podemos conversar, vou te dar outras dicas..."], ["Eu consigo te entender melhor quando você me faz perguntas curtas e me manda uma pergunta de cada vez."], @@ -561,7 +561,7 @@ "fields": { "name": "utter_menu", "multiple_alternatives": true, - "alternatives":[ + "alternatives":[ ["Podemos conversar sobre como submeter um projeto, as etapas, algumas dúvidas operacionais…"], ["Ou se você já tiver um projeto cadastrado, a gente pode falar sobre isso também."], ["Para lembrar dos assuntos que eu domino, é só digitar: MEAJUDA"], @@ -577,7 +577,7 @@ "fields": { "name": "utter_agradecimento", "multiple_alternatives": true, - "alternatives":[ + "alternatives":[ ["De nada, é sempre um prazer ajudar!"], ["Qual o próximo assunto que te interessa?"] @@ -591,7 +591,7 @@ "fields": { "name": "utter_definicao_tais", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ ["Eu sou uma assistente virtual da Secretaria Especial da Cultura, meu objetivo é tirar dúvidas sobre a Lei de Incentivo à Cultura.", "Uma assistente virtual é uma inteligência artificial criada para interagir com as pessoas. Para me comunicar com vocês utilizo algoritmos de aprendizagem profundo (deep learning) e sou toda software livre :P", "Em cada interação que temos aqui, eu aprendendo mais para que a gente se comunique melhor. Obrigada por conversar comigo!"] @@ -605,7 +605,7 @@ "fields": { "name": "utter_objetivo", "multiple_alternatives": false, - "alternatives":[ + "alternatives":[ ["Mas antes, para a nossa conversa ser mais eficiente, você quer saber como submeter uma proposta?"] ], "project": 1 @@ -615,6 +615,7 @@ "model": "api.story", "pk": 1, "fields": { + "is_checkpoint": true, "content":[ {"type": "intent", "name": "quem_criou_a_tais"}, {"type": "utter", "name": "utter_definicao_tais"}, @@ -691,7 +692,8 @@ "content":[ {"type": "intent", "name": "endereco"}, {"type": "utter", "name": "utter_endereco"}, - {"type": "utter", "name": "utter_continuar_conversa"} + {"type": "utter", "name": "utter_continuar_conversa"}, + {"type": "checkpoint", "name": "Diálogo_RasaBot_1" } ] } }, diff --git a/src/api/migrations/0003_story_is_checkpoint.py b/src/api/migrations/0003_story_is_checkpoint.py new file mode 100644 index 00000000..b746736d --- /dev/null +++ b/src/api/migrations/0003_story_is_checkpoint.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2019-10-03 15:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_auto_20190909_1838'), + ] + + operations = [ + migrations.AddField( + model_name='story', + name='is_checkpoint', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/api/models/__init__.py b/src/api/models/__init__.py index e555d0fc..aec02e8d 100644 --- a/src/api/models/__init__.py +++ b/src/api/models/__init__.py @@ -1,4 +1,4 @@ from .project import Project, ProjectSerializer from .intent import Intent, IntentSerializer, IntentListSerializer, IntentExampleSerializer from .utter import Utter, UtterSerializer, UtterListSerializer, UtterExampleSerializer -from .story import Story, StorySerializer, StoryListSerializer +from .story import Story, StorySerializer, StoryListSerializer, CheckpointSerializer, CheckpointListSerializer diff --git a/src/api/models/story.py b/src/api/models/story.py index 62b14f42..296f952c 100644 --- a/src/api/models/story.py +++ b/src/api/models/story.py @@ -10,6 +10,7 @@ class Story(models.Model): project = models.EmbeddedModelField( model_container=Project ) + is_checkpoint = models.BooleanField(default=False) objects = models.DjongoManager() @@ -17,24 +18,22 @@ class StorySerializer(serializers.ModelSerializer): content = serializers.SerializerMethodField() def get_example(self, obj): - intent_ids = [element['id'] for element in obj.content if element['type'] == 'intent'] - intents = Intent.objects.filter(pk__in=intent_ids) - utter_ids = [element['id'] for element in obj.content if element['type'] == 'utter'] - utters = Utter.objects.filter(pk__in=utter_ids) + for i, element in enumerate(obj.content): - elements = {} - elements['intent'] = intents - elements['utter'] = utters + if element['type'] == 'utter': + element_obj = Utter.objects.filter(pk=element['id']).first() + obj.content[i]['example'] = random.choice(getattr(element_obj, 'alternatives')) + obj.content[i]['name'] = getattr(element_obj, 'name') + elif element['type'] == 'intent': + element_obj = Intent.objects.filter(pk=element['id']).first() + obj.content[i]['example'] = random.choice(getattr(element_obj, 'samples')) + obj.content[i]['name'] = getattr(element_obj, 'name') + elif element['type'] == 'checkpoint': + element_obj = Story.objects.filter(pk=element['id']).first() + obj.content[i]['name'] = getattr(element_obj, 'name') - field = {} - field['intent'] = 'samples' - field['utter'] = 'alternatives' - for i, element in enumerate(obj.content): - element_obj = elements[element['type']].filter(pk=element['id']).first() - obj.content[i]['example'] = random.choice(getattr(element_obj, field[element['type']])) - obj.content[i]['name'] = getattr(element_obj, 'name') def get_content(self, obj): self.get_example(obj) @@ -42,7 +41,7 @@ def get_content(self, obj): class Meta: model = Story - fields = ['id', 'name', 'content'] + fields = ['id', 'name', 'content', 'is_checkpoint'] class StoryListSerializer(serializers.ModelSerializer): content = serializers.SerializerMethodField() @@ -57,4 +56,29 @@ def get_content(self, obj): class Meta: model = Story - fields = ['id', 'name', 'content'] \ No newline at end of file + fields = ['id', 'name', 'content'] + + +class CheckpointSerializer(serializers.ModelSerializer): + content = serializers.SerializerMethodField() + + def get_content(self, obj): + StorySerializer.get_example(self, obj) + + return [{ + 'id': content['id'], + 'name': content['name'], + 'example': content['example'], + 'type': content['type'] + } for content in obj.content] + + class Meta: + model = Story + fields = ['id', 'name', 'content'] + + +class CheckpointListSerializer(serializers.ModelSerializer): + + class Meta: + model = Story + fields = ['id', 'name'] diff --git a/src/api/parser.py b/src/api/parser.py index dcc1fd66..39ce39b9 100644 --- a/src/api/parser.py +++ b/src/api/parser.py @@ -8,6 +8,9 @@ def format_utter(utter_name): else: return utter_name +def format_checkpoint(name): + return "checkpoint_" + name + class StoryParser: """ Generate a markdown string from a given @@ -16,12 +19,17 @@ class StoryParser: def parse(self, story: Story): name = f'## {unidecode(story.name)}\n' body = '' - + + if story.is_checkpoint: + body += self._checkpoint_parser(story.name) + for c in story.content: if c['type'] == "intent": body += self._intent_parser(c) elif c['type'] == "utter": body += '\t' + self._utter_parser(c) + elif c['type'] == "checkpoint": + body += self._checkpoint_parser(c["name"]) return name + body + '\n' @@ -29,10 +37,12 @@ def parse(self, story: Story): def _intent_parser(self, intent): return f'* {unidecode(intent["name"])}\n' - def _utter_parser(self, utter): return f'- {unidecode(format_utter(utter["name"]))}\n' + def _checkpoint_parser(self, name): + return f'> {unidecode(format_checkpoint(name))}\n' + class IntentParser: """ @@ -55,7 +65,7 @@ class DomainParser: """ def parse(self, project: Project): content = '' - + intents = Intent.objects.filter(project=project) utters = Utter.objects.filter(project=project) entities = [] @@ -67,7 +77,7 @@ def parse(self, project: Project): # content += self._generic_list_parser('entities', [e.name for e in entities]) content += self._templates_parser(utters) content += self._generic_list_parser('actions', [unidecode(format_utter(u.name)) for u in utters]) - + return content def _generic_list_parser(self, name: str, elements: list): @@ -75,17 +85,17 @@ def _generic_list_parser(self, name: str, elements: list): for e in elements: result += f' - {e}\n' - + return result + '\n' - - + + def _templates_parser(self, utters: list): result = f'templates:\n' - for u in utters: + for u in utters: ident = 2 * ' ' utter_name = format_utter(u.name) result += f'{ident}{unidecode(utter_name)}:\n' @@ -96,5 +106,5 @@ def _templates_parser(self, utters: list): ident = 10 * ' ' result += f'{ident}{t}\n' result += f'\n' - + return result + '\n' diff --git a/src/api/urls.py b/src/api/urls.py index de43d0f5..d519a346 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -10,10 +10,12 @@ path('projects//intents/', ListIntents.as_view()), path('projects//intents//example', ListIntentExample.as_view()), path('projects//utters/', ListUtters.as_view()), - path('projects//utters/', ListUtters.as_view()), + path('projects//utters/', ListUtters.as_view()), path('projects//utters//example', ListUtterExample.as_view()), path('projects//stories/', ListStories.as_view()), path('projects//stories/', ListStories.as_view()), + path('projects//stories/checkpoints', ListCheckpoints.as_view()), + path('projects//stories//checkpoint', ListCheckpoints.as_view()), path('files//stories/', StoriesFile.as_view(), name='stories-file'), path('files//intents/', IntentsFile.as_view(), name='intents-file'), path('files//domain/', DomainFile.as_view(), name='domain-file'), diff --git a/src/api/utils/formatters.py b/src/api/utils/formatters.py index d1936d5a..a743d944 100644 --- a/src/api/utils/formatters.py +++ b/src/api/utils/formatters.py @@ -1,5 +1,5 @@ import json -from api.models import Utter, Intent +from api.models import Utter, Intent, Story from django.shortcuts import get_object_or_404 @@ -19,4 +19,8 @@ def story_content_formatter(content): utter = get_object_or_404(Utter, pk=element['id']) element['name'] = utter.name new_content.append(element) + elif element['type'] == 'checkpoint': + checkpoint = get_object_or_404(Story, pk=element['id']) + element['name'] = checkpoint.name + new_content.append(element) return new_content diff --git a/src/api/utils/story_validation.py b/src/api/utils/story_validation.py index b56edad2..411bddd4 100644 --- a/src/api/utils/story_validation.py +++ b/src/api/utils/story_validation.py @@ -6,6 +6,9 @@ def intent_exists(id): def utter_exists(id): return len(Utter.objects.filter(id=id)) > 0 +def checkpoint_exists(id): + return len(Story.objects.filter(id=id)) > 0 + def validate_content_keys(content): return 'id' in content and 'type' in content @@ -23,7 +26,11 @@ def validate_content(content_array): # validate utter if not utter_exists(content['id']): is_valid = False - + + elif content['type'] == 'checkpoint': + # validate checkpoint + if not checkpoint_exists(content['id']): + is_valid = False else: # invalid type is_valid = False @@ -52,4 +59,3 @@ def delete_related_stories(deleted_obj, type_str): for element in story.content: if element['type'] == type_str and element['id'] == deleted_obj.id: story.delete() - \ No newline at end of file diff --git a/src/api/views/__init__.py b/src/api/views/__init__.py index e4fbcdb2..1719e329 100644 --- a/src/api/views/__init__.py +++ b/src/api/views/__init__.py @@ -2,4 +2,4 @@ from .intent import ListIntents, ListIntentExample from .utter import ListUtters, ListUtterExample from .files import * -from .story import ListStories +from .story import ListStories, ListCheckpoints diff --git a/src/api/views/story.py b/src/api/views/story.py index a3ee703c..25e1b893 100644 --- a/src/api/views/story.py +++ b/src/api/views/story.py @@ -1,7 +1,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from django.shortcuts import get_object_or_404 -from api.models import Story, StorySerializer, Project, StoryListSerializer +from api.models import Story, StorySerializer, Project, StoryListSerializer, CheckpointSerializer, CheckpointListSerializer from api.utils import request_to_dict, validate_content, story_content_formatter, validate_story, filter_content_by_name from api.webhook import stories_delete_hook, domain_delete_hook @@ -16,13 +16,13 @@ def get(self, request, project_id=None, story_id=None, format=None): story = get_object_or_404(Story, pk=story_id) return Response(StorySerializer(story).data) - + project = get_object_or_404(Project, pk=project_id) name_filter = request.GET.get('filter') or "" stories = StoryListSerializer( - Story.objects.filter(project=project, ), + Story.objects.filter(project=project, ), many=True ).data @@ -33,19 +33,20 @@ def get(self, request, project_id=None, story_id=None, format=None): def post(self, request, project_id=None, story_id=None, format=None): if not project_id: return Response(status=404) - + data = request_to_dict(request) if not validate_story(data): return Response({'errors': ['Invalid data']}, status=400) - + project = get_object_or_404(Project, pk=project_id) if validate_content(data['content']): story = Story.objects.create( name="Default Name", content=story_content_formatter(data['content']), - project=project + project=project, + is_checkpoint=data['is_checkpoint'] ) story.name = "Diálogo_{0}_{1}".format(story.project.name, story.id) story.save() @@ -60,7 +61,7 @@ def delete(self, request, project_id=None, story_id=None, format=None): story_delete_hook(project_id) domain_delete_hook(project_id) - return Response(status=204) + return Response(status=204) def put(self, request, project_id=None, story_id=None, format=None): story = get_object_or_404(Story, pk=story_id) @@ -83,3 +84,24 @@ def put(self, request, project_id=None, story_id=None, format=None): return Response(StorySerializer(story).data) else: return Response({'errors': ['Invalid content array']}, status=400) + + +class ListCheckpoints(APIView): + + def get(self, request, project_id=None, story_id=None, format=None): + if not project_id: + return Response(status=404) + + if story_id: + story = get_object_or_404(Story, pk=story_id) + + return Response(CheckpointSerializer(story).data) + + project = get_object_or_404(Project, pk=project_id) + + checkpoints = CheckpointListSerializer( + Story.objects.filter(project=project, is_checkpoint=True), + many=True + ).data + + return Response(checkpoints) diff --git a/src/populate_models.py b/src/populate_models.py index 5a248978..5352b883 100644 --- a/src/populate_models.py +++ b/src/populate_models.py @@ -1,5 +1,6 @@ import json import os +import random from api.models import Intent, Utter, Project, Story from api.utils import story_content_formatter, validate_content @@ -30,8 +31,8 @@ name=each['fields']['name'], samples=each['fields']['samples'], project=Project.objects.all().first() - ) - + ) + for each in models_dict['api.utter']: Utter.objects.create( name=each['fields']['name'], @@ -43,13 +44,14 @@ cls_dict = {} cls_dict['intent'] = Intent cls_dict['utter'] = Utter + cls_dict['checkpoint'] = Story for each in models_dict['api.story']: contents = [] for content in each['fields']['content']: obj = cls_dict[content['type']].objects.filter(name=content['name']).first() - + if not obj: print("=========== MISSING DATA TO CREATE STORY ===========") print(content, "WAS NOT DECLARED BEFORE") @@ -61,16 +63,28 @@ 'id': obj.id } + if content['type'] == "checkpoint": + for i, element in enumerate(obj.content): + if element['type'] == 'utter': + element_obj = Utter.objects.filter(pk=element['id']).first() + obj.content[i]['example'] = random.choice(getattr(element_obj, 'alternatives')) + elif element['type'] == 'intent': + element_obj = Intent.objects.filter(pk=element['id']).first() + obj.content[i]['example'] = random.choice(getattr(element_obj, 'samples')) + + new_obj['content'] = obj.content + contents.append(new_obj) - + if validate_content(contents): story = Story.objects.create( name="Default Name", content=story_content_formatter(contents), + is_checkpoint=each['fields']['is_checkpoint'] if 'is_checkpoint' in each['fields'] else False, project=Project.objects.all().first() ) story.name = "Diálogo_{0}_{1}".format(story.project.name, story.id) story.save() else: - print("Database already contains objects. Skipping database seeding...") \ No newline at end of file + print("Database already contains objects. Skipping database seeding...")