diff --git a/Example.md b/Example.md index 2bda8d2..d31e274 100644 --- a/Example.md +++ b/Example.md @@ -1,3 +1,86 @@ # Beispiel: Rätsel & Agent -TODO \ No newline at end of file +Der Beispielagent kann Rechenaufgaben lösen, dabei ist das Rätsel eine einfache Aufgabe mit Variablen und in den Daten (Textdateien) werden den Variablen Werte zugewiesen. + +> Der Beispielagent ist nicht sonderlich *schlau*, sondern funktioniert nur Rätseln, die genau einen bestimmten Aufbau haben. + +## Beispiel Rätsel (`AgentMessage`) +- Daten (als Dateien in `./data/share`) + - `./example/x.txt`: `x = 10` + - `./example/y.txt`: `y = 15` + - `./example/z.txt`: `z = 20` +- Rätsel z.B. + - Kontext: "[Taschenrechner]" + - Frage: "x * y =" +- Eingabe unter möglich + - Für die Dateien ist eine Liste vorhandener Dateien hinterlegt, sodass Eingaben vervollständigt werden. + - Es wird eine JSON-Vorschau angezeigt und das Rätsel kann an das Management geschickt werden +- Beispiel für JSON Darstellung [↓](#json-darstellung) +- Nachrichten könnten unter angesehen werden + - Nachricht mit `Solution=True` sollte dann die Lösung beinhalten + +## Ablauf der Bestimmung der Lösung +1. Nachricht mit Rätsel an Management senden +2. Management wertet Nachricht und sendet Nachricht an alle ihm bekannten Extract-Agenten +3. Extract-Agenten extrahieren Daten und senden Ergebnis zurück an Management (`status.extract.finished == True`) +4. Management senden Nachricht (mit Extraktionen) an alle ihm bekannten Solve-Agenten +5. Solve-Agenten versuchen das Rätsel zu lösen + - Können Sub-Rätsel erstellen () + - Bekommen ein Rätsel evtl. mehrfach (jeweils wenn ein Extract-Agent) fertig geworden ist. (Evtl. Lösen mit Teilen der Infos schon möglich oder auch nicht.) + - Können Antworten an das Management stoppen () +6. Management bekommt Ergebnis von Solve-Agenten und sendet dies an Gatekeeper-Agenten +7. Gatekeeper-Agent prüft Lösung und akzeptiert oder lehnt diese ab. Sendet Entscheidung an Management. +8. Management prüft, ob Lösung angenommen. Falls Lösung nicht akzeptiert, so wird Rätsel erneut gestellt (maximal `SOLUTION_MAX_TRIALS` mal) + +## Agenten Quellcode +1. Extract [`./src/extract/agent.py`](./src/extract/agent.py) +2. Solve [`./src/solve/agent.py`](./src/solve/agent.py) +3. Validate [`./src/validate/agent.py`](./src/validate/agent.py) + +### JSON-Darstellung +```json +{ + "id": "ex1", + "sub_ids": [], + "riddle": { + "context": "[Taschenrechner]", + "question": "x * y =", + "solutions_before": [] + }, + "solution": null, + "data": [ + { + "type": "text", + "file_plain": "example/x.txt", + "file_extracted": null + }, + { + "type": "text", + "file_plain": "example/y.txt", + "file_extracted": null + }, + { + "type": "text", + "file_plain": "example/z.txt", + "file_extracted": null + } + ], + "status": { + "extract": { + "required": true, + "finished": false + }, + "solve": { + "required": true, + "finished": false + }, + "validate": { + "required": true, + "finished": false + }, + "trial": 0, + "solved": false + }, + "contacts": 0 +} +``` \ No newline at end of file diff --git a/data/share/example/x.txt b/data/share/example/x.txt new file mode 100644 index 0000000..574657a --- /dev/null +++ b/data/share/example/x.txt @@ -0,0 +1,3 @@ +hello hello hello +x = 10 +bye bye \ No newline at end of file diff --git a/data/share/example/y.txt b/data/share/example/y.txt new file mode 100644 index 0000000..9325c4e --- /dev/null +++ b/data/share/example/y.txt @@ -0,0 +1,2 @@ +y = 15 +huhu huhu huhu \ No newline at end of file diff --git a/data/share/example/z.txt b/data/share/example/z.txt new file mode 100644 index 0000000..3cb06ff --- /dev/null +++ b/data/share/example/z.txt @@ -0,0 +1,2 @@ +z = 20 +blubb \ No newline at end of file diff --git a/src/extract/agent.py b/src/extract/agent.py index 3c36ba2..77b3149 100644 --- a/src/extract/agent.py +++ b/src/extract/agent.py @@ -8,32 +8,71 @@ # source code released under the terms of GNU Public License Version 3 # https://www.gnu.org/licenses/gpl-3.0.txt +import re + from typing import Callable from ums.agent import ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent from ums.utils.types import RiddleData, AgentMessage - +from ums.utils.schema import ExtractedData, ExtractedContent, ExtractedPositions class SimpleExtractAudioAgent(ExtractAudioAgent): + # here we do not have an implementation for extracting audio, + # normally, we would not have this class, but here as example def handle(self, data: RiddleData) -> RiddleData: print("Audio Process:", data.file_plain) return data class SimpleExtractImageAgent(ExtractImageAgent): + # equally, we would not have this class without implementation + + def before_response(self, response: AgentMessage, send_it: Callable[[], None]) -> bool: + # agents are able to prevent sending response messages to the management + # or send this messages later via `send_it()`` + + print("The response would be:", response) + + # just stop the response from being sent + return False def handle(self, data: RiddleData) -> RiddleData: print("Image Process:", data.file_plain) return data class SimpleExtractTextAgent(ExtractTextAgent): - - def before_response(self, response: AgentMessage, send_it: Callable[[], None]) -> bool: - print("The response will be:", response) - return True - + def handle(self, data: RiddleData) -> RiddleData: print("Text Process:", data.file_plain) + + # here we extract the variables assigned with numbers + found = False + with open(data.file_plain) as f: + for i, line in enumerate(f): + if "=" in line: + match = re.match(r"([a-z]{1})\s*=\s*(\d+)", line.strip()) + if not match is None: + variable = match.group(1) + value = int(match.group(2)) + found = True + line_no = i + if found: + extracted = ExtractedData( + contents=[ + ExtractedContent(type="variable",content=variable), + ExtractedContent(type="value",content=value) + ], + positions=[ + ExtractedPositions(type="line",position=line_no), + ExtractedPositions(type="line",position=line_no) + ], + other={ + "variable" : variable, + "value" : value + } + ) + data.file_extracted = self.store_extracted(data, extracted) + return data AGENT_CLASSES = [ diff --git a/src/solve/agent.py b/src/solve/agent.py index 716e1b9..c03e0e4 100644 --- a/src/solve/agent.py +++ b/src/solve/agent.py @@ -9,20 +9,91 @@ # https://www.gnu.org/licenses/gpl-3.0.txt -from ums.agent import SolveAgent +import re, random -from ums.utils.types import Riddle, RiddleData, RiddleSolution, RiddleStatus +from typing import Callable + +from ums.agent import SolveAgent +from ums.utils.types import Riddle, RiddleData, RiddleSolution, RiddleDataType, RiddleStatus, AgentMessage class SimpleSolveAgent(SolveAgent): - def handle(self, riddle: Riddle, data: RiddleData) -> RiddleSolution: - - if self.message().id == "test": - status = RiddleStatus() - status.extract.required = False - self.sub_riddle(riddle=Riddle(context="Haha", question="Blubber"), status=status) + def before_response(self, response: AgentMessage, send_it: Callable[[], None]) -> bool: + # do not send a response, if this is not a calculator riddle! + return not self.stop_response - return RiddleSolution(solution="Huii", explanation="Blubb") + def handle(self, riddle: Riddle, data: RiddleData) -> RiddleSolution: + # remove whitespace + expression = riddle.question.strip() + + # this is a very simple calculator, if the riddle it not for the calculator + # just do not try to solve it and do not answer management! + if "[Taschenrechner]" in riddle.context: + self.stop_response = False + + # get all the extracted values + var_vals = {} + used_data = [] + for d in data: + e = self.get_extracted(d) + if not e is None \ + and "variable" in e.other and "value" in e.other \ + and e.other["variable"] in expression: + used_data.append(d) + var_vals[e.other["variable"]] = e.other["value"] + + # require "=" at the end + if not expression[-1] == "=": + return RiddleSolution(solution="Error", explanation="No = at the end of the expression!") + + # solve the expression + # remove the = and whitespace + expression = expression[:-1].strip() + + for var, val in var_vals.items(): + # replace the variables by values + expression = expression.replace(var, str(val)) + + # check the expression + if re.match(r"^[0-9+\-*\/ ]+$", expression) is None: + return RiddleSolution(solution="Error", explanation="Missing data or faulty expression") + + try: + # using eval is a bad idea, but this is only for demonstration + # and expression may only contain "0-9 +-*/" + result = eval(expression) + except: + return RiddleSolution(solution="Error", explanation="Unable to calculate value of expression") + + # add some noise and invalidate result (for gatekeeper to check) + if random.random() > 0.5: + print("UPPS UPPS") + result += 1 + int(random.random()*9) + + return RiddleSolution( + solution=str(result), + explanation="{} = {}".format(expression, result), + used_data=used_data + ) + + else: + self.stop_response = True + + # but we will start a nice riddle, we can solve :D + self.sub_riddle( + riddle=Riddle( + context="[Taschenrechner]", + question="x * x =" + ), + data=[ + RiddleData( + type=RiddleDataType.TEXT, + file_plain="./example/x.txt" + ) + ] + ) + + return RiddleSolution(solution="Error", explanation="No context [Taschenrechner]!") AGENT_CLASSES = [ diff --git a/src/validate/agent.py b/src/validate/agent.py index 781878d..ee9ffb0 100644 --- a/src/validate/agent.py +++ b/src/validate/agent.py @@ -8,17 +8,58 @@ # source code released under the terms of GNU Public License Version 3 # https://www.gnu.org/licenses/gpl-3.0.txt +import re +from typing import Callable from ums.agent import GatekeeperAgent - -from ums.utils.types import Riddle, RiddleSolution +from ums.utils.types import Riddle, RiddleSolution, AgentMessage class SimpleSolveAgent(GatekeeperAgent): + def before_response(self, response: AgentMessage, send_it: Callable[[], None]) -> bool: + # do not send a response, if this is not a calculator riddle! + return not self.stop_response + def handle(self, solution: RiddleSolution, riddle: Riddle) -> RiddleSolution: - solution.accepted = True - solution.review = "Ok" + self.stop_response = False + + # first check for errors + if solution.solution == "Error": + solution.accepted = True + solution.review = "An error of the riddle can not be fixed!" + return solution + + # this is just a simple check, we check if solution and explanation match to the expression + + match = re.match(r"^([0-9+\-*\/ ]+)\s*=\s*(\d+)$", solution.explanation) + if match is None: + self.stop_response = True + return solution + + expression, result = match.group(1), match.group(2) + + if result != solution.solution: + solution.accepted = False + solution.review = "Inconsistent values" + return solution + + try: + # using eval is a bad idea, but this is only for demonstration + # and expression may only contain "0-9 +-*/" + own_result = eval(expression) + except: + solution.accepted = False + solution.review = "Unsolvable expression" + return solution + + # check the values + if str(own_result) != solution.solution: + solution.accepted = False + solution.review = "Value of expression does not match solution!" + else: + solution.accepted = True + solution.review = "Yes, {}".format(solution.explanation) return solution