/****************************************************************************** * * Copyright 2014 Paphus Solutions Inc. * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package org.botlibre.sense.text; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.Writer; import java.net.URL; import java.util.logging.Level; import org.botlibre.BotException; import org.botlibre.api.knowledge.Network; import org.botlibre.api.knowledge.Vertex; import org.botlibre.emotion.EmotionalState; import org.botlibre.knowledge.Primitive; import org.botlibre.self.SelfCompiler; import org.botlibre.self.SelfParseException; import org.botlibre.sense.BasicSense; import org.botlibre.thought.language.Language; import org.botlibre.thought.language.Language.LanguageState; import org.botlibre.thought.language.Language.LearningMode; import org.botlibre.util.TextStream; import org.botlibre.util.Utils; /** * Process text to and from networks and * stimulates natural language thoughts. */ public class TextEntry extends BasicSense { public static int LOG_SLEEP = 5000; /** * Keeps track of the user involved in the conversation. */ protected Long userId; /** * Keeps track of the current conversation. */ protected Long conversationId; /** * The writer is the stream, text-box or chat client to output text to. */ protected Writer writer; /** Allows contact info to be passed to sense. */ protected String info; public TextEntry() { } /** * Return the user involved in the conversation. */ public Vertex getUser(Network network) { if (this.userId == null) { return null; } return network.findById(this.userId); } /** * Set the user involved in the conversation. */ public void setUser(Vertex user) { if (user == null) { this.userId = null; } else { this.userId = user.getId(); } } public String getInfo() { return info; } public void setInfo(String info) { this.info = info; } /** * Return the writer used to output text to. */ public Writer getWriter() { return this.writer; } /** * Set the writer used to output text to. */ public void setWriter(Writer writer) { this.writer = writer; } /** * Process the text input. */ @Override public void input(Object inputText, Network network) { if (!isEnabled()) { return; } TextInput text = null; if (inputText instanceof TextInput) { text = (TextInput)inputText; } else { text = new TextInput((String)inputText); } log("Input", Level.INFO, text.text, getUser(network), getConversation(network)); inputSentence(text, network); } /** * Process the text sentence. */ public void inputSentence(TextInput text, Network network) { Vertex input = null; boolean newConversation = text.getText() == null; if (newConversation) { // Null input is used to get greeting. input = network.createInstance(Primitive.INPUT); input.addRelationship(Primitive.SENSE, getPrimitive()); input.addRelationship(Primitive.INSTANTIATION, Primitive.CHAT); input.addRelationship(Primitive.INPUT, Primitive.NULL); } else { input = createInputSentence(text.getText().trim(), network); input.addRelationship(Primitive.INSTANTIATION, Primitive.CHAT); } if (text.isCorrection()) { input.addRelationship(Primitive.ASSOCIATED, Primitive.CORRECTION); } if (text.isOffended()) { input.addRelationship(Primitive.ASSOCIATED, Primitive.OFFENDED); } input.addRelationship(Primitive.TARGET, Primitive.SELF); // Process speaker. Vertex speaker = getUser(network); if (speaker == null) { speaker = network.createSpeaker(DEFAULT_SPEAKER); speaker.addRelationship(Primitive.ASSOCIATED, Primitive.ANONYMOUS); if (this.info != null && !this.info.isEmpty()) { String name = new TextStream(this.info).nextWord(); speaker.addRelationship(Primitive.NAME, network.createName(name)); } setUser(speaker); } Language language = this.bot.mind().getThought(Language.class); boolean applyEmote = language.shouldLearn(input, speaker) || (text.isCorrection() && language.shouldCorrect(input, speaker)); input.addRelationship(Primitive.SPEAKER, speaker); speaker.addRelationship(Primitive.INPUT, input); if (this.emotionalState != null && this.emotionalState != EmotionalState.NONE) { this.emotionalState.apply(input); if (applyEmote) { this.emotionalState.apply(input.getRelationship(Primitive.INPUT)); } } if (this.action != null) { input.addRelationship(Primitive.ACTION, new Primitive(this.action)); if (applyEmote) { input.getRelationship(Primitive.INPUT).addRelationship(Primitive.ACTION, new Primitive(this.action)); } } // Process conversation. Vertex conversation = getConversation(network); if (newConversation || (conversation == null)) { conversation = network.createInstance(Primitive.CONVERSATION); conversation.addRelationship(Primitive.TYPE, Primitive.CHAT); setConversation(conversation); conversation.addRelationship(Primitive.SPEAKER, speaker); conversation.addRelationship(Primitive.SPEAKER, Primitive.SELF); if (this.info != null && !this.info.isEmpty()) { Vertex infoInput = createInputSentence("Info: " + this.info.trim(), network); infoInput.addRelationship(Primitive.INSTANTIATION, Primitive.CHAT); Language.addToConversation(infoInput, conversation); } } if (!newConversation) { Language.addToConversation(input, conversation); } else { input.addRelationship(Primitive.CONVERSATION, conversation); } network.save(); this.bot.memory().addActiveMemory(input); } /** * Output the vertex to text. */ @Override public synchronized void output(Vertex output) { if (!isEnabled()) { return; } Vertex sense = output.mostConscious(Primitive.SENSE); // If not output to Text, ignore. if (sense == null || (!getPrimitive().equals(sense.getData()))) { return; } if (getWriter() == null) { log("Missing writer", Level.WARNING); return; } try { getWriter().write(printInput(output)); } catch (Exception failed) { log(failed); } } /** * Return the current conversation. */ public Long getConversationId() { return this.conversationId; } /** * Return the current conversation. */ public Vertex getConversation(Network network) { if (this.conversationId == null) { return null; } return network.findById(this.conversationId); } public void clearConversation() { this.conversationId = null; this.userId = null; this.action = null; this.emotionalState = EmotionalState.NONE; this.info = null; } /** * Stop sensing. */ @Override public void shutdown() { super.shutdown(); clearConversation(); } /** * Reset state when instance is pooled. */ @Override public void pool() { clearConversation(); } /** * Set the current conversation. */ public void setConversation(Vertex conversation) { this.conversationId = conversation.getId(); } /** * Process the log file from a URL. */ public void loadChatFile(URL file, String format, String encoding, boolean processUnderstanding, boolean pin) { try { loadChatFile(Utils.openStream(file), format, encoding, MAX_FILE_SIZE, processUnderstanding, pin); } catch (Exception exception) { throw new BotException(exception); } } /** * Process the log file. */ public void loadChatFile(File file, String format, String encoding, boolean processUnderstanding, boolean pin) { try { loadChatFile(new FileInputStream(file), format, encoding, MAX_FILE_SIZE, processUnderstanding, pin); } catch (Exception exception) { throw new BotException(exception); } } /** * Process the log file for a chat conversation. * Input each message, in a listening only mode. */ public void loadChatFile(InputStream stream, String format, String encoding, int maxSize, boolean processUnderstanding, boolean pin) { try { String text = Utils.loadTextFile(stream, encoding, maxSize); loadChat(text, format, processUnderstanding, pin); } catch (BotException exception) { throw exception; } catch (Exception exception) { throw new BotException(exception); } } /** * Process the log file for a chat conversation. * Input each message, in a listening only mode. */ public void loadChat(String text, String format, boolean processUnderstanding, boolean pin) { try { if ("Response List".equalsIgnoreCase(format)) { processResponseLog(text, pin); } else if ("Chat Log".equalsIgnoreCase(format)) { processChatLog(text, processUnderstanding, pin); } else if ("CSV List".equalsIgnoreCase(format)) { processCSVLog(text, pin); } else { throw new BotException("Invalid chat log format '" + format + "'"); } } catch (BotException exception) { throw exception; } catch (Exception exception) { throw new BotException(exception); } } /** * Process the log file for a chat conversation. * Input each message, in a listening only mode. */ public void processChatLog(String log, boolean comprehension, boolean pin) { log("Loading chat log", Level.INFO, log.length()); TextStream stream = new TextStream(log); Vertex lastSpeaker = null; LanguageState oldState = getLanguageState(); Language languageThought = this.bot.mind().getThought(Language.class); LearningMode oldMode = languageThought.getLearningMode(); if (comprehension) { setLanguageState(LanguageState.ListeningOnly); if (languageThought.getLearningMode() == LearningMode.Disabled) { languageThought.setLearningMode(LearningMode.Everyone); } } Vertex conversation = null; Vertex question = null; Vertex previous = null; boolean wasAdmin = false; boolean first = true; boolean cycle = false; int count = 0; try { while (!stream.atEnd()) { count++; int marker = stream.getPosition(); String line = stream.nextLine().trim(); if (first && line.indexOf("<?xml") != -1) { throw new SelfParseException("Chat log format must be text, not XML", stream); } first = false; boolean cr = false; // Skip blank lines. while (line.length() == 0) { if (stream.atEnd()) { return; } cr = true; marker = stream.getPosition(); line = stream.nextLine().trim(); } Vertex input = null; log("Processing chat log", Level.INFO, count, line); for (int index = 0; index < RETRY; index++) { Network network = this.bot.memory().newMemory(); try { if (comprehension) { // CR means new conversation. if (cr || (conversation == null)) { conversation = network.createInstance(Primitive.CONVERSATION); conversation.addRelationship(Primitive.TYPE, Primitive.CHAT); lastSpeaker = null; } else { conversation = network.createVertex(conversation); } } if (cr) { question = null; previous = null; } TextStream lineStream = new TextStream(line); String speakerName = lineStream.upTo(':'); if (lineStream.atEnd()) { lineStream.reset(); if (cycle) { speakerName = "self"; } else { speakerName = "anonymous"; } cycle = !cycle; } else { lineStream.skip(); } char peek = lineStream.peek(); EmotionalState state = EmotionalState.NONE; if (peek == '<') { lineStream.skip(); String emotion = lineStream.upTo('>'); if (lineStream.atEnd()) { stream.setPosition(marker); throw new SelfParseException("Expected '<emotion>' in chat text", stream); } lineStream.skip(); try { state = EmotionalState.valueOf(emotion.toUpperCase()); } catch (Exception exception) { stream.setPosition(marker); throw new SelfParseException("Invalid '<emotion>' in chat text", stream); } } setEmotionalState(state); String message = lineStream.upToEnd().trim(); if (speakerName.equalsIgnoreCase("default")) { Vertex language = network.createVertex(Language.class); input = network.createSentence(message); input.setPinned(true); language.addRelationship(Primitive.RESPONSE, input); } else if (speakerName.equalsIgnoreCase("greeting")) { Vertex language = network.createVertex(Language.class); input = network.createSentence(message); input.setPinned(true); language.addRelationship(Primitive.GREETING, input); } else if (speakerName.equalsIgnoreCase("script")) { input = SelfCompiler.getCompiler().evaluateExpression( message, network.createVertex(Primitive.SELF), network.createVertex(Primitive.SELF), false, network); } else { if (comprehension) { input = createInputSentence(message, network); input.addRelationship(Primitive.INSTANTIATION, Primitive.CHAT); if (this.emotionalState != EmotionalState.NONE) { this.emotionalState.apply(input); this.emotionalState.apply(input.getRelationship(Primitive.INPUT)); } if (lastSpeaker != null) { lastSpeaker = network.createVertex(lastSpeaker); input.addRelationship(Primitive.TARGET, lastSpeaker); } Vertex speaker = null; if (speakerName.toLowerCase().equals("self")) { speaker = network.createVertex(Primitive.SELF); wasAdmin = true; } else { speaker = network.createSpeaker(speakerName); if (speakerName.equals("admin")) { speaker.addRelationship(Primitive.ASSOCIATED, Primitive.ADMINISTRATOR); wasAdmin = true; } } input.addRelationship(Primitive.SPEAKER, speaker); speaker.addRelationship(Primitive.INPUT, input); conversation.addRelationship(Primitive.SPEAKER, speaker); Language.addToConversation(input, conversation); lastSpeaker = speaker; } Vertex sentence = network.createSentence(message); if (pin) { sentence.setPinned(true); } if (question != null) { question = network.createVertex(question); if (pin) { question.setPinned(true); } Language.addResponse(question, sentence, null, null, null, languageThought.getLearningRate(), network); if (previous != null) { previous = network.createVertex(previous); Language.addSentencePreviousMeta(question, sentence, previous, false, network); } } previous = question; question = sentence; } network.save(); break; } catch (SelfParseException failed) { failed.printStackTrace(); log(failed); network.clear(); throw failed; } catch (Exception failed) { failed.printStackTrace(); log(failed); network.clear(); } } if (comprehension) { if (input == null) { return; } this.bot.memory().addActiveMemory(input); int abort = 0; while ((abort < 20) && !input.hasRelationship(Primitive.CONTEXT)) { Utils.sleep(100); Network memory = this.bot.memory().newMemory(); input = memory.createVertex(input); abort++; } if (count == 100) { log("Chat log import exceeds 100 lines, offloading to background task", Level.WARNING); } if (abort >= 20 || count > 100) { Utils.sleep(LOG_SLEEP); } } } if (languageThought.getLearningMode() == LearningMode.Administrators && !wasAdmin) { throw new SelfParseException("Expected 'admin:' speaker in chat text when learning mode is administrators only", stream); } } finally { setLanguageState(oldState); languageThought.setLearningMode(oldMode); } } /** * Process the log file for a list of question/answers. * Input each message, in a listening only mode. */ public void processResponseLog(String log, boolean pin) { log("Loading response log", Level.INFO, log.length()); TextStream stream = new TextStream(log); Network network = this.bot.memory().newMemory(); Vertex question = null; Vertex answer = null; boolean first = true; boolean isDefault = false; while (!stream.atEnd()) { String line = stream.nextLine().trim(); if (first && line.indexOf("<?xml") != -1) { throw new SelfParseException("Chat log format must be text, not XML", stream); } first = false; String originalLine = line; // Skip blank lines. while (line.isEmpty()) { if (stream.atEnd()) { return; } question = null; answer = null; line = stream.nextLine().trim(); originalLine = line; if (!line.isEmpty()) { network = this.bot.memory().newMemory(); } } log("Processing response log", Level.INFO, line); TextStream lineStream = new TextStream(line); String command = lineStream.upTo(':'); if (!lineStream.atEnd()) { lineStream.skip(); line = lineStream.upToEnd().trim(); } else { command = ""; } if (command.equalsIgnoreCase("default")) { isDefault = true; Vertex language = network.createVertex(Language.class); question = network.createSentence(line); question.setPinned(true); language.addRelationship(Primitive.RESPONSE, question); } else if (command.equalsIgnoreCase("greeting")) { Vertex language = network.createVertex(Language.class); question = network.createSentence(line); question.setPinned(true); language.addRelationship(Primitive.GREETING, question); } else if (command.equalsIgnoreCase("script")) { SelfCompiler.getCompiler().evaluateExpression( line, network.createVertex(Primitive.SELF), network.createVertex(Primitive.SELF), false, network); } else if (command.equalsIgnoreCase("keywords")) { if (question == null || answer == null) { throw new BotException("Missing question and response for keywords"); } Language.addSentenceKeyWordsMeta(question, answer, line, network); } else if (command.equalsIgnoreCase("required")) { if (question == null || answer == null) { throw new BotException("Missing question and response for required words"); } Language.addSentenceRequiredMeta(question, answer, line, network); } else if (command.equalsIgnoreCase("emotions")) { if (question == null) { throw new BotException("Missing phrase for emotions"); } if (answer == null) { question.internalRemoveRelationships(Primitive.EMOTION); for (String emote : Utils.getWords(line)) { if (!emote.equals("none")) { try { EmotionalState.valueOf(emote.toUpperCase()).apply(question); } catch (Exception exception) { throw new BotException("Invalid emotion: " + emote); } } } } else { Language.addSentenceEmotesMeta(question, answer, line, network); } } else if (command.equalsIgnoreCase("actions")) { if (question == null) { throw new BotException("Missing phrase for actions"); } if (answer == null) { question.internalRemoveRelationships(Primitive.ACTION); for (String action : Utils.getWords(line)) { if (!action.equals("none")) { question.addRelationship(Primitive.ACTION, new Primitive(action)); } } } else { Language.addSentenceActionMeta(question, answer, line, network); } } else if (command.equalsIgnoreCase("poses")) { if (question == null) { throw new BotException("Missing phrase for poses"); } if (answer == null) { question.internalRemoveRelationships(Primitive.POSE); for (String pose : Utils.getWords(line)) { if (!pose.equals("none")) { question.addRelationship(Primitive.POSE, new Primitive(pose)); } } } else { Language.addSentencePoseMeta(question, answer, line, network); } } else if (command.equalsIgnoreCase("previous")) { if (question == null || (answer == null && !isDefault)) { throw new BotException("Missing question and response for previous"); } Vertex previous = null; if (line.startsWith("#")) { line = line.substring(1, line.length()); if (!Utils.isAlphaNumeric(line)) { throw new BotException("A label must be a single alpha numeric string with no spaces (use - for a space) - " + line); } previous = network.createVertex(new Primitive(line)); } else { previous = network.createSentence(line); } if (pin) { previous.setPinned(true); } if (isDefault) { Vertex language = network.createVertex(Language.class); Language.addSentencePreviousMeta(language, question, previous, false, network); } else { Language.addSentencePreviousMeta(question, answer, previous, false, network); } } else if (command.equalsIgnoreCase("require previous")) { if (question == null || (answer == null && !isDefault)) { throw new BotException("Missing question and response for previous"); } Vertex previous = null; if (line.startsWith("#")) { line = line.substring(1, line.length()); if (!Utils.isAlphaNumeric(line)) { throw new BotException("A label must be a single alpha numeric string with no spaces (use - for a space) - " + line); } previous = network.createVertex(new Primitive(line)); } else { previous = network.createSentence(line); } if (pin) { previous.setPinned(true); } if (isDefault) { Vertex language = network.createVertex(Language.class); Language.addSentencePreviousMeta(language, question, previous, true, network); } else { Language.addSentencePreviousMeta(question, answer, previous, true, network); } } else if (command.equalsIgnoreCase("label")) { if (question == null) { throw new BotException("Missing phrase for label"); } if (line.startsWith("#")) { line = line.substring(1, line.length()); } if (!Utils.isAlphaNumeric(line)) { throw new BotException("A label must be a single alpha numeric string with no spaces (use - for a space) - " + line); } Vertex label = network.createVertex(new Primitive(line)); if (pin) { label.setPinned(true); } label.addRelationship(Primitive.INSTANTIATION, Primitive.LABEL); if (answer == null) { question.setRelationship(Primitive.LABEL, label); label.setRelationship(Primitive.RESPONSE, question); } else { answer.setRelationship(Primitive.LABEL, label); label.setRelationship(Primitive.RESPONSE, answer); } } else if (command.equalsIgnoreCase("on repeat")) { if (question == null) { throw new BotException("Missing question for on repeat"); } Vertex repeat = network.createSentence(line); if (pin) { repeat.setPinned(true); } if (answer == null) { question.addRelationship(Primitive.ONREPEAT, repeat); } else { answer.addRelationship(Primitive.ONREPEAT, repeat); } } else if (command.equalsIgnoreCase("no repeat")) { if (question == null) { throw new BotException("Missing question for no repeat"); } if (answer == null) { question.addRelationship(Primitive.REQUIRE, Primitive.NOREPEAT); } else { answer.addRelationship(Primitive.REQUIRE, Primitive.NOREPEAT); } } else if (command.equalsIgnoreCase("topic")) { if (question == null) { throw new BotException("Missing phrase for topic"); } if (answer == null) { if (isDefault) { Vertex language = network.createVertex(Language.class); Language.addSentenceTopicMeta(language, question, line, network); } else { Vertex topicFragment = network.createFragment(line); topicFragment.addRelationship(Primitive.INSTANTIATION, Primitive.TOPIC); network.createVertex(Primitive.TOPIC).addRelationship(Primitive.INSTANCE, topicFragment); topicFragment.addRelationship(Primitive.QUESTION, question); question.setRelationship(Primitive.TOPIC, topicFragment); } } else { Language.addSentenceTopicMeta(question, answer, line, network); } } else if (command.equalsIgnoreCase("command")) { if (question == null) { throw new BotException("Missing phrase for command"); } if (answer == null) { if (isDefault) { Vertex language = network.createVertex(Language.class); Language.addSentenceCommandMeta(language, question, line, network); } } else { Language.addSentenceCommandMeta(question, answer, line, network); } } else if (command.equalsIgnoreCase("think")) { if (question == null) { throw new BotException("Missing phrase for think"); } if (answer == null) { if (isDefault) { Vertex language = network.createVertex(Language.class); Language.addSentenceThinkMeta(language, question, line, network); } } else { Language.addSentenceThinkMeta(question, answer, line, network); } } else if (command.equalsIgnoreCase("condition")) { if (question == null) { throw new BotException("Missing phrase for condition"); } if (answer == null) { if (isDefault) { Vertex language = network.createVertex(Language.class); Language.addSentenceConditionMeta(language, question, line, network); } } else { Language.addSentenceConditionMeta(question, answer, line, network); } } else { isDefault = false; Vertex sentence = null; if (originalLine.startsWith("#")) { originalLine = originalLine.substring(1, originalLine.length()); if (!Utils.isAlphaNumeric(originalLine)) { throw new BotException("A label must be a single alpha numeric string with no spaces (use - for a space) - " + originalLine); } sentence = network.createVertex(new Primitive(originalLine)); if (!sentence.hasRelationship(Primitive.INSTANTIATION, Primitive.LABEL)) { log("Missing label", Level.INFO, originalLine); } } else { sentence = network.createSentence(originalLine); } if (pin) { sentence.setPinned(true); } if (question == null) { question = sentence; } else { answer = sentence; Language.addResponse(question, answer, null, null, null, 0.9f, network); } } network.save(); } network.save(); } /** * Process the log file for list a of question/answers. * Input each message, in a listening only mode. */ public void processCSVLog(String log, boolean pin) { log("Loading csv log", Level.INFO, log.length()); TextStream stream = new TextStream(log); Network network = this.bot.memory().newMemory(); Vertex question = null; Vertex answer = null; boolean first = true; while (!stream.atEnd()) { String line = stream.nextLine().trim(); if (first && line.indexOf("<?xml") != -1) { throw new SelfParseException("Chat log format must be text, not XML", stream); } first = false; // Skip blank lines. while (line.isEmpty()) { if (stream.atEnd()) { return; } question = null; answer = null; line = stream.nextLine().trim(); if (!line.isEmpty()) { network = this.bot.memory().newMemory(); } } // Allow either ',' or '","' separators. boolean quotes = line.contains("\""); log("Processing csv line", Level.INFO, line); // "questions","answer","topic" // "What is this? What's this?","This is Open Bot.","Bot" TextStream lineStream = new TextStream(line); if (quotes) { lineStream.skipTo('"'); lineStream.skip(); } if (lineStream.atEnd()) { log("Expecting \" character", Level.WARNING, line); continue; } String questionText = null; if (quotes) { questionText = lineStream.upToAll("\",\"").trim(); lineStream.skip("\",\"".length()); } else { questionText = lineStream.upTo(',').trim(); lineStream.skip(); } if (lineStream.atEnd()) { log("Expecting \",\" characters", Level.WARNING, line); continue; } log("Processing csv question", Level.INFO, questionText); String answerText = null; if (quotes) { answerText = lineStream.upToAll("\"").trim(); } else { answerText = lineStream.upTo(',').trim(); } lineStream.skip(); log("Processing csv answer", Level.INFO, answerText); answer = network.createSentence(answerText); if (pin) { answer.setPinned(true); } // Topic String topic = ""; if (quotes) { if (lineStream.peek() != ',') { lineStream.skipTo('"', true); topic = lineStream.upTo('"').trim(); } } else { topic = lineStream.upTo(',').trim(); } lineStream.skip(); // Keywords String keywords = ""; if (quotes) { if (lineStream.peek() != ',') { lineStream.skipTo('"', true); keywords = lineStream.upTo('"').trim(); } } else { keywords = lineStream.upTo(',').trim(); } lineStream.skip(); // Required String required = ""; if (quotes) { if (lineStream.peek() != ',') { lineStream.skipTo('"', true); required = lineStream.upTo('"').trim(); } } else { required = lineStream.upTo(',').trim(); } TextStream questionStream = new TextStream(questionText); while (!questionStream.atEnd()) { questionText = questionStream.upTo('?', true).trim(); if (!questionText.isEmpty() && !questionText.equals("?")) { question = network.createSentence(questionText); if (pin) { question.setPinned(true); } Language.addResponse(question, answer, topic, keywords, required, 0.9f, network); } } network.save(); } network.save(); } }